Discovery and public pages

How does someone who is not signed in find, hear and save a Toy, and how do search engines and social cards see it? This document covers the public routes the API serves, card prev

How does someone who is not signed in find, hear and save a Toy, and how do search engines and social cards see it? This document covers the public routes the API serves, card previews and image derivatives, the public catalog search, HTML metadata and indexing, and the listening/saving flow.

Status: implemented local browser/API slice. Search-engine indexing and third-party social cache refresh are external behaviors, not qualified compatibility claims. Managed hosting remains pending; see implementation status.

Public routes#

RouteServesIndexing
/Product homepageIndexable
/explorePaginated public catalog with GET search formLanding indexable; search results noindex
/explore/instruments, /explore/effectsCategory landing pagesIndexable
/sounds, /explore/chains, /creatorsSounds grid, published chains, creators grid (sounds)First unfiltered page indexable; filters, search and later pages noindex
/sounds/:idOne sound: preview, resolved fields, links to its landing pagesPublic indexable; unlisted noindex
/sounds/{genre,instrument,mood}/:value, /sounds/tempo/:bpmOne-facet landing pages over the media vocabularies and 60–200 bpm in steps of 5Page 1 indexable with at least 4 sounds; unknown values 404
/templates, /templates/:slugOfficial template index and three template pages from @tunetoys/discoveryIndexable
/learn/...Original practical guidesIndexable
/creators/:handleCreator profile: public work, optional liked work, collections, relationships (community)Created, Sounds and Collections tabs, page 1, indexable; other tabs noindex
/t/:idCurrent public Toy alias, the search targetIndexable
/@:handlePublisher storefront: published revision of theme tokens and typed sections, or the generated default (hero, product grid)Public storefronts indexable
/@:handle/:slugProduct page: header, trailer and carousels, Overview/Versions/Reviews/Discussion/Docs anchored sectionsPublic listings of public Toys indexable; unlisted noindex
/@:handle/:slug/v/:semver, /docs/:docSlugOne version's changelog and platforms; one manual, guide or FAQ pageFollows the listing
/r/:digestExact pinned release with historical metadatanoindex,follow
/workspace, /library/…, /settings/…Editor, Library, Settings (SPA shell)noindex,nofollow

The templates and guides are original editorial metadata over existing admitted examples, not invented user accounts or catalog releases.

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#FAFAF7', 'primaryTextColor': '#1C1C1B', 'primaryBorderColor': '#1C1C1B', 'lineColor': '#64645F', 'fontFamily': 'DM Sans, Inter, system-ui, sans-serif'}}}%%
flowchart LR
  HTML["Server head + semantic fallback"] --> APP["React router + shared shell"]
  APP -->|Preview or Play| ENGINE["Lazy shared engine + Frame controls"]
  APP -->|New / Remix / Workspace| EDITOR["Lazy persistent editor session"]
  MODELS["Visibility-filtered page models"] --> HTML
  MODELS -->|Public JSON| APP
  classDef shared fill:#DCEBDD,stroke:#1C1C1B,color:#1C1C1B
  classDef host fill:#DED8F0,stroke:#1C1C1B,color:#1C1C1B
  class HTML,APP,MODELS shared
  class ENGINE,EDITOR host

React renders every public URL inside the same navigation, account and theme shell, including community and notifications. main.tsx always mounts the router. The server retains status, redirects, cache/ETag rules, CSP, <head> metadata, structured data, sitemaps, images and semantic fallback inside main[data-fallback]. Fallback has headings, text, links, images with alt text and working GET forms; the small token-based fallback.css is its only stylesheet. React replaces fallback with createRoot; this is not hydration.

pages/marketplace.ts resolves the published listing/storefront once for HTML and generated ProductPage/StorefrontPage JSON contracts. pages/sounds.ts supplies validated sound landing copy and filters. Their public endpoints are GET /v1/pages/product, /storefront and /sounds; previews retain member checks and private caching. Creator and collection pages use the existing community endpoints. Templates and lessons share the bundled discovery content. After navigation, React updates title, description and canonical; crawler metadata remains server-owned.

The shell loads without an editor document, Frame renderer or audio host. WorkspaceApp.tsx loads on the first editor action, then keeps the document and audio session mounted across pages. Public page chunks have a 450 KiB gzip cold-load budget, verified from the build's actual module map. Template and release Preview load the real engine on demand, last at most twelve seconds and respect lower/muted monitor preferences. Switching, cancellation, hiding and leaving dispose the audition; template controls use the shared Frame renderer during playback.

