Generative Assets · site 444 · caravan park
A caravan park page that answers the two questions asked at the wheel.
Redgum Flats is a fictional 24-site park on a smallholding out of Dwellingup, built as a demonstration of the lead-generation page Sapience Analytics would put in front of a real operator’s customers. The couple towing a van down the South Western Highway want to know two things before they commit — will it fit and is there a site free next Thursday — and the page’s single job is to answer both in the first screen and take the enquiry direct instead of handing it to somebody else’s inventory.
The proof element
Fourteen nights, computed from the date and nothing else
The card beside the headline is the whole argument. It carries the four site types with their real rates, four steppers (nights, adults, children, van length), an arrival date, and a strip of the next fourteen nights drawn as fourteen bars showing free sites out of 24.
Availability is a pure function of the ISO date. A fixed 32-bit string hash modulo the 24 camp sites, with Friday and Saturday pushed one band tighter because that is what a park’s book actually looks like. There is no Math.random anywhere on the page, so the strip is byte-identical on a reload, in the screenshot pipeline and under the test harness — which is what lets the FAQ say, out loud, that the strip is not a marketing device.
function avail(iso) {
var h = 0;
for (var i = 0; i < iso.length; i++) h = (h * 31 + iso.charCodeAt(i)) >>> 0;
var dow = dateOf(iso).getDay();
var sites = h % 24; // 0..23 of 24 camp sites
var free = (dow === 5 || dow === 6) ? Math.floor(sites / 3) : sites;
return { free: free, state: free === 0 ? 'full'
: free <= 4 ? 'limited' : 'open' };
}
The quote is a four-line function held entirely in whole dollars — no cents, no floats, nothing that can arrive as 1839.9999999999998. Two adults are inside every site rate; a third is $14 a night and a child 3–15 is $8; and every seventh night is free, which is expressed as nights − floor(nights / 7) charged nights rather than as a discount percentage.
| Site | A night | 5 nights | 7 nights | What the seventh night is worth |
|---|---|---|---|---|
| Unpowered bush site | $32 | $160 | $192 | $32 back |
| Powered site | $46 | $230 | $276 | $46 back |
| Drive-through powered | $54 | $270 | $324 | $54 back |
| Ensuite cabin | $135 | $675 | $810 | $135 back |
Every figure on this page and in the card was re-derived by a separate oracle inside the test harness rather than read back from the page’s own function. The harness also enumerates the entire reachable control grid — four site types × 1–14 nights × 1–6 adults × 0–6 children, less the cells the cabin clamp removes: 1,904 priced cells, zero non-integer dollars, and a cheapest reachable night of $32, which is exactly the rates grid’s “from $”.
The move that costs the business money
The card gives away the drive-through bay
The two dearest things Redgum Flats sells are the drive-through bay and the ensuite cabin. The card argues against both, at full size, in the same frame as the price:
- Van under 6.0 m on a drive-through bay → “Your 5.5 m van does not need a drive-through bay … it is $8 a night less on a standard powered site. We would rather keep bay 21 for the person who cannot fit anywhere else.” The button beside it moves you to the cheaper site.
- Cabin selected → the card prices the powered site for the same stay and prints the difference, then offers to swap.
- Van over 7.5 m → this is the only state where the page recommends the dearer bay, and it says so: “That is the only time we will sell you one.”
- Bush site → the card names the $14 a night you keep by going without power, every night you are there.
The second half of the move is what happens on a full night. Rather than a dead “sold out”, the card names the nearest night that still has a site on it, prints how many are free and says the price does not change. It deliberately offers the nearest night with space, not the nearest wide-open one — driving the first full Saturday the hash produces (17 October) shows a following week of tight-but-bookable nights, and telling somebody to come back in eight days when there are two sites free tomorrow is losing the enquiry.
The measured copy on that Saturday, read out of the running page: “The nearest night we can put you on is Sunday 18 October, with 2 sites free. Sunday nights empty out after lunch, so the camp kitchen is usually yours. The price does not change.”
Colour
Wet forest and laterite, not holiday brochure
What this park sells is a shaded gravel pad under jarrah and marri in the middle of the year. Aqua and sunset gradients were ruled out by argument before a single hex was picked: they sell a beach, and there is no beach. The ground is the cream of a canvas annexe, the dark is the colour of jarrah forest at four in the afternoon, and the one accent is laterite — the orange gravel every access road in the Darling Scarp is cut into.
Every ratio below is pasted from contrast-w57.log, section caravanpark, not computed by hand.
#FAF5E9Page ground. Body ink lands on it at 14.49:1.#F0E8D6Cards, rate tiles, the strip selection tint. Ink 12.92:1.#1D2B21The answer band, buildings on the site plan, the footer. Cream on it 13.6:1.#9A4318Every primary button. Cream on it 6.05:1; as a mark on paper 6.05:1.#20241EBody copy and every map label. 13.04:1 on map land, 10.65:1 on map water.#55594DSecondary copy only. 6.61 / 5.89:1 — and forbidden on both map fills.#2C5335Eyebrows, open nights, roads and route lines. 8.06 / 7.19 / 7.25 / 5.92:1.#8E3D13Links and the give-away headline. 6.83 / 6.09:1.#D3C3A0Hairlines and the gravel roads on the plan. 1.6 / 1.42:1 — lines only, never type.#37453BDividers inside the answer band and the footer. 1.46:1.#F1E9D7The locator’s ground.#C6D8D6The Indian Ocean and Nanga Brook.Two forbidden pairs that changed the drawings
The log prints every unlicensed pairing with its measured ratio, and two of them decided real design choices. Muted grey is forbidden on both map fills (5.95:1 and 4.86:1), so every word on both maps is set in full-strength ink rather than the quieter grey the rest of the page uses for captions. Laterite is forbidden on map land in both its mark and its ink form, which is why the locator map draws nothing in orange: the destination is a panel-filled callout box with a forest border, and the laterite ink inside it is landing on panel, where it is licensed at 6.09:1. The park site plan is drawn on a cream sheet inside the panel card for the same reason — every token on it is then a licensed paper pair.
Type
A slab with signage weight, and a workhorse
Bitter 600/700 for display. A slab serif reads like the park’s own routed timber sign; its heavier weights hold a headline at 43px without the municipal stiffness a grotesque brings to a caravan park. Karla 400/600 for body, labels and every number in the card — a humanist sans with a large x-height that stays legible at 12px on a phone in daylight, which is where most of this traffic is.
Numbers everywhere in the card, the facts ledger and the rate tiles are set font-variant-numeric: tabular-nums, so the total does not jitter horizontally as you tap a stepper. Type scale is 10.5 / 12 / 13.5 / 15 / 16.5 / 21 / 24 / 30 / 43, and the eyebrows are the only tracked-out caps on the page.
Technique
How the strip is drawn
- Real CSS pixels, not a fixed viewBox. The strip measures its own rendered width and writes
viewBox="0 0 W 122"to match, so one viewBox unit is one CSS pixel at every screen size and a 10px label is 10px at 375 as well as at 1440. A fixed viewBox scaled to a phone renders its labels at about half that, which is a defect this collection has shipped before. - Wall-clock entrance, never a frame accumulator. The bars grow once on cold load from
performance.now()deltas over 900 ms with a cubic ease, plus a 1,400 mssetTimeoutbackstop that snaps them to their final height. A per-frame accumulator would have advanced only as far as the frames it was given, and headless Chrome services requestAnimationFrame only when a frame is asked for — which is exactly how a screenshot pipeline photographs a half-drawn page. The rAF delta is also clamped at zero, because a frame timestamp can precede the timestamp captured when the loop was scheduled. - The entrance never re-enters. A resize re-lays the strip out and paints the bars at full height; it does not replay the choreography. Taking a clipped screenshot changes device metrics and therefore fires the resize handler, so an entrance that re-runs on resize photographs an empty scene while every assert stays green.
- Reduced motion is checked in CSS and in
matchMediabefore the loop is started at all, and the settled frame is drawn immediately rather than left blank. - SVG children are toggled by attribute.
el.hiddenis anHTMLElementproperty and sets a silent expando on an SVG node, so the selection tint and the underline are driven with explicitopacityattributes, and every HTML panel that hides is guarded by[hidden]{display:none!important}. - Everything is procedural. No raster images anywhere: the site plan, the locator, the wordmark, the favicon and the phone glyphs are all inline SVG or CSS.
Conversion decisions
Why the page is shaped like this
- The proof card sits in the hero’s second column at 1024px and up, while the mobile reading order stays masthead → hero → rates → card. The 1200×750 preview is the sales asset, and a hero plus a six-tile rate grid spends the whole frame before anything is drawn. Measured on the shipped page: the $230 answer finishes at y 697 and the fourteen bars run 502–552, both comfortably inside the frame.
- The left column carries a four-row facts ledger — $46 a night, a week for $276, 7.5 m on a pad and 16 m in a bay, family-run since 2011 — so the rate and the fit rule are in the preview twice, once as copy and once as computation.
- Form fields are the ones a park actually needs to hold a site: name, phone, arrival date, nights, van length and site type, with email, home town and a notes box optional. Van length is on the form because it is the field that decides which site you get, and asking for it up front is what removes the phone call.
- The card hands off to the form. “Send this as an enquiry” copies the arrival, nights, van length and site type into the form and writes a one-line summary into the notes, so the enquiry that arrives is already qualified.
- No deposit, no payment field, no fake network call. The confirmation is a designed local state that says plainly that this is a demonstration build and nothing was sent.
- The mobile sticky bar owns a real
tel:link and an anchor to the form, and the hero’s headline, primary CTA and trust line are all measured to finish above the fold minus the bar — 477px against a 604px working fold at 375×667.
Iteration
The three passes
Pass 1 — Craft
- The trust line finished 7px below the working fold at 375. The fold is the viewport minus the sticky bar (667 − 63 = 604) and the trust line ended at 611. Cut the hero subline from six lines to three, shortened the primary CTA to “Check a night” and moved the licence number to the footer; the trust line now ends at 477.
- The state probe reported the page unsettled forever. The animation loop’s terminal branch set its own flag instead of calling
finishEntrance(), so__caravanpark.settlednever flipped while the drawing was in fact at rest. Invisible to the eye, fatal to a harness that gates on it. - The desktop hero’s left column ran out at about 520px against a card running to 700, leaving a quarter of the preview empty. That is where the facts ledger came from.
- Site plan, three collisions only pixels could find: the OFFICE block sat on top of the entry road, two of the seven drawn trees were planted in the middle of the one-way loop, and the north arrow was inside the dog paddock. Office moved up, trees relocated, and the north arrow deleted outright — it was the one ornament the drawing did not need.
- The drive-through bays were a lie in the drawing. They were four horizontal pads floating east of the loop with no road touching them — a “drive-through” you could only reverse out of, on a page whose FAQ says you never reverse. Redrawn as four north-south bays between an entry spur off the loop’s top-right arc and an exit spur off the bottom-right, which is what the caption and the FAQ had been claiming all along.
- At 375 the wordmark wrapped mid-phrase (“CARAVAN PARK ·” / “DWELLINGUP”). The long half is now hidden below 600px.
Pass 2 — Depth
- The strip now tints every night of the stay, not just the arrival, with an ink underline on the arrival itself. Driving nights from 1 to 14 moves a visible band across the fortnight, which turns “night 3 of your stay is full” from a sentence into a picture.
- The cabins sleep four, so the card says so with a clamp. Selecting a cabin caps adults and children at four between them and remembers the party you had; leaving restores it, and any explicit change while the clamp is on clears the memory. Driven end to end: powered with 4 adults and 2 children → cabin clamps to 4 → back to powered restores 4 and 2.
- The nearest-night rule was wrong for the case it was written for. It looked for the next wide-open night, which on the first full Saturday the hash produces (17 October) skips a whole week of bookable nights and answers “25 October”. Changed to the next night with any space at all, and the harness now asserts that specific Saturday against an independently computed answer.
- Weekday labels were single letters at 375, where Thursday and Tuesday are both “T”. Two-letter abbreviations now appear below a 26px column width.
- Added the “which nights are hardest to get” FAQ so the Friday and Saturday tightening is explained rather than merely observed, and so the page can say in plain words that the strip is the same numbers for whoever loads it.
Pass 3 — Hardening
- Rapid-fire drive. Ten control clicks dispatched in a single evaluate with no settle between them, then twelve more — no stale-closure race, the total still equals per-night × charged nights, zero console errors. Per-control drives structurally cannot see a swap race.
- Junk in the format-constrained field. Chrome compiles a
patternattribute with thevflag, where(,)and-are reserved inside a character class — and av-invalid pattern is silently discarded rather than reported, leaving the field accepting anything. All three are escaped, the attribute is compiled withnew RegExp(p, 'v')in the checker, and “ring me maybe” was typed into the phone box and rejected with the confirmation still hidden. - Grid enumeration. 1,904 reachable priced cells, zero non-integer dollars, cheapest reachable night $32 — identical to the rates grid floor, so the page cannot argue with itself.
- Two live resizes (1200 → 1000 → 1400): bars stay at full height, the entrance does not replay,
settledstays true. - Rectangle intersection at three widths. The answer band’s box never overlaps the drawn bars at 375, 1000 or 1200 at DPR 2, and there is no horizontal overflow at any of them.
- Keyboard. Each of the fourteen nights is a focusable
role="button"that responds to Enter, the focus ring is a 3px solid outline, and the drawing is mirrored as plain text for a screen reader with every night’s free count and state. - The real thumbnail, looked at. Reproduced the pipeline exactly — 1200×750, seven seconds, same renderer — and then opened the JPEG. Page at rest, every numeric probe field identical eight seconds later, the fourteen bars and the $230 both in frame. Asserts prove state; only pixels prove composition.
- The mobile reading order was wrong and 125 green asserts had not noticed. At 768 the proof card rendered straight after the hero, ahead of the rates grid, because it sat before the rates section in the source — and the desktop grid placement put both in the right place at 1200, so every layout check passed. Caught by opening a 768 screenshot. The two sections were swapped in the source (the desktop placement is by explicit grid row and column, so it did not move), and the harness now asserts the source order of every section under
#main. - Then the green was tested rather than believed. Seven defects were planted one at a time — availability switched to
Math.random, the phone pattern escaped only its hyphen, the seventh night stopped being free, the[hidden]guard deleted, a second<h1>added to this page, the answer card pushed out of the 750px frame, and the give-away steer demoted to 11px — and all seven fired, with both files restored byte-for-byte and verified after each. Two of the first seven plants were silent, and both times the plant was wrong rather than the check: the[hidden]guard cannot be caught by removing it alone, because the browser’s own default already hides the panel — the guard exists to beat an authored display rule, so the check now plants one and watches it lose.
Final count: 101 asserts in the behavioural harness, 24 in the hardening harness and 16 structural checks across both pages, all green, against an oracle that re-implements the rates and the availability hash from the specification rather than reading them off the page.
What this page declined to publish
The park, its 24 sites, its rig lengths and its rates are invented and need no external source. Several things a real caravan park page would be tempted to print are not here, because they are somebody else’s facts and they move:
- No booking platform is named anywhere, and no commission percentage is published. The page makes its case on its own rate — “these are the rates you pay us” — and says nothing about what anyone else charges or keeps.
- No trail distances. The Munda Biddi and the Bibbulmun are named because they genuinely pass through the town; not one kilometre figure is attached to either.
- No national park fees, no fire-ban dates, no shire camping rules. The fire answer names the mechanism instead: restrictions are set by the shire and the state, they change through the season, and the page tells you to ring on the morning you leave rather than printing a date it cannot keep current.
- The one real-world distance — “about 100 km south-east of Perth” — is deliberately rounded, because the road figure varies with the route you take and a precise number would be a false precision.
- No LocalBusiness, Campground, Review or AggregateRating structured data is emitted. The page ships a single
WebPageblock that names itself a demonstration. Fictional structured data in a real index is dishonest markup, and the reviews on the page say in plain text that they are sample copy.
Local SEO
What we would ship for a real park
For a real operator the structured data is most of the local-search win, because a campground has a rich, well-supported schema type and almost nobody in the sector uses it. The correct type is Campground — a subtype of both LodgingBusiness and CivicStructure — not the generic LocalBusiness most templates emit. This is the block we would put on the real page, with real reviews from the operator’s own feed:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Campground",
"@id": "https://example.com.au/#park",
"name": "Redgum Flats Caravan Park",
"url": "https://example.com.au/",
"telephone": "+61 8 9XXX XXXX",
"priceRange": "$$",
"currenciesAccepted": "AUD",
"petsAllowed": true,
"checkinTime": "11:00:00+08:00",
"checkoutTime": "10:00:00+08:00",
"numberOfRooms": { "@type": "QuantitativeValue", "value": 26, "unitText": "sites and cabins" },
"address": {
"@type": "PostalAddress",
"streetAddress": "the real street address",
"addressLocality": "Dwellingup",
"addressRegion": "WA",
"postalCode": "6213",
"addressCountry": "AU"
},
"geo": { "@type": "GeoCoordinates", "latitude": -32.71, "longitude": 116.06 },
"image": [ "https://example.com.au/img/loop-1x1.jpg",
"https://example.com.au/img/loop-4x3.jpg",
"https://example.com.au/img/loop-16x9.jpg" ],
"amenityFeature": [
{ "@type": "LocationFeatureSpecification", "name": "Powered sites", "value": true },
{ "@type": "LocationFeatureSpecification", "name": "Drive-through sites", "value": true },
{ "@type": "LocationFeatureSpecification", "name": "Dump point", "value": true },
{ "@type": "LocationFeatureSpecification", "name": "Camp kitchen", "value": true },
{ "@type": "LocationFeatureSpecification", "name": "Laundry", "value": true }
],
"openingHoursSpecification": [{
"@type": "OpeningHoursSpecification",
"dayOfWeek": [ "Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday" ],
"opens": "08:00", "closes": "18:00"
}],
"makesOffer": [
{ "@type": "Offer", "name": "Powered site, two adults",
"price": "46.00", "priceCurrency": "AUD",
"availability": "https://schema.org/InStock" },
{ "@type": "Offer", "name": "Unpowered bush site, two adults",
"price": "32.00", "priceCurrency": "AUD" }
],
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.7", "reviewCount": "212", "bestRating": "5"
},
"review": [{
"@type": "Review",
"author": { "@type": "Person", "name": "a real, named guest" },
"datePublished": "2026-07-14",
"reviewRating": { "@type": "Rating", "ratingValue": "5", "bestRating": "5" },
"reviewBody": "the guest's own words, pulled from the review feed"
}]
}
</script>
Alongside it we would ship an FAQPage block over the questions already on the page (they are the exact long-tail queries a towing customer types), a BreadcrumbList, and a claimed Google Business Profile with the site plan and the drive-through bays photographed, because “caravan park Dwellingup drive through” is a search with an answer and almost no operator answers it on their own domain.
The rest of the local-search work is unglamorous and it is where the traffic is: one page per real question rather than one page per keyword, the town and the highway named in the <title>, the phone number as a tel: link everywhere it appears, and the availability strip kept on the landing page so the page has a reason to be re-crawled.
For the real client
What changes when this stops being a demonstration
- The hash comes out and the park management system goes in. Most WA parks run NewBook or RMS; both expose availability and rates over an API. The strip becomes a cached read of real inventory on a short TTL, and the fourteen bars, the states and the “nearest night with space” logic keep working unchanged because they already take a date and return a count. That is the point of building it as a pure function first — the shape of the answer does not change when the data becomes real.
- A channel manager sits behind it so the direct enquiries taken here and the third-party inventory never double-sell a site. The page’s job is to be the cheapest channel the park owns, not to pretend the others do not exist.
- Deposits through Stripe — a card hold or a one-night deposit at the point of enquiry, with the balance on arrival. That is the single biggest change to the funnel: it converts an enquiry into a booking without a phone call, and it is the reason a park tolerates a third party in the first place.
- Photographs replace the procedural drawing in the hero and the “what is here” block — the loop in winter, a rig in bay 22, the camp kitchen at breakfast. The site plan stays: it is drawn, it is accurate, and it is the thing people screenshot before they drive.
- Real reviews from the operator’s own feed, marked up properly, replacing the sample copy — and the review markup only goes live once the reviews are real.
- The enquiry form posts somewhere. On a park that size the honest plumbing is an email to the office plus an SMS to the manager’s phone, because the answer to “is there a site free next Thursday” is worth minutes, not hours. Anything more elaborate is a system nobody in the office will keep using.
- Fire restrictions link out to the shire and the state’s own pages rather than being restated, so the page can never be stale about something that matters.
For the person deciding whether to hire us
Why this converts
The visitor arrives with two questions — will the van fit and is there a night free — and both are answered above the fold on a phone, with a real number and a real date, before they have scrolled once.
Publishing the rate is the whole play: a caravan park that prints $46 a night on its own page beats one that says “enquire for pricing”, because the person towing has already decided what they will pay and is only looking for a reason to stop shopping.
The card argues against its own dearest products — it gives away the drive-through bay to anyone under six metres and prices the powered site against the cabin — and a customer who watches a page cost its owner $8 a night reads the rest of the quote as a model rather than a pitch.
A full night names the next night with space instead of showing a dead “sold out”, which keeps an enquiry that every competitor loses at exactly that moment.
And the form asks for van length, so what lands in the office inbox is a qualified booking the manager can answer in one line instead of a phone call — which is the difference between a website that looks nice and a website that gives somebody their afternoon back.