diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 40423c776..5e8fb1301 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -10,6 +10,7 @@ on: paths: - 'docs/**' - 'apps/edit-docs/**' + - 'apps/build-docs/**' - 'packages/share-theme/**' # Allow manual triggering from Actions tab @@ -23,6 +24,7 @@ on: paths: - 'docs/**' - 'apps/edit-docs/**' + - 'apps/build-docs/**' - 'packages/share-theme/**' jobs: @@ -60,6 +62,8 @@ jobs: - name: Validate Built Site run: | test -f site/index.html || (echo "ERROR: site/index.html not found" && exit 1) + test -f site/developer-guide/index.html || (echo "ERROR: site/developer-guide/index.html not found" && exit 1) + echo "✓ User Guide and Developer Guide built successfully" - name: Deploy uses: ./.github/actions/deploy-to-cloudflare-pages diff --git a/apps/build-docs/src/build-docs.ts b/apps/build-docs/src/build-docs.ts index 39a3dd30b..5d1a0cdd6 100644 --- a/apps/build-docs/src/build-docs.ts +++ b/apps/build-docs/src/build-docs.ts @@ -14,17 +14,12 @@ import BuildContext from "./context.js"; const DOCS_ROOT = "../../../docs"; const OUTPUT_DIR = "../../site"; -async function buildDocsInner() { - const i18n = await import("@triliumnext/server/src/services/i18n.js"); - await i18n.initializeTranslations(); +async function importAndExportDocs(sourcePath: string, outputSubDir: string) { + const note = await importData(sourcePath); - const sqlInit = (await import("../../server/src/services/sql_init.js")).default; - await sqlInit.createInitialDatabase(true); - - const note = await importData(join(__dirname, DOCS_ROOT, "User Guide")); - - // Export - const zipFilePath = "output.zip"; + // Use a meaningful name for the temporary zip file + const zipName = outputSubDir || "user-guide"; + const zipFilePath = `output-${zipName}.zip`; try { const { exportToZip } = (await import("@triliumnext/server/src/services/export/zip.js")).default; const branch = note.getParentBranches()[0]; @@ -36,25 +31,50 @@ async function buildDocsInner() { const fileOutputStream = fsExtra.createWriteStream(zipFilePath); await exportToZip(taskContext, branch, "share", fileOutputStream); await waitForStreamToFinish(fileOutputStream); - await extractZip(zipFilePath, OUTPUT_DIR); + + // Output to root directory if outputSubDir is empty, otherwise to subdirectory + const outputPath = outputSubDir ? join(OUTPUT_DIR, outputSubDir) : OUTPUT_DIR; + await extractZip(zipFilePath, outputPath); } finally { if (await fsExtra.exists(zipFilePath)) { await fsExtra.rm(zipFilePath); } } +} + +async function buildDocsInner() { + const i18n = await import("@triliumnext/server/src/services/i18n.js"); + await i18n.initializeTranslations(); + + const sqlInit = (await import("../../server/src/services/sql_init.js")).default; + await sqlInit.createInitialDatabase(true); + + // Wait for becca to be loaded before importing data + const beccaLoader = await import("../../server/src/becca/becca_loader.js"); + await beccaLoader.beccaLoaded; + + // Build User Guide + console.log("Building User Guide..."); + await importAndExportDocs(join(__dirname, DOCS_ROOT, "User Guide"), "user-guide"); + + // Build Developer Guide + console.log("Building Developer Guide..."); + await importAndExportDocs(join(__dirname, DOCS_ROOT, "Developer Guide"), "developer-guide"); // Copy favicon. await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "favicon.ico")); + await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "user-guide", "favicon.ico")); + await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "developer-guide", "favicon.ico")); console.log("Documentation built successfully!"); } export async function importData(path: string) { const buffer = await createImportZip(path); - const importService = (await import("@triliumnext/server/src/services/import/zip.js")).default; - const TaskContext = (await import("@triliumnext/server/src/services/task_context.js")).default; + const importService = (await import("../../server/src/services/import/zip.js")).default; + const TaskContext = (await import("../../server/src/services/task_context.js")).default; const context = new TaskContext("no-progress-reporting", "importNotes", null); - const becca = (await import("@triliumnext/server/src/becca/becca.js")).default; + const becca = (await import("../../server/src/becca/becca.js")).default; const rootNote = becca.getRoot(); if (!rootNote) { diff --git a/apps/build-docs/src/index.html b/apps/build-docs/src/index.html new file mode 100644 index 000000000..47a0bfb34 --- /dev/null +++ b/apps/build-docs/src/index.html @@ -0,0 +1,10 @@ + + + + + Redirecting... + + +

