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('/')}`)
+}