Back to Projects

Bilingual Real Estate Site

A hand-written static site for a Quebec real estate broker: sixteen pages in two languages, no JavaScript, no build step, and a perfect Lighthouse run on a $0 hosting bill.

Category

Client Work

Released

September 2026

HTML CSS SEO Schema.org Accessibility Performance Netlify Client Work

Don Joyce sells houses around Lac-Brome, in the Eastern Townships. He needed a site that would show up when someone in Knowlton searched for a broker, work on a phone with two bars of signal, and read the same in French as in English. He didn’t need a web application.

So there’s no framework, no build step and no JavaScript. Sixteen hand-written HTML pages, one stylesheet, deployed as static files. The whole site is 1.4 MB including every image and both self-hosted fonts.


No JavaScript

The site has thirty-two <script> tags and none of them execute. They’re all application/ld+json, which is structured data for search engines rather than code. The Content Security Policy says so out loud:

Content-Security-Policy: default-src 'self'; script-src 'none'; object-src 'none';
                         base-uri 'none'; form-action 'none'; frame-ancestors 'self'

script-src 'none' means the browser refuses to run script on this origin at all. That’s not a hardening measure bolted on at the end. It’s a design constraint asserted at the boundary: if a dependency, an analytics vendor or a future me tries to add a tracker, the page breaks loudly instead of quietly shipping it to visitors.

What it buys is everything a small business site actually wants. Total blocking time is 0 ms because there’s nothing to block on. There is no dependency tree to audit, no supply chain to inherit, and nothing that goes stale between now and the next time anyone touches it.

The rest of the header set is the ordinary list, and it’s ordinary on purpose: HSTS for a year, nosniff, SAMEORIGIN framing, a referrer policy, and a permissions policy that turns off geolocation, camera, microphone and cohort tracking rather than leaving them available to a page that will never ask for them.


English and French

Every page exists as /en/ and /fr/, and the French URLs are French: analyse-de-marche, not market-analysis-fr. Each page declares all three hreflang alternates including x-default, so Google serves a Quebec searcher the French page and doesn’t treat the pair as duplicate content.

This is the part clients most often get sold badly. A translation widget bolted onto an English site produces one indexable page and a machine translation that a French speaker in Quebec can identify in about four words. Two native pages produce two indexable pages, in a market where a good share of the searches are in French.


Structured data

Search engines can read prose, but they’ll act on markup. Every page carries JSON-LD describing what it is:

TypeWhat it does
RealEstateAgent, Person, OrganizationIdentifies the broker, the brokerage and their relationship
PostalAddressFeeds the local pack, where “real estate broker near me” is answered
FAQPage with eleven Q&A pairsEligible to render as expandable answers directly in results
Article on seven pagesMarks the market-analysis writing as editorial, not sales copy
BreadcrumbList on seven pagesPuts a readable path in the result instead of a raw URL
Place twenty timesNames the specific towns served, which is how “Sutton” or “Bolton-Est” gets matched

There’s also an llms.txt at the root, which is a plain-language summary of who Don is and what the site covers, aimed at language-model crawlers rather than search engines. It’s a young convention and it may not amount to anything. It’s also a text file that took an hour, on a site where a growing share of “who’s a broker in Knowlton” questions now get asked of a chatbot instead of a search bar.


Performance

Lighthouse 13.4.1, run against production on 2 September 2026:

MobileDesktop
Performance99100
Accessibility100100
Best Practices100100
SEO100100
Largest Contentful Paint1.4 s0.3 s
Total Blocking Time0 ms0 ms
Cumulative Layout Shift00.006

Perfect scores on a sixteen-page static site aren’t an achievement to boast about. Static HTML with no scripts starts near the ceiling and the work is not falling off it: subset the fonts and self-host them, size every image, reserve layout space so nothing shifts, and decline the things that would cost more than they return.


Accessibility

The site scores 100. Getting there took one fix; the part worth writing down is what the score didn’t cover.

Lighthouse reported 95, from a single failing contrast rule on the homepage that missed the WCAG AA threshold of 4.5:1 by 0.03. Fixing that one rule would have produced the 100, and 100 is what most people ship.

Instead I read every color declaration in the stylesheet against its background by hand, and found three more failures of the same kind that Lighthouse structurally cannot see. One was on a page the run didn’t scan. Two were in :hover states, which the audit engine never evaluates because it only ever measures the resting page.

The worst of them was the footer link hover: brand red on the navy footer at 2.87:1, effectively invisible, and it would have survived every automated run forever.

RuleWasNowFound by
.reference-card__translation2.855.10Lighthouse
.intro__cta4.475.58Lighthouse
.market-table .is-down4.475.58Hand audit
.faq__answer a:hover4.475.58Hand audit
.footer__section a:hover2.876.02Hand audit

A green audit means the checks that ran, passed. Automated tools measure the resting page, so hover, focus and active states go unmeasured, and any page the crawl didn’t reach goes unmeasured too.


Caching

Images and fonts are served immutable with a one-year lifetime, HTML with must-revalidate so a correction to a listing or a phone number goes live immediately. That split is what makes a repeat visit load instantly on cellular while keeping the operator’s ability to fix a typo now rather than after a cache expires.

The tradeoff is documented in the header file itself, because it’s the kind of thing that bites whoever touches it next:

because these are immutable for a year, a REPLACEMENT image must ship under a NEW filename or returning visitors will keep the old one indefinitely

Print materials point back to the site through QR codes, generated separately for the English and French market pages so a scan lands in the language the card was printed in. Redirects are checked by a script rather than by hand, because thirteen rules is already more than anyone reliably remembers.


Scope

This is a small site. It has no database, no listings feed, no search, no user accounts, and it deliberately doesn’t need them. Royal LePage already runs the inventory system, and rebuilding that badly would have made the site worse.

The work went into details a visitor never sees itemised: the contrast ratio no automated tool would have caught, the cache header with its own failure mode written down beside it, French URLs that are actually in French. A broker’s site has to load on a phone, on the first visit, for someone deciding whether to trust him with the largest transaction of their life.