Skip to content
New: Customize your theme with an AI assistant. Read the guide

Pricing & The Estimator

Files: src/content/pricing/english/-index.md, src/content/pricing/french/-index.md, src/config/config.toml ([settings.estimator]) · Skill level: Beginner

Homio estimator on the pricing page

Every price in Homio comes from one file per language: src/content/pricing/english/-index.md (and the French copy). The estimator, the plans, the add-ons, the trade rate sheet, the booking form total, the “from” prices on service and area pages, and the prices in structured data all read it. Change a rate there and every page that shows it updates on the next build. You never type the same price twice.

What Is In The Pricing File

The top of the file is the pricing page (title, page header, jump links). Under the line PRICE DATA come four blocks:

BlockWhat it holds
roundingThe step that discounted prices round to (5 rounds to the nearest 5)
frequenciesHow often a visit repeats (weekly, every 2 weeks, monthly, one-time) and the discount for each
rateCardsOne card per bookable service: how it is priced, its limits, and its minimum
addOnsExtras with a fixed price, such as “Inside the oven”

The page sections (plansSection, estimateSection, addOnsSection, ratesSection, includedSection, questionsSection, ctaSection) come after the data. They set titles and layouts only. Their prices are computed from the data above.

Frequencies And Discounts

rounding: 5 # Discounted service prices round to the nearest 5. Use 1 to keep exact amounts
frequencies: # Discount applies to the service line only, never to add-ons
- id: "weekly" # used in /book/?frequency=
label: "Weekly"
discount: 20 # percent, sample
- id: "biweekly"
label: "Every 2 weeks"
discount: 15 # sample
default: true # preselected everywhere
- id: "monthly"
label: "Monthly"
discount: 10 # sample
- id: "once" # keep the id `once` for the one-visit option
label: "One-time"
discount: 0
  • discount is a percent from 0 to 100.
  • default: true marks the frequency that is selected first. Put it on one frequency only.
  • Keep the id once for the one-visit option. The ticket then says “one visit” instead of “per visit”.
  • Change the label freely. Keep the id values: they appear in booking links (/book/?frequency=biweekly).

Rate Cards

Each rate card is one service you can price online. Its id must match the file name of the service page in src/content/services/english/ (standard-cleaning for standard-cleaning.md). That link gives the service page its “from” price and its estimator. A service without a rate card shows “Price on request”.

A card uses one of three pricing modes:

modeThe visitor setsPrice of the service line
roomsBedrooms and bathroomsbase + bedrooms × perBedroom + bathrooms × perBathroom
hourlyHourscallOutFee + the larger of (hours, hours.min) × hourlyRate
unitsA count: panes, rooms, items, sq ft, storeysbase + units × perUnit

A rooms card, from the shipped file:

rateCards:
- id: "standard-cleaning"
label: "Standard clean"
trade: "cleaning" # trade id from src/content/services/{lang}/-index.md `trades:`
mode: "rooms" # rooms | hourly | units
base: 70 # sample
perBedroom: 20 # sample
perBathroom: 25 # sample
minimum: 110 # sample; smallest total for one visit
recurring: true # frequency discounts apply
frequencies: [] # ids allowed for this card; empty = all
bedrooms: { min: 0, max: 6, default: 2 } # 0 reads "Studio"
bathrooms: { min: 1, max: 5, default: 1 }
duration: { base: 1, perBedroom: 0.5, perBathroom: 0.5 } # hours, optional
note: "Supplies and equipment included."
largerNote: "More than 6 bedrooms? Call {phone} for a walkthrough price."

An hourly card and a units card:

- id: "handyman"
label: "Handyman"
trade: "repairs"
mode: "hourly"
hourlyRate: 75 # sample
callOutFee: 0 # sample; added once per visit
hours: { min: 2, max: 8, step: 0.5, default: 2 }
recurring: false
note: "Parts are billed at cost with receipts."
- id: "window-cleaning"
label: "Windows"
trade: "windows"
mode: "units"
unit: { one: "pane", other: "panes" } # plural forms for this language
base: 40 # sample
perUnit: 6 # sample
minimum: 90 # sample
units: { min: 5, max: 80, step: 1, default: 20 }
recurring: true
frequencies: ["monthly", "once"]
KeyWhat it does
idThe service id. Same as the service file name, and the value in /book/?service=
labelThe service name on tickets, chips, and the booking form
tradeThe trade it belongs to (cleaning, repairs, windows, carpets, outdoor). Groups the trade rate sheet and the add-ons
moderooms, hourly, or units (see above)
base, perBedroom, perBathroomPrices for rooms cards. base also works on units cards
hourlyRate, callOutFeePrices for hourly cards. callOutFee is added once per visit
perUnit, unitPrice per unit and the unit name in singular (one) and plural (other)
bedrooms, bathrooms, hours, unitsThe limits of each number field: min, max, step (optional), and the starting value default
minimumThe smallest total for one visit, add-ons included
recurringtrue lets frequency discounts apply. false (or left out) prices a one-off service with no frequency choice
frequenciesThe frequency ids this card offers. Empty means all of them
durationOptional time on site: base, plus perBedroom and perBathroom (rooms) or perUnit (units). Shown as “About 2.5 h”
noteA short line on the ticket, such as “Supplies and equipment included.”
largerNoteShown when the visitor reaches the largest size. {phone} becomes your business phone

