clawdie-ai/bootstrap/cms/clawdie-docs/scripts/export-strapi.mjs
Operator & Codex e3ad322d3b Rename Astro docs project to clawdie-docs (Sam & Claude)
Make the docs renderer name match its purpose, add CMS_DOCS_SITE_PATH with ASTRO_SITE_PATH compatibility, and update docs publishing paths.

---
Build: pass | Tests: pass — 2372 passed (704 files)
2026-05-10 19:49:39 +02:00

161 lines
5.6 KiB
JavaScript

/**
* Strapi export script — snapshots Strapi content into the repo as committed .md files.
*
* Git is the source of record. Strapi is an optional content service.
* Run this script when Strapi content has changed and you want to snapshot it:
*
* npm run export-strapi
*
* Then review the diff and commit. The build never requires Strapi to be running —
* it uses whatever .md files are committed in src/content/docs/.
*
* Directories this script fully owns (cleared + rewritten on each run):
* guides/ ← sl guides (English slug, Slovenian content)
* en/guides/ ← en guides
*
* Strapi-generated pages are tracked in .strapi-manifest.json so stale files
* are cleaned up across runs. The manifest itself is committed.
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DOCS_DIR = path.join(__dirname, '..', 'src', 'content', 'docs');
const MANIFEST = path.join(DOCS_DIR, '.strapi-manifest.json');
const TENANT_SITE_DIR = path.join(__dirname, '..', 'src', 'content', 'tenant-sites');
const STRAPI_URL = process.env.STRAPI_URL ?? 'http://localhost:1337';
const TOKEN = process.env.STRAPI_API_TOKEN ?? '';
if (!TOKEN) {
console.error('ERROR: STRAPI_API_TOKEN is not set. Run with --env-file=.env or export the variable.');
process.exit(1);
}
async function fetchStrapi(endpoint) {
const res = await fetch(`${STRAPI_URL}/api/${endpoint}`, {
headers: { Authorization: `Bearer ${TOKEN}` },
});
if (!res.ok) throw new Error(`Strapi ${res.status}: ${endpoint}`);
return res.json();
}
function htmlToMarkdown(html) {
if (!html) return '';
return html
.replace(/<p>(.*?)<\/p>/gs, '$1\n\n')
.replace(/<strong>(.*?)<\/strong>/g, '**$1**')
.replace(/<em>(.*?)<\/em>/g, '_$1_')
.replace(/<code>(.*?)<\/code>/g, '`$1`')
.replace(/<[^>]+>/g, '')
.trim();
}
function writeDoc(filePath, frontmatter, body) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
const fm = Object.entries(frontmatter)
.filter(([, v]) => v !== null && v !== undefined && v !== '')
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
.join('\n');
fs.writeFileSync(filePath, `---\n${fm}\n---\n\n${body}\n`);
console.log(' wrote', path.relative(DOCS_DIR, filePath));
}
function writeJson(filePath, value) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + '\n');
console.log(' wrote', path.relative(path.join(__dirname, '..', 'src', 'content'), filePath));
}
// Remove stale page files from previous export
const prevManifest = fs.existsSync(MANIFEST)
? JSON.parse(fs.readFileSync(MANIFEST, 'utf8'))
: { pages: [], tenantSiteSnapshots: [] };
for (const f of prevManifest.pages ?? []) {
if (fs.existsSync(f)) {
fs.rmSync(f);
console.log(' removed stale', path.relative(DOCS_DIR, f));
}
}
for (const f of prevManifest.tenantSiteSnapshots ?? []) {
if (fs.existsSync(f)) {
fs.rmSync(f);
console.log(' removed stale', path.relative(TENANT_SITE_DIR, f));
}
}
const manifest = { pages: [], guideDirs: [], tenantSiteSnapshots: [] };
async function exportLocale(locale) {
const prefix = locale === 'sl' ? '' : `${locale}/`;
const guidesDir = path.join(DOCS_DIR, prefix, 'guides');
const [guidesRes, pagesRes] = await Promise.all([
fetchStrapi(`guides?locale=${locale}&pagination[pageSize]=100&sort=order:asc`),
fetchStrapi(`pages?locale=${locale}&pagination[pageSize]=100&sort=order:asc`),
]);
// Fully clear + recreate guides directory (this script owns it)
if (fs.existsSync(guidesDir)) fs.rmSync(guidesDir, { recursive: true });
fs.mkdirSync(guidesDir, { recursive: true });
manifest.guideDirs.push(guidesDir);
console.log(`\n[${locale}] guides:`);
for (const guide of guidesRes.data ?? []) {
writeDoc(
path.join(guidesDir, `${guide.slug}.md`),
{ title: guide.title, description: guide.summary ?? '' },
htmlToMarkdown(guide.content)
);
}
console.log(`[${locale}] pages:`);
for (const page of pagesRes.data ?? []) {
// Skip homepage — handled by committed index.md
if (page.slug === 'home') continue;
const filePath = path.join(DOCS_DIR, prefix, `${page.slug}.gen.md`);
writeDoc(filePath, { title: page.title, description: page.summary ?? '' }, htmlToMarkdown(page.content));
manifest.pages.push(filePath);
}
}
async function exportTenantSites() {
const tenantSitePagesRes = await fetchStrapi(
'tenant-site-pages?pagination[pageSize]=200&sort=tenant_id:asc&sort=site_id:asc&sort=locale:asc&sort=order:asc'
);
const grouped = new Map();
for (const page of tenantSitePagesRes.data ?? []) {
const key = `${page.tenant_id}/${page.site_id}`;
const bucket = grouped.get(key) || {
tenantId: page.tenant_id,
siteId: page.site_id,
title: page.title,
pages: [],
};
bucket.pages.push({
slug: page.slug,
locale: page.locale,
title: page.title,
summary: page.summary ?? '',
content: htmlToMarkdown(page.content),
order: page.order ?? 0,
});
grouped.set(key, bucket);
}
console.log('\n[tenant-sites] pages:');
for (const snapshot of grouped.values()) {
const filePath = path.join(TENANT_SITE_DIR, snapshot.tenantId, `${snapshot.siteId}.json`);
writeJson(filePath, snapshot);
manifest.tenantSiteSnapshots.push(filePath);
}
}
await exportLocale('sl');
await exportLocale('en');
await exportTenantSites();
fs.writeFileSync(MANIFEST, JSON.stringify(manifest, null, 2));
console.log('\nStrapi export complete. Review the diff and commit if the content looks right.');