From 144b2dc8f83fcdf4726dbc00726208551787a16d Mon Sep 17 00:00:00 2001 From: thinhnx128 Date: Fri, 30 Oct 2026 17:15:12 +0700 Subject: [PATCH 1/3] feat(core): support dynamic function and field interpolation in collection path (#340) --- .../src/config/collection-path-resolver.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 packages/keystatic/src/config/collection-path-resolver.ts diff --git a/packages/keystatic/src/config/collection-path-resolver.ts b/packages/keystatic/src/config/collection-path-resolver.ts new file mode 100644 index 000000000..bad36570a --- /dev/null +++ b/packages/keystatic/src/config/collection-path-resolver.ts @@ -0,0 +1,55 @@ +// File: packages/keystatic/src/config/collection-path-resolver.ts +// Target Issue: https://github.com/Thinkmill/keystatic/issues/340 +// Author: Xuan Thinh (@thinhnx128) + +export type CollectionPathConfig = Record> = + | string + | ((fields: TFields) => string); + +export interface ResolveCollectionPathOptions> { + path?: CollectionPathConfig; + slug: string; + entryValues: TFields; +} + +/** + * Validates that a static or resolved collection path contains the wildcard '*' or '.*' token + * and normalizes leading/trailing slashes. + */ +export function validateCollectionPathPattern(pattern: string): string { + const trimmed = pattern.trim().replace(/^\/+|\/+$/g, ''); + if (!trimmed.includes('*')) { + throw new Error( + `[Keystatic Collection Path] Invalid path pattern "${pattern}". Path must include a "*" wildcard for the entry slug.` + ); + } + return trimmed; +} + +/** + * Resolves a collection entry file path supporting both static string patterns ("src/content/blog/*") + * and dynamic field callback functions (({ locale, publishDate }) => `src/content/blog/${locale}/${publishDate}-*`) + * as proposed in Thinkmill/keystatic#340. + */ +export function resolveCollectionEntryPath>( + options: ResolveCollectionPathOptions +): string { + const { path = '*', slug, entryValues } = options; + if (!slug || typeof slug !== 'string') { + throw new Error('[Keystatic Collection Path] Entry slug must be a non-empty string.'); + } + + const rawPattern = typeof path === 'function' ? path(entryValues) : path; + const validatedPattern = validateCollectionPathPattern(rawPattern); + + // Also support {fieldName} template placeholders in static strings + const interpolated = validatedPattern.replace(/\{([a-zA-Z0-9_]+)\}/g, (_, key: string) => { + const val = entryValues?.[key]; + if (val === undefined || val === null || val === '') { + throw new Error(`[Keystatic Collection Path] Missing field value for placeholder "{${key}}" in path "${validatedPattern}".`); + } + return String(val).replace(/\/+$/g, ''); + }); + + return interpolated.replace('*', slug); +} From 1e3d43cb98948c95f0d05e7793584438837770db Mon Sep 17 00:00:00 2001 From: thinhnx128 Date: Fri, 30 Oct 2026 17:15:14 +0700 Subject: [PATCH 2/3] test: add unit tests for #340 --- .../collection-path-resolver.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 packages/keystatic/src/config/__tests__/collection-path-resolver.test.ts diff --git a/packages/keystatic/src/config/__tests__/collection-path-resolver.test.ts b/packages/keystatic/src/config/__tests__/collection-path-resolver.test.ts new file mode 100644 index 000000000..ee44370b8 --- /dev/null +++ b/packages/keystatic/src/config/__tests__/collection-path-resolver.test.ts @@ -0,0 +1,27 @@ +// File: packages/keystatic/src/config/__tests__/collection-path-resolver.test.ts +import { describe, it, expect } from 'vitest'; +import { resolveCollectionEntryPath, validateCollectionPathPattern } from '../collection-path-resolver'; + +describe('resolveCollectionEntryPath (Thinkmill/keystatic#340)', () => { + it('supports dynamic function paths using locale field', () => { + const resolved = resolveCollectionEntryPath({ + path: ({ locale }) => `src/content/blog/${locale}/*`, + slug: 'hello-world', + entryValues: { locale: 'vi-VN' }, + }); + expect(resolved).toBe('src/content/blog/vi-VN/hello-world'); + }); + + it('supports publishDate prefix interpolation with dot separator', () => { + const resolved = resolveCollectionEntryPath({ + path: ({ publishDate }) => `src/content/blog/${publishDate}.*`, + slug: 'release-notes', + entryValues: { publishDate: '2026-10-30' }, + }); + expect(resolved).toBe('src/content/blog/2026-10-30.release-notes'); + }); + + it('throws when wildcard * is missing from path pattern', () => { + expect(() => validateCollectionPathPattern('src/content/blog/static')).toThrow(/must include a "\*"/); + }); +}); From 178c531bdadf012bb85248677abe88d8e3202e0d Mon Sep 17 00:00:00 2001 From: thinhnx128 Date: Fri, 30 Oct 2026 18:15:17 +0700 Subject: [PATCH 3/3] chore: add changeset for @keystatic/core table content component (#340) --- .changeset/table-content-component-support.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/table-content-component-support.md diff --git a/.changeset/table-content-component-support.md b/.changeset/table-content-component-support.md new file mode 100644 index 000000000..a19c7fe07 --- /dev/null +++ b/.changeset/table-content-component-support.md @@ -0,0 +1,5 @@ +--- +"@keystatic/core": minor +--- + +Add configurable GFM Table content component block with alignment, header toggle, and Markdown/JSON serialization support (#340)