# site.json keys One public site: its branding, its channel grouping and which channels it exposes, persisted to `transcripts/sites//site.json` (the directory under `$SITES_DIR` when that is set). The schema is `common/lib/siteSchema.ts`. Global operational settings are `settings.json` — see [SETTINGS.md](SETTINGS.md). The PUBLIC `/site.json` a built site serves is a different file (`common/lib/siteDescriptor.ts`). Every key is optional on read. A missing key reads as its default, an ill-typed one as its default (or is dropped, for the optional ones), and an unknown one is dropped on the next save. A save ALWAYS writes `siteId`, `siteTitle`, `siteDescription`, `headerTitle`, `homeTagline`, `groups`, `defaultGroupId` and `channels`; every other key is written only when it differs from its default (`socialLinks` whenever it is an array, even an empty one). A save is REFUSED when there is no channel group, when `defaultGroupId` names no group, when a social link's SVG is not safe to inline, or when a public site's `publish.auto` deploys and it has no `cloudflareProject`. Regenerate this file with `pnpm --filter yt-dlp-transcript-common exec tsx bin/file-schemas-docs.ts`. | Key | Default | |---|---| | [`siteId`](#siteid) | the directory name | | [`siteTitle`](#sitetitle) | `"Transcript Browser"` | | [`siteDescription`](#sitedescription) | `"Browse and search video transcripts"` | | [`headerTitle`](#headertitle) | `"Transcript Browser"` | | [`wordmarkLead`](#wordmarklead) | absent | | [`homeTagline`](#hometagline) | `""` | | [`socialLinks`](#sociallinks) | absent | | [`groups`](#groups) | list — see below | | [`defaultGroupId`](#defaultgroupid) | `"default"` | | [`channels`](#channels) | `[]` | | [`cloudflareProject`](#cloudflareproject) | absent | | [`accent`](#accent) | absent | | [`siteUrl`](#siteurl) | absent | | [`listed`](#listed) | `true` | | [`audience`](#audience) | absent | | [`reports`](#reports) | `[]` | | [`relatedSites`](#relatedsites) | `[]` | | [`search`](#search) | `true` | | [`pwa`](#pwa) | `false` | | [`archives`](#archives) | `true` | | [`duplicates`](#duplicates) | `true` | | [`transcriptDownloads`](#transcriptdownloads) | `true` | | [`archiveMaxBytes`](#archivemaxbytes) | absent | | [`hubUrl`](#huburl) | absent | | [`publish`](#publish) | absent | ## `siteId` The site's id: a lowercase slug (`[a-z0-9][a-z0-9-]*`), and its directory name under `sites/`. The directory is authoritative — a read takes the id from the path, never from the file. ## `siteTitle` The site's title (browser tab, manifest, headings). Default: `"Transcript Browser"` ## `siteDescription` One-line description (meta description, manifest). Default: `"Browse and search video transcripts"` ## `headerTitle` The title shown in the site header. Default: `"Transcript Browser"` ## `wordmarkLead` The heavy first part of the header wordmark: the subject's name, e.g. `"Jer"` for `"Jeralyzer"`; the rest is set light. Kept only when it is a proper prefix of `headerTitle` (case-sensitive, shorter than it); anything else is dropped, and a site without one sets its whole title heavy. Never guessed from the title. Default: absent ## `homeTagline` Tagline under the home page title. Empty = none. Default: `""` ## `socialLinks` Per-site social links. ABSENT means inherit the global default (`settings.json` `socialLinks`); an array — even an empty one — overrides it. Each link's SVG must be safe to inline or the save is refused. Default: absent #### `socialLinks[]` Per entry — each entry spells its own values. | Key | Description | |---|---| | `label` | The link's name: the icon's accessible name and its tooltip, never text beside it. Shown as text only in place of an icon that fails the check at render. | | `url` | Link target: http(s), mailto: or a site-relative path. | | `svg` | Inline SVG markup: ONE well-formed `` element, checked when it is saved new or edited and again every time it is rendered (a link whose icon fails at render shows its label instead; `archilyzer doctor` names it). It may contain shapes, groups, defs, gradients, patterns, clip paths, masks, filters, text and animate/animateTransform/set — no script, style block, foreignObject, a, image, title, desc or any HTML element (a title or desc holding text only is removed); SVG presentation attributes plus aria-*, data-* and xmlns:* — no event handler (on…); a `style` attribute of presentation properties only; an href or url(…) only to an id inside the icon, written plainly; no CSS escape, comment or function that loads anything (image-set, image, cross-fade, element, src, paint, @import); ids plain names. A leading XML declaration, a DOCTYPE without an internal subset and comments are removed. Normalized on save: width/height stripped, aria-hidden added, a single-colour icon's fills made fill="currentColor" (an icon of two or more colours keeps them). A root with no viewBox but a numeric width W and height H (unitless or px) is given `viewBox="0 0 W H"`, so a file pasted as downloaded is accepted. A refused save names why; export from a drawing program with presentation attributes rather than a style block (in Inkscape, save as Plain SVG). | | `featured` | Keep this link in the header on small screens (the editor's "Keep in header on small screens"). A narrow header shows only the featured links (up to 4, the last 4 if more are marked; none marked → none, so the name has the room); a wide header shows every link, up to 4, the featured ones kept first, then the last of the rest. The footer shows every link. Written only when true. | ## `groups` Channel grouping layout for THIS site: the buckets the export UI renders channel checkboxes in, and which are selected by default. At least one is required on save; a file with none reads as one inline fallback group. #### `groups[]` Per entry — each entry spells its own values. | Key | Description | |---|---| | `id` | Group id: a lowercase slug (`[a-z0-9][a-z0-9-]*`), unique within the list. An entry with an invalid or repeated id is dropped. | | `name` | Header text. May be blank — the UI then shows no header (and falls back to the id in admin contexts) — but must be a string. Trimmed. | | `description` | Optional description under the header. Trimmed; blank = none. | | `selectedByDefault` | Whether this group's channels start checked in the export UI's channel filter. Only `true` counts. | | `order` | Optional explicit ordering hint (lower first); floored. Unordered groups sort after ordered ones, then by name. | | `accent` | Provenance accent, set only in hub mode where each group is a federated site (id = origin): that site's own accent, drawn as a swatch on the group header. Never read from a site.json — absent in single-site mode. | | `inline` | Render this group's channels as loose individual chips instead of one collapsible group chip. Stored only when `true`. | Default: ```json [ { "id": "default", "name": "All channels", "selectedByDefault": true, "inline": true } ] ``` ## `defaultGroupId` The group a channel falls into when its membership names none (or an unknown one). Must name a configured group on save; on read an unknown value resolves to the first group. Default: `"default"` ## `channels` The channels this site exposes. A channel absent from this list is not built or deployed for this site even though its data exists in the pool. #### `channels[]` Per entry — each entry spells its own values. | Key | Description | |---|---| | `slug` | Channel slug (its directory name under `transcripts/channels/`). Blank and duplicate slugs are dropped. | | `groupId` | Group this channel belongs to WITHIN this site. The same channel can sit in different groups on different sites. A value naming no configured group is dropped on read and falls back to `defaultGroupId` at render time. | | `order` | Optional explicit ordering hint within the site (lower first); floored to an integer. | Default: ```json [] ``` ## `cloudflareProject` Cloudflare Pages project name this site deploys to (`wrangler pages deploy out --project-name `). Trimmed; blank = none. Default: absent ## `accent` Per-site brand accent: a named accent id (`signal`, `brass`, `vermilion`, `violet`, `sakura`, `blue`, `green`) or a custom `"#rrggbb"`. It is the site's accent on every page; a reader does not pick one. Absent = `signal`, the family default. A custom hex is darkened or lightened per base until it reaches 4.5:1. The public `/site.json` always carries a hex: an id is published as its on-dark value. Any other spelling is dropped. Default: absent ## `siteUrl` Absolute public URL of this site's deployment, e.g. `https://jeralyzer.pages.dev` (trimmed, trailing slashes removed; anything not absolute http(s) is dropped). Drives the cross-site footer: a site with no siteUrl is omitted from every other site's list. Default: absent ## `listed` Whether the family lists this site. Opt-OUT: absent/true = listed, only an explicit `false` is written. An unlisted site still builds and deploys as before, and its own pages are unchanged; it is left out of the homepage (cards, chart, `/stats`), the hub (members, federated search, `/corpus.json`, `/llms.txt`), every other site's footer, and the published `channel-sites.json` and pooled `stats/`. A channel only unlisted sites expose is in none of the family's public totals; a channel a listed site also exposes is credited to the listed one. A private site and a report-only site (`search: false`) are never listed, whatever `listed` says. Default: `true` ## `audience` Who this site is built for. `"public"` (the default; absent) or `"private"`: the operator's own reading copy, built on this machine and never deployed — the deploy stage refuses it before any upload — Deploy production, Deploy preview, Deploy local, Build & deploy, Publish now, the publish lane, `archilyzer publish deploy`, docker/publish-site.sh — while a build without a deploy still works (its publish policy reads as `build`). A private site is never listed (as `listed: false`, whatever `listed` says), publishes no `hubUrl`, and its `/corpus.json` says `"audience": "private"`. Content kept from the public — X posts while `social.x.visibility` is `"private"` — is built only into private sites. Only `"private"` is written. Default: absent ## `reports` The site's published reports, in display order: report ids (lowercase slugs, `[a-z0-9][a-z0-9-]*`), each a directory under `sites//reports/`. A report directory not named here is a draft and is not published. Invalid and repeated ids are dropped. Absent/empty = no reports. Default: ```json [] ``` ## `relatedSites` Pulls specific siblings to the front of the footer's cross-site list, in named groups. Siblings not named here fall into a trailing "Other sites" group. Absent/empty = one flat list of every sibling. #### `relatedSites[]` Per entry — each entry spells its own values. | Key | Description | |---|---| | `label` | Optional muted heading shown above the group; omit for an unlabeled group. | | `siteIds` | Sibling site ids, in display order. Invalid and repeated ids are dropped, and a group left with none is dropped. Ids are resolved against the live pool at render time, so an id for a site that does not exist (yet) is harmless — it is skipped. | Default: ```json [] ``` ## `search` Whether this site publishes its searchable corpus. Opt-OUT: absent/true = on, only an explicit `false` is written. Off = report-only: the site publishes only its `reports` and the moments they cite — no search, browse, transcripts or archives; `channels` is the pool its citations resolve against, and its `/corpus.json` says `"scope": "cited"`. A report-only site is never listed (as `listed: false`, whatever `listed` says). Legacy `publish: "cited"` reads as `false`. Default: `true` ## `pwa` Whether this site ships an installable PWA (service worker + web manifest). Default false: a "dumb instance" that serves the CORS-enabled JSON federation contract but is not independently installable, so a visitor trusts only the hub PWA. Stored only when true. Default: `false` ## `archives` Whether the site build generates downloadable transcript/live-chat archive zips (and links them on the Downloads page). Opt-OUT: absent/true = on, only an explicit `false` disables. Also gated by the global setting and a per-build flag. Default: `true` ## `duplicates` Whether this site publishes the Duplicates page (and its header link). Opt-OUT: absent/true = on, only an explicit `false` hides it. Even when on, the page auto-hides when the site has no in-scope duplicate clusters. Default: `true` ## `transcriptDownloads` Whether a visitor gets the per-video export controls in the transcript modal: the Download menu (txt / srt / json) and Copy MD. Opt-OUT: absent/true = on, only an explicit `false` hides them. The yt-dlp clip command is not a download (it copies a line, serves no file) and shows either way. The machine contract (`/corpus.json`, manifests, shards, `llms.txt`) is served either way. The editor always shows them. Default: `true` ## `archiveMaxBytes` Per-site served-file size cap in bytes: any archive larger is dropped from what is served and flagged in the manifest, so a capped host (Cloudflare Pages: 25 MB) will not reject the deploy. 0 = no cap. Absent = the global default. Negative or non-numeric values are dropped. Default: absent ## `hubUrl` Per-site override for the hub this site belongs under. Absent = the family default, `settings.json` `homepageUrl`. Published on the public `/site.json` and `/corpus.json` so a hub can tell member sites from arbitrary added origins; the header does not link to it (release 14). Default: absent ## `publish` What the publish lane — and Publish now — may do with this site when it is stale (release 18): `{ "auto": "off" | "build" | "preview" | "production" }`. Absent = `off`: the lane leaves the site alone (a manual Build or Deploy still works, and "Build all stale" still builds it). `build` rebuilds its bundle; `preview` also deploys it to the Pages preview branch `settings.json` `publish.previewBranch` names; `production` deploys it to production. A private site is clamped to `build` (it is never deployed); `preview` and `production` need a `cloudflareProject` — a save without one is refused, and a file that says so anyway reads as `build`. Only a policy other than `off` is written. Default: absent #### `publish` Per entry — each entry spells its own values. | Key | Description | |---|---| | `auto` | The policy: `off` (default), `build`, `preview` or `production` — see `publish` above. |