Repository navigation
SEO: metadata overrides, hidden posts, and structured data - #16
Merged
Merged
Conversation
The post query never projected the studio's seo object, so metaTitle, metaDescription, ogImage and "No Index" had no effect. Posts now prefer the overrides, and hidden or "No Index" posts emit noindex,follow. "No Index" posts also drop out of the sitemap, RSS and llms.txt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNw4RCS11ZZRVjxynk9baS
The sitemap used lastModified || date and never saw _updatedAt, while the JSON-LD fell back to it, so lastmod stayed at the publish date. Both now share postModifiedAt(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNw4RCS11ZZRVjxynk9baS
The renderer read value.asset.url, but a portableImage holds its asset under value.image as an unresolved reference, so every inline image was dropped from the feed. Build the URL with urlFor(value.image) and keep the caption. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNw4RCS11ZZRVjxynk9baS
RSS and llms.txt rendered every internal link as /<slug>, so links to gems pointed at pages that don't exist, and on the site /gems#<slug> had no matching anchor. One internalLinkPath() now serves the page, RSS and llms.txt, and each gem card carries its slug as id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNw4RCS11ZZRVjxynk9baS
/gems returned no pageMetaTags, so it shipped the English home page's title, description and Open Graph tags. It now has its own, plus ItemList and BreadcrumbList JSON-LD. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNw4RCS11ZZRVjxynk9baS
On maxdaten.de, relative links to English pages (blog, gems, posts, feed) went through a 308 to www.maxdaten.io, and the logo and "Start over" on English pages hit www.maxdaten.io/ → /en. The legal pages were served on both hosts. - siteHref() links to canonicalUrl() on the production hosts (including the apex the prerender may run as) and stays relative elsewhere; header, footer, post cards, recent posts, RSS link and error pages use it - the logo and "Start over" go to the home page in the page's language - Card and Button treat links to the site's own hosts as internal, so they no longer open in a new tab (replaces the unused HttpRegex) - www.maxdaten.io/{impressum,datenschutz} redirect to maxdaten.de Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNw4RCS11ZZRVjxynk9baS
Both were built from url.origin, which at prerender is VERCEL_PROJECT_PRODUCTION_URL and may be a redirecting host. They now use the canonical English host, and prod-check asserts it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNw4RCS11ZZRVjxynk9baS
Inline images had no width or height, so the text below jumped as each one loaded, and the LQIP prop was never passed. The post query now projects the asset's dimensions and LQIP (none for transparent images, where the blur would show through); the image sets width/height from the cropped size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNw4RCS11ZZRVjxynk9baS
It took the legacy BlogPost type, so the post page padded it with placeholders (hidden, keywords, relatedPosts, content) it never read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNw4RCS11ZZRVjxynk9baS
- Open Graph article:published_time, modified_time, author and tags - BlogPosting wordCount (counted, not estimated) and timeRequired, which were never set - BlogPosting author from the post's Sanity author instead of hard-coded - BlogPosting image at 1200px instead of the full-size cover upload - ProfilePage only on /about/jloos instead of on every page SITE_AUTHOR moves to its own module so the universal post load can use it without bundling the Sanity client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNw4RCS11ZZRVjxynk9baS
…views robots.txt explicitly allows the search crawlers and user fetchers of ChatGPT, Claude and Perplexity; training crawlers stay under the wildcard (still allowed, as before). Pages emit max-image-preview:large. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNw4RCS11ZZRVjxynk9baS
The prerendered /404 page had the default index,follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNw4RCS11ZZRVjxynk9baS
lastBuildDate is the newest change to a post in the feed, so it moves only when the feed's content does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNw4RCS11ZZRVjxynk9baS
Heading ids were derived per heading in the browser: non-ASCII letters
vanished ("für" → "fr") and repeated headings shared an id. The post load
now assigns each heading a unique _anchor (-2, -3 for repeats) with
accented letters folded to their base letter. Existing anchors on public
posts are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNw4RCS11ZZRVjxynk9baS
meta.description repeated the whole hero text (~330 characters), which search results cut off after about 155. It is now heroSummary(), the paragraph the OG card already shows, so the two can't drift apart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNw4RCS11ZZRVjxynk9baS
- Title on / and /en: "Platform & Product Engineer, Hamburg | Jan-Philip Loos" (54 characters, no site-name suffix), also og:title - The hero's first paragraph, and with it the meta description, OG card and llms.txt summary, now names the stack and where: "I build your product and the platform under it: Kubernetes, Nix, continuous delivery. …" (151/152 characters) - The second paragraph drops its trailing "Hamburg", now in the first Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNw4RCS11ZZRVjxynk9baS
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNw4RCS11ZZRVjxynk9baS
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds editor-controlled SEO overrides in Sanity, respects hidden posts in search, and improves structured data across the site.
Summary
Editors can now override post titles, descriptions, and social images in Sanity's SEO group. Hidden posts remain reachable by link but are excluded from search results and sitemaps. Structured data is more complete: posts carry word count and author info, the /gems page has its own metadata and ItemList schema, and the author's /about page is correctly marked as a ProfilePage (not every page).
Key changes
seo.metaTitle,seo.metaDescription, andseo.ogImagefrom Sanity; fall back to post title/excerpt if not setnoindexin robots meta and excluded from sitemap; still reachable by direct linkheadingSlug()andassignHeadingAnchors()utilities for unique, stable heading IDs (handles repeated headings like GitHub)internalLinkPath()utility for consistent post and gem linkingsiteHref()generates correct URLs across production domains;isExternalHref()replaces the removed HttpRegexBlogPosttype andHttpRegexutilityTesting
headingSlug(),postModifiedAt(), andinternalLinkPath()seo.noIndexand dates posts by last edithttps://claude.ai/code/session_01MNw4RCS11ZZRVjxynk9baS