# MkDocs Export Integration Export landing pages to MkDocs Material theme with Jinja2 template wrapping, front matter configuration, and synchronized stub files. --- ## Overview The MkDocs Export system bridges the Page Builder and static documentation site. Administrators can publish landing pages to the main MkDocs site, where they benefit from Material theme styling, navigation, and SEO features. ### Key Features - **Two Export Modes**: THEMED (extends Material theme) vs STANDALONE (full HTML document) - **Jinja2 Template Wrapping**: Integrates with MkDocs Material theme inheritance - **Front Matter Configuration**: Control navigation, table of contents visibility - **Dual-File Output**: HTML override + Markdown stub - **Automatic Sync**: Export triggered on publish, cleanup on unpublish - **Path Validation**: Prevents directory traversal attacks - **Stub Backfill**: Repair missing files via "Validate Exports" --- ## Architecture ```mermaid graph TD A[Admin] -->|Publish Page| B[PUT /api/pages/:id] B --> C[pages.service.update] C --> D{published && !skipExport?} D -->|Yes| E[exportToMkDocs] E --> F[wrapInMaterialOverride] E --> G[generateMdStub] F --> H[Write .html to overrides/] G --> I[Write .md to docs/] J[MkDocs Build] --> K[Read .md stub] K --> L[Front matter: template] L --> H H --> M[Render with Material theme] M --> N[Public site] style E fill:#9d4edd style H fill:#3498db style N fill:#2ecc71 ``` **Flow:** 1. **Trigger**: Admin publishes page (or updates published page) 2. **Service**: `pages.service.update()` checks publish status 3. **Export**: Calls `exportToMkDocs()` with page data 4. **Wrap**: HTML wrapped in Jinja2 `{% extends "main.html" %}` 5. **Write**: Two files created: - `mkdocs/docs/overrides/{slug}.html` — HTML override - `mkdocs/docs/{slug}.md` — Markdown stub 6. **Build**: MkDocs rebuild (`mkdocs build`) 7. **Render**: Stub references override, Material theme applies 8. **Serve**: Page accessible at `https://cmlite.org/pages/{slug}/` --- ## Export Modes ### THEMED Mode (Default) **Purpose:** Integrate page with MkDocs Material theme (header, footer, navigation) **Jinja2 Template:** ```jinja2 {% extends "main.html" %} {% block content %}

Welcome

Page content here.

{% endblock %} ``` **Features:** - Uses Material theme header/footer - Respects site navigation - Table of contents auto-generated - Search integration works - Responsive design inherited **Use Cases:** - Documentation pages - Campaign info pages - Community guidelines --- ### STANDALONE Mode **Purpose:** Full control over HTML (no MkDocs chrome) **HTML Document:** ```html About Us | Campaign 2026

Welcome

Page content here.

