Skip to content

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:

  1. 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.
  2. Ship the PDF alongside. scripts/build_pdf.mjs renders 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.