From 4da4f76875f548c2686e8e0e2286fd04d4cc1377 Mon Sep 17 00:00:00 2001 From: Flannel007 Date: Sun, 12 Jul 2026 09:56:27 -0400 Subject: [PATCH] init --- .gitignore | 1 + README.md | 2 + content/changelog/v0-1-0.mdx | 20 ++++++ content/changelog/v0-2-0.mdx | 29 ++++++++ content/configuration/configuration.mdx | 2 +- content/getting-started/introduction.mdx | 2 +- content/guides/changelog.mdx | 49 ++++++++++++++ e2e/prerender.spec.ts | 44 ++++++++++++ src/App.tsx | 4 ++ src/hooks/useDocs.ts | 1 + src/lib/markdown.ts | 12 ++++ src/navigation.ts | 5 ++ src/pages/docs/ChangelogIndex.tsx | 71 ++++++++++++++++++++ src/pages/docs/DocPage.tsx | 17 ++++- src/pages/docs/DocsIndex.tsx | 4 +- vite-plugins/page-html.ts | 1 + vite-plugins/prerender.ts | 20 ++++++ vite-plugins/search-index.ts | 85 +++++++++++++++++++++--- vite.config.ts | 2 + 19 files changed, 356 insertions(+), 15 deletions(-) create mode 100644 content/changelog/v0-1-0.mdx create mode 100644 content/changelog/v0-2-0.mdx create mode 100644 content/guides/changelog.mdx create mode 100644 src/pages/docs/ChangelogIndex.tsx diff --git a/.gitignore b/.gitignore index dbda677..df0be64 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ public/docs-manifest.json public/sitemap.xml public/llms.txt public/llms-full.txt +public/feed.xml public/md/ # Playwright diff --git a/README.md b/README.md index cdb5b63..5cb4699 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ React + Tamagui app you fully own and can edit. - **AI-native** — generates `llms.txt`, per-page "Copy as Markdown", and "Open in ChatGPT/Claude". - **Edit this page** — every doc links to its source on GitHub (menu + below the article), so readers can send fixes. +- **Changelog** — dated release notes in `content/changelog/` get their own + tab, a newest-first listing, and an RSS feed. - **Authoring components** — callouts, tabbed code samples, API method badges, see-also cards. - **Multi-deploy** — env-driven, so one codebase powers many branded docs sites. - **Public by default** — auth is opt-in; a fresh clone runs with zero config. diff --git a/content/changelog/v0-1-0.mdx b/content/changelog/v0-1-0.mdx new file mode 100644 index 0000000..676c6e1 --- /dev/null +++ b/content/changelog/v0-1-0.mdx @@ -0,0 +1,20 @@ +--- +title: v0.1.0 — Initial release +date: "2026-06-15" +description: The first release of Capsa — a self-hosted documentation platform you own end to end. +--- + +# v0.1.0 — Initial release + +The first public release of Capsa. + +## Added + +- MDX content pipeline with callouts, tabbed code samples, API method badges, + and see-also cards. +- ⌘K command palette over a build-time search index. +- Interactive OpenAPI reference powered by Scalar. +- Four theme styles (steel, aurora, retro, shadcn) × light/dark, token-driven. +- AI-native exports: `llms.txt`, per-page "Copy as Markdown", open-in-ChatGPT/Claude. +- Optional WorkOS authentication for private docs. +- One-command deploy to Cloudflare Pages (or any static host). diff --git a/content/changelog/v0-2-0.mdx b/content/changelog/v0-2-0.mdx new file mode 100644 index 0000000..fe8a2f0 --- /dev/null +++ b/content/changelog/v0-2-0.mdx @@ -0,0 +1,29 @@ +--- +title: v0.2.0 — Prerendering, search, and a front door +date: "2026-07-10" +description: Full static prerendering, Pagefind search, social cards, a landing page, and edit-this-page links. +--- + +# v0.2.0 — Prerendering, search, and a front door + +The release that makes Capsa sites fully static-first. + +## Added + +- **Full prerendering (SSG)** — every doc ships as real HTML with the complete + article and crawlable nav links, then hydrates into the SPA. `PRERENDER=0` + opts out. +- **Pagefind search** — the ⌘K palette now runs on a typo-tolerant static + index built from the prerendered pages; no search service required. +- **Social cards** — a branded `og:image` PNG is generated per page at build + time. +- **Landing page** — drop in `content/home.mdx` and `/` becomes a real home + page with hero, feature-grid, and CTA components. +- **Edit this page** — every doc links to its source on GitHub. + +## Fixed + +- Body text rendered semi-bold site-wide due to size-linked font weights; all + body sizes now default to weight 400. +- Theme styles now apply their full semantic token sets (accents, sidebar + colors, radii) in both JS and CSS. diff --git a/content/configuration/configuration.mdx b/content/configuration/configuration.mdx index 17a297c..3ab9460 100644 --- a/content/configuration/configuration.mdx +++ b/content/configuration/configuration.mdx @@ -18,7 +18,7 @@ All are optional. A bare deploy needs none of them. | `VITE_SITE_NAME` | Brand shown in the top bar, titles, footer. Defaults to `Capsa`. | | `VITE_SITE_ORG` | Footer copyright name. Defaults to `VITE_SITE_NAME`. | | `VITE_DEFAULT_THEME_STYLE` | Pin a theme (`steel`, `aurora`, `retro`, `shadcn`) and hide the switcher. | -| `VITE_SITE_URL` | Base URL — enables `sitemap.xml`, canonical + `og:image` tags, absolute links in `llms.txt`. | +| `VITE_SITE_URL` | Base URL — enables `sitemap.xml`, canonical + `og:image` tags, the changelog `feed.xml`, absolute links in `llms.txt`. | | `VITE_PRODUCT` | Scope nav + search to one product folder (multi-product setups). | | `VITE_GITHUB_URL` / `VITE_STATUS_URL` / `VITE_SUPPORT_URL` | Footer links — each renders only if set. `VITE_GITHUB_URL` also powers the "Edit this page" links. | | `VITE_GITHUB_BRANCH` | Branch the "Edit this page" links target. Defaults to `main`. | diff --git a/content/getting-started/introduction.mdx b/content/getting-started/introduction.mdx index a645abc..d7779c8 100644 --- a/content/getting-started/introduction.mdx +++ b/content/getting-started/introduction.mdx @@ -24,7 +24,7 @@ edit. - **MDX content** — write Markdown, drop in React components when you need them. - **⌘K command palette** — instant, keyboard-first search over a build-time index. - **OpenAPI reference** — a full interactive API explorer (Scalar) from your spec. -- **Theming** — five built-in styles × light/dark, all token-driven and brandable. +- **Theming** — four built-in styles × light/dark, all token-driven and brandable. - **AI-native** — generates `llms.txt`, a per-page "Copy as Markdown", and "Open in ChatGPT/Claude". - **Authoring components** — callouts, tabbed code samples, API method badges, "see also" cards. - **Public by default** — auth is opt-in; a fresh clone runs with no configuration. diff --git a/content/guides/changelog.mdx b/content/guides/changelog.mdx new file mode 100644 index 0000000..44f5c86 --- /dev/null +++ b/content/guides/changelog.mdx @@ -0,0 +1,49 @@ +--- +title: Changelog +description: Publish dated release notes with a listing page and an RSS feed. +order: 6 +--- + +# Changelog + +Add dated MDX files under `content/changelog/` and Capsa gives you a +**Changelog tab**, a newest-first listing at `/docs/changelog`, per-entry pages, +and an RSS feed — no configuration. + +## Writing an entry + +```mdx title="content/changelog/v1-2-0.mdx" +--- +title: v1.2.0 — Webhooks +date: "2026-08-01" +description: Outbound webhooks, retries, and a new signing scheme. +--- + +# v1.2.0 — Webhooks + +## Added + +- Outbound webhooks with automatic retries. +``` + +- `date` (ISO `YYYY-MM-DD`, quoted) drives the listing order and shows next to + the breadcrumb on the entry page. +- `title` and `description` appear on the listing card, in search, and in the + RSS feed. +- Entries are otherwise **normal docs**: searchable, prerendered, social-carded, + with edit-on-GitHub links — everything a page under `content/` gets. + +## How it behaves + +- Entries never appear in the Documentation sidebar — the changelog is its own + tab (an `href` tab in `src/navigation.ts`, like the API Reference). +- The listing page at `/docs/changelog` sorts entries newest-first by `date`. +- Remove the `content/changelog/` folder and the tab simply 404s — delete the + tab entry in `src/navigation.ts` to hide it entirely. + +## RSS + +On deploys with `VITE_SITE_URL` set, the build emits `feed.xml` (RSS 2.0) with +every dated entry, and the site `` advertises it via +``. Readers and release-watchers can subscribe to +`https://your-site/feed.xml`. diff --git a/e2e/prerender.spec.ts b/e2e/prerender.spec.ts index 232b60c..6061bce 100644 --- a/e2e/prerender.spec.ts +++ b/e2e/prerender.spec.ts @@ -208,6 +208,50 @@ test('docs pages link to GitHub editing and back home', async ({ page, request } await expect(page.locator('#root h1')).toContainText('Documentation that ships itself'); }); +test('changelog listing is prerendered newest-first', async ({ request }) => { + const html = await (await request.get('/docs/changelog')).text(); + expect(html).toContain('Changelog — Capsa'); + const root = html.split('
')[1] ?? ''; + const v2 = root.indexOf('v0.2.0'); + const v1 = root.indexOf('v0.1.0'); + expect(v2).toBeGreaterThan(-1); + expect(v1).toBeGreaterThan(-1); + expect(v2, 'newest entry listed first').toBeLessThan(v1); + expect(root).toContain('Jul 10, 2026'); // TZ-free formatted date +}); + +test('changelog entry renders as a doc with its date and hydrates', async ({ page, request }) => { + const html = await (await request.get('/docs/changelog/v0-2-0')).text(); + const root = html.split('
')[1] ?? ''; + expect(root).toContain('Jul 10, 2026'); // breadcrumb date + expect(root).toContain('Pagefind search'); // article body prerendered + await expectHydrated(page, '/docs/changelog/v0-2-0/'); +}); + +test('changelog stays out of the Documentation sidebar and docs index', async ({ request }) => { + const doc = await (await request.get('/docs/guides/theming')).text(); + const sidebar = doc.split('
')[1] ?? ''; + expect(sidebar).not.toContain('v0.1.0'); + const index = await (await request.get('/docs/')).text(); + const indexRoot = index.split('
')[1] ?? ''; + // No changelog CATEGORY CARD on the docs index (cards render

labels; + // the TopNav tab and the "Changelog" guide's sidebar links are spans). + expect(indexRoot).not.toMatch(/]*>Changelog]*>Guides { + await page.goto('/docs/'); + const hits = await page.evaluate(async () => { + const pf = await import(/* @vite-ignore */ '/pagefind/pagefind.js'); + await pf.init?.(); + const { results } = await pf.search('front door'); + const pages = await Promise.all(results.map((r: { data: () => Promise<{ url: string }> }) => r.data())); + return pages.map((p) => p.url); + }); + expect(hits).toContain('/docs/changelog/v0-2-0/'); +}); + test('Scalar route client-renders via the marker-mismatch fallback', async ({ page, request }) => { // /docs/api is not prerendered; static hosts serve the landing-page HTML as // the SPA fallback. The route marker ("/") must not match, forcing a clean diff --git a/src/App.tsx b/src/App.tsx index 67b3ccd..f459f46 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,7 @@ import LoginPage from './pages/authentication/LoginPage'; import HomePage from './pages/HomePage'; import DocPage from './pages/docs/DocPage'; import DocsIndex from './pages/docs/DocsIndex'; +import ChangelogIndex from './pages/docs/ChangelogIndex'; import { HAS_HOME } from './hooks/useDocs'; // Lazy so Scalar (heavy) is split out of the main bundle. @@ -51,6 +52,9 @@ const App: FC = () => { ), }, + // Exact segment only — /docs/changelog/ still hits the splat + // below and renders as a normal doc. + { path: 'changelog', element: }, { path: '*', element: }, ], }, diff --git a/src/hooks/useDocs.ts b/src/hooks/useDocs.ts index 523df96..7de409c 100644 --- a/src/hooks/useDocs.ts +++ b/src/hooks/useDocs.ts @@ -33,6 +33,7 @@ export interface ManifestEntry { description: string; order: number; product: string; + date?: string; // ISO YYYY-MM-DD — set on changelog entries } // Optional per-product scope (4.1): when set, only that product's docs show. diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts index 5fe8207..a2bc79c 100644 --- a/src/lib/markdown.ts +++ b/src/lib/markdown.ts @@ -25,3 +25,15 @@ export interface TocEntry { text: string; level: number; } + +// Format an ISO date string (YYYY-MM-DD) as "Jul 10, 2026" WITHOUT going +// through Date/toLocaleDateString: UTC-midnight parsing shifts a day across +// timezones, and prerendered pages must render the identical string during +// hydration on any client. Pure string math is deterministic everywhere. +const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; +export function formatDate(iso: string): string { + const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})/); + if (!m) return iso; + const month = MONTHS[Number(m[2]) - 1] ?? m[2]; + return `${month} ${Number(m[3])}, ${m[1]}`; +} diff --git a/src/navigation.ts b/src/navigation.ts index 6fa5073..e664b01 100644 --- a/src/navigation.ts +++ b/src/navigation.ts @@ -47,5 +47,10 @@ export const navigation: NavConfig = { icon: 'code', href: '/docs/api', }, + { + tab: 'Changelog', + icon: 'file', + href: '/docs/changelog', + }, ], }; diff --git a/src/pages/docs/ChangelogIndex.tsx b/src/pages/docs/ChangelogIndex.tsx new file mode 100644 index 0000000..db96aa5 --- /dev/null +++ b/src/pages/docs/ChangelogIndex.tsx @@ -0,0 +1,71 @@ +import { useNavigate } from 'react-router-dom'; +import { Helmet } from 'react-helmet-async'; +import { YStack, H1, H3, Paragraph, Text } from 'tamagui'; + +import { useDocsList } from '../../hooks/useDocs'; +import { anchorNavProps } from '../../lib/navLink'; +import { formatDate } from '../../lib/markdown'; +import { SITE_NAME } from '../../lib/site'; + +// Newest-first listing of content/changelog/ entries. Entries themselves are +// ordinary docs (rendered by DocPage via the * splat); this page just lists +// them by their `date` frontmatter. +export default function ChangelogIndex() { + const { docs } = useDocsList(); + const navigate = useNavigate(); + + const entries = docs + .filter((d) => d.category === 'changelog') + .sort((a, b) => (b.date ?? '').localeCompare(a.date ?? '')); + + return ( + + + {`Changelog — ${SITE_NAME}`} + + + +

Changelog

+ + Release notes and updates for {SITE_NAME}. + +
+ + + {entries.map((entry) => ( + navigate(`/docs/${entry.slug}`))} + padding="$4" + gap="$1.5" + borderWidth={1} + borderColor="$borderColor" + borderRadius="$lg" + cursor="pointer" + hoverStyle={{ borderColor: '$accentBorder' }} + > + {entry.date && ( + + {formatDate(entry.date)} + + )} +

+ {entry.title} +

+ {entry.description && ( + + {entry.description} + + )} +
+ ))} + {entries.length === 0 && ( + + No entries yet. Add dated MDX files under content/changelog/. + + )} +
+
+
+ ); +} diff --git a/src/pages/docs/DocPage.tsx b/src/pages/docs/DocPage.tsx index 1c10be7..2476ed6 100644 --- a/src/pages/docs/DocPage.tsx +++ b/src/pages/docs/DocPage.tsx @@ -8,6 +8,7 @@ import { ArrowLeft, ArrowRight, PencilSimple } from '@phosphor-icons/react'; import { useDoc, useDocMeta, normalizeSlug } from '../../hooks/useDocs'; import { anchorNavProps } from '../../lib/navLink'; import { useNavigation } from '../../hooks/useNavigation'; +import { formatDate } from '../../lib/markdown'; import type { TocEntry } from '../../lib/markdown'; import mdxComponents from '../../components/markdown/mdxComponents'; import TableOfContents from '../../components/layout/TableOfContents'; @@ -81,12 +82,16 @@ export default function DocPage() { const prev = idx > 0 ? orderedPages[idx - 1] : undefined; const next = idx >= 0 && idx < orderedPages.length - 1 ? orderedPages[idx + 1] : undefined; + // Frontmatter title (via the manifest) beats the filename-derived one — + // e.g. "v0.2.0 — …" instead of "V0 2 0" for changelog entries. + const pageTitle = meta?.title ?? doc.title; + return ( - {`${doc.title} — ${SITE_NAME}`} + {`${pageTitle} — ${SITE_NAME}`} {meta?.description && } - + {meta?.description && } @@ -105,8 +110,14 @@ export default function DocPage() { / - {doc.title} + {pageTitle} + {/* Dated docs (changelog entries) show their date inline. */} + {meta?.date && ( + + · {formatDate(meta.date)} + + )} diff --git a/src/pages/docs/DocsIndex.tsx b/src/pages/docs/DocsIndex.tsx index 5c92ecd..ada7aa5 100644 --- a/src/pages/docs/DocsIndex.tsx +++ b/src/pages/docs/DocsIndex.tsx @@ -5,8 +5,10 @@ import { useDocsList } from '../../hooks/useDocs'; import { SITE_NAME } from '../../lib/site'; export default function DocsIndex() { - const { navTree } = useDocsList(); + const { navTree: fullTree } = useDocsList(); const navigate = useNavigate(); + // Changelog entries have their own tab + listing — no category card here. + const navTree = fullTree.filter((category) => category.name !== 'changelog'); return ( diff --git a/vite-plugins/page-html.ts b/vite-plugins/page-html.ts index d5374c3..efe37ef 100644 --- a/vite-plugins/page-html.ts +++ b/vite-plugins/page-html.ts @@ -9,6 +9,7 @@ export interface ManifestEntry { description: string; order: number; product: string; // top-level folder unless overridden in frontmatter + date?: string; // ISO YYYY-MM-DD — set on changelog entries } export const escapeHtml = (s: string) => diff --git a/vite-plugins/prerender.ts b/vite-plugins/prerender.ts index 6d18ada..cb7b5fa 100644 --- a/vite-plugins/prerender.ts +++ b/vite-plugins/prerender.ts @@ -147,6 +147,26 @@ export function prerenderPlugin(options: PrerenderOptions): Plugin { dirDest: join(outDir, 'docs', 'index.html'), ogRel: 'assets/og/docs-index.png', }, + // Changelog listing — a synthetic entry rides pageHtml() so the route + // gets a real title/description/canonical and a "Changelog" OG card. + ...(entries.some((e) => e.category === 'changelog') + ? [ + { + url: '/docs/changelog', + entry: { + slug: 'changelog', + title: 'Changelog', + description: `Release notes and updates for ${siteName}.`, + category: 'changelog', + order: 0, + product: '', + } satisfies ManifestEntry, + dirDest: join(outDir, 'docs', 'changelog', 'index.html'), + flatDest: join(outDir, 'docs', 'changelog.html'), + ogRel: 'assets/og/changelog.png', + }, + ] + : []), ...entries.map((e) => ({ url: `/docs/${e.slug}`, entry: e, diff --git a/vite-plugins/search-index.ts b/vite-plugins/search-index.ts index 92621de..8b3ee9f 100644 --- a/vite-plugins/search-index.ts +++ b/vite-plugins/search-index.ts @@ -47,6 +47,7 @@ interface Options { llmsFile?: string; // llms.txt (curated index for AI agents) llmsFullFile?: string; // llms-full.txt (concatenated content) pagesDir?: string; // per-page raw markdown for the "Copy page" action + rssFile?: string; // feed.xml — RSS of dated changelog entries (public deploys) siteTitle?: string; } @@ -119,6 +120,15 @@ function buildEntry(contentDir: string, file: string): SearchEntry { const description = typeof fm.description === 'string' ? fm.description : extractExcerpt(content); + // gray-matter parses unquoted YAML dates into Date objects — normalize both + // forms to ISO YYYY-MM-DD. + const date = + typeof fm.date === 'string' + ? fm.date + : fm.date instanceof Date + ? fm.date.toISOString().slice(0, 10) + : undefined; + return { slug, title: typeof fm.title === 'string' ? fm.title : titleFromFilename(filename), @@ -126,6 +136,7 @@ function buildEntry(contentDir: string, file: string): SearchEntry { description, order: typeof fm.order === 'number' ? fm.order : Number.MAX_SAFE_INTEGER, product: typeof fm.product === 'string' ? fm.product : category, + date, hidden: fm.hidden === true, headings: extractHeadings(content), excerpt: extractExcerpt(content), @@ -151,6 +162,40 @@ function writeSitemap( writeFileSync(sitemapFile, xml, 'utf-8'); } +// RSS 2.0 feed of dated changelog entries (newest first). Emitted only on +// public deploys (needs absolute URLs, like the sitemap). Whether the last +// generate() emitted a feed is tracked so transformIndexHtml can advertise it. +let rssEmitted = false; +export const hasRss = () => rssEmitted; + +function writeRss(rssFile: string, siteUrl: string, siteTitle: string, entries: SearchEntry[]): void { + const base = siteUrl.replace(/\/$/, ''); + const items = entries + .map( + (e) => + ` \n` + + ` ${escapeHtml(e.title)}\n` + + ` ${base}/docs/${e.slug}\n` + + ` ${base}/docs/${e.slug}\n` + + ` ${new Date(`${e.date}T00:00:00Z`).toUTCString()}\n` + + ` ${escapeHtml(e.description)}\n` + + ` `, + ) + .join('\n'); + const xml = + `\n` + + `\n` + + ` \n` + + ` ${escapeHtml(siteTitle)} Changelog\n` + + ` ${base}/docs/changelog\n` + + ` ${escapeHtml(`Release notes and updates for ${siteTitle}.`)}\n` + + `${items}\n` + + ` \n` + + `\n`; + mkdirSync(dirname(rssFile), { recursive: true }); + writeFileSync(rssFile, xml, 'utf-8'); +} + // llms.txt — a curated, link-first index for AI coding assistants (the emerging // standard shipped by Anthropic, Vercel, etc.). llms-full.txt concatenates the // actual prose for agents that want the whole corpus in one fetch. @@ -210,14 +255,17 @@ function generate(opts: Options): number { writeFileSync(outFile, JSON.stringify(visible), 'utf-8'); // Slim manifest: nav + SEO + sitemap don't need headings/body. - const manifest: ManifestEntry[] = visible.map(({ slug, title, category, description, order, product }) => ({ - slug, - title, - category, - description, - order, - product, - })); + const manifest: ManifestEntry[] = visible.map( + ({ slug, title, category, description, order, product, date }) => ({ + slug, + title, + category, + description, + order, + product, + ...(date ? { date } : {}), + }), + ); mkdirSync(dirname(manifestFile), { recursive: true }); writeFileSync(manifestFile, JSON.stringify(manifest), 'utf-8'); @@ -232,6 +280,17 @@ function generate(opts: Options): number { ); } + rssEmitted = false; + if (opts.rssFile && siteUrl) { + const changelog = visible + .filter((e) => e.category === 'changelog' && e.date) + .sort((a, b) => (b.date ?? '').localeCompare(a.date ?? '')); + if (changelog.length > 0) { + writeRss(opts.rssFile, siteUrl, opts.siteTitle ?? 'Documentation', changelog); + rssEmitted = true; + } + } + writeLlms(opts, visible); // Per-page raw markdown (frontmatter stripped) for the "Copy page" / "View as @@ -271,10 +330,18 @@ export function searchIndexPlugin(options: Options): Plugin { const siteName = env.VITE_SITE_NAME || options.siteTitle || 'Capsa'; const description = env.VITE_SITE_DESCRIPTION || `${siteName} documentation — guides and API reference.`; - return html + let out = html .replaceAll('__CAPSA_SITE_NAME__', escapeHtml(siteName)) .replaceAll('__CAPSA_SITE_DESCRIPTION__', escapeHtml(description)) .replaceAll('__CAPSA_PINNED_THEME__', env.VITE_DEFAULT_THEME_STYLE || ''); + // Advertise the changelog feed when generate() emitted one this build. + if (rssEmitted) { + out = out.replace( + '', + ` \n `, + ); + } + return out; }, // After the bundle is on disk, stamp out one HTML file per doc route so // link unfurls and search snippets see real per-page metadata. diff --git a/vite.config.ts b/vite.config.ts index c2e86c5..9a5ef99 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -38,6 +38,8 @@ export default defineConfig(({ isSsrBuild }) => ({ llmsFullFile: path.resolve(__dirname, './public/llms-full.txt'), // Per-page raw markdown for the "Copy page" action (served at /md/.md). pagesDir: path.resolve(__dirname, './public/md'), + // RSS feed of dated changelog entries — emitted only with a public base URL. + rssFile: path.resolve(__dirname, './public/feed.xml'), siteTitle: process.env.VITE_SITE_NAME || 'Capsa', }), // MDX must run before the React plugin so its JSX output is transformed.