Skip to content

SEO: launch fixes across metadata, brand rasters and the hero preload - #38

Merged
Abuubkar merged 4 commits into
devfrom
seo/launch-fixes
Sep 8, 2026
Merged

Abuubkar merged 4 commits into
devfrom
seo/launch-fixes

Conversation

@Abuubkar

@Abuubkar Abuubkar commented Sep 8, 2026

Copy link
Copy Markdown
Owner

The code half of the pre-launch SEO list. Baseline was already good — Lighthouse
SEO 100, accessibility 100, and 0 placeholders remaining — so this is a set of
specific gaps rather than a rescue. Four commits, each standing alone.

Non-canonical hostnames — _headers

execil.<sub>.workers.dev serves the whole site and stays enabled by choice
(#34); the preview aliases from non-production branch builds live on the same
domain. Both are public, and robots.txt is Allow: /.

One _headers rule covers all of them:

https://:worker.:subdomain.workers.dev/*
  X-Robots-Tag: noindex, nofollow

Absolute-URL rules match host-first and :name is a one-label placeholder, so
this catches execil.<sub>.workers.dev and every <alias>-execil.<sub> preview
alike. No Worker change and no run_worker_first — these are asset responses,
which every prerendered page is. Wrangler confirms: Parsed 4 valid header rules.

nosniff and Referrer-Policy come along too. Deliberately not HSTS — that
binds the apex and every subdomain for as long as browsers remember it, so it
wants your decision, not a commit.

Organization.logo and the OG card

logo.png is new. Organization.logo pointed at og.png, a 1200×630 social
card with a headline across it; Google reads that field for the knowledge panel
and wants the logo itself, on white, 112px minimum in both axes. Now the
wordmark on white at 1024×406.

The OG card's own lockup was still the placeholder teal-square mark. Both now
draw the real wordmark from the same geometry as public/brand/cross-dot/.

Two notes for whoever next runs scripts/og-image.py, both recorded in the
file: glyph pen positions are listed rather than laid out, because this Pillow
has no Raqm and its own layout drops the kerning — it would set the word 25
units wide and land the badge 78 units late. And the knockout ring samples the
canvas before any letter is drawn; sampling after put it on the tittle and the
ring came out black.

Description, titles, 404, sitemap

The meta description was 215 characters against Google's ~155, so results cut it
mid-sentence. Now exactly 155, with the same claims.

The 404 inherited the home page's title and its index, follow. This one
needed a restructure, and it is the part worth reviewing.
A not-found page
cannot set a route head — nothing matched, so there is no route to hang one on.
React 19 hoists a rendered <title> into <head>, but the root head's title is
emitted first and wins, which left the document carrying two titles and two
contradictory robots metas. My first attempt shipped exactly that; the build
output is what caught it.

So the title moved to the home route and the root keeps none — every route
already set its own except home. The robots meta is deleted rather than moved:
index, follow is the crawler default, so it existed only to collide.

Verified on the built site:

path status title robots
/ 200 Execil | Medical Billing & RCM… none (default)
/privacy 200 Privacy Policy | Execil none (default)
/does-not-exist 404 That page does not exist | Execil noindex, follow

One title per page, and the 404 status is unchanged — that is what actually
keeps it out of the index; the meta is for the tab and for a crawler reading
markup before status.

Sitemap gains lastmod, the one field there Google reads, dated from when each
page's source last changed.

Hero preload and FAQPage

The picture is the LCP element on narrow screens, where it sits above the copy.
Markup alone is not enough: the preload scanner finds the <img> only once the
CSS that sizes it has arrived, and fetchpriority cannot start a request nobody
has discovered. Verified on the built site at 375px — one request for the
hero, initiatorType: "link", the 900w candidate, and no "preloaded but not
used" warning.

That needs identical src, srcset and sizes in two files, so they moved to
src/hero-image.ts. A mismatch there is not a no-op: the browser resolves a
different candidate and fetches the picture twice.

FAQPage covers the eight questions, on the home route rather than beside
organizationLd in the root, because they render on one page. Expectations
first: since 2023 Google shows FAQ rich results only for government and health
authority sites, and Execil is neither. It is valid, cheap, and read by other
consumers of structured data — the accordion is not the reason to have it.

Checked

pnpm check and pnpm build pass. Lighthouse against the production build:
SEO 100, best-practices 100. Both JSON-LD blocks parse; Organization.logo
resolves to /logo.png.

Best-practices reads 81 on the live site, not because of anything here — three
deprecation warnings, all from Cloudflare's Bot Fight Mode script at
/cdn-cgi/challenge-platform/…. Turning that off is a dashboard change.

Still yours, not code

Sitemap submission to Search Console (the open item on #34), a www → apex
redirect rule, and the Google Business Profile decision.

The Worker's own workers.dev subdomain serves the whole site and stays
enabled by choice (#34), and the preview aliases from non-production
branch builds live on the same domain. Both are public, and robots.txt
says Allow: /. The prerendered canonical already points at execil.net,
which Google usually honours; X-Robots-Tag is the signal that does not
depend on it being honoured.

_headers matches host-first when the rule is an absolute URL, and `:name`
is a placeholder for one label, so a single rule covers
execil.<subdomain>.workers.dev and every <alias>-execil.<subdomain>
preview. No Worker change and no run_worker_first: these are asset
responses, which is what every prerendered page is.

nosniff and Referrer-Policy come along while the file is open. Not HSTS —
that one binds the apex and every subdomain for as long as browsers
remember it, so it is a decision, not a commit.
logo.png is new. Organization.logo pointed at og.png, which is a
1200x630 social card with a headline across it — Google reads that field
for the knowledge panel and wants the logo itself, on white, 112px
minimum in both axes. The new file is the wordmark on white at 1024x406.

The OG card's lockup was still the placeholder mark, a teal square with a
green square inside it. Both now draw the real wordmark from the same
geometry as public/brand/cross-dot/.

Two things about drawing it in Pillow. Glyph pen positions are listed
rather than laid out, because this Pillow has no Raqm: its own layout
drops the kerning, which would set the word 25 units wide and land the
badge 78 units late. And the knockout ring samples the canvas before a
letter is drawn — after the text, that point is the tittle, and the ring
came out black.
The meta description was 215 characters against Google's ~155, so it was
being cut mid-sentence in results. Now 155.

The 404 inherited the home page's title and its `index, follow`. Its 404
status is what actually keeps it out of the index, but the tab said
"Execil | Medical Billing & RCM" and a crawler reading markup before
status saw an invitation.

It cannot set a route head — nothing matched, so there is no route to
hang one on. React 19 hoists a rendered <title> into <head>, but the root
head's title is emitted first and wins, which left the document with two.
So the title moves to the home route, and the root keeps none. The robots
meta is deleted outright rather than moved: `index, follow` is the
default, so it only existed to collide.

Sitemap gains lastmod, the one field there Google reads.
The picture is the LCP element on narrow screens, where it sits above the
copy. Markup alone is not enough there: the preload scanner finds the
<img> only once the CSS that sizes it has arrived, and fetchpriority
cannot start a request nobody has discovered yet. Verified on the built
site at 375px — one request for the hero, initiator "link", the 900w
candidate, and no "preloaded but not used".

That needs the same src, srcset and sizes in two files, so they move to
hero-image.ts. A mismatch there is not a no-op: the browser resolves a
different candidate and downloads the picture twice.

FAQPage covers the eight questions. Expectations first: since 2023 Google
shows FAQ rich results only for government and health *authority* sites,
and Execil is neither. It is valid, cheap, and read by other consumers of
structured data. It sits on the home route rather than beside
organizationLd in the root, because the questions render on one page.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
execil 446f15c Commit Preview URL

Branch Preview URL
Sep 08 2026, 12:56 AM

@Abuubkar
Abuubkar merged commit 446f15c into dev Sep 8, 2026
2 checks passed
@Abuubkar
Abuubkar deleted the seo/launch-fixes branch September 8, 2026 00:55
Abuubkar added a commit that referenced this pull request Sep 13, 2026
…ts.txt (#44)

The code half of pre-launch SEO (#38) covered the search crawlers. This covers
the answer engines, which read different things.

Structured data was two disconnected islands: Organization from the root, a
FAQPage from the home route, neither carrying an @id, so nothing referred to
anything. Now every page emits Organization and WebSite with stable @ids, and
each route adds its own nodes referencing them — the home page a WebPage, the
Service with the Services section as a nested OfferCatalog, and the FAQ; the
legal pages a WebPage with dateModified and a BreadcrumbList. Consumers merge a
graph by @id across script tags, so the publisher is described once.

Every value is derived from messages.json, including Organization.knowsAbout,
which is the specialty list — the thing an engine asked "who bills for
dermatology practices" has to match on.

public/llms.txt restates the page as plain text an answer engine can quote:
services, specialties, the comparison table, the FAQ. Generated by
scripts/llms-txt.mjs and run from `build`, because a second hand-kept copy of
these facts is the same drift the brand assets already taught us about. The
output is committed so a copy change shows up in the diff.

robots.txt names the AI crawlers. Cloudflare's managed robots.txt is prepended
at the edge and disallows GPTBot, ClaudeBot, Google-Extended and five others by
default; the real fix is the dashboard toggle, but repeating a user-agent merges
the groups and an equal-specificity Allow wins the tie-break.

FAQ answers get an id each, so one answer can be cited on its own.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant