Revert wiki theme CSS to previous version
This commit is contained in:
parent
b4bd938ac3
commit
c22dd91556
3 changed files with 351 additions and 350 deletions
174
scripts/migrate-hub.js
Normal file
174
scripts/migrate-hub.js
Normal 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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue