# Sveltepress > SvelteKit docs & blog SSG — Svelte 5 in Markdown, versioning, i18n, and Pagefind. # [SvelteKit docs & blog SSG](https://sveltepress.site/) --- ## guide # [Configuration](https://sveltepress.site/guide/blog-theme/configuration) All options are passed to `blogTheme()` in `vite.config.ts`. ## `BlogThemeOptions` | Option | Type | Default | Description | |---|---|---|---| | `title` | `string` | — | Site title. Used in ``, sidebar brand, OG tags, RSS feed. | | `description` | `string` | — | Site description. Used for meta + OG. | | `base` | `string` | — | Fully-qualified site URL (e.g. `https://example.com`). Used to produce absolute RSS links and OG image URLs. | | `author` | [`AuthorProfile`](#authorprofile) | — | Author identity shown in the sidebar. | | `about` | `{ html: string }` | — | Raw HTML block rendered below author info in the sidebar (keep it short — About no longer has a standalone page). | | `navbar` | `Array<{ title, to }>` | — | Nav links rendered below author info. `to` values are automatically prefixed with SvelteKit's `base`. | | `themeColor` | [`ThemeColor`](#themecolor) | Ember palette | Dark-mode palette override. | | `themeColorLight` | [`ThemeColor`](#themecolor) | warm cream | Light-mode palette override. | | `defaultMode` | `'system' \| 'dark' \| 'light'` | `'system'` | Initial colour mode before user toggles. | | `postsDir` | `string` | `'src/posts'` | Directory scanned for `*.md` posts. | | `pageSize` | `number` | `12` | Posts per page on the home listing. | | `highlighter` | [`HighlighterOptions`](#highlighteroptions) | — | Shiki/Twoslash options. | | `rss` | `{ enabled, limit, copyright }` | enabled, 20 | RSS feed generation. Writes to `static/rss.xml`. | | `ogImage` | `{ enabled, fontPath, tagline }` | enabled | Per-post OG image generation via Satori. Skipped when native addons cannot load. | | `giscus` | [`GiscusConfig`](#giscusconfig) | — | Set to enable GitHub-discussions-backed comments. | | `search` | reserved | — | Do not configure yet. The current release always uses the built-in Pagefind search. | ## `AuthorProfile` ```ts interface AuthorProfile { name: string avatar?: string // absolute URL or /-prefixed static/ path bio?: string socials?: { github?: string // username, not URL twitter?: string // username, not URL mastodon?: string // full profile URL bluesky?: string // handle like 'name.bsky.social' email?: string website?: string // full URL rss?: string // full URL or /-prefixed path } } ``` ## `ThemeColor` ```ts interface ThemeColor { primary?: string // default '#fb923c' secondary?: string // default '#dc2626' bg?: string // default '#1a0a00' surface?: string // default '#2d1200' } ``` ## `HighlighterOptions` ```ts interface HighlighterOptions { themeDark?: string // Shiki theme, default 'night-owl' themeLight?: string // Shiki theme, default 'vitesse-light' languages?: string[] // extra languages merged with defaults twoslash?: boolean // enable Twoslash TypeScript hover info } ``` ## `GiscusConfig` ```ts interface GiscusConfig { repo: `${string}/${string}` repoId: string category: string categoryId: string mapping?: 'pathname' | 'url' | 'title' | 'og:title' | 'specific' | 'number' reactionsEnabled?: boolean inputPosition?: 'top' | 'bottom' lang?: string } ``` Fill in these values from [giscus.app](https://giscus.app/). A `GiscusComments` component is rendered below each post automatically when `giscus` is set. Theme switching (light/dark) is wired through automatically. ## Environment variables The example site reads two env vars at build time: | Var | Effect | |---|---| | `BASE_PATH` | SvelteKit `paths.base`. Set when deploying under a subpath. | | `SITE_URL` | Overrides `blogTheme()` `base`. Used for OG/RSS absolute URLs. | The theme itself doesn't read these — they're wired by your `svelte.config.js` and `vite.config.ts`. --- # [Customisation](https://sveltepress.site/guide/blog-theme/customization) ## Colour palette Override the dark or light palette via `themeColor` / `themeColorLight`: ```ts // @noErrors blogTheme({ themeColor: { primary: '#fb923c', secondary: '#dc2626', bg: '#1a0a00', surface: '#2d1200', }, themeColorLight: { primary: '#c2410c', bg: '#fef9f0', surface: '#fde8c8', }, }) ``` Any key you omit keeps the theme default. At runtime these are injected as CSS custom properties scoped to `[data-theme="dark"] .sp-blog-root` and `[data-theme="light"] .sp-blog-root`. ## CSS custom properties All components read these variables. Override them in your own styles for fine-grained control: | Variable | Purpose | |---|---| | `--sp-blog-bg` | Page background | | `--sp-blog-surface` | Card/sidebar background | | `--sp-blog-border` | Separators, card borders | | `--sp-blog-text` | Primary text (titles) | | `--sp-blog-content` | Body text | | `--sp-blog-muted` | Meta text (dates, counts) | | `--sp-blog-primary` | Accent (links, tags, buttons) | | `--sp-blog-secondary` | Secondary accent | ## Swapping components The scaffolded files under `src/routes/` import components from `@sveltepress/theme-blog/components/*`. To replace one — say the post cards — edit `src/routes/+page.svelte` and substitute your own component: ```svelte title="src/routes/+page.svelte" <script lang="ts"> import MyGrid from '$lib/MyGrid.svelte' const { data } = $props() </script> <MyGrid posts={data.posts} /> ``` The theme re-exports these building blocks: - `@sveltepress/theme-blog/GlobalLayout.svelte` - `@sveltepress/theme-blog/PageLayout.svelte` - `@sveltepress/theme-blog/PostLayout.svelte` - `@sveltepress/theme-blog/components/MasonryGrid.svelte` - `@sveltepress/theme-blog/components/Timeline.svelte` - `@sveltepress/theme-blog/components/Pagination.svelte` - `@sveltepress/theme-blog/components/Sidebar.svelte` - `@sveltepress/theme-blog/components/SearchModal.svelte` - `@sveltepress/theme-blog/components/ReadingProgress.svelte` - `@sveltepress/theme-blog/components/GiscusComments.svelte` - `@sveltepress/theme-blog/components/RelatedPosts.svelte` - `@sveltepress/theme-blog/components/TaxonomyHeader.svelte` - `@sveltepress/theme-blog/components/AuthorCard.svelte` - `@sveltepress/theme-blog/components/AuthorProfile.svelte` ## Data modules The package ships TypeScript declarations for the virtual modules used by scaffolded routes. The main customization points are: | Module | Export | |---|---| | `virtual:sveltepress/blog-posts-meta` | `posts: BlogPostMeta[]` | | `virtual:sveltepress/blog-tags-index` | `tags: Array<{ name, count }>` | | `virtual:sveltepress/blog-categories-index` | `categories: Array<{ name, count }>` | | `virtual:sveltepress/blog-config` | `blogConfig: BlogThemeOptions` | Per-post, per-tag, and per-category records are also exposed through literal module IDs such as `virtual:sveltepress/blog-post/<slug>`. `virtual:sveltepress/blog-runtime` contains absolute cache paths for server loads and must not be imported into client components. ## Deploying under a subpath Read `BASE_PATH` in `svelte.config.js` and `SITE_URL` in `vite.config.ts`: ```js title="svelte.config.js" export default { kit: { paths: { base: process.env.BASE_PATH ?? '', relative: false, }, }, } ``` ```ts title="vite.config.ts" // @noErrors blogTheme({ base: process.env.SITE_URL ?? 'http://localhost:4173', // ... }) ``` Then build with both set, e.g. for GitHub Pages project site at `user.github.io/repo/blog`: ```bash BASE_PATH=/repo/blog SITE_URL=https://user.github.io/repo/blog \ pnpm build ``` All internal links in the theme components use SvelteKit's `$app/paths` `base`, so they resolve correctly under the subpath. OG image URLs and RSS item URLs use `SITE_URL` so social crawlers get fully-qualified URLs. ## Adding pages Anything under `src/routes/` that isn't reserved by the scaffolder is yours. Wrap custom pages in `GlobalLayout` automatically by leaving `src/routes/+layout.svelte` untouched — every route renders inside the sidebar + main grid. --- # [Features](https://sveltepress.site/guide/blog-theme/features) Everything on this page is on by default unless noted. ## Sidebar with author identity The left rail shows the site brand, author avatar + name + bio + socials, optional About HTML block, nav links, search button, and theme toggle. On viewports below 1024 px it collapses to a stacked top bar and hides the verbose bits (bio, socials, about). The avatar, name, bio, and socials come from `author` in your config. The About block (if any) comes from `about.html`. ## Reading progress bar A 3 px bar fixed to the top of the viewport fills as the reader scrolls through a post. Rendered only on post pages. No config. ## Masonry home grid The first post is featured (full-width card with cover or gradient). Remaining posts flow into a 1/2/3-column masonry layout depending on viewport. Posts with `cover` render as large cards; posts without render as compact cards. The theme picks a tag-hashed gradient for uncovered cards so they still feel distinct. ## Timeline `/timeline/` groups non-draft posts by month, with year/month labels in a sticky left column and cards on the right connected by a rail. IntersectionObserver animates cards in on scroll. ## Tags & categories Each tag and category gets its own page built from the same masonry grid. `/tags/` also exposes an index showing every tag with its post count. Add `navbar` entries like `{ title: 'Tags', to: '/tags/' }` to link them from the sidebar. ## RSS `rss.xml` is written to `static/rss.xml` on each build. Configure: ```ts // @noErrors blogTheme({ base: 'https://example.com', rss: { enabled: true, // default limit: 20, // default copyright: `© ${new Date().getFullYear()} My Blog`, }, }) ``` Disable with `rss: { enabled: false }`. ## OG images Per-post Open Graph images are rendered with Satori + resvg at build time and written to `static/og/<slug>.png`. A site-wide image is written to `static/og/__home.png`. ```ts // @noErrors blogTheme({ ogImage: { enabled: true, // default fontPath: '/abs/path/font.ttf', // defaults to Inter Bold 700 tagline: 'Subhead under the title', }, }) ``` Set `enabled: false` to skip generation and loading the renderer entirely. The resvg renderer requires native Node.js addons, which browser containers such as StackBlitz WebContainers do not support. The theme probes that restriction before importing the renderer. If addons are disabled or the renderer cannot be loaded, it warns and skips OG PNG generation; the blog still starts and existing images are preserved. Generate fresh OG images in a native Node.js environment before deployment. For the OG URLs in meta tags to be fully-qualified (required by Facebook and most crawlers), set `base` to your site's full URL: `base: 'https://example.com'`. ## Search (Pagefind) `⌘K` / `Ctrl+K` opens the search modal. Pagefind is the indexer; run it as a post-build step: Configure `"build": "vite build && pagefind --site dist"` in `package.json`, then run `pnpm build`. In dev, the theme serves `/pagefind/` from your last build's `dist/pagefind/` directory, so search works if you've built at least once. ## Comments (Giscus) Set `giscus` in your config with values from [giscus.app](https://giscus.app/) and `GiscusComments` is rendered below each post. Theme switches (light/dark) are forwarded to the Giscus iframe automatically. ## Syntax highlighting Code blocks are highlighted with Shiki. Dual themes switch based on `data-theme`. Turn on Twoslash for TypeScript hover info: ```ts // @noErrors blogTheme({ highlighter: { themeDark: 'night-owl', themeLight: 'vitesse-light', twoslash: true, }, }) ``` Code blocks also get a copy button, optional title, line numbers, line highlighting, diff markers, and focus dimming — see the default theme docs for the fence syntax (the same markdown extensions are shared). ## Theme toggle A sun/moon button in the sidebar cycles `system → dark → light`. An anti-FOWT script is injected into `src/app.html` on first build so the chosen theme is applied before paint. ## Auto-scaffolded routes Listed in [Getting started](../getting-started/). The scaffolder only writes files that don't already exist, so your customisations are safe across upgrades. --- # [Getting started](https://sveltepress.site/guide/blog-theme/getting-started) :::tip[See it live first]{icon=noto:rocket} Before you scaffold anything, **take a look at the live demo — [sveltepress.github.io/sveltepress/blog-demo](https://sveltepress.github.io/sveltepress/blog-demo/)**. Everything described on this page is already working there. The navbar **Blog demo** is that finished showcase. The navbar **Playground** opens the Feature directory. Configuration, Writing posts, Features, and Customisation are the Blog theme Entries that auto-boot the Blog starter. This getting-started page is not a Playground Entry and has no Open in Playground control. Source: [`packages/example-blog`](https://github.com/SveltePress/sveltepress/tree/main/packages/example-blog) in the monorepo. After cloning the repository, run `pnpm install` and `pnpm --filter @sveltepress/example-blog dev` from the repository root. The demo starts on `http://localhost:36739`. ::: `@sveltepress/theme-blog` is a magazine-style blog theme with a left-rail sidebar, masonry post grid, per-post OG images, RSS, Pagefind search, and Giscus comments. This page walks you through scaffolding a working blog. ## Install @install-pkg(@sveltepress/theme-blog) The theme is loaded by the Sveltepress Vite plugin: @install-pkg(@sveltepress/vite) The theme requires `@sveltejs/adapter-static` because it generates a fully static site (prerendered HTML, JSON, RSS, OG images). @install-pkg(@sveltejs/adapter-static) Pagefind builds the local search index after Vite finishes: @install-pkg(pagefind) ## Configure Vite ```ts title="vite.config.ts" // @noErrors import { blogTheme } from '@sveltepress/theme-blog' import { sveltepress } from '@sveltepress/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ sveltepress({ theme: blogTheme({ title: 'My Blog', description: 'Thoughts on Svelte and the web.', base: 'https://example.com', author: { name: 'Your Name', avatar: '/avatar.png', bio: 'Short bio shown in the sidebar.', socials: { github: 'your-handle', twitter: 'your-handle', rss: '/rss.xml', }, }, navbar: [ { title: 'Home', to: '/' }, { title: 'Timeline', to: '/timeline/' }, { title: 'Tags', to: '/tags/' }, ], }), }), ], }) ``` ## Configure SvelteKit ```js title="svelte.config.js" import adapter from '@sveltejs/adapter-static' import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' export default { extensions: ['.svelte'], preprocess: [vitePreprocess()], kit: { adapter: adapter({ pages: 'dist', assets: 'dist', fallback: '404.html', }), prerender: { handleMissingId: 'ignore', handleUnseenRoutes: 'ignore', }, paths: { base: process.env.BASE_PATH ?? '', relative: false, }, }, compilerOptions: { runes: true, }, } ``` `BASE_PATH` lets you deploy under a subpath (e.g. GitHub Pages project site). Leave the env var unset for root deployments. ## Write your first post Create `src/posts/hello-world.md`: ```md title="src/posts/hello-world.md" --- title: Hello world date: 2026-04-17 tags: [intro] category: meta excerpt: First post on the new blog. --- # Hello Welcome to my blog. Everything is markdown. ``` ## Routes auto-scaffolded On the next `vite dev` or `vite build`, the theme writes these files if they don't exist. Edit them freely — the scaffolder only creates missing files. | Path | Purpose | |---|---| | `src/routes/+layout.ts` | Enables prerender + `trailingSlash: 'always'` | | `src/routes/+layout.svelte` | Wraps pages in `GlobalLayout` | | `src/routes/+page.{server.ts,svelte}` | Paginated home | | `src/routes/page/[n]/...` | Page 2+ of the list | | `src/routes/posts/[slug]/...` | Individual post pages | | `src/routes/tags/+page.svelte` | Tag index | | `src/routes/tags/[tag]/...` | Posts for a tag | | `src/routes/categories/[cat]/...` | Posts for a category | | `src/routes/timeline/+page.svelte` | Archive timeline | ## Build Add Pagefind to your project build script: ```txt title="package.json" { "scripts": { "build": "vite build && pagefind --site dist" } } ``` ```bash pnpm build ``` The Pagefind step indexes the built site so the built-in search modal (`⌘K` / `Ctrl+K`) works. The current theme always renders this Pagefind integration, so keep the post-build indexing step enabled. The resulting `dist/` is a static bundle deployable to any static host. --- # [Writing posts](https://sveltepress.site/guide/blog-theme/writing-posts) Posts live in `src/posts/` (configurable via `postsDir`). Each post is a single `.md` file. The filename (minus `.md`) becomes the slug and the URL path under `/posts/`. ## Frontmatter ```md title="src/posts/my-post.md" --- title: A new beginning date: 2026-04-17 author: Dongsheng tags: [svelte, web] category: engineering excerpt: A short one-liner shown in cards and RSS. cover: /covers/new-beginning.jpg draft: false --- Write markdown below the frontmatter. ``` | Field | Type | Required | Notes | |---|---|---|---| | `title` | string | yes | Shown in the post hero and list cards. | | `date` | string | yes | ISO date (`YYYY-MM-DD`). Drives sort order and timeline grouping. | | `excerpt` | string | recommended | Short description. Used in cards, RSS, OG. If omitted, the theme derives one from the body. | | `tags` | string[] | — | Rendered as pill links below the post title. Each tag gets a `/tags/<name>/` page. | | `category` | string | — | At most one per post. Gets a `/categories/<name>/` page. | | `author` | string | — | Overrides the site-wide `author.name` for this post. | | `cover` | string | — | Path or URL to a cover image. If set, the post renders with `PostCardLarge`; if not, the theme picks a tag-hashed gradient. | | `draft` | boolean | — | Drafts are excluded from generated routes, listings, RSS, OG images, and the timeline. | ## Images and links Put site-owned images in `static/` and reference them with site-absolute paths such as `/covers/new-beginning.jpg`. The theme prefixes site-absolute cover paths, Markdown image sources, and links in post bodies with SvelteKit's configured base path, so the same post works at the site root and under a subpath such as GitHub Pages. Fully qualified, protocol-relative, fragment, mail, and relative URLs remain unchanged. ## Listings - The home page (`/`) shows posts in a masonry grid with the newest post featured. - `/timeline/` shows all non-draft posts grouped by month. - `/tags/` shows all tags with their counts. - `/categories/<name>/` and `/tags/<name>/` show posts filtered by that taxonomy. ## Excerpts If you don't provide an `excerpt`, the theme derives one from the first paragraph of the body. For cleaner control, always set `excerpt` explicitly — especially for posts that open with an image or code block. ## Reading time Reading time is computed automatically from word count and shown in post cards, the timeline, and the post meta strip. Words are counted on the rendered HTML. ## Drafts Set `draft: true` in frontmatter to exclude a post from the generated site. Drafts are not written to the public post index and their `/posts/<slug>/` route is not prerendered, so direct links return 404 in a production build. Flip `draft: false` when you're ready to publish. ## Related posts Each post page shows up to a few related posts at the bottom. Relatedness is scored by shared tags, shared category, and year proximity — no config needed. --- # [Admonitions](https://sveltepress.site/guide/default-theme/admonitions) ## Grammar This feature integrated [remark-directive](https://github.com/remarkjs/remark-directive) :::important[Icon pre-build required]{icon=tabler:icons} The iconify icons should be in the [Pre-build iconify icons config](/reference/default-theme/#preBuildIconifyIcons) ::: ```md :::tip|info|note|warning|important|caution|danger[Title]{icon=icon-collection:icon-name} Some admonition content ::: ``` ## Tip ```md live no-ast :::tip[Tip title] Some tip content ::: :::tip[Tip with custom icon]{icon=ph:smiley} Some tip content ::: ``` ## Info ```md live no-ast :::info[Info title] Some info content ::: :::info[Info with custom icon]{icon=ph:smiley} Some info content ::: ``` ## Note ```md live no-ast :::note[Note title] Some note content ::: :::note[Note with custom icon]{icon=ph:smiley} Some note content ::: ``` ## Warning ```md live no-ast :::warning[Warning title] Some warning content ::: :::warning[Warning with custom icon]{icon=ph:smiley} Some warning content ::: ``` ## Important ```md live no-ast :::important[Important title] Some important content ::: :::important[Important with custom icon]{icon=ph:smiley} Some important content ::: ``` ## Caution ```md live no-ast :::caution[Caution title] Some caution content ::: :::caution[Caution with custom icon]{icon=ph:smiley} Some caution content ::: ``` ## Danger ```md live no-ast :::danger[Danger title] Some danger content ::: :::danger[Danger with custom icon]{icon=ph:smiley} Some danger content ::: ``` ## Links and inline code Links and inline code inside an admonition inherit the admonition's theme color and background: ```md live no-ast :::tip[Tip title] A [tip link](/guide/introduction/) and `tip` both match the tip color. ::: :::info[Info title] An [info link](/guide/introduction/) and `info` both match the info color. ::: :::note[Note title] A [note link](/guide/introduction/) and `note` both match the note color. ::: :::warning[Warning title] A [warning link](/guide/introduction/) and `warning` both match the warning color. ::: :::important[Important title] An [important link](/guide/introduction/) and `important` both match the important color. ::: :::caution[Caution title] A [caution link](/guide/introduction/) and `caution` both match the caution color. ::: :::danger[Danger title] A [danger link](/guide/introduction/) and `danger` both match the danger color. ::: ``` --- # [Built-in Components](https://sveltepress.site/guide/default-theme/builtin-components) :::note[Manually import]{icon=carbon:import-export} All built-in Components can directly use in markdown files. But should be imported manually in svelte files. ::: ## Links ### Props * `label` - The link label text * `to` - The link address * `target` - The link target attribute (e.g., `_blank` to open in new tab, `_self` to open in same tab). Default behavior: external links open in new tab automatically * `withBase` - Determine whether to with [sveltekit config.kit.paths.base](https://kit.svelte.dev/docs/modules#$app-paths-base). Default is `true` :::info[Auto external icon]{icon=ic:sharp-rocket-launch} Would auto add a external icon when link address starts with http or https ::: ### In markdown ```md live * <Link to="https://github.com/" label="Github" /> * <Link to="/" label="Home page" /> ``` ### In svelte ```svelte live <script> import { Link } from '@sveltepress/theme-default/components' // [svp! ~~] </script> <div style="line-height: 24px;"> <Link to="/" label="Home page" /> <br /> <Link to="https://github.com/" label="Github" /> </div> ``` ## Tabs & TabPanel ### Tab Props * `activeName` - The default active panel name. * `bodyPadding` - Determine whether the panel body has a padding or not. Default is `true` ### TabPanel Props * `name` - The panel name. * `activeIcon` - The icon component used when tab is active * `inactiveIcon` - The icon component used when tab is inactive ### In Markdown ````md live <Tabs activeName="Svelte"> <TabPanel name="Svelte"> ```svelte title="Counter.svelte" <script> let count = $state(0) </script> <button onclick={() => count++}> You've clicked {count} times </button> ``` </TabPanel> <TabPanel name="Vue"> ```html title="Counter.vue" <script setup> import { ref } from 'vue' const count = ref(0) </script> <button @click="count++"> You've clicked {count} times </button> ``` </TabPanel> </Tabs> ```` ### In svelte ```svelte live <script> import { TabPanel, Tabs } from '@sveltepress/theme-default/components' // [svp! ~~] </script> <Tabs activeName="Tab2"> <TabPanel name="Tab1"> <div>Tab1 content</div> </TabPanel> <TabPanel name="Tab2"> <div>Tab2 content</div> </TabPanel> </Tabs> ``` ## Expansion ### Props * `title` - The expansion title * `showIcon` - Determine whether to the icon or not. Default is `true` * `headerStyle` - Customize the header inline style * `bind:expanded` - Determine the expanded status. Default is `false` ### Slots * `default` - The expansion content * `icon-fold` - The icon used for folded * `icon-expanded` - The icon used for expanded * `arrow` - Customize the expansion arrow indicator ### In markdown ```md live <Expansion title="Click to expand/fold panel"> <div class="text-[24px]">Some content</div> </Expansion> ``` ### In svelte ```svelte live <script> import { Expansion } from '@sveltepress/theme-default/components' </script> <Expansion title="A expansion without custom icon and arrow"> <div class="p-4 text-[24px]"> Look at the icon left. It gets colored when expanded! </div> {#snippet iconFold()} <div class="i-bxs-wink-smile"></div> {/snippet} {#snippet iconExpanded()} <div class="i-fxemoji-smiletongue"></div> {/snippet} {#snippet arrow()} <div class="i-material-symbols-thumbs-up-down"></div> {/snippet} </Expansion> ``` ## Icons (Pre-build iconify icons) :::important[Icon pre-build required]{icon=tabler:icons} The iconify icons should be in the [Pre-build iconify icons config](/reference/default-theme/#preBuildIconifyIcons) ::: ### In markdown ```md live <div class="text-8 p-4"> <IconifyIcon collection="vscode-icons" name="file-type-svelte" /> </div> ``` ### In svelte ```svelte live <script> import { IconifyIcon } from '@sveltepress/theme-default/components' </script> <div style="font-size: 28px;"> <IconifyIcon collection="vscode-icons" name="file-type-svelte" /> </div> ``` ## Floating ### In markdown ```md live <Floating placement="top"> <div class="text-xl b-1 b-solid b-blue rounded py-10 px-4"> Trigger </div> {#snippet floatingContent()} <div class="bg-white dark:bg-dark b-solid b-1 b-red rounded p-4"> Floating content </div> {/snippet} </Floating> ``` ### In svelte ```svelte live <script> import Floating from '@sveltepress/twoslash/FloatingWrapper.svelte' </script> <Floating placement="right"> <div class="text-xl b-1 b-solid b-blue rounded py-10 px-4"> Trigger </div> {#snippet floatingContent()} <div class="bg-white dark:bg-dark b-solid b-1 b-red rounded p-4"> Floating content </div> {/snippet} </Floating> ``` ### Props * `alwaysShow` - Determine whether to always show the floating content. Default is `false` * `placement` - Determine the floating content position. See [placement - floating-ui](https://floating-ui.com/docs/computePosition#placement). Default is `bottom`. * `floatingClass` - The addition classes that will be added to the floating content container. ## CodeBlock ### In markdown ```md live <CodeBlock lang="ts" code="const foo = 'bar'" /> ``` ### In markdown ```svelte live <script> import { CodeBlock } from '@sveltepress/theme-default/components' </script> <CodeBlock lang="ts" code="const foo = 'bar'" /> ``` ### Props * `lang` - The language name. `'svelte'`, `'md'`, `'js'`, ...etc * `code` - The code content --- # [Code related](https://sveltepress.site/guide/default-theme/code-related) :::tip[Dark Mode Compatible]{icon=mdi:theme-light-dark} All the code block features are fully compatible with dark mode. Toggle dark mode to see the styles. ::: ## Code highlight This feature integrate [Shiki](https://github.com/shikijs/shiki) ````md live ```svelte <script> const msg = 'world!' </script> <h1> Hello, {msg} </h1> ``` ```` You can customize the supported languages and theme in light/dark mode. More info reference to [Default theme reference - highlight option](/reference/default-theme/#highlighter) ## Code title ````md live ```svelte title="HelloWorld.svelte" <script> const msg = 'world!' </script> <h1> Hello, {msg} </h1> ``` ```` ## Line numbers Add `ln` in code block meta field would add line numbers in the rendered result. ````md live ```svelte ln <script> const msg = 'world!' </script> <h1> Hello, {msg} </h1> ``` ```` ## Line highlight Use `// [svp! hl]` `// [svp! ~~]` to highlight the line you want Use `// [svp! hl:num]` or `// [svp! ~~:num]` to highlight the num lines from the current line you want ````md live ```svelte <script> const msg = 'world!' // [svp! hl] function hello() { const foo = 'bar' // [svp! hl:2] const bar = foo return foo } </script> <h1> Hello, {msg} // [svp! ~~] </h1> ``` ```` ## Diff Use `// [svp! df:+]` or `// [svp! ++]` for diff add Use `// [svp! df:-]` or `// [svp! --]` for diff subtract ````md live ```js const msg = 'world!' // [svp! df:-] const newMsg = 'new world!' // [svp! df:+] function hello() { console.log('Hello', msg) // [svp! --] console.log('Hello', newMsg) // [svp! ++] } ``` ```` ## Focus Use `// [svp! fc]` or `// [svp! !!]` to focus line Use `// [svp! fc:num]` or `// [svp! !!:num]` to focus num lines from current line :::since[Multiple focus ranges]{version="2026-08-31" id="multiple-focus-ranges" summary="Use multiple non-contiguous focus commands or ranges in one code block."} You can use multiple focus commands in the same code block. Each selected line or range stays focused, while the remaining lines are dimmed. ````md live ```html <div> this would be blur </div> <div> this would be blur </div> <h1> // [svp! !!:3] this would be focus </h1> <div> this would be blur </div> <div> this would be blur </div> <p>this would also be focus</p> // [svp! fc] <div> this would be blur </div> ``` ```` ::: ## Markdown live code :::since[Literal Markdown live source]{version="2026-08-31" id="literal-markdown-live-source" summary="Keep Markdown live-code source literal while transforms run only in the preview."} Use the `md` language with the `live` prop to render the Markdown preview and show its source below. The source panel only applies Markdown syntax highlighting: code commands and other transforms run in the preview, while the original Markdown remains unchanged in the source. **Input** `````md ````md live ### Title * item1 * item2 ```js const foo = 'bar' // [svp! hl] ``` ```` ````` **Output** ````md live ### Title * item1 * item2 ```js const foo = 'bar' // [svp! hl] ``` ```` ::: ## Svelte live code Use svelte lang and live prop would render the result and the source codes under the result. **Input** ````md ```svelte live ln title=Counter.svelte <script> let count = $state(0) const handleClick = () => { count++ } </script> <button onclick={handleClick}> You've clicked {count} times </button> <style> button { background-color: purple; color: white; outline: 0; border: 0; padding: 8px 16px; border-radius: 4px; cursor: pointer; } </style> ``` ```` **Output** ```svelte live ln title=Counter.svelte <script> let count = $state(0) const handleClick = () => { count++ } </script> <button onclick={handleClick}> You've clicked {count} times </button> <style> button { background-color: purple; color: white; outline: 0; border: 0; padding: 8px 16px; border-radius: 4px; cursor: pointer; } </style> ``` ## Async svelte live code The js file that render the demo part would be bundled into a single js file and load async in the page **Input** ````md ```svelte live async <h1>This is an async svelte live code</h1> ``` ```` **Output** ```svelte live async <h1>This is an async svelte live code</h1> ``` :::warning[Live code in live code is not supported] The nested live code block below would be rendered as normal highlight code block. `````md live ````md ```md live ### Title ``` ```` ````` ::: ## Combine features You can use more than one features mentioned above: ````md live ```js ln title="complex-example.js" function hello() { const oldMsg = 'Some msg with focus, diff --' // [svp! --] // [svp! !!:3] const newMsg1 = 'Some msg with both focus, diff ++, highlight line' // [svp! ++] // [svp! ~~] const newMsg2 = 'Some msg with both focus, diff ++' // [svp! ++] } ``` ```` ## Import code This feature can allow you to import some code directly from a file. And use the file extension name as `lang` to highlight the code. And you can specify the startLine and endLine to intercept the code you want. ```md @code(/path/to/file[,startLine[,endLine]]) ``` Path can starts with `.` or `/` * `.` is the relative path to the current md file * `/` is the relative path to project root, where you start the `vite` command For example you have file tree like this ```txt ├─ src │ ├─ routes │ │ ├─ foo │ │ │ ├─ +page.md │ │ │ ├─ Foo.svelte ``` * `@code(./Foo.svelte)` - import all code from Foo.svelte * `@code(/src/routes/foo/Foo.svelte)` - import all code from Foo.svelte * `@code(./Foo.svelte,5,10)` - import the line 10 to line 20 in Foo.svelte * `@code(/src/routes/foo/Foo.svelte,10,20)` - import the line 10 to line 20 in Foo.svelte :::tip[Tip]{icon=solar:chat-square-code-outline} Notice that start line and end line both would be included in the final content. ::: ## Cheat list | Alias | Equals | Function | | -------- | -------- | ------------------------------------ | | `~~` | `hl` | highlight single line | | `~~:num` | `hl:num` | highlight num line from current line | | `++` | `df:+` | diff add line | | `--` | `df:-` | diff subtract line | | `!!` | `fc` | focus single line | | `!!:num` | `fc:num` | focus num line from current line | --- # [Frontmatter in default theme](https://sveltepress.site/guide/default-theme/frontmatter) ## Shared frontmatter ```ts interface CommonFrontmatter { title?: string description?: string lastUpdate?: string } ``` ### `title` The final page title would be `page frontmatter title | siteConfig.title` ### `description` The final page description would be use the page frontmatter if provided instead of `siteConfig.description` ### `lastUpdate` It will affect the "last update at: " content at the end of the page. Default it would read the git commit time of the +page file. You can override it by config this field. # Theme specific frontmatter ### `sidebar` Determine to show the sidebar or not. Default is `true`, change to `false` to hide the sidebar ### `header` Determine to show the header or not. Default is `true`, change to `false` to hide the header ### `layout` Determine to use the theme page layout or not. Default is `true`, change to `false` to disable the theme page layout for this page. ## Special frontmatter * [Home page frontmatter](/guide/default-theme/home-page/#Frontmatter) --- # [Google Analytics](https://sveltepress.site/guide/default-theme/google-analytics) ## Introduction Pass `ga` to default theme to use Google Analytics. Value is the id provided by [Google Analytics](https://analytics.google.com/analytics/web/). Something like `G-XXXXXXXXX`. Would auto config gtag script in site head if provided. ## Example config ```ts title="vite.config.(js|ts)" import { defaultTheme } from '@sveltepress/theme-default' import { sveltepress } from '@sveltepress/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ sveltepress({ theme: defaultTheme({ ga: 'G-XXXXXXXXX' // [svp! ++] }) }) ], }) ``` --- # [Headings & Anchors](https://sveltepress.site/guide/default-theme/headings-and-anchors) All headings in your page md files would render the toc on the right of the page As you currently can see on every page And multi levels are supported :::since[Stepped TOC heading hierarchy]{version="2026-08-31" id="toc-heading-hierarchy" summary="Show nested heading depth and the active viewport section with a stepped guide."} Nested headings are indented by depth in the table of contents. The guide line turns whenever the heading level changes, and its highlighted segment follows the section currently active in the viewport. ::: # Level 1 heading ```md live ## Heading1 ### Some heading contains special characters&?:/. ### Heading1 nested2 ## Heading2 ### Heading2 nested1 ### Heading2 nested2 ``` You can look the page toc to see the result --- # [Home Page](https://sveltepress.site/guide/default-theme/home-page) ## Frontmatter The `src/routes/+page.(md|svelte)` would be identified as home page Home frontmatter can be like this. Take the frontmatter this site use for example: @code(../../../+page.md) And you can see [Home page](/) for result ### `heroImage` The home page logo image. It is recommended to use an image with high quality. Set it to `false` when a landing page should use the full content width without the default hero illustration. :::since[Localized default hero visual]{version="2026-08-31" id="hero-code-localization" summary="Localize the default two-pane hero preview with i18n.heroCode."} When `heroImage` is omitted, the default theme shows a two-pane code preview. Localize the text inside this visual with the theme's `i18n.heroCode` option: ```ts import { defaultTheme } from '@sveltepress/theme-default' defaultTheme({ i18n: { heroCode: { title: 'Hello', messageBefore: 'Svelte in ', messageStrong: 'markdown', messageAfter: '', tipLabel: 'TIP', counterLabel: 'Count: 1', }, }, }) ``` `messageBefore`, `messageStrong`, and `messageAfter` compose the Markdown sentence shown in the source pane. `tipLabel` and `counterLabel` customize the rendered pane. ::: ### `tagline` The small description text under site title and description ### `actions` The action buttons * `label` The label text of action button * `to` The link address of action button. Internal paths should be logical routes such as `/guide/introduction/`; the Default Theme prefixes them with the active locale. * `external` Determine whether to render a external icon on the right of action button ### `features` The feature cards * `title` The title * `description` The text description under the title * `icon` custom icon config * `type` - `'svg' | 'iconify'` * `value` - the svg content for svg type icon * `collection` - the collection name of iconify * `name` - the icon name in the specified collection of iconify * `link` The link that would go to when the feature card is clicked. Internal paths should be logical routes such as `/guide/markdown/frontmatter/`; the Default Theme prefixes them with the active locale. If the link starts with http(s), would be auto recognized as external link. And would add an external icon on the top-right corner. And the feature card has a clickable style when link is provided. :::important[Icon pre-build required]{icon=tabler:icons} The iconify icons should be in the [Pre-build iconify icons config](/reference/default-theme/#preBuildIconifyIcons) ::: ### `home` The root route uses the home layout by default. Set `home: true` on any other route to reuse the same landing-page presentation with that page's own `title`, `description`, and `tagline`. Landing pages do not render the documentation sidebar, table of contents, edit metadata, or previous/next page switcher. Set `home: false` on the root route to remove the default home page content. ## Slots ### `hero-image` Use custom hero image content. Example: ```svelte title="/src/routes/+page.(md|svelte)" {#snippet heroImage()} <div> Custom hero image content </div> {/snippet} ``` --- # [Navbar](https://sveltepress.site/guide/default-theme/navbar) :::tip[Auto base] The links configured will be auto prefixed with [`base`](https://svelte.dev/docs/kit/$app-paths#base) ::: :::important[Absolute mode] You need to set [`paths.relative`](https://svelte.dev/docs/kit/configuration#paths) to `false` ```js title="svelte.config.js" import adapter from '@sveltejs/adapter-static' /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { paths: { relative: false, // [svp! ++] }, }, } export default config ``` ::: ## Introduction Pass `navbar` option to theme default to configure navbar Each item can hold these props: * `title` - The label text * `to` - The link address * `icon` An HTML string. Will show the html content instead of `title`. It is useful to display a custom icon on the navbar. For example a twitter svg icon navbar item would be like this: ```js const twitterNavItem = { icon: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> <path fill="currentColor" d="M22.46 6c-.77.35-1.6.58-2.46.69c.88-.53 1.56-1.37 1.88-2.38c-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29c0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15c0 1.49.75 2.81 1.91 3.56c-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.22 4.22 0 0 1-1.93.07a4.28 4.28 0 0 0 4 2.98a8.521 8.521 0 0 1-5.33 1.84c-.34 0-.68-.02-1.02-.06C3.44 20.29 5.7 21 8.12 21C16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56c.84-.6 1.56-1.36 2.14-2.23Z" /> </svg>`, to: 'https://twitter.com/' } ``` * `external` - Determine whether the item is an external link * `items` - Sub links :::note[One level only]{icon=carbon:tree-view-alt} For now, navbar can only hold one level sub links. ::: ## Example ```ts title="vite.config.(js|ts)" import { defaultTheme } from '@sveltepress/theme-default' import { sveltepress } from '@sveltepress/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ sveltepress({ theme: defaultTheme({ navbar: [ { title: 'Foo page', to: '/foo/' }, { title: 'With dropdown', items: [ { title: 'Bar page', to: '/bar/' }, { title: 'External Github page', to: 'https://github.com/', external: true } ] } ] }) }) ] }) ``` :::since[Section tabs]{version="2026-10-05" id="theme-section-tabs" summary="sectionTabs adds a row of section tabs under the navbar, and the sidebar follows the active tab."} ## Section tabs Pass `sectionTabs` to show a row of tabs under the navbar, one tab per documentation section, like the tabs on this site. The sidebar follows the active tab: give each section its own [sidebar](/guide/default-theme/sidebar/) key, and point each tab at a page in that section. ```ts title="vite.config.(js|ts)" import { defaultTheme } from '@sveltepress/theme-default' import { sveltepress } from '@sveltepress/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ sveltepress({ theme: defaultTheme({ sectionTabs: [ { title: 'Get started', to: '/guide/introduction/', icon: 'tabler:rocket' }, { title: 'Markdown', to: '/guide/markdown/basic-writing/', icon: 'tabler:markdown' }, { title: 'Reference', to: '/reference/vite-plugin/', icon: 'tabler:code' }, ], sidebar: { '/guide/': [/* Get started pages */], '/guide/markdown/': [/* Markdown pages */], '/reference/': [/* Reference pages */], }, }), }), ], }) ``` Each tab can hold these props: * `title` - The tab label * `to` - The section's landing page. Like navbar links, it follows the active locale and documentation version. External URLs open in a new tab. * `icon` - Optional. An Iconify icon in `collection:name` form, such as `tabler:rocket`, or an HTML string such as an inline SVG. Iconify icons need the matching `@iconify-json/*` package. The theme pre-builds the icons of the `sectionTabs` passed to `defaultTheme()`; add icons used only by a locale's tabs to `preBuildIconifyIcons`. * `activeMatch` - Optional. A route prefix, or an array of prefixes, that makes the tab active. A tab is active while the current page is inside its section. By default, the section is the most specific sidebar key that contains the tab's `to` link: `/guide/markdown/` for the Markdown tab above. A tab whose link is under no sidebar key is active on its own page and the pages below it. Set `activeMatch` when sections don't line up with sidebar keys. When several tabs match, the most specific one wins. The tabs travel with the sidebar, so they stay hidden on the home page and on pages whose frontmatter sets `sidebar: false`, `header: false`, or `layout: false`. Below the `950px` breakpoint, they move to the top of the sidebar drawer. On a multi-language site, set `sectionTabs` in each locale's `theme`, as with `navbar`, and translate the navigation label with `i18n.sectionTabsLabel`. Historical documentation versions show the current tabs; each link stays inside the frozen version when that version has the page. ::: --- # [PWA](https://sveltepress.site/guide/default-theme/pwa) ## Introduction This feature integrated [@vite-pwa/sveltekit](https://vite-pwa-org.netlify.app/frameworks/sveltekit.html#sveltekit-pwa-plugin) :::warning[SvelteKit 2 only for now] `@vite-pwa/sveltekit` does not support SvelteKit 3 yet. On SvelteKit 3 no service worker is generated and the build prints a warning. See [Upgrading to SvelteKit 3](/guide/quick-start/#Upgrading-to-SvelteKit-3). ::: Pass `pwa` option to theme default to use pwa. The options are exactly the same as [SvelteKit PWA Plugin Options](https://vite-pwa-org.netlify.app/frameworks/sveltekit.html#sveltekit-pwa-plugin-options) except for `darkManifest`, which is the manifest path that would used for dark theme And the svelte.config.js need to config `files.serviceWorker`, use the `SERVICE_WORKER_PATH` exported from `@sveltepress/theme-default` ```ts title="svelte.config.js" import adapter from '@sveltejs/adapter-static' import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' import { SERVICE_WORKER_PATH } from '@sveltepress/theme-default' // [svp! ++] /** @type {import('@sveltejs/kit').Config} */ const config = { extensions: ['.svelte', '.md'], preprocess: [vitePreprocess()], kit: { adapter: adapter(), files: { // [svp! ++] serviceWorker: SERVICE_WORKER_PATH, // [svp! ++] }, // [svp! ++] }, } export default config ``` :::note[package required]{icon=noto:package} If you want to enable pwa. You will need to add `workbox-window` as a dev dependency to your Vite project. ::: ## Precache (versions & i18n) By default Sveltepress only precaches the **app shell** and the **homepage HTML**. The shell is SvelteKit’s entry modules, hashed CSS / fonts, and root icons — not per-route `_app/immutable/nodes` or shared chunks. Other documentation pages and those hashed modules are cached at runtime when the user visits them (pages: `NetworkFirst`, capped at 50 entries; hashed client files: `CacheFirst`, capped at 400 entries / 30 days). Images and SvelteKit `__data.json` responses are also runtime-cached. This keeps service worker install and update fast when the site has many pages, versions, and locales, so the refresh prompt can appear soon after a deploy. Precaching every prerendered HTML file or every client module makes Workbox hash, compare and download `versions × locales × pages` on every update. On first install, homepage hydration may need the network until those hashed modules have been runtime-cached. After one online visit, visited pages (including home) stay available offline through the runtime cache. ### `pwa.precachePages` | Value | Precached HTML | | --- | --- | | `false` (default) | Homepage only | | `true` | All prerendered HTML (historical versions are still ignored) | | `string[]` | Homepage + matching URL prefixes | Precache only the current locale and a version snapshot: ```ts import { defaultTheme } from '@sveltepress/theme-default' defaultTheme({ pwa: { precachePages: ['/zh/', '/v/2026-08-27/'], }, }) ``` Restore the previous “cache every page” behavior: ```ts import { defaultTheme } from '@sveltepress/theme-default' defaultTheme({ pwa: { precachePages: true, }, }) ``` :::tip A glob starting with `prerendered/` is always included. Otherwise `@vite-pwa/sveltekit` would append `prerendered/**/*.{html,json}` and pull every version/locale page back into the precache. ::: Visited pages still work offline through the runtime cache, even when they are not precached. :::since[App-shell client precache]{version="2026-09-09" id="pwa-precache-client" summary="Default client precache is the app shell; precacheClient restores the catch-all glob."} ### `pwa.precacheClient` | Value | Precached client files | | --- | --- | | `false` (default) | App shell only (entry + CSS / fonts + root icons) | | `true` | Every matching client file | Restore the previous “precache every client JS/CSS module” behavior: ```ts import { defaultTheme } from '@sveltepress/theme-default' defaultTheme({ pwa: { precacheClient: true, }, }) ``` `precachePages` and `precacheClient` are independent: HTML policy does not change the client glob, and the other way around. ::: ## Example config Take the config this site use for example: @code(/config/pwa.ts) --- # [Search](https://sveltepress.site/guide/default-theme/search) SveltePress default theme provides built-in **Local Search** powered by [Pagefind](https://pagefind.app/) out of the box with zero configuration. In addition, the default theme supports **Algolia DocSearch** through `docsearch` and custom search components through `search`, including `@sveltepress/meilisearch`. ## Local Search (Default) Local search is enabled by default with zero configuration. When building your documentation site (`pnpm build`), SveltePress automatically runs Pagefind to index all static HTML pages and bundles the search assets into `/pagefind/`. ### Features - **Zero-config**: Works immediately without external API keys or remote indexing services. - **Offline & Static**: Runs entirely in the browser via WebAssembly, fast and privacy-friendly. - **Multi-locale support**: Automatically detects `<html lang="...">` and filters search queries by the active page language. - **Keyboard navigation**: Opens via `Cmd+K` (macOS) or `Ctrl+K` (Windows/Linux), navigate with Arrow keys, select with Enter, close with Escape. - **Development notice**: In development mode (`pnpm dev`), searching displays an informative notice explaining that the full index is created during production build. ### Disabling Local Search If you want to disable search entirely: ```ts title="vite.config.(js|ts)" import { defaultTheme } from '@sveltepress/theme-default' import { sveltepress } from '@sveltepress/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ sveltepress({ theme: defaultTheme({ search: false, }), }), ], }) ``` You can also disable the build-time indexer in the Vite plugin: ```ts title="vite.config.(js|ts)" import { sveltepress } from '@sveltepress/vite' sveltepress({ pagefind: false, }) ``` ## Algolia DocSearch Pass a `docsearch` config object to `defaultTheme` to use [Algolia DocSearch](https://docsearch.algolia.com/) instead of the default local search in the navbar. Required fields are `appId`, `apiKey`, and `indexName`. Every other [DocSearch option](https://docsearch.algolia.com/docs/api) is also accepted. ```ts title="vite.config.(js|ts)" import { defaultTheme } from '@sveltepress/theme-default' import { sveltepress } from '@sveltepress/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ sveltepress({ theme: defaultTheme({ docsearch: { appId: 'YOUR_APP_ID', apiKey: 'YOUR_SEARCH_API_KEY', indexName: 'YOUR_INDEX_NAME', }, }), }), ], }) ``` :::note[Apply for DocSearch] DocSearch is free for open-source documentation sites. Apply at [docsearch.algolia.com](https://docsearch.algolia.com/apply/). ::: ## Meilisearch `@sveltepress/meilisearch` is the supported Meilisearch search component. Install it first: @install-pkg(@sveltepress/meilisearch) Create a wrapper that provides your Meilisearch connection settings: ```svelte title="src/lib/MeilisearchSearch.svelte" <script lang="ts"> import Search from '@sveltepress/meilisearch/Search.svelte' </script> <Search host="https://search.example.com" apiKey="YOUR_SEARCH_ONLY_KEY" indexName="docs" /> ``` Then pass the wrapper path to the default theme's custom-search hook: ```ts title="vite.config.(js|ts)" import { defaultTheme } from '@sveltepress/theme-default' defaultTheme({ search: '/src/lib/MeilisearchSearch.svelte', }) ``` The component queries an existing Meilisearch index; it does not build the index for you. Each record should provide `id`, `title`, `content`, and either `url` or `path`. Use a search-only API key in browser code. :::note[Production builds] A custom `search` source path is bundled into static production builds: the theme resolves the configured `.svelte` path at build time and loads it as a lazy chunk, so no extra runtime configuration is needed. Configure the wrapper as a source path (as above). Passing a component object directly is not supported — theme options are serialized to JSON for the client, so objects are dropped. If a production deployment shows no search, make sure `search` is a source-path string. ::: For another search provider, implement the same `search` hook with your own Svelte wrapper. Search precedence is: custom `search` component > explicit `docsearch` > default `LocalSearch`. ## Search across locales and versions When your site combines i18n locales with version management, search stays per-locale and per-version, and the crawler-facing outputs follow the same URL scheme the site serves (`/`, `/zh/`, `/bn/`, `/v/<id>/…`, `/zh/v/<id>/…`). ### Locale-aware search Each locale carries its own theme options, so give each locale its own DocSearch index — the pre-i18n documentation site used one index per language: ```ts title="vite.config.(js|ts)" import { defaultTheme } from '@sveltepress/theme-default' import { sveltepress } from '@sveltepress/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ sveltepress({ theme: defaultTheme({ // Site-level options shared by every locale (logo, github, pwa, ...) }), locales: { '/': { lang: 'en', label: 'English', theme: { docsearch: { appId: 'YOUR_APP_ID', apiKey: 'YOUR_SEARCH_API_KEY', indexName: 'sveltepress', }, }, }, '/zh/': { lang: 'zh', label: '中文', theme: { docsearch: { appId: 'YOUR_APP_ID', apiKey: 'YOUR_SEARCH_API_KEY', indexName: 'cn', }, }, }, }, }), ], }) ``` The Navbar remounts the DocSearch widget whenever the active index or version changes, so switching locale or version queries the right index. ### Version-aware search A version in the manifest may carry `search` metadata. While a reader is on that historical version's pages (`/v/<id>/…`), the theme switches DocSearch to the configured `indexName` and merges `facetFilters` into the query: ```json title="sveltepress.versions.json" { "versions": [ { "id": "2026-08-28", "search": { "indexName": "sveltepress-v2026-08-28", "facetFilters": ["version:2026-08-28"] } } ] } ``` Keep the crawler's facet tags in sync with this metadata. :::since[Historical Local Search vs DocSearch]{version="2026-09-03" id="search-historical-pagefind-vs-docsearch" summary="Pagefind historical indexes stay available; DocSearch/custom search still need version search metadata."} Historical versions without a `search` object show "Search is not available for this documentation version." for **DocSearch** and custom `search` components only. Built-in **Local Search** still loads the frozen Pagefind index under that version's `/pagefind/` path (via `syncHistoricalPagefind` on release) and does not require `search` metadata. ::: ### Crawling and result URLs The generated `sitemap.xml` lists every locale's current pages and every eligible historical version page with hreflang alternates; EOL history is excluded unless the version opts out (`noIndex: false`), and every version page emits its own `rel="canonical"` link. Index records must point at these real, prefixed URLs — a Chinese record's `url` is `/zh/guide/…`, a frozen version's is `/v/2026-08-28/guide/…`. ### Custom search components The Navbar renders a custom `search` component only when search is available for the current route, remounts it per version, and passes it two props: - `version` — the active version object (`{ id, label, status, … }`), or the current version on unprefixed pages. - `versionSearch` — the version's `search` metadata (`{ indexName?, facetFilters? }`), or `null`. When your index stores multiple versions, filter results by the facets in `versionSearch`. Record URLs must be the full prefixed routes. No locale prop is passed: if you keep one index per locale, read the locale from `location.pathname` yourself. --- # [Sidebar](https://sveltepress.site/guide/default-theme/sidebar) ## Introduction Pass `sidebar` option to theme default to configure sidebar. There are two ways to set up the sidebar: - **Auto-generated** — Automatically scan your routes directory and build the sidebar from your file structure and frontmatter - **Manual** — Explicitly define every sidebar item in your Vite config ## Auto-generated sidebar Set `sidebar` to `{ enabled: true }` to let SveltePress automatically generate the sidebar by scanning your `src/routes/` directory. ```ts import { defaultTheme } from '@sveltepress/theme-default' import { sveltepress } from '@sveltepress/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ sveltepress({ theme: defaultTheme({ sidebar: { enabled: true, }, }), }), ], }) ``` SveltePress will detect top-level route directories (e.g. `/guide/`, `/reference/`) and build sidebar groups from them automatically. :::since[Auto sidebar skips historical versions]{version="2026-09-09" id="auto-sidebar-skip-version-snapshots" summary="Auto-generated sidebars skip frozen and dev-mounted /v/{id}/ trees."} Historical version snapshot trees such as `/v/1.0/` (and locale-composed bases such as `/zh/v/1.0/`) are skipped, including when `roots` explicitly lists `/v/`. Frozen history keeps its own sidebar from snapshot metadata. ::: ### Options | Option | Type | Default | Description | | --- | --- | --- | --- | | `enabled` | `boolean` | — | Set to `true` to enable auto sidebar | | `routesDir` | `string` | `'src/routes'` | Custom routes directory path | | `roots` | `string[]` | auto-detected | Root paths to generate sidebar for, e.g. `['/guide/', '/reference/']`. If not specified, auto-detect from top-level route directories | ```txt sidebar: { enabled: true, routesDir: 'src/routes', roots: ['/guide/', '/reference/'], } ``` ### Frontmatter control Use frontmatter in your `+page.md` files to control how pages appear in the auto-generated sidebar. ```md --- title: Getting Started order: 1 sidebar: true sidebarTitle: Start Here collapsible: true --- ``` | Field | Type | Default | Description | | --- | --- | --- | --- | | `title` | `string` | Inferred from filename | Page title, also used as sidebar label | | `sidebarTitle` | `string` | — | Override the sidebar label (takes precedence over `title`) | | `order` | `number` | `100` | Sort order within the same level. Lower numbers appear first | | `sidebar` | `boolean` | `true` | Set to `false` to exclude this page from the sidebar | | `collapsible` | `boolean` | — | Whether the sidebar group is collapsible | :::tip[File naming]{icon=bi:folder2-open} If no `title` or `sidebarTitle` is provided, the directory name is humanized (e.g. `getting-started` → `Getting Started`). ::: :::info[HMR support]{icon=mdi:refresh} When using auto sidebar in dev mode, adding or removing route files will automatically regenerate the sidebar — no restart needed. ::: ## Manual sidebar Manually define the sidebar structure in your Vite config. This gives you full control over titles, links, grouping, and ordering. :::tip[Auto base] The links configured will be auto prefixed with [`base`](https://svelte.dev/docs/kit/$app-paths#base) ::: :::important[Absolute mode] You need to set [`paths.relative`](https://svelte.dev/docs/kit/configuration#paths) to `false` ```js title="svelte.config.js" import adapter from '@sveltejs/adapter-static' /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { paths: { relative: false, // [svp! ++] }, }, } export default config ``` ::: ```ts import { defaultTheme } from '@sveltepress/theme-default' import { sveltepress } from '@sveltepress/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ sveltepress({ theme: defaultTheme({ sidebar: { '/foo/': [ { title: 'Bar', to: '/foo/bar/', }, { title: 'Zoo', collapsible: true, items: [ { title: 'Sub item', to: '/sub/item/link', }, ], }, { title: 'External github page', to: 'https://github.com', }, ], }, }), }), ], }) ``` :::since[Nested sidebar keys]{version="2026-10-05" id="sidebar-nested-keys" summary="The most specific matching sidebar key wins, so nested keys can give each section tab its own sidebar."} When several keys match a page, the most specific one wins, whatever their order. Nested keys such as `/guide/` and `/guide/markdown/` can therefore split one route tree into separate sidebars. Pair each key with a [section tab](/guide/default-theme/navbar/#Section-tabs) to switch between them from a row under the navbar. ::: ### `title` The sidebar item title ### `to` The link address :::info[Auto external]{icon=ic:sharp-rocket-launch} Unlike the navbar item, sidebar item use the `Link` component. Which means link starts with `http(s)` would be auto recognized as external links. ::: ### `collapsible` Determine whether the sidebar group is collapsible or not. Default is `false` ### `items` Sub items :::info[Nested items]{icon=bi:list-nested} Nested items is supported ::: --- # [Twoslash](https://sveltepress.site/guide/default-theme/twoslash) This feature integrate [Twoslash](https://github.com/twoslashes/twoslash) All of the Typescript code blocks would provide inline type hover. ## Enable twoslash * Install @sveltepress/twoslash package @install-pkg(@sveltepress/twoslash) * Config `highlighter.twoslash` to `true` ```ts title="vite.config.(js|ts)" import { defaultTheme } from '@sveltepress/theme-default' import { sveltepress } from '@sveltepress/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ sveltepress({ theme: defaultTheme({ highlighter: { // [svp! ++] twoslash: true // [svp! ++] } // [svp! ++] }) }) ] }) ``` ## Basic type annotation ````md live ```ts const foo = false const obj = { a: 'a', b: 1 } ``` ```` ## Errors ````md live ```ts // @errors: 2304 2322 const foo: Foo = null const a: number = '1' ``` ```` ## Queries ````md live ```ts const hi = 'Hello' const msg = `${hi}, world` // ^? // // ``` ```` ## Cut codes ### Cut before use `// ---cut---` or `// ---cut-before---` can cut all codes before this line ````md live ```ts const level: string = 'Danger' // ---cut--- console.log(level) ``` ```` ### Cut after use `// ---cut-after---` can cut all codes after this line ````md live ```ts const level: string = 'Danger' // ---cut-before--- console.log(level) // ---cut-after--- console.log('This is not shown') ``` ```` ### Cut start/end use `// ---cut-start---` and `// ---cut-end---` to cut contents between them ````md live ```ts const level: string = 'Danger' // ---cut-start--- console.log(level) // This is not shown. // ---cut-end--- console.log('This is shown') ``` ```` ## Twoslash for svelte ```svelte <script> import { onMount } from 'svelte' let { message = 'World' } = $props() let count = $state(0) onMount(() => { }) </script> <button onclick={() => count++}> Count is: {count} </button> <div class="text-6"> Hello, {message} </div> ``` --- # [Unocss](https://sveltepress.site/guide/default-theme/unocss) ## Introduction The default theme use the [Unocss](https://github.com/unocss/unocss) to implement all styles Let's have some examples to see the benefits: ## Easily dark mode The following content would show 3 red cards per line in light mode. And 2 blue cards per line in dark mode. Isn't this great with few codes? ```svelte live <div class="grid grid-cols-3 gap-4 dark:grid-cols-2"> <div class="card"></div> <div class="card"></div> <div class="card"></div> <div class="card"></div> </div> <style> .card { --at-apply: h-[100px] flex items-center justify-center rounded bg-rose-5 dark:bg-blue-5; } </style> ``` ## Easily multi screen compatible Toggle the window size or use different device to ses what's happening ```svelte live <div class="sm:text-5 sm:text-rose-5 text-blue-5 text-10"> This is a text with different font size and color in wide or narrow width device </div> ``` ## Easily pure css icons ```svelte live <div class="text-[56px]"> <div class="i-openmoji-red-apple"></div> <div class="i-openmoji-banana"></div> <div class="i-openmoji-grapes"></div> </div> ``` ## And more... Use your talents, ideas to achieve more! --- # [Internationalization (i18n)](https://sveltepress.site/guide/i18n) SveltePress can serve multiple locales from one site. Internationalization is opt-in: omit `locales` and the site stays single-locale with unchanged behavior. The Playground [Internationalization](/playground/i18n/) Entry boots a three-locale Default Theme starter as authored at `config/locales.ts`. Chinese and Bengali Playground URLs keep that same starter and start the preview on `/zh/` or `/bn/`. This is the exception: other Playground Entries are single-locale trees written in the page language at the default paths. :::since[Opt-in locales]{version="2026-09-03" id="i18n-opt-in-locales" summary="Pass sveltepress({ locales }) to enable multi-locale routing, theme options, and switcher."} ## Enable locales Pass a `locales` map to `sveltepress()`. Keys are URL prefixes (`'/'` for the default locale, `'/zh/'`, `'/bn/'`, …). Each entry needs a BCP 47 `lang`, a user-facing `label` for the language switcher, and that locale's full theme options: ```ts title="vite.config.ts" // @noErrors import { defaultTheme } from '@sveltepress/theme-default' import { sveltepress } from '@sveltepress/vite' import { defineConfig } from 'vite' import { locales } from './config/locales' export default defineConfig({ plugins: [ sveltepress({ theme: defaultTheme({ // Site-wide options shared by every locale (logo, github, pwa, ...) }), locales, }), ], }) ``` ```ts title="config/locales.ts" // @noErrors import type { LocalesConfig } from '@sveltepress/vite' import navbar from './navbar' import sidebar from './sidebar' import zhI18n from './zh/i18n' import zhNavbar from './zh/navbar' import zhSidebar from './zh/sidebar' export const locales: LocalesConfig = { '/': { lang: 'en', label: 'English', theme: { navbar, sidebar }, }, '/zh/': { lang: 'zh', label: '中文', theme: { navbar: zhNavbar, sidebar: zhSidebar, i18n: zhI18n, }, }, } ``` Without `locales`, SveltePress does not rewrite paths, emit a language switcher, or scan locale route trees. ::: :::since[Locale routes and prefixes]{version="2026-09-03" id="i18n-route-prefixes" summary="Default locale stays at /; other locales live under /zh/, /bn/, and matching src/routes trees."} ## Routes and prefixes Place translated pages beside the default locale under matching route IDs: ```txt src/routes/ ├─ guide/introduction/+page.md # English → /guide/introduction/ ├─ zh/guide/introduction/+page.md # Chinese → /zh/guide/introduction/ └─ bn/guide/introduction/+page.md # Bengali → /bn/guide/introduction/ ``` The default locale stays unprefixed at `/`. Other locales use their configured prefixes (`/zh/`, `/bn/`, …). Logical paths are the same across locales; only the prefix changes. Navbar, sidebar, home action, and feature-card links should use those logical paths (for example `/guide/introduction/`); the Default Theme prefixes them with the active locale. ::: :::since[Language switcher and fallbacks]{version="2026-09-03" id="i18n-language-switcher" summary="Default Theme switcher keeps the documentation version, logical page, and heading hash."} ## Language switcher and fallbacks When `locales` is configured, the Default Theme renders a language switcher in the navbar. Switching locales keeps the same documentation version, the same logical page, and the in-view heading (`#hash`). On a historical version page (`/v/<id>/…` or `/zh/v/<id>/…`), the switcher prefers the same frozen version in the target locale. Only if that frozen page is missing does it fall back to the current version of the same logical page, then to that locale's home with a short notice (`svp-locale-fallback=1`). Customize switcher and notice copy with theme `i18n.localeSwitcher` and `i18n.localePageUnavailable`. ::: :::since[virtual:sveltepress/locale]{version="2026-09-03" id="i18n-virtual-locale" summary="Client helpers resolve the active locale, localize links, and compute switch targets."} ## `virtual:sveltepress/locale` Themes and custom layouts can import locale helpers from the virtual module: ```ts // @noErrors import { locales, resolveLocale, resolveLocaleSwitch, resolveLocalizedPath, } from 'virtual:sveltepress/locale' const active = resolveLocale('/zh/guide/introduction/') const href = resolveLocalizedPath('/guide/quick-start/', active) const target = resolveLocaleSwitch('/zh/guide/introduction/', '/') ``` * `locales` — the configured map, or `null` when i18n is off * `resolveLocale(pathname)` — active locale (`lang`, `label`, `prefix`, `theme`, …) * `resolveLocalizedPath(to, locale)` — rewrite an internal link into the active locale * `resolveLocaleSwitch(pathname, targetPrefix)` — `{ href, fallback }` for the switcher ::: :::since[createLocaleHandle]{version="2026-09-03" id="i18n-create-locale-handle" summary="SSR hook sets <html lang> from the active locale before hydration."} ## SSR `<html lang>` Use `createLocaleHandle` from `@sveltepress/vite/hooks` so the initial HTML carries the correct language for crawlers and assistive technology. The Default Theme keeps `document.documentElement.lang` in sync after client-side navigation. ```js title="src/hooks.server.js" // @noErrors import { createLocaleHandle } from '@sveltepress/vite/hooks' import { locales } from '../config/locales' export const handle = createLocaleHandle(locales) ``` ::: :::since[Per-locale llms and hreflang sitemap]{version="2026-09-03" id="i18n-llms-sitemap" summary="Each locale gets its own llms indexes; sitemap lists hreflang across locales and historical versions."} ## Per-locale llms and sitemap With `llms` enabled, production builds write locale-scoped `llms.txt` / `llms-full.txt` (for example `/llms.txt` and `/zh/llms.txt`), listing only that locale's pages with prefixed URLs. Historical indexes land under each locale's version base (`/v/<id>/`, `/zh/v/<id>/`, …). `sitemap.xml` lists every locale's current pages with hreflang alternates for locales that share the logical route, plus eligible historical version URLs from each locale manifest. EOL history is excluded unless a version opts out with `noIndex: false`. ::: :::since[Locale-aware versioning]{version="2026-09-03" id="i18n-locale-versioning" summary="Each locale has its own versions manifest; CLI --locale selects sveltepress.versions.<locale>.json."} ## Locale-aware versioning Adding a locale does not require changes to SveltePress packages. Configure the prefix in `locales`, put pages under `src/routes/<slug>/`, and if you use document versions run `sveltepress versions init --locale <slug>`. Language switching, sidebar localization, and historical URLs follow that prefix automatically. Each locale can keep its own document-version manifest: | Locale prefix | Manifest | Version base | | --- | --- | --- | | `/` (default) | `sveltepress.versions.json` | `/v` | | `/<slug>/` (for example `/zh/`, `/bn/`, `/ja/`) | `sveltepress.versions.<slug>.json` | `/<slug>/v` | A default `sveltepress versions build` drafts every configured locale and composes each locale's `/v/` tree into one output. Pass `--locale` to `init`, `create`, `validate`, and single-locale draft jobs: ```sh pnpm exec sveltepress versions build pnpm exec sveltepress versions create 8.2 --label "8.2" --locale zh pnpm exec sveltepress versions validate --locale zh ``` `versions init --locale zh` creates `sveltepress.versions.zh.json` with base path `/zh/v` by default. Incremental deltas live in `version-deltas-zh/` (or the manifest's `artifacts.sources`). See [Document version management](/guide/version-management/) for the full release workflow. ::: ## Search per locale Local Search indexes respect `<html lang="…">`. DocSearch and custom search should use per-locale theme options (separate indexes). Details and version-aware behavior: [Search](/guide/default-theme/search/). --- # [Introduction](https://sveltepress.site/guide/introduction) Sveltepress is a SvelteKit docs and blog site builder. Inspired by [VitePress](https://vitepress.dev/). Built on [SvelteKit](https://kit.svelte.dev/) and [UnoCSS](https://github.com/unocss/unocss) — so you get a content toolkit and the full SvelteKit surface (SSR, adapters, server routes, hooks). The navbar **Playground** opens a Feature directory of in-browser Starters. Markdown, Default Theme, and Vite plugin Entries share the Default Theme starter. Document versions ships on a Versions starter (`versions init` / `create` · `versions build`). Internationalization ships on a three-locale starter. Locale Playground URLs embed a same-path starter written in that language (`src/routes/+page.md`, `config/navbar.js`) — they do not add `/zh/` or `/bn/` routes, except the Internationalization Entry. Custom theme ships from Playground home only (there is no Guide leaf); its preview frames `GlobalLayout`, `+layout.svelte`, and `PageLayout` to match the [layout hierarchy](#Layout-hierarchy). Blog theme Entries (Configuration, Writing posts, Features, Customisation) share the Blog starter. Virtual modules is one Reference Entry on the Kitchen-sink starter; Open in Playground on `virtual:sveltepress/site`, `locale`, and `versions` all land there. Kitchen sink is the All features Entry and a Playground chrome CTA (not a site-navbar item). Blog theme and custom theme never live in that tree. Guide pages keep frozen Live code; the Blog demo stays a finished showcase. ## Comparison How does Sveltepress compare to other modern documentation solutions? | Feature / Aspect | Sveltepress | VitePress | Astro (Starlight) | Docusaurus | |---|---|---|---|---| | **Ecosystem** | Svelte / SvelteKit | Vue 3 | Framework-agnostic | React | | **Build Engine** | Vite 8 + SvelteKit 2 | Vite + Vue 3 | Vite + Astro compiler | Webpack | | **Fullstack Power** | Full SvelteKit surface (SSR, API routes, hooks) | Static-focused SSG | Requires SSR adapter | Requires Node plugins | | **Reactivity** | Svelte 5 Runes natively in `.md` | Vue 3 Composition API | Static by default (Islands) | React Hooks | | **Interactive Code** | Native live components + Twoslash hover | Vue components | Requires plugins | Requires react-live | | **Doc Versioning** | Content-addressed immutable deltas & diffs | Branch/dir-based | Requires plugins | Directory snapshot copies | | **Search** | Pagefind (built-in) / DocSearch / Meilisearch | Pagefind / DocSearch | Pagefind | Algolia / local plugins | | **AI Knowledge (`llms.txt`)** | Built-in automatic generation | Third-party plugin | Third-party plugin | Third-party plugin | ## Project structure Exactly the same as [Project structure - SvelteKit](https://kit.svelte.dev/docs/project-structure) Except for that you can use .md files for pages or layouts. For example: * `src/routes/+page.md` is recognized as home page * `src/routes/+layout.md` is used for root custom layout :::tip[Full power of SvelteKit]{icon=logos:svelte-kit} Sveltepress preserves the full power of SvelteKit. You can do more than SSG. For example use +page.server.js, +layout.server.js, hooks.server.js to do some server side logic like: Authentication, DB Docking, ... ::: ## Layout hierarchy :::note[Root layout is required]{icon=ph:layout-duotone} There must be a `src/routes/+layout.svelte` or `src/routes/+layout.md` as root layout file. **Otherwise the global layout provided by theme would not working!** ::: For example if your file tree look like this ```txt . ├─ src │ ├─ routes │ │ └─ +layout.(svelte|md) │ │ ├─ foo │ │ │ ├─ +page.(svelte|md) │ │ │ ├─ +layout.(svelte|md) ``` `theme.globalLayout` > `src/routes/+layout.(svelte|md)` > `theme.pageLayout` > `src/routes/foo/+layout.(md|svelte)` > `src/routes/foo/+page.md` Here's a graph to help you understand <script> import LayoutHierarchy from '$lib/components/LayoutHierarchy.svelte' </script> <LayoutHierarchy /> ## Configuration Sveltepress's config is passed to `@sveltepress/vite` vite plugin, all options are fully typed. Read [Vite plugins options](/reference/vite-plugin/) for more details. :::since[Internationalization guide]{version="2026-09-03" id="intro-link-i18n" summary="Link to the new multi-locale guide."} Building a multi-language site? See [Internationalization](/guide/i18n/). ::: ## Deployment It is recommended to read [Adapters - SvelteKit](https://kit.svelte.dev/docs/adapters) first. If you use `npm/yarn/pnpm create @sveltepress` to create a new project. The [Adapter Static](https://github.com/sveltejs/kit/tree/master/packages/adapter-static) would be used as default. But feel free to change to any adapters you want. --- # [Basic writing](https://sveltepress.site/guide/markdown/basic-writing) ## Heading ```md live no-ast # Heading level1 ## Heading level2 ### Heading level3 #### Heading level4 ##### Heading level5 ###### Heading level6 ``` ## Paragraph ```md live This is paragraph This is another paragraph Lines end with more than one space would be soft wrap ``` ## Bold ```md live **Bold text** ``` ## Strike through ```md live no-ast ~one~ or ~~two~~ tildes. ``` ## Inline code ```md live `const foo = 'bar'` ``` ## Links ```md live Normal links [Home page](/) [Google](https://google.com/) Auto links www.example.com https://example.com contact@example.com ``` ## List ```md live - item1 - item2 - item3 * item1 * item2 * item3 ``` ## Table ```md live no-ast | Name | Color | Count | | --- | --- | --- | | Apple | Red | 1 | | Grapes | Purple | 20 | | Banana | Yellow | 3 | ``` ## Footnote ````md live no-ast Something[^1] Big footnote[^bigfootnote] [^1]: Footnote item [^bigfootnote]: bigfootnote Indent paragraphs ```js const foo = 'bar' ``` ```` <!-- --> :::info[Label customization] You can use [`theme.footnoteLabel`](/reference/vite-plugin/#footnoteLabel) to display your custom auto generated footnotes title ::: ## Task list ```md live no-ast * [ ] to do * [x] done ``` ## Emojis ```md live I :heart: :pizza: Emojis in this text will be replaced: :dog: :+1: ``` --- # [Frontmatter](https://sveltepress.site/guide/markdown/frontmatter) ## Frontmatter in markdown Use yaml format at the beginning of your md file. ```md --- title: Some title description: some description --- ``` ## Frontmatter in svelte Export a const variable name as `frontmatter` in [Svelte script module](https://svelte.dev/docs/svelte/svelte-files#script-module) would do ```svelte title="/src/routes/foo/+page.svelte" <script module> export const frontmatter = { title: 'Some title', description: 'Some description', } </script> ``` ## Accessing frontmatter in markdown Use `fm` variable: ```md live Frontmatter of this page is: <pre> {JSON.stringify(fm, null, 2)} </pre> ``` ## Site frontmatter The global shared frontmatter for every +page.md file ```ts interface SiteConfig { title?: string description?: string } ``` --- # [Svelte in markdown](https://sveltepress.site/guide/markdown/svelte-in-markdown) This feature allows you to write `<style>`, `#if`, <code><script></code>, <code><script module></code> `#each`, `#await`, `#snippet`, `@render` `@html`, `@const`, `<svelte:xxx>`' in .md files ## Basic Here's a basic example with `#if`, `#each`, `#await`, `@html`, `@const` ```md live no-ast <script> let count = $state(0) const items = ['foo', 'bar', 'zoo'] let boolVal = $state(false) const promisePass = () => new Promise(resolve => { setTimeout(() => { resolve('Promise Passed!') }, 2000) }) const promiseFail = () => new Promise((_, reject) => { setTimeout(() => { reject('Promise Failed!') }, 2000) }) let promise = $derived(boolVal ? promisePass() : promiseFail()) </script> <ul> {#each items as item, i} {@const str = `${i + 1}: ${item}`} <li> {str} </li> {/each} </ul> <button onclick="{() => boolVal = !boolVal}"> Toggle </button> {#if boolVal} <h3 class="text-green"> Pass </h3> {:else} <h3 class="text-red"> Fail </h3> {/if} {#await promise} <h3 class="text-orange"> Loading </h3> {:then res} <h3 class="text-green"> {res} </h3> {:catch err} <h3 class="text-red"> {err} </h3> {/await} {@html "<h1>Content render with @html</h1>"} ``` :::note[Syntax Restrictions]{icon=solar:chat-square-code-outline} Always use quotes in markdown files. ```md <script> let count = $state(0) </script> <button onclick={() => count++}></button> // [svp! --] <button onclick="{() => count++}"></button> // [svp! ++] ``` Or you can wrap it with a `div` ```md <script> let count = $state(0) </script> <button onclick={() => count++}></button> // [svp! --] <div> // [svp! ++] <button onclick={() => count++}></button> // [svp! ++] </div> // [svp! ++] ``` ::: ## A Counter <Tabs activeName="Output"> <TabPanel name="Output"> > A counter <div> <button onclick={() => count++} style="margin-bottom: 12px;"> You've clicked {count} times </button> </div> </TabPanel> <TabPanel name="Input"> ```md > A counter <script> let count = $state(0) </script> <div> <button onclick={() => count++} style="margin-bottom: 12px;"> You've clicked {count} times </button> </div> ``` </TabPanel> </Tabs> --- # [Quick Start](https://sveltepress.site/guide/quick-start) ## Requirements - Node.js `^20.19.0` or `>=22.12.0` - Svelte 5, SvelteKit 2 (`2.65.1` or later) or SvelteKit 3, and Vite 8 - pnpm 10 is recommended; npm, Yarn, and Bun are also supported by the create command ## Creating a project Run the command for your package manager. :::tip[PNPM first] The repository and generated templates are tested with pnpm 10. ::: @install-pkg(@sveltepress@latest,create) ## Adding to an existing sveltekit project ### Install vite plugin package @install-pkg(@sveltepress/vite) ### Replace `sveltekit` plugin in vite.config.(js|ts) ```ts title="vite.config.(js|ts)" import { sveltekit } from '@sveltejs/kit/vite' // [svp! --] import { sveltepress } from '@sveltepress/vite' // [svp! ++] import { defineConfig } from 'vite' const config = defineConfig({ plugins: [ sveltekit(), // [svp! --] sveltepress(), // [svp! ++] ], }) export default config ``` :::warning[Remove the original `sveltekit()` plugin] `sveltepress()` already sets up SvelteKit for you. Keeping both `sveltekit()` and `sveltepress()` in `plugins` compiles every Svelte file twice and crashes the dev server with `Expected token }`. ::: ### Pass your SvelteKit config through `sveltepress()` :::since[SvelteKit 3 support]{version="2026-10-05" id="sveltekit-3" summary="Sveltepress supports SvelteKit 3. SvelteKit config goes through svelteKitOptions because svelte.config.js is no longer read."} SvelteKit 3 no longer reads `svelte.config.js`, and refuses to start while one exists. Move its options, including everything that was under `kit`, into `sveltepress({ svelteKitOptions })`, then delete `svelte.config.js`. Sveltepress adds the `'.md'` extension for you. ```ts title="vite.config.ts" // @noErrors import adapter from '@sveltejs/adapter-static' import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' import { sveltepress } from '@sveltepress/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ sveltepress({ svelteKitOptions: { preprocess: [vitePreprocess()], adapter: adapter({ pages: 'dist', }), }, }), ], }) ``` ::: :::tip[Still on SvelteKit 2?] SvelteKit 2 accepts `svelteKitOptions` too. Alternatively, keep `svelte.config.js` and add `'.md'` to its `extensions`: ```ts title="svelte.config.js" // @noErrors import adapter from '@sveltejs/adapter-static' import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' /** * @type {import('@sveltejs/kit').Config} */ const config = { extensions: ['.svelte'], // [svp! --] extensions: ['.svelte', '.md'], // add .md here // [svp! ++] preprocess: [vitePreprocess()], kit: { adapter: adapter({ pages: 'dist', }), }, } export default config ``` ::: ## Upgrading to SvelteKit 3 1. Upgrade `@sveltejs/kit` to `^3.0.0` and your adapter to its SvelteKit 3 release, for example `@sveltejs/adapter-static` `^4.0.0`. SvelteKit 3 needs Node.js `>=22.17`. 2. Run `npx sv migrate sveltekit-3` to migrate your own code, such as `$app/stores`, `base` from `$app/paths`, and `$lib` imports. 3. Make sure your SvelteKit config lives in `sveltepress({ svelteKitOptions })` as shown above. The migration moves `svelte.config.js` into a `sveltekit(...)` call in your Vite config: move that object into `svelteKitOptions` and remove the `sveltekit(...)` plugin. Sveltepress itself needs no other change. Keep these in mind: - `$lib` is replaced by the `#lib` subpath import. [Version management](/guide/version-management/) tracks `#lib/` imports like `$lib/` ones, so if your manifest shares `$lib/**`, add `#lib/**` to `content.shared` as well. - The Default Theme [`pwa`](/guide/default-theme/pwa/) option relies on `@vite-pwa/sveltekit`, which does not support SvelteKit 3 yet. On SvelteKit 3 no service worker is generated, and the build prints a warning. --- # [Themes](https://sveltepress.site/guide/themes) ## Introduction Pass `theme` option to sveltepress to use a theme Read [Vite plugin options](/reference/vite-plugin/) and [Default theme options](/reference/default-theme/) for more details | Choose | Best for | Content model | Search | |---|---|---|---| | Default theme | Documentation, product guides, API references | SvelteKit routes written as Markdown or Svelte | Algolia DocSearch or Meilisearch through a custom wrapper (see the production caveat) | | Blog theme | Editorial sites and chronological publications | Markdown files in `src/posts` plus generated routes | Built-in Pagefind integration | ## Default theme ### Install @install-pkg(@sveltepress/theme-default) ### Add in your vite config ```ts title="vite.config.(js|ts)" import { defaultTheme } from '@sveltepress/theme-default' // [svp! ++] import { sveltepress } from '@sveltepress/vite' import { defineConfig } from 'vite' const config = defineConfig({ plugins: [ sveltepress({ theme: defaultTheme(/** theme options */) // [svp! ++] }) ], }) export default config ``` Get more info about default theme in [Default theme reference](/reference/default-theme/#Theme-Options) ## Blog theme The blog theme adds post indexing, pagination, tags, categories, RSS, Open Graph images, related posts, comments, and a Pagefind search UI. It generates a static site and currently requires `@sveltejs/adapter-static` plus a Pagefind post-build step. Start with the [blog theme guide](/guide/blog-theme/getting-started/). The [live demo](https://sveltepress.github.io/sveltepress/blog-demo/) is the finished showcase; the [Playground Blog theme Entries](/playground/) auto-boot the Blog starter. ## Custom theme There is no Guide leaf for Custom theme. Open it from [Playground home](/playground/) — the Feature directory Entry auto-boots a newly authored Custom theme starter (no Default Theme, no Blog theme) at `src/routes/+layout.svelte` and `src/routes/+page.md`. Colored frames in the preview mark each wrap from the [layout hierarchy](/guide/introduction/#Layout-hierarchy): `theme.globalLayout` (`GlobalLayout.svelte`), `src/routes/+layout.svelte`, `theme.pageLayout` (`PageLayout.svelte`), and the page. Open `/foo` to also see `src/routes/foo/+layout.svelte`. Chinese and Bengali embed `custom-theme-zh` / `custom-theme-bn`, written in that language at the same default paths — not extra `/zh/` or `/bn/` routes. The Playground [Kitchen sink](/playground/kitchen-sink/) Entry is the largest Default Theme project that can coexist (Markdown, Default Theme, i18n, versions, Vite plugin, and virtual modules). Blog theme and custom theme never live in that tree. --- # [Working with TypeScript](https://sveltepress.site/guide/typescript) See: * [Vite plugin TypeScript support](/reference/vite-plugin/#Working-with-typescript) * [Default theme TypeScript support](/reference/default-theme/#Working-with-typescript) --- # [Document version management](https://sveltepress.site/guide/version-management) SveltePress can keep the latest documentation at its normal URLs while publishing immutable historical snapshots below a version prefix such as `/v/8.1/`. Version management is opt-in: sites without a `sveltepress.versions.json` manifest behave exactly as before. The Playground [Document versions](/playground/version-management/) Entry boots a Default Theme starter with a versions manifest, one frozen snapshot, an automatic What's new catalog, auto sidebar, and New badges as authored at `sveltepress.versions.json`. ## Install and initialize Install the CLI beside the Vite plugin and theme: ```sh pnpm add -D @sveltepress/cli ``` Initialize version management with the version currently served by your site: ```sh pnpm exec sveltepress versions init --current 8.1 --label "8.1" ``` This creates `sveltepress.versions.json`. The Vite plugin discovers that file automatically; use `sveltepress({ versions: false })` to disable discovery or `sveltepress({ versions: { manifest: 'path/to/versions.json' } })` to select another manifest. ## Enable incremental artifacts Existing sites can migrate their committed snapshot directories once. New sites can run the same command immediately after `versions init`: ```sh pnpm exec sveltepress versions migrate --site-id docs-example ``` Migration replaces full `src/routes/v/{id}` copies with committed `version-deltas/{id}` source deltas and initializes a content-addressed page store under `.sveltepress/version-artifacts`. The store is a build cache; commit the source deltas and restore/cache the artifact store in CI. Use the incremental command as the production build script: ```json { "scripts": { "build": "sveltepress versions build" } } ``` :::since[Overlay historical HTML]{version="2026-09-17" id="versions-overlay-html" summary="Vite prerenders current locale trees only; frozen /v/ pages are overlaid HTML that share current _app assets."} `versions plan` reports compiled, reused, removed, and recomposed routes without building, plus `vitePrerenderScope: "current-only"` and `overlaidHistoricalHtml`. `versions build` restores missing historical artifacts from committed deltas, compiles only changed current pages, then runs Vite on the **current** locale trees only (`/`, `/zh/`, `/bn/`, Playground). Frozen history is **overlaid** into `dist` as HTML that shares the current build’s `_app` assets — Vite does not mount `/v/`, `/zh/v/`, or `/bn/v/` as SvelteKit routes. A shell or index change may rewrite overlay HTML without recompiling page modules; a page compiler or artifact schema change intentionally invalidates every page artifact. ::: For GitHub Actions, restore the newest compatible store before the build and save the updated store under the current commit key: ```yaml - uses: actions/cache@v4 with: path: .sveltepress/version-artifacts key: sveltepress-pages-${{ runner.os }}-${{ github.sha }} restore-keys: | sveltepress-pages-${{ runner.os }}- - run: pnpm build ``` Use the equivalent persistent-cache facility on other CI providers. Do not share a store between different `siteId` values. Generated page modules, including Default Theme LiveCode components, are embedded in their owning page artifacts. Cache only `.sveltepress/version-artifacts`; `.sveltepress/live-code` is a local development directory and is not required when a CI worker restores reusable pages. ## LiveCode artifact self-check This page dogfoods that behavior. The interactive component below is generated from this Markdown file, embedded in the page artifact, and server-rendered again after the local `.sveltepress/live-code` directory is removed. If the card renders and its button responds, both the reusable artifact and client hydration paths are working. ```svelte live <script> let interactions = $state(0) const checks = [ 'Generated module embedded', 'Server render complete', 'Client hydration ready', ] </script> <section class="artifact-check" data-version-artifact-live-code> <div class="artifact-check__status" aria-hidden="true">✓</div> <div class="artifact-check__content"> <p class="artifact-check__eyebrow">LIVE DOCUMENTATION CHECK</p> <h3>Artifact self-check passed</h3> <ul> {#each checks as check} <li><span aria-hidden="true">✓</span>{check}</li> {/each} </ul> <button type="button" onclick={() => interactions++}> Test interaction{interactions ? ` · ${interactions}` : ''} </button> </div> </section> <style> .artifact-check { display: grid; grid-template-columns: auto 1fr; gap: 1rem; overflow: hidden; padding: 1.25rem; border: 1px solid color-mix(in srgb, currentColor 18%, transparent); border-radius: 1rem; background: radial-gradient(circle at 100% 0%, rgb(255 94 122 / 18%), transparent 45%), color-mix(in srgb, currentColor 4%, transparent); } .artifact-check__status { display: grid; width: 2.75rem; height: 2.75rem; place-items: center; border-radius: 0.85rem; color: #14231a; font-size: 1.4rem; font-weight: 800; background: #70e19b; box-shadow: 0 0 0 0.35rem rgb(112 225 155 / 12%); } .artifact-check__content h3, .artifact-check__content p { margin: 0; } .artifact-check__eyebrow { color: #ff5e7a; font-size: 0.72rem; font-weight: 800; letter-spacing: 0.14em; } .artifact-check__content h3 { margin-top: 0.15rem; font-size: 1.2rem; } .artifact-check__content ul { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.85rem 0; padding: 0; list-style: none; } .artifact-check__content li { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.3rem 0.55rem; border-radius: 999px; font-size: 0.78rem; background: color-mix(in srgb, currentColor 8%, transparent); } .artifact-check__content li span { color: #45c97c; font-weight: 800; } .artifact-check__content button { padding: 0.55rem 0.8rem; border: 1px solid color-mix(in srgb, currentColor 20%, transparent); border-radius: 0.65rem; color: inherit; font: inherit; font-size: 0.85rem; font-weight: 700; background: transparent; cursor: pointer; } .artifact-check__content button:hover { border-color: #ff5e7a; } </style> ``` ## Create a release snapshot :::since[Advance before editing the next version]{version="2026-08-31" id="next-version-doc-workflow" summary="Freeze outgoing docs before editing and marking the new current version."} Start from clean, complete outgoing documentation. Build and create the next version before editing its pages or adding its `:::since` markers. `create` freezes the outgoing current version and makes the supplied ID current; only then should new documentation use that ID. ```sh pnpm exec sveltepress versions build pnpm exec sveltepress versions create 8.2 --label "8.2" # 8.2 is now current: edit docs and add version="8.2" markers pnpm exec sveltepress versions build pnpm exec sveltepress versions validate ``` Never run `versions create` over documentation that already contains the next version's edits. Those edits would be frozen into the outgoing version. If work started early, restore the known clean outgoing state, build and advance it, then reapply the edits to the new current version. ::: :::since[CLI --locale for per-locale manifests]{version="2026-09-03" id="versions-cli-locale" summary="Pass --locale to target sveltepress.versions.<locale>.json for zh, bn, and other locales."} Multi-locale sites keep one versions manifest per locale (`sveltepress.versions.json`, `sveltepress.versions.zh.json`, `sveltepress.versions.bn.json`, …). Pass `--locale <id>` to `init`, `create`, `validate`, and other versions subcommands so they read and write that locale's manifest and deltas (for example `version-deltas-zh/`). Default locale commands omit `--locale`. `sveltepress versions build` with no `--locale` drafts every locale, then composes `/v/`, `/zh/v/`, `/bn/v/`, … into one production output. Use `--locale zh` (optionally with `--draft-only`) only when you need a single-locale job. ```sh sveltepress versions build --locale zh sveltepress versions create 8.2 --label "8.2" --locale zh sveltepress versions validate --locale zh ``` See [Internationalization](/guide/i18n/) for how locales and version bases compose (`/zh/v`, `/bn/v`). Each locale freeze is independent: run `build` then `create` per locale (default, then `--locale zh`, `--locale bn`, …). A default-locale create must only capture unprefixed English routes — never `src/routes/zh/` or `src/routes/bn/`. Locale creates write logical routes into `version-deltas-<locale>/` (no double `/zh/v/.../zh/...` prefixes). If a freeze accidentally included sibling locale trees, recreate it with the locale-scoped CLI rather than hand-editing deltas. ::: `create` publishes the current draft manifest, writes only changed source pages and tombstones to `version-deltas/8.1/`, freezes route/sidebar/change metadata, moves `8.1` into history, and makes `8.2` current. It refuses stale drafts, duplicate IDs, symbolic links, a dirty Git worktree, and dependencies outside the frozen boundary. Use `--allow-dirty` only when the uncommitted state is intentionally the release source. Published versions also receive a generated `sourceHash`. Each delta binds the frozen route, sidebar, and change catalog with a metadata hash. `versions validate` reconstructs every committed delta and checks both hashes, so source or metadata drift is detected even when the artifact cache is empty. Do not edit the hashes or delta files by hand. The operation is atomic: a failed preflight does not leave a partial delta or update the manifest. `versions list` prints the version order, `versions publish 8.1` prints the immutable manifest hash for CI publication, and `versions gc --dry-run` reports unreferenced local blobs before cleanup. ## Manifest ```json { "$schema": "./node_modules/@sveltepress/cli/schema/versions.schema.json", "basePath": "/v", "current": { "id": "8.2", "label": "8.2" }, "versions": [ { "id": "8.1", "label": "8.1", "status": "deprecated", "message": "Upgrade to 8.2 for fixes.", "sourceRef": "v8.1.0", "search": { "facetFilters": ["version:8.1"] } } ], "content": { "include": ["**"], "exclude": ["internal/**"], "shared": ["$lib/**", "static/**"] }, "artifacts": { "mode": "incremental", "siteId": "docs-example", "store": ".sveltepress/version-artifacts", "sources": "version-deltas" } } ``` Version IDs are URL-safe lowercase identifiers and may contain dots or hyphens. `include` and `exclude` select route files to freeze. `shared` explicitly allows dependencies that remain live instead of being copied; keep this list narrow because a later change can affect every historical version. Set `status` to `deprecated` or `eol` to show a prominent site-wide banner above the navigation. It warns that older-site functionality may be unavailable and links to the current version of the same logical page. `sourceRef` redirects historical edit links to a matching Git ref; set `editLink: false` to hide them. EOL versions default to `noindex`, and `noIndex: true` can opt another version out of indexing. ## Navigation and missing pages The Default Theme adds an accessible version selector automatically. Internal links and the frozen sidebar stay in the selected historical version when the target route existed there. Switching versions preserves the logical page when possible; otherwise it opens that version's home page and displays an explanatory notice. Custom themes can import `virtual:sveltepress/versions`. It exposes the validated manifest plus `resolveVersionContext`, `resolveVersionedPath`, and `resolveVersionSwitch` helpers. Browser code that imports these helpers directly from the package should use `@sveltepress/vite/versioning/runtime`; this entry contains no Node file-system code. Build and configuration code can continue to use `@sveltepress/vite/versioning`. ## Search, PWA, and generated files :::since[Historical Local Search vs DocSearch]{version="2026-09-03" id="version-historical-pagefind-search" summary="Pagefind historical indexes freeze on version release; DocSearch still needs search metadata."} Built-in **Local Search** (Pagefind) keeps working on historical versions: production builds freeze each version's Pagefind assets via `syncHistoricalPagefind`, and the theme loads `/v/{id}/pagefind/` (or the locale-prefixed equivalent) while you browse that snapshot. You do **not** need a `search` object on the version for Local Search. **DocSearch** and custom `search` components still require an explicit `search` object on that historical version (for example `indexName` / `facetFilters`). Without it, the navbar shows that search is unavailable for the documentation version, so current remote results are not mistaken for historical docs. The Default Theme passes the selected version and search metadata to custom search components and merges configured DocSearch facet filters. ::: Version-aware builds also: * emit canonical links for every page and `noindex` for configured versions; * include current and historical URLs in `sitemap.xml`; * keep root `llms.txt`/`llms-full.txt` current-only and write historical files below `/v/{id}/`; * exclude historical HTML from PWA precaching and fetch it with a network-first strategy. Treat `version-deltas` as immutable release source: review and commit it, but do not edit it by hand. The artifact store can be restored from those deltas on a cold CI worker, while a persistent CI cache avoids recompiling historical pages. Make corrections in current routes and create the next release delta. ## Describe what changed SveltePress compares the current route inventory with the most recent historical version in manifest order. A route that only exists in the current version is listed as a new page. Use page frontmatter to provide a focused summary or keep a page out of the change catalog: ```yaml --- title: What's new versionChanges: exclude: true summary: Optional overview summary --- ``` For an existing Markdown page, mark only the important new section. The version, title, and page-unique stable ID are required: ```md ### Hot reload Existing documentation content. :::since[Hot reload configuration]{version="8.2" id="hot-reload" summary="No restart required"} New documentation content. ::: ``` Unknown versions, duplicate IDs, unknown fields, and invalid field types stop development and production builds. New pages appear only under **New pages**; their `since` sections are not repeated under **Updated pages**. The first managed version has no comparison baseline and does not report the entire site as new. ### Automatic "New" badges on pages, sections, and navigation The Default Theme automatically marks newly introduced features across multiple navigation and content surfaces. Importantly, these badges are **only shown while browsing the specific documentation version that introduced them**; they automatically hide or adjust when switching to older or newer versions: 1. **Page title badge (page level)**: - When a page is newly added in the active version compared to the preceding baseline version (listed under `newPages`), the Default Theme automatically displays an accent badge beside the main page title (`h1.page-title`), such as `New in 8.2`. - The badge template defaults to `"New in {version}"`. Customize it via `i18n.versionNewLabel` in theme options (where `{version}` is replaced with the active version's label or ID). - When browsing subsequent versions, the page is no longer considered new, so the title badge is automatically omitted. 2. **Section and paragraph badge (section level)**: - In existing pages, sections wrapped in `:::since[Title]{version="8.2" id="unique-id" summary="..."}` directives are rendered as stylized container boxes with an automatic version badge in their section header. - The section badge only displays when the user browses the exact version specified in `version="..."`. When browsing other versions, the badge is hidden. The text template also follows `i18n.versionNewLabel`. 3. **In-page navigation badge ("On this page" / TOC level)**: - The right-side "On this page" table of contents automatically resolves associations between headings and `:::since` markers: - Headings contained inside a `:::since` marker are associated directly; - When a `:::since` marker contains no headings of its own, the Default Theme automatically associates it with the nearest preceding heading within the same Markdown container; - Multiple markers can belong to one heading. - While browsing the version that introduced those changes, any associated heading anchor in the table of contents automatically renders a compact **New** badge (`VersionNavigationBadge`). - The compact badge text defaults to `"New"`. Override it with `i18n.versionNavigationNewLabel`. 4. **Sidebar navigation badge (sidebar level)**: - The sidebar automatically adds the compact **New** badge to both new pages (`newPages`) and updated pages (`updatedPages`), also configured via `i18n.versionNavigationNewLabel`. ### What's New overview page In addition to contextual badges on pages, sections, and navigation, you can embed the full release change catalog anywhere on your site: ```svelte title="src/routes/whats-new/+page.svelte" <script> import VersionChanges from '@sveltepress/theme-default/VersionChanges.svelte' </script> <VersionChanges /> ``` See the official site's live catalog on the [What's new page](/whats-new/). The Playground [Document versions](/playground/version-management/) starter ships the same catalog at `/whats-new/`, auto sidebar under `/guide/`, and New badges on the current 1.1 pages. :::since[Route-scoped What's New catalogs]{version="2026-08-31" id="version-scoped-whats-new" summary="Keep each What's New route bound to its own frozen preceding-version comparison."} `VersionChanges` defaults to the documentation version resolved from the current page URL. A valid `?version={id}` explicitly overrides that context. Each version's catalog is generated against its immediately preceding version in manifest order, while frozen historical catalogs stay independent of later current documentation. Current links stay unprefixed; historical links target `/v/{id}/...`, including exact section anchors. ::: Custom themes can read the same immutable data from `virtual:sveltepress/versions`: ```ts import { changeSets, resolveVersionChanges, } from 'virtual:sveltepress/versions' const currentChanges = resolveVersionChanges() const historicalChanges = resolveVersionChanges('8.1') ``` `versions create` freezes the outgoing current change set in its immutable artifact manifest and source delta. Historical catalogs are always read from frozen metadata rather than reconstructed from later current documentation, and `versions validate` detects malformed markers, invalid references, duplicate anchors, corrupt artifacts, and delta drift. --- ## reference # [Blog theme](https://sveltepress.site/reference/blog-theme) The `@sveltepress/theme-blog` package provides a content-centered, responsive blog theme for SveltePress with built-in post listing, pagination, tags, categories, reading time, RSS feed generation, and Giscus comments. ## Installation ```sh pnpm add -D @sveltepress/theme-blog ``` ## Vite Configuration ```ts title="vite.config.ts" // @noErrors import { blogTheme } from '@sveltepress/theme-blog' import { sveltepress } from '@sveltepress/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ sveltepress({ theme: blogTheme({ author: { name: 'Jane Doe', avatar: '/avatar.png', bio: 'Writing about web development.', }, pageSize: 10, }), }), ], }) ``` ## Theme Options * `author`: `AuthorProfile | string` - Author details including `name`, `avatar`, `bio`, and `socials`. * `logo`: `string` - Path to the blog logo in `static/`. * `footer`: `string` - Custom footer text or markdown. * `postsDir`: `string` - Markdown posts directory (defaults to `'src/routes/posts'`). * `pageSize`: `number` - Number of posts per pagination page (default: `10`). * `themeColor`: `ThemeColor` - Colors for `primary`, `secondary`, `bg`, and `surface`. * `giscus`: `GiscusConfig` - Giscus comment system integration (`repo`, `repoId`, `category`, `categoryId`). * `socials`: `AuthorSocials` - Links for GitHub, Twitter/X, Mastodon, Bluesky, RSS, and email. * `pwa`: `SvelteKitPWAOptions` - PWA service worker options. * `highlighter`: Shiki code highlighter configuration. ## Virtual Modules The blog theme Vite plugin provides several virtual modules for querying blog content: | Module | Exports / Type | Description | | --- | --- | --- | | `virtual:sveltepress/blog-config` | `blogConfig: BlogThemeOptions` | The resolved blog configuration passed to `blogTheme()`. | | `virtual:sveltepress/blog-posts-meta` | `posts: BlogPostMeta[]` | Lightweight metadata array for all published blog posts. | | `virtual:sveltepress/blog-post/<slug>` | `default: BlogPost` | Full post record with pre-rendered HTML content. | | `virtual:sveltepress/blog-tags-index` | `tags: Array<{ name, count }>` | List of all tags and their post counts. | | `virtual:sveltepress/blog-tag/<tag>` | `default: BlogPostMeta[]` | Posts matching a specific tag. | | `virtual:sveltepress/blog-categories-index` | `categories: Array<{ name, count }>` | List of all categories and their post counts. | | `virtual:sveltepress/blog-category/<cat>` | `default: BlogPostMeta[]` | Posts matching a specific category. | | `virtual:sveltepress/blog-runtime` | `postsJsonDir, tagsJsonDir, categoriesJsonDir` | Absolute disk cache paths for server load functions. **Server only**. | ## Public Components You can import and customize the theme's built-in Svelte components: * `@sveltepress/theme-blog/AuthorCard.svelte` - Compact author banner. * `@sveltepress/theme-blog/AuthorProfile.svelte` - Full author profile with social links. * `@sveltepress/theme-blog/PostMeta.svelte` - Post date, reading time, author, and tags bar. * `@sveltepress/theme-blog/GiscusComments.svelte` - Embedded Giscus comments container. * `@sveltepress/theme-blog/GlobalLayout.svelte` - Global layout wrapper. * `@sveltepress/theme-blog/PostLayout.svelte` - Individual post layout wrapper. --- # [Sveltepress CLI](https://sveltepress.site/reference/cli) The `@sveltepress/cli` package provides the `sveltepress` command line tool for managing document versions, manifests, immutable source deltas, and incremental build artifacts. ## Installation ```sh pnpm add -D @sveltepress/cli ``` ## Synopsis ```sh sveltepress versions <command> [options] ``` ## Subcommands ### `init` Initialize a new version manifest file (`sveltepress.versions.json`): ```sh sveltepress versions init --current 1.0 --label "1.0" ``` * `--current <id>`: Initial version identifier (default: `'current'`). * `--label <label>`: Display label for the current version. * `--base-path <path>`: Root prefix for historical routes (default: `'/v'`). * `--locale <id>`: Target locale (e.g. `zh`, `bn`). Creates `sveltepress.versions.<locale>.json`. ### `create` Freeze the current documentation into an immutable historical version snapshot: ```sh sveltepress versions create 1.0 --label "v1.0" ``` * `<version-id>`: Unique identifier for the frozen release. * `--label <label>`: Display label in the version selector (defaults to `<version-id>`). * `--locale <id>`: Freeze only the specified locale manifest. * `--allow-dirty`: Allow creating a release with uncommitted git changes. ### `list` Print all registered documentation versions and their statuses: ```sh sveltepress versions list sveltepress versions list --locale zh ``` * `--locale <id>`: Target locale. ### `validate` Verify manifest structure, release change catalogs, artifact integrity, and delta consistency: ```sh sveltepress versions validate sveltepress versions validate --locale bn ``` * `--locale <id>`: Target locale. :::since[Current-only Vite compose]{version="2026-09-17" id="versions-cli-overlay-plan" summary="versions plan reports overlay counts; versions build prerenders current trees then overlays frozen /v/ HTML."} `versions plan` JSON includes `vitePrerenderScope: "current-only"` and `overlaidHistoricalHtml`. Without `--locale`, `versions build` prerenders current locale trees only, then overlays frozen `/v/` HTML into the same output. ::: ### `plan` Preview the incremental build plan, showing page reuse, changed routes, overlay counts, and invalidations. The JSON includes `vitePrerenderScope: "current-only"` and `overlaidHistoricalHtml`. ```sh sveltepress versions plan ``` * `--locale <id>`: Target locale. ### `build` Execute version-aware production compilation. Without `--locale`, it drafts every locale, runs Vite on current trees only, then overlays frozen `/v/` HTML into the same output: ```sh sveltepress versions build sveltepress versions build --locale zh ``` * `--locale <id>`: Build only the specified locale. * `--draft-only`: Compile draft artifacts without composing final outputs. ### `compose` Compose precompiled page artifacts into the final site distribution directory: ```sh sveltepress versions compose ``` * `--locale <id>`: Target locale. ### `publish` Update lifecycle status (`stable`, `deprecated`, `eol`) and advisory messages in the manifest: ```sh sveltepress versions publish --status deprecated --message "Please upgrade to v2" ``` * `--status <stable|deprecated|eol>`: New lifecycle status. * `--message <text>`: Advisory warning text for the lifecycle banner. * `--locale <id>`: Target locale. ### `migrate` Migrate older version metadata to the content-addressed incremental artifact store: ```sh sveltepress versions migrate --site-id my-docs ``` * `--site-id <id>`: Unique identifier for the artifact store. * `--store <dir>`: Custom artifact store directory. * `--locale <id>`: Target locale. ### `gc` Clean up orphaned or unreferenced page artifacts from the local artifact store: ```sh sveltepress versions gc ``` * `--locale <id>`: Target locale. --- # [Default theme](https://sveltepress.site/reference/default-theme) ## Install @install-pkg(@sveltepress/theme-default) ## Add in your Vite config ```js title="vite.config.(js|ts)" import { defaultTheme } from '@sveltepress/theme-default' // [svp! ++] import { sveltepress } from '@sveltepress/vite' import { defineConfig } from 'vite' const config = defineConfig({ plugins: [ sveltepress({ theme: defaultTheme(/** theme options */) // [svp! ++] }) ], }) export default config ``` ## Types Overview @code(/../theme-default/types.d.ts) ## Theme Options ### `navbar` * `title` The label text of the item in the navigation bar. * `to` The link address * `icon` An HTML string. Will show the html content instead of `title`. It is useful to display a custom icon on the navbar. * `external` Determine whether the link is external or not. Would render an external icon if set to `true` * `items` Children links. If this prop is provided it would render a dropdown instead of a single link. :::since[Section tabs]{version="2026-10-05" id="theme-section-tabs" summary="sectionTabs adds a row of section tabs under the navbar, and the sidebar follows the active tab."} ### `sectionTabs` Tabs shown in a row under the navbar on pages with a sidebar, one tab per documentation section. Below the `950px` breakpoint they move into the sidebar drawer. See [Section tabs](/guide/default-theme/navbar/#Section-tabs). * `title` The tab label. * `to` The section's landing page. Like navbar links, it follows the active locale and documentation version; external URLs open in a new tab. * `icon` An Iconify icon in `collection:name` form, such as `tabler:rocket`, or an HTML string such as an inline SVG. The icons of the `sectionTabs` passed to `defaultTheme()` are pre-built automatically. * `activeMatch` A route prefix, or an array of prefixes, that makes the tab active. Defaults to the most specific sidebar key that contains `to`, or to `to` itself when no sidebar key does. ::: ### `discord` The web invite address to the project's Discord server. Would show a discord icon on the navigation bar when provided. ### `github` The web address of the project's GitHub repository. Would show a GitHub icon on the navigation bar when provided ### `logo` The address of the site's logo, to be shown on the navigation bar. ### `sidebar` The sidebar can be configured in two ways: #### Auto-generated sidebar Pass an object with `enabled: true` to automatically generate the sidebar from your routes directory. * `enabled` - Set to `true` to enable auto sidebar generation * `routesDir` - Custom routes directory path. Default is `'src/routes'` * `roots` - Root paths to generate sidebar for, e.g. `['/guide/', '/reference/']`. If not specified, auto-detected from top-level route directories. Historical version snapshot trees (`/v/{id}/`, including locale-composed bases) are skipped. Pages can control their sidebar appearance via frontmatter: * `title` - Page title, also used as sidebar label * `sidebarTitle` - Override the sidebar label (takes precedence over `title`) * `order` - Sort order within the same level. Lower numbers appear first. Default is `100` * `sidebar` - Set to `false` to exclude this page from the sidebar * `collapsible` - Whether the sidebar group is collapsible #### Manual sidebar Pass an object where each key is a group route prefix and each value is an array of sidebar items: * `title` The label text of the sidebar item * `collapsible` Determine whether the sidebar group is collapsible or not. * `to` The link address * `items` Children links. If this prop is provided would render a sidebar group instead of a single sidebar item ### `highlighter` An object that contains custom highlight options. * `languages` - Customize the supported highlight languages. Default is: `['svelte', 'sh', 'js', 'html', 'ts', 'md', 'css', 'scss']` * `themeLight` - The code theme that will be applied in light mode. Default is `vitesse-light` * `themeDark` - The code theme that will be applied in dark mode. Default is `night-owl` * `twoslash` - Set to `true` to enable [Twoslash](/guide/default-theme/twoslash/). Default is `false` * `codeCollapseLines` - Collapse code blocks taller than this number of lines. Default is `30`; set it to `0` to disable collapsing. :::important[TIP] You can get all the supported languages and themes in [Shiki Repo](https://github.com/shikijs/shiki) ::: ### `editLink` The link used for bottom edit this page on GitHub button For example this site uses `https://github.com/SveltePress/sveltepress/edit/main/packages/docs-site/src/routes/:route` `:route` represent the route path, for example: `/foo/bar/+page.md` ### `ga` The id provided by [Google Analytics](https://analytics.google.com/). Something like `G-XXXXXXX`. Would add gtag script in site head if provided. :::since[Language switcher and default Local Search]{version="2026-09-03" id="theme-locale-switcher-localsearch" summary="Locales enable the navbar language switcher; Local Search remains the default when docsearch/search are unset."} When `sveltepress({ locales })` is configured, the Default Theme shows a language switcher in the navbar (customize copy with `i18n.localeSwitcher` / `i18n.localePageUnavailable`). See [Internationalization](/guide/i18n/). If neither `docsearch` nor a custom `search` component is set, the theme uses built-in **Local Search** (Pagefind). Set `search: false` to hide search entirely. Details: [Search](/guide/default-theme/search/). ::: ### `search` The supported custom-search hook, with type `Component | string`. Use it to integrate a Svelte search component such as `@sveltepress/meilisearch`. A string identifies the wrapper component's source path: ```ts import { defaultTheme } from '@sveltepress/theme-default' defaultTheme({ search: '/src/lib/MeilisearchSearch.svelte', }) ``` :::note[Production builds] This public API and the M Search integration are supported. A source `.svelte` path configured through `search` is bundled into static production builds — the theme resolves it at build time and loads it as a lazy chunk. Passing a component object directly is not supported, because theme options are serialized to JSON for the client. See the [search guide](/guide/default-theme/search/) for the wrapper setup. ::: ### `docsearch` * `appId` * `apiKey` * `indexName` All these values are provided by Algolia. Visit [Docsearch](https://docsearch.algolia.com/) for more details. > `docsearch` is used only when `search` is not provided. ### `pwa` See [PWA](/guide/default-theme/pwa/) for details. :::since[App-shell client precache]{version="2026-09-09" id="theme-pwa-precache-client" summary="pwa.precacheClient defaults to the app shell so service-worker updates stay fast."} `pwa.precacheClient` (default `false`) precaches only the app shell. Set `precacheClient: true` to restore the previous catch-all client glob. HTML policy remains on `pwa.precachePages`. ::: ### `themeColor` The color of window bar when opened as a local PWA application. * `light` - the color that applied on light theme * `dark` - the color that applied on dark theme * `gradient` - the gradient theme color. Would be applied on home page action button and main title. Default is: ```js const defaultGradient = { start: '#fa709a', end: '#fee140', } ``` * `primary` - the primary theme color of the site * `primaryDeep` - a darker primary color used for accessible accent text on light backgrounds * `hover` - the hovered links color ### `i18n` The fixed text contents that can be replaced by your config. * `onThisPage` - The text for "On this page" * `suggestChangesToThisPage` - The text for "Suggest changes to this page" * `lastUpdateAt` - The text for "Last update at:" * `previousPage` - The text for "Previous" * `nextPage` - The text for "Next" * `expansionTitle` - The text for "Click to expand/fold code" in markdown or svelte live code * `expandCode` - The label on the expand bar for collapsed long code blocks * `heroCode` - The localized text inside the default home page code preview: `title`, `messageBefore`, `messageStrong`, `messageAfter`, `tipLabel`, and `counterLabel` * `versionDeprecated` / `versionEol` - The message in the global old-version lifecycle bar * `versionDeprecatedLabel` / `versionEolLabel` - The compact lifecycle status label * `versionViewCurrent` - The link label that opens the current version of the same logical page * `versionNewLabel` - The template for page title and section badges. Use `{version}` for the version label placeholder. Default is `"New in {version}"` * `versionNavigationNewLabel` - The compact badge shown beside changed sidebar pages and table-of-contents headings automatically associated with `:::since` markers. Default is `"New"` * `pwa` - The PWA prompt relative text contents. All of the following fields correspond to the same text in PWA prompt * `tip` * `reload` * `close` * `appReadyToWorkOffline` * `newContentAvailable` * `footnoteLabel` - The auto generated footnotes title. Default is `"Footnotes"` * `sectionTabsLabel` - The accessible label of the [section tabs](#sectionTabs) navigation. Default is `"Sections"` :::since[Optional title-row action]{version="2026-09-09" id="theme-title-row-action" summary="PageLayout can render an optional action after the page title and version-new badge."} ### Title-row action Default Theme `PageLayout` can render an optional control on the `h1` row, after the title and any version-new badge. Sites that omit it keep today's title row. Pass a `titleAction` snippet to `PageLayout`, or provide a `{ current }` object through `TITLE_ROW_ACTION_KEY` from `@sveltepress/theme-default/context` (`current` is `{ href, label }` or `undefined`): ```svelte <script> import { TITLE_ROW_ACTION_KEY } from '@sveltepress/theme-default/context' import { setContext } from 'svelte' const action = $derived({ href: '/playground/markdown/basic-writing/', label: 'Open in Playground', }) setContext(TITLE_ROW_ACTION_KEY, { get current() { return action }, }) </script> ``` The action is a same-page link. Theme consumers do not inherit a SveltePress-docs-only control unless they set one. ::: ### `preBuildIconifyIcons` The icons in [Iconify](https://iconify.design/) that you want to pre-build for future usage. An object, key is the collection name, value is the icons array. For example these are the icons this site using: @code(/vite.config.ts,25,39) These icons looks like this: ```svelte live <script> import { IconifyIcon } from '@sveltepress/theme-default/components' import themeOptions from 'virtual:sveltepress/theme-default' </script> <div class="flex items-center gap-4 text-[48px] flex-wrap"> {#each Object.entries(themeOptions.preBuildIconifyIcons || {}) as [collection, names]} {#each names as name} <div> <IconifyIcon {collection} {name} /> </div> {/each} {/each} </div> ``` ## Global context The global context key is in the `@sveltepress/theme-default/context`. You can get all the contexts with the [`getContext`](https://svelte.dev/docs/svelte#getcontext) API This is an example: ```svelte live <script lang="ts"> import type { SveltepressContext } from '@sveltepress/theme-default/context' import { SVELTEPRESS_CONTEXT_KEY } from '@sveltepress/theme-default/context' import { getContext } from 'svelte' const { isDark } = getContext<SveltepressContext>(SVELTEPRESS_CONTEXT_KEY) </script> <div class:dark-text={$isDark} class="text-10"> isDark: {$isDark} </div> <style> .dark-text { --at-apply: 'text-red'; } </style> ``` All contexts: * `$isDark` - Determine the current theme is dark or not. It is a [reactive svelte store](https://svelte.dev/docs/svelte-store). ## Virtual modules ### `virtual:sveltepress/theme-default` This module hold the theme options that pass to `defaultTheme()` function. Here's an example for showing the theme options of this site: ```svelte live <script> import { JsonViewer } from 'svelte-json-discovery' import themeOptions from 'virtual:sveltepress/theme-default' </script> <div class="viewer"> <JsonViewer data={themeOptions} /> </div> <style> .viewer { max-height: 40vh; overflow: auto; } :global(html.dark) .viewer { --discovery-background-color: #1a1a1a; --sjd-app-bg: #1a1a1a; --sjd-fmt-color: #999; --sjd-fmt-hover-color: #aaa; --sjd-fmt-property-color: #d17a8c; --sjd-fmt-number-color: #0f8dc2; --sjd-fmt-atom-color: #0f8dc2; --sjd-fmt-string-color: #7faf20; --sjd-fmt-string-underline-color: #85ab51; --sjd-fmt-string-hover-color: #97cf26; --sjd-ui-color: #ccc; --sjd-match-bg: #565638; --sjd-match-border: #a7a73b; --sjd-error-border: #0004; --sjd-error-bg: #622b29; --sjd-error-color: #c66; --sjd-error-message-bg: #443232; --sjd-toggle-color: #72b372; --sjd-touch-button-color: #aaa; --sjd-touch-button-bg: #50505080; --sjd-popup-bg: #333; --sjd-popup-color: #ccc; --sjd-popup-notes-color: #999; --sjd-popup-error-color: #e66; color-scheme: dark; } </style> ``` ## Working with TypeScript You need to include `@sveltepress/theme-default/types` in your `src/app.d.ts` to get theme options and virtual modules type tips ```ts title="/src/app.d.ts" /// <reference types="@sveltepress/theme-default/types" /> // Your other types ``` --- # [virtual:sveltepress/locale](https://sveltepress.site/reference/locale) When multi-locale support is enabled in `@sveltepress/vite` via `locales`, `virtual:sveltepress/locale` exposes the resolved locale configurations, route locale detection, localized link helpers, and language switch resolution. Custom themes and components can import this module to build language switchers, localized navigation links, and locale-aware UI chrome. The Playground [Virtual modules](/playground/virtual-modules/) Entry boots the Kitchen-sink starter where `virtual:sveltepress/site`, `locale`, and `versions` are all populated at `src/routes/reference/virtual-modules/+page.md`. Chinese and Bengali embed `kitchen-sink-zh` / `kitchen-sink-bn` at that same path. That page writes Svelte in markdown and renders them with the same `JsonViewer` as this page. This docs page keeps svelte live so it can show the source next to the rendered result. Open in Playground on this leaf lands there. ## Live data Here is an interactive inspection of the `locales` configuration on this site: ```svelte live <script> import { JsonViewer } from 'svelte-json-discovery' import { locales } from 'virtual:sveltepress/locale' </script> <div class="viewer"> <JsonViewer data={locales} /> </div> <style> .viewer { max-height: 40vh; overflow: auto; } :global(html.dark) .viewer { --discovery-background-color: #1a1a1a; --sjd-app-bg: #1a1a1a; --sjd-fmt-color: #999; --sjd-fmt-hover-color: #aaa; --sjd-fmt-property-color: #d17a8c; --sjd-fmt-number-color: #0f8dc2; --sjd-fmt-atom-color: #0f8dc2; --sjd-fmt-string-color: #7faf20; --sjd-fmt-string-underline-color: #85ab51; --sjd-fmt-string-hover-color: #97cf26; --sjd-ui-color: #ccc; --sjd-match-bg: #565638; --sjd-match-border: #a7a73b; --sjd-error-border: #0004; --sjd-error-bg: #622b29; --sjd-error-color: #c66; --sjd-error-message-bg: #443232; --sjd-toggle-color: #72b372; --sjd-touch-button-color: #aaa; --sjd-touch-button-bg: #50505080; --sjd-popup-bg: #333; --sjd-popup-color: #ccc; --sjd-popup-notes-color: #999; --sjd-popup-error-color: #e66; color-scheme: dark; } </style> ``` ## Module exports ### `locales` * **Type**: `LocalesConfig | null` The multi-locale configuration dictionary keyed by route prefix (`'/'`, `'/zh/'`, `'/bn/'`, etc.). Returns `null` on a single-locale site where `locales` is omitted. ### `resolveLocale` * **Type**: `(pathname: string, base?: string) => ResolvedLocale | null` Resolves the active `ResolvedLocale` object for a URL pathname, containing: * `prefix`: The route prefix (such as `'/'`, `'/zh/'`, or `'/bn/'`). * `lang`: The BCP 47 language tag (e.g. `'en'`, `'zh-CN'`, `'bn'`). * `label`: User-facing label (e.g. `'English'`, `'简体中文'`, `'বাংলা'`). * `title`: Optional site title override for this locale. * `description`: Optional site description override for this locale. * `theme`: Theme options specific to this locale. ### `resolveLocalizedPath` * **Type**: `(to: string, locale: ResolvedLocale | null, base?: string) => string` Prepends the given locale's prefix to an internal link pathname if the target does not already have it. Returns external and unlocalized links unchanged. ### `resolveLocaleSwitch` * **Type**: `(pathname: string, targetPrefix: string, base?: string) => LocaleSwitchTarget | null` Calculates the destination URL and fallback state when switching language from `pathname` to `targetPrefix`. Returns `{ href, fallback }`, where `fallback` is `true` if the page does not exist in the target language and falls back to that language's home route. ### `default` * **Type**: `LocaleRuntime` The default export is the locale runtime object itself, bundling `locales`, `resolveLocale`, `resolveLocalizedPath`, and `resolveLocaleSwitch`. ## Working with TypeScript Include `@sveltepress/vite/types` in your `src/app.d.ts` to get full type tips for `virtual:sveltepress/locale`: ```ts title="/src/app.d.ts" /// <reference types="@sveltepress/vite/types" /> // Your other types ``` --- # [virtual:sveltepress/site](https://sveltepress.site/reference/site) The `virtual:sveltepress/site` module exposes the core site metadata (`title`, `description`) passed to `sveltepress({ siteConfig })`. Custom themes, components, and layout templates can import this module to display the global site name and description or populate meta tags. The Playground [Virtual modules](/playground/virtual-modules/) Entry boots the Kitchen-sink starter as authored at `src/routes/reference/virtual-modules/+page.md`. Chinese and Bengali embed `kitchen-sink-zh` / `kitchen-sink-bn`, written in that language at the same default path. That page writes Svelte in markdown and renders `virtual:sveltepress/site`, `locale`, and `versions` with the same `JsonViewer` as this page. This docs page keeps svelte live so it can show the source next to the rendered result. Open in Playground on this leaf, [`virtual:sveltepress/locale`](/reference/locale/), and [`virtual:sveltepress/versions`](/reference/versions/) all land there. ## Live data Here is an interactive inspection of `siteConfig` on this site: ```svelte live <script> import { JsonViewer } from 'svelte-json-discovery' import siteConfig from 'virtual:sveltepress/site' </script> <div class="viewer"> <JsonViewer data={siteConfig} /> </div> <style> .viewer { max-height: 40vh; overflow: auto; } :global(html.dark) .viewer { --discovery-background-color: #1a1a1a; --sjd-app-bg: #1a1a1a; --sjd-fmt-color: #999; --sjd-fmt-hover-color: #aaa; --sjd-fmt-property-color: #d17a8c; --sjd-fmt-number-color: #0f8dc2; --sjd-fmt-atom-color: #0f8dc2; --sjd-fmt-string-color: #7faf20; --sjd-fmt-string-underline-color: #85ab51; --sjd-fmt-string-hover-color: #97cf26; --sjd-ui-color: #ccc; --sjd-match-bg: #565638; --sjd-match-border: #a7a73b; --sjd-error-border: #0004; --sjd-error-bg: #622b29; --sjd-error-color: #c66; --sjd-error-message-bg: #443232; --sjd-toggle-color: #72b372; --sjd-touch-button-color: #aaa; --sjd-touch-button-bg: #50505080; --sjd-popup-bg: #333; --sjd-popup-color: #ccc; --sjd-popup-notes-color: #999; --sjd-popup-error-color: #e66; color-scheme: dark; } </style> ``` ## Module exports ### `default` * **Type**: `{ title: string, description: string }` The default export is the site configuration object: * `title`: The site title configured in `siteConfig.title`. Defaults to `'Untitled site'` if not specified. * `description`: The site description configured in `siteConfig.description`. Defaults to `'Build by sveltepress'` if not specified. ## Working with TypeScript Include `@sveltepress/vite/types` in your `src/app.d.ts` to get full type tips for `virtual:sveltepress/site`: ```ts title="/src/app.d.ts" /// <reference types="@sveltepress/vite/types" /> // Your other types ``` --- # [virtual:sveltepress/versions](https://sveltepress.site/reference/versions) When document version management is enabled in `@sveltepress/vite`, `virtual:sveltepress/versions` exposes version manifests, route helpers, release change sets, and the version runtime. Custom themes and components can import this module to build version selectors, lifecycle banners, changelog catalogs, and version-aware navigation links. The Playground [Virtual modules](/playground/virtual-modules/) Entry boots the Kitchen-sink starter where `virtual:sveltepress/site`, `locale`, and `versions` are all populated at `src/routes/reference/virtual-modules/+page.md`. Chinese and Bengali embed `kitchen-sink-zh` / `kitchen-sink-bn` at that same path. That page writes Svelte in markdown and renders them with the same `JsonViewer` as this page. This docs page keeps svelte live so it can show the source next to the rendered result. Open in Playground on this leaf lands there. ## Live data Here is an interactive inspection of the version runtime data on this site: ```svelte live <script> import { JsonViewer } from 'svelte-json-discovery' import versions from 'virtual:sveltepress/versions' </script> <div class="viewer"> <JsonViewer data={versions} /> </div> <style> .viewer { max-height: 40vh; overflow: auto; } :global(html.dark) .viewer { --discovery-background-color: #1a1a1a; --sjd-app-bg: #1a1a1a; --sjd-fmt-color: #999; --sjd-fmt-hover-color: #aaa; --sjd-fmt-property-color: #d17a8c; --sjd-fmt-number-color: #0f8dc2; --sjd-fmt-atom-color: #0f8dc2; --sjd-fmt-string-color: #7faf20; --sjd-fmt-string-underline-color: #85ab51; --sjd-fmt-string-hover-color: #97cf26; --sjd-ui-color: #ccc; --sjd-match-bg: #565638; --sjd-match-border: #a7a73b; --sjd-error-border: #0004; --sjd-error-bg: #622b29; --sjd-error-color: #c66; --sjd-error-message-bg: #443232; --sjd-toggle-color: #72b372; --sjd-touch-button-color: #aaa; --sjd-touch-button-bg: #50505080; --sjd-popup-bg: #333; --sjd-popup-color: #ccc; --sjd-popup-notes-color: #999; --sjd-popup-error-color: #e66; color-scheme: dark; } </style> ``` ## Module exports ### `manifest` * **Type**: `VersionManifest | null` The version manifest for the default locale (`'/'`). When document versioning is not enabled or no manifest exists, this is `null`. ### `manifests` * **Type**: `Record<string, VersionManifest | null>` A dictionary of version manifests keyed by locale prefix (e.g. `'/'`, `'/zh/'`, `'/bn/'`). On a single-locale site, it contains `{ '/': manifest }`. ### `changeSets` * **Type**: `Record<string, VersionChangeSet>` A dictionary of release change sets keyed by version ID. Each entry describes `newPages` and `updatedPages` relative to its baseline historical version. ### `resolveVersionManifest` * **Type**: `(pathname: string) => VersionManifest | null` Resolves the matching `VersionManifest` for the given route pathname based on its locale prefix. ### `resolveVersionChanges` * **Type**: `(versionId?: string, pathname?: string) => VersionChangeSet | null` Returns the change set for the specified `versionId`. When omitted, it resolves changes for the current version. The optional `pathname` selects the locale's own manifest on multi-locale sites. ### `resolveVersionContext` * **Type**: `(pathname: string) => VersionContext | null` Resolves the `VersionContext` for a URL pathname, containing: * `versionId`: The active version ID (e.g. current or a historical version ID). * `version`: The matching `DocumentationVersion` configuration object. * `logicalPath`: The normalized route path stripped of version and locale prefixes. * `historical`: `boolean` indicating whether the route is a historical version snapshot. * `basePath`: The version base path (such as `'/v'` or `'/zh/v'`). * `manifest`: The resolved `VersionManifest` instance. ### `resolveVersionedPath` * **Type**: `(to: string, context: VersionContext | null) => string` Resolves an internal link target within a historical version context. If the target route existed in that historical version snapshot, it returns the versioned link; otherwise it returns the original unversioned path. ### `resolveVersionSwitch` * **Type**: `(pathname: string, targetVersionId: string) => VersionSwitchTarget | null` Calculates navigation destinations when switching versions from `pathname` to `targetVersionId`. Returns `{ href, fallback }`, where `fallback` is `true` if the route did not exist in the target version and fell back to the version home page. ### `default` * **Type**: `VersionRuntime` The default export is the runtime object itself, bundling all helper functions alongside `manifest`, `manifests`, and `changeSets`. ## Working with TypeScript Include `@sveltepress/vite/types` in your `src/app.d.ts` to get full type tips for `virtual:sveltepress/versions`: ```ts title="/src/app.d.ts" /// <reference types="@sveltepress/vite/types" /> // Your other types ``` --- # [Vite plugin](https://sveltepress.site/reference/vite-plugin) ## Types overview @code(/../vite/src/types.ts) ## Plugin options ### `siteConfig` * `title`: The site's title. Would be `'Untitled site'` if not provided. * `description`: The site's description. Would be `'Build by sveltepress'` if not provided. ### `addInspect` If set to `true`, will add [Vite plugin inspect](https://github.com/antfu/vite-plugin-inspect). It is useful to inspect or observe the Vite pipeline. ### `theme` See [`ResolvedTheme`](#ResolvedTheme) below ### `remarkPlugins` The remark plugins used for markdown parse. Read [Remark plugins](https://github.com/remarkjs/remark#plugins) for more details. :::important[Customize remark/rehype plugins order] The `remarkPlugins` and `rehypePlugins` can be one of these two format: 1. An array of `Plugins`. The plugins provided here will run after theme provide remark plugins. 2. A function that accept the `themeRemarkPlugins` then return an array of `Plugins`, for example: ```ts title="vite.config.ts" import { defaultTheme } from '@sveltepress/theme-default' import { sveltepress } from '@sveltepress/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ sveltepress({ theme: defaultTheme(/* theme options */), remarkPlugins: (themeRemarkPlugins) => { // Add your custom plugin. Feel free to control the final order to apply all the plugins return [ ...themeRemarkPlugins ] } }) ] }) ``` ::: ### `rehypePlugins` The rehype plugins used for html generator. Read [Rehype plugins](https://github.com/rehypejs/rehype#plugins) for more details. ### `llms` Generate machine-readable documentation indexes during production builds. It is disabled by default. ```ts title="vite.config.ts" import { sveltepress } from '@sveltepress/vite' sveltepress({ siteConfig: { title: 'My docs', description: 'Documentation for my project', }, llms: { enabled: true, baseUrl: 'https://docs.example.com', filter: (_filePath, frontmatter) => frontmatter.llms !== false, }, }) ``` | Option | Type | Default | Purpose | |---|---|---|---| | `enabled` | `boolean` | `false` | Write `llms.txt` and `llms-full.txt` during a build. | | `title` | `string` | `siteConfig.title` | Title used in both generated files. | | `description` | `string` | `siteConfig.description` | Description used in both generated files. | | `baseUrl` | `string` | `''` | Absolute site origin prepended to route links. | | `routesDir` | `string` | `'src/routes'` | Directory scanned for pages. | | `filter` | `(filePath, frontmatter) => boolean` | — | Exclude selected pages. | | `sort` | `(a, b) => number` | route path | Customize page order. | The generator reads Markdown pages only; Svelte-only pages and runtime data are not included. With incremental document versions, historical indexes read each page's frozen Markdown artifact rather than current source. Builds write the files into both `static/` and the production bundle so a clean CI build includes them in the deployed output. Decide whether to commit the `static/` copies or ignore and regenerate them consistently in CI. :::since[Version change discovery]{version="2026-08-28" id="version-change-discovery" summary="Build-time change catalogs are available to themes and custom pages."} ### `versions` Document version management is discovered from `sveltepress.versions.json` by default. Disable discovery or choose another manifest path explicitly: ```ts import { sveltepress } from '@sveltepress/vite' sveltepress({ versions: false, }) sveltepress({ versions: { manifest: 'config/document-versions.json' }, }) ``` When enabled, `virtual:sveltepress/versions` exports `changeSets` and `resolveVersionChanges(versionId?)` alongside the manifest and route helpers. See [Document version management](/guide/version-management/) for snapshot and What’s New usage. ::: :::since[Locales, Pagefind, and locale hooks]{version="2026-09-03" id="vite-locales-pagefind-hooks" summary="Document locales, pagefind, virtual:sveltepress/locale, and createLocaleHandle."} ### `locales` Opt-in multi-locale configuration keyed by URL prefix (`'/'`, `'/zh/'`, …). Each entry provides `lang`, `label`, and that locale's theme options. When omitted, the site stays single-locale. See [Internationalization](/guide/i18n/). ### `pagefind` Controls post-build Pagefind indexing for Local Search. Defaults to enabled; set `pagefind: false` to disable, or pass a `PagefindOptions` object (`rootSelector`, `excludeSelectors`, `forceLanguage`, `outputPath`, …). Historical version indexes are frozen with `syncHistoricalPagefind` when document versions are enabled. ### `virtual:sveltepress/locale` When `locales` is set, this virtual module exports `locales`, `resolveLocale`, `resolveLocalizedPath`, and `resolveLocaleSwitch`. Without `locales`, `locales` is `null` and the helpers no-op. ### `createLocaleHandle` Import from `/vite/hooks` to set `<html lang>` during SSR from the active locale. Pair it with `src/hooks.server.js` as shown in the [i18n guide](/guide/i18n/). ::: ## ResolvedTheme ### `name` The name of the theme. ### `globalLayout` The absolute path of the global layout. **Should be a svelte file** For example: `path.resolve(process.cwd(), 'ThemeGlobalLayout.svelte')` ### `pageLayout` The absolute path of the page layout. **Should be a svelte file** For example: `path.resolve(process.cwd(), 'ThemePageLayout.svelte')` ### `vitePlugins` * If passed a plugin or a group of plugins, these plugins would applied in before `sveltepress` * If passed a function, it will accept the `sveltepress` plugin and need to return a group of plugins. You can customize the `sveltepress` plugin order in your returned plugin chain. :::info[About theme vite plugins]{icon=vscode-icons:file-type-vite} It maybe a little strange that theme has vite plugins. But it is useful when the theme want's to add some [virtual modules](https://vitejs.dev/guide/api-plugin.html#virtual-modules-convention) or write some temp files. ::: ### `highlighter` Used for code highlighting. For example, the default theme use [Shiki](https://github.com/shikijs/shiki). You can check the [default theme highlighter source code](https://github.com/SveltePress/sveltepress/blob/main/packages/theme-default/src/markdown/highlighter.ts) for detailed usage. ### `remarkPlugins` The remark plugins used for Markdown parsing. Read [Remark plugins](https://github.com/remarkjs/remark#plugins) for more details. ### `rehypePlugins` The rehype plugins used for HTML generation. Read [Rehype plugins](https://github.com/rehypejs/rehype#plugins) for more details. :::important[Plugins order]{icon=solar:reorder-outline} The remark and rehype plugins that theme provide would be called before the plugins provide by vite plugin. For example: ```ts title="vite.config.(js|ts)" import { defaultTheme } from '@sveltepress/theme-default' import { sveltepress } from '@sveltepress/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ sveltepress({ theme: defaultTheme(/* theme options */), remarkPlugins: [/* yourRemarkPlugin */] }) ] }) ``` yourRemarkPlugin would run after the remark plugins in defaultTheme ::: ### `footnoteLabel` Customize the footnotes title, default is: `"Footnotes"` ## Virtual modules ### `virtual:sveltepress/site` This module holds the `siteConfig`. For example: ```svelte live <script> import siteConfig from 'virtual:sveltepress/site' </script> <p>The site title is: {siteConfig.title}</p> <p>The site description is: {siteConfig.description}</p> ``` ## Low level API The `@sveltepress/vite` package has a low level function `mdToSvelte`. It is used for all the major Markdown rendering in Sveltepress. It can be used for a more basic Markdown render engine involved with Svelte. Here's usage example: ```ts ln import { mdToSvelte } from '@sveltepress/vite' const mdContent = ` --- title: Foo --- <script> const foo = 'bar' </script> # Title foo in script is: {foo} [Foo Link](https://foo.bar) ` const { code, data } = await mdToSvelte({ mdContent, remarkPlugins: [], // your custom remark plugins rehypePlugins: [], // your custom rehype plugins highlighter: async (code, lang, meta) => Promise.resolve('The rendered highlighted code html'), // your custom code highlighter filename: 'foo.md', // the virtual file path }) // The rendered svelte code code // The frontmatter object, { title: 'Foo' } data ``` :::since[Literal code-block preparation]{version="2026-08-31" id="literal-code-preparation" summary="Preserve code commands and @noErrors while still parsing title and line-number metadata."} ### `@sveltepress/vite/highlight` Import `prepareCodeBlock` from `@sveltepress/vite/highlight` and pass `{ mode: 'literal' }` as its third argument when the source must not be transformed. Code commands such as `// [svp! hl]` and a leading `// @noErrors` remain unchanged in `processedCode`, while metadata parsing still resolves `title` and `ln`. ```ts import { prepareCodeBlock } from '@sveltepress/vite/highlight' const prepared = prepareCodeBlock( '// @noErrors\nconst value = 1 // [svp! hl]', 'title="source.md" ln', { mode: 'literal' }, ) ``` Here `prepared.processedCode` equals the input, `prepared.title` is `'source.md'`, and `prepared.containLineNumbers` is `true`. `prepared.noErrors` remains `false` because literal mode preserves `@noErrors` instead of processing it as a directive. ::: ## Working with TypeScript You need to include `@sveltepress/vite/types` in your `src/app.d.ts` to get plugin options and virtual module's type tips ```ts title="/src/app.d.ts" /// <reference types="@sveltepress/vite/types" /> // Your other types ``` --- ## whats-new # [What's new](https://sveltepress.site/whats-new) <script> import VersionChanges from '@sveltepress/theme-default/VersionChanges.svelte' </script> <VersionChanges />