ClearwayBuild notes View the page Index

Industry demonstration · site 411

How the Clearway rubbish removal page was built.

Clearway Rubbish Removal is a fictional Bibra Lake operator — two tippers and a hooklift, six days a week, south and central metro. The page has one job: get a householder standing in a garage full of stuff to press call inside sixty seconds. Everything below is in service of that, including the parts that look like art direction.

Why this converts

Five sentences for the person deciding whether to hire us.

The phone number is the primary call to action in three places — masthead, hero, and a sticky bar that never leaves the bottom of a phone screen — because a rubbish job is decided in one call and a form that waits for a reply loses to the next operator on the list.

The enquiry form asks five things and no more (name, mobile, suburb, rough timing, what needs to go), because every extra field on a trade form is a person who closes the tab, and the four things a dispatcher actually needs to price a load are already answered by the estimate above it.

The proof element is a three-control load estimate rather than a seven-field quote engine, because the customer does not know their cubic metres — they know they have "about half a trailer, down the side of the house, and some of it is soil", which is exactly what the three controls ask.

The trust strip is one line, 11 years, 9,400 loads, sitting directly under the two buttons where the eye already is, because a number and a duration outperform a row of badges nobody reads and neither one costs the page any height.

Every price on the page comes from one small model written down before a line of code was typed, so the estimate's cheapest possible answer and the services grid's "from $120" are the same number by construction — a page that argues with itself about price reads as dishonest even when each figure is fine on its own.

Conversion decisions

Why the call button leads and the form follows.

This trade is not a considered purchase. The customer has a deadline (a settlement, an inspection, a skip that never came) and a pile that is not getting smaller. The decision they are making is not "which rubbish removalist has the best brand" — it is "can someone come today, and roughly what will it cost". So the page answers both above the fold and then puts a phone number in front of them at every scroll position.

The form exists for the two cases where a call does not suit: an after-hours enquiry, and a property manager who wants a paper trail. It sits after the FAQ, not before, because a person who has read that far is going to fill it in properly, and a person who has not is going to ring instead. When they do reach it, the estimate they just built is already carried into it — the panel above the fields reads back their configured load in words and a hidden field carries the same string, so the callback starts from a number both sides can see.

Why three controls and not seven

A seven-field quote engine is a better model and a worse page. Volume, access and material are the three things that actually move a rubbish removal price in this market, and they are the only three a householder can answer without measuring anything. Adding bin size, stair count, distance from the kerb and a date picker would make the estimate more precise and would move the call button below the fold — which costs more calls than the precision wins. The one thing the page must never do is compute a figure it cannot honour, so the result card says "That's the price, not a starting point" and then states, plainly, what happens if the pile is bigger than described.

The second read

Diversion is the only green on the page, used once: about 6 in 10 loads leave the yard as metal, green waste or recovered timber. It is set below the result card rather than in the hero because it is not why anyone rings — but it is why some people ring back, and it is the one line a property manager quotes to their owner. A second, quieter one is in the drawing itself: the trailer's cage mid-rail sits exactly at the half-load fill line, because both are derived from the same cage height.

The load estimate

The arithmetic, and where the floor comes from.

Three controls, whole dollars, no rounding step, GST inclusive. Load volume sets the base; access adds a carry charge; heavy material adds a flat surcharge because the tip charges soil, brick and concrete by weight rather than by volume. That is the entire model, and the FAQ says so in the same words.

The whole model. Total = load + access + heavy.
ControlValueAdds
LoadQuarter trailer$120
LoadHalf trailer$190
LoadThree-quarter$260
LoadFull 6×4 heaped$330
LoadTruck load$470
AccessKerbside$0
AccessInside, ground floor$40
AccessStairs or upper level$90
MaterialSoil, brick or concrete$60
Every figure below was driven in the browser and read back off window.__rubbishremoval, then checked against an oracle re-implemented from the spec in separate code. All 30 cells of the grid agree.
ScenarioLoad · access · materialPrinted
Floor — equals the services-grid anchorQuarter · kerbside · light$120
Rest state — the thumbnailHalf · inside · light$230
A garage clear-outFull 6×4 · stairs · heavy$480
The dearest cell on the gridTruck · stairs · heavy$620

The floor is load-bearing. The cheapest thing the estimate can print is $120, and the first card in the services grid says "from $120". Those are the same number because the grid anchor is the model's minimum, not a separate marketing figure — if they ever diverged the page would be quoting two prices for one job.

The estimate carries a fourth load size the grid does not: a three-quarter load at $260. The grid lists the four sizes a customer would ring about; the estimate lets them land between two of them without rounding up.

The drawing

A 6×4 trailer that fills, drawn in about eighty lines of SVG.

No images anywhere on either page. The trailer, the house, the truck, the carry line and the service-area map are all built as SVG elements at run time from a small set of functions. The cage is the volume: its interior is 100 user units tall and the load rect is that height times the selected fraction, so a half load literally reaches the mid rail. Picking a truck load swaps the whole silhouette rather than scaling the trailer up, because a truck load is not a bigger trailer and drawing it that way would be the first small lie on the page.

The load's texture is deterministic, not random: timber angles for a light load, courses of brick for a heavy one, seeded from a fixed constant list so two renders of the same state are identical. Each texture group gets its own sequence-numbered clip path — a fixed id collides the moment a heaped load draws a second texture patch for the crown above the rail, and the crown then clips to the wrong rectangle.

Proportion is an honesty question

The first version drew the trailer 332 units long under a 76-unit cage — a 4.4:1 box, which is a semi-trailer. A 6×4 cage trailer side-on is closer to 1.9:1. It was redrawn at 190 by 100 with a tray rail, a mudguard over the single axle, a drawbar and a jockey wheel. The page is selling a specific vehicle; the drawing has to be that vehicle.

Animation is wall-clock only. The load level tweens over 380 ms measured against performance.now(), never a per-frame accumulator, and it settles immediately if the tab is hidden or the visitor prefers reduced motion. Headless Chrome only services an animation frame when a frame is requested, so a frame-counted tween renders a fraction of its motion inside the seven-second thumbnail capture and the preview ships a half-empty trailer. Nothing on the page animates at rest, which is what the thumbnail needs.

Palette

Workwear, not hi-vis novelty.

Paper and panel are the colour of a dusty invoice book; the ink is close to black but not black. Amber is a tipper's beacon and a wear-plate, used as a fill and a figure and never as a large ground. Green appears exactly once, on the diversion line. Every ratio below is the measured row from the wave's contrast sweep, quoted with the hex it was measured against.

  • Paper#F5F3EEThe page ground. Reversed on the dark band it measures 14.68:1, and on amber 5.22:1.
  • Panel#E7E3DACards, the price rail and the scene ground. Ink on it is 13.82:1.
  • Ink#15191BBody and headings. 15.96:1 on paper, 13.82:1 on panel. Forbidden on the band (1.09:1), on amber (3.06:1) and on green (1.92:1).
  • Muted#4B5257Secondary copy. 7.16:1 on paper, 6.2:1 on panel.
  • Tipper amber#A24E07CTA fills, the price plate, marks. As ink 5.22:1 on paper and 4.52:1 on panel; paper reversed on it is 5.22:1.
  • Diversion green#17513AOne line on the page and nothing else. 8.33:1 on paper, 7.21:1 on panel.
  • Dark band#1B2124Reviews and the Sapience band. Paper reversed on it is 14.68:1.
  • Field#6E6B62Input borders and drawn marks. 4.8:1 on paper, 4.16:1 on panel, both clear of the 3:1 mark floor.
  • Hairline#B9B2A5Section rules only. 1.9:1 on paper — a hairline, never a mark and never text.
  • Water#78817CDerived for the map. Measured here: 3.14:1 on panel and 3.62:1 on paper, both over the 3:1 non-text floor. Labels crossing it are ink cased in a 3.4-unit paper stroke.
  • Light load#C8B99BScene fill. No text is ever drawn on it; the texture strokes over it measure 2.76:1.
  • Heavy load#8C8378Scene fill for soil, brick and concrete. Texture strokes over it measure 1.43:1 — a deliberate density difference, not a legibility claim.
  • Steel (cased)#CFCABE1.28:1 on panel — invisible bare. Every steel shape carries a 2-unit ink outline, so it is judged on its casing, and it is forbidden as an uncased area fill.

Two colour defects the wave sweep could not see

The sweep measures the eight declared tokens against five grounds. It does not measure the scene fills, and both of this page's colour defects lived there. First: the selected chip's sub-label was paper at 88% opacity on amber, which blends to #EBDFD2 and measures 4.41:1 — under the line for 11px text. It ships at full strength, which is the log's 5.22:1. Second: the map's ocean and both rivers were filled with the steel token at 1.28:1 against the map ground, so the coastline and the Swan were effectively not there. That is what the water token was minted for.

Type

Archivo for the plate, Inter for everything else.

Archivo at 700 and 800 is squarish and slightly plated — it reads like signwriting on a truck door, which is the right register for a business whose brand is a vehicle. It carries the wordmark, the headings, the "from $" figures and the estimate total. Inter at 400 and 500 does the work: body copy, form labels, the FAQ answers, the map labels.

