diff --git a/cli/cli.ts b/cli/cli.ts index 537565b..7745d93 100755 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -2,7 +2,7 @@ /* eslint-disable no-console */ import { Command } from 'commander' import { build, dev, preview, sync } from 'astro' -import { join, resolve } from 'path' +import { dirname, join, resolve } from 'path' import { createCollectionContent } from './createCollectionContent.js' import { setFsRootDir } from './setFsRootDir.js' import { createConfigFile } from './createConfigFile.js' @@ -13,6 +13,7 @@ import { buildPropsData } from './buildPropsData.js' import { hasFile } from './hasFile.js' import { convertToMDX } from './convertToMDX.js' import { mkdir, copyFile } from 'fs/promises' +import { fileURLToPath } from 'url' import { fileExists } from './fileExists.js' const currentDir = process.cwd() @@ -34,8 +35,11 @@ try { .replace('file://', '') } catch (e: any) { if (e.code === 'ERR_MODULE_NOT_FOUND') { - console.log('@patternfly/patternfly-doc-core not found, using current directory as astroRoot') - astroRoot = process.cwd() + // When running from the doc-core package itself (e.g. via portal: link), + // derive astroRoot from the CLI's own location (dist/cli/cli.js) + const cliDir = dirname(fileURLToPath(import.meta.url)) + astroRoot = resolve(cliDir, '..', '..') + console.log('Resolved astroRoot from CLI location:', astroRoot) } else { console.error('Error resolving astroRoot', e) } diff --git a/cli/getConfig.ts b/cli/getConfig.ts index b639002..351e416 100644 --- a/cli/getConfig.ts +++ b/cli/getConfig.ts @@ -5,6 +5,8 @@ export interface CollectionDefinition { version?: string pattern: string name: string + frontmatterDefaults?: Record + frontmatterMapping?: Record } export interface PropsGlobs { diff --git a/src/components/NavEntry.tsx b/src/components/NavEntry.tsx index 70dd9fa..5187a51 100644 --- a/src/components/NavEntry.tsx +++ b/src/components/NavEntry.tsx @@ -8,6 +8,7 @@ export interface TextContentEntry { section: string tab?: string sortValue?: number + subsection?: string } } diff --git a/src/components/NavSection.tsx b/src/components/NavSection.tsx index 9c1b484..cccbe9c 100644 --- a/src/components/NavSection.tsx +++ b/src/components/NavSection.tsx @@ -16,7 +16,20 @@ export const NavSection = ({ const isExpanded = window.location.pathname.includes(kebabCase(sectionId)) const isActive = entries.some((entry) => entry.id === activeItem) - const items = entries.map((entry) => ( + // Group entries by subsection + const topLevelEntries = entries.filter((entry) => !entry.data.subsection) + const subsections = new Map() + entries.forEach((entry) => { + if (entry.data.subsection) { + const sub = entry.data.subsection + if (!subsections.has(sub)) { + subsections.set(sub, []) + } + subsections.get(sub)!.push(entry) + } + }) + + const renderEntry = (entry: TextContentEntry) => ( - )) + ) return ( - {items} + {topLevelEntries.map(renderEntry)} + {Array.from(subsections.entries()).map(([subsection, subEntries]) => { + const subIsExpanded = subEntries.some( + (entry) => activeItem === entry.id || window.location.pathname.includes(kebabCase(entry.data.id)) + ) + return ( + + {subEntries.map(renderEntry)} + + ) + })} ) } diff --git a/src/components/Navigation.astro b/src/components/Navigation.astro index fea0b66..f96b365 100644 --- a/src/components/Navigation.astro +++ b/src/components/Navigation.astro @@ -39,7 +39,7 @@ const sortedSections = [...orderedSections, ...unorderedSections.sort()] const navData = sortedSections.map((section) => { const entries = navDataRaw .filter((entry) => entry.data.section === section) - .map(entry => ({ id: entry.id, data: { id: entry.data.id, section, sortValue: entry.data.sortValue }} as TextContentEntry)) + .map(entry => ({ id: entry.id, data: { id: entry.data.id, section, sortValue: entry.data.sortValue, subsection: entry.data.subsection }} as TextContentEntry)) const uniqueEntries = [ ...entries diff --git a/src/content.config.ts b/src/content.config.ts index 6f8d9a7..b1752ce 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -6,8 +6,7 @@ import type { CollectionDefinition } from '../cli/getConfig' import { convertToMDX } from '../cli/convertToMDX' function defineContent(contentObj: CollectionDefinition) { - const { base, packageName, pattern, name } = contentObj - + const { base, packageName, pattern, name, frontmatterDefaults, frontmatterMapping } = contentObj if (!base && !packageName) { // eslint-disable-next-line no-console @@ -24,26 +23,57 @@ function defineContent(contentObj: CollectionDefinition) { convertToMDX(`${base}/${pattern}`) const mdxPattern = pattern.replace(/\.md$/, '.mdx') + const hasExternalFrontmatter = !!(frontmatterDefaults || frontmatterMapping) + + const baseSchema = z.object({ + id: hasExternalFrontmatter ? z.string().optional() : z.string(), + section: hasExternalFrontmatter ? z.string().optional() : z.string(), + subsection: z.string().optional(), + title: z.string().optional(), + // Generic frontmatter fields from external sources + category: z.string().optional(), + subcategory: z.string().optional(), + description: z.string().optional(), + tags: z.array(z.string()).optional(), + propComponents: z.array(z.string()).optional(), + tab: z.string().optional().default(tabMap[name]), // for component tabs + source: z.string().optional(), + tabName: z.string().optional(), + sortValue: z.number().optional(), // used for sorting nav entries, + cssPrefix: z + .union([ + z.string().transform((val: string) => [val]), + z.array(z.string()), + z.null().transform(() => undefined), + ]) + .optional(), + }).transform((data) => { + const result: Record = { ...data } + + // Apply frontmatter mapping (e.g. { title: "id" } maps the title value to id) + if (frontmatterMapping) { + for (const [sourceField, targetField] of Object.entries(frontmatterMapping)) { + if (result[sourceField] != null && result[targetField] == null) { + result[targetField] = result[sourceField] + } + } + } + + // Apply frontmatter defaults (e.g. { section: "AI" } sets section if not already present) + if (frontmatterDefaults) { + for (const [field, value] of Object.entries(frontmatterDefaults)) { + if (result[field] == null) { + result[field] = value + } + } + } + + return result + }) + return defineCollection({ loader: glob({ base, pattern: mdxPattern }), - schema: z.object({ - id: z.string(), - section: z.string(), - subsection: z.string().optional(), - title: z.string().optional(), - propComponents: z.array(z.string()).optional(), - tab: z.string().optional().default(tabMap[name]), // for component tabs - source: z.string().optional(), - tabName: z.string().optional(), - sortValue: z.number().optional(), // used for sorting nav entries, - cssPrefix: z - .union([ - z.string().transform((val: string) => [val]), - z.array(z.string()), - z.null().transform(() => undefined), - ]) - .optional(), - }), + schema: baseSchema, }) } diff --git a/src/pages/[section]/[...page].astro b/src/pages/[section]/[...page].astro index 1484c5b..28537dd 100644 --- a/src/pages/[section]/[...page].astro +++ b/src/pages/[section]/[...page].astro @@ -57,7 +57,7 @@ export async function getStaticPaths() { } return { - params: { page: kebabCase(entry.data.id), section: entry.data.section }, + params: { page: kebabCase(entry.data.id), section: kebabCase(entry.data.section) }, props: { entry, title: entry.data.title, @@ -74,7 +74,7 @@ const { Content } = await render(entry) if (tabsDictionary[id]) { // if tab exists, rewrite to first tab content - return Astro.rewrite(`/${section}/${kebabCase(id)}/${tabsDictionary[id][0]}`) + return Astro.rewrite(`/${kebabCase(section)}/${kebabCase(id)}/${tabsDictionary[id][0]}`) } --- diff --git a/src/pages/[section]/[page]/[tab].astro b/src/pages/[section]/[page]/[tab].astro index c3d8366..855239b 100644 --- a/src/pages/[section]/[page]/[tab].astro +++ b/src/pages/[section]/[page]/[tab].astro @@ -64,7 +64,7 @@ export async function getStaticPaths() { return { params: { page: kebabCase(entry.data.id), - section: entry.data.section, + section: kebabCase(entry.data.section), tab, }, props: { entry, ...entry.data }, @@ -77,7 +77,8 @@ export async function getStaticPaths() { } const { entry, propComponents, cssPrefix } = Astro.props -const { title, id, section } = entry.data +const { title, id, section: rawSection } = entry.data +const section = kebabCase(rawSection) const { Content } = await render(entry) const currentPath = Astro.url.pathname