If you are not redirected automatically, click here.

+ + \ No newline at end of file diff --git a/apps/build-docs/src/main.ts b/apps/build-docs/src/main.ts index 8e6804fe5..19d533420 100644 --- a/apps/build-docs/src/main.ts +++ b/apps/build-docs/src/main.ts @@ -1,7 +1,7 @@ import { join } from "path"; import BuildContext from "./context"; import buildSwagger from "./swagger"; -import { existsSync, mkdirSync, rmSync } from "fs"; +import { cpSync, existsSync, mkdirSync, rmSync } from "fs"; import buildDocs from "./build-docs"; import buildScriptApi from "./script-api"; @@ -21,6 +21,9 @@ async function main() { await buildDocs(context); buildSwagger(context); buildScriptApi(context); + + // Copy index file. + cpSync(join(__dirname, "index.html"), join(context.baseDir, "index.html")); } main(); diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index 3043e1636..53e0eb540 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -253,6 +253,10 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch, for (let i = 0; i < targetPath.length - 1; i++) { const meta = noteIdToMeta[targetPath[i]]; + if (meta === rootMeta && format === "share") { + continue; + } + if (meta.dirFileName) { url += `${encodeURIComponent(meta.dirFileName)}/`; } @@ -371,10 +375,12 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch, } if (noteMeta.children?.length || 0 > 0) { - const directoryPath = filePathPrefix + noteMeta.dirFileName; + const directoryPath = filePathPrefix !== "" || format !== "share" ? filePathPrefix + noteMeta.dirFileName : ""; // create directory - archive.append("", { name: `${directoryPath}/`, date: dateUtils.parseDateTime(note.utcDateModified) }); + if (directoryPath) { + archive.append("", { name: `${directoryPath}/`, date: dateUtils.parseDateTime(note.utcDateModified) }); + } for (const childMeta of noteMeta.children || []) { saveNote(childMeta, `${directoryPath}/`); diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index f06871857..1788e38b9 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -27,6 +27,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider { private assetsMeta: NoteMeta[] = []; private indexMeta: NoteMeta | null = null; private searchIndex: Map = new Map(); + private rootMeta: NoteMeta | null = null; prepareMeta(metaFile: NoteMetaFile): void { const assets = [ @@ -50,6 +51,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider { noImport: true, dataFileName: "index.html" }; + this.rootMeta = metaFile.files[0]; metaFile.files.push(this.indexMeta); } @@ -58,7 +60,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider { if (!noteMeta?.notePath?.length) { throw new Error("Missing note path."); } - const basePath = "../".repeat(noteMeta.notePath.length - 1); + const basePath = "../".repeat(Math.max(0, noteMeta.notePath.length - 2)); let searchContent = ""; if (note) { @@ -71,6 +73,9 @@ export default class ShareThemeExportProvider extends ZipExportProvider { if (typeof content === "string") { content = content.replace(/href="[^"]*\.\/([a-zA-Z0-9_\/]{12})[^"]*"/g, (match, id) => { if (match.includes("/assets/")) return match; + if (id === this.rootMeta?.noteId) { + return `href="${basePath}"`; + } return `href="#root/${id}"`; }); content = this.rewriteFn(content, noteMeta);