From ed8c1ce4c7a88084f6832c43b7c8396e4d25e3e9 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 18 Sep 2026 11:23:41 +0200 Subject: [PATCH] feat(js): support category-based page filtering in frontmatter Add `supportedCategories` / `notSupportedCategories` frontmatter fields that gate common-content inheritance by platform category, alongside the existing guide-name `supported` / `notSupported` lists. - A page with `supportedCategories` is shown only for platforms/guides whose categories match (in addition to any `supported` guide keys). - A page with `notSupportedCategories` is hidden for matching platforms/guides (in addition to any `notSupported` guide keys). This lets pages target e.g. all server SDKs with `notSupportedCategories: [browser]` instead of enumerating every framework, and auto-covers new guides. Both `isSupported` implementations (the doc-tree collector in frontmatter.ts and the search collector in mdx.ts) resolve the current platform/guide's categories and apply the new lists. Behavior is unchanged for pages that don't use the new fields. Covered by commonContentInheritance.spec.ts against both collectors. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/commonContentInheritance.spec.ts | 39 ++++++++++++++++++++++- src/frontmatter.ts | 47 ++++++++++++++++++++++------ src/mdx.ts | 47 ++++++++++++++++++++++------ src/types/frontmatter.ts | 15 +++++++++ 4 files changed, 127 insertions(+), 21 deletions(-) diff --git a/src/commonContentInheritance.spec.ts b/src/commonContentInheritance.spec.ts index bfd24766b8535..8346aa3c3bd3f 100644 --- a/src/commonContentInheritance.spec.ts +++ b/src/commonContentInheritance.spec.ts @@ -24,7 +24,14 @@ beforeAll(async () => { const platformPath = path.join(docsPath, 'platforms', 'test'); const commonPath = path.join(platformPath, 'common'); const guidesPath = path.join(platformPath, 'guides'); - const guideNames = ['index-opt-out', 'config-opt-out', 'default', 'override']; + const guideNames = [ + 'index-opt-out', + 'config-opt-out', + 'default', + 'override', + 'browser-guide', + 'server-guide', + ]; await Promise.all([ mkdir(commonPath, {recursive: true}), @@ -40,6 +47,14 @@ beforeAll(async () => { frontmatter('Versioned common index') ), writeFile(path.join(commonPath, 'common-only.mdx'), frontmatter('Common only')), + writeFile( + path.join(commonPath, 'not-on-browser.mdx'), + frontmatter('Not on browser', 'notSupportedCategories:\n - browser\n') + ), + writeFile( + path.join(commonPath, 'only-browser.mdx'), + frontmatter('Only browser', 'supportedCategories:\n - browser\n') + ), writeFile( path.join(guidesPath, 'index-opt-out', 'index.mdx'), frontmatter('Index opt-out', 'inheritCommonContent: false\n') @@ -61,6 +76,14 @@ beforeAll(async () => { frontmatter('Config opt-out own page') ), writeFile(path.join(guidesPath, 'default', 'index.mdx'), frontmatter('Default')), + writeFile( + path.join(guidesPath, 'browser-guide', 'index.mdx'), + frontmatter('Browser guide', 'categories:\n - browser\n') + ), + writeFile( + path.join(guidesPath, 'server-guide', 'index.mdx'), + frontmatter('Server guide', 'categories:\n - server\n') + ), writeFile( path.join(guidesPath, 'override', 'index.mdx'), frontmatter('Override', 'inheritCommonContent: false\n') @@ -102,6 +125,20 @@ describe.each([ expect(slugs.has('platforms/test/guides/default/common-only')).toBe(true); expect(slugs.has('platforms/test/guides/override/common-only')).toBe(true); }); + + test('filters common pages by supportedCategories/notSupportedCategories', async () => { + const slugs = new Set( + (await collectFrontmatter(docsPath)).map(({slug}) => slug.replace(/\/index$/, '')) + ); + + // notSupportedCategories: [browser] -> hidden on the browser guide, shown on server + expect(slugs.has('platforms/test/guides/browser-guide/not-on-browser')).toBe(false); + expect(slugs.has('platforms/test/guides/server-guide/not-on-browser')).toBe(true); + + // supportedCategories: [browser] -> shown only on the browser guide + expect(slugs.has('platforms/test/guides/browser-guide/only-browser')).toBe(true); + expect(slugs.has('platforms/test/guides/server-guide/only-browser')).toBe(false); + }); }); describe('getSourcePathsBySlug', () => { diff --git a/src/frontmatter.ts b/src/frontmatter.ts index 4b9a5b5512d01..1c393842cba9c 100644 --- a/src/frontmatter.ts +++ b/src/frontmatter.ts @@ -8,7 +8,7 @@ import {limitFunction} from 'p-limit'; import {apiCategories} from './build/resolveOpenAPI'; import getAllFilesRecursively from './files'; import {readGuideConfig, shouldInheritCommonContent} from './guideConfig'; -import {FrontMatter, PlatformConfig} from './types'; +import {FrontMatter, PlatformCategory, PlatformConfig} from './types'; import {isNotNil} from './utils'; import {VERSION_INDICATOR} from './versioning'; @@ -25,21 +25,34 @@ const formatSlug = (slug: string): string => slug.replace(/\.(mdx|md)$/, ''); const isSupported = ( frontmatter: FrontMatter, platformName: string, - guideName?: string + guideName?: string, + categories: PlatformCategory[] = [] ): boolean => { const canonical = guideName ? `${platformName}.${guideName}` : platformName; - if (frontmatter.supported && frontmatter.supported.length) { - if (frontmatter.supported.includes(canonical)) { + + const matchesCategory = (list?: PlatformCategory[]) => + !!list?.some(category => categories.includes(category)); + + const hasAllowlist = + !!frontmatter.supported?.length || !!frontmatter.supportedCategories?.length; + if (hasAllowlist) { + // An exact guide match always wins. + if (frontmatter.supported?.includes(canonical)) { return true; } - if (!frontmatter.supported.includes(platformName)) { + // Otherwise a platform-level or category allowlist match keeps the page, but + // still lets the notSupported lists below filter it out. + if ( + !frontmatter.supported?.includes(platformName) && + !matchesCategory(frontmatter.supportedCategories) + ) { return false; } } if ( - frontmatter.notSupported && - (frontmatter.notSupported.includes(canonical) || - frontmatter.notSupported.includes(platformName)) + frontmatter.notSupported?.includes(canonical) || + frontmatter.notSupported?.includes(platformName) || + matchesCategory(frontmatter.notSupportedCategories) ) { return false; } @@ -191,7 +204,14 @@ export async function getDocsFrontMatterFromDirectory( commonFiles.map( limitFunction( commonFile => { - if (!isSupported(commonFile.frontmatter, platformName)) { + if ( + !isSupported( + commonFile.frontmatter, + platformName, + undefined, + platformFrontmatter.categories + ) + ) { return; } @@ -270,7 +290,14 @@ export async function getDocsFrontMatterFromDirectory( commonFiles.map( limitFunction( commonFile => { - if (!isSupported(commonFile.frontmatter, platformName, guideName)) { + if ( + !isSupported( + commonFile.frontmatter, + platformName, + guideName, + guideFrontmatter.categories + ) + ) { return; } diff --git a/src/mdx.ts b/src/mdx.ts index 7ceb1f532ca15..9f61d9c722181 100644 --- a/src/mdx.ts +++ b/src/mdx.ts @@ -42,7 +42,7 @@ import remarkImageProcessing from './remark-image-processing'; import remarkImageResize from './remark-image-resize'; import remarkTocHeadings, {TocNode} from './remark-toc-headings'; import remarkVariables from './remark-variables'; -import {FrontMatter, Platform, PlatformConfig} from './types'; +import {FrontMatter, Platform, PlatformCategory, PlatformConfig} from './types'; import {isNotNil} from './utils'; import {isVersioned, stripVersion, VERSION_INDICATOR} from './versioning'; @@ -151,21 +151,34 @@ function formatSlug(slug: string) { const isSupported = ( frontmatter: FrontMatter, platformName: string, - guideName?: string + guideName?: string, + categories: PlatformCategory[] = [] ): boolean => { const canonical = guideName ? `${platformName}.${guideName}` : platformName; - if (frontmatter.supported && frontmatter.supported.length) { - if (frontmatter.supported.indexOf(canonical) !== -1) { + + const matchesCategory = (list?: PlatformCategory[]) => + !!list?.some(category => categories.includes(category)); + + const hasAllowlist = + !!frontmatter.supported?.length || !!frontmatter.supportedCategories?.length; + if (hasAllowlist) { + // An exact guide match always wins. + if (frontmatter.supported?.includes(canonical)) { return true; } - if (frontmatter.supported.indexOf(platformName) === -1) { + // Otherwise a platform-level or category allowlist match keeps the page, but + // still lets the notSupported lists below filter it out. + if ( + !frontmatter.supported?.includes(platformName) && + !matchesCategory(frontmatter.supportedCategories) + ) { return false; } } if ( - frontmatter.notSupported && - (frontmatter.notSupported.indexOf(canonical) !== -1 || - frontmatter.notSupported.indexOf(platformName) !== -1) + frontmatter.notSupported?.includes(canonical) || + frontmatter.notSupported?.includes(platformName) || + matchesCategory(frontmatter.notSupportedCategories) ) { return false; } @@ -382,7 +395,14 @@ export async function getAllFilesFrontMatter( await Promise.all( commonFiles.map(f => limit(async () => { - if (!isSupported(f.frontmatter, platformName)) { + if ( + !isSupported( + f.frontmatter, + platformName, + undefined, + platformFrontmatter.categories + ) + ) { return; } @@ -434,7 +454,14 @@ export async function getAllFilesFrontMatter( await Promise.all( commonFiles.map(f => limit(async () => { - if (!isSupported(f.frontmatter, platformName, guideName)) { + if ( + !isSupported( + f.frontmatter, + platformName, + guideName, + guideFrontmatter.categories + ) + ) { return; } diff --git a/src/types/frontmatter.ts b/src/types/frontmatter.ts index 2178b8de09a9e..f05953a980dc9 100644 --- a/src/types/frontmatter.ts +++ b/src/types/frontmatter.ts @@ -1,4 +1,5 @@ import {PaginationNavNode} from './paginationNavNode'; +import {PlatformCategory} from './platform'; /** ** a YAML-formatted blob defined at the top of every markdown or mdx file @@ -69,6 +70,13 @@ export interface FrontMatter { */ notSupported?: string[]; + /** + * Platform categories this page is not relevant to (e.g. `['server']`). + * Applied in addition to `notSupported`; a page is hidden when the current + * platform/guide matches either list. + */ + notSupportedCategories?: PlatformCategory[]; + /** * Set this to true to disable page-level table of contents rendering. */ @@ -131,6 +139,13 @@ export interface FrontMatter { * Specific guides that this page is relevant to. */ supported?: string[]; + + /** + * Platform categories this page is relevant to (e.g. `['server']`). + * Applied in addition to `supported`; when either allowlist is present, the + * page is shown only for platforms/guides matching one of them. + */ + supportedCategories?: PlatformCategory[]; /** * Available versions for this page * @example ['v7.119.0', 'next']