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
33 changes: 33 additions & 0 deletions docs/content/docs/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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, shown on the author's page | — |
| `email` | `string` | Contact address, shown on the author's page 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.
Expand Down
20 changes: 16 additions & 4 deletions docs/content/docs/frontmatter.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,22 @@ A single author can be written without the list:
authors: Jane Doe <jane@example.com>
```

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
byline links to it with `mailto:`. The avatar beside each name is drawn from the
author's initials — no image is fetched.
Authors appear as a byline on the page, in the page's `Article` structured data,
and on the generated social card. Every author also gets a page at
`/authors/<slug>` listing everything they wrote, and byline names link there. Any
`url` or `email` an author has is shown on that page rather than in the byline.

A byline shows two authors at most, collapsing the rest into a `+N` counter that
names them on hover. The avatar beside each name is drawn from the 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

Expand Down
7 changes: 7 additions & 0 deletions examples/basic/chronicle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions examples/basic/content/docs/components.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
title: Components
description: Live demo of Chronicle's built-in MDX components
order: 3
authors: [jane]
---

# Components
Expand Down
2 changes: 1 addition & 1 deletion examples/basic/content/docs/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Getting Started
description: Quick start guide for Chronicle
order: 1
authors:
- Jane Doe <jane@example.com>
- jane
- Sam Patel
---

Expand Down
7 changes: 7 additions & 0 deletions examples/versioned/chronicle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ site:
title: Versioned Example
description: Multi-content + multi-version sample

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: paper

Expand Down
1 change: 1 addition & 0 deletions examples/versioned/content/dev/api.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
title: API notes
order: 2
authors: [jane, Sam Patel, Ana Ruiz <ana@example.com>]
---

# Dev API notes — latest
Expand Down
2 changes: 1 addition & 1 deletion examples/versioned/content/docs/guide.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Guide
order: 2
authors: Jane Doe <jane@example.com>
authors: [jane]
---

# Guide — latest docs
Expand Down
23 changes: 21 additions & 2 deletions packages/chronicle/src/cli/commands/static-generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<void> {
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,
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.byline {
display: flex;
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: var(--rs-space-4);
Expand All @@ -21,3 +21,32 @@ a.name:hover {
color: var(--rs-color-foreground-base-primary);
text-decoration: underline;
}

.more {
font-size: var(--rs-font-size-small);
color: var(--rs-color-foreground-base-tertiary);
cursor: default;
}

/* Inherits the surrounding meta line's font, size, and color. */
.inlineByline {
display: inline-flex;
align-items: center;
gap: var(--rs-space-4);
font: inherit;
letter-spacing: inherit;
color: inherit;
}

.inlineByline .more {
font: inherit;
letter-spacing: inherit;
color: inherit;
}

.inlineByline .name {
font: inherit;
letter-spacing: inherit;
color: inherit;
text-transform: uppercase;
}
64 changes: 48 additions & 16 deletions packages/chronicle/src/components/common/author-byline.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,71 @@
'use client'

import { Avatar, getAvatarColor } from '@raystack/apsara'
import { authorInitials, parseAuthors } from '@/lib/authors'
import { Link } from 'react-router'
import type { Author } from '@/types'
import { authorInitials, resolveAuthors } from '@/lib/authors'
import { usePageContext } from '@/lib/page-context'
import { authorPageUrl } from '@/lib/route-resolver'
import styles from './author-byline.module.css'

/** Beyond this, the remainder is collapsed into a `+N` counter. */
const MAX_VISIBLE = 2

interface AuthorBylineProps {
/** Raw `authors` frontmatter entries — `Name <email>` or a bare name. */
authors?: string[]
/**
* `block` stands on its own under a page title; `inline` sits in a meta line
* beside other text and inherits its typography.
*/
variant?: 'block' | 'inline'
className?: string
}

/** Avatar-and-name byline for the authors declared in a page's frontmatter. */
export function AuthorByline({ authors, className }: AuthorBylineProps) {
const parsed = parseAuthors(authors)
/**
* The name, linked to the author's page. Their `url` and `email` are left to that
* page rather than competing with it here.
*/
function AuthorName({ author, href }: { author: Author; href: string | null }) {
if (!href) return <span className={styles.name}>{author.name}</span>
return (
<Link className={styles.name} to={href}>
{author.name}
</Link>
)
}

/** Byline for the authors declared in a page's frontmatter. */
export function AuthorByline({ authors, variant = 'block', className }: AuthorBylineProps) {
const { config, version } = usePageContext()
const parsed = resolveAuthors(authors, config)
if (parsed.length === 0) return null

const wrapper = variant === 'inline' ? styles.inlineByline : styles.byline
const shown = parsed.slice(0, MAX_VISIBLE)
const hidden = parsed.slice(MAX_VISIBLE)

return (
<div className={className ? `${styles.byline} ${className}` : styles.byline}>
{parsed.map((author, index) => (
<span className={styles.author} key={`${author.name}-${index}`}>
<span className={className ? `${wrapper} ${className}` : wrapper}>
{shown.map((author, index) => (
<span className={styles.author} key={`${author.slug}-${index}`}>
<Avatar
size={2}
radius='full'
src={author.avatar}
alt={author.avatar ? author.name : undefined}
fallback={authorInitials(author.name)}
color={getAvatarColor(author.name)}
aria-hidden='true'
aria-hidden={author.avatar ? undefined : 'true'}
/>
{author.email ? (
<a className={styles.name} href={`mailto:${author.email}`}>
{author.name}
</a>
) : (
<span className={styles.name}>{author.name}</span>
)}
<AuthorName author={author} href={authorPageUrl(author.slug, config, version)} />
</span>
))}
</div>
{hidden.length > 0 && (
<span className={styles.more} title={hidden.map(author => author.name).join(', ')}>
{`+${hidden.length}`}
</span>
)}
</span>
)
}
Loading
Loading