diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index def01b42..10d54654 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -30,6 +30,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Generate sitemap + run: node scripts/generate-sitemap.mjs + - name: Build run: npx nuxi build diff --git a/.github/workflows/sync-sdk-docs.yml b/.github/workflows/sync-sdk-docs.yml index c2a644f9..bebae0cc 100644 --- a/.github/workflows/sync-sdk-docs.yml +++ b/.github/workflows/sync-sdk-docs.yml @@ -40,10 +40,13 @@ jobs: cp tmp/laravel-repo/docs/*.md tmp/laravel-docs/ 2>/dev/null || true node scripts/transform-laravel-docs.mjs tmp/laravel-docs content/3.packages/2.laravel + - name: Regenerate sitemap + run: node scripts/generate-sitemap.mjs + - name: Check for changes id: changes run: | - git add content/3.packages/ + git add content/3.packages/ public/sitemap.xml if git diff --staged --quiet; then echo "changed=false" >> $GITHUB_OUTPUT else diff --git a/package.json b/package.json index 79f6d094..826bc8b3 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,8 @@ "dev": "nuxt dev --extends docus", "cache:clear": "rm -rf .nuxt .data node_modules/.cache", "cache:clear:dev": "npm run cache:clear && npm run dev", + "sitemap": "node scripts/generate-sitemap.mjs", + "prebuild": "node scripts/generate-sitemap.mjs", "build": "nuxt build --extends docus", "postinstall": "cp app/components/OgImage/OgImageDocs.vue node_modules/docus/app/components/OgImage/OgImageDocs.vue", "lint:md": "npx markdownlint-cli2 'content/**/*.md'" diff --git a/public/sitemap.xml b/public/sitemap.xml index 3c6cb090..733855a5 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -3,21 +3,25 @@ https://docs.vatly.com/1.0 https://docs.vatly.com/introduction0.9 https://docs.vatly.com/guides/quickstart0.8 + https://docs.vatly.com/guides/selling-workflow0.8 https://docs.vatly.com/guides/sdks0.8 https://docs.vatly.com/guides/authentication0.8 - https://docs.vatly.com/guides/pagination0.7 - https://docs.vatly.com/guides/errors0.7 + https://docs.vatly.com/guides/pagination0.8 + https://docs.vatly.com/guides/errors0.8 https://docs.vatly.com/guides/webhooks0.8 - https://docs.vatly.com/guides/idempotency0.7 + https://docs.vatly.com/guides/idempotency0.8 + https://docs.vatly.com/guides/testing0.8 https://docs.vatly.com/api-reference/customers0.8 https://docs.vatly.com/api-reference/checkouts0.8 + https://docs.vatly.com/api-reference/one-off-products0.8 https://docs.vatly.com/api-reference/subscriptions0.8 https://docs.vatly.com/api-reference/subscription-plans0.8 https://docs.vatly.com/api-reference/orders0.8 https://docs.vatly.com/api-reference/refunds0.8 - https://docs.vatly.com/api-reference/global-refunds0.7 - https://docs.vatly.com/api-reference/chargebacks0.7 - https://docs.vatly.com/api-reference/one-off-products0.8 + https://docs.vatly.com/api-reference/global-refunds0.8 + https://docs.vatly.com/api-reference/chargebacks0.8 + https://docs.vatly.com/api-reference/test-helpers0.8 + https://docs.vatly.com/api-reference/webhook-events0.8 https://docs.vatly.com/packages0.7 https://docs.vatly.com/packages/php/readme0.7 https://docs.vatly.com/packages/php/customers0.6 @@ -35,5 +39,8 @@ https://docs.vatly.com/packages/laravel/subscriptions0.6 https://docs.vatly.com/packages/laravel/orders0.6 https://docs.vatly.com/packages/laravel/webhooks0.6 + https://docs.vatly.com/packages/laravel/comparison0.6 + https://docs.vatly.com/packages/laravel/migrating-to-vatly0.6 + https://docs.vatly.com/packages/laravel/configuration0.6 https://docs.vatly.com/integrations0.7 diff --git a/scripts/generate-sitemap.mjs b/scripts/generate-sitemap.mjs new file mode 100644 index 00000000..4c6768c5 --- /dev/null +++ b/scripts/generate-sitemap.mjs @@ -0,0 +1,85 @@ +#!/usr/bin/env node +/** + * Generate public/sitemap.xml from the content/ directory. + * + * The sitemap used to be hand-maintained, which meant every newly-added page — + * and especially pages synced in from the vatly-laravel / vatly-api-php repos — + * silently went missing from it. This walks content/ and derives the exact + * route Docus serves for each page, so the sitemap can never drift again. + * + * Route derivation (verified against the live site): + * - Strip the leading "N." ordering prefix from every path segment. + * - A file named `index.md` maps to its parent directory's route. + * - `readme.md` is NOT special — it stays as `/.../readme`. + * - content/index.md -> "/" (home) + * + * Run: node scripts/generate-sitemap.mjs + */ + +import { readdirSync, writeFileSync } from 'fs' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') +const CONTENT_DIR = join(ROOT, 'content') +const OUTPUT = join(ROOT, 'public', 'sitemap.xml') +const BASE_URL = 'https://docs.vatly.com' + +// Strip the leading "N." ordering prefix (e.g. "7.migrating-to-vatly" -> "migrating-to-vatly"). +const stripOrder = (name) => name.replace(/^\d+\./, '') + +// Numeric sort key from the leading "N." prefix. `index` sorts first so a +// directory's own page leads its children; unprefixed files sort last. +const orderKey = (name) => { + const base = name.replace(/\.md$/, '') + if (base === 'index') return -1 + const m = name.match(/^(\d+)\./) + return m ? parseInt(m[1], 10) : 1e9 +} + +// SEO priority, derived from route depth so it needs no per-page upkeep. +const priorityFor = (segments) => { + if (segments.length === 0) return '1.0' // home + if (segments.length === 1) return segments[0] === 'introduction' ? '0.9' : '0.7' + if (segments[0] === 'packages') { + return segments[segments.length - 1] === 'readme' ? '0.7' : '0.6' + } + return '0.8' // guides/*, api-reference/* +} + +/** Recursively collect route segment-arrays from a content directory. */ +function collect(dir, segments, routes) { + const entries = readdirSync(dir, { withFileTypes: true }) + .filter((e) => e.isDirectory() || e.name.endsWith('.md')) + .sort((a, b) => orderKey(a.name) - orderKey(b.name) || a.name.localeCompare(b.name)) + + for (const entry of entries) { + const full = join(dir, entry.name) + if (entry.isDirectory()) { + collect(full, [...segments, stripOrder(entry.name)], routes) + } else { + const base = stripOrder(entry.name.replace(/\.md$/, '')) + routes.push(base === 'index' ? segments : [...segments, base]) + } + } +} + +const routes = [] +collect(CONTENT_DIR, [], routes) + +const urls = routes.map((segments) => { + const loc = segments.length ? `${BASE_URL}/${segments.join('/')}` : `${BASE_URL}/` + return ` ${loc}${priorityFor(segments)}` +}) + +const xml = ` + +${urls.join('\n')} + +` + +writeFileSync(OUTPUT, xml) +console.log(`Wrote ${routes.length} URLs to ${OUTPUT.replace(ROOT + '/', '')}`) +for (const segments of routes) { + console.log(` /${segments.join('/')}`) +}