Uh oh!
There was an error while loading. Please reload this page.
feat(docs): emit JSON-LD on the homepage, doc pages and blog posts - #12355
Merged
Conversation
Adds a schema.org @graph to the three served page shapes, built from the same page data generateMetadata() already reads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The blog date comment asserted a YAML-timestamp coercion that the rendered page falsifies, and the breadcrumb comment misattributed the missing ancestor URL to the page not being listed. Both replaced with what was measured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 25, 2026
os-zhuang
marked this pull request as ready for review
August 25, 2026 20:00
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 25, 2026
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 freeto 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.
Fixes#12240
Adds a schema.org
@graphto the three served page shapes, built from the samepageobjectgenerateMetadata()already reads, so the JSON-LD and the canonical/Open Graph layer cannot describe two different pages./(app/[lang]/page.tsx)Organization+SoftwareSourceCode/docs,/docs/**Organization+TechArticle+BreadcrumbList/blog/**Organization+BlogPosting/blog(index)Every identifier and URL goes through
absoluteUrl()fromlib/site.ts.Two decisions worth reviewing
SoftwareSourceCode, notSoftwareApplication. The card allows either. Google's Software App rich result requiresoffers,aggregateRatingorreview; ObjectStack is an Apache-2.0 runtime with no price, no store listing and no ratings, so aSoftwareApplicationnode would report missing-required-property errors in the very validator the acceptance criteria name, buying a rich result it can never be eligible for.SoftwareSourceCodehas no Google rich-result feature and therefore no required properties, andcodeRepository/programmingLanguage/runtimePlatform/licensedescribe what this project is.dateModifiedreuses the sitemap's own output rather than re-deriving it.lib/structured-data.tsxcallsapp/sitemap.ts's default export once per process and indexes the array. The JSON-LD value and the<lastmod>in/sitemap.xmlare the same object serialised the same way, so the ruling on this card ("two different answers for the same page's date is worse than one absent date") is satisfied by construction, not by two derivations agreeing. Pages the sitemap ships without a<lastmod>get nodateModifiedhere either. Verified over all 406 dated pages — see below.Declared surface expansion — a fourth file
The card's file surface is the three page routes. This PR adds a fourth,
apps/docs/lib/structured-data.tsx, for two reasons that are not style:Organizationis theauthor/publisherof every doc page and every blog post. Three copies is three definitions of one company, free to drift.JSON.stringify(...).replace(/</g, …)inJsonLdis the only thing between page frontmatter and a</script>injected into every page. Three copies is three places for that.replace()to go missing.Contention checked before adding it, not assumed: at the time of writing no open PR in this repo touches
apps/docsat all (enumerated over every open PR's file list). A new path cannot collide with #12242, which is queued behind this card onapp/[lang]/page.tsxand the blog page.The homepage's
YOUTUBE_CHANNEL_URLmoved into that module and is imported back, so the hero links andOrganization.sameAsare one string.Measured on the rendered output of a local production build, not reasoned about:
fumadocs-core'sgetBreadcrumbItems()links a folder crumb tofolder.index?.url. Fumadocs attaches a folder'sindex.mdxas thatindexnode only when the folder'smeta.jsondoes not list"index"inpages— and 17 of the 35meta.jsonfiles undercontent/docsdo list it. Those folders arrive with a title and no URL.Confirmed causally by ablation: deleting the one line
"index",fromcontent/docs/data-modeling/meta.json, rebuilding, and re-reading the rendered page (then restoring the file —git diff HEADempty, blob hash back to the HEAD blob):⛔ Not worked around in this PR. Google requires
itemon every crumb but the last, so a name-only crumb is not an option; reconstructing the URL from the slug in the page component would make a content-config bug invisible and permanent. The un-linkable ancestor is dropped. When #12352 lands, these trails complete with no change to this code.A hypothesis of mine that did not hold, recorded because I acted on it for a while: I expected those 17 folder-index pages to be navigation orphans too. Crawling all 408 sitemap URLs for inbound links found 4 orphans, none of them folder indexes — filed as #12353.
Emitted JSON-LD, verbatim
From
next build && next startat13791c71d, read out of the HTTP response. ⛔ Not from production: per #12333 the live site has served one unchanged deployment since ~16:55, so it is evidence about a build that predates this branch./{ "@context": "https://schema.org", "@graph": [ { "@type": "Organization", "@id": "https://objectstack.ai/#organization", "name": "ObjectStack", "url": "https://objectstack.ai/", "logo": "https://objectstack.ai/logo.svg", "sameAs": [ "https://github.com/objectstack-ai", "https://www.youtube.com/@objectstack" ] }, { "@type": "SoftwareSourceCode", "@id": "https://objectstack.ai/#software", "name": "ObjectStack", "url": "https://objectstack.ai/", "headline": "Metadata framework for AI-written apps", "description": "ObjectStack turns the whole app — data model, UI, workflows, permissions — into typed metadata: a complete CRM in under 150k tokens, one context window.", "image": "https://objectstack.ai/hero-cover-dark.png", "codeRepository": "https://github.com/objectstack-ai/framework", "programmingLanguage": "TypeScript", "runtimePlatform": "Node.js", "license": "https://www.apache.org/licenses/LICENSE-2.0", "author": { "@id": "https://objectstack.ai/#organization" }, "maintainer": { "@id": "https://objectstack.ai/#organization" } } ] }/docs/data-modeling/objects{ "@context": "https://schema.org", "@graph": [ { "@type": "Organization", "@id": "https://objectstack.ai/#organization", "name": "ObjectStack", "url": "https://objectstack.ai/", "logo": "https://objectstack.ai/logo.svg", "sameAs": [ "https://github.com/objectstack-ai", "https://www.youtube.com/@objectstack" ] }, { "@type": "TechArticle", "@id": "https://objectstack.ai/docs/data-modeling/objects#article", "headline": "Object Metadata", "name": "Object Metadata", "description": "Define business entities with ObjectSchema — the core building block of every ObjectStack application", "url": "https://objectstack.ai/docs/data-modeling/objects", "mainEntityOfPage": "https://objectstack.ai/docs/data-modeling/objects", "inLanguage": "en", "image": "https://objectstack.ai/og/docs/data-modeling/objects/image.png", "dateModified": "2026-08-25T16:30:09.000Z", "author": { "@id": "https://objectstack.ai/#organization" }, "publisher": { "@id": "https://objectstack.ai/#organization" } }, { "@type": "BreadcrumbList", "@id": "https://objectstack.ai/docs/data-modeling/objects#breadcrumb", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "ObjectStack", "item": "https://objectstack.ai/" }, { "@type": "ListItem", "position": 2, "name": "Documentation", "item": "https://objectstack.ai/docs" }, { "@type": "ListItem", "position": 3, "name": "Object Metadata", "item": "https://objectstack.ai/docs/data-modeling/objects" } ] } ] }/blog/context-window-is-the-constraint{ "@context": "https://schema.org", "@graph": [ { "@type": "Organization", "@id": "https://objectstack.ai/#organization", "name": "ObjectStack", "url": "https://objectstack.ai/", "logo": "https://objectstack.ai/logo.svg", "sameAs": [ "https://github.com/objectstack-ai", "https://www.youtube.com/@objectstack" ] }, { "@type": "BlogPosting", "@id": "https://objectstack.ai/blog/context-window-is-the-constraint#article", "headline": "The Constraint Isn't Typing Speed. It's the Context Window.", "name": "The Constraint Isn't Typing Speed. It's the Context Window.", "description": "AI can write an enterprise app. Whether it can maintain one depends on whether the whole system fits in its head — which is an argument about the target format, not the model.", "url": "https://objectstack.ai/blog/context-window-is-the-constraint", "mainEntityOfPage": "https://objectstack.ai/blog/context-window-is-the-constraint", "inLanguage": "en", "image": "https://objectstack.ai/hero-cover-dark.png", "datePublished": "2026-07-17T00:00:00.000Z", "dateModified": "2026-08-18T01:17:36.000Z", "keywords": [ "ai", "architecture", "metadata", "positioning" ], "author": { "@type": "Organization", "name": "ObjectStack Team" }, "publisher": { "@id": "https://objectstack.ai/#organization" } } ] }How it was validated
⛔ Google's Rich Results Test was not run. It is a hosted tool that needs a public URL, and this branch is not deployed. The card's first acceptance box is therefore reported, not ticked — it can only close against a preview or production URL after deploys resume (#12333).
What was run instead: a structural check of every JSON-LD block the site serves, field by field, against schema.org's property ranges and Google's documented required/recommended properties. Over all 408 sitemap URLs:
The 408th is
/blog, which emits none by design. Each block was asserted on:ld+jsonscript per page; payload parses as JSON;@contextishttps://schema.org<survives into the payload — the injection guard, checked on output rather than trustedhttps://objectstack.ai, except the three declared external ones (sameAs,license,codeRepository); no relative URL anywhere{"@id": …}reference resolves to a node defined in that same page's@graph(no cross-page dangling references)Organization:name,url,logo,sameAsall presentTechArticle/BlogPosting:headline,image,author,publisher,mainEntityOfPage,urlpresent;urlequals the page's canonical;headlinewithin Google's 110-character cap;datePublished/dateModifiedmatch ISO 8601dateModifiedbyte-equal to that page's<lastmod>in/sitemap.xml— the card's third acceptance box, checked on all 406 dated pages, e.g./docs/data-modeling/objectscarries2026-08-25T16:30:09.000Zin bothBreadcrumbList:positioncontiguous from 1, every entry has anameand an absoluteitem, last entry is the page itselfSoftwareSourceCode.licenseis the Apache-2.0 URL (schema.org'slicenseranges overCreativeWork | URL, notText, so the bare SPDX idApache-2.0is not a value it accepts)Commands
Union re-run on the final commit
13791c71d, after the last push:The five
check:*families are whatnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackderives for this diff; re-derived after the final commit and unchanged.check:nul-bytesadded by standing rule, plus a direct control-byte scan of the four changed files (no hits).pnpm lintaborts with a V8 heap OOM on this host at its declared--stack-size=4000with the default heap. It is not a narrowing and not a red: re-run asnode --stack-size=4000 --max-old-space-size=8192 node_modules/eslint/bin/eslint.js . --no-inline-config, the full unnarrowed population passes clean in 46s.content/docs/**/meta.jsoncloses the block comment at*/.tscreported 18 syntax errors and eslint a parse error. Caught only because the gate union was re-run on the new head rather than skipped for "just comments".Declared narrowing — verification ran UNLOCKED.
scripts/pm/os-verify-lock.shcould not take the shared verify lock on this host: no usable
flock. The sharedverify lock is declared Linux-only (
flockis util-linux, and a stock macOS doesnot ship it), so the commands above were run directly, without the lock —
a declared narrowing, not a silent one. No serialization guarantee held for those
runs, nor for any sibling agent in this container while they ran.
Out of scope, filed
meta.jsonlisting"index"detaches the folder index #12352 —meta.jsonlisting"index"detaches the folder index; 172/403 breadcrumbs skip their sectiongitConfig.repois the pre-renameframework, so every GitHub link (and nowcodeRepository) rides a redirect #12354 —gitConfig.repois the pre-renameframework; every GitHub link, and nowcodeRepository, rides a redirectNo changeset:
apps/docsis private and publishes nothing.skip-changesetapplied.Generated by Claude Code