Concept projectFictional business demonstrationHVAC

Stellwood Heating & Air

A symptom-first website for a heating and cooling contractor.

Concept project · Fictional business demonstration Stellwood Heating & Air is a fictional company. The site was designed and built to demonstrate residential HVAC web design and development. It has no licence, certifications, reviews, warranty or trading history — and unlike most demonstrations, it displays none of those things rather than inventing them.

A 12-route site for a residential heating and cooling contractor in the Denver metro. The visitor does not know what is broken, only what the system is doing, so the page starts from the symptom and works toward the diagnosis. Every claim the business cannot support is designed out instead of invented.

The demo is a fictional business. It is noindex in the response header, a meta tag and robots.txt, and shows a concept disclosure on every page.

Type
12-route local-service website: 5 main pages, 5 service pages, privacy, 404
Stack
Eleventy 3 · Nunjucks · hand-written CSS; WordPress theme (validated locally only)
Hosting
Cloudflare Pages (static build, Direct Upload)
Built
September 2026
Status
Live demo, noindex
Stellwood Heating & Air concept site: the desktop home page showing the headline "Running, but the house never cools. Denver." beside a technician working at an open furnace cabinet.

Brief

The problem

A homeowner whose air conditioning has stopped does not know whether they need a capacitor, a refrigerant leak diagnosis, a new condenser or a duct rebalance. They know one thing: what the system is doing.

Most contractor sites answer a different question than the one being asked:

  1. A menu of services asks the visitor to self-diagnose before they are allowed to ask for help.
  2. A promise — 24/7, a 60-minute arrival window, a blanket satisfaction pledge — asks them to trust a claim they cannot check.

This site does neither. It opens with a symptom and offers a method: tell us what it is doing, and we will tell you what we would check and in what order, before anyone is booked.

Conversion

Conversion strategy

Calling is the primary action, and it is the only primary action on screen at any moment. The estimate request is the secondary path for people who are not ready to talk.

The headline is a symptom, not a service

The home page opens with "Running, but the house never cools. Denver." A symptom router below it lists what the visitor is actually experiencing — no heat, no cooling, rooms on one floor that never match, a noise that started recently — and routes each to the relevant service page. The services menu still exists; it is just not the first question.

Home page above the fold at desktop width: utility bar with the phone number and opening hours, the headline "Running, but the house never cools. Denver.", a supporting line promising to say what would be checked before anyone is booked, call and estimate buttons, and a technician reading an instrument at an open furnace cabinet.

Method and refusals instead of badges

A method strip states three things worth knowing before the visit, written as diagnosis rather than sales copy: refrigerant is not a consumable, so if the level is down it is leaving somewhere and topping it up buys a few weeks before the same call comes back. A later section names four things the company will not do. Naming what a contractor declines to do is a stronger signal than a badge, and unlike a badge it costs something to say.

The services hub at desktop width: heating and cooling presented as two parallel tracks on warm and cool surfaces, each listing its services with its own button.

One primary action per viewport

On mobile a persistent call bar appears after 40% scroll and hides itself while the closing call-to-action band is in view, so the same primary action never appears twice in one screen. Two identical CTAs at once is a hierarchy problem, not redundancy: the visitor has to work out which one is the real one.

Home page on a phone without scrolling: a Call now button and Menu in the header, the symptom headline, the supporting line, and full-width call and estimate buttons above the photograph.

Screenshots from the live demo. The phone number is in the reserved 555-01xx range and does not resolve.

Claim gating

Designing the absence of claims

The constraint
A fictional business has no licence, no certification, no years in business, no reviews and no warranty it could honour. Every such claim is marked UNKNOWN and asserted:false in one config file rather than filled with a plausible value.
Trust strip
Nothing to show, so it is replaced by a method strip that describes procedure instead of credentials.
Emergency band
No availability claim, so the band is re-scoped to carry the phone number rather than a promise.
Warranty
No warranty exists, so the /warranty-terms/ route is not generated at all and returns 404 on the WordPress build. A terms page for a warranty that does not exist asserts one by implication.
Reviews and rating
The testimonials section is removed entirely, and no AggregateRating appears in the structured data.
Years in business
founded_year is left empty on purpose: the loader derives years from it, so a year typed there would resurrect the claim through the back door.
Enforcement
The gate covers the whole sentence, not just the value, so no page renders a label with nothing after it. A scanner runs on every build and fails it if claim language reappears: 0 hits on both platforms.
A service detail page for furnace repair: what the visit covers, written as procedure, with no badges, no warranty claim and no review count anywhere on the page.

Local service

Local-service architecture