A root visit with browser recovery, a source fragment or an account-project handoff routes to /workspace; otherwise / shows the app home. Source fragments never reach server metadata. Template creation opens the real New Toy chooser, preserving current source until acceptance.

Card previews#

ToyThumbnail.tsx draws each Toy as an SVG node-and-cable map through PatchThumbnail.tsx and patchThumbnail.ts. This includes home, Explore, community, storefront Toy cards, Library, recently played, reusable components and the workspace Toy browser. Saved positions, instance widths/colors, catalog colors, typed cables, visual groups and durable identities are retained. Populated Frames do not replace the graph. Local Toys and bundled official templates render directly; published cards use lazy, visibility-checked source reads through the account-scoped release query cache, which is also used by playback. Remote source is admitted before drawing, and missing or mismatched releases show an unavailable state while the Toy link remains usable. Browsing loads no audio engine or media assets and makes no thumbnail-image requests.

ToyPreview.tsx retains inert Frame collages for the separate image-capture entry, with the entry Frame first and an SVG map when no visible Frame exists. toyThumbnail.ts supplies independent nested-instance source values without DSP.

Image derivatives#

PreviewCapture.tsx is a separate production build entry at preview-render.html. The API's bounded PreviewImages service opens only this trusted bundle in an isolated Chromium context, admits source through readToy, serves every request from local files and blocks external networking, cookies, service workers and downloads. It waits for fonts, layout, lazy icon geometry and decoded Frame images, then captures the existing renderer. Only image assets referenced by Frames are read from the authorized release inventory with verified hashes and lengths; audio assets are not loaded and no engine is initialized. Sharp encodes 320/640 px WebP cards and 1200 × 630 PNG OG artwork; it is not a second Frame renderer.

RouteOutput
/previews/r/:digest.webp?w=320|640Release card
/previews/template/:slug.webp?w=320|640Template card
/social/r/:digest.png, /social/t/:id.png, /social/template/:slug.pngOG artwork (WebP also accepted)
/social/site.png, /social/site.webpSite artwork
Limit or rule
Aggregate image budget 16 megapixels / 16 MiB; capture flattens a bounded first raster frame to PNG in a render-only source copy
Disk key covers admitted source, wrapper metadata, output dimensions/format and the fingerprinted renderer build
One capture at a time, at most eight queued jobs, weighted Frame-complexity cap, twenty-second deadline; identical concurrent work shares a promise
Cache prunes to 128 MiB / 512 images, writes atomically, closes idle Chromium after thirty seconds
Authorization is checked before cache/render access and again after asynchronous work
Toy images use private/no-store caching; only intentionally public images permit indexing; template/site images have short public lifetimes
Previously downloaded copies and external social caches cannot be recalled

Correct content types, lengths and HEAD responses accompany all images. Public HTML embeds real image URLs with alt text, dimensions, responsive sources and lazy loading below the primary image. Derived bytes stay separate from portable source and immutable releases.

Local capture needs the pinned Chromium headless shell (node node_modules/playwright-core/cli.js install --only-shell chromium); TUNETOYS_CHROMIUM_PATH selects an existing executable, and the container installs the shell during its build. Build the web app before serving the API: a missing capture bundle produces an explicit image-build error, never invented artwork. Preview disk space lives beside the API data by default.

Public catalog#

GET /v1/catalog accepts q (maximum 200 characters), category (instrument or effect) and offset. Every whitespace-separated term must match the published title/description, current creator handle/display name, public tags or category. Search is parameterized literal substring matching over all public releases before 24-item pagination; % and _ have no wildcard meaning. This first search uses SQLite's lowercase behavior and has no fuzzy matching or locale-aware stemming.

Category follows the published entrypoint: an audio input makes a Toy an effect, otherwise an instrument. This is an audio-discovery grouping, not a compatibility profile. Search and filter state live in the Explore URL; requests are debounced, stale responses discarded, and errors, empty results and reset actions are visible.

Creators enter up to twelve public tags of at most 32 characters during publication. PUT /projects/:id/catalog updates tags with owner authentication, idempotency and the expected project version. Tag changes advance mutable project/catalog metadata and notify library subscribers without modifying source, release bytes or selected library releases; tag-only edits preserve the current unsaved editor document. Public tags are distinct from private personal library tags. Omitting tags on publication preserves existing metadata. 005-discovery.sql adds the catalog columns and backfills category; no portable Toy schema or release envelope changes.

HTML, metadata and indexing#

