From b3be08ffd28056182f3d01b77cfe2d4bb9cdb55d Mon Sep 17 00:00:00 2001 From: Sameer Pasha Date: Sat, 16 May 2026 21:44:27 +0530 Subject: [PATCH 1/2] feat: add 'created' metadata to front matter for docs plugin (AI-assisted) Resolves #5691 - Add created field to DocFrontMatter type (accepts a date string or Date object) - Add FrontMatterCreatedSchema in docusaurus-utils-validation - Export the new schema from the validation package index - Validate the created field in the docs plugin frontMatter schema - Parse the created front matter into a createdAt millisecond timestamp on DocMetadataBase, following the same nullable semantics as lastUpdatedAt - Add comprehensive tests for the new created front matter field --- .../src/__tests__/frontMatter.test.ts | 32 +++++++++++++++++++ .../src/docs.ts | 12 +++++++ .../src/frontMatter.ts | 2 ++ .../src/plugin-content-docs.d.ts | 13 ++++++++ .../docusaurus-utils-validation/src/index.ts | 2 ++ .../src/validationSchemas.ts | 7 ++++ 6 files changed, 68 insertions(+) diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/frontMatter.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/frontMatter.test.ts index 5a166fc071f3..35d555ddf50f 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/frontMatter.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/frontMatter.test.ts @@ -492,3 +492,35 @@ describe('validateDocFrontMatter last_update', () => { ], }); }); + +describe('validateDocFrontMatter created', () => { + testField({ + prefix: 'created', + validFrontMatters: [ + {created: undefined}, + {created: '1/1/2000'}, + {created: new Date('1/1/2000')}, + {created: '2024-01-15'}, + {created: '1995-12-17T03:24:00'}, + {created: 'December 17, 1995 03:24:00'}, + ], + invalidFrontMatters: [ + [ + {created: 'I am not a date :('}, + 'does not look like a valid created date', + ], + [ + {created: '2011-10-45'}, + 'does not look like a valid created date', + ], + [ + {created: '2011-0-10'}, + 'does not look like a valid created date', + ], + [ + {created: ''}, + 'does not look like a valid created date', + ], + ], + }); +}); diff --git a/packages/docusaurus-plugin-content-docs/src/docs.ts b/packages/docusaurus-plugin-content-docs/src/docs.ts index 06704fbff1dd..555ce09e7126 100644 --- a/packages/docusaurus-plugin-content-docs/src/docs.ts +++ b/packages/docusaurus-plugin-content-docs/src/docs.ts @@ -120,6 +120,7 @@ async function doProcessDocMetadata({ // but allow to disable this behavior with front matter parse_number_prefixes: parseNumberPrefixes = true, last_update: lastUpdateFrontMatter, + created: createdFrontMatter, } = frontMatter; const lastUpdate = await readLastUpdateData( @@ -225,6 +226,16 @@ async function doProcessDocMetadata({ // NodeJS optimization. // Adding properties to object after instantiation will cause hidden // class transitions. + + // Parse the created front matter value into a millisecond timestamp + const createdAt: number | null | undefined = + createdFrontMatter !== undefined + ? (() => { + const parsed = new Date(createdFrontMatter).getTime(); + return Number.isNaN(parsed) ? null : parsed; + })() + : undefined; + return { id, title, @@ -240,6 +251,7 @@ async function doProcessDocMetadata({ version: versionMetadata.versionName, lastUpdatedBy: lastUpdate.lastUpdatedBy, lastUpdatedAt: lastUpdate.lastUpdatedAt, + createdAt, sidebarPosition, frontMatter, }; diff --git a/packages/docusaurus-plugin-content-docs/src/frontMatter.ts b/packages/docusaurus-plugin-content-docs/src/frontMatter.ts index 091b7aef9cf2..8722b3062235 100644 --- a/packages/docusaurus-plugin-content-docs/src/frontMatter.ts +++ b/packages/docusaurus-plugin-content-docs/src/frontMatter.ts @@ -14,6 +14,7 @@ import { validateFrontMatter, ContentVisibilitySchema, FrontMatterLastUpdateSchema, + FrontMatterCreatedSchema, } from '@docusaurus/utils-validation'; import type {DocFrontMatter} from '@docusaurus/plugin-content-docs'; @@ -46,6 +47,7 @@ export const DocFrontMatterSchema = Joi.object({ pagination_prev: Joi.string().allow(null), ...FrontMatterTOCHeadingLevels, last_update: FrontMatterLastUpdateSchema, + created: FrontMatterCreatedSchema, }) .unknown() .concat(ContentVisibilitySchema); diff --git a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts index f89b3f63169e..fd657ee129ec 100644 --- a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts +++ b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts @@ -409,6 +409,12 @@ declare module '@docusaurus/plugin-content-docs' { unlisted?: boolean; /** Allows overriding the last updated author and/or date. */ last_update?: FrontMatterLastUpdate; + /** + * The creation date of the document. Can be any + * [parsable date string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse). + * @see https://github.com/facebook/docusaurus/issues/5691 + */ + created?: Date | string; }; export type DocMetadataBase = LastUpdateData & { @@ -460,6 +466,13 @@ declare module '@docusaurus/plugin-content-docs' { editUrl?: string | null; /** Tags, normalized. */ tags: TagMetadata[]; + /** + * The creation timestamp of the document in milliseconds, sourced from + * the `created` front matter field. + * `undefined`: no `created` front matter provided + * `null`: value could not be parsed + */ + createdAt?: number | null; /** Front matter, as-is. */ frontMatter: DocFrontMatter & {[key: string]: unknown}; }; diff --git a/packages/docusaurus-utils-validation/src/index.ts b/packages/docusaurus-utils-validation/src/index.ts index cbf2ce14bb1f..0f0d0263b465 100644 --- a/packages/docusaurus-utils-validation/src/index.ts +++ b/packages/docusaurus-utils-validation/src/index.ts @@ -29,5 +29,7 @@ export { ContentVisibilitySchema, FrontMatterLastUpdateErrorMessage, FrontMatterLastUpdateSchema, + FrontMatterCreatedErrorMessage, + FrontMatterCreatedSchema, } from './validationSchemas'; export {getTagsFilePathsToWatch, getTagsFile} from './tagsFile'; diff --git a/packages/docusaurus-utils-validation/src/validationSchemas.ts b/packages/docusaurus-utils-validation/src/validationSchemas.ts index f1c79abcf34f..96a56a6169f0 100644 --- a/packages/docusaurus-utils-validation/src/validationSchemas.ts +++ b/packages/docusaurus-utils-validation/src/validationSchemas.ts @@ -182,3 +182,10 @@ export const FrontMatterLastUpdateSchema = Joi.object({ 'object.missing': FrontMatterLastUpdateErrorMessage, 'object.base': FrontMatterLastUpdateErrorMessage, }); + +export const FrontMatterCreatedErrorMessage = + '{{#label}} does not look like a valid created date. Please use a string or Date value.'; + +export const FrontMatterCreatedSchema = Joi.date().raw().messages({ + 'date.base': FrontMatterCreatedErrorMessage, +}); From 0dc509580be73223646951bbebfc52763ba4066a Mon Sep 17 00:00:00 2001 From: Sameer Pasha Date: Sun, 17 May 2026 17:24:07 +0530 Subject: [PATCH 2/2] test: add integration tests and docs for 'created' metadata field (AI-assisted) --- .../simple-site/docs/customCreated.md | 6 ++ .../__tests__/__snapshots__/docs.test.ts.snap | 34 ++++++++++- .../src/__tests__/docs.test.ts | 57 +++++++++++++++++++ .../docs/api/plugins/plugin-content-docs.mdx | 2 + 4 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/customCreated.md diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/customCreated.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/customCreated.md new file mode 100644 index 000000000000..f5221556fb1b --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/customCreated.md @@ -0,0 +1,6 @@ +--- +title: Custom Created +created: 2024-01-15 +--- + +Custom created date diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/docs.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/docs.test.ts.snap index 573eae414932..309bacf7b404 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/docs.test.ts.snap +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/docs.test.ts.snap @@ -3,13 +3,24 @@ exports[`simple site > custom pagination - development 1`] = ` { "pagination": [ + { + "id": "customCreated", + "next": { + "permalink": "/docs/customLastUpdate", + "title": "Custom Last Update", + }, + "prev": undefined, + }, { "id": "customLastUpdate", "next": { "permalink": "/docs/doc with space", "title": "Hoo hoo, if this path tricks you...", }, - "prev": undefined, + "prev": { + "permalink": "/docs/customCreated", + "title": "Custom Created", + }, }, { "id": "doc with space", @@ -236,6 +247,10 @@ exports[`simple site > custom pagination - development 1`] = ` ], "sidebars": { "defaultSidebar": [ + { + "id": "customCreated", + "type": "doc", + }, { "id": "customLastUpdate", "type": "doc", @@ -359,13 +374,24 @@ exports[`simple site > custom pagination - development 1`] = ` exports[`simple site > custom pagination - production 1`] = ` { "pagination": [ + { + "id": "customCreated", + "next": { + "permalink": "/docs/customLastUpdate", + "title": "Custom Last Update", + }, + "prev": undefined, + }, { "id": "customLastUpdate", "next": { "permalink": "/docs/doc with space", "title": "Hoo hoo, if this path tricks you...", }, - "prev": undefined, + "prev": { + "permalink": "/docs/customCreated", + "title": "Custom Created", + }, }, { "id": "doc with space", @@ -571,6 +597,10 @@ exports[`simple site > custom pagination - production 1`] = ` ], "sidebars": { "defaultSidebar": [ + { + "id": "customCreated", + "type": "doc", + }, { "id": "customLastUpdate", "type": "doc", diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts index daff30d421e5..3bd354c1a24c 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts @@ -263,6 +263,7 @@ describe('simple site', () => { 'doc-draft.md', 'doc-unlisted.md', 'customLastUpdate.md', + 'customCreated.md', 'lastUpdateAuthorOnly.md', 'lastUpdateDateOnly.md', 'foo/bar.md', @@ -710,6 +711,62 @@ describe('simple site', () => { }); }); + it('docs with created front matter', async () => { + const {defaultTestUtils} = await loadSite(); + + await defaultTestUtils.testMeta('customCreated.md', { + version: 'current', + id: 'customCreated', + sourceDirName: '.', + permalink: '/docs/customCreated', + slug: '/customCreated', + title: 'Custom Created', + description: 'Custom created date', + frontMatter: { + title: 'Custom Created', + // YAML parses bare date values (e.g. 2024-01-15) into Date objects + created: new Date('2024-01-15'), + }, + createdAt: new Date('2024-01-15').getTime(), + sidebarPosition: undefined, + tags: [], + unlisted: false, + }); + }); + + it('docs without created front matter have undefined createdAt', async () => { + const {defaultTestUtils} = await loadSite(); + + // lorem.md has no 'created' field, so createdAt should be undefined + const metadata = await defaultTestUtils.processDocFile('lorem.md'); + expect(metadata.createdAt).toBeUndefined(); + }); + + it('docs with created front matter using processDocFile directly', async () => { + const {defaultTestUtils} = await loadSite(); + + // Use a fake doc file to test various created date formats + const isoDateDoc = createFakeDocFile({ + source: 'isoDate.md', + frontMatter: {created: '2023-06-15T10:30:00'}, + }); + const isoMeta = await defaultTestUtils.processDocFile(isoDateDoc); + expect(isoMeta.createdAt).toBe(new Date('2023-06-15T10:30:00').getTime()); + + const plainDateDoc = createFakeDocFile({ + source: 'plainDate.md', + frontMatter: {created: '1/1/2000'}, + }); + const plainMeta = await defaultTestUtils.processDocFile(plainDateDoc); + expect(plainMeta.createdAt).toBe(new Date('1/1/2000').getTime()); + + const noCreatedDoc = createFakeDocFile({ + source: 'noCreated.md', + }); + const noCreatedMeta = await defaultTestUtils.processDocFile(noCreatedDoc); + expect(noCreatedMeta.createdAt).toBeUndefined(); + }); + it('docs with last_update front matter disabled', async () => { const {siteDir, context, options, currentVersion, createTestUtilsPartial} = await loadSite({ diff --git a/website/docs/api/plugins/plugin-content-docs.mdx b/website/docs/api/plugins/plugin-content-docs.mdx index 38ef345599e5..f227df44dc56 100644 --- a/website/docs/api/plugins/plugin-content-docs.mdx +++ b/website/docs/api/plugins/plugin-content-docs.mdx @@ -301,6 +301,7 @@ Accepted fields: | `draft` | `boolean` | `false` | Draft documents will only be available during development. | | `unlisted` | `boolean` | `false` | Unlisted documents will be available in both development and production. They will be "hidden" in production, not indexed, excluded from sitemaps, and can only be accessed by users having a direct link. | | `last_update` | `FrontMatterLastUpdate` | `undefined` | Allows overriding the last update author/date. Date can be any [parsable date string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse). | +| `created` | `Date \| string` | `undefined` | The creation date of the document. Can be any [parsable date string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse). Exposed as `createdAt` (milliseconds timestamp) in the doc metadata. | ```mdx-code-block @@ -331,6 +332,7 @@ keywords: tags: [docusaurus] image: https://i.imgur.com/mErPwqL.png slug: /myDoc +created: 2024-01-15 last_update: date: 1/1/2000 author: custom author name