960 lines
21 KiB
Markdown
960 lines
21 KiB
Markdown
# 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 %}
|
|
<style>
|
|
section { padding: 40px; }
|
|
</style>
|
|
<section>
|
|
<h1>Welcome</h1>
|
|
<p>Page content here.</p>
|
|
</section>
|
|
{% 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
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>About Us | Campaign 2026</title>
|
|
<meta name="description" content="Join our movement for change.">
|
|
<style>
|
|
section { padding: 40px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<section>
|
|
<h1>Welcome</h1>
|
|
<p>Page content here.</p>
|
|
</section>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
**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 %}
|
|
<style>
|
|
/* Page CSS */
|
|
</style>
|
|
<!-- Page HTML -->
|
|
{% endblock %}
|
|
```
|
|
|
|
**Content (STANDALONE mode):**
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<title>About Us</title>
|
|
<style>/* Page CSS */</style>
|
|
</head>
|
|
<body>
|
|
<!-- Page HTML -->
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
**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 ? `<style>\n${css}\n</style>` : '';
|
|
return `{% extends "main.html" %}
|
|
{% block content %}
|
|
${styleBlock}
|
|
${html}
|
|
{% endblock %}
|
|
`;
|
|
}
|
|
|
|
// Usage
|
|
const content = wrapInMaterialOverride(
|
|
'<section><h1>About Us</h1></section>',
|
|
'section { padding: 40px; }'
|
|
);
|
|
|
|
// Result:
|
|
// {% extends "main.html" %}
|
|
// {% block content %}
|
|
// <style>
|
|
// section { padding: 40px; }
|
|
// </style>
|
|
// <section><h1>About Us</h1></section>
|
|
// {% endblock %}
|
|
```
|
|
|
|
---
|
|
|
|
### Standalone Mode Export
|
|
|
|
```typescript
|
|
function wrapInStandaloneDocument(
|
|
html: string,
|
|
css: string | null,
|
|
title: string,
|
|
description: string | null
|
|
): string {
|
|
const metaDesc = description
|
|
? `\n <meta name="description" content="${description.replace(/"/g, '"')}">`
|
|
: '';
|
|
const styleBlock = css ? `\n <style>\n${css}\n </style>` : '';
|
|
|
|
return `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${title.replace(/</g, '<')}</title>${metaDesc}${styleBlock}
|
|
</head>
|
|
<body>
|
|
${html}
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
// Usage
|
|
const content = wrapInStandaloneDocument(
|
|
'<section><h1>About Us</h1></section>',
|
|
'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 '<style>'
|
|
# Should include page CSS
|
|
```
|
|
|
|
3. **Use !important for overrides:**
|
|
```css
|
|
/* In page CSS */
|
|
section {
|
|
padding: 40px !important;
|
|
}
|
|
```
|
|
|
|
4. **Test STANDALONE mode:**
|
|
- Settings → Full page MkDocs (checked)
|
|
- Bypasses Material theme CSS
|
|
|
|
---
|
|
|
|
### Problem: Hide Navigation Not Working
|
|
|
|
**Symptoms:**
|
|
|
|
- Page settings: `mkdocsHideNav=true`
|
|
- Navigation sidebar still shows
|
|
|
|
**Causes:**
|
|
|
|
1. Stub front matter not updated
|
|
2. MkDocs cache not cleared
|
|
3. STANDALONE mode enabled (hide options ignored)
|
|
|
|
**Solutions:**
|
|
|
|
1. **Check stub front matter:**
|
|
```bash
|
|
cat mkdocs/docs/about-us.md
|
|
# Should have:
|
|
# hide:
|
|
# - navigation
|
|
```
|
|
|
|
2. **Re-export:**
|
|
- Edit page settings → Save
|
|
- Triggers stub regeneration
|
|
|
|
3. **Clear MkDocs cache:**
|
|
```bash
|
|
rm -rf mkdocs/site/
|
|
docker compose exec mkdocs mkdocs build
|
|
```
|
|
|
|
4. **Verify not STANDALONE:**
|
|
- Settings → Full page MkDocs (unchecked)
|
|
- STANDALONE ignores hide options
|
|
|
|
---
|
|
|
|
## Performance Considerations
|
|
|
|
### File System I/O
|
|
|
|
**Export operation:** Writes 2 files per page (~1ms each)
|
|
|
|
**Bottleneck:** Synchronous file writes in API request handler
|
|
|
|
**Impact:**
|
|
|
|
- Publish operation: +2ms overhead
|
|
- Batch operations: Linear scaling (10 pages = +20ms)
|
|
|
|
**Optimization (future):**
|
|
|
|
```typescript
|
|
// Current: Synchronous writes in request
|
|
await fs.writeFile(path, content);
|
|
|
|
// Future: Background job queue
|
|
await queue.add('export-page', { pageId });
|
|
```
|
|
|
|
---
|
|
|
|
### MkDocs Build Time
|
|
|
|
**Build duration:** Proportional to page count
|
|
|
|
- 10 pages: ~2 seconds
|
|
- 100 pages: ~10 seconds
|
|
- 1000 pages: ~90 seconds
|
|
|
|
**Optimization:**
|
|
|
|
- Use `mkdocs serve --dirtyreload` in dev (incremental builds)
|
|
- Production builds: Full rebuild recommended
|
|
|
|
---
|
|
|
|
## Security Considerations
|
|
|
|
### Path Traversal Protection
|
|
|
|
**Validation:**
|
|
|
|
1. **Null byte check:** Prevents `about\0.html` attacks
|
|
2. **Normalization:** `path.normalize()` resolves `../`
|
|
3. **Absolute path check:** Rejects `/etc/passwd.html`
|
|
4. **Encoded traversal:** Blocks `%2e%2e/admin.html`
|
|
5. **Extension validation:** Must end with `.html`
|
|
|
|
**Rejected Paths:**
|
|
|
|
- `../../../etc/passwd.html`
|
|
- `/var/www/config.html`
|
|
- `admin%2e%2e%2fconfig.html`
|
|
- `about.md` (wrong extension)
|
|
|
|
---
|
|
|
|
### File Permission Isolation
|
|
|
|
**Docker Volume Mount:**
|
|
|
|
```yaml
|
|
volumes:
|
|
- ./mkdocs:/mkdocs:rw
|
|
```
|
|
|
|
**Permissions:**
|
|
|
|
- API container writes as `node` user (UID 1000)
|
|
- Host user must have write access to `mkdocs/docs/`
|
|
- MkDocs container reads as `mkdocs` user (UID 1001)
|
|
|
|
**Risk:** Container escape could write arbitrary files
|
|
|
|
**Mitigation:**
|
|
|
|
- API container runs as non-root user
|
|
- Volume mount scoped to `/mkdocs` only (no host root access)
|
|
|
|
---
|
|
|
|
### Template Injection
|
|
|
|
**Risk:** Malicious admin injects Jinja2 code
|
|
|
|
**Example:**
|
|
|
|
```html
|
|
<!-- Malicious HTML in editor -->
|
|
<h1>{{ config.site_name }}</h1>
|
|
```
|
|
|
|
**Rendering:**
|
|
|
|
- THEMED mode: Jinja2 processes `{{ }}` expressions
|
|
- Could expose MkDocs config or Material theme internals
|
|
|
|
**Mitigation:**
|
|
|
|
- **Accepted risk:** Admins are trusted users
|
|
- Template code only renders in MkDocs (isolated from main app)
|
|
- Public users cannot edit landing pages
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
### Frontend Components
|
|
|
|
- **[LandingPagesPage](/v2/frontend/pages/LandingPagesPage)** — Export buttons + validation
|
|
- **[PageEditorPage](/v2/frontend/pages/LandingPageEditor)** — Auto-export on publish
|
|
|
|
### Backend Modules
|
|
|
|
- **[pages.service](/v2/backend/modules/pages/pages.service)** — Export logic (`exportToMkDocs`, `validateExports`, `syncOverrides`)
|
|
- **[pages-admin.routes](/v2/backend/modules/pages/pages-admin.routes)** — `/sync` and `/validate` endpoints
|
|
|
|
### Features
|
|
|
|
- **[Page Builder](page-builder.md)** — Landing page system overview
|
|
- **[GrapesJS Editor](grapes-editor.md)** — Editor integration
|
|
- **[Block Library](block-library.md)** — Reusable blocks
|
|
|
|
### MkDocs Resources
|
|
|
|
- **[MkDocs Material Templates](https://squidfunk.github.io/mkdocs-material/customization/)** — Theme customization
|
|
- **[Jinja2 Documentation](https://jinja.palletsprojects.com/)** — Template syntax
|
|
- **[MkDocs Configuration](https://www.mkdocs.org/user-guide/configuration/)** — mkdocs.yml reference
|
|
|
|
### Deployment
|
|
|
|
- **[Docker Setup](/v2/deployment/docker)** — Volume mounts + permissions
|
|
- **[MkDocs Service](/v2/deployment/services/mkdocs)** — Container configuration
|