Every public landing page has a descriptive title, summary, canonical URL, Open Graph/Twitter large-image metadata, meaningful alt text and max-image-preview:large. URLs use the configured TUNETOYS_PUBLIC_ORIGIN, never the request Host. HTML is escaped and structured JSON-LD receives a CSP hash. Home describes the WebSite, organization/confirmed social profiles and browser application; collections, creator profiles, app/template pages, breadcrumbs and guides carry matching structured data. Toy pages are SoftwareApplication with their product's paid offers or a free Offer, and an aggregateRating only from real reviews; sound pages are AudioObject with a licence deed URL, millisecond duration and preview contentUrl. og:type follows the page: profile for creators, music.song for audio, article for guides. Collections are a CollectionPage listing their Toys and, when they hold public audio, also a MusicPlaylist of MusicRecording tracks; guides add HowTo steps (#step-<n> anchors) beside their TechArticle. Share cards are 1200×630 brand cards from shareImage.ts: creators get a monogram card with their counts (/social/creator/:handle.png), sound landing pages a waveform seeded by the facet (/social/sounds/:facet/:value.png); the page URL carries ?v= from what the card prints, so share caches refresh when it changes. No ratings, prices or eligibility claims are fabricated. Each category page ends its first unfiltered page with a short explanatory section, and template and guide pages link the nodes they use to docs.tune.toys, whose node pages link back to the templates.

Home and Explore self-canonicalize. Each catalog page has its own ?page=N URL, canonical and Previous/Next links. Unlisted and private pages/images remain excluded; inaccessible resources return real 404s. Trailing slashes redirect to canonical paths. The public sitemap lists landing pages, official templates/guides, creators who publish Toys or sounds, current public Toy aliases, public sounds and collections, and the sound landing pages that pass the indexing threshold, with image references; Toy lastmod comes from its published release time (sitemap-pages.ts holds the non-Toy queries). Past 45 000 URLs /sitemap.xml becomes a sitemap index of /sitemaps/<n>.xml parts. /robots.txt advertises the sitemap. One robotsDirective sets both the robots meta tag and X-Robots-Tag, so they never disagree. Indexed pages are sent public,max-age=60,stale-while-revalidate=60 (never with a Set-Cookie), which bounds how long an unpublished page can outlive its visibility change; all others stay private,no-store. GET and HEAD return consistent metadata; public image indexing does not weaken release visibility checks.

Optional TUNETOYS_GOOGLE_SITE_VERIFICATION and TUNETOYS_BING_SITE_VERIFICATION add property-verification tokens to the homepage; they do not create, connect or submit an external account. Operators verify the domain, submit the sitemap and inspect indexing and field Core Web Vitals in the appropriate account. Neither search indexing nor a social crawler's rendering is guaranteed by local tests. Vite's static preview does not generate this dynamic HTML; use npm run build and the API static host for public-route, metadata and image qualification.

Listening, saving and returning#

Explore cards audition verified pinned releases through the existing BrowserHost with the admitted engine and verified assets. Effects use their Toy preview audio or the original demo loop; instruments use their own generated audio.

Audition rule
Starts only on a user action, at up to 20% monitor level (respecting a lower or muted editor setting), and stops after twelve seconds
One catalog audition at a time; switching Toys, cancelling, changing search/type, navigating away, opening editor dialogs, hiding the page or starting workspace playback disposes it
Previewing stops existing workspace audio without changing its source or saving/publishing it
Errors stay visible with retry; cancelled and failed starts never enter history

Library → Recently played remembers up to thirty successfully started releases from Explore auditions and published-page Play. It stores only project identity, exact release digest and playback time on this browser, in separate guest/account namespaces; it does not synchronize history or alter a saved/pinned library release. Storage denial falls back to in-memory history. Current authorization is rechecked before showing titles or replay links. Items can be removed and history cleared. Replay uses /r/:digest, so later publication cannot silently change the sound previously heard.

Explore, published playback and Recently played have a one-click Save/Saved toggle; guests are prompted to sign in. Initial bookmark flags are fetched in bounded batches by explicit project identities, independently of the 500-item full-library view. PUT /library/:projectId/bookmark changes only the bookmark field inside the existing transactional mutation boundary, retaining private tags, ratings and selected releases. It is a set-to-value command with a retained idempotency key, so retries do not toggle again; a no-change request creates no annotation version/event, and unsaving a missing item creates no row. Actual changes advance durable library versions and sync events, so stale full-annotation writes reject. Saving does not pin, download or activate a release.

