# About Source: https://svocs.dev/docs/about What SVOCS is and where to find it. ## About SVOCS SVOCS is a markdown-first documentation site generator built on SvelteKit and Svelte 5. It's the framework this site is written with — every page you're reading, including this one, is a plain `.md` file in `content/`. ## Status SVOCS is under active development. The core is stable — content pipeline, routing, search, theming, and navigation all work as documented — but the API isn't frozen yet, and things may still shift before a 1.0. ## Open source SVOCS is fully open-source. Issues, discussions, and pull requests are welcome on [GitHub](https://github.com/juddisjudd/svocs). If you'd like to support development, see [Sponsors](/sponsors). ## License MIT. --- # AI & LLMs Source: https://svocs.dev/docs/ai llms.txt, llms-full.txt, and per-page raw markdown for AI tools. ## The problem An LLM (or an agent, or a tool like Cursor) reading your docs by scraping rendered HTML gets your sidebar markup, your ⌘K dialog's DOM, your footer — noise around the actual content. SVOCS exposes the same content two other ways, built at the same time as the HTML, with no extra config. ## `llms.txt` A single page listing every doc, grouped by the same categories as your sidebar, each linking to its markdown source: ```txt # SVOCS > Markdown-first documentation site generator built on SvelteKit and Svelte 5. ## Getting Started - [Introduction](https://svocs.dev/docs/introduction.md): What SVOCS is, why it exists... - [Quick Start](https://svocs.dev/docs/getting-started.md): Scaffold a project... ``` Live at [`/llms.txt`](/llms.txt) — this is the emerging [llms.txt convention](https://llmstxt.org), an index an AI tool can fetch once to know what your site has and where. ## `llms-full.txt` Every page's full markdown source, concatenated, separated by `---`. One fetch, the whole site, no crawling. Live at [`/llms-full.txt`](/llms-full.txt). ## Raw markdown per page Every doc page is also available with a `.md` suffix — `/docs/introduction` renders the HTML page, `/docs/introduction.md` returns the exact same content as plain markdown, headings and code fences intact, `Content-Type: text/markdown`. Every doc page has **Copy Markdown** and **View as Markdown** buttons under its title wired to this. ## How it's built All three reuse one function, `getAllLlmsDocuments()` in `src/lib/core/content.ts` — the _unstripped_ markdown source per page, deliberately kept separate from the search system's `getAllSearchDocuments()` (which strips markup for tokenization; an AI consumer wants the real source, not stripped plain text). `llms.txt`'s category grouping walks the same page-map tree the sidebar renders from, so it never drifts out of sync with your actual navigation. Nothing here needs configuration — it's on by default, for every SVOCS site. --- # Architecture Source: https://svocs.dev/docs/architecture Content pipeline, routing strategy, and theme boundaries. ## Content pipeline `Markdown -> transform -> compile -> page map`. SVOCS currently uses mdsvex for markdown compilation and a small content registry for routing and sidebar navigation. ```mermaid graph LR A[content/*.md] --> B[mdsvex] B --> C[Svelte component] C --> D[page map] D --> E[sidebar + routes] ``` ## Theme boundary Themes render the page-map and content components. They do not own parsing or route generation. ## Routing Docs are served from `/docs/*` via an optional catch-all route, and prerendered with `adapter-static`. ## Reading time Every page's reading time comes from its word count, rounded up to whole minutes at a fixed 200 words-per-minute rate, with a one-minute floor for very short pages: $$ t = \max\left(1, \left\lceil \frac{w}{200} \right\rceil\right) $$ where $w$ is the page's word count (code fences excluded) and $t$ is the reading time shown in minutes. --- # Components Source: https://svocs.dev/docs/components Built-in Svelte components you can use inside .svx content. These components ship with SVOCS — import them from `$lib/components` in any `.svx` file. ## Callout Use `.svx` instead of `.md` for any page that needs live components. Frontmatter and sidecar `.meta.json` both work on `.svx` files too. Component imports only work in `.svx` — plain `.md` files can't have a script block. Don't nest a `Callout` inside another `Callout` — styling assumes one level. Import path: `$lib/components/Callout.svelte`, added at the top of the `.svx` file's script block alongside your other imports. ## Tabs ```sh bun install bun run dev ``` ```sh pnpm install pnpm dev ``` ```sh deno task dev ``` ## Steps ### Add a markdown file Create `content/example.md` with frontmatter or a sidecar `.meta.json`. ### Register it in the sidebar Folders pick up ordering automatically from each page's `order` field — no extra step for a flat page. ### Preview it Run `bun run dev` and visit `/docs/example`. ## Cards Bootstrap your first SVOCS site. Ship to Cloudflare Pages or GitHub Pages. Source and issue tracker. ## Collapse It compiles Markdown straight to Svelte components, so `.svx` files can mix prose with live, interactive components — no separate MDX runtime required. ## FileTree ## Bleed Break wide content — a table, a diagram, a screenshot — out of the prose column's side padding: ```sh filename="all-runtimes.sh" bun install && bun run dev pnpm install && pnpm dev deno task install && deno task dev ``` _(runs across all three package managers)_ That's the full component set. --- # Deployment Source: https://svocs.dev/docs/deployment Ship your SVOCS site as static files to any host. ## Static output SVOCS builds to plain static files, so it deploys anywhere that can serve HTML. Running the build produces a self-contained `build/` directory that already includes your search index: ```sh bun run build # → vite build && node scripts/search/postbuild.mjs ``` The postbuild step dispatches to whichever search backend is active — see [Search](/docs/search) for the full list and how to switch. Preview the exact output locally before shipping it: ```sh bun run preview ``` ## What the build contains - Prerendered HTML for every docs page and the landing page - Hashed JS/CSS assets with immutable cache headers ready - Your search index — shape depends on the active backend, see [Search](/docs/search) - Everything from `static/` copied verbatim (including `.nojekyll`) ## Host requirements There are none beyond static file serving — no Node server, no functions, no environment variables at runtime. The two guides in this section walk through concrete setups: - [Cloudflare Pages](/docs/deployment/cloudflare-pages) — zero-config, great default choice - [GitHub Pages](/docs/deployment/github-pages) — free hosting straight from your repository > Deploying under a sub-path (like `https://user.github.io/my-repo/`)? Set the `BASE_PATH` environment variable at build time. The GitHub Pages guide covers this. --- # Cloudflare Pages Source: https://svocs.dev/docs/deployment/cloudflare-pages Deploy SVOCS to Cloudflare Pages with zero configuration. ## Connect your repository 1. In the Cloudflare dashboard, go to **Workers & Pages → Create → Pages → Connect to Git**. 2. Select your repository and the production branch. 3. Use these build settings: | Setting | Value | | ---------------------- | -------------------------- | | Framework preset | None (or SvelteKit static) | | Build command | `npm run build` | | Build output directory | `build` | That's it — sites deploy at the root of a `*.pages.dev` domain, so no `BASE_PATH` is needed. > Prefer Bun? Set the build command to `bun run build`. Cloudflare's build image detects `bun.lock` and provisions Bun automatically. ## Deploy from the CLI instead If you'd rather push builds directly (or from your own CI), use Wrangler: ```sh bun run build bunx wrangler pages deploy build --project-name my-docs ``` ## Production tips - **Preview deployments** — every pull request automatically gets its own preview URL. - **Custom domains** — add one under **Custom domains** in the project settings; TLS is provisioned for you. - **Redirects/headers** — drop a `_redirects` or `_headers` file into `static/` and it ships with the build: ```txt filename="static/_redirects" /old-guide /docs/getting-started 301 ``` - **Search works out of the box** — the Pagefind index is part of `build/`, no extra step required. --- # GitHub Pages Source: https://svocs.dev/docs/deployment/github-pages Publish SVOCS from your repository with GitHub Actions. ## Sub-path hosting and BASE_PATH Project sites are served from `https://.github.io//` — a sub-path, not the domain root. Tell the build about it with the `BASE_PATH` environment variable: ```sh BASE_PATH=/my-repo bun run build ``` Every internal link, asset URL, and the search index loader respect this automatically. (User/organization sites served from the domain root can skip `BASE_PATH` entirely.) A `.nojekyll` file already ships in `static/`, so GitHub won't run the output through Jekyll or strip underscore-prefixed asset folders. ## Deploy with GitHub Actions Create `.github/workflows/deploy.yml`: ```yaml name: Deploy docs to GitHub Pages on: push: branches: [main] permissions: contents: read pages: write id-token: write jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - run: bun install --frozen-lockfile - run: bun run build env: BASE_PATH: /${{ github.event.repository.name }} - uses: actions/upload-pages-artifact@v3 with: path: build deploy: needs: build runs-on: ubuntu-latest environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - id: deployment uses: actions/deploy-pages@v4 ``` Then in your repository settings, set **Settings → Pages → Source** to **GitHub Actions**. Every push to `main` builds and publishes the site. > Using npm instead of Bun? Swap the two Bun steps for `actions/setup-node@v4`, `npm ci`, and `npm run build`. ## Verify locally You can reproduce exactly what Pages will serve: ```sh BASE_PATH=/my-repo bun run build bun run preview # visit http://localhost:4173/my-repo ``` --- # Quick Start Source: https://svocs.dev/docs/getting-started Scaffold a project and ship your first page. ## Scaffold a project The fastest way to start is the `create-svocs-docs` starter, which drops in a working SVOCS site with this exact page structure — sidebar, search, theming, and all: ```sh filename="bun" bunx create-svocs-docs@latest my-docs ``` ```sh filename="npm" npm create svocs-docs@latest my-docs ``` ```sh filename="pnpm" pnpm create svocs-docs my-docs ``` ```sh filename="deno" deno run -A npm:create-svocs-docs my-docs ``` Answer the prompts, then start the dev server: ```sh cd my-docs bun install bun run dev ``` Your new site is live at `http://localhost:5173`. The prompts also let you pick an accent color, a search backend, and, optionally, generate baseline content from an existing GitHub repo instead of the generic starter pages — see [Theming](/docs/theming) and [Repo Analysis](/docs/repo-analysis). ## Add a page Every file under `content/` becomes a route. Drop a markdown file at `content/hello.md`: ```md filename="content/hello.md" --- title: Hello description: My first SVOCS page. --- Hello from SVOCS. ``` Save it, and `/docs/hello` appears — no route file, no manual registration. The sidebar picks it up automatically, sorted alongside your other pages. ## Control the sidebar Ordering and labels come from a `_meta.json` file next to the pages it applies to: ```json filename="content/_meta.json" { "items": { "hello": { "title": "Hello, World", "order": 1 } } } ``` `_meta.json` is also how you group pages under category headings, like "Getting Started" and "Guides" in this sidebar — see [Navigation](/docs/navigation) for the full schema. ## Build for production ```sh bun run build ``` This prerenders every page with `adapter-static` and indexes the site with Pagefind, so `bun run preview` serves the exact static output you'll deploy. See [Deployment](/docs/deployment) for Cloudflare Pages and GitHub Pages walkthroughs. ## Next steps - [Writing Content](/docs/writing-content) — frontmatter, sidecar metadata, GFM, code blocks - [Components](/docs/components) — the built-in `.svx` component library - [Theming](/docs/theming) — change the accent color, or the rest of the palette - [Navigation](/docs/navigation) — the full `_meta.json` schema - [Repo Analysis](/docs/repo-analysis) — generate starter content from an existing repo, heuristically or with an AI --- # Introduction Source: https://svocs.dev/docs/introduction What SVOCS is, why it exists, and how the pieces fit together. ## What is SVOCS SVOCS is a markdown-first documentation site generator built on SvelteKit and Svelte 5. Write plain Markdown, drop in live Svelte components exactly where you need them, and ship a fully static site with almost no client-side JavaScript. ## Why SVOCS - **Markdown-first, components when you need them.** `.md` files are plain prose. Switch a file to `.svx` and it can import and render real Svelte components inline. - **File-system routing.** `content/` maps directly to `/docs/*` — no route config to maintain. - **Zero-config search.** Every build indexes your docs with Pagefind. No server, no API keys. - **Static output.** Every page prerenders via `adapter-static`. The Svelte compiler moves work to build time, so readers download very little JavaScript. - **Runes-first.** Sidebar state, theme, search — all built on Svelte 5 runes, no legacy stores. ## How the pieces fit together ```txt content/ Your markdown source _meta.json Sidebar ordering, titles, and category labels getting-started.md src/lib/core/ Content pipeline: parsing, page-map, metadata src/lib/themes/docs/ The docs theme: sidebar, search, TOC ``` `_meta.json` is the file worth understanding first — it controls everything about how your sidebar looks, independent of your file names. The [Navigation](/docs/navigation) guide covers it in full, including how to group pages under non-clickable category labels like the ones in this sidebar. ## Next steps Head to [Quick Start](/docs/getting-started) to scaffold your first page, or jump straight to [Navigation](/docs/navigation) if you already have content and just want to organize it. --- # Navigation Source: https://svocs.dev/docs/navigation The _meta.json schema: ordering, titles, precedence, and category separators. ## What `_meta.json` controls Drop a `_meta.json` file in any `content/` folder (including the root) to control that folder's slice of the sidebar — order, display titles, and category grouping — independent of file names or frontmatter. ```json filename="content/_meta.json" { "items": { "introduction": { "order": 1 }, "getting-started": { "title": "Quick Start", "order": 2 } } } ``` Each key under `items` is a file or folder name (no extension) relative to that `_meta.json`. ## Fields - `title` — overrides the display title, in both the sidebar and on the page itself. - `order` — a sort key. Lower sorts first. Items without an explicit order default to `999` and sort after everything that has one. - `type: "separator"` — turns this entry into a non-clickable heading (see below) instead of pointing at a real file. ## Precedence A page's title and order can come from four places. From highest priority to lowest: 1. `_meta.json` in the page's directory 2. The page's own sidecar `name.meta.json` 3. The page's own frontmatter 4. Auto-derived from the filename (title-cased) and `order: 999` Each field resolves independently — a page can take its `order` from `_meta.json` while its `title` still comes from frontmatter, if `_meta.json` doesn't set a title for that key. This means `_meta.json` is the right place to reorganize navigation without touching content files: renaming a sidebar label, reordering pages, or moving a page into a different category is a one-line change in `_meta.json`, no matter what the page's own frontmatter says. ## Category separators Set `type: "separator"` to inject a non-clickable heading into the sidebar at a given position — useful for grouping pages under labels like **Getting Started** or **Guides** without those labels being real pages: ```json filename="content/_meta.json" { "items": { "getting-started-heading": { "type": "separator", "title": "Getting Started", "order": 1 }, "introduction": { "order": 2 }, "getting-started": { "title": "Quick Start", "order": 3 }, "guides-heading": { "type": "separator", "title": "Guides", "order": 4 }, "writing-content": { "order": 5 } } } ``` The separator's key (`getting-started-heading` above) doesn't need to match a real file — it only needs to be unique within that `items` map. Separators sort into the list by `order` exactly like real items, so they interleave naturally with the pages around them. This site's own sidebar is built this way — see `content/_meta.json` in the repository. ## Folders A folder's own title and order can be set from its _parent_ directory's `_meta.json`, keyed by the folder name: ```json filename="content/_meta.json" { "items": { "deployment": { "title": "Deploy", "order": 6 } } } ``` This only applies to folders that don't resolve to a real document (no `index.md`/`index.svx` inside them) — if the folder has an index page, that page's own title wins, since it's a real page with its own metadata. --- # Repo Analysis Source: https://svocs.dev/docs/repo-analysis Generate baseline docs content from an existing GitHub repo, heuristically or with an AI. ## The problem Scaffolding a new SVOCS site gets you generic starter pages — a "Quick Start," a "Writing Content" guide — that you're expected to replace by hand. If you already have a real project with a README, `create-svocs-docs` can generate a baseline `content/` tree from that repo instead, so you're editing real pages from the start rather than deleting placeholders. ## Two modes **Heuristic** (default, no AI, no key needed) — fetches the repo's README and splits it along its own `##` headings. Each section becomes a page; whatever comes before the first heading becomes the introduction. Fast, free, and faithful to however the repo's own README is already organized. **LLM-powered** (bring your own key — Anthropic, OpenAI, or OpenRouter) — sends real repo material (the README plus, depending on scan depth, the repo's other markdown docs, root config files, or actual source files) to a model and asks for a proper set of docs pages back: an overview plus whichever topics that material actually supports (installation, usage, configuration...), synthesized rather than mechanically split. Better output, at the cost of real API calls. OpenRouter routes to whichever backend the model you pick actually runs on (Anthropic, Google, open-weight models, and more), so it's the option if you want a model neither Anthropic nor OpenAI serves directly. ## Using it The CLI asks during scaffolding: ```txt Analyze an existing GitHub repo for a baseline docs setup? (y/N) y GitHub repo (owner/repo or URL): owner/repo Analysis mode: 1) Heuristic — reorganizes the README, no AI, no key needed (default) 2) LLM-powered — an AI rewrites the content into docs pages (bring your own key) ``` Picking LLM-powered adds a provider prompt, then a masked key prompt — typed once, used for validation plus that single analysis request, never written to disk. For scripted use, skip the prompts entirely: ```sh bunx create-svocs-docs my-docs --repo=owner/repo --repo-mode=heuristic bunx create-svocs-docs my-docs --repo=owner/repo --repo-mode=llm --llm-provider=anthropic bunx create-svocs-docs my-docs --repo=owner/repo --repo-mode=llm --llm-provider=openrouter --llm-model=anthropic/claude-sonnet-5 --scan-depth=deep ``` The LLM key comes from `ANTHROPIC_API_KEY`/`OPENAI_API_KEY`/`OPENROUTER_API_KEY` in this non-interactive path — it's never prompted for and never read from an env var when you're answering prompts interactively, to keep the "typed once, nothing ambient" behavior simple. ## Key validation Before spending a real analysis call, the key gets checked against the provider — free, since it costs no tokens (a models-list request for Anthropic/OpenAI; OpenRouter's models list is public regardless of key validity, so it uses its key-info endpoint instead). Success shows `API key validated ✓`; a bad key shows `API key invalid ✗` and falls back to heuristic immediately, rather than failing only after the "asking the AI" step. `--llm-model=`/`--scan-depth=` skip validation the same way flags skip every other prompt — non-interactive runs still validate the key itself, just without asking anything. ## Model and scan depth Once the key validates, you pick a **model**. This list is fetched live from the provider you picked, not hardcoded — providers ship new models faster than a static list could ever track (this CLI's previous curated list was already missing models that existed by the time it shipped). Anthropic and OpenAI need the key to list models; OpenAI's raw list mixes in non-chat models (embeddings, Whisper, DALL-E, etc.), so it's filtered down to chat-capable ones. OpenRouter's catalog is public and large (300+ models across every backend it routes to), so its picker is a type-to-search list rather than a short menu. If fetching the list fails for any reason, a small offline fallback set is used instead. Either way there's also a "Custom model ID…" option for anything the list doesn't surface. `--llm-model=` sets it non-interactively — for OpenRouter, model IDs are `provider/model` slugs (e.g. `anthropic/claude-sonnet-5`, `openai/gpt-5.4`), not the same string you'd pass to that provider's own native API. Then a **scan depth**, which changes both the page budget and what repo material the model gets to write from: | Depth | Pages | Material beyond the README | How pages are generated | | ----------------------- | ------- | ------------------------------------------------------------------------------------- | ----------------------------------------------- | | Quick Scan | 1 to 3 | the repo's other `.md` docs (root + `docs/`, e.g. `CONTRIBUTING.md`, guides) | one call returns every page | | Standard Scan (default) | 1 to 8 | quick's material + root config/manifest files (`Dockerfile`, `pyproject.toml`, CI, …) | a plan call, then one call per page | | Deep Scan | 1 to 12 | the entire repo, downloaded once as a tarball | a plan call, then one call per page from source | Quick and standard fetch their extra files through the same unauthenticated GitHub endpoints as the README/`package.json` fetch — no GitHub token needed, and no LLM-side web search or tool use involved. Deep Scan doesn't fetch file-by-file (the unauthenticated API allows only 60 requests/hour); it downloads the repo as a single archive, indexes every text file in it, and shows the model the real file tree. The plan call then names which source files each page should be written from — a `cli/` directory doesn't just imply a CLI reference page exists, the page gets written from the actual CLI source. If the archive download fails, Deep Scan degrades to standard-depth material and says so. `--scan-depth=` sets it non-interactively (defaults to `standard`). Because standard and deep generate pages one at a time, a single malformed or truncated page response only skips that page (with a warning naming it) instead of discarding the whole analysis, and the spinner reports progress per page. There's no request timeout on the generation calls themselves — they pair with whatever model you picked, including the slowest one on any given provider. Rather than guess a cutoff that's wrong for some combination of scan depth and model, each request just runs until it finishes or errors on its own; Ctrl+C cancels at any point, same as any other prompt in this CLI. ## What gets replaced Generated pages replace **all** of the starter content — introduction, quick start, writing content, components, AI & LLMs, theming, navigation, search, deployment, about — with whatever the analysis produced, so a repo-analysis scaffold ends up entirely about the repo you pointed it at rather than a mix of your content and generic SVOCS reference pages. ## It never blocks the scaffold Every failure mode — repo not found, GitHub rate limit, no README, a rejected key, a failed archive download, a malformed AI response — degrades one tier instead of stopping the CLI: Deep Scan falls back to standard-depth material, LLM-powered falls back to heuristic, heuristic falls back to leaving the normal starter content in place untouched. Warnings now say _why_ an AI response was rejected (truncated at the token limit, unparseable JSON, a missing field) instead of a generic "not valid". Relative links and images in the source README (`[LICENSE](LICENSE)`) are rewritten to absolute GitHub URLs rather than left pointing at paths that don't exist on the new site. ## Reference - `packages/create-svocs-docs/lib/repo-analysis.mjs` — fetch, generation, and validation logic - `--repo=`, `--repo-mode=`, `--llm-provider=`, `--llm-model=`, `--scan-depth=` CLI flags --- # Search Source: https://svocs.dev/docs/search Five interchangeable backends behind one switch — Pagefind, Orama, FlexSearch, Typesense, and Chroma. ## Picking a backend SVOCS ships five interchangeable search backends behind one switch: **Pagefind** (the zero-config default), **Orama**, **FlexSearch**, **Typesense**, and **Chroma**. Every page in the docs UI — the ⌘K dialog, keyboard navigation, result rendering — is identical no matter which one is active; only how the index gets built and queried changes. Set `PUBLIC_SVOCS_SEARCH_PROVIDER` at build time to switch: ```sh PUBLIC_SVOCS_SEARCH_PROVIDER=orama bun run build ``` Leave it unset and you get Pagefind — nothing to configure, nothing to run. ## The three shapes | Backend | How the index is built | Who runs it | Live server? | | ------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------- | ------------------------- | | [Pagefind](/docs/search/pagefind) | WASM search index generated from your built HTML | A post-build CLI step | No | | [Orama](/docs/search/orama) | Raw page content shipped as static JSON, indexed in-browser | Part of the static build | No | | [FlexSearch](/docs/search/flexsearch) | Same idea, different in-browser index library | Part of the static build | No | | [Typesense](/docs/search/typesense) | Content pushed to a collection on your Typesense server | A sync script after the build | Yes, self-hosted or cloud | | [Chroma](/docs/search/chroma) | Content pushed to a collection on your Chroma server (semantic search — server embeds it for you) | A sync script after the build | Yes, self-hosted | Pagefind, Orama, and FlexSearch never leave the static-only story SVOCS is built around — nothing to run, nothing to host, nothing that can go down. Typesense and Chroma trade that away for a live, queryable server: better relevance tuning and (for Chroma) genuine semantic search, at the cost of standing up and securing a server the browser talks to directly. ## Adding a backend to a scaffolded project `create-svocs-docs` only ships Pagefind by default, to keep a fresh scaffold's install small. The other four backends are documented, working code you copy in when you actually want them — each backend's page below has the exact files and the one package to install. --- # Chroma Source: https://svocs.dev/docs/search/chroma ## Setup ```sh bun add chromadb ``` You'll need a running Chroma server — self-hosted (Coolify, Docker) is the common path. ## This is semantic search, not keyword search Chroma is a vector database. Your query and every indexed page get turned into embeddings and matched by similarity, not exact text overlap — "how do I ship this" can match a page titled "Deployment" even though it never says "ship." The upside is genuinely better recall for natural-language questions; the trade-off is you're running a real service instead of a static index. Chroma's server embeds everything for you — `collection.add({documents})` and `collection.query({queryTexts})` both take plain text. There's no separate embedding pipeline to build or an API key for an embedding provider to manage. ## Environment variables Sync-only: ```txt CHROMA_HOST=your-chroma-host CHROMA_PORT=8000 CHROMA_SSL=true CHROMA_ADMIN_TOKEN=... PUBLIC_CHROMA_COLLECTION_NAME=svocs-docs ``` Client-safe: ```txt PUBLIC_SVOCS_SEARCH_PROVIDER=chroma PUBLIC_CHROMA_HOST=your-chroma-host PUBLIC_CHROMA_PORT=8000 PUBLIC_CHROMA_SSL=true PUBLIC_CHROMA_COLLECTION_NAME=svocs-docs PUBLIC_CHROMA_TOKEN=... ``` > **Security — read this before deploying.** Unlike Typesense, Chroma has no turnkey "generate a read-only key" call. A genuinely query-only credential requires manually configuring token auth (`CHROMA_SERVER_AUTHN_PROVIDER=chromadb.auth.token_authn.TokenAuthenticationServerProvider`) _and_ role-based authorization (`CHROMA_SERVER_AUTHZ_PROVIDER=chromadb.auth.simple_rbac_authz.SimpleRBACAuthorizationProvider`) scoping `PUBLIC_CHROMA_TOKEN` to the `QUERY` action only, on your server — this project's code can't do that part for you. There is also a known unauthenticated remote-code-execution advisory against Chroma instances left without auth enabled ("ChromaToast"). Never expose a Chroma instance publicly without token auth and TLS already configured and verified. ## Building ```sh bun run build ``` `scripts/search/postbuild.mjs` runs `bun run scripts/search/sync-chroma.ts` automatically — it reads `build/search-index.json`, deletes and recreates the collection (so removed/renamed pages never linger), and adds every page's text content with its url/title/description as metadata. ## Reference - Sync script: `scripts/search/sync-chroma.ts` (also runnable directly: `bun run search:sync:chroma`) - Client: `src/lib/search/providers/chroma-client.ts` --- # FlexSearch Source: https://svocs.dev/docs/search/flexsearch ## Setup ```sh bun add flexsearch ``` ```sh PUBLIC_SVOCS_SEARCH_PROVIDER=flexsearch bun run build ``` No server, no API keys, no sync step — same static-only shape as Orama, different index library. ## How it works A prerendered route, `/search-index.flexsearch.json`, serves FlexSearch's exported index chunks — FlexSearch's `export()` is callback-based and multi-chunk (one call per internal data structure), so every `{key, data}` pair gets collected into one JSON object rather than a single blob. The `⌘K` dialog fetches that once and calls `import(key, data)` for each entry to rebuild the index client-side, then searches with `enrich: true, merge: true` to get full documents back per hit instead of bare ids. ## A shared config, on purpose `src/lib/search/providers/flexsearch-config.ts` holds one `Document` config, imported by both the indexer and the client. FlexSearch's exported chunks only re-import correctly against a `Document` instance built from an identical config to the one that exported them — two separately-written configs would drift silently. ## Reference - Shared config: `src/lib/search/providers/flexsearch-config.ts` - Indexer: `src/lib/search/providers/flexsearch-indexer.ts` - Route: `src/routes/search-index.flexsearch.json/+server.ts` - Client: `src/lib/search/providers/flexsearch-client.ts` --- # Orama Source: https://svocs.dev/docs/search/orama ## Setup ```sh bun add @orama/orama ``` ```sh PUBLIC_SVOCS_SEARCH_PROVIDER=orama bun run build ``` That's the whole configuration — no server, no API keys, no sync step. ## How it works A prerendered route, `/search-index.orama.json`, serves every page's `{id, url, title, content}` as a plain JSON array — built during `vite build` itself, guarded so it 404s (and is skipped) on any other backend. The `⌘K` dialog fetches that JSON once, builds a fresh in-memory Orama index with `create()` + `insertMultiple()`, and searches locally from then on. ## Why build the index in the browser instead of shipping a pre-built one? Orama does support serializing a pre-built database via `@orama/plugin-data-persistence`, but that plugin pulls in `dpack`, which depends on Node's `Transform` streams — unavailable in the browser, and it breaks at runtime even when you only use the `'json'` format ([orama#876](https://github.com/oramasearch/orama/issues/876)). Shipping raw documents and indexing client-side sidesteps the dependency entirely, and re-indexing a few dozen docs in-browser is effectively free. ## Reference - Indexer: `src/lib/search/providers/orama-indexer.ts` - Route: `src/routes/search-index.orama.json/+server.ts` - Client: `src/lib/search/providers/orama-client.ts` --- # Pagefind Source: https://svocs.dev/docs/search/pagefind ## The default Pagefind is what you get with no configuration at all — `PUBLIC_SVOCS_SEARCH_PROVIDER` unset, nothing to install beyond what's already in `package.json`. ## How it works `bun run build` does two things: `vite build` prerenders every page to static HTML, then `pagefind --site build` crawls that HTML and writes a WASM-powered search index into `build/pagefind/`. The `⌘K` dialog dynamically imports `pagefind/pagefind.js` on first use and queries it entirely client-side — no server, no API keys. ## Testing it locally Pagefind's index only exists after a build — `bun run dev` has nothing to query, so `⌘K` will 404 on `pagefind/pagefind.js` there by design. Run `bun run build` then `bun run preview` to test it against the real index. ## Why you might switch away from it Pagefind indexes rendered HTML, so results are whatever text ends up on the page — including decorative UI text if you're not careful with `data-pagefind-ignore`. If you want cleaner excerpts sourced from frontmatter/sidecar `description` fields instead, see [Orama](/docs/search/orama) or [FlexSearch](/docs/search/flexsearch); if you want semantic ("how do I deploy this" matching a page that never says those words) search, see [Chroma](/docs/search/chroma). ## Reference - Indexing: `pagefind --site build`, dispatched by `scripts/search/postbuild.mjs` - Client: `src/lib/search/providers/pagefind-client.ts` --- # Typesense Source: https://svocs.dev/docs/search/typesense ## Setup ```sh bun add typesense ``` You'll need a running Typesense server — self-hosted (Coolify, Docker, anywhere) or Typesense Cloud. ## Environment variables Sync-only — never expose these to the browser: ```txt TYPESENSE_HOST=your-typesense-host TYPESENSE_PORT=443 TYPESENSE_PROTOCOL=https TYPESENSE_ADMIN_API_KEY=... ``` Client-safe — these ship to the browser, so `PUBLIC_TYPESENSE_SEARCH_API_KEY` must be a **search-only, collection-scoped** key, not the admin key above: ```txt PUBLIC_SVOCS_SEARCH_PROVIDER=typesense PUBLIC_TYPESENSE_HOST=your-typesense-host PUBLIC_TYPESENSE_PORT=443 PUBLIC_TYPESENSE_PROTOCOL=https PUBLIC_TYPESENSE_COLLECTION_NAME=svocs-docs PUBLIC_TYPESENSE_SEARCH_API_KEY=... ``` Generate the scoped key once, manually, against your admin key — this isn't part of the automated sync: ```js await client.keys().create({ actions: ['documents:search'], collections: ['svocs-docs'] }); ``` ## Building ```sh bun run build ``` `scripts/search/postbuild.mjs` runs `bun run scripts/search/sync-typesense.ts` automatically after the static build — it reads the already-prerendered `build/search-index.json`, recreates the collection, and imports every page with `action: 'upsert'`. A full recreate (not an incremental sync) is deliberate: it guarantees removed or renamed pages never leave stale entries behind. ## CORS Set your Typesense server's CORS allowlist to your exact production origin (`https://svocs.dev`) plus your local dev origin (`http://localhost:5173`). Don't wildcard it — the search-only key is safe to expose, but only from origins you actually control. ## Reference - Sync script: `scripts/search/sync-typesense.ts` (also runnable directly: `bun run search:sync:typesense`) - Client: `src/lib/search/providers/typesense-client.ts` --- # Theming Source: https://svocs.dev/docs/theming Pick an accent color during setup, or hand-edit the CSS custom properties directly. ## During setup `create-svocs-docs` asks for an accent color when scaffolding a new site: ```txt Accent color (hex): (#ff3c00) #2563eb ``` Leave it blank to keep the default ember orange, or type any hex color — non-interactively, pass `--accent=#2563eb`. Everything else — buttons, links, badges, the search dialog's focus ring, the header glow — updates to match. ## After setup The whole palette lives in `src/routes/+layout.svelte`, in one `