#429 · A timeline's page arrives with its heading, and describes itself to search engines
- timeline-app
- pull request
Closes #428. **The pause:** a timeline's page showed its title, cover and description only after the editor in its iframe had loaded its scripts and fetched the timeline. Until then it showed the editor's sample ("A notebook of moments") and then "Loading timeline…". **Search:** `robots.txt` keeps crawlers out of `/editor` and `/api/`, so they saw only the page's visually hidden summary. The cover appeared only as `og:image`, never as an image on the page. **Changes** - **The frame arrives headed.** The page frames `/editor/frame?timeline= `. For a reader who may open that timeline, the frame's HTML arrives with the timeline's heading already in place: image, title, description and moment count. The description is rendered by the new `markdownHtml`, which produces `renderMarkdown`'s output element for element, as escaped HTML text. Comparisons, copies still being prepared and timelines with sensitive-content labels arrive without the heading. Strangers to a private timeline get the plain editor. - **The editor keeps it.** `heading()` holds the server's heading until the editor has opened that timeline. Any other document ends the hold. - **Crawlers.** `robots.txt` now allows `/editor/frame`. The frame answers `X-Robots-Tag: noindex, indexifembedded`, so the heading is indexed as part of the timeline's page and never on its own. - **Structured data.** A public, unlabelled timeline's page carries a schema.org `CreativeWork` in JSON-LD: name, URL, description, image, keywords, created and modified dates, and author. - **Docs:** `docs/sharing.md` and the frame's README describe all of this. **Design note:** the issue first proposed a server-rendered header above the frame. The timeline page is a full-height app shell, though, with the frame filling the screen and scrolling inside itself, so a header above it would take space from the editor permanently. Filling the frame's own heading on the server keeps the layout and removes the pause for writers too. **Tests** - **`test/description-markdown.test.mjs`:** `markdownHtml` covers inline formatting, links, lists, tables, code, rules and escaping. - **`test/sharing-postgres.mjs`:** - the frame's heading appears for readers and owners and not for strangers or invalid ids; - the image and its alt text are in the HTML; - labelled timelines get neither the heading nor JSON-LD; - the page's iframe asks for its timeline, and the `X-Robots-Tag` and `robots.txt` rules are in place. - **Browser check:** the heading's title is the timeline's from the frame's first frame. Run here in the Playwright 1.58.2 image against PostgreSQL 16, it passes with the change. With the hold disabled it fails, recording "A notebook of moments" and then "Loading timeline…" before the real title. - **Typecheck and unit tests** pass, apart from `test/contents.test.mjs`, which can't run in this checkout: 13 GB of untracked files overflow `git ls-files`. It passes on a clean checkout of main.