Skip to content

SEO/AEO fixes: valid JSON-LD, llms.txt, modified dates, descriptions, 404 page - #125

Merged
ryanspletzer merged 6 commits into
mainfrom
fix/json-ld-validity
Aug 29, 2026
Merged

ryanspletzer merged 6 commits into
mainfrom
fix/json-ld-validity

Conversation

@ryanspletzer

Copy link
Copy Markdown
Owner

Summary

Follow-ups from auditing the site against Anthony Nocentino's SEO/AEO post. Most of the checklist was already covered; this branch closes the gaps.

  • Fix invalid Article JSON-LD on 46 of 49 posts. escape HTML-escaped values (Don't) and folded description: > values carried a raw newline into the JSON string literal, so no JSON parser accepted the block. Content-derived strings now render via jsonify (with normalize_whitespace | strip on descriptions). New bun run check:jsonld parses every ld+json block in _site and runs in the html-proofer job next to check:csp.
  • Add llms.txt (llmstxt.org format): site summary, every post with its description, pages, and feeds.
  • Modified-date support. Optional last_modified_at post frontmatter feeds JSON-LD dateModified, article:modified_time, sitemap <lastmod>, and Atom <updated>, falling back to date; validated by check:frontmatter.
  • timezone: UTC so local builds render zone-less frontmatter dates the same as CI. The macOS baselines had captured the local-zone shift (a 2023-03-26 post rendered as "March 25, 2023"); the six that crossed the diff tolerance are regenerated.
  • Real descriptions for the site, homepage, archive, and tags index (previously all fell back to "Personal site of Ryan Spletzer").
  • Useful 404 page with archive/tags/home links and the five most recent posts; marked noindex. Its visual test switches to viewport capture like the other listing pages, with macOS and Linux baselines regenerated.

Deliberately not done: Article → BlogPosting (Google treats them identically) and noindex on thin posts.

Test plan

  • bun run check:jsonld — 344 blocks across 147 pages parse; against the pre-fix build it flagged exactly the 46 broken posts
  • check:frontmatter (incl. valid/invalid last_modified_at cases), typecheck, check:csp, markdownlint-cli2, actionlint, htmlproofer
  • bun run test:visual locally — 56 passed, 4 skipped
  • Update Visual Baselines workflow run on this branch (Linux 404 baselines committed in 303d85a)
  • After merge: confirm https://www.spletzer.com/llms.txt resolves and a post passes Google's Rich Results Test

🤖 Generated with Claude Code

https://claude.ai/code/session_018PGCnJxq4L3ShzTP9km88L

ryanspletzer and others added 6 commits August 29, 2026 13:49
The per-post Article block used Liquid's `escape` filter, which
HTML-escapes (Don't -> Don&#39;t) rather than JSON-escapes, and
emitted folded `description: >` values verbatim, leaving a raw newline
inside the JSON string literal. 46 of 49 posts shipped structured data
that no JSON parser would accept.

Render every content-derived string with `jsonify` (and `strip` the
description) so the output is always valid JSON, and add
`bun run check:jsonld`, which parses every ld+json block in _site and
requires @context/@type, wired into the html-proofer job next to the
CSP check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PGCnJxq4L3ShzTP9km88L
A Liquid-templated /llms.txt in the llmstxt.org format: site summary,
every post (newest first) with its description, plus the standing
pages and feeds. Descriptions pass through normalize_whitespace so a
folded frontmatter value containing a paragraph break still renders as
a single link line; the Article JSON-LD description now gets the same
treatment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PGCnJxq4L3ShzTP9km88L
Introduce an optional `last_modified_at` post frontmatter field that
feeds JSON-LD `dateModified`, the `article:modified_time` OG tag, the
sitemap `<lastmod>`, and the Atom `<updated>` element, all falling back
to `date`. Listing pages (home, archive, tags) take the newest post's
date as their lastmod; about and linkfarm carry none rather than a
misleading build time. The frontmatter check validates the field's
format and that it is not before the publish date.

Set `timezone: UTC` so zone-less frontmatter dates render the same in
local builds as in CI. Local builds in US Eastern were shifting every
post to the previous evening, which the macOS visual baselines had
captured (a 2023-03-26 post rendered as "March 25, 2023"); regenerate
the six baselines whose diff crossed the tolerance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PGCnJxq4L3ShzTP9km88L
Replace the placeholder site description ("Personal site of Ryan
Spletzer"), which the homepage, archive, tags index, and about page all
fell back to for their meta and og:description, and which the feeds
and llms.txt reuse. The homepage, archive, and tags index now carry
their own description frontmatter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PGCnJxq4L3ShzTP9km88L
Add a short pointer to the archive, tags, and home, plus the five most
recent posts, so a dead inbound link still lands somewhere navigable.
Give it a description and mark it noindex (GitHub Pages serves it at
/404.html with a 200, so it is otherwise crawlable).

The page now lists posts, so its visual test switches to a viewport
screenshot like the other listing pages to avoid baseline churn on
every new post; macOS baselines regenerated. 404.md joins archive.md
and tags.md in the markdownlint ignore list because of the Liquid/HTML
loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PGCnJxq4L3ShzTP9km88L
@ryanspletzer
ryanspletzer merged commit 0e30f30 into main Aug 29, 2026
11 checks passed
@ryanspletzer
ryanspletzer deleted the fix/json-ld-validity branch August 29, 2026 18:26
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