Skip to content

feat(seo): structured data and share images per page, sitemap dates from git, every product in the changelog - #16

Merged
fylorn merged 4 commits into
mainfrom
seo/website-technical
Sep 25, 2026
Merged

fylorn merged 4 commits into
mainfrom
seo/website-technical

Conversation

@fylorn

@fylorn fylorn commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Technical SEO for thinkwat.ch now that ThinkWatch Lite ships on macOS, Windows and Linux and connects to a remote core. This PR does not change page prose; a later pass rewrites the copy.

Structured data

  • Enterprise only where it belongs. Base.astro injected the Enterprise SoftwareApplication on every page without noJsonLd (/thinkwatch, /changelog, 404). That default and the noJsonLd switch are gone. /thinkwatch and the home page describe Enterprise. /license and /changelog carry WebPage and BreadcrumbList, the docs carry BreadcrumbList and TechArticle, and the 404 page has only the Organization.
  • One description per product. src/lib/structured-data.ts builds the JSON-LD for each product's own page and for the home page, so the two agree:
    • ThinkWatch Lite: a SoftwareApplication with operatingSystem "macOS, Windows, Linux", processorRequirements, the softwareVersion and installer downloadUrls of the latest release (read at build time, as for the download buttons), installUrl (…/lite/#install), the six screenshots with their captions, inLanguage en and zh-CN (the interface languages), a featureList taken from the page's feature titles, the MIT license and a price of 0. It also has a SoftwareSourceCode entry.
    • ThinkWatch Core: the existing SoftwareSourceCode, plus a SoftwareApplication for the twcore binary. It lists Linux, macOS and Windows, and takes its version and binaries from the latest Core release, fetched the same way. Its description has its own field (meta.twcoreDescription in src/i18n/pages/core.ts) instead of being cut out of the page's card title.
    • Entities have @ids. The source code points at the application through targetProduct, and author and publisher refer to the Organization. The Organization adds homebrew-tap to sameAs, and its description now calls the server product ThinkWatch Enterprise.
  • The WebSite entity appears on the home page only. Its SearchAction is removed: the docs never read ?q=, and Google has retired the sitelinks search box.
  • JSON-LD escapes <, so a string can never close its <script> early.
  • Breadcrumbs. A doc's trail is Home › Documentation › product › doc. The Enterprise guides leave out the product step: the Enterprise docs home is /docs itself, which would otherwise appear twice.

Share images (Open Graph / X)

  • There is one 1200×630 card for the site and one each for Lite, Core and Enterprise, rendered at build time by src/pages/og/[card].png.ts (logic in src/lib/og.ts). A card holds the product name, the page's headline (its h1, which matches the meta title) and the platforms:
    • Lite: macOS · Windows · Linux
    • Core: Linux server · macOS · Windows
    • Enterprise: Docker Compose · Kubernetes
    • Site card: the three products
  • Reproducible. Text is drawn as outlines from the Geist and Geist Mono fonts in node_modules (@fontsource/geist, @fontsource/geist-mono, fontkit). The images no longer depend on the fonts installed on the build machine. The live og-image.png was being redrawn by CI in DejaVu Sans. A build on Linux (Node 22, as in CI) gives byte-identical PNGs to a build on macOS.
  • Each page sets its own og:image, og:image:type and og:image:alt, with the alt in the page's language. The alt leaves out labels the headline already names, so the Lite alt does not list the platforms twice. Docs pages are og:type article with article:published_time and article:modified_time, and the docs home stays website. twitter:card is summary_large_image. There is no twitter:site because ThinkWatch has no X account.
  • The old scripts/generate-og.mjs, scripts/og-image.svg and public/og-image.png are removed. Nothing links to /og-image.png.
  • If a headline grows past two lines on its card, the build fails with a message saying so.

Indexing

  • The 404 page is noindex and has no canonical, hreflang or og:url. Its language choice now leads to the home page instead of the missing /404/.
  • The sitemap's <lastmod> used to be the build time for every URL. It is now the date of the last commit that touched the files a page is built from (src/lib/lastmod.ts), or, when later, the date of the latest release of a product the page shows in its content or structured data: Lite for /lite, Core for /core, Enterprise for /thinkwatch, and all three for the home page (the Lite download button and the structured data of every product) and for the changelog. The home page's sources include the hero, the terminal, the console mockups and the Lite download button. Without git history (a shallow clone), <lastmod> is omitted.
  • deploy.yml checks out the full history (fetch-depth: 0). It also passes GITHUB_TOKEN to the build, which already read it but was never given it, so every build ran against the anonymous API limit. The build job runs npm packages with that token in its environment, so it gets contents: read only; pages: write and id-token: write belong to the deploy job alone.

Changelog and RSS

  • /changelog lists every release of Enterprise, Lite and Core from the GitHub API (product, version, date, link to the release), newest first, with a product filter (All · Enterprise · Lite · Core) in place of the type filter. The Enterprise notes written for this site (v0.1.0–v0.4.0) keep their content, and the zh page still prefers the Chinese notes. Summaries are not invented.
  • When GitHub cannot be reached, the build falls back to src/data/releases.json. Refresh it with pnpm releases. Tested with a bad token: all 85 entries still render and versions are left out of the JSON-LD.
  • The page and the feed are titled "ThinkWatch release notes", and their descriptions name all three products. Feed items link to the GitHub release, or to the notes on this site. The four Enterprise items that were already in the feed (v0.1.0–v0.4.0) keep their link and guid (https://thinkwat.ch/changelog#v0.4.0), so feed readers do not show them again. Every Enterprise entry on the page keeps its old anchor (#v0.4.0) next to the new one (#enterprise-v0.4.0), so old links still lead to the release. The channel links to /changelog/ and declares its language.
  • Both languages now share ChangelogPage.astro, with copy in src/i18n/pages/changelog.ts, following the other pages.
  • Visible change: a note's title (e.g. "See every byte") had been caught by the body's section-heading style and shown as small teal caps. It now renders at the size its classes ask for.

Alt text

The Lite page's feature screenshots used alt={item.title}. Each item now has an alt in src/i18n/pages/lite.ts (en and zh-CN) that describes what the screenshot shows. The images are unchanged.

Also

  • A Check workflow builds the site for every pull request. deploy.yml only runs on main, so a broken build used to show up only as a failed deploy. Drop commit 6b963b6 (ci: build the site for every pull request) if this is not wanted.
  • The sitemap's change frequencies use ChangeFreqEnum, which fixes the seven type errors in astro.config.mjs.
  • The home page's title and description moved from Base.astro into src/i18n/pages/home.ts, unchanged.

Verification

  • pnpm build (pnpm 10) passes: 45 pages, 4 cards, sitemap, feed. tsc --noEmit passes. astro check reports only the existing FeaturesSection.astro error.
  • Every JSON-LD block parses (137 blocks), and every @type and property exists in the current schema.org vocabulary with a matching domain.
  • Every internal link and anchor resolves in dist/ (4,462), including the feed's links and the sitemap. All 111 external links return 2xx or 3xx.
  • The first version was also built in a node:22 Linux container from a fresh full clone; the Check workflow builds every push on Linux with Node 22.
  • Review fixes (a26ebc7, e12145a), against the build before them: the four cards are byte-identical, and the JSON-LD differs only in the Enterprise guides' breadcrumbs. The feed's four existing items match the live feed in guid, link, date and description. With made-up newer releases in src/data/releases.json and GitHub unreachable, a Core release moves <lastmod> of /, /core and /changelog only, and Lite and Enterprise releases move /, /lite, /thinkwatch and /changelog. In Chrome, /zh-CN/changelog/#v0.1.0 lands on the release exactly where #enterprise-v0.1.0 does. A build without git history emits no <lastmod> and no article dates. actionlint passes on both workflows.
  • Checked in a browser: the changelog in both languages and its filters (59 Core, 17 Lite, 9 Enterprise, 85 in total).

🤖 Generated with Claude Code

fylorn and others added 4 commits September 25, 2026 03:46
…ap, all products in the changelog

Structured data
- The Enterprise SoftwareApplication is no longer injected on every page by
  Base.astro; it lives on /thinkwatch (and the home page) only. /license,
  /changelog and the docs carry WebPage, BreadcrumbList or TechArticle, the 404
  page only the Organization.
- src/lib/structured-data.ts describes each product once, for its own page and
  the home page: ThinkWatch Lite as a SoftwareApplication (operating systems,
  processors, version and installers of the latest release, install URL,
  screenshots, interface languages, feature list from the page copy, MIT,
  free) plus its SoftwareSourceCode; ThinkWatch Core as SoftwareSourceCode plus
  a SoftwareApplication for the twcore binary (Linux, macOS, Windows; version
  and binaries of the latest Core release); ThinkWatch Enterprise as before.
  Entities carry @ids and refer to the Organization, which gains the
  homebrew-tap in sameAs.
- The WebSite entity is on the home page only, without the SearchAction: the
  docs never read ?q=, and Google no longer shows a sitelinks search box.
- JSON-LD escapes "<" so no string can end its script element.

Share images
- One 1200x630 card per product and one for the site, rendered at build time
  by src/pages/og/[card].png.ts: the product name, the page's headline and the
  platforms. Text is drawn as outlines from the Geist fonts in node_modules
  (@fontsource/geist, fontkit), so the card no longer depends on the fonts of
  the machine that builds it; CI had been drawing the old one in DejaVu Sans.
- Each page sets its own og:image and og:image:alt (in the page's language);
  docs pages are og:type article with published and modified times.
  No twitter:site: there is no ThinkWatch account on X.

Indexing
- The 404 page is noindex and has no canonical or hreflang links; its language
  choice leads to the home page instead of the missing /404/.
- Sitemap <lastmod> is the last commit that touched the files a page is built
  from (src/lib/lastmod.ts), and for the changelog the newest release; without
  git history the dates are left out rather than set to the build time. The
  deploy workflow checks out the full history and passes GITHUB_TOKEN to the
  build.

Changelog and feed
- /changelog lists every release of the three products from the GitHub API
  (product, version, date, link to the release), filterable by product, with
  the Enterprise notes written for this site kept on their versions. When
  GitHub cannot be reached, the build uses src/data/releases.json (refreshed
  with `pnpm releases`). Both languages share ChangelogPage.astro.
- The page and the RSS feed are titled "ThinkWatch release notes" and cover
  Enterprise, Lite and Core; feed items link to the GitHub release, or to the
  notes on this site.

Also: per-screenshot alt text for the Lite page's features (both languages),
the sitemap's change frequencies typed with ChangeFreqEnum.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The deploy workflow only runs on main, so a change that breaks the build was
first seen when it failed to deploy. The share images now fail the build when
a headline no longer fits, which is worth catching in review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y their releases

- The changelog keeps the anchors it had when it listed Enterprise alone
  (#v0.4.0): each Enterprise entry carries one next to its new
  #enterprise-v0.4.0 anchor. The four Enterprise items that were already in
  the feed keep their old link and guid (/changelog#v0.4.0), so feed readers
  do not show them again.
- A page's <lastmod> also counts the latest release of the products it
  shows: Lite for /lite, Core for /core, Enterprise for /thinkwatch, and all
  three for the home page (the Lite download, and the structured data of
  every product) and for the changelog. The home page's sources now include
  the hero, the terminal, the console mockups and the Lite download button.
- deploy.yml: the build job, which runs npm packages with GITHUB_TOKEN set,
  gets contents: read only; pages: write and id-token: write move to the
  deploy job.
- og:image:alt leaves out labels the headline already names, so the Lite
  alt no longer lists the platforms twice.
- The twcore description in the structured data has its own copy field
  instead of being cut out of the Core card title.
- The breadcrumbs of the Enterprise guides no longer list /docs twice
  (Documentation and ThinkWatch Enterprise are the same page).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d by releases

A release date alone does not say when a page last changed, so a build
without history (a shallow clone) emits no <lastmod> at all, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fylorn
fylorn merged commit 4a34ce1 into main Sep 25, 2026
1 check passed
@fylorn
fylorn deleted the seo/website-technical branch September 25, 2026 02:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant