1
0
mirror of synced 2025-12-19 18:10:59 -05:00

Support a new contentType frontmatter property (#56715)

This commit is contained in:
Sarah Schneider
2025-07-23 11:47:04 -04:00
committed by GitHub
parent 45c42cfb34
commit 2b0e25a26d
7 changed files with 220 additions and 3 deletions

View File

@@ -0,0 +1,186 @@
// This script auto-populates the `contentType` frontmatter property based on
// the directory location of the content file.
// Run with:
// npm run-script -- add-content-type --help
import fs from 'fs'
import path from 'path'
import { program } from 'commander'
import frontmatter from '@/frame/lib/read-frontmatter'
import walkFiles from '@/workflows/walk-files'
import { contentTypesEnum } from '#src/frame/lib/frontmatter.js'
import type { MarkdownFrontmatter } from '@/types'
const RESPONSIBLE_USE_STRING = 'responsible-use'
const LANDING_TYPE = 'landing'
const RAI_TYPE = 'rai'
const OTHER_TYPE = 'other'
interface ScriptOptions {
dryRun?: boolean
paths?: string[]
removeType?: boolean
verbose?: boolean
}
program
.description('Auto-populate the contentType frontmatter property based on file location')
.option(
'-p, --paths [paths...]',
'One or more specific paths to process (e.g., copilot or content/copilot/how-tos/file.md)',
)
.option('-r, --remove-type', `Remove the legacy 'type' frontmatter property if present`)
.option('-d, --dry-run', 'Preview changes without modifying files')
.option('-v, --verbose', 'Show detailed output of changes made')
.addHelpText(
'after',
`
Possible contentType values:
${contentTypesEnum.join(', ')}
Examples:
npm run-script -- add-content-type // runs on all content files, does not remove legacy 'type' prop
npm run-script -- add-content-type --paths copilot actions --remove-type --dry-run
npm run-script -- add-content-type --paths content/copilot/how-tos
npm run-script -- add-content-type --verbose`,
)
.parse(process.argv)
const options: ScriptOptions = program.opts()
const contentDir = path.join(process.cwd(), 'content')
async function main() {
const filesToProcess: string[] = walkFiles(contentDir, ['.md']).filter((file: string) => {
if (file.endsWith('README.md')) return false
if (file.includes('early-access')) return false
if (!options.paths) return true
return options.paths.some((p: string) => {
// Allow either a full content path like "content/foo/bar.md"
// or a top-level directory name like "copilot"
if (!p.startsWith('content')) {
p = path.join('content', p)
}
if (!fs.existsSync(p)) {
console.error(`${p} not found`)
process.exit(1)
}
if (path.relative(process.cwd(), file).startsWith(p)) return true
})
})
let processedCount = 0
let updatedCount = 0
for (const filePath of filesToProcess) {
try {
const result = processFile(filePath, options)
if (result.processed) processedCount++
if (result.updated) updatedCount++
} catch (error) {
console.error(
`Error processing ${filePath}:`,
error instanceof Error ? error.message : String(error),
)
}
}
console.log(`\nUpdated ${updatedCount} files out of ${processedCount}`)
}
function processFile(filePath: string, options: ScriptOptions) {
const fileContent = fs.readFileSync(filePath, 'utf8')
const relativePath = path.relative(contentDir, filePath)
const { data, content } = frontmatter(fileContent) as unknown as {
data: MarkdownFrontmatter & { contentType?: string }
content: string
}
if (!data) return { processed: false, updated: false }
// Remove the legacy type property if option is passed
const removeLegacyType = Boolean(options.removeType && data.type)
// Skip if contentType already exists and we're not removing legacy type
if (data.contentType && !removeLegacyType) {
console.log(`contentType already set on ${relativePath}`)
return { processed: true, updated: false }
}
const newContentType = data.contentType || determineContentType(relativePath, data.type || '')
if (options.dryRun) {
console.log(`\n${relativePath}`)
if (!data.contentType) {
console.log(` ✅ Would set contentType: "${newContentType}"`)
}
if (removeLegacyType) {
console.log(` ✂️ Would remove legacy type: "${data.type}"`)
}
return { processed: true, updated: false }
}
// Set the contentType property if it doesn't exist
if (!data.contentType) {
data.contentType = newContentType
}
let legacyTypeValue
if (removeLegacyType) {
legacyTypeValue = data.type
delete data.type
}
// Write the file back
fs.writeFileSync(filePath, frontmatter.stringify(content, data, { lineWidth: -1 } as any))
if (options.verbose) {
console.log(`\n${relativePath}`)
console.log(` ✅ Set contentType: "${newContentType}"`)
if (removeLegacyType) {
console.log(` ✂️ Removed legacy type: "${legacyTypeValue}"`)
}
}
return { processed: true, updated: true }
}
function determineContentType(relativePath: string, legacyType: string): string {
// The split path array will be structured like:
// [ 'copilot', 'how-tos', 'troubleshoot', 'index.md' ]
// where the content type we want is in slot 1.
const pathSegments = relativePath.split(path.sep)
const topLevelDirectory = pathSegments[0]
const derivedContentType = pathSegments[1]
// There is only one content/index.md, and it's the homepage.
if (topLevelDirectory === 'index.md') return 'homepage'
// SPECIAL HANDLING FOR RAI
// If a legacy type includes 'rai', use it for the contentType.
// If a directory name includes a responsible-use string, assume the 'rai' type.
if (legacyType === 'rai' || derivedContentType.includes(RESPONSIBLE_USE_STRING)) {
return RAI_TYPE
}
// When the content directory matches any of the allowed
// content type values (such as 'get-started',
// 'concepts', 'how-tos', 'reference', and 'tutorials'),
// immediately return it. We're satisfied.
if (contentTypesEnum.includes(derivedContentType)) {
return derivedContentType
}
// There is only one content/<product>/index.md file per doc set.
// This index.md is always a landing page.
if (derivedContentType === 'index.md') {
return LANDING_TYPE
}
// Classify anything else as 'other'.
return OTHER_TYPE
}
main().catch(console.error)