Skip to content

Commit

Permalink
Add extend options to docs and i18n schemas (#1162)
Browse files Browse the repository at this point in the history
  • Loading branch information
delucis authored Nov 29, 2023
1 parent 7c0b8cb commit 00d101b
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 98 deletions.
5 changes: 5 additions & 0 deletions .changeset/short-toes-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/starlight': minor
---

Adds support for extending Starlight’s content collection schemas
16 changes: 16 additions & 0 deletions docs/src/content/docs/guides/authoring-content.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ Starlight supports the full range of [Markdown](https://daringfireball.net/proje

Please be sure to check the [MDX docs](https://mdxjs.com/docs/what-is-mdx/#markdown) or [Markdoc docs](https://markdoc.dev/docs/syntax) if using those file formats, as Markdown support and usage can differ.

## Frontmatter

You can customize individual pages in Starlight by setting values in their frontmatter.
Frontmatter is set at the top of your files between `---` separators:

```md title="src/content/docs/example.md"
---
title: My page title
---

Page content follows the second `---`.
```

Every page must include at least a `title`.
See the [frontmatter reference](/reference/frontmatter/) for all available fields and how to add custom fields.

## Inline styles

Text can be **bold**, _italic_, or ~~strikethrough~~.
Expand Down
25 changes: 25 additions & 0 deletions docs/src/content/docs/guides/i18n.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,28 @@ You can provide translations for additional languages you support — or overrid
"pagefind.searching": "Searching for [SEARCH_TERM]..."
}
```

### Extend translation schema

Add custom keys to your site’s translation dictionaries by setting `extend` in the `i18nSchema()` options.
In the following example, a new, optional `custom.label` key is added to the default keys:

```diff lang="js"
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';

export const collections = {
docs: defineCollection({ schema: docsSchema() }),
i18n: defineCollection({
type: 'data',
schema: i18nSchema({
+ extend: z.object({
+ 'custom.label': z.string().optional(),
+ }),
}),
}),
};
```

Learn more about content collection schemas in [“Defining a collection schema”](https://docs.astro.build/en/guides/content-collections/#defining-a-collection-schema) in the Astro docs.
68 changes: 68 additions & 0 deletions docs/src/content/docs/reference/frontmatter.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,3 +370,71 @@ sidebar:
target: _blank
---
```

## Customize frontmatter schema

The frontmatter schema for Starlight’s `docs` content collection is configured in `src/content/config.ts` using the `docsSchema()` helper:

```ts {3,6}
// src/content/config.ts
import { defineCollection } from 'astro:content';
import { docsSchema } from '@astrojs/starlight/schema';

export const collections = {
docs: defineCollection({ schema: docsSchema() }),
};
```

Learn more about content collection schemas in [“Defining a collection schema”](https://docs.astro.build/en/guides/content-collections/#defining-a-collection-schema) in the Astro docs.

`docsSchema()` takes the following options:

### `extend`

**type:** Zod schema or function that returns a Zod schema
**default:** `z.object({})`

Extend Starlight’s schema with additional fields by setting `extend` in the `docsSchema()` options.
The value should be a [Zod schema](https://docs.astro.build/en/guides/content-collections/#defining-datatypes-with-zod).

In the following example, we provide a stricter type for `description` to make it required and add a new optional `category` field:

```ts {8-13}
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
import { docsSchema } from '@astrojs/starlight/schema';

export const collections = {
docs: defineCollection({
schema: docsSchema({
extend: z.object({
// Make a built-in field required instead of optional.
description: z.string(),
// Add a new field to the schema.
category: z.enum(['tutorial', 'guide', 'reference']).optional(),
}),
}),
}),
};
```

To take advantage of the [Astro `image()` helper](https://docs.astro.build/en/guides/images/#images-in-content-collections), use a function that returns your schema extension:

```ts {8-13}
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
import { docsSchema } from '@astrojs/starlight/schema';

export const collections = {
docs: defineCollection({
schema: docsSchema({
extend: ({ image }) => {
return z.object({
// Add a field that must resolve to a local image.
cover: image(),
});
},
}),
}),
};
```
245 changes: 149 additions & 96 deletions packages/starlight/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,100 +8,153 @@ import { HeroSchema } from './schemas/hero';
import { SidebarLinkItemHTMLAttributesSchema } from './schemas/sidebar';
export { i18nSchema } from './schemas/i18n';

export function docsSchema() {
return (context: SchemaContext) =>
z.object({
/** The title of the current page. Required. */
title: z.string(),

/**
* A short description of the current page’s content. Optional, but recommended.
* A good description is 150–160 characters long and outlines the key content
* of the page in a clear and engaging way.
*/
description: z.string().optional(),

/**
* Custom URL where a reader can edit this page.
* Overrides the `editLink.baseUrl` global config if set.
*
* Can also be set to `false` to disable showing an edit link on this page.
*/
editUrl: z.union([z.string().url(), z.boolean()]).optional().default(true),

/** Set custom `<head>` tags just for this page. */
head: HeadConfigSchema(),

/** Override global table of contents configuration for this page. */
tableOfContents: TableOfContentsSchema().optional(),

/**
* Set the layout style for this page.
* Can be `'doc'` (the default) or `'splash'` for a wider layout without any sidebars.
*/
template: z.enum(['doc', 'splash']).default('doc'),

/** Display a hero section on this page. */
hero: HeroSchema(context).optional(),

/**
* The last update date of the current page.
* Overrides the `lastUpdated` global config or the date generated from the Git history.
*/
lastUpdated: z.union([z.date(), z.boolean()]).optional(),

/**
* The previous navigation link configuration.
* Overrides the `pagination` global config or the link text and/or URL.
*/
prev: PrevNextLinkConfigSchema(),
/**
* The next navigation link configuration.
* Overrides the `pagination` global config or the link text and/or URL.
*/
next: PrevNextLinkConfigSchema(),

sidebar: z
.object({
/**
* The order of this page in the navigation.
* Pages are sorted by this value in ascending order. Then by slug.
* If not provided, pages will be sorted alphabetically by slug.
* If two pages have the same order value, they will be sorted alphabetically by slug.
*/
order: z.number().optional(),

/**
* The label for this page in the navigation.
* Defaults to the page `title` if not set.
*/
label: z.string().optional(),

/**
* Prevents this page from being included in autogenerated sidebar groups.
*/
hidden: z.boolean().default(false),
/**
* Adds a badge to the sidebar link.
* Can be a string or an object with a variant and text.
* Variants include 'note', 'tip', 'caution', 'danger', 'success', and 'default'.
* Passing only a string defaults to the 'default' variant which uses the site accent color.
*/
badge: BadgeConfigSchema(),
/** HTML attributes to add to the sidebar link. */
attrs: SidebarLinkItemHTMLAttributesSchema(),
})
.default({}),

/** Display an announcement banner at the top of this page. */
banner: z
.object({
/** The content of the banner. Supports HTML syntax. */
content: z.string(),
})
.optional(),

/** Pagefind indexing for this page - set to false to disable. */
pagefind: z.boolean().default(true),
});
/** Default content collection schema for Starlight’s `docs` collection. */
const StarlightFrontmatterSchema = (context: SchemaContext) =>
z.object({
/** The title of the current page. Required. */
title: z.string(),

/**
* A short description of the current page’s content. Optional, but recommended.
* A good description is 150–160 characters long and outlines the key content
* of the page in a clear and engaging way.
*/
description: z.string().optional(),

/**
* Custom URL where a reader can edit this page.
* Overrides the `editLink.baseUrl` global config if set.
*
* Can also be set to `false` to disable showing an edit link on this page.
*/
editUrl: z.union([z.string().url(), z.boolean()]).optional().default(true),

/** Set custom `<head>` tags just for this page. */
head: HeadConfigSchema(),

/** Override global table of contents configuration for this page. */
tableOfContents: TableOfContentsSchema().optional(),

/**
* Set the layout style for this page.
* Can be `'doc'` (the default) or `'splash'` for a wider layout without any sidebars.
*/
template: z.enum(['doc', 'splash']).default('doc'),

/** Display a hero section on this page. */
hero: HeroSchema(context).optional(),

/**
* The last update date of the current page.
* Overrides the `lastUpdated` global config or the date generated from the Git history.
*/
lastUpdated: z.union([z.date(), z.boolean()]).optional(),

/**
* The previous navigation link configuration.
* Overrides the `pagination` global config or the link text and/or URL.
*/
prev: PrevNextLinkConfigSchema(),
/**
* The next navigation link configuration.
* Overrides the `pagination` global config or the link text and/or URL.
*/
next: PrevNextLinkConfigSchema(),

sidebar: z
.object({
/**
* The order of this page in the navigation.
* Pages are sorted by this value in ascending order. Then by slug.
* If not provided, pages will be sorted alphabetically by slug.
* If two pages have the same order value, they will be sorted alphabetically by slug.
*/
order: z.number().optional(),

/**
* The label for this page in the navigation.
* Defaults to the page `title` if not set.
*/
label: z.string().optional(),

/**
* Prevents this page from being included in autogenerated sidebar groups.
*/
hidden: z.boolean().default(false),
/**
* Adds a badge to the sidebar link.
* Can be a string or an object with a variant and text.
* Variants include 'note', 'tip', 'caution', 'danger', 'success', and 'default'.
* Passing only a string defaults to the 'default' variant which uses the site accent color.
*/
badge: BadgeConfigSchema(),
/** HTML attributes to add to the sidebar link. */
attrs: SidebarLinkItemHTMLAttributesSchema(),
})
.default({}),

/** Display an announcement banner at the top of this page. */
banner: z
.object({
/** The content of the banner. Supports HTML syntax. */
content: z.string(),
})
.optional(),

/** Pagefind indexing for this page - set to false to disable. */
pagefind: z.boolean().default(true),
});
/** Type of Starlight’s default frontmatter schema. */
type DefaultSchema = ReturnType<typeof StarlightFrontmatterSchema>;

/** Plain object, union, and intersection Zod types. */
type BaseSchemaWithoutEffects =
| z.AnyZodObject
| z.ZodUnion<[BaseSchemaWithoutEffects, ...BaseSchemaWithoutEffects[]]>
| z.ZodDiscriminatedUnion<string, z.AnyZodObject[]>
| z.ZodIntersection<BaseSchemaWithoutEffects, BaseSchemaWithoutEffects>;
/** Base subset of Zod types that we support passing to the `extend` option. */
type BaseSchema = BaseSchemaWithoutEffects | z.ZodEffects<BaseSchemaWithoutEffects>;

/** Type that extends Starlight’s default schema with an optional, user-defined schema. */
type ExtendedSchema<T extends BaseSchema> = T extends BaseSchema
? z.ZodIntersection<DefaultSchema, T>
: DefaultSchema;

interface DocsSchemaOpts<T extends BaseSchema> {
/**
* Extend Starlight’s schema with additional fields.
*
* @example
* // Extend the built-in schema with a Zod schema.
* docsSchema({
* extend: z.object({
* // Add a new field to the schema.
* category: z.enum(['tutorial', 'guide', 'reference']).optional(),
* }),
* })
*
* // Use the Astro image helper.
* docsSchema({
* extend: ({ image }) => {
* return z.object({
* cover: image(),
* });
* },
* })
*/
extend?: T | ((context: SchemaContext) => T);
}

/** Content collection schema for Starlight’s `docs` collection. */
export function docsSchema<T extends BaseSchema>({ extend }: DocsSchemaOpts<T> = {}) {
return (context: SchemaContext): ExtendedSchema<T> => {
const UserSchema = typeof extend === 'function' ? extend(context) : extend;

return (
UserSchema
? StarlightFrontmatterSchema(context).and(UserSchema)
: StarlightFrontmatterSchema(context)
) as ExtendedSchema<T>;
};
}
Loading

0 comments on commit 00d101b

Please sign in to comment.