One typographic move, and only one: the estimate figure is set in Archivo 800 at a size nothing else on the page comes near — up to 3.9rem, tabular-numeral, on an amber plate. It is the largest thing on the page after the headline, because it is the second thing the visitor came for. Body copy is 17px at 375 rather than 16, because this page is read one-handed in a garage.

Local SEO

What we would ship for the real business.

Clearway is fictional, so this page emits only WebPage, WebSite and CreativeWork structured data, naming itself a demonstration. Publishing fictional LocalBusiness or Review data into a real search index would be dishonest markup, and the reviews on the page are never marked up at all. For a real operator, the schema below is what would go on the page — shown here as a sample, not emitted.

{
  "@context": "https://schema.org",
  "@type": "HomeAndConstructionBusiness",
  "name": "Clearway Rubbish Removal",
  "url": "https://<their domain>.com.au/",
  "telephone": "+61 8 9xxx xxxx",
  "priceRange": "$$",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "Unit x, xx Example Road",
    "addressLocality": "Bibra Lake",
    "addressRegion": "WA",
    "postalCode": "6163",
    "addressCountry": "AU"
  },
  "geo": { "@type": "GeoCoordinates", "latitude": -32.098, "longitude": 115.816 },
  "areaServed": [
    { "@type": "City", "name": "Fremantle" },
    { "@type": "City", "name": "Cockburn Central" },
    { "@type": "City", "name": "Canning Vale" },
    { "@type": "City", "name": "Rockingham" }
  ],
  "openingHoursSpecification": [
    { "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
      "opens": "06:30", "closes": "17:00" },
    { "@type": "OpeningHoursSpecification",
      "dayOfWeek": "Saturday", "opens": "07:00", "closes": "14:00" }
  ],
  "hasOfferCatalog": {
    "@type": "OfferCatalog",
    "name": "Rubbish removal",
    "itemListElement": [
      { "@type": "Offer",
        "itemOffered": { "@type": "Service", "name": "Quarter-trailer clearance" },
        "priceSpecification": {
          "@type": "PriceSpecification",
          "minPrice": 120, "priceCurrency": "AUD" } }
    ]
  },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.8", "reviewCount": "137"
  },
  "review": [
    { "@type": "Review",
      "author": { "@type": "Person", "name": "<a real reviewer>" },
      "reviewRating": { "@type": "Rating", "ratingValue": "5" },
      "reviewBody": "<their actual words, syndicated from the GBP feed>" }
  ]
}

Alongside it: a Google Business Profile with the yard's real address and service-area radius, the same NAP string byte-identical across the site footer, GBP and the major AU directories, one page per high-intent suburb where the search volume justifies it (Fremantle, Rockingham, Canning Vale) rather than a doorway page for all twenty-four, and the aggregateRating block wired to the live review feed so it never drifts from the profile.

For the real client

What changes when it stops being a demo.

  • The job flow. The enquiry form posts into ServiceM8 or Tradify — both are what WA operators this size actually run — creating the job, attaching the customer's photo and the estimate string, and paging the on-shift driver. The "we'd call you back within the hour" promise becomes a real SLA with a timestamp against it.
  • The estimate becomes a booking. Same three controls, but the result card gains a time-slot picker fed from the ServiceM8 dispatch board, so the load the customer priced arrives as a scheduled job rather than a callback.
  • Photos. Their trucks, their crew, their before-and-afters. The procedural drawing stays as the estimate's diagram — it does a job a photo cannot, which is showing the selected load — but the hero and the why-us grid earn real photography.
  • Reviews. The three demo testimonials are replaced by a live Google Business Profile feed, and only then does the Review and AggregateRating schema above go live.
  • Payment. Tap-on-the-truck already happens; the site adds a pay-invoice link and account terms for property managers, which is where the repeat revenue in this trade sits.
  • Prices. Every figure on this page is a plausible Perth market shape, not their price list. Their real rates go into the same model — the arithmetic and the floor identity are the part that transfers.

Iteration log

Three passes, each one driving the page.

Every pass drove the three estimate controls and read the printed dollars back off the state probe, submitted the form empty and filled, tapped the sticky bar links, and looked at a real screenshot. The estimate was checked against an oracle re-implemented in separate code, not against the page's own function.

Pass 1

Craft

  1. The proof element was outside the thumbnail. At 1200×750 the drawn trailer measured a bounding box of y 986–1059 — the seven-second capture contained the masthead, the hero and the price rail, and not one line of the drawing. Reclaimed 373 px by moving the section heading into the controls column at desktop so the scene starts at the top of the section, setting the trust line beside the buttons rather than under them, widening the headline measure to two lines, and trimming the masthead and rail rhythm. The drawn subject now measures y 593–725, whole and in frame, and it is the drawing that is measured — not the card and not the price plate.
  2. The selected chip's sub-label failed contrast. Paper at 88% opacity over amber blends to #EBDFD2 and measures 4.41:1; at 11 px that is body text. Shipped at full strength, 5.22:1.
  3. The map's water was invisible. The ocean and both rivers used the steel token at 1.28:1 against the map ground. Minted a water token at 3.14:1 on panel and 3.62:1 on paper. The steel token stays — but only cased, because every steel shape in the vehicle drawing carries a 2-unit ink outline.
  4. The trailer was drawn as a semi-trailer. 332 units long under a 76-unit cage. Redrawn at the proportions a 6×4 actually has.
  5. The service-area map was geographically wrong. The ocean was a straight vertical bar, the Canning River was painted over by the service-area fill because the two were drawn in the wrong order, and five label pairs collided. Rebuilt on a real longitude/latitude projection with the coast, Cockburn Sound, the Swan from the harbour to Guildford and the Canning down to Armadale, the boundary drawn first and the water over it.
  6. Map labels rendered at 9.6 px. They are set in user units, so they scale with the rendered map, not the viewport. Base raised to 14 units with a mobile bump; the suburb list beside the map is the plain-text equivalent either way.
  7. The map box shrink-wrapped to 300 px. An auto margin on a grid item cancels the stretch default, so the wrapper collapsed onto the SVG's 300 px user-agent width and the whole map rendered a third under size. An explicit width restored it.
Pass 2

Depth

  1. The truck hid the one thing the access control has to show. With "stairs or upper level" selected and a truck load, the cab covered the external stair down the side of the house. The house was tightened, the stair moved to clear x 134, and every vehicle silhouette now starts at x 140 — so the state the customer is paying $90 for is visible in all five load sizes.
  2. The kerbside pile sat under the drawbar. The pile that distinguishes kerbside access was overlapped by the trailer's drawbar and jockey wheel. Both were moved; the coupling now sits at 170 and the pile ends at 168.
  3. The heaped load needed its own texture patch, and a fixed clip-path id would have collided. Drawing the crown above the cage rail means a second texture group in the same render, and two elements sharing an id makes the second clip resolve to the first rectangle. Texture clips are now sequence-numbered per draw.
  4. The state probe lied during the tween. The load-level animation published only at its endpoints, so settled read true and fill read stale for the whole 380 ms. It now publishes every frame, which is what makes the "is the page at rest at seven seconds" check meaningful rather than decorative.
  5. One micro-interaction added, and only one. The price figure settles from 105.5% over 260 ms when the total changes — input-driven only, off under reduced motion, and nothing animates at rest. It exists because the estimate's whole argument is that the number moves when you tell it something true.
Pass 3

Hardening

  1. The 375 px masthead, checked first. The strapline wrapped onto a second line, pushing the header taller than the call button beside it. The suburb now drops out of the wordmark under 420 px. That fix then left the wordmark link 34 px tall — under the 44 px target floor — so it carries an explicit minimum height.
  2. All 30 cells of the estimate driven against an independently re-implemented oracle, asserting the plate figure, all four ledger lines, the state probe and the string handed to the form. Zero disagreements.
  3. Two rapid-fire drives — seven control clicks dispatched in a single evaluation with no settle between, once with an even heavy-toggle parity and once odd, ending on a silhouette swap. Both settle on the correct state with the correct shape count. A per-control drive structurally cannot see a swap race.
  4. The form, three ways. Empty submit on a fresh navigation raises five visible errors and five aria-invalid flags, moves focus to the first bad field, and leaves the confirmation panel hidden. A filled submit shows it, carrying the priced load and declaring plainly that nothing was sent. "Send another" clears every field and error and returns focus.
  5. The radiogroups driven by keyboard, not only by dispatched clicks — arrow keys move the selection and the focus together.
  6. Reduced motion, hidden tab, focus states. The hero never sits at zero opacity, the load level settles instantly instead of tweening, and the tween abandons itself if the tab goes away mid-flight.
  7. At rest for eight seconds after the capture instant. Every numeric field on the state probe is byte-identical at seven seconds and at fifteen.
  8. Both pages scanned for typographic quotes used as attribute delimiters, for phone numbers outside the reserved pattern, and for live structured data outside the allowlist. Body copy keeps its typographic quotes; attributes never do.
  9. Three of the failures this build reported were the harness, not the page — a rapid-fire expectation that forgot the heavy toggle's parity carried over from the previous test, a reduced-motion baseline that assumed kerbside when the rest state is inside, and a 375 px sweep that flagged the deliberately off-screen skip link and a header button that is display:none at that width. Each was fixed in the harness. The ornament removed at the end: the footer's enquiry link was a mailto pointed at our own address, which made the fictional business's contact route ours; it is now an anchor to the form on the page.