Nest exported wiki docs under collection/parent folders
Outline docs with the same title under different parents (e.g. Peer Support
Playbook > Session Guides > Session 0 vs Manual > Session Content > Session 0)
were silently overwriting each other under the flat {collection}--{title}.md
naming, dropping 9 of 99 docs on every export. Use the full slugified doc
path instead, and have orphan cleanup scope to known collection-slug roots
(sweeping legacy flat files too) so sibling dirs like hub/, _local/, .claude/
stay untouched.
Also switch the cron's git add to -A so doc deletions and renames in Outline
actually propagate.
This commit is contained in:
parent
bac3fc6dd2
commit
16aef35682
2 changed files with 58 additions and 15 deletions
|
|
@ -30,8 +30,9 @@ fi
|
||||||
# Run export
|
# Run export
|
||||||
node scripts/export-content.js
|
node scripts/export-content.js
|
||||||
|
|
||||||
# Commit and push if there are changes
|
# Commit and push if there are changes. -A stages deletions and renames too,
|
||||||
git add content/wiki/
|
# so docs removed or renamed in Outline actually propagate to git.
|
||||||
|
git add -A content/wiki/
|
||||||
if ! git diff --cached --quiet; then
|
if ! git diff --cached --quiet; then
|
||||||
git commit -m "wiki content export $(date +%Y-%m-%d)"
|
git commit -m "wiki content export $(date +%Y-%m-%d)"
|
||||||
git push
|
git push
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,35 @@ function buildPath(doc, docMap, collectionName) {
|
||||||
return `${collectionName}/${parts.join("/")}`;
|
return `${collectionName}/${parts.join("/")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildRelFilePath(doc, docMap, collectionName) {
|
||||||
|
const parts = [slugify(doc.title)];
|
||||||
|
let current = doc;
|
||||||
|
|
||||||
|
while (current.parentDocumentId) {
|
||||||
|
const parent = docMap.get(current.parentDocumentId);
|
||||||
|
if (!parent) break;
|
||||||
|
parts.unshift(slugify(parent.title));
|
||||||
|
current = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.unshift(slugify(collectionName));
|
||||||
|
return parts.join("/") + ".md";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function walkMarkdownFiles(dir, baseDir) {
|
||||||
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||||
|
const results = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
const full = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
results.push(...(await walkMarkdownFiles(full, baseDir)));
|
||||||
|
} else if (entry.name.endsWith(".md")) {
|
||||||
|
results.push(path.relative(baseDir, full).split(path.sep).join("/"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
function getParentTitle(doc, docMap) {
|
function getParentTitle(doc, docMap) {
|
||||||
if (!doc.parentDocumentId) return null;
|
if (!doc.parentDocumentId) return null;
|
||||||
const parent = docMap.get(doc.parentDocumentId);
|
const parent = docMap.get(doc.parentDocumentId);
|
||||||
|
|
@ -136,14 +165,15 @@ async function main() {
|
||||||
|
|
||||||
// Write all documents
|
// Write all documents
|
||||||
const writtenFiles = new Set();
|
const writtenFiles = new Set();
|
||||||
|
const collectionSlugs = new Set(
|
||||||
|
Array.from(collectionMap.values()).map(slugify)
|
||||||
|
);
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
for (const doc of allDocs) {
|
for (const doc of allDocs) {
|
||||||
const collectionName = collectionMap.get(doc.collectionId) || "unknown";
|
const collectionName = collectionMap.get(doc.collectionId) || "unknown";
|
||||||
const collectionSlug = slugify(collectionName);
|
|
||||||
const docSlug = slugify(doc.title);
|
|
||||||
const filename = `${collectionSlug}--${docSlug}.md`;
|
|
||||||
|
|
||||||
|
const relPath = buildRelFilePath(doc, docMap, collectionName);
|
||||||
const docPath = buildPath(doc, docMap, collectionName);
|
const docPath = buildPath(doc, docMap, collectionName);
|
||||||
const parentTitle = getParentTitle(doc, docMap);
|
const parentTitle = getParentTitle(doc, docMap);
|
||||||
|
|
||||||
|
|
@ -161,25 +191,37 @@ async function main() {
|
||||||
// starts with `---` (markdown horizontal rule) gets misread as having
|
// starts with `---` (markdown horizontal rule) gets misread as having
|
||||||
// YAML frontmatter and crashes the export.
|
// YAML frontmatter and crashes the export.
|
||||||
const content = matter.stringify({ content: doc.text || "" }, frontmatter);
|
const content = matter.stringify({ content: doc.text || "" }, frontmatter);
|
||||||
const filePath = path.join(OUTPUT_DIR, filename);
|
const filePath = path.join(OUTPUT_DIR, relPath);
|
||||||
|
|
||||||
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||||
await fs.writeFile(filePath, content, "utf-8");
|
await fs.writeFile(filePath, content, "utf-8");
|
||||||
writtenFiles.add(filename);
|
writtenFiles.add(relPath);
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\nWrote ${count} documents to content/wiki/`);
|
console.log(`\nWrote ${count} documents to content/wiki/`);
|
||||||
|
|
||||||
// Clean up orphaned files
|
// Orphan cleanup is scoped to files we own:
|
||||||
const existing = (await fs.readdir(OUTPUT_DIR)).filter((f) =>
|
// - anything under a known <collection-slug>/ directory
|
||||||
f.endsWith(".md")
|
// - legacy flat files matching <collection-slug>--*.md at the top level
|
||||||
);
|
// This leaves sibling dirs (hub/, _local/, .claude/) and loose source files alone.
|
||||||
|
const existing = await walkMarkdownFiles(OUTPUT_DIR, OUTPUT_DIR);
|
||||||
let removed = 0;
|
let removed = 0;
|
||||||
|
|
||||||
for (const file of existing) {
|
for (const rel of existing) {
|
||||||
if (!writtenFiles.has(file)) {
|
if (writtenFiles.has(rel)) continue;
|
||||||
await fs.unlink(path.join(OUTPUT_DIR, file));
|
|
||||||
console.log(` Removed orphan: ${file}`);
|
const segments = rel.split("/");
|
||||||
|
const topDir = segments.length > 1 ? segments[0] : null;
|
||||||
|
const flatSlug = segments.length === 1 ? rel.split("--")[0] : null;
|
||||||
|
|
||||||
|
const owned =
|
||||||
|
(topDir && collectionSlugs.has(topDir)) ||
|
||||||
|
(flatSlug && collectionSlugs.has(flatSlug));
|
||||||
|
|
||||||
|
if (owned) {
|
||||||
|
await fs.unlink(path.join(OUTPUT_DIR, rel));
|
||||||
|
console.log(` Removed orphan: ${rel}`);
|
||||||
removed++;
|
removed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue