# tidypress > A publishing framework for Git-native authorship. - [Home](https://tidypress.pages.dev/) ## docs ### [Advanced configuration](https://tidypress.pages.dev/docs/advanced-configuration) Optional settings for i18n, versions, analytics, capabilities, and content snapshots. Most sites do not need this page on day one. Add these settings when the default writing-and-work layout stops being enough. Path trees below use the publish root `site/` from `init`. `src/content/docs/` is the **`docs` collection**, not the whole publish root. ## i18n Configure locale-prefixed routes and UI labels: ```ts import { defineConfig } from 'tidypress/config' export default defineConfig({ name: 'my-project', i18n: { defaultLocale: 'en', locales: ['en', 'fr'], }, }) ``` Routes: ```txt /docs/getting-started /fr/docs/getting-started /writing /fr/writing ``` Default-locale content stays at the root path. Non-default locales use a locale prefix. ```txt site/ └── src/content/ ├── writing/ # dated posts (RSS, tags, archive) │ ├── hello.md │ └── fr/hello.md └── docs/ # docs collection — sidebar-ordered guides at /docs/… ├── getting-started.md └── fr/getting-started.md ``` If a non-default locale is missing a page, TidyPress falls back to the default-locale content for that route. ## UI strings ```ts i18n: { defaultLocale: 'en', locales: ['en', 'fr'], strings: { fr: { searchLabel: 'rechercher', searchPlaceholder: 'Rechercher...', docsTitle: 'Docs', writingTitle: 'Articles', onThisPageLabel: 'Sur cette page', }, }, } ``` ## Versions Use folder-based docs versions when you need old docs to stay online: ```ts export default defineConfig({ name: 'my-project', versions: [ { label: 'v2 (latest)', path: '/docs' }, { label: 'v1', path: '/docs/v1' }, ], }) ``` Content for the **`docs` collection** only — not the whole publish root: ```txt site/src/content/docs/ ├── getting-started.md ├── configuration.md └── v1/ ├── getting-started.md └── configuration.md ``` Scaffold a version: ```bash tidypress add-version 2.0 tidypress add-version 2.0 --set-latest ``` Configured versions appear in the docs right rail above the table of contents. ## Analytics Analytics are off by default: ```ts analytics: { type: 'none' } ``` Supported adapters: | Type | Notes | |------|-------| | `none` | default; no analytics script | | `plausible` | Plausible-style script injection | | `fathom` | Fathom-style script injection | | `umami` | Umami script | Example: ```ts analytics: { type: 'plausible', endpoint: 'https://plausible.io/js/script.js', siteId: 'docs.example.com', } ``` TidyPress renders the script tag with the configured endpoint and site id. ## Capabilities Capabilities guard optional and experimental surfaces. Defaults: ```txt docs writing pages theming ``` Off by default: ```txt themingCustom editor export ai ``` Enable or disable capabilities: ```ts capabilities: { disable: ['pages'], enable: ['themingCustom'], }, ``` Writing-only sites often disable `docs` and `pages` (as the `blog` init preset does) so `/docs` and empty `/pages/` routes are not built: ```ts capabilities: { disable: ['docs', 'pages'], }, collections: { docs: { enabled: false, basePath: '/docs' }, writing: { enabled: true, basePath: '/writing', kind: 'writing' }, pages: { enabled: false, kind: 'page' }, }, ``` When `docs` is disabled, the search modal placeholder defaults to **Search writing...** instead of **Search docs...**. Override with `i18n.strings` if needed. Experimental commands require config and CLI opt-ins: ```ts experimental: { ai: true, } ``` ```bash tidypress ai suggest docs/draft.md --enable-experimental-ai ``` These surfaces are guarded on purpose. They are not part of the default docs and writing workflow. ## LLM export (`llms.txt`) By default, every `tidypress build` writes `build/llms.txt` at the site root of the static output. It lists published pages with public URLs and includes the **full markdown body** of each entry, not just titles and excerpts. The export skips disabled collections, drafts (`published: false`), future-scheduled entries, and missing content folders. Enabled custom collections are included. Agents and tools can fetch `https://yoursite.example/llms.txt` after deploy, or read `site/build/llms.txt` locally after build. **Turn it off** when you do not want that file in `build/`: ```ts capabilities: { disable: ['llmsTxt'], }, ``` One-off skip for a single build: ```bash tidypress build --no-llms-txt ``` --- ### [How TidyPress works](https://tidypress.pages.dev/docs/architecture) How the CLI, config package, Astro engine, build output, and cache fit together. TidyPress is split into three packages and one optional Python wrapper. ```txt packages/ ├── cli/ # commands, project setup, builds, deploy helpers ├── config/ # typed config, defaults, validation, normalization └── engine/ # Astro runtime, layouts, routing, components, search UI wrappers/ └── python/ # Python entrypoint and Python-native helpers ``` ## Build flow When you run `tidypress dev` or `tidypress build`, the CLI resolves your **publish root** and runs the pinned `@tidypress/engine` package. Your markdown stays in place; only a static artifact and a local cache are created. `init` scaffolds `site/` by default — the conventional publish root for config, content, and `build/`. In a monorepo, config may sit at the project root instead, as in `apps/site/`. The CLI resolves whichever folder contains `tidypress.config.ts`. ```txt site/ ├── tidypress.config.ts ├── src/content/ │ ├── writing/ # dated posts (RSS, tags, archive) │ ├── projects/ # cards + optional pages (lab preset) │ └── docs/ # docs collection — sidebar-ordered guides at /docs/… ├── public/ └── build/ ``` Steps: 1. The CLI resolves the publish root and validates config. 2. Plugin manifest codegen writes to the cache directory. 3. Astro runs from `node_modules/@tidypress/engine` with `TIDYPRESS_PROJECT_ROOT` pointing at your project. 4. Static HTML is written to `build/`. 5. Pagefind indexes `build/`. 6. The CLI writes `build/llms.txt` with full published markdown for agents. Disable with `capabilities.disable: ['llmsTxt']` or `tidypress build --no-llms-txt`. Power users can add `@tidypress/astro` and `astro.config.mjs` via `tidypress init --with-astro`. ## CLI package `packages/cli` owns commands: - `init` - `dev` - `build` - `preview` - `clean` - `deploy` - `migrate-sections` - `add-version` - `domain` - `import` - guarded experimental commands: `editor`, `export`, `ai` The default command is `dev`. Ports default to `4321`. Deploy commands work from static output in `build/`. ## Config package `packages/config` defines `defineConfig()`, defaults, capabilities, collections, nav policy, theme tokens, i18n strings, versions, analytics, and legacy migration from `sections` to `collections`. Only `name` is required. Defaults enable starter collections: ```txt docs -> /docs writing -> /writing pages -> root-level custom pages ``` `collections` is the current section model. `sections` is the legacy shim. ## Engine package `packages/engine` is an Astro project published as an npm package. It owns: - layouts - routing strategies - collection views - MDX components - sidebar and table-of-contents UI - theme CSS - Pagefind search UI - sitemap and metadata output Bundled client assets land under `assets/` in `build/`. ## Content model Under the publish root, `src/content/` holds **collections**—typed folders configured in `tidypress.config.ts`. **Writing** — dated posts with RSS, the default public voice: ```txt site/src/content/writing/ ``` **Projects / works** — home-page work, `kind: 'projects'`: ```txt site/src/content/projects/ site/src/content/works/ ``` **Docs collection** — sidebar-ordered pages at `/docs/…`: ```txt site/src/content/docs/ ``` Forms: `doc` for reference pages, `manual` for procedural guides. Other collection keys use the kinds in config and [Conventions](./conventions). ## Search Search is powered by [Pagefind](https://pagefind.app/). `tidypress build` renders HTML, then builds the Pagefind index inside `build/`. ## Python wrapper The Python package delegates site commands to the Node.js CLI. ## Extension points Advanced projects can extend rendering with: - `collections..render` - `extensions.docForms` - project-local presentation modules - optional Astro view files (resolved via `@project/`) - `@tidypress/astro` integration for explicit Astro projects See [CI and deployment](/docs/manual/ci) for caching and upload guidance. --- ### [Body of work](https://tidypress.pages.dev/docs/body-of-work) Works, projects, writing, reference, process, and pages — product docs use the docs collection separately. TidyPress models a public body of work with collection **keys** and built-in **kinds**. The `body-of-work` preset is generated from `publicationSurfaceDefinitions` in `@tidypress/config` — six surfaces, not product docs. ## Quick start ```bash npx tidypress init --preset body-of-work --site-url https://yoursite.example ``` For work **and** product documentation in one site: ```bash npx tidypress init --preset body-of-work-docs --site-url https://yoursite.example ``` | Key | Kind | Purpose | |-----|------|---------| | `works` | `projects` | Case studies, major artifacts | | `projects` | `projects` | Repos, apps, tools, experiments | | `writing` | `writing` | Essays, technical notes | | `reference` | `content` | API, config, CLI reference in the docs sidebar shell | | `process` | `content` | ADRs, decisions, build logs | | `pages` | `page` | About, now, contact, uses, resume | The homepage shows **works**, **projects**, and **writing**. **Reference** and **process** link from the **footer**, not the header, so mobile nav stays readable. Add them to `home.order` to surface them on the home page. ## Product docs The **`docs`** collection is sidebar-ordered product documentation — tutorials, how-tos, and guides. The default `body-of-work` preset disables it; use **`body-of-work-docs`** or **`docs-writing`** to enable it from init. ```bash npx tidypress init --preset body-of-work-docs ``` Or enable `collections.docs` later and add content under `src/content/docs/`. ## siteUrl Set **`siteUrl`** in `tidypress.config.ts` before deploy. Until then, canonical URLs, Open Graph, RSS, and the sitemap use the placeholder host. `tidypress doctor` and `tidypress build` remind you when it is still unset. ## Reference vs docs | Surface | Use for | |---------|---------| | `docs` | Teaching and product documentation | | `reference` | Facts: API, config, CLI | | `process` | Decisions: ADRs, roadmaps | ## Featured on the home page Set `featured: true` in frontmatter for writing and projects-kind collections (including a `works` key). ## Related - [Configuration](./configuration) — presets - [Examples](./examples) - [Advanced configuration](./advanced-configuration) — `build/llms.txt` for agents --- ### [Components](https://tidypress.pages.dev/docs/components) Built-in MDX components for notes, tabs, file trees, diagrams, images, tooltips, and steps. Components are available in `.mdx` files without imports. ## Callout Notes, warnings, tips, and pull quotes. ```mdx The config file lives at `site/tidypress.config.ts`. Do not commit `site/build/` — it is generated. Deploy it, do not treat it as source. Run `tidypress clean` if a build looks stale after upgrading TidyPress. The canonical copy lives in git, not in an export workflow. ``` The config file lives at `site/tidypress.config.ts`. Do not commit `site/build/` — it is generated. Deploy it, do not treat it as source. Run `tidypress clean` if a build looks stale after upgrading TidyPress. The canonical copy lives in git, not in an export workflow. | Prop | Type | Default | |------|------|---------| | `type` | `"note" \| "warning" \| "tip" \| "quote"` | `"note"` | ## Tabs Alternate commands or examples. ````mdx ```bash npm install -g tidypress ``` ```bash pnpm add -g tidypress ``` ```` ```bash npm install -g tidypress ``` ```bash pnpm add -g tidypress ``` | Prop | Type | Required | |------|------|----------| | `labels` | `string[]` | yes | `labels` must match the number of direct `` children. ## FileTree Project structure. Folders collapse on click. Wrap important files in `**` to highlight them. In plain `txt` trees elsewhere in the docs, inline `#` comments label collections — for example `# docs collection — sidebar-ordered guides at /docs/…`. ```mdx - site/ - **tidypress.config.ts** - public/ - images/ - src/ - content/ - writing/ - hello.md - projects/ - docs/ - getting-started.md ``` - site/ - **tidypress.config.ts** - public/ - images/ - src/ - content/ - writing/ - hello.md - projects/ - docs/ - getting-started.md Two spaces per indent level. No props. ## Mermaid Mermaid diagrams. ```mdx Build[tidypress build] Build --> Static[Static site] Static --> Host[Static host] `} /> B`} /> ``` Build[tidypress build] Build --> Static[Static site] Static --> Host[Static host] `} /> | Prop | Type | Default | Required | |------|------|---------|----------| | `code` | string | — | yes | | `align` | `"left" \| "center" \| "right"` | `"left"` | no | Diagrams adapt when the theme changes. Use `align="center"` for tall vertical flowcharts in narrow writing columns. ## Image Put images in `site/public/images/` — for instance `site/public/images/flower.jpg`. Use the component for captions; use markdown for simple embeds. ```mdx Colorful tulips in bloom ``` Colorful tulips in bloom | Prop | Type | Required | |------|------|----------| | `src` | `string \| ImageMetadata` | yes | | `alt` | string | yes | | `caption` | string | no | | `width` | number | no | | `height` | number | no | Same file with markdown: ```md ![Colorful flowers](/images/flower.jpg) ``` ## Tooltip Short inline definitions. ```mdx The build folder is regenerated on each build. ``` The build folder is regenerated on each build. | Prop | Type | Required | |------|------|----------| | `tip` | string | yes | ## Steps Numbered procedural blocks in MDX. Manual pages use the same components with manual page chrome. ```mdx Add a post under `site/src/content/writing/`, work under `projects/`, or a guide under `site/src/content/docs/`. Run `tidypress dev`. ``` Add a post under `site/src/content/writing/`, work under `projects/`, or a guide under `site/src/content/docs/`. Run `tidypress dev`. | Component | Prop | Type | Required | |-----------|------|------|----------| | `Step` | `title` | string | no | Wrap one or more `` elements inside ``. ## Native details Native HTML also works: ````html
Generated files ```txt site/build/ ```
````
Generated files ```txt site/build/ ```
--- ### [Configuration](https://tidypress.pages.dev/docs/configuration) The shape of tidypress.config.ts — collections, nav, and site metadata. TidyPress reads one config file at the publish root: `site/tidypress.config.ts`. That file defines how writing, work, and the **`docs` collection** are routed—not the prose itself. ```txt site/tidypress.config.ts ``` Only `name` is required. Keep the file small until the site needs more shape. ```ts import { defineConfig } from 'tidypress/config' export default defineConfig({ name: 'my-project', description: 'A publishing framework for Git-native authorship.', nav: [ { label: 'docs', href: '/docs' }, { label: 'writing', href: '/writing' }, ], collections: { docs: { enabled: true, basePath: '/docs', label: 'docs' }, writing: { enabled: true, basePath: '/writing', kind: 'writing', label: 'writing' }, }, siteUrl: 'https://example.com', }) ``` ## Site metadata ```ts name: 'my-project', description: 'A publishing framework for Git-native authorship.', siteUrl: 'https://example.com', ``` `name` appears in titles and the header. `description` is used for metadata and homepage copy. `siteUrl` is used for canonical URLs, sitemap output, RSS, and social metadata. Pass it at init when you know the domain: ```bash npx tidypress init --site-url https://yoursite.example ``` `tidypress doctor` and `tidypress build` warn when `siteUrl` is still the `https://example.com` placeholder. Until it is set, builds omit absolute canonical/OG URLs and the Astro sitemap (no fake `example.com` hosts). See [Conventions](./conventions). ## Navigation Use `nav` for header links. ```ts nav: [ { label: 'docs', href: '/docs' }, { label: 'writing', href: '/writing' }, { label: 'GitHub', href: 'https://github.com/you/project' }, ], ``` Navigation is strict by default. Internal `href` values are validated against built routes. If a site has links that are generated outside TidyPress, use relaxed mode: ```ts navPolicy: { mode: 'relaxed', } ``` ## Footer Community links for the docs site live in [`CONTRIBUTING.md`](https://github.com/Raphjacksun7/tidypress/blob/main/CONTRIBUTING.md) on GitHub (bug reports, feature requests, security). The product site footer can point to those URLs via `footer.main` link slots. The page footer has two bands: | Band | Position | Purpose | |------|----------|---------| | **Main** (`main`) | Top | Two plain-text slots (`start` / `end`). Hidden when both are empty and the site has only one locale. | | **Sub** | Bottom | Icon/text links on the left; attribution on the right. | The main band is the future extension point for richer blocks (menus, grids, newsletter, banners, and so on). Today it is text only. Markup uses `data-footer-zone` and `data-footer-slot` attributes so later releases can hang structured blocks off the same layout. ### Config shapes `footer` accepts either: 1. **Link array** — shorthand for sub-footer links only. 2. **Object** — full control over main band, links, copyright, and product credit. ```ts footer: [ { label: 'GitHub', href: 'https://github.com/you/project', icon: 'github' }, ], ``` ```ts footer: { main: { start: 'Acme Labs', end: 'Questions? hello@example.com', }, copyright: '© {year} {name}', links: [ { label: 'GitHub', href: 'https://github.com/you/project', icon: 'github' }, { label: 'RSS', href: '/writing/rss.xml', icon: 'rss', external: false }, ], }, ``` `aside` is accepted as an alias for `main.end`. ### Defaults Unless you override them: | Field | Default | |-------|---------| | `copyright` | `© {year} {name}` (`{year}` = build year, `{name}` = `name`) | | `showCredit` | `true` — appends a product credit link on the attribution line | | `credit` | `prefix: ', Made with '`, `label: 'tidypress'`, `href: 'https://tidypress.pages.dev/'` | | `links` | If you omit GitHub, one is added using `repository.url` when set, otherwise `https://github.com/you` | | `main` | Empty — main band hidden until you set `start` / `end` or enable multi-locale i18n | You do **not** need `showCredit: true` in config; it is already on. Set `showCredit: false` only when you want to hide the product credit segment. Attribution renders as one line, for example: `© 2026 my-project, Made with tidypress`. ### Field reference **`main`** — top band text slots. Each slot is plain `string` text or an array of inline links (shown with ` · ` separators). | Key | Type | Description | |-----|------|-------------| | `start` | `string` \| `FooterMainLink[]` | Left slot. | | `end` | `string` \| `FooterMainLink[]` | Right slot. Language switcher renders here when `i18n.locales` has more than one entry. | `FooterMainLink` fields: `label`, `href`, optional `external: false` for same-tab internal paths. ```ts main: { start: [ { label: 'Improve these docs', href: 'https://github.com/you/project/tree/main/apps/site/src/content/docs' }, { label: 'Share a feature idea', href: 'https://github.com/you/project/issues/new' }, ], end: 'Questions? hello@example.com', }, ``` **`copyright`** — `string` on the sub-footer attribution line. Supports `{year}` and `{name}` tokens. **`showCredit`** — `boolean`. Default `true`. Set `false` to omit the product credit segment (copyright still shows). **`credit`** — partial override of the product credit segment. Used only when `showCredit` is true. | Key | Type | Default | |-----|------|---------| | `prefix` | `string` | `', Made with '` | | `label` | `string` | `tidypress` | | `href` | `string` | `https://tidypress.pages.dev/` | **`links`** — `FooterItem[]` in the sub-footer (left). Each item: | Key | Type | Description | |-----|------|-------------| | `label` | `string` | Required. Visible text for text links; screen-reader label for icon links. | | `href` | `string` | Required. | | `icon` | `FooterItemIcon` | When set, renders a built-in SVG icon instead of text. | | `external` | `boolean` | `false` keeps same-tab navigation for internal paths. Icon links default to external/new tab unless `external: false`. | ### Built-in footer icons Set `icon` to one of: `github`, `x`, `linkedin`, `discord`, `youtube`, `instagram`, `bluesky`, `facebook`, `reddit`, `twitch`, `mastodon`, `slack`, `telegram`, `tiktok`, `npm`, `rss`, `email` Text links (no `icon`) render as plain text in the sub-footer. ### Repository URL `repository.url` feeds the default GitHub footer link when `links` does not already include a `github` icon: ```ts repository: { url: 'https://github.com/you/project', branch: 'main', editPath: 'site/src/content', ``` `editPath` is relative to the repo root and should cover all collections you want “edit on GitHub” links for. See [Repository links](#repository-links). ### Writing and RSS When the writing collection is enabled, TidyPress generates a feed at `/rss.xml`. With the default path, that is `/writing/rss.xml`. Add a footer link when you want the RSS icon in the sub-footer: ```ts { label: 'RSS', href: '/writing/rss.xml', icon: 'rss', external: false }, ``` ### i18n With multiple locales, a language switcher appears in `main.end` (footer main band). Customize its accessible name via `i18n.strings..languageLabel`. See [i18n](./i18n). ### Examples **Hide product credit** (copyright only): ```ts footer: { showCredit: false, links: [{ label: 'GitHub', href: 'https://github.com/you/project', icon: 'github' }], }, ``` **Custom product credit**: ```ts footer: { credit: { prefix: ' · Built with ', label: 'TidyPress', href: 'https://tidypress.pages.dev/', }, }, ``` **Icon and text links**: ```ts footer: { links: [ { label: 'GitHub', href: 'https://github.com/you/project', icon: 'github' }, { label: 'X', href: 'https://x.com/you', icon: 'x' }, { label: 'Privacy', href: '/privacy', external: false }, ], }, ``` ## Collections Collections map folders under `src/content/` to route families. The collection key `docs` is the **docs collection** at `/docs/…`; it is not the publish root folder `site/` from `init`. ```ts collections: { writing: { enabled: true, basePath: '/writing', kind: 'writing', label: 'writing', }, docs: { enabled: false, basePath: '/docs', label: 'docs', }, guides: { enabled: true, basePath: '/guides', kind: 'content', label: 'guides', }, } ``` Kinds: | Kind | Use for | |------|---------| | omitted on `docs` | the `docs` collection, sidebar-ordered routes | | `content` | reference shelves, playbooks, ADRs | | `writing` | dated posts | | `projects` | project cards and optional project pages | | `page` | standalone pages | Example projects collection: ```ts projects: { enabled: true, basePath: '/projects', kind: 'projects', label: 'projects', }, ``` ### Collection key, URL, and label Three settings control different surfaces. They are not renamed automatically for each other. | Setting | Controls | |---------|----------| | Collection key `works` | Content folder `site/src/content/works/` and `home.order` entries | | **`basePath`** | Public URLs (`/works`, `/works/sample`) | | **`label`** | UI copy on indexes and the homepage section title | | **`kind`** | Engine behavior (schema, layout, routing). Must be one of the kinds in the table above | There is no `kind: 'works'`. Use `kind: 'projects'` for project cards and optional project pages, then pick any key and paths you want. Rename a projects showcase to **works** everywhere visitors see it: ```ts nav: [ { label: 'writing', href: '/writing' }, { label: 'works', href: '/works' }, ], home: { order: ['writing', 'works'], }, collections: { docs: { enabled: false, basePath: '/docs', label: 'docs' }, writing: { enabled: true, basePath: '/writing', kind: 'writing', label: 'writing' }, works: { enabled: true, basePath: '/works', kind: 'projects', label: 'works', }, }, capabilities: { disable: ['docs'], }, ``` Place markdown in `site/src/content/works/`. Match `nav` `href` values to each collection `basePath`. ### Init presets `tidypress init` seeds content and config. `default` is an alias for `lab`. | Preset | Shape | |--------|--------| | `lab` | writing + projects, docs off | | `blog` | writing only, docs and pages off | | `persona` | hero, projects, writing, about page | | `body-of-work` | works, projects, writing, reference, process, pages; docs disabled | | `body-of-work-docs` | body-of-work + enabled `docs` | | `docs-writing` | docs + writing | | `custom` | docs + writing + a `playbooks` content collection | ```bash npx tidypress init --preset blog --site-url https://yoursite.example ``` See [Examples](./examples) for runnable copies under `examples/`. ### Capabilities Starter collections (`docs`, `writing`, `pages`) respect `capabilities.disable` and `capabilities.enable` after per-collection `enabled` flags. The blog preset disables `docs` and `pages` so empty `/pages/` routes are not generated. ```ts capabilities: { disable: ['docs', 'pages'], }, ``` See [Advanced configuration](./advanced-configuration#capabilities) for the full capability list. `sections` is the legacy shim. ## Hero bar The home hero bar is opt-in. Set `enabled: true` to show role, pronunciation, lead, and links above homepage previews: ```ts hero: { enabled: true, role: 'Engineer', pronunciation: 'your-name', lead: 'Short bio on the home page.', image: '/images/portrait.jpg', links: [ { label: 'Email', href: 'mailto:you@example.com' }, { label: 'GitHub', href: 'https://github.com/you', external: true }, ], }, ``` Omit `hero` or leave `enabled` unset/false to hide the bar. The `persona` init preset enables a starter hero. ### Persona / CV sites The `persona` preset gives you a hero, projects, optional writing, and an `/about` page. **Experience, education, and skills belong in markdown** on that page or in writing posts — not in structured config blocks. TidyPress does not ship résumé schema (no job blocks, degree blocks, or employment location fields). That keeps the product markdown-first and avoids vague HR metadata (`hybrid`, `remote`, `present`, and similar). When you need a CV section, use normal headings on `/about`: ```md ## Experience **Company — Role** · 2022–2024 What you shipped. ## Education **School — Degree** · 2020 ``` Use `projects` for highlights and `writing` for essays; link out with `url` + `linkOnly` when a row is only a link. ## Pages Two related mechanisms — do not confuse them: | Mechanism | Purpose | |-----------|---------| | `pages: [...]` in config | Register root routes and optional nav labels | | `collections.pages` with `kind: 'page'` | Astro collection under `src/content/pages/`; includes a `/pages/` index when enabled | For a single **About** or **CV** page at `/about`, use a file in `src/content/pages/` and list it in config: ```ts pages: [{ slug: 'about', navLabel: 'about' }], collections: { pages: { enabled: true, kind: 'page' }, }, ``` ```txt site/src/content/pages/about.md -> /about ``` **Lab** and **blog** presets disable the pages collection so an empty `/pages/` route is not generated. Enable `pages` only when you add root pages (persona enables it for `/about`). Legacy shorthand (slug only): ```ts pages: ['about', { slug: 'work', navLabel: 'work' }], ``` ## Branding ```ts branding: { icon: '/favicon.svg', favicon: '/favicon-white.svg', } ``` Place files in `site/public/`. ## Search Search is powered by [Pagefind](https://pagefind.app/) and generated during `tidypress build`. ```ts search: { exclude: ['docs/internal/*', 'writing/drafts/*'], } ``` Exclude one page with frontmatter: ```yaml --- search: false --- ``` ## Repository links Docs pages can show an “Edit this page” link: ```ts repository: { url: 'https://github.com/you/project', branch: 'main', editPath: 'site/src/content', } ``` ## Presentation settings Keep presentation separate from the base config: - [Site layout](./site-layout) covers sidebar groups, chapter navigation, homepage previews, indexes, tags, and icons. - [Theme](./theme-typography) covers typography, theme mode, code highlighting, and custom tokens. - [Advanced configuration](./advanced-configuration) covers i18n, versions, analytics, capabilities, and `build/llms.txt`. --- ### [Conventions](https://tidypress.pages.dev/docs/conventions) What TidyPress fixes in the site graph, what it leaves to markdown, and how the folder names map to intent. TidyPress fixes **site shape**—navigation, home page, collection kinds, build output—and stays out of **prose**. You choose a preset; you write markdown inside the boundaries that preset defines. The constraints exist so you do not rebuild the same presentation layer for every repository that deserves a public face. ## Vocabulary Two different things share the word *docs*, and conflating them is the usual source of confusion. The **publish root** is the folder that holds `tidypress.config.ts`, `src/content/`, `public/`, and eventually `build/` — the site’s home in git. `tidypress init` defaults to `site/`; you can use any folder name. Set `TIDYPRESS_PUBLISH_ROOT` when the CLI cannot infer it from your working directory. The **`docs` collection** is a first-class routed subtree under `src/content/docs/`, served at `/docs/…`, with sidebar ordering—the same class of feature as `writing` and `projects`. It carries tutorials, guides, and stable reference. The default **lab** preset disables it so the home page foregrounds writing and work; turn it on with `docs-writing`, `body-of-work-docs`, or `collections.docs.enabled`. | Name | Role | |------|------| | Publish root `site/` | Config, content collections, static output | | `writing` | Dated posts, RSS, tags—the usual public voice | | `projects` / `works` | Work on the home page; folder name follows the collection key | | `docs` (collection) | Sidebar-ordered guides and reference at `/docs/…` | ## What we enforce | Layer | Rule | |-------|------| | **Preset** | `lab`, `blog`, `persona`, `body-of-work`, `body-of-work-docs`, `docs-writing`, `custom` — nav, home, and default collections | | **Kind** | `writing`, `projects`, `content`, `page` — routing, RSS, sidebar, layouts | | **Config** | `siteUrl` for production URLs; `tidypress doctor` / `tidypress build` warn on placeholders | | **Nav** | Strict mode validates internal `href` values; overflow uses a “more” menu on small screens | ## What we do not enforce - Per-file JSON Schema or lint scores (`check` / `sweep` are out of scope) - Résumé blocks, OpenAPI, or embed platforms in core - A single folder tree for every author — use `custom` when you need extra collections ## Collection kinds | Kind | Use for | |------|---------| | `writing` | Dated posts, RSS, tags | | `projects` | Cards, featured work, optional project pages | | `content` | Reference shelves, ADRs, playbooks in the docs-style shell | | `page` | `/about`, `/now`, standalone pages | ## Docs collection vs body of work | Collection | Purpose | |------------|---------| | `docs` | Sidebar-ordered product documentation | | `reference` | Facts: API, CLI, config | | `process` | Decisions: ADRs, roadmaps | Preset **`body-of-work`** disables the `docs` collection and surfaces **reference** and **process** from the footer. Use **`body-of-work-docs`** or **`docs-writing`** when you want `docs` enabled from init. ## siteUrl and sitemap Set **`siteUrl`** to your production origin before deploy: ```bash npx tidypress init --site-url https://yoursite.example ``` Until `siteUrl` is set to a real production origin — not the `https://example.com` placeholder — TidyPress does not emit absolute canonical or Open Graph URLs, and it does not write a sitemap that points at a placeholder host. After `siteUrl` is set, `tidypress build` produces `sitemap-index.xml` for that origin. ## Escape hatches - **`custom` preset** — extra collections with a known `kind` - **`navPolicy.mode: 'relaxed'`** — nav links not validated against routes - Enable or disable collections in `tidypress.config.ts` after init ## Related - [Body of work](./body-of-work) - [Configuration](./configuration) --- ### [Deploy](https://tidypress.pages.dev/docs/deploying) Build static output, copy it, or hand it to the host you already use. TidyPress builds static files. Deployment is whatever you do with those files. ```bash tidypress build ``` Output lands in `site/build/` after `tidypress init`. ```txt site/build/ ``` That directory contains HTML, Astro assets, sitemap files, public assets, Pagefind search, and `llms.txt` when enabled. ## Preview the production build ```bash tidypress preview ``` Preview serves the built output locally, with search and sitemap output in place. ## Copy output somewhere else ```bash tidypress build --output ./dist ``` or: ```bash tidypress deploy ./release/docs tidypress deploy file:///tmp/tidypress-site ``` Local path targets copy the built `build/` directory. ## Artifact-only deploy ```bash tidypress deploy ``` With no target, TidyPress builds the site and prints the artifact path. Upload that directory with your own CI, host, or script. ## Provider CLI targets These targets call local provider tools: ```bash tidypress deploy vercel # runs vercel deploy --prod tidypress deploy netlify # runs netlify deploy --dir --prod tidypress deploy surge # runs surge tidypress deploy github-pages # runs npx gh-pages -d tidypress deploy cloudflare # runs wrangler pages deploy ``` The command hands your `build/` directory to the selected CLI. ## Docker ```bash tidypress deploy docker ``` This writes `Dockerfile` and `docker-compose.yml` into `site/build/`. Then run: ```bash cd site/build docker compose up -d --build ``` The generated Dockerfile serves the static output with `nginx:alpine`. ## S3 Use an explicit target: ```bash tidypress deploy s3://my-bucket/docs ``` or set an environment variable: ```bash TIDYPRESS_S3_TARGET=s3://my-bucket/docs tidypress deploy s3 ``` TidyPress runs: ```bash aws s3 sync site/build/ s3://my-bucket/docs --delete ``` You need the AWS CLI installed and authenticated. ## SSH Use an SSH-style target: ```bash tidypress deploy deploy@example.com:/var/www/docs ``` or: ```bash TIDYPRESS_SSH_TARGET=deploy@example.com:/var/www/docs tidypress deploy ssh ``` TidyPress runs: ```bash rsync -az --delete site/build/ deploy@example.com:/var/www/docs ``` You need `rsync` and SSH access. ## Other URI targets Unknown URI schemes are treated as external targets: ```bash tidypress deploy gs://my-bucket/docs ``` TidyPress builds the site and prints the artifact path for unknown URI schemes. ## CI workflow generation ```bash tidypress deploy vercel --with-ci ``` This writes `.github/workflows/deploy.yml` for the selected target. Add the required secrets before using the workflow. ## Nginx If you copy the output to a server, point Nginx at the static directory: ```nginx server { listen 80; server_name docs.example.com; root /var/www/docs; index index.html; location / { try_files $uri $uri/ $uri/index.html =404; } } ``` ## What to upload Deploy the contents of: ```txt site/build/ ``` Do not upload `~/.cache/tidypress/`. Upload only the `build/` contents. --- ### [Display options](https://tidypress.pages.dev/docs/display-options) Configure homepage previews, collection indexes, cards, lists, dates, descriptions, tags, and gaps. Display options are optional. Start without them, then add only the overrides your site needs. ## Defaults Homepage defaults: | Option | Default | |--------|---------| | `home.previewLimit` | `5` | | `home.order` | `['writing', 'docs']` | | `layout` | `list` | | `gap` | `sm` | | writing dates on homepage | shown | | descriptions on homepage | hidden | | tags | hidden | Writing dates on the homepage are shown by default. Set `showDate: false` only when you want to hide them. Descriptions on the homepage are opt-in. `showDescription: false` and an omitted `showDescription` both hide descriptions. ## Homepage Use `home` for the front page preview sections: ```ts home: { previewLimit: 3, order: ['docs', 'writing'], collections: { writing: { showDate: false, showDescription: true, }, }, } ``` `home.order` controls section order. Entries are **collection keys** such as `works`, not the display label. Omit `home.order` to use enabled collections in default order, or set `home.preset` to `lab`, `blog`, `docs-writing`, or `persona` for init-style ordering. See [Site layout](./site-layout#home-presets). `home.previewLimit` controls how many entries each homepage section shows. Omit it to use `5`. ## Layout Use `layout: 'list'` for compact rows: ```ts home: { collections: { writing: { layout: 'list', }, }, } ``` Use `layout: 'card'` for richer rows: ```ts home: { collections: { docs: { layout: 'card', showDescription: true, }, }, } ``` If `layout: 'card'` is set but `showDescription` is not true, homepage previews stay compact: title on the left, date on the right for writing entries. ## Gap Spacing presets: | Value | Use for | |-------|---------| | `sm` | tight lists | | `md` | normal card stacks | | `lg` | airy landing pages | ```ts home: { collections: { docs: { gap: 'lg', }, }, } ``` ## Collection Indexes Use `collections..display` for collection index pages: ```ts collections: { writing: { display: { layout: 'card', gap: 'md', showDate: true, showDescription: true, }, }, } ``` Collection index display and homepage display are separate. A writing index can show descriptions while the homepage keeps writing previews compact. ## Tags And Icons Entries can define tags and icons: ```yaml --- title: Release notes date: 2026-05-22 icon: /images/release.svg tags: [release, notes] --- ``` Tags are emitted in metadata. They render only when `showTags: true`. Icons render on card-style previews when an entry or display config provides an `icon`. ## Search Pagefind indexes enabled content collections (everything except `kind: 'page'`). Each searchable region gets a `collection:` filter attribute. When two or more collections are searchable, the header search modal shows filter chips (All, writing, projects, and so on). Pick a chip to scope results to that collection. Standalone pages (`kind: 'page'`) are not included in collection filter chips. Set `search: false` on an entry to exclude it from the index. --- ### [Examples](https://tidypress.pages.dev/docs/examples) Runnable example projects for common TidyPress shapes. The repository keeps focused examples under `examples/`. Use them when you want one publish-root shape—writing-first lab, blog-only, body-of-work—without copying the full TidyPress marketing site. ## Minimal ```bash pnpm --filter @tidypress/example-minimal build ``` `examples/minimal` is the smallest runnable project. It mirrors the default `tidypress init` shape: ```txt examples/minimal/site/ ├── tidypress.config.ts └── src/content/ ├── docs/getting-started.md └── writing/hello.md ``` Use it to test the basic docs + writing flow. ## Lab ```bash pnpm --filter @tidypress/example-lab build ``` `examples/lab` matches `tidypress init` (lab preset): writing and projects on the home page, docs off. ## Blog ```bash pnpm --filter @tidypress/example-blog build ``` `examples/blog` matches `tidypress init --preset blog`: writing only; docs, projects, and pages off. ## Persona ```bash pnpm --filter @tidypress/example-persona build ``` `examples/persona` matches `tidypress init --preset persona`: opt-in hero, projects, writing, and an about page. ## Body of work ```bash npx tidypress init --preset body-of-work ``` Seeds works, projects, writing, reference, and process collections. See [Body of work](./body-of-work). ## i18n and versions ```bash pnpm --filter @tidypress/example-i18n-versioned build ``` `examples/i18n-versioned` shows: - latest docs at `/docs` - archived docs at `/docs/v1` - French routes under `/fr` - default-locale content without an `en/` folder ## Custom collections ```bash pnpm --filter @tidypress/example-custom-collections build ``` `examples/custom-collections` adds a `playbooks` collection: ```ts collections: { playbooks: { enabled: true, basePath: '/playbooks', kind: 'content', label: 'playbooks', }, } ``` Use this when writing, projects, and the `docs` collection are not enough for the shape of your site. ## Product docs site `apps/site` is the full TidyPress documentation site in this repository. The focused examples are better starting points when you want a small fixture to copy. --- ### [Extending TidyPress](https://tidypress.pages.dev/docs/extensibility) Advanced rendering hooks for custom collections and custom docs forms. Project-local presentation hooks for custom sections and docs forms. ## Two axes TidyPress separates content location from page model: | Axis | Declared in | Use for | |------|-------------|---------| | Collection | `collections.` | a site section and base path | | Doc form | docs frontmatter `form` | a page model inside the docs collection | Folders build URLs. They do not declare page models. ## Built-in docs forms Docs pages support: | `form` | Layout | |--------|--------| | `doc` | default docs page with sidebar, table of contents, and chapter navigation | | `manual` | procedural page chrome and step styling | ```yaml --- title: Install guide form: manual order: 1 --- ``` Doc pages can set `part` for chapter grouping: ```yaml --- title: Routing part: Part I order: 2 --- ``` Doc chapter navigation follows the configured sidebar order when present, then frontmatter `order`. ## Custom collections Add another section with a built-in kind: ```ts collections: { guides: { enabled: true, basePath: '/guides', kind: 'content', label: 'guides', }, } ``` Kinds: `content`, `writing`, `page`. ## Custom collection rendering Project-local presentation code: ```ts collections: { api: { enabled: true, basePath: '/api', kind: 'content', render: { presentation: './site/renderers/api-presentation.ts', views: './site/views/api/', }, }, } ``` On `tidypress dev` and `tidypress build`: 1. The config is validated. 2. TidyPress writes a plugin manifest into the local cache (`~/.cache/tidypress/.../codegen/`). 3. Custom Astro views resolve from your project via the `@project` alias. 4. The engine imports the presentation module. 5. `RouteViewShell` uses your view when a matching key exists. Paths must be project-local `./` paths. Parent directory traversal is rejected. `collections.docs` cannot set `render`. Docs pages use `form`. ## Presentation module Export `createPresentation(site, context)`: ```ts import type { TidyPressConfig } from '@tidypress/config' import type { TidyPressPluginPresentation } from '@tidypress/engine/plugins' export function createPresentation( site: TidyPressConfig, context: { collectionKey: string }, ): TidyPressPluginPresentation { return { async buildIndex(route) { return { viewKey: `${context.collectionKey}:collection-index`, site, route, title: 'API', headings: [], pagefindIgnore: true, } }, async buildEntry(route) { return { viewKey: `${context.collectionKey}:collection-entry`, site, route, title: route.slug ?? 'API entry', headings: [], pagefindIgnore: false, } }, } } ``` Prefix view keys with the collection key. ## Optional Astro views If `render.views` points to `./site/views/api/`, TidyPress looks for: ```txt collection-index.astro collection-entry.astro version-root.astro ``` Missing files are skipped. Built-in views are used as fallback. ## Custom docs forms Register custom forms with `extensions.docForms`: ```ts extensions: { docForms: { 'api-reference': { label: 'API reference', presentation: './site/renderers/api-reference-presentation.ts', views: './site/views/api-reference/', }, }, } ``` Then use the form in docs frontmatter: ```yaml --- title: POST /v1/widgets form: api-reference --- ``` Built-in form names cannot be overridden. ## Development reload During `tidypress dev`, changes to config, presentation modules, and optional views regenerate the plugin manifest and reload the browser. If a plugin path or export is invalid, TidyPress fails before the server starts. Fix the config path or module export and run again. --- ### [Getting started](https://tidypress.pages.dev/docs/getting-started) Scaffold a git-native public site, run it locally, and ship static output. TidyPress treats a public site as **files in git**: a config file, typed collections of markdown, a build step, static output you host yourself. Starter collections include **writing**, **projects**, and **`docs`** — sidebar-ordered guides at `/docs/…`. The default **lab** preset foregrounds writing and work on the home page and leaves the **`docs` collection** disabled until you enable it. `init` scaffolds `site/` at the project root — the publish root for config, content, and `build/`. The **`docs` collection** lives at `src/content/docs/` and routes at `/docs/…` with sidebar layout and ordering. ## Install ```bash npm install tidypress ``` ```bash pnpm add tidypress ``` ```bash pip install tidypress ``` Node.js 22.12 or newer is required for rendering. The Python package can run Python-native helpers, but site builds still use the Node.js CLI and Astro engine. ## Start a project From the root of your project: ```bash npx tidypress init --site-url https://yoursite.example npx tidypress dev ``` Open `http://localhost:4321`. `init` creates a `site/` folder. The **lab** preset seeds writing and projects; the `docs/` subtree is present but disabled in config until you turn the collection on. ```txt site/ ├── tidypress.config.ts ├── public/ │ └── images/ └── src/ └── content/ ├── writing/ # dated posts (RSS, tags, archive) │ └── hello.md ├── projects/ # cards + optional pages (lab preset) ├── docs/ # docs collection — sidebar-ordered guides at /docs/… │ └── getting-started.md └── pages/ # root routes (e.g. /about) ``` The **lab** preset is the default: writing and projects on the home page, **`docs` collection** disabled in config. Replace the seeded files with your own material. Other presets: | Preset | Shape | |--------|--------| | `lab` (default) | writing + projects | | `persona` | hero bar, projects, writing, about page | | `blog` | writing only | | `docs-writing` | docs + writing | | `body-of-work` | works, projects, writing, reference, process | | `body-of-work-docs` | body-of-work + enabled `docs` | | `custom` | docs + writing + a `playbooks` content collection | `default` is an alias for `lab`. ```bash npx tidypress init --preset blog --site-url https://yoursite.example npx tidypress init --preset persona --site-url https://yoursite.example npx tidypress init --preset body-of-work --site-url https://yoursite.example npx tidypress init --preset docs-writing --site-url https://yoursite.example npx tidypress init --preset custom --site-url https://yoursite.example ``` ## Writing Dated posts live in the **writing** collection: ```md --- title: Release notes date: "2026-05-22" description: Notes from the latest release. --- What changed and why. ``` Path: `site/src/content/writing/` under the publish root. Project cards: `site/src/content/projects/` on presets that include projects — `lab`, `persona`, `body-of-work`, and others. ## Docs collection The **`docs` collection** is sidebar-ordered markdown at `/docs/…`—tutorials, how-tos, reference. Enable it with preset `docs-writing` or `body-of-work-docs`, or set `collections.docs.enabled: true` in config. ```md --- title: Install description: Install and configure the project. order: 1 --- ## Requirements Write the page in markdown. ``` Path: `site/src/content/docs/` — routes under `/docs/…`, sidebar order via `order` in frontmatter. ## Build and preview ```bash npx tidypress build npx tidypress preview ``` Build output is written to: ```txt site/build/ ``` Search is generated during `tidypress build`, so the search modal is available in `preview`, not in the dev server. Deploy the `build/` folder with your static host, CI job, or `tidypress deploy`. ## Generated files — do not edit TidyPress keeps your markdown in place. It writes two kinds of generated output: ```txt your project └── site/ ├── tidypress.config.ts ├── src/content/ └── build/ ~/.cache/tidypress// ``` - **`build/`** — HTML, assets, Pagefind search, `llms.txt`, and sitemap when `siteUrl` is production-ready. Upload only this folder. Skip `llms.txt` with `tidypress build --no-llms-txt` or `capabilities.disable: ['llmsTxt']`. - **Cache** — plugin codegen and Astro cache under `~/.cache/tidypress/`. Safe to delete; recreated on the next `dev` or `build`. If a local build behaves strangely after an engine upgrade, run: ```bash npx tidypress clean npx tidypress build ``` ## Static assets Put files that should be served as-is in `site/public/`: ```txt site/public/ ├── images/diagram.png ├── downloads/spec.pdf ├── robots.txt └── favicon.svg ``` Files under `public/` are served from the site root. `site/public/images/diagram.png` becomes `/images/diagram.png`. ```md ![Architecture diagram](/images/diagram.png) [Download the spec](/downloads/spec.pdf) ``` `downloads/` is only an example folder. Any file under `public/` is served from the site root. ## Next steps - [Markdown and frontmatter](./writing-content) covers fields, links, images, drafts, and scheduling. - [Configuration](./configuration) covers `tidypress.config.ts`. - [Deploy](./deploying) explains `build/` output and provider targets. - [Agents and markdown](/writing/agents-and-markdown) — same workflow for human edits and agent drafts; use `build/llms.txt` after build. - [Body of work](./body-of-work) — works, reference, and process collections. --- ### [CI and deployment](https://tidypress.pages.dev/docs/manual/ci) Cache build inputs and upload only the static build/ artifact. TidyPress builds are deterministic static sites. In CI, cache compiler inputs and publish only `build/`. ## What to upload Upload the contents of your publish root’s `build/` folder: ```txt site/build/ ├── index.html ├── assets/ ├── pagefind/ └── ... ``` Do not upload: - `~/.cache/tidypress/` (local compiler cache) - `node_modules/` ## GitHub Actions pattern ```yaml - uses: actions/cache@v4 with: path: ~/.cache/tidypress key: tidypress-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', '**/tidypress.config.ts', '**/src/content/**') }} restore-keys: | tidypress-${{ runner.os }}- - run: pnpm exec tidypress build - run: pnpm exec wrangler pages deploy apps/site/build --project-name=tidypress --branch=main env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} ``` Add `wrangler` as a devDependency in the repo that runs CI, then use `pnpm exec wrangler` — `wrangler-action` may try to install Wrangler during the job and fail on a private pnpm workspace root. ```yaml # optional: create project once - run: pnpm exec wrangler pages project create tidypress --production-branch main continue-on-error: true env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} ``` Adjust `apps/site/build` to your publish root’s `build/` path when your config lives at the project root. Set `CI=true` so TidyPress emits structured JSON build logs. ## Environment variables | Variable | Purpose | |----------|---------| | `CI=true` | JSON structured logs from Astro | | `TIDYPRESS_JSON_LOGS=1` | Force JSON logs locally | | `TIDYPRESS_PROJECT_ROOT` | Set automatically by the CLI; required only when running Astro directly with `@tidypress/astro` | | `TIDYPRESS_PUBLISH_ROOT` | Explicit publish root when auto-discovery is ambiguous | --- ### [Python wrapper](https://tidypress.pages.dev/docs/python) Use TidyPress from Python and run Python-native helper commands. The Python package provides a `tidypress` entrypoint for Python environments. Rendering still uses the Node.js CLI and Astro engine. Node.js 22.12 or newer is required for `init`, `dev`, `build`, `preview`, and `deploy`. ## Install ```bash pip install tidypress ``` For site commands, the wrapper resolves the Node CLI in this order: 1. `TIDYPRESS_CLI_JS` 2. a local monorepo or project `node_modules/tidypress` 3. `tidypress` on `PATH` It does not use `npx` by default. `TIDYPRESS_USE_NPX=1` is available as an explicit escape hatch. ## Site commands These commands are delegated to the Node CLI — the same surface as `npx tidypress`: ```bash tidypress init [--preset lab|blog|persona|docs-writing|custom] tidypress dev tidypress build tidypress preview tidypress clean tidypress deploy # build/llms.txt on every build tidypress import devto tidypress doctor tidypress migrate-sections tidypress add-version