From 04c84fc2fbcdb4b6b7de8d628f7a2c2b2e995500 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Wed, 19 Aug 2026 15:02:50 +0530 Subject: [PATCH 1/6] feat: add author pages listing everything an author wrote Adds an optional `authors` registry to chronicle.yaml holding each author's name, bio, avatar, url, and email. A frontmatter string that matches a registry key picks up that profile; anything else still reads as a plain name, so occasional contributors need no config. /authors lists everyone found in the content, and /authors/ shows one author's profile followed by their pages grouped by content dir. The index is served from /api/authors, written to /data/authors.json for static builds, and embedded in the SSR payload so both routes render without JavaScript. A content dir named `authors` keeps its own pages. Bylines now use the registry avatar and profile link when present. Co-Authored-By: Claude Opus 5 (1M context) --- docs/content/docs/configuration.mdx | 33 ++++ docs/content/docs/frontmatter.mdx | 14 +- examples/basic/chronicle.yaml | 7 + examples/basic/content/docs/components.mdx | 1 + .../basic/content/docs/getting-started.mdx | 2 +- .../src/cli/commands/static-generate.ts | 23 ++- .../src/components/common/author-byline.tsx | 18 +- .../chronicle/src/lib/author-index.test.ts | 108 ++++++++++++ packages/chronicle/src/lib/author-index.ts | 86 +++++++++ packages/chronicle/src/lib/authors.test.ts | 87 ++++++++- packages/chronicle/src/lib/authors.ts | 50 +++++- packages/chronicle/src/lib/data-urls.ts | 4 + packages/chronicle/src/lib/page-context.tsx | 9 +- .../chronicle/src/lib/route-resolver.test.ts | 52 ++++++ packages/chronicle/src/lib/route-resolver.ts | 20 +++ .../src/pages/AuthorsPage.module.css | 155 ++++++++++++++++ packages/chronicle/src/pages/AuthorsPage.tsx | 165 ++++++++++++++++++ packages/chronicle/src/pages/DocsPage.tsx | 4 +- packages/chronicle/src/server/App.tsx | 12 ++ packages/chronicle/src/server/api/authors.ts | 16 ++ .../chronicle/src/server/entry-client.tsx | 4 + .../chronicle/src/server/entry-server.tsx | 23 ++- packages/chronicle/src/types/config.ts | 10 ++ packages/chronicle/src/types/content.ts | 10 +- 24 files changed, 878 insertions(+), 35 deletions(-) create mode 100644 packages/chronicle/src/lib/author-index.test.ts create mode 100644 packages/chronicle/src/lib/author-index.ts create mode 100644 packages/chronicle/src/pages/AuthorsPage.module.css create mode 100644 packages/chronicle/src/pages/AuthorsPage.tsx create mode 100644 packages/chronicle/src/server/api/authors.ts diff --git a/docs/content/docs/configuration.mdx b/docs/content/docs/configuration.mdx index 7b5352d9..cf580974 100644 --- a/docs/content/docs/configuration.mdx +++ b/docs/content/docs/configuration.mdx @@ -363,6 +363,39 @@ analytics: | `enabled` | `boolean` | Enable/disable analytics | `false` | | `googleAnalytics.measurementId` | `string` | Google Analytics measurement ID | — | +### authors + +Optional registry of the people who write your docs. Pages reference an entry by +its key in their `authors` frontmatter, so the details live in one place. + +```yaml +authors: + jane: + name: Jane Doe + bio: Writes about distributed systems. + avatar: /team/jane.png + url: https://github.com/jane + email: jane@example.com +``` + +| Field | Type | Description | Default | +|-------|------|-------------|---------| +| `name` | `string` | **Required.** Display name | — | +| `bio` | `string` | Short description shown on the author's page | — | +| `avatar` | `string` | Image path for the avatar; initials are used without it | — | +| `url` | `string` | Profile link — the byline links here in preference to the email | — | +| `email` | `string` | Contact address, linked with `mailto:` | — | + +```yaml +# page.mdx frontmatter +authors: [jane] +``` + +A frontmatter string that matches no key is still valid — it renders as a plain +name, so occasional contributors need no registry entry. See +[Frontmatter](/docs/frontmatter) for the field itself, and browse the people +writing a site at `/authors`. + ### telemetry Prometheus metrics export via OpenTelemetry. Served on a separate port. diff --git a/docs/content/docs/frontmatter.mdx b/docs/content/docs/frontmatter.mdx index 2b7e029d..66073b2a 100644 --- a/docs/content/docs/frontmatter.mdx +++ b/docs/content/docs/frontmatter.mdx @@ -106,9 +106,19 @@ authors: Jane Doe ``` Authors appear as a byline under the page title, in the page's `Article` -structured data, and on the generated social card. When an email is given, the +structured data, and on the generated social card. Every author also gets a page +at `/authors/` listing everything they wrote, linked from the `/authors` +index. When an email is given, the byline links to it with `mailto:`. The avatar beside each name is drawn from the -author's initials — no image is fetched. +author's initials unless the author has an `avatar` in the +[authors registry](/docs/configuration). + +Registry keys work here too, and bring the author's bio, avatar, and profile +link along with them: + +```yaml +authors: [jane] +``` ## Navigation Ordering diff --git a/examples/basic/chronicle.yaml b/examples/basic/chronicle.yaml index b56727ea..be890c65 100644 --- a/examples/basic/chronicle.yaml +++ b/examples/basic/chronicle.yaml @@ -13,6 +13,13 @@ content: label: Docs icon: /icons/docs.svg +authors: + jane: + name: Jane Doe + bio: Writes about the parts of the system nobody else wants to document. + url: https://github.com/jane + email: jane@example.com + theme: name: default diff --git a/examples/basic/content/docs/components.mdx b/examples/basic/content/docs/components.mdx index cfa63945..94d62759 100644 --- a/examples/basic/content/docs/components.mdx +++ b/examples/basic/content/docs/components.mdx @@ -2,6 +2,7 @@ title: Components description: Live demo of Chronicle's built-in MDX components order: 3 +authors: [jane] --- # Components diff --git a/examples/basic/content/docs/getting-started.mdx b/examples/basic/content/docs/getting-started.mdx index 5da77c9e..69f16e5a 100644 --- a/examples/basic/content/docs/getting-started.mdx +++ b/examples/basic/content/docs/getting-started.mdx @@ -3,7 +3,7 @@ title: Getting Started description: Quick start guide for Chronicle order: 1 authors: - - Jane Doe + - jane - Sam Patel --- diff --git a/packages/chronicle/src/cli/commands/static-generate.ts b/packages/chronicle/src/cli/commands/static-generate.ts index 09d20a99..10760b2b 100644 --- a/packages/chronicle/src/cli/commands/static-generate.ts +++ b/packages/chronicle/src/cli/commands/static-generate.ts @@ -24,7 +24,8 @@ import { isAnimatedImage } from '@/lib/image-animation'; import { getAssetVersion } from '@/lib/asset-version'; import type { VersionContext } from '@/lib/version-source'; import type { Frontmatter, PageNavLink } from '@/types'; -import { normalizeAuthorList, parseAuthors } from '@/lib/authors'; +import { buildAuthorIndex } from '@/lib/author-index'; +import { normalizeAuthorList, resolveAuthors } from '@/lib/authors'; export interface StaticGenerateOptions { projectRoot: string; @@ -439,6 +440,21 @@ async function generatePageDataFiles( } } +/** `/data/authors.json` — what `/api/authors` serves in server mode. */ +async function generateAuthorsData( + pages: ScannedPage[], + config: ChronicleConfig, + outputDir: string, +): Promise { + const index = buildAuthorIndex( + pages.map(page => ({ url: page.url, frontmatter: page.frontmatter })), + config, + ); + const dataDir = path.join(outputDir, 'data'); + await fs.mkdir(dataDir, { recursive: true }); + await fs.writeFile(path.join(dataDir, 'authors.json'), JSON.stringify(index)); +} + async function generateSearchIndex( pages: ScannedPage[], config: ChronicleConfig, @@ -633,7 +649,7 @@ async function generateOgImages( for (const page of pages) { const title = page.frontmatter.title; const description = page.frontmatter.description ?? ''; - const authors = parseAuthors(page.frontmatter.authors) + const authors = resolveAuthors(page.frontmatter.authors, config) .map(author => author.name) .join(', '); const slugKey = page.slugs.join(',') || 'index'; @@ -1054,6 +1070,9 @@ export async function generateStaticSite(options: StaticGenerateOptions): Promis console.log(chalk.gray(' Generating page data files...')); await generatePageDataFiles(pages, navMap, outputDir); + console.log(chalk.gray(' Generating authors data...')); + await generateAuthorsData(pages, config, outputDir); + console.log(chalk.gray(' Generating search index...')); await generateSearchIndex(pages, config, outputDir, projectRoot); diff --git a/packages/chronicle/src/components/common/author-byline.tsx b/packages/chronicle/src/components/common/author-byline.tsx index 64d73105..edd82dd4 100644 --- a/packages/chronicle/src/components/common/author-byline.tsx +++ b/packages/chronicle/src/components/common/author-byline.tsx @@ -1,7 +1,8 @@ 'use client' import { Avatar, getAvatarColor } from '@raystack/apsara' -import { authorInitials, parseAuthors } from '@/lib/authors' +import { authorInitials, resolveAuthors } from '@/lib/authors' +import { usePageContext } from '@/lib/page-context' import styles from './author-byline.module.css' interface AuthorBylineProps { @@ -12,20 +13,27 @@ interface AuthorBylineProps { /** Avatar-and-name byline for the authors declared in a page's frontmatter. */ export function AuthorByline({ authors, className }: AuthorBylineProps) { - const parsed = parseAuthors(authors) + const { config } = usePageContext() + const parsed = resolveAuthors(authors, config) if (parsed.length === 0) return null return (
{parsed.map((author, index) => ( - +