AEO: structured data, agent surfaces, crawlable content, robots hardening - #148
Conversation
…ning Port the basement.studio AEO system to Shader Lab: - JSON-LD @graph per page (Organization, WebSite, WebApplication, CreativeWork scenes with like/remix InteractionCounters, ProfilePage, CollectionPage, FAQPage, BreadcrumbList) anchored on the shared basement.studio Organization @id, driven by a canonical product-facts.ts - /tools/shader-lab/about: server-rendered prose page with the full effect catalog, package docs, and FAQ — the editor itself has no crawlable text (plus an sr-only h1 on the editor shell) - /llms.txt and /agents.md route handlers (runtime base URL, community links gated on deployment config) - Markdown mirrors: /index.md, /sitemap.md, and per-scene .md twins via middleware rewrite + Accept: text/markdown negotiation, with Link rel=alternate advertised on scene HTML responses - robots.ts: per-AI-bot allow groups, disallow /api/ /auth/ /monitoring, and disallow-all + noindex meta + empty sitemap on non-production deployments (previews were fully indexable) - Remove the root-layout canonical that mislabeled every page not overriding it; fix OG siteName (was the npm package name); richer default description - manifest.ts (PWA icons pending brand assets) and a .well-known/mcp.json discovery card for @basementstudio/shader-lab-mcp
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
…values - mdText() neutralizes markdown syntax and collapses whitespace in user-generated scene titles, descriptions, and author names before they are interpolated into the scene .md mirrors and /sitemap.md, so published metadata can't inject links/headings into agent-facing docs - Middleware content negotiation now parses Accept q-values instead of a substring check: text/markdown;q=0 or an html preference no longer serves the markdown mirror - Document why unset VERCEL_ENV counts as production (only occurs off-Vercel; every real Vercel deployment sets it) - Cap next/image deviceSizes at 2560 (drops multi-MB 3840 srcset candidates on full-bleed community hero imagery)
The ?effect= filter URLs were linked from every scene tag but shared one canonical with no distinct metadata — nothing an answer engine could cite for queries like "browser CRT shader effect". - /tools/shader-lab/community/effects/[effect]: one landing page per effect with unique title/description/canonical built from the layer catalog, example image, community scenes using it (reuses PublicSceneGrid with server-fetched initial data), sibling-effect nav, and CollectionPage + BreadcrumbList JSON-LD - /tools/shader-lab/community/effects: index hub of all effects with previews and descriptions - Scene-page tags and the about-page catalog now link the effect pages (the interactive gallery filter keeps its query URLs) - Effect pages added to sitemap.xml, sitemap.md, and llms.txt (llms.txt now lists each effect with a description and link when community is enabled) - Middleware excludes the `effects` segment from scene .md handling
With cacheComponents enabled, awaiting params in the page's top-level
component fails the PPR shell prerender ("Uncached data was accessed
outside of <Suspense>") — but only when community is enabled at build
time, so the local community-disabled build passed while the Vercel
deployment failed. Restructured to the scene-page pattern: sync default
export, params awaited in a child rendered inside Suspense with a
skeleton fallback. Reproduced the failure and verified the fix locally
with community env vars set.
Ports the basement.studio AEO system (website-2k25) to Shader Lab.
Fixes
alternates.canonical: "/tools/shader-lab"was inherited by every page that didn't override it (not-found, any future page). Removed — each page owns its canonical.@basementstudio/shader-lab); now "Shader Lab", with a richer default description.VERCEL_ENV !== "production") now serve robots.txt disallow-all, anoindexmeta on every page, and an empty sitemap.Structured data
One JSON-LD
@graphper page viaPageJsonLd, always inlining the Organization node anchored athttps://basement.studio/#organizationso both properties resolve to the same entity. Canonical facts live insrc/lib/structured-data/product-facts.ts; effect names derive fromLAYER_CATALOGso they can't drift.WebSite+WebApplication(free offer, WebGPU requirement, feature list)CreativeWorkwith like/remixInteractionCounters,isBasedOnfor remixes, effect keywords, authorPersonProfilePage(only whenpublishedCount > 0, matching the noindex rule)CollectionPage+ItemList; breadcrumbs everywhere;FAQPageon the about pageScene titles/descriptions are user-generated, so the JSON output escapes
<to prevent</script>breakout.Crawlable content
/tools/shader-lab/about: what Shader Lab is, how layers work, the full effect catalog with preview images, both npm packages, and a 6-question FAQ. The editor page had zero crawlable text (full-viewport WebGPU app, no h1) — this page is the fix. Editor shell also gets an sr-onlyh1.Agent surfaces
/llms.txt+/agents.mdas route handlers (absolute URLs derive from the runtime base URL; community links gated on deployment config)/index.md(product overview),/sitemap.md(content index), and per-scene.mdtwins —/community/<slug>.mdrewrites through middleware,Accept: text/markdownnegotiation on the HTML path, andLink rel="alternate"advertised on scene HTML responses/api/,/auth/,/monitoringmanifest.ts(PWA icons pending brand assets — favicon placeholder for now) and/.well-known/mcp.jsondiscovery card pointing at@basementstudio/shader-lab-mcpEverything degrades gracefully when community is disabled (no community URLs, no throws).
Verified
next buildpasses; lint clean (2 pre-existing warnings);tsc --noEmitclean.mdcontent types + canonical Link headers, mcp.json, manifest, canonicals on every page type, JSON-LD graphs,.mdrewrite + Accept negotiation + alternate headerVERCEL_ENV=previewbuild: disallow-all robots, noindex meta, empty sitemapReview notes
APP_DESCRIPTION, editor/about descriptions, about-page prose, FAQ answers.APP_BASE_URL-relative; setNEXT_PUBLIC_BASE_URLin Vercel prod once the domain is final.