diff --git a/.github/workflows/generator.yml b/.github/workflows/generator.yml index 3e49d6d..07ad9fb 100644 --- a/.github/workflows/generator.yml +++ b/.github/workflows/generator.yml @@ -44,7 +44,6 @@ jobs: id: auto-commit uses: stefanzweifel/git-auto-commit-action@v5 with: - commit_options: '--no-verify' commit_message: 'chore: auto-generate data and og images' - name: Comment on PR diff --git a/.github/workflows/slug-check.yml b/.github/workflows/slug-check.yml new file mode 100644 index 0000000..b18cdc7 --- /dev/null +++ b/.github/workflows/slug-check.yml @@ -0,0 +1,25 @@ +name: Slug Check + +on: + pull_request: + branches: [main] + +jobs: + slug-check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: pnpm + + - run: pnpm install --frozen-lockfile + - run: pnpm check:slugs diff --git a/package.json b/package.json index 3f2c590..783f470 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "prepare": "husky", "check": "astro check", "generate:og": "node scripts/generate-og.mjs", - "generate:data": "node scripts/update-data.mjs" + "generate:data": "node scripts/update-data.mjs", + "check:slugs": "node scripts/check-slugs.mjs" }, "dependencies": { "@astrojs/mdx": "^5.0.2", diff --git a/scripts/check-slugs.mjs b/scripts/check-slugs.mjs new file mode 100644 index 0000000..680c0ef --- /dev/null +++ b/scripts/check-slugs.mjs @@ -0,0 +1,71 @@ +#!/usr/bin/env node +import { promises as fs } from 'fs' +import path from 'path' +import matter from 'gray-matter' +import { SUPPORTED_LANGS, LANGUAGES } from '../src/config.mjs' + +const RESERVED_SLUGS = { + tr: new Set(['articles', 'categories', 'tags', '404']), + en: new Set(['en', 'en/articles', 'en/categories', 'en/tags', 'en/404']), +} + +let hasErrors = false + +const __dirname = new URL('.', import.meta.url).pathname +const repoRoot = path.resolve(__dirname, '..') + +;(async () => { + for (const lang of SUPPORTED_LANGS) { + const contentDir = path.join(repoRoot, 'src/content/articles', lang) + console.log(`Checking slugs for language: ${lang}`) + + let mdFiles + try { + mdFiles = (await fs.readdir(contentDir)).filter((f) => f.endsWith('.md') || f.endsWith('.mdx')) + } catch { + console.log(` No content directory for language '${lang}': ${contentDir}`) + console.log(` Skipping.`) + continue + } + + const slugMap = new Map() + const reserved = RESERVED_SLUGS[lang] || new Set() + const langPrefix = LANGUAGES[lang]?.prefix || '' + + for (const file of mdFiles) { + const filePath = path.join(contentDir, file) + const raw = await fs.readFile(filePath, 'utf8') + const { data } = matter(raw) + + const slug = data.slug?.trim() + if (!slug) { + console.log(` ✗ ${file} — MISSING slug field in frontmatter`) + hasErrors = true + continue + } + + if (reserved.has(slug) || (langPrefix && slug.startsWith(langPrefix.slice(1) + '/'))) { + console.log(` ✗ ${file} — RESERVED slug '${slug}' (conflicts with static route)`) + hasErrors = true + continue + } + + if (slugMap.has(slug)) { + const original = slugMap.get(slug) + console.log(` ✗ ${file} — DUPLICATE slug '${slug}' (already used by ${original})`) + hasErrors = true + } else { + slugMap.set(slug, file) + console.log(` ✓ ${slug} — slug OK`) + } + } + } + + if (hasErrors) { + console.log('\nFound slug errors. Exiting with code 1.') + process.exit(1) + } else { + console.log('\nAll slugs are valid.') + process.exit(0) + } +})()