Skip to content
This repository was archived by the owner on Oct 13, 2026. It is now read-only.

feat(seo): Open Graph card, trailing-slash links, search-language titles - #31

Merged
zz-plant merged 5 commits into
mainfrom
seo/og-image-and-retitles
Oct 9, 2026
Merged

zz-plant merged 5 commits into
mainfrom
seo/og-image-and-retitles

Conversation

@zz-plant

@zz-plant zz-plant commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #30. A crawl of the published site found three problems:

  • no page had an og:image;
  • seven internal links 301-redirected;
  • three page titles did not use the words people search for.

This PR fixes all three. It also makes CI run lint and tests, so the redirect check gates merges. For those to run on Linux, the lockfile had to record the Linux builds of three native packages.

Changes

Open Graph card (social-card.mjs, build.mjs)

  • The build renders dist/social-card.png, 1200×630, about 49 KB. The PNG is a build output, not a committed file; after deploy it is at https://refract-org.github.io/refract-docs/social-card.png.
  • Layout. On the --bg-deep background (#07090f):
    • the site's triangle icon in --accent teal, then "Refract" in bold white;
    • the positioning line in --text, word-wrapped to three lines;
    • a --border-strong rule, then refract-org.github.io/refract-docs in --text-muted.
  • The card is drawn from SITE_NAME and SITE_DESCRIPTION, so it changes when the positioning line changes. A description too long for the card fails the build instead of being clipped.
  • Every page's head now has:
    <meta property="og:image" content="https://refract-org.github.io/refract-docs/social-card.png">
    <meta property="og:image:width" content="1200">
    <meta property="og:image:height" content="630">
    <meta property="og:image:alt" content="Refract. A deterministic observation engine for revision histories. It reads a page&#39;s edits and emits a typed event for each change.">
    <meta name="twitter:card" content="summary_large_image">
  • Font. The site's stack starts with system-ui, which resolves to the reader's OS face (SF Pro, Segoe UI); neither may be redistributed in an image. The card uses Roboto, the stack's OFL-licensed face. It is loaded from @expo-google-fonts/roboto, and system fonts are never loaded, so every machine renders the same PNG.
  • Rendering. @resvg/resvg-js turns an SVG into the PNG in about 70 ms per build. Both new packages are devDependencies.
  • No per-page titles on the card. That would mean 53 PNGs (about 2.6 MB) and about 3 s more per build. The build runs three times per npm test. Platforms also print og:title beside the image.

Trailing-slash links (build.mjs rewriteLink)

GitHub Pages serves dist/<slug>/index.html at <slug>/ and 301-redirects <slug>. The resolver added the slash only to links ending in .md, so these seven links redirected:

Page Link
mcp [CLI reference](cli)
schema, sdk (./cli)
downstream (schema#version-compatibility)
index (python-sdk), (visualizer)
visualizer (tutorials/refract-ui)
  • rewriteLink now resolves every relative link to a page (cli, ./cli, cli/, cli.md, ../index.md) to the page's trailing-slash URL.
  • A path whose last segment has a file extension (.svg, .png, .ipynb) is an asset and keeps its form.
  • Links with any URL scheme, such as mailto:, are left as written. Before, only http(s) links were.
  • The markdown sources keep their slash-less links, so the built-site test still exercises the resolver on them.

Retitles

Each <title> comes from the page's H1, so these H1s changed:

Page Before After
/mcp/ MCP — Model Context Protocol — Refract Wikipedia & MediaWiki revision-history MCP server — Refract
/tutorials/wikipedia-history/ Tutorial: Track changes on a Wikipedia page — Refract Track changes to a Wikipedia article over time — Refract
/compare/ Refract vs. Wikipedia's page history — Refract Wikipedia history tools compared — Refract
  • MCP. The server's analyze and export tools take an api parameter, the base URL of any MediaWiki API. That makes "Wikipedia & MediaWiki" accurate. The description is unchanged: it still comes from the first paragraph.
  • Tutorial. The other 18 tutorials keep their "Tutorial:" prefix. This one drops it so the search words come first. The sidebar still lists it under Tutorials.
  • Old H1 anchors. These change, but no page links to them.
  • Compare. The intro now names the tools the page compares and the question each answers. Its first sentence is the new meta description: "This page compares tools for reading a Wikipedia article's history: the built-in page history, XTools, WikiBlame, Who Wrote That? and Refract."

Corrections on the compare page

Each claim about another tool was checked against that tool's documentation, and the page links it.

Claim Was Now Source
Page history: find when a sentence first appeared Manual — search each revision Not built in. On English Wikipedia the history page's "Find addition/removal" link opens WikiBlame Help:Page history
Page history: detect edit wars Manual Partly. Undos, rollbacks and manual reverts are tagged, and so are the edits they revert Manual:Reverts
Page history: hash an export No — screenshots are the only record Per revision: a permanent link, and an API SHA-1 of the content API:Revisions
Page history: automated monitoring No Watchlist, with optional email when a watched page changes Help:Watchlist
WhoColor / WikiBlame One row: "visual diff highlighting" Two rows. WikiBlame searches revisions (binary or linear) for where a text was inserted or removed. WhoColor colors text by author WikiBlame manual, WhoColor
WikiWho Editor-level authorship attribution Token-level provenance for about 70 Wikipedias: who added, removed or reinserted each token WikiWho
Wikimedia Enterprise Commercial licensing Snapshot, On-demand and Realtime APIs; free access and paid plans. Customers buy a service, not a licence Enterprise FAQ
XTools, Who Wrote That? Absent New rows XTools, Who Wrote That?

The closing line no longer says the page history lacks hashed exports and monitoring.

CI and lockfile

  • ci.yml ran only npm run build. It now runs npm run lint and npm test first.
  • Lockfile. The first CI run failed: Cannot find module '@biomejs/cli-linux-x64/biome'. On main, package-lock.json lists only the darwin-arm64 builds of @biomejs/biome, rolldown (Vitest's bundler) and lightningcss. So npm ci on Linux installs no Biome or Rolldown binary, and lint and tests cannot run there.
  • Fix. Those entries were re-resolved with npm install --package-lock-only, and the lockfile now lists every platform's build.
    • Biome 2.5.15 and Rolldown 1.0.2 keep their versions.
    • lightningcss moves from 1.32.0 to 1.33.0 (released 2026-07-20), within Vite's ^1.32.0 range.
    • No other package version changes.

Tests

28 new tests; 103 in total, up from 75.

  • Redirects (tests/links.test.ts). Fails if any internal link in the built site points at a directory without the trailing slash. Absolute links to the published site, such as the canonical URL, count as internal.
  • rewriteLink: unit tests for page, asset and external links. build.mjs now builds only when run as a script, so tests can import it.
  • Head tags (tests/seo.test.ts): og:image, its width, height and alt, and twitter:card on every page.
  • Card: the published PNG is 1200×630 and over 20 KB, which a blank rectangle is not; lines wrap within the card; SVG text is escaped; a description too long for the card throws.
  • Escaping: the <title> element is now escaped (the MCP title contains &); every & in the head outside scripts starts a character reference.
  • Mutation checks: each of these makes a test fail:
    • the old resolver (9 failures);
    • an unescaped <title>;
    • twitter:card set to summary;
    • a relative og:image;
    • no card written.

Verification

  • npm run lint: no errors. The 3 warnings are the existing noDescendingSpecificity ones in assets/style.css.
  • npm test: 4 files, 103 tests pass.
  • CI on this branch runs npm ci, lint, tests and build on Ubuntu, and all pass.
  • npm run build: 53 pages, plus dist/social-card.png (PNG, 1200×630, 8-bit RGBA, 49,509 bytes). Rebuilding produces the same bytes.
  • A separate scan of the built site checked 3,279 internal href, src and content URLs. None redirects or is missing.
  • On the live site, /refract-docs/cli returns 301 to /refract-docs/cli/.

Not in this PR

  • Unused images. The 18 placeholder refract-docs-*.png scene images in docs/assets/ are still there, along with source.svg, scripts/generate-scenes.ts and check-assets.ts. No page references them, and removing them is a separate decision.
  • MCP intro. The first paragraph, which is also the description, does not mention Wikipedia or MediaWiki. The new title does.
  • AI-evaluation section on the compare page. It says Refract "has no direct competitor". That is not a comparison of history tools, and this PR does not check it.

🤖 Generated with Claude Code

zz-plant and others added 5 commits October 8, 2026 16:31
GitHub Pages serves dist/<slug>/index.html at <slug>/ and 301-redirects
the slash-less form. The markdown link resolver only added the slash when
a link ended in .md, so seven built links redirected: `(cli)` and `(./cli)`
in mcp, schema and sdk, `(schema#version-compatibility)` in downstream,
`(python-sdk)` and `(visualizer)` on the home page, and
`(tutorials/refract-ui)` in visualizer.

rewriteLink now resolves every relative link to a page (cli, ./cli, cli/,
cli.md, ../index.md) to the page's trailing-slash URL. A path whose last
segment has a file extension (.svg, .png, .ipynb) is an asset and keeps
its form. Links with any URL scheme, such as mailto:, are left alone;
before, only http(s) links were.

Tests:
- a built-site test fails if any internal link, including the canonical
  URL, points at a directory without the trailing slash;
- unit tests cover rewriteLink's page, asset and external cases. build.mjs
  now builds only when run as a script, so tests can import it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No page had an og:image, and the PNGs in docs/assets are placeholders.
The build now renders dist/social-card.png: the site icon and "Refract",
the positioning line, and the site address, in the colors from
assets/style.css. It is drawn from SITE_NAME and SITE_DESCRIPTION, so the
card changes when the positioning line does.

Every page's head gets og:image (absolute URL), og:image:width,
og:image:height, og:image:alt, and twitter:card summary_large_image.

social-card.mjs renders an SVG with @resvg/resvg-js. The site's font
stack starts with system-ui, which resolves to the reader's OS face (SF
Pro, Segoe UI) and cannot be redistributed in an image, so the card uses
Roboto, the stack's OFL-licensed face, from @expo-google-fonts/roboto.
System fonts are never loaded, so every machine renders the same PNG.
The description is word-wrapped by measuring it in that font; a
description too long for the card fails the build instead of clipping.

Tests cover the head tags on every page, the published PNG's size, line
wrapping, SVG escaping, and the overflow error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI only ran the build, so the link, SEO and docs tests ran only where a
contributor ran them by hand. It now runs `npm run lint` and `npm test`
before the build, which makes the redirect check a merge gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lockfile listed only the darwin-arm64 builds of @biomejs/biome,
rolldown (Vitest's bundler) and lightningcss. `npm ci` on Linux therefore
installed no Biome or Rolldown binary, and `npm run lint` and `npm test`
failed there. CI passed only because it ran nothing but the build.

The entries for those three packages were re-resolved with
`npm install --package-lock-only`. Biome (2.5.15) and Rolldown (1.0.2)
keep their versions and gain their other platform builds. lightningcss
moves from 1.32.0 to 1.33.0, within Vite's ^1.32.0 range.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Titles come from each page's H1, so these H1s now use the words people
search for:
- mcp: "MCP — Model Context Protocol" becomes "Wikipedia & MediaWiki
  revision-history MCP server". The server's tools analyze any MediaWiki
  page's revisions.
- tutorials/wikipedia-history: "Tutorial: Track changes on a Wikipedia
  page" becomes "Track changes to a Wikipedia article over time".
- compare: "Refract vs. Wikipedia's page history" becomes "Wikipedia
  history tools compared". The intro names the tools the page compares
  and the question each answers.

Each claim about another tool was checked against that tool's
documentation, and the page now links it:
- page history: the history page links WikiBlame for finding text;
  reverts are tagged; each revision has a permanent link and an API
  SHA-1; the watchlist can send email. The page had said "manual",
  "screenshots are the only record" and "no" for these;
- XTools and Who Wrote That? rows added;
- WikiBlame split from WhoColor: it searches revisions for the one that
  added or removed a text, not "visual diff highlighting";
- WikiWho is token-level provenance, including deletions and
  reinsertions;
- Wikimedia Enterprise sells access to a service, not a licence, and has
  free access.

The <title> element is now escaped like the other head values, since the
MCP title contains "&".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zz-plant
zz-plant marked this pull request as ready for review October 8, 2026 21:53
@zz-plant
zz-plant merged commit 5c27a16 into main Oct 9, 2026
1 check passed
@zz-plant
zz-plant deleted the seo/og-image-and-retitles branch October 9, 2026 00:10
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant