From bc8c5931fc84c66832da1c3e7603ef3663c22522 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Thu, 18 Jun 2026 10:19:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(docs):=20SEO=20=E2=80=94=20sitemap,=20robo?= =?UTF-8?q?ts.txt,=20per-page=20og:=20tags,=20canonical=20URLs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit found that individual formula pages were SEO-perfect (title, description, content all in SSR HTML) but Google literally could not discover them: /browse/ renders its list client-side via Vue, so its static HTML has zero formula links, and there was no sitemap.xml or robots.txt. Fixes: 1. Enable VitePress built-in sitemap config — generates /sitemap.xml listing all 4,283 formula pages. VitePress sitemap doesn't auto-include the base path, so transformItems prepends SITE_PATH. 2. Add docs/public/robots.txt — allows all crawlers, references sitemap. 3. Add transformHead hook — emits per-page og:title, og:description, og:url, and from page frontmatter. Removed conflicting og:title/og:description from global head to avoid duplicates (verified: exactly 1 og:title per page). 4. Make og:image absolute (was '/logo-full.svg', now full URL). 5. Add TODO.style.md and V5_MIGRATION_PLAN.md to srcExclude — they were being built as public pages and appearing in the sitemap. Local-dev friendliness: SITE_URL comes from process.env.SITE_URL with 'http://localhost:5173' default. CI sets SITE_URL=https://www.fontist.org in docs.yml and links.yml env. Verified locally with default (localhost URLs) and override (SITE_URL=https://example.org → example.org URLs). --- .github/workflows/docs.yml | 5 ++++ .github/workflows/links.yml | 4 +++ docs/.vitepress/config.ts | 57 +++++++++++++++++++++++++++++++------ docs/public/robots.txt | 4 +++ 4 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 docs/public/robots.txt diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a3f9c6bef..f2a08cf95 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,6 +20,11 @@ permissions: pages: write id-token: write +# Used by VitePress config for og:url, canonical URLs, and sitemap hostname. +# VitePress reads process.env.SITE_URL; defaults to localhost for local dev. +env: + SITE_URL: https://www.fontist.org + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml index c39c78906..3d3a73394 100644 --- a/.github/workflows/links.yml +++ b/.github/workflows/links.yml @@ -19,6 +19,10 @@ permissions: contents: read pull-requests: write +# Match docs.yml — VitePress config reads SITE_URL for og:url / canonical / sitemap. +env: + SITE_URL: https://www.fontist.org + jobs: link_checker: runs-on: ubuntu-latest diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 0883afd86..30ff7fd78 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,5 +1,12 @@ import { defineConfig } from "vitepress"; +// Allow override via env (CI sets SITE_URL=https://www.fontist.org). +// Default to localhost so `vitepress dev` and local `npm run build` produce +// URLs that actually resolve during testing. +const SITE_ORIGIN = process.env.SITE_URL || "http://localhost:5173"; +const SITE_PATH = process.env.BASE_PATH || "/formulas/"; +const SITE_BASE = `${SITE_ORIGIN}${SITE_PATH}`; + // https://vitepress.dev/reference/site-config export default defineConfig({ lang: "en-US", @@ -16,6 +23,8 @@ export default defineConfig({ "testing-google-import.md", "GOOGLE_FONTS_IMPLEMENTATION.md", "TODO.revamp.md", + "TODO.style.md", + "V5_MIGRATION_PLAN.md", ], ignoreDeadLinks: true, @@ -85,18 +94,48 @@ export default defineConfig({ ], ["link", { rel: "manifest", href: "/site.webmanifest" }], ["meta", { property: "og:type", content: "website" }], - ["meta", { property: "og:title", content: "Fontist Formulas" }], - [ - "meta", - { - property: "og:description", - content: "Searchable index of all Fontist Formulas", - }, - ], - ["meta", { property: "og:image", content: "/logo-full.svg" }], + ["meta", { property: "og:image", content: `${SITE_BASE}logo-full.svg` }], ["meta", { name: "twitter:card", content: "summary_large_image" }], ], + // Generates /sitemap.xml at build time so crawlers can discover all + // 4,283 formula pages even though /browse/ renders its list client-side. + // VitePress sitemap doesn't auto-include the `base` path (/formulas/), + // so transformItems prepends SITE_PATH to every URL. + sitemap: { + hostname: SITE_ORIGIN, + transformItems(items) { + return items.map((item) => ({ + ...item, + url: `${SITE_PATH}${item.url}`.replace(/\/{2,}/g, "/"), + })); + }, + }, + + // Per-page SEO tags (og:title, og:description, og:url, canonical URL) + // sourced from page frontmatter. Global head only sets site-wide og:type + // and og:image — title/description/url must be per-page or scrapers see + // generic "Fontist Formulas" for every page. + transformHead(context) { + const pageData = context.pageData; + const title = + (pageData.frontmatter.title as string) || "Fontist Formulas"; + const description = + (pageData.frontmatter.description as string) || + "Searchable index of all Fontist Formulas"; + const canonicalPath = pageData.relativePath + .replace(/(index)?\.md$/, "") + .replace(/\\/g, "/"); + const canonicalURL = `${SITE_BASE}${canonicalPath}`; + + return [ + ["meta", { property: "og:title", content: title }], + ["meta", { property: "og:description", content: description }], + ["meta", { property: "og:url", content: canonicalURL }], + ["link", { rel: "canonical", href: canonicalURL }], + ]; + }, + themeConfig: { logo: "/logo-full.svg", siteTitle: false, diff --git a/docs/public/robots.txt b/docs/public/robots.txt new file mode 100644 index 000000000..550b98ede --- /dev/null +++ b/docs/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://www.fontist.org/formulas/sitemap.xml