Revert wiki theme CSS to previous version

This commit is contained in:
Jennie Robinson Faber 2026-03-29 21:30:58 +01:00
parent b4bd938ac3
commit c22dd91556
3 changed files with 351 additions and 350 deletions

174
scripts/migrate-hub.js Normal file
View file

@ -0,0 +1,174 @@
#!/usr/bin/env node
/**
* Migrate Hub User Guide content from content/wiki/hub/ into Outline.
*
* Reads plain markdown files (no frontmatter), extracts titles from H1 headers,
* and creates nested documents matching the directory structure.
*
* Usage:
* OUTLINE_URL=https://wiki.ghostguild.org OUTLINE_API_TOKEN=your-token node migrate-hub.js
*/
import fs from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const OUTLINE_URL = process.env.OUTLINE_URL;
const OUTLINE_API_TOKEN = process.env.OUTLINE_API_TOKEN;
const CONTENT_DIR = path.resolve(__dirname, "../content/wiki/hub");
const COLLECTION_NAME = "Hub User Guide";
const RATE_LIMIT_MS = 200;
if (!OUTLINE_URL || !OUTLINE_API_TOKEN) {
console.error(
"Error: OUTLINE_URL and OUTLINE_API_TOKEN env vars are required."
);
process.exit(1);
}
async function outlineApi(endpoint, body) {
const res = await fetch(`${OUTLINE_URL}/api/${endpoint}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${OUTLINE_API_TOKEN}`,
},
body: JSON.stringify(body),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`API ${endpoint} failed (${res.status}): ${text}`);
}
return res.json();
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function extractTitle(content, filename) {
const match = content.match(/^#\s+(.+)$/m);
if (match) return match[1];
// Fallback: clean up filename
return filename
.replace(/^\d+-/, "")
.replace(/\.md$/, "")
.replace(/-/g, " ")
.replace(/\b\w/g, (c) => c.toUpperCase());
}
function stripH1(content) {
// Remove the first H1 line since Outline uses the title field
return content.replace(/^#\s+.+\n*/, "").trim();
}
// Section display names for subdirectories
const SECTION_NAMES = {
admin: "Admin Guide",
reviewer: "Reviewer Guide",
reference: "Reference",
};
async function findCollection(name) {
const { data } = await outlineApi("collections.list", { limit: 100 });
const existing = data.find(
(c) => c.name.toLowerCase() === name.toLowerCase()
);
if (!existing) {
throw new Error(
`Collection "${name}" not found. Available: ${data.map((c) => c.name).join(", ")}`
);
}
return existing.id;
}
async function createDoc({ title, text, collectionId, parentDocumentId }) {
const body = { title, text, collectionId, publish: true };
if (parentDocumentId) body.parentDocumentId = parentDocumentId;
const { data: doc } = await outlineApi("documents.create", body);
return doc;
}
async function importFiles(dir, collectionId, parentDocumentId) {
const entries = await fs.readdir(dir, { withFileTypes: true });
// Sort: files first (by name), then directories (by name)
const files = entries.filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name));
const dirs = entries.filter((e) => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
let success = 0;
let failed = 0;
// Import markdown files
for (const file of files) {
const filePath = path.join(dir, file.name);
const raw = await fs.readFile(filePath, "utf-8");
const title = extractTitle(raw, file.name);
const text = stripH1(raw);
try {
const doc = await createDoc({
title,
text,
collectionId,
parentDocumentId,
});
console.log(`${title}${doc.url}`);
success++;
} catch (err) {
console.error(`${title}: ${err.message}`);
failed++;
}
await delay(RATE_LIMIT_MS);
}
// Recurse into subdirectories, creating a parent doc for each
for (const sub of dirs) {
const sectionName = SECTION_NAMES[sub.name] || sub.name.replace(/\b\w/g, (c) => c.toUpperCase());
console.log(`\n📂 ${sectionName}`);
try {
// Create a parent document for this section
const parentDoc = await createDoc({
title: sectionName,
text: "",
collectionId,
parentDocumentId,
});
await delay(RATE_LIMIT_MS);
const result = await importFiles(
path.join(dir, sub.name),
collectionId,
parentDoc.id
);
success += result.success;
failed += result.failed;
} catch (err) {
console.error(` ✗ Failed to create section "${sectionName}": ${err.message}`);
failed++;
}
}
return { success, failed };
}
async function main() {
console.log(`Looking up collection: ${COLLECTION_NAME}\n`);
const collectionId = await findCollection(COLLECTION_NAME);
console.log(`Found collection: ${collectionId}\n`);
const { success, failed } = await importFiles(CONTENT_DIR, collectionId, null);
console.log(`\nDone. ${success} imported, ${failed} failed.`);
}
main().catch((err) => {
console.error("Migration failed:", err);
process.exit(1);
});