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

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:
| Block | What it holds |
|---|---|
rounding | The step that discounted prices round to (5 rounds to the nearest 5) |
frequencies | How often a visit repeats (weekly, every 2 weeks, monthly, one-time) and the discount for each |
rateCards | One card per bookable service: how it is priced, its limits, and its minimum |
addOns | Extras 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: 0discountis a percent from0to100.default: truemarks the frequency that is selected first. Put it on one frequency only.- Keep the id
oncefor the one-visit option. The ticket then says “one visit” instead of “per visit”. - Change the
labelfreely. Keep theidvalues: 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:
mode | The visitor sets | Price of the service line |
|---|---|---|
rooms | Bedrooms and bathrooms | base + bedrooms × perBedroom + bathrooms × perBathroom |
hourly | Hours | callOutFee + the larger of (hours, hours.min) × hourlyRate |
units | A count: panes, rooms, items, sq ft, storeys | base + 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"]| Key | What it does |
|---|---|
id | The service id. Same as the service file name, and the value in /book/?service= |
label | The service name on tickets, chips, and the booking form |
trade | The trade it belongs to (cleaning, repairs, windows, carpets, outdoor). Groups the trade rate sheet and the add-ons |
mode | rooms, hourly, or units (see above) |
base, perBedroom, perBathroom | Prices for rooms cards. base also works on units cards |
hourlyRate, callOutFee | Prices for hourly cards. callOutFee is added once per visit |
perUnit, unit | Price per unit and the unit name in singular (one) and plural (other) |
bedrooms, bathrooms, hours, units | The limits of each number field: min, max, step (optional), and the starting value default |
minimum | The smallest total for one visit, add-ons included |
recurring | true lets frequency discounts apply. false (or left out) prices a one-off service with no frequency choice |
frequencies | The frequency ids this card offers. Empty means all of them |
duration | Optional time on site: base, plus perBedroom and perBathroom (rooms) or perUnit (units). Shown as “About 2.5 h” |
note | A short line on the ticket, such as “Supplies and equipment included.” |
largerNote | Shown 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| Key | What it does |
|---|---|
price | A flat price. Frequency discounts never apply to add-ons |
services | The rate-card ids it can be added to. Empty means every service |
task | Optional task id from the “what’s included” list. The checklist then shows the add-on price on that task (see Services & What’s Included) |
description | Optional 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.
- 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.
- Frequency discount. If the card is
recurringand the visitor picked a frequency with a discount, take the percent off the service line, then round to the nearestrounding(5 by default). The difference shows as its own discount line. - Add-ons. Add the price of each selected add-on. Add-ons are never discounted.
- 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. - 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
| Place | What it shows | Where you set it |
|---|---|---|
/pricing/ | Plans, the estimator with its price breakdown, add-ons, the trade rate sheet, the cleaning comparison, and the payment questions | The page blocks in the pricing file |
| Cleaning service pages | The estimator in the hero, locked to that service | options.layout: "cleaning" in the service file |
Service pages on the default layout | An estimate section locked to that service, with the breakdown | estimateSection 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 header | Computed from the rate cards |
/book/ | The “from” price on each service tile and the live total | Computed from the rate cards |
/home-cleaning/ | The recurring-discount plans (savings layout) and the first booking step | plansSection and bookingSection in src/content/homepage/english/home-cleaning.md |
| Area pages | “Standard clean from $110” on the area ticket | The 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 | savingsestimateSection.serviceslists 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: trueshows 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 stringrateCardandhomeset 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 motionEach language formats the same amount its own way:
currency / currencyDisplay | English | French |
|---|---|---|
USD / symbol | $1,240 | 1 240 $US |
USD / narrowSymbol | $1,240 | 1 240 $ |
EUR / symbol | €1,240 | 1 240 € |
GBP / symbol | £1,240 | 1 240 £GB |
USD / code | USD 1,240 | 1 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.hourlyRateThe 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 therepairtemplate insrc/content/services/english/-index.md(for examplevalue: "$115"). - Fees in questions and policies:
questionsSectionin the pricing file (the late-cancellation fee),src/content/pages/english/cancellation-policy.md,src/content/faq/english/-index.md. - Project prices (
price.amountinsrc/content/projects/), gift card amounts (src/content/pages/english/gift-cards.md), and the insurance value insrc/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], perbathroom [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 everyid and key, remove the `# sample` comment from each value you replace, and copy the same numbers tosrc/content/pricing/french/-index.md. Then work out a 2-bed, 1-bath visit every 2 weeks with theformula 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" instep previews, fees in questions and policies, and project prices. List each one with its file,and update them to match my pricing file.