Head metadata and link previews¶
What someone sees before they open the page. Get this wrong and a well-built report lands in a channel as an anonymous grey file card.
The ordering rule¶
Content first, provenance second — in the markup, and in what each tag says.
The <title> and description exist to answer "what will I learn if I open
this?" in the words the reader would use. How the page was made is real
information, but it belongs in author, generator and dcterms.modified,
below the content tags, where a curious engineer will still find it.
A description that opens "Generated status page for…" has spent its most valuable words on the tooling. Open with the subject instead:
Moving ABT TMS off a SQLite file on one laptop to managed Postgres on Databricks Lakebase, and GDT market data off Microsoft Fabric — progress, who can touch the data at each step, and what is still undecided.
The verifier fails a description that opens with build vocabulary.
Know what actually unfurls¶
This is the part people get wrong, so be clear with whoever asked:
| How the page is shared | What Teams / Slack show |
|---|---|
.html attached to a chat or channel |
A syntax-highlighted code snippet of the source, with line numbers. Both clients classify .html as a text file; neither will render it. Open Graph tags are never read, because nothing fetches the file. |
.pdf attached |
A real page thumbnail and an inline reader. |
| Link to a hosted copy (SharePoint, ADO Pages, any http(s) host) | A proper unfurl built from og:title, og:description, and og:image if present. |
Link to a file:// path |
Nothing. Nobody else can resolve it. |
The first row surprises people, so say it plainly when you hand the page over: dropping the HTML into Slack shows the raw markup, not the page. That is deliberate on the platforms' side — rendering arbitrary uploaded HTML in-app would be an XSS vector — and no meta tag or change to the file alters it.
There are only two real answers, and which one you want depends on the reader:
- Host it. Then the link unfurls, the OG tags do their job, and the reader gets the interactive page — themes, the section jump, the collapsible regeneration block.
- Ship the PDF alongside.
scripts/build_pdf.mjsrenders the built page to A4 with a print stylesheet that drops the interactive chrome and keeps figures off page breaks. It previews properly in every chat client and survives being emailed. Send the HTML for people who will interact with it, the PDF for people who will read it once.
Filenames still matter for both: an attached file is identified by its name.
ABT Database Roadmap.pdf, not report_v3_final.pdf. The build's -o argument
sets it, so it is a deliberate choice.
Add the Open Graph tags regardless — they cost nothing, and the moment anyone puts the file behind a URL the preview is already correct.
The tags¶
| Tag | Why |
|---|---|
<title> |
Tab, bookmark, and unfurl fallback. Under 70 chars. A name, not a sentence. |
description |
The one that matters. 70–300 chars. Subject first. |
og:type, og:title, og:description |
The unfurl. og:description must match description exactly — two versions drift. |
og:site_name, og:locale |
Context in the preview card; en_NZ. |
twitter:card = summary |
Slack reads Twitter card tags as a fallback. |
author, dcterms.modified |
Who owns it, and as-at when. Use the date the content was derived, not the build date. |
generator |
Names this skill, so the next person can find the method. |
robots = noindex, nofollow |
An internal programme page that ends up on an indexable host should not be crawled. |
theme-color (light + dark) |
Browser chrome on mobile. |
link rel="icon" |
See below. |
og:image¶
A preview card with an image gets far more attention than one without — but
og:image must be an absolute http(s) URL. A data: URI will not work:
crawlers do not render them, and Slack rejects them outright. So it is only
worth adding once the page has a hosting home, and the image has to be hosted
alongside it. Skip it until then rather than shipping a broken tag.
Favicon¶
Embed it as a data: URI — a linked .ico breaks the moment the file moves,
which defeats the point of a standalone page.
Draw the icon as SVG paths, not as an emoji. An emoji favicon
(<text>�</text> inside an SVG) depends on the viewer's operating system
having a colour emoji font. Windows and macOS do; a stripped Linux container or
a locked-down VDI image may not, and there it renders as tofu — an empty
rectangle. A drawn path renders identically everywhere.
The template ships a memo glyph — a ruled page with a pencil — drawn as paths.
Swap it for something that suits the page, but keep it a data: URI.
Keep it legible at 16px: bold strokes, two or three shapes at most, no fine
detail. Render it at 80, 32 and 16px and look before shipping. If the icon is a
single-colour line drawing rather than a filled glyph, give it a
prefers-color-scheme rule inside the SVG so it does not disappear into a dark
browser tab:
<style>
path{fill:none;stroke:#1f4e79;stroke-width:9;stroke-linecap:round}
@media (prefers-color-scheme:dark){path{stroke:#7fb0ff}}
</style>
What the verifier checks¶
scripts/verify_page.mjs fails the build on: a missing or over-long title, a
description that is missing, too thin, too long, a repeat of the title, or
opening with build vocabulary; a missing og:title or og:description; an
og:description that has drifted from description; and a favicon that is
missing, does not decode, or is not embedded.