Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/content/docs/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ api:
header: Authorization
placeholder: "Bearer token"

redirects:
- from: /old-page
to: /docs/new-page
- from: /legacy/api
to: /apis
permanent: true

analytics:
enabled: true
googleAnalytics:
Expand Down Expand Up @@ -319,6 +326,27 @@ api:
| `auth.header` | `string` | Header name for auth token |
| `auth.placeholder` | `string` | Placeholder text in auth input |

### redirects

URL redirects for migrating old routes to new ones. Checked before all other routes.

```yaml
redirects:
- from: /old-page
to: /docs/getting-started
- from: /legacy/api-docs
to: /apis
permanent: true
```

| Field | Type | Description | Default |
|-------|------|-------------|---------|
| `from` | `string` | Old URL path to redirect from | — |
| `to` | `string` | New URL path to redirect to | — |
| `permanent` | `boolean` | `true` for 308 (permanent), `false` for 307 (temporary) | `false` |

Use `permanent: true` when the old URL should never be used again — search engines and browsers will cache the redirect.

### analytics

Analytics integration for tracking page views.
Expand Down
145 changes: 145 additions & 0 deletions docs/content/docs/features.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
---
title: Features
description: Overview of Chronicle features
order: 2
---

# Features

Chronicle is a self-hosted documentation platform built with Vite + Nitro.

## Content

- **MDX support** — write documentation in MDX with React component embedding
- **Frontmatter** — `title`, `description`, `order`, `icon`, `lastModified`
- **Directory metadata** — `meta.json` for folder titles, ordering, and sidebar config
- **Remark plugins** — directives, admonitions, image resolution, link resolution, mermaid, reading time
- **Syntax highlighting** — powered by Shiki via Apsara CodeBlock
- **Versioning** — multiple documentation versions with URL-based routing

## API Reference

- **OpenAPI / Swagger support** — auto-generates API reference pages from specs (OpenAPI 3.x and Swagger 2.0)
- **Read-only overview** — field names, types, required badges, examples, response schemas
- **Playground dialog** — test requests with editable fields, JSON body editor, auth switching
- **Auth types** — API Key, Bearer Token, Basic Auth (auto-detected from spec `securitySchemes`)
- **Code snippets** — cURL, Python, Go, TypeScript with language switcher
- **Response panel** — status code tabs with JSON syntax highlighting
- **`.md` export** — every API endpoint has a `.md` URL with full documentation

## Navigation

- **Sidebar** — auto-generated from file structure, configurable via `meta.json`
- **Breadcrumbs** — shows path hierarchy for docs and API pages
- **Prev/Next** — arrow navigation between pages and API endpoints
- **Search** — full-text search with SQLite FTS5 across titles, headings, and body content
- **Folder sorting** — via `order` in `meta.json`
- **Page sorting** — via `order` in frontmatter or `pages` array in `meta.json`

## Themes

- **Default theme** — sidebar + content layout with sub-navigation bar
- **Paper theme** — book-style single-column with reading progress
- **Dark/light mode** — system preference or manual toggle

## SEO & AI

- **Meta tags** — auto-generated title, description, Open Graph, Twitter Card
- **Sitemap** — auto-generated `sitemap.xml`
- **robots.txt** — auto-generated
- **llms.txt** — AI-discoverable documentation index
- **`.md` URLs** — every page (docs and API) has a markdown URL for AI tools
- **Open in AI** — copy as markdown, open in ChatGPT or Claude

## Developer Experience

- **CLI** — `chronicle dev`, `chronicle build`, `chronicle start`
- **Hot reload** — instant updates during development
- **Monorepo support** — works as a package in monorepos
- **Docker support** — containerized deployment
- **Zod-validated config** — `chronicle.yaml` with schema validation

## Redirects

Configure URL redirects in `chronicle.yaml` for migrating old routes.

```yaml
redirects:
- from: /old-page
to: /docs/getting-started
- from: /legacy/api
to: /apis
permanent: true
```

- `permanent: false` (default) — 307 temporary redirect
- `permanent: true` — 308 permanent redirect