``` **Features:** - No Material theme elements - Custom head section (meta tags, styles) - Independent from site navigation - Full design freedom **Use Cases:** - Marketing landing pages (like `lander.html`) - Event registration pages - Embedded pages (iframes) --- ## File Outputs ### Override File (.html) **Location:** `mkdocs/docs/overrides/{slug}.html` **Example:** `mkdocs/docs/overrides/about-us.html` **Content (THEMED mode):** ```jinja2 {% extends "main.html" %} {% block content %} {% endblock %} ``` **Content (STANDALONE mode):** ```html About Us ``` **Access Control:** - Readable by MkDocs build process - Not directly served (accessed via stub) --- ### Stub File (.md) **Location:** `mkdocs/docs/{slug}.md` **Example:** `mkdocs/docs/about-us.md` **Content:** ```markdown --- template: about-us.html title: "About Us | Campaign 2026" description: "Join our movement for change." hide: - navigation - toc --- ``` **Front Matter Fields:** | Field | Type | Description | |-------|------|-------------| | `template` | string | Override filename (relative to `custom_dir`) | | `title` | string | Page title (from `seoTitle` or `title`) | | `description` | string | Meta description (from `seoDescription`) | | `hide` | array | Hide navigation/toc elements | **Important:** Template path is relative to `custom_dir` (`mkdocs/overrides/`). Use `about-us.html`, NOT `overrides/about-us.html` (causes `TemplateNotFound` error). --- ## Database Fields ### LandingPage Export Configuration **Fields:** | Field | Type | Default | Description | |-------|------|---------|-------------| | `mkdocsPath` | String? | `{slug}.html` | Override filename (auto-generated from slug) | | `mkdocsStubPath` | String? | `{slug}.md` | Stub filename (derived from mkdocsPath) | | `mkdocsExportMode` | Enum | `THEMED` | `THEMED` or `STANDALONE` | | `mkdocsHideNav` | Boolean | `false` | Hide navigation sidebar (THEMED only) | | `mkdocsHideToc` | Boolean | `false` | Hide table of contents (THEMED only) | | `mkdocsSkipExport` | Boolean | `false` | Skip MkDocs export entirely | **Behavior:** - **On publish (`published=true`)**: - If `mkdocsSkipExport=false`: Export files - If `mkdocsSkipExport=true`: No export (page only at `/p/:slug`) - **On unpublish (`published=false`)**: Remove export files - **On title change**: Regenerate slug, update `mkdocsPath`, clean up old files --- ## Admin Workflow ### Exporting a Page **Automatic Export (on publish):** 1. Admin → Pages → Click "Publish" button 2. API updates `published=true` 3. Service checks `mkdocsSkipExport` 4. If `false`: Calls `exportToMkDocs()` 5. Files written to disk 6. Database updated with `mkdocsStubPath` **Manual Export Trigger:** 1. Edit page settings 2. Change `mkdocsExportMode` or `mkdocsHideNav` 3. Save settings 4. If published: Auto re-exports --- ### Configuring Export Options **Location:** Page Settings modal → MkDocs Integration section **Steps:** 1. Admin → Pages → Click gear icon (Settings) 2. Scroll to "MkDocs Integration" 3. Configure options: - **Skip MkDocs Export**: ☐ (unchecked) - **Override Path**: `about.html` (auto-filled) - **Full page MkDocs**: ☐ (THEMED mode) - **Hide navigation sidebar**: ☑ (checked) - **Hide table of contents**: ☑ (checked) 4. Click "Save" 5. If published: Files re-exported immediately --- ### Rebuilding MkDocs Site **Trigger:** After exporting pages **Methods:** **Option 1: Admin UI** 1. Admin → Pages → "Build Site" button (SUPER_ADMIN only) 2. Confirmation modal appears 3. Click "Confirm" 4. API executes `docker compose exec mkdocs mkdocs build` 5. Success notification **Option 2: Command Line** ```bash docker compose exec mkdocs mkdocs build # Rebuilds site from mkdocs/docs/ directory # Output: mkdocs/site/ (static HTML) ``` **Auto-rebuild:** Not implemented (manual trigger required) --- ### Syncing Overrides **Purpose:** Import hand-coded `.html` files from `overrides/` directory **Workflow:** 1. Place `.html` file in `mkdocs/docs/overrides/custom.html` 2. Admin → Pages → "Sync Overrides" button 3. API scans directory: - Untracked files → Create CODE-mode page - Tracked CODE-mode pages → Update `htmlOutput` from disk - VISUAL pages → Skip (managed by GrapesJS) 4. Backfills missing `.md` stubs 5. Shows result: `Synced: 2 imported, 1 updated, 3 stubs created` **Use Cases:** - Migrate legacy templates - Import designer-created HTML - Restore after file system corruption --- ### Validating Exports **Purpose:** Verify files exist on disk, repair if missing **Workflow:** 1. Admin → Pages → "Validate Exports" button 2. API queries all published, non-skipped pages 3. For each page: - Check `.html` override exists - Check `.md` stub exists - If either missing: Re-export 4. Shows result: `Validated 10 pages: 2 repaired, 0 errors` **Use Cases:** - Recover from accidental deletion - Fix state after container restarts - Audit before production deploy --- ## Code Examples ### Themed Mode Export ```typescript // From pages.service.ts function wrapInMaterialOverride(html: string, css: string | null): string { const styleBlock = css ? `` : ''; return `{% extends "main.html" %} {% block content %} ${styleBlock} ${html} {% endblock %} `; } // Usage const content = wrapInMaterialOverride( '

