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

feat(seo): canonical URLs, Open Graph tags, JSON-LD and sitemap.xml - #29

Merged
zz-plant merged 1 commit into
mainfrom
seo/canonical-og-sitemap
Oct 8, 2026
Merged

zz-plant merged 1 commit into
mainfrom
seo/canonical-og-sitemap

Conversation

@zz-plant

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

Copy link
Copy Markdown
Contributor

Summary

The 53 published pages had no canonical URL, Open Graph or Twitter tags, or structured data, and the site had no sitemap. build.mjs now emits all four. Titles and the meta description are unchanged.

Changes

  • Canonical URL on every page, under https://refract-org.github.io/refract-docs/. It uses the trailing-slash form because GitHub Pages serves dist/<slug>/index.html at <slug>/ and 301-redirects <slug> to it. The home page canonical is /refract-docs/, which also consolidates /refract-docs/index.html (that URL returns 200 too).
  • Open Graph and Twitter: og:title, og:description, og:url, og:type (website on home, article on doc pages), og:site_name ("Refract") and twitter:card = summary. og:title is the page H1 without the " — Refract" suffix, since og:site_name carries the name. og:description repeats the existing meta description.
  • JSON-LD, one @graph per page:
    • home: WebSite, linked through about to a SoftwareSourceCode node whose codeRepository is https://github.com/refract-org/refract;
    • doc pages: TechArticle (headline, description, url, isPartOf the WebSite) and a two-item BreadcrumbList (Home, then the page's nav label). The nav groups ("Reference", "Tutorials"…) have no pages of their own, and a breadcrumb item needs a URL, so the trail has no middle level.
  • dist/sitemap.xml lists all 53 pages, sorted.
  • SITE_URL (default https://refract-org.github.io/refract-docs/) drives the absolute URLs, kept separate from BASE. A local build with a different BASE still points canonicals at production.
  • tests/seo.test.ts builds the site and checks every page for one canonical matching its served address, the OG/Twitter values, attribute escaping (compare has an apostrophe in its title), JSON-LD that parses with the expected nodes, and a sitemap that lists each page exactly once with no <lastmod>.

Verification

  • npm run lint: no errors. The 3 warnings are the existing noDescendingSpecificity ones in assets/style.css.
  • npm test: 4 files, 70 tests pass (63 before this PR).
  • npm run build: 53 pages built. xmllint --noout dist/sitemap.xml passes, and the sitemap has 53 <loc> entries.
  • Every generated HTML file has exactly one JSON-LD block, and all of them parse. That gives 52 TechArticle, 52 BreadcrumbList, 1 WebSite and 1 SoftwareSourceCode node.
  • Mutation checks: building with a different SITE_URL, or dropping the trailing slash from page URLs, fails the new tests.

Decisions left to the owner

  • Positioning lines differ. This PR leaves all of them as they are:
    • home page title/H1: "Refract: deterministic observation layer for model evaluation";
    • meta description (every page): "Refract — the open claim-history layer for public knowledge. …";
    • refract-org/refract GitHub description: "Open infrastructure for agent-readable knowledge change — turns source histories into replayable semantic change events";
    • AGENTS.md: "deterministic observation engine".
  • One description for all 53 pages. As a result, search snippets and link cards read the same everywhere. getExcerpt() already builds a per-page excerpt for the search index and could supply page descriptions.
  • No og:image. There is no raster logo. Facebook, X and LinkedIn don't accept the SVG concept diagrams as og:image. The generated PNGs in docs/assets/ were rejected too: refract-docs-og.png, -unfurl.png and -square.png are mostly empty dark rectangles with small "refract-docs" text, and -github-preview.png is filled with placeholder "documented route" labels. A purpose-made 1200×630 card would allow og:image with summary_large_image.
  • A title contains backticks. "Tutorial: Cross-wiki comparison with `refract diff`" shows the literal backticks in <title>, and now in og:title.

Not done

  • No <lastmod>. deploy.yml uses actions/checkout@v6 with the default fetch-depth: 1, so git log would give every page the date of the latest commit. To add it later, set fetch-depth: 0 in the deploy job and read git log -1 --format=%cI -- docs/<file> per page.
  • No robots.txt. Crawlers only read it at the host root (https://refract-org.github.io/robots.txt), so a copy under /refract-docs/ would be ignored.

After merge

Submit https://refract-org.github.io/refract-docs/sitemap.xml in Google Search Console, under a URL-prefix property for https://refract-org.github.io/refract-docs/.

🤖 Generated with Claude Code

Every built page now carries a canonical URL at the trailing-slash address
GitHub Pages serves it from, Open Graph and twitter:card tags, and one
JSON-LD block: WebSite plus SoftwareSourceCode on the home page, TechArticle
plus BreadcrumbList on doc pages. The build also writes dist/sitemap.xml
listing every page. It has no <lastmod> because the deploy checkout is
shallow and cannot date pages from git history.

Titles and the meta description are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zz-plant
zz-plant merged commit 9549bd4 into main Oct 8, 2026
1 check passed
zz-plant added a commit that referenced this pull request Oct 8, 2026
Brings in the squash merge of #29 so the follow-up PR only shows new changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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