When a card leaves out a limit, the theme uses these fallbacks: bedrooms 0 to 6 (start 2), bathrooms 1 to 5 (start 1), hours 1 to 8 in steps of 0.5 (start 2), units 1 to 100 (start 1).

Add-ons

addOns:
- {
id: "inside-oven",
label: "Inside the oven",
description: "Racks, glass, and walls degreased.",
price: 35,
services: ["standard-cleaning", "deep-cleaning"],
task: "inside-oven",
} # sample
- {
id: "sofa",
label: "Sofa, 3 seats",
description: "Fabric only, dry in about 6 hours.",
price: 95,
services: ["carpet-cleaning"],
} # sample
KeyWhat it does
priceA flat price. Frequency discounts never apply to add-ons
servicesThe rate-card ids it can be added to. Empty means every service
taskOptional task id from the “what’s included” list. The checklist then shows the add-on price on that task (see Services & What’s Included)
descriptionOptional short detail, shown in the add-ons section

How The Estimate Is Calculated

The formula is in src/lib/pricing.ts. The estimator, the booking summary, the plans, and the “from” prices all call it, so two pages never show different prices for the same job.

  1. Service line. Work out the line for the card’s mode (the table under Rate Cards). A number outside the card’s limits is moved to the nearest allowed value. The result is rounded to a whole amount.
  2. Frequency discount. If the card is recurring and the visitor picked a frequency with a discount, take the percent off the service line, then round to the nearest rounding (5 by default). The difference shows as its own discount line.
  3. Add-ons. Add the price of each selected add-on. Add-ons are never discounted.
  4. Minimum. If the total so far is below the card’s minimum, the total is raised to the minimum and the ticket shows a “Minimum” line with the difference.
  5. Time on site. If the card has a duration, it is worked out the same way and rounded to the nearest half hour. Hourly cards show the hours chosen.

Worked examples with the shipped sample rates:

  • Standard clean, 2 bedrooms, 1 bathroom, every 2 weeks: 70 + 2 × 20 + 1 × 25 = $135. 15% off is 114.75, rounded to the nearest 5 = $115. The ticket shows a ”−$20” discount line and “About 2.5 h”.
  • Same visit with “Inside the oven”: $115 + $35 = $150.
  • Studio (0 bedrooms), 1 bathroom, weekly: 70 + 25 = $95, 20% off = $75. That is below the $110 minimum, so the total is $110 with a ”+$35” minimum line.
  • Handyman, 2 hours: 0 + 2 × 75 = $150.
  • Windows, 20 panes, monthly: 40 + 20 × 6 = $160, 10% off = 144, rounded = $145.

“From” prices (services list, booking tiles, area pages, structured data) use the smallest allowed size, one visit, and no add-ons, then apply the minimum. The Standard clean is 70 + 0 × 20 + 1 × 25 = $95, raised to its $110 minimum, so it shows “from $110”. Hourly cards show the hourly rate (“$75 / h”).

Check any change by hand with these steps, then open /pricing/ and compare the ticket.

Where Prices Appear

PlaceWhat it showsWhere you set it
/pricing/Plans, the estimator with its price breakdown, add-ons, the trade rate sheet, the cleaning comparison, and the payment questionsThe page blocks in the pricing file
Cleaning service pagesThe estimator in the hero, locked to that serviceoptions.layout: "cleaning" in the service file
Service pages on the default layoutAn estimate section locked to that service, with the breakdownestimateSection in the detail: block of src/content/services/english/-index.md
/services/“from $110 / visit” on each row, and the lowest price in the page headerComputed from the rate cards
/book/The “from” price on each service tile and the live totalComputed from the rate cards
/home-cleaning/The recurring-discount plans (savings layout) and the first booking stepplansSection and bookingSection in src/content/homepage/english/home-cleaning.md
Area pages“Standard clean from $110” on the area ticketThe first rate card the area offers

The page sections in the pricing file:

estimateSection: # defaults: estimate-section.md
title: "Price your home"
services: [
"standard-cleaning",
"deep-cleaning",
"move-out-cleaning",
"office-cleaning",
] # rate-card ids in the picker
service: "standard-cleaning" # preselected
ledger: true # price breakdown under the intro (collapsed under the ticket on phones)
ratesSection: # a second plans section, rendered with the `hourly` layout (trade rate sheet)
title: "Repairs, windows, carpets, and outdoor"
note: "All rates include travel inside our areas."
trades: [] # empty = every trade with an hourly or per-unit rate card
options:
layout: "hourly" # default | hourly | table | savings
  • estimateSection.services lists the services in the estimator’s picker, in order. Empty offers all of them. Up to 5 show as chips, 6 or more as a list.
  • ledger: true shows the “How this price is built” breakdown next to the estimator.
  • The defaults of every section are in src/content/sections/english/ (estimate-section.md, plans-section.md, add-ons-section.md).

Plans

A plan is a rate card at a reference home size. You never type a plan price:

plansSection:
description: "Prices for a 2-bed, 1-bath home."
showDiscounts: true # appends the recurring discounts from `frequencies` to the description
plans:
- name: "Standard clean"
rateCard: "standard-cleaning"
home: { bedrooms: 2, bathrooms: 1 }
featured: true
badge: "Booked most" # sample
features: ["Every room on the standard checklist", "Same crew each visit"]
button: {
enable: true,
label: "Book Standard",
url: "/book/",
variant: "fill",
} # the selection is added as a query string
  • rateCard and home set the price. A size outside the card’s limits is moved inside them, and the build prints a [plans] warning.
  • customPrice: "Quote" shows text instead of a price.
  • The plan button opens the booking form with the plan’s service and size already chosen.
  • Layouts: default (plan cards), hourly (trade rate sheet), table, savings (the recurring discounts as tiles). See Section Layouts.

Currency And Number Formats

The currency is set once, for every language, in src/config/config.toml:

[settings.estimator]
enable = true
currency = "USD" # ISO 4217 code, one currency for every language
currencyDisplay = "symbol" # symbol ($ / $US) | narrowSymbol ($ in every language) | code (USD)
bookUrl = "/book/" # "Book this visit" and the no-JS form go here, with the selection in the query string
ticketPrefix = "EST" # Mono code on the ticket, e.g. EST-4K7Q2
animateTotal = true # Count between totals; always off with reduced motion

Each language formats the same amount its own way:

currency / currencyDisplayEnglishFrench
USD / symbol$1,2401 240 $US
USD / narrowSymbol$1,2401 240 $
EUR / symbol€1,2401 240 €
GBP / symbol£1,2401 240 £GB
USD / codeUSD 1,2401 240 USD

Whole amounts show without decimals; other amounts (such as perUnit: 0.09) show two. Write the numbers in the pricing file as plain numbers (75, 0.09), never with a currency sign.

enable = false hides the estimator everywhere. animateTotal = false stops the total from counting up between values.

Keep English And French Identical

Each language has its own pricing file, so the labels can be translated. The numbers must match. The build compares every number in the French file with the English one and prints a warning that names each difference:

[pricing] Numbers in src/content/pricing/french/-index.md differ from src/content/pricing/english/-index.md: rateCards.handyman.hourlyRate

The key reads as block.id.field. Fix the French file until the warning is gone. Translate label, note, largerNote, description, and unit; keep every id the same in both files.

The pricing collection has a schema, so npm run astro-check reports a missing mode or a price written as text. If the file has an error, the build stops with a schema error that names the file and the field.

Prices Typed As Text

A few places show a price as written, not computed. They do not update when you change a rate, so check them after a price change:

  • The step previews in src/content/sections/english/steps-section.md, src/content/pages/english/how-it-works.md, and the repair template in src/content/services/english/-index.md (for example value: "$115").
  • Fees in questions and policies: questionsSection in the pricing file (the late-cancellation fee), src/content/pages/english/cancellation-policy.md, src/content/faq/english/-index.md.
  • Project prices (price.amount in src/content/projects/), gift card amounts (src/content/pages/english/gift-cards.md), and the insurance value in src/content/sections/english/trust-strip.md.

Search your content for the currency sign to find the rest.

The estimate ticket itself (the card with the mono code and the total) is documented in The Job Ticket.

Do It With AI

Set my prices in src/content/pricing/english/-index.md. Currency: [ISO CODE] in
[settings.estimator] in src/config/config.toml. Standard clean: base [X], per bedroom [X], per
bathroom [X], minimum [X]. Deep clean: [...]. Handyman: [X] per hour, [X] hours minimum.
Discounts: weekly [X]%, every 2 weeks [X]%, monthly [X]%. Add-ons: [NAME: PRICE; ...]. Keep every
id and key, remove the `# sample` comment from each value you replace, and copy the same numbers to
src/content/pricing/french/-index.md. Then work out a 2-bed, 1-bath visit every 2 weeks with the
formula in src/lib/pricing.ts and show me the total.
Search my src/content folder (English and French) for prices typed as text, such as "$115" in
step previews, fees in questions and policies, and project prices. List each one with its file,
and update them to match my pricing file.
Type something to search...