Published playback gives the shared Frame a compact header and action area. Play, presets and monitor volume stay visible; input selection and explicitly requested MIDI access live in an Audio popover with nested Escape and focus return. Input changes stay disabled during playback. Navigation and secondary Library/Remix actions use menus; the description is expandable. Package verification, pinned engine playback, rights and bookmark semantics are unchanged.

Product pages and storefronts#

Rendered by StorefrontPage.tsx and ProductPage.tsx over the shared services/api/src/pages/marketplace.ts models (storefrontPages.ts re-exports the HTTP routes), with domain rules in listing-model.ts and the shared @tunetoys/markdown renderer.

  • Content source. A product page renders the listing's published revision (overrides[field] ?? generated[field]); without one it renders the generated baseline (generateListingBaseline: title, first-sentence tagline, description, category, tags, entry-Frame controls, requirements) from the stable version, else the newest published version, else the latest release. Publisher members may open ?preview=<revisionId> for drafts and older revisions (private,no-store, noindex, preview banner). ?lang=<locale> applies a non-stale translation per field and the page lists hreflang alternates.
  • Visibility. Listing visibility is separate from release visibility and product state. private listings, draft products and suspended publishers return 404 to non-members; unlisted sends noindex,follow; delisted or removed products render a 410 tombstone. Lists (storefront grids, "More by", sitemap) include only published products whose listing is public and whose wrapped Toy is public. A private release never contributes its preview image, Play link, social card or library button.
  • Gallery. React renders the trailer and carousels; fallback retains their media links and alt text. Carousels are scroll-snap lists with a labelled region and a focusable slide list (arrow keys scroll natively); PublicGallery.tsx adds previous/next buttons, a position announcement and Home/End. Uploaded media renders only when the media item is public. YouTube and Vimeo items are poster <button>s with a fallback link; the iframe (youtube-nocookie.com or player.vimeo.com?dnt=1, sandbox="allow-scripts allow-same-origin allow-presentation", allow="autoplay; picture-in-picture") is created only after activation, so no third-party request happens before. Only pages that contain such a poster add frame-src https://www.youtube-nocookie.com https://player.vimeo.com to their CSP. GIFs under prefers-reduced-motion stay hidden until Play; smooth scrolling is disabled.
  • Sections. Overview (Markdown body or description, controls with notes, requirements and download platforms from the stable version's included targets, tags, links, publisher card, bundle "Included", "More by"), Versions (published and withdrawn versions with rendered changelogs and alias badges), Reviews and Discussion (direct React PostFeed components), Docs (document links; FAQ inline). /@h/:slug/versions|reviews|discussion|docs redirect (308) to the anchored section.
  • Metadata. Canonical /@handle/:slug, OG/Twitter image from an uploaded og asset, else the generated OG card, else the release social card, JSON-LD SoftwareApplication (plus Product with offers only when an active paid offer exists, aggregateRating only with ratings, operatingSystem from included targets) and a breadcrumb. Storefronts emit ProfilePage.
  • Redirects. Retired handles (publisher_handle_history) and slugs (product_slug_history) redirect with 308. /creators/:handle redirects to /@handle once the publisher has published a storefront; until then it stays the community profile.
  • Caching. Public and unlisted pages send public, max-age=60, stale-while-revalidate=600 with a weak ETag over the rendered HTML (304 on match); previews, private pages and tombstones send private,no-store.
  • Storefront theme. Validated tokens become scoped CSS custom properties on the storefront page container, using the same preview renderer as Settings → Publisher. The shared navigation and focus rings stay outside the theme scope.
  • Cards and sitemap. The web Explore/Library card image is /previews/listing/:projectId.webp: the uploaded (else generated) listing thumbnail when the listing is public, otherwise a 307 to the release preview. The sitemap adds every public product page and its storefront, with image and video entries from generated listing assets.

Generated listing assets#

Implemented by WP-04: services/api/src/listing-assets.ts (triggers, routes, page helpers) and services/worker (generation). Publishing a product version that the listing shows (the stable alias, else the newest published version), moving the stable alias, or publishing a listing revision only enqueues asset_jobs; nothing renders in a request. Captures reuse PreviewImages and the PreviewCapture bundle above (no second Frame renderer); cards reuse the shareImage.ts glyph-path typography; the audio preview runs the shared WASM engine offline. Kinds whose tool is missing (Chromium, the built capture bundle, TUNETOYS_FFMPEG_PATH) are recorded as skipped with a reason and never replaced by a placeholder.

Route (GET and HEAD)ContentCache-Control
/assets/l/:listingId/:kind/:variant.:extImage, motion, audio, PDF, caption bytespublic, max-age=31536000, immutable with ?v=<digest>; otherwise public, max-age=300, stale-while-revalidate=3600 (og/social: max-age=3600, stale-while-revalidate=86400)
/@handle/:slug/llms.txttext/plain agent summarypublic, max-age=300, stale-while-revalidate=3600
/@handle/:slug/README.mdtext/markdown product READMESame
/@handle/:slug/manifest.jsonVersion, artifact inventory and asset indexpublic, max-age=60, stale-while-revalidate=600
/@handle/:slug/controls.schema.jsonJSON Schema 2020-12 of the public controlspublic, max-age=3600
/@handle/:slug/structured.jsonldschema.org graphpublic, max-age=300
  • Visibility. Rechecked on every request: a private listing, a draft/delisted/removed product, a suspended publisher or a private wrapped Toy serves 404; unlisted listings and non-public Toys serve by URL with X-Robots-Tag: noindex. Members preview any ready asset through GET /api/v1/products/:productId/listing/assets/:assetId/bytes.
  • Selection. An uploaded thumbnail or og override wins (the binary route answers 307 to the public media bytes); otherwise the ready generated row; otherwise the page falls back to the release preview and social card. A failed or skipped regeneration never replaces a ready asset.
  • Headers. Strong ETag (the SHA-256 digest) with 304 on If-None-Match; Content-Length on HEAD; text and JSON allow any origin; SVG carries default-src 'none'; sandbox.
  • Product page. Uses the generated OG card and thumb-640, adds <link rel="alternate"> for llms.txt, README, manifest and control schema, og:video (MP4) and og:audio when ready, and VideoObject/AudioObject JSON-LD on indexable pages.
  • Sitemap. Each public product <url> lists <image:image> entries (thumb-1280, OG, desktop/phone screenshots, captioned with alt text) and a <video:video> entry when the MP4 and poster are ready.
  • Machine drafts. Alt text and captions are template-derived and stored with metadata.unverified = true; the editor labels them "Draft, unverified".
  • Not claimed: social crawler rendering, search indexing of video entries, or external cache refresh.

Evidence#

CheckCovers
API fixtures (platform, seo, preview-images)Cross-page search, public vs personal tags, category filtering, literal queries, owner/version/bounds checks, unchanged release bytes, HTML escaping/CSP, canonicals, PNG dimensions, sitemap exclusion, private artwork denial, historical-release metadata, GET/HEAD, visibility changes during image work
services/api/test/listings.test.mjsGenerated page with no creator work, overrides and reset, member preview, publish, click-to-load posters with page-scoped frame-src, tab redirects, documents, slug 308, unlisted noindex and sitemap exclusion, private 404, storefront contrast/sections/preview/publish/restore, creator redirect
apps/web/test/listening.test.mjsBounded/scoped persistence, preview cancellation, late-load suppression, failure/retry and timeout
npm run test:seo-ui --workspace @tunetoys/webReal shared-renderer WebP/PNG exports, byte caches, font/icon readiness, image access after revocation, public pages without editor/WASM startup, layout shift, search, template playback with live controls and editor handoff
npm run test:listening-ui --workspace @tunetoys/webAgainst a disposable real API/database after a browser build: real shared-engine signal, single-preview switching, cancellation, retry, timeout, exact replay/history scope, revoked access, saving without pinning, desktop/mobile layout
Browser checksNew/recovered/snapshot entry, search/reset/filtering, real Remix navigation, desktop/mobile layout

Accounts, databases, caches and browser profiles in these suites are disposable; UI_SCREENSHOT_DIR retains visual evidence. References: Google JavaScript SEO, site names, Open Graph, Sharp and OpenType.js.

Implementation#

  • services/api — publicSite.ts, discovery.ts, previewImages.ts, shareImage.ts, pages/, storefrontPages.ts, listing-model.ts, listing-assets.ts
  • services/worker — generated listing asset jobs
  • packages/markdown — shared Markdown parser and allowlisting renderer
  • packages/discovery — official templates and guides
  • apps/web — main.tsx, Catalog.tsx, ToyPreview.tsx, PreviewCapture.tsx, RecentlyPlayed.tsx, PublishedToy.tsx
  • packages/frame-runtime — the one renderer behind cards, captures and playback

Edit this page on GitHub

esc

↑ ↓ move↩ open⌘↩ new tabesc close