See [Configuration](/docs/configuration#redirects) for full reference.

## Sorting

### Pages

Add `order` to frontmatter. Lower numbers appear first.

```mdx
---
title: Introduction
order: 1
---
```

Or use `pages` array in `meta.json`:

```json
{
"pages": ["introduction", "installation", "configuration"]
}
```

### Folders

Add `order` to the folder's `meta.json`:

```json
{
"title": "Getting Started",
"order": 1
}
```

Folder sorting is controlled only by `meta.json` `order`. The index page frontmatter `order` does not affect folder position.

## Markdown URLs

Every page has a `.md` URL that returns raw markdown:

- **Docs pages** — `/{slug}.md` returns the raw MDX content
- **API endpoints** — `/apis/{spec}/{operationId}.md` generates markdown with parameters, examples, responses, and cURL

```bash
curl https://docs.example.com/docs/getting-started.md
curl https://docs.example.com/apis/petstore/findPetsByStatus.md
```

The "Open in AI" dropdown uses these URLs to copy markdown, open in ChatGPT, or open in Claude.

## API Reference Page

### Overview

The read-only overview shows endpoint title, method badge, path, authorisation fields, query/path parameters, request body, and response schemas. The right column has code snippets and response JSON.

### Playground

Click **Test request** in the navbar to open the playground dialog with editable fields, JSON body editor, auth type switching, and live response with status and timing.

### View Documentation

If the OpenAPI spec has `externalDocs`, a **View documentation** button appears in the navbar.

## Health & Readiness

- `GET /api/health` — liveness probe, always returns `200`
- `GET /api/ready` — readiness probe, returns `200` when search index is built, `503` otherwise
6 changes: 3 additions & 3 deletions packages/chronicle/src/lib/route-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe('resolveRoute — root', () => {
expect(resolveRoute('/', singleContent())).toEqual({
type: RouteType.Redirect,
to: '/docs',
status: 302,
status: 307,
})
})

Expand All @@ -75,15 +75,15 @@ describe('resolveRoute — root', () => {
expect(resolveRoute('/', multiContentNoLanding())).toEqual({
type: RouteType.Redirect,
to: '/docs',
status: 302,
status: 307,
})
})

test('redirects single-content version root to /<v>/<dir>', () => {
expect(resolveRoute('/v2', versioned())).toEqual({
type: RouteType.Redirect,
to: '/v2/docs',
status: 302,
status: 307,
})
})

Expand Down
14 changes: 12 additions & 2 deletions packages/chronicle/src/lib/route-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { StatusCodes } from 'http-status-codes'
import type { ChronicleConfig } from '@/types'
import { getLatestContentRoots, getVersionContentRoots } from './config'
import { type VersionContext, resolveVersionFromUrl } from './version-source'
Expand All @@ -13,7 +14,7 @@ export const RouteType = {
export type RouteType = (typeof RouteType)[keyof typeof RouteType]

export type Route =
| { type: typeof RouteType.Redirect; to: string; status: 302 }
| { type: typeof RouteType.Redirect; to: string; status: StatusCodes.TEMPORARY_REDIRECT | StatusCodes.PERMANENT_REDIRECT }
| { type: typeof RouteType.DocsIndex; version: VersionContext }
| { type: typeof RouteType.DocsPage; version: VersionContext; slug: string[] }
| { type: typeof RouteType.ApiIndex; version: VersionContext }
Expand Down Expand Up @@ -45,6 +46,15 @@ export function resolveRoute(
pathname: string,
config: ChronicleConfig,
): Route {
const redirect = config.redirects?.find((r) => r.from === pathname)
if (redirect) {
return {
type: RouteType.Redirect,
to: redirect.to,
status: redirect.permanent ? StatusCodes.PERMANENT_REDIRECT : StatusCodes.TEMPORARY_REDIRECT,
}
}

const parts = pathname.split('/').filter(Boolean)
const version = resolveVersionFromUrl(pathname, config)
const remainder =
Expand All @@ -65,7 +75,7 @@ export function resolveRoute(
return {
type: RouteType.Redirect,
to: `${version.urlPrefix}/${dirs[0]}`,
status: 302,
status: StatusCodes.TEMPORARY_REDIRECT,
}
}

Expand Down
8 changes: 8 additions & 0 deletions packages/chronicle/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ const RESERVED_ROUTE_SEGMENTS = [
'sitemap.xml',
] as const

const redirectSchema = z.object({
from: z.string(),
to: z.string(),
permanent: z.boolean().optional(),
})

export const chronicleConfigSchema = z
.object({
site: siteSchema,
Expand All @@ -144,6 +150,7 @@ export const chronicleConfigSchema = z
navigation: navigationSchema.optional(),
search: searchSchema.optional(),
api: z.array(apiSchema).optional(),
redirects: z.array(redirectSchema).optional(),
analytics: analyticsSchema.optional(),
telemetry: telemetrySchema.optional(),
})
Expand Down Expand Up @@ -225,6 +232,7 @@ export type SocialLink = z.infer<typeof socialLinkSchema>
export type SearchConfig = z.infer<typeof searchSchema>
export type ApiConfig = z.infer<typeof apiSchema>
export type ApiServerConfig = z.infer<typeof apiServerSchema>
export type RedirectConfig = z.infer<typeof redirectSchema>
export type ApiAuthConfig = z.infer<typeof apiAuthSchema>
export type AnalyticsConfig = z.infer<typeof analyticsSchema>
export type GoogleAnalyticsConfig = z.infer<typeof googleAnalyticsSchema>
Expand Down
Loading