Discovery: search engines and AI engines as first-class audiences
- Status: requirement, recorded 2026-08-02 (owner flagged it as the audience that nearly got lost in the Phase 8 cleanup)
- Applies to: Inception Space and every Lucas Academy site
- Related: the Lucas Academy knowledge platform's locked principle — GEO / AI-readability first, EN + ZH only, no top menu
Why this needs writing down
Everything the museum shows lives inside a WebGL canvas. To a crawler or an LLM, index.html is an empty page with a script tag: no room names, no artist statements, no artwork titles. A product whose entire content is invisible to search and to AI assistants cannot be found, cited, or recommended — and for a school project, "a parent asks an assistant about kids' art programs" is a real discovery path.
Three audiences, not one:
- People — the 3D experience.
- Search engines — need crawlable HTML, titles, descriptions, sitemaps, structured data.
- AI engines (assistants, answer engines) — need clean text they can read and quote, ideally without executing JavaScript.
Requirements
R1. Every space has a text twin
Each room, artwork, and exhibition gets a real URL serving server-rendered HTML: title, description, the artist statement, the medium, the class or program it belongs to, and a link into the 3D view at that spot. The canvas becomes the enhanced view of a page that already works as text.
R2. Structured data
schema.org JSON-LD on those pages: VisualArtwork for pieces, ExhibitionEvent / Collection for shows, Organization + EducationalOrganization for Lucas Academy, Person-free by policy (student identity stays out — use pseudonymous attribution).
R3. Machine-readable mirrors
sitemap.xmlandrobots.txtthat actually list the text twins.- An
llms.txtat the site root plus Markdown mirrors of the key pages, so an assistant can read the content without a headless browser. - Stable, human-readable URLs (
/rooms/van-gogh-house, not?slot=3).
R4. Social/answer previews
Per-page <title>, meta description, OpenGraph and Twitter cards with a real rendered image (a curated screenshot of the room, not a logo). These double as the thumbnail an answer engine shows.
R5. Language policy
EN + ZH only, site-wide, both indexable, hreflang pairs. This matches the knowledge platform's locked decision; do not add more locales here.
R6. Privacy is non-negotiable and comes first
Indexable text is a published surface: it may contain only what the public_snapshot layer allows (execution plan §2.3 / §5). No real names, no emails, no class membership, no draft history, no AI dialogue. When in doubt, a page is not indexed at all — noindex is the default for anything that is not a curated public snapshot. The current index.html carries <meta name="robots" content="noindex"> and keeps it until the public surface exists.
The worked example is /class/ (owner 2026-09-12). A lesson record names the children who were in the room, quotes what they said about being loved, and shows their faces. R6 does not forbid the page — it decides where the page lives: not indexed at all. The section is absent from sitemap.xml and llms.txt, sits behind Disallow: /class/, and every record and its index carry noindex, nofollow, noarchive, noimageindex; tests/class-section.test.js holds all four, because any one of them is easy to forget. What R6 cannot do is make an unlisted URL private — a link is a secret, not a door — so the rule for what belongs there is the same as for a printed handout: nothing that would harm a child if a stranger read it.
Reusable lesson plans are public teaching material (owner clarification, 2026-09-12). The six-lesson plan is published at /docs/van-gogh-love-lessons.html, included in the public docs navigation, sitemap and llms.txt. All lesson records and their index stay unlisted. Links may lead from a record to the public plan; public plans and their navigation must never reveal record URLs, student photographs or the private record index. Previously shared plan URLs use the public doc's navigation and canonical URL, so they do not retain a route into the records.
What this changes about hosting
Static Vite output cannot server-render the text twins. Two workable paths, to decide when the public surface is built:
- Prerender at build time — generate one static HTML file per public room/artwork from the same manifest the 3D world reads. Simplest, works on Render's static hosting, no runtime cost. Recommended default.
- Server-render on request — needed only if public content changes faster than deploys (it will not, for curated snapshots).
What is built (2026-08-03)
npm run build runs scripts/prerender-discovery.mjs after Vite, which generates the readable surface from the SAME manifest and room documents the 3D world reads — so there is no second description to drift out of date:
- R1
dist/rooms/<slot>.htmlper PUBLIC room: title, a description derived from the room's own shape, face count and prop count, and a link into the museum. A private room is not written at all. - R2
schema.orgJSON-LD —EducationalOrganization+WebSite+Collectionon the root,Collectionper room. NoPerson, ever. - R3
sitemap.xml,robots.txt(allow the root, the room pages andllms.txt; nothing else), andllms.txt— a Markdown brief an assistant can read without running JavaScript. - R4
<title>, description and OpenGraph on the root and every room page — and, since 2026-08-31, a preview image on all of them. The root had declaredtwitter:card = summary_large_imagefrom the start, which promises a big-picture card, with noog:imageto fill it; the card therefore rendered as a plain text box, and the room pages carried no picture and notwitter:tag at all. One shared card now answers every page:public/og-cover-v1.jpg, a 1200 x 630 crop of the museum's own constellation portal, owned bysrc/inception/discovery/socialCard.jsso the landing page and both generated templates cannot drift, with the build refusing if the file is missing fromdist/. This is the shared card, not what R4 asks for. R4 wants a curated screenshot per room — "a room, not a logo" — because the same image is what an answer engine shows beside a specific room. That is still unbuilt (owner 2026-08-31: one image for everything first). When it is built, it replaces this card by giving the same module something better to return per page; the shared card stays as the fallback for pages that are not a room. - R6 Two tests enforce it: the published files may not contain any owner email or the word "password", and a room appears in the sitemap iff its document says
visibility: public. - The
/docs/documentation site (2026-08-03): the repository's own Markdown docs — vision, direction, the bilingual execution plan, the.lprformat, performance notes and the ADRs — rendered to static HTML with a raw.mdmirror per page (src/inception/discovery/docsCatalog.jsis the published list). Email addresses are redacted at render time, and a test scans everything under/docs/for address shapes.llms.txtlinks the.mdmirrors so an assistant with no repository context can read the same source of truth people do. The ops runbook and the agent-instruction files are deliberately not published.
The landing page itself is deliberately almost empty — the door and the controls, with everything else behind ?debug (owner 2026-08-03). Discovery therefore rides on the head metadata and the prerendered pages listed in the sitemap, not on visible text in the app shell. That is a real constraint: if a crawler ignores the sitemap it sees very little.
Still open
- R4's PER-ROOM preview image. A curated screenshot of each room, rendered headlessly at build time. Every page has a picture now, but it is the same picture; a room page's card does not yet show that room.
- R5's
hreflangpair. The pages are English only; the ZH twin needs the room documents to carry a translated title first. - Per-artwork pages. Rooms exist as pages; individual paintings do not, because the public-snapshot layer that decides what an artwork may say (execution plan §2.3) is not built.