About Us

', 'section { padding: 40px; }' ); // Result: // {% extends "main.html" %} // {% block content %} // //

About Us

// {% endblock %} ``` --- ### Standalone Mode Export ```typescript function wrapInStandaloneDocument( html: string, css: string | null, title: string, description: string | null ): string { const metaDesc = description ? `\n ` : ''; const styleBlock = css ? `\n ` : ''; return ` ${title.replace(/</g, '<')}${metaDesc}${styleBlock} ${html} `; } // Usage const content = wrapInStandaloneDocument( '

About Us

', 'section { padding: 40px; }', 'About Us | Campaign 2026', 'Join our movement for change.' ); ``` --- ### Markdown Stub Generation ```typescript interface StubOptions { overrideFilename: string; title: string; description: string | null; hideNav: boolean; hideToc: boolean; } function generateMdStub(opts: StubOptions): string { const hideItems: string[] = []; if (opts.hideNav) hideItems.push(' - navigation'); if (opts.hideToc) hideItems.push(' - toc'); const hideBlock = hideItems.length > 0 ? `hide:\n${hideItems.join('\n')}\n` : ''; const descLine = opts.description ? `description: "${opts.description.replace(/"/g, '\\"')}"\n` : ''; return `--- template: ${opts.overrideFilename} ${hideBlock}title: "${opts.title.replace(/"/g, '\\"')}" ${descLine}--- `; } // Usage const stub = generateMdStub({ overrideFilename: 'about-us.html', title: 'About Us | Campaign 2026', description: 'Join our movement.', hideNav: true, hideToc: true, }); // Result: // --- // template: about-us.html // hide: // - navigation // - toc // title: "About Us | Campaign 2026" // description: "Join our movement." // --- ``` --- ### Export Orchestration ```typescript // From pages.service.update() // After updating page in database if (page.published && !page.mkdocsSkipExport && page.mkdocsPath && page.htmlOutput) { const stubPath = await exportToMkDocs({ mkdocsPath: page.mkdocsPath, html: page.htmlOutput, css: page.cssOutput, editorMode: page.editorMode, exportMode: page.mkdocsExportMode, title: page.title, seoTitle: page.seoTitle, seoDescription: page.seoDescription, hideNav: page.mkdocsHideNav, hideToc: page.mkdocsHideToc, }); // Store stubPath if changed if (stubPath !== page.mkdocsStubPath) { await prisma.landingPage.update({ where: { id }, data: { mkdocsStubPath: stubPath }, }); } } else if ((!page.published || page.mkdocsSkipExport) && existing.mkdocsPath) { // Clean up exports on unpublish or skip await removeFromMkDocs(existing.mkdocsPath, existing.mkdocsStubPath); if (existing.mkdocsStubPath) { await prisma.landingPage.update({ where: { id }, data: { mkdocsStubPath: null }, }); } } ``` --- ### Path Validation ```typescript function validateMkdocsPath(mkdocsPath: string): void { // Check for null bytes if (mkdocsPath.includes('\0')) { throw new AppError(400, 'Invalid path: null byte detected', 'INVALID_MKDOCS_PATH'); } // Normalize and check for traversal const normalized = path.normalize(mkdocsPath); if (normalized.includes('..') || path.isAbsolute(normalized)) { throw new AppError(400, 'Path traversal not allowed', 'INVALID_MKDOCS_PATH'); } // Check for encoded traversal sequences if (mkdocsPath.includes('%2e') || mkdocsPath.includes('%2E')) { throw new AppError(400, 'Encoded path traversal not allowed', 'INVALID_MKDOCS_PATH'); } if (!mkdocsPath.endsWith('.html')) { throw new AppError(400, 'Path must end with .html', 'INVALID_MKDOCS_PATH'); } } // Safe paths validateMkdocsPath('about.html'); // ✓ validateMkdocsPath('pages/contact.html'); // ✓ // Rejected paths validateMkdocsPath('../etc/passwd.html'); // ✗ Path traversal validateMkdocsPath('/etc/shadow.html'); // ✗ Absolute path validateMkdocsPath('admin%2e%2e/config.html'); // ✗ Encoded traversal validateMkdocsPath('about.md'); // ✗ Missing .html extension ``` --- ## MkDocs Configuration ### mkdocs.yml Settings **Required Configuration:** ```yaml site_name: Changemaker Lite theme: name: material custom_dir: overrides # Points to mkdocs/docs/overrides/ nav: - Home: index.md - Pages: - About: about-us.md - Contact: contact.md ``` **Key Points:** - `custom_dir: overrides` — Enables template overrides - Stub files must be listed in `nav` to appear in navigation - Unlisted stubs still accessible via direct URL ### Template Search Paths **MkDocs Material searches:** 1. `mkdocs/overrides/` (custom_dir) 2. Material theme templates 3. MkDocs core templates **Resolution:** ```yaml # In stub front matter template: about-us.html # MkDocs searches: # 1. mkdocs/overrides/about-us.html ✓ (found here) # 2. material/templates/about-us.html # 3. mkdocs/templates/about-us.html ``` **Common Mistake:** ```yaml # WRONG - causes TemplateNotFound template: overrides/about-us.html # MkDocs searches: # 1. mkdocs/overrides/overrides/about-us.html ✗ (not found) ``` **Solution:** Use filename only, not path with `overrides/`. --- ## Troubleshooting ### Problem: Template Not Found Error **Symptoms:** - MkDocs build fails - Error: `jinja2.exceptions.TemplateNotFound: overrides/about-us.html` **Causes:** 1. Stub uses `template: overrides/about-us.html` (incorrect path) 2. `custom_dir` not configured in `mkdocs.yml` 3. Override file doesn't exist **Solutions:** 1. **Fix stub front matter:** ```yaml # Before (wrong) template: overrides/about-us.html # After (correct) template: about-us.html ``` 2. **Verify custom_dir:** ```yaml # In mkdocs.yml theme: name: material custom_dir: overrides ``` 3. **Check file exists:** ```bash ls -la mkdocs/docs/overrides/about-us.html # Should exist if page published ``` 4. **Validate exports:** - Admin → Pages → "Validate Exports" - Repairs missing files --- ### Problem: Export Files Missing After Restart **Symptoms:** - Pages were published before restart - After `docker compose restart`: Files gone - MkDocs build fails **Causes:** 1. Volume mount not configured 2. Files written to container filesystem (not host) 3. Container recreated (ephemeral storage lost) **Solutions:** 1. **Check volume mount:** ```yaml # In docker-compose.yml services: api: volumes: - ./mkdocs:/mkdocs:rw # Must have :rw for write access ``` 2. **Verify host files:** ```bash ls -la mkdocs/docs/overrides/ # Files should persist on host filesystem ``` 3. **Re-export all pages:** - Admin → Pages → "Validate Exports" - Regenerates all missing files --- ### Problem: Page Not Appearing in MkDocs Site **Symptoms:** - Page published, files exist - MkDocs builds successfully - Page shows 404 on site **Causes:** 1. Stub not listed in `mkdocs.yml` nav 2. MkDocs not rebuilt after export 3. Nginx cache serving old version **Solutions:** 1. **Add to nav (optional):** ```yaml nav: - Pages: - About: about-us.md # Stub filename ``` 2. **Rebuild MkDocs:** ```bash docker compose exec mkdocs mkdocs build # Or Admin → Pages → "Build Site" ``` 3. **Clear Nginx cache:** ```bash docker compose exec nginx nginx -s reload ``` 4. **Test direct access:** ```bash curl http://localhost:4001/pages/about-us/ # Should return HTML, not 404 ``` --- ### Problem: Styles Not Applying in MkDocs **Symptoms:** - Page renders in GrapesJS editor - MkDocs site shows unstyled content **Causes:** 1. CSS not exported (CODE mode without `cssOutput`) 2. Material theme CSS conflicts 3. Inline styles overridden **Solutions:** 1. **Check cssOutput field:** ```sql SELECT css_output FROM landing_pages WHERE slug = 'about-us'; -- Should contain CSS, not NULL ``` 2. **Inspect rendered HTML:** ```bash curl http://localhost:4001/pages/about-us/ | grep '