Service areas
Six Denver-metro cities on one page, each with the housing stock that defines it and the failure that stock produces — central Denver's pre-war brick with cooling retrofitted into optimistically sized ductwork; Lakewood's mid-century ranches where the long duct run through crawl space means the complaint is one room and the cause is never in that room.
Not a template with the name swapped
A changed-value test proves each city's content is independently sourced from config: adding, removing or renaming a city updates every rendered instance and leaves no stale value behind.
One source per fact
Phone, hours, service areas and every claim state resolve from a single config directory shared by both platforms. Business hours are derived from one list rather than stored twice — the changed-value test caught the duplicate when editing one left the footer and contact page stale.
Two platforms, one content model
The same config drives the Eleventy static build and a WordPress theme; 92 automated parity checks hold them together, including four that compare the bytes the WordPress server actually serves against the project's own stylesheets.
The service areas page: Denver-metro cities each with their own paragraph describing the local housing stock and the heating and cooling work that comes with it.

Measured

Measured performance

Lighthouse 13.4.1 on stellwood-hvac.pages.dev, 13 Sep 2026. Mobile rows use simulated mobile throttling; single run per route.

Lighthouse on the live demo
PagePerformanceAccessibilityLCPCLSTransfer
Home, mobile 100 100 1.60 s 0 160 KB
Home, desktop 100 100 0.34 s 0.001 160 KB
Services, mobile 100 100 1.29 s 0.013 153 KB
Service areas, mobile 100 100 1.44 s 0.013 114 KB
About, mobile 100 100 1.46 s 0 171 KB
Contact, mobile 100 100 1.43 s 0 113 KB

Also measured on production

  • axe-core 4.10.2: 0 violations across 32 runs — 9 routes at 390px and 1440px on the static build, 7 routes at both widths on WordPress, and one run with the mobile drawer open.
  • 0 horizontal overflow across 80 measurements: 10 viewport widths from 320 to 1920px, including the reflow widths equivalent to 200% and 400% browser zoom, on 8 routes.
  • 0 tap targets below 44 x 44. 0 PHP notices, warnings, deprecations or fatals across 12 WordPress routes and 5 form POSTs.
  • All 57 deployed files byte-identical to the built artifact, which carries the accessibility evidence from the audited build to the live origin.
  • Security headers present exactly once each on 12 sampled responses; the CSP was proved enforced in the browser, not just read from the header.

Why SEO is 66–69 and not 100

  • The only failing SEO audit on every route is is-crawlable, because the site sends noindex,nofollow in the response header, a meta tag and robots.txt. A business that does not exist must not appear in local search beside real contractors. Every other SEO audit passes.
  • Two of the three SEO deductions found at QA turned out to be real defects rather than the deliberate noindex — a relative canonical and a relative sitemap URL, both invalid — and were fixed.

Not claimed

  • Conversion uplift, customer outcomes, commercial results or client satisfaction: there is no client and no traffic.
  • Keyboard activation of the mobile menu button by Enter or Space is UNVERIFIED, not passed — the test harness cannot dispatch native button activation. Tab reach, focus ring, focus trap, Escape, scroll lock and focus return all pass on the live site.
  • WordPress performance on real hosting: the theme was validated only against a local runtime on SQLite, never on MySQL, a real host, or with real outbound mail.

Accessibility

Accessibility testing performed

What was tested, and how. Nothing here is a conformance certification.

  • axe-core 4.10.2 across 32 runs with 0 violations, including one run with the mobile drawer open — the only state in which the focus trap, the scroll lock and the overlay exist, and one a route-level sweep never reaches.
  • Lighthouse Accessibility 100 with zero failing audits on all six measured production routes.
  • Keyboard: every one of the first 14 tab stops had a visible focus indicator and matched :focus-visible. The drawer's focus trap holds in both directions, Escape closes it, and focus returns to the element that opened it.
  • Tap targets: 0 below 44 x 44 after the breadcrumb link was widened — its height had been fixed in an earlier phase and its width never checked.
  • Contrast: a primary-CTA focus ring measuring 1:1 (amber on amber) was found by measurement and replaced with an inset ink ring. The first fix used :where(), which carries zero specificity, so the section rule kept winning — it took a second measurement to catch.

Not done No screen-reader pass and no real-device testing. Keyboard activation of the menu button by Enter or Space could not be dispatched by the harness and is recorded as unverified.

Credits

Credits and disclosure

  • Concept project. Stellwood Heating & Air is a fictional business; it has no customers, credentials, reviews or trading history, and the site displays none.
  • Photography: AI-generated for this project (OpenAI GPT Image, model and date recorded per file in the asset manifest). No people are identifiable and no stock imagery is used.
  • Fonts: Space Grotesk and Inter (SIL Open Font License 1.1).
  • The name was chosen after a collision check, not before one: four earlier names were retired for colliding with real businesses. Stellwood is not claimed to be globally unique — a French real-estate company and a North Carolina property LLC share the spelling — only that no HVAC or adjacent comfort-trade business in the US or Canada uses it.

Contact

Talk about your site

Send a short note with your current website address and what you'd like it to do better.