--- name: mkdocs-github-pages-deployment description: Use when deploying MkDocs documentation to GitHub Pages with GitHub Actions - covers Python-Markdown gotchas (indentation, footnotes, grid tables), workflow configuration, and systematic markdown fixing --- # MkDocs + GitHub Pages Deployment with GitHub Actions ## Overview This skill documents lessons learned from deploying MkDocs documentation to GitHub Pages using GitHub Actions, including common pitfalls and their solutions. --- ## GitHub Pages Deployment Methods ### Method 1: Legacy `mkdocs gh-deploy` (Deprecated) ```yaml - name: Deploy to GitHub Pages run: mkdocs gh-deploy --force --clean --verbose ``` **Issues:** - Uses legacy deployment method - Requires write access to gh-pages branch - Less transparent in GitHub UI - Harder to debug ### Method 2: GitHub Actions (Recommended) ✅ ```yaml permissions: contents: write pages: write # Required for GitHub Pages id-token: write # Required for OIDC authentication concurrency: group: "pages" cancel-in-progress: false jobs: build: runs-on: ubuntu-latest steps: - name: Build MkDocs site run: mkdocs build --site-dir output/site - name: Upload artifact for GitHub Pages uses: actions/upload-pages-artifact@v3 with: path: output/site deploy: needs: build runs-on: ubuntu-latest environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ``` **Benefits:** - Modern GitHub Actions workflow - Proper environment tracking - Deployment URL visible in workflow - Better security with OIDC - Clear separation of build and deploy stages --- ## Repository Settings **CRITICAL:** In GitHub repository settings: 1. Go to **Settings → Pages** 2. Under **Source**, select **"GitHub Actions"** (not "Deploy from a branch") 3. This enables the modern deployment method --- ## MkDocs Configuration Issues and Solutions ### Issue 1: Grid Tables Not Rendering **Problem:** Pandoc-style grid tables (used in policy documents) not rendering correctly. **Example of Grid Table Syntax:** ```markdown +-----------------+-----------------+--------------------------------------------------------------+ | **Consequence** | **Consequence** | **Description** | | **Level** | **Score** | | +=================+=================+==============================================================+ | Low | 0 | Loss of confidentiality... | +-----------------+-----------------+--------------------------------------------------------------+ ``` **Solution:** Add `markdown-grid-tables` extension ```yaml # mkdocs.yml markdown_extensions: - markdown_grid_tables # Supports Pandoc/reStructuredText grid tables ``` **Installation:** ```bash pip install markdown-grid-tables ``` **In GitHub Actions:** ```yaml - name: Install MkDocs and extensions run: | pip install mkdocs-material markdown-grid-tables ``` --- ### Issue 2: Content Rendered as Code Blocks Instead of Lists **Problem:** Nested list items and bullets rendering with line numbers in gray code blocks. **Root Cause:** 1. **4-space indentation creates code blocks** in Markdown (not continuation of lists) 2. **Python-Markdown doesn't recognize `i.` as list markers** (roman numerals not supported) **Example of Broken Markdown:** ```markdown a. **Side-Channel Attack Mitigations:** i. **Timing Attacks:** - All cryptographic operations must be constant-time - Hardware wallets use secure elements ``` **What Happens:** - The ` i. **Timing Attacks:**` (4 spaces) is treated as a code block - Renders with line numbers and gray background - Not formatted as a list item **Solution Part 1: Fix Indentation** Change 4-space indent to 3-space indent: ```markdown a. **Side-Channel Attack Mitigations:** i. **Timing Attacks:** - All cryptographic operations must be constant-time - Hardware wallets use secure elements ``` **Solution Part 2: Use Standard List Markers** Convert `i.` to `1.` (Python-Markdown recognizes numbered lists): ```markdown a. **Side-Channel Attack Mitigations:** 1. **Timing Attacks:** - All cryptographic operations must be constant-time - Hardware wallets use secure elements ``` **Automation Script:** ```python #!/usr/bin/env python3 import re from pathlib import Path def fix_lists(content): """Fix list markers and indentation.""" lines = content.split('\n') fixed_lines = [] for line in lines: # Replace 'i.' with '1.' at any indentation if re.match(r'^(\s+)i\.\s', line): fixed_line = re.sub(r'^(\s+)i\.', r'\g<1>1.', line) fixed_lines.append(fixed_line) else: fixed_lines.append(line) return '\n'.join(fixed_lines) # Apply to all markdown files for md_file in Path('policies').glob('*.md'): content = md_file.read_text() fixed = fix_lists(content) md_file.write_text(fixed) ``` --- ### Issue 3: Footnotes Only Showing First Line **Problem:** Footnotes/endnotes render with only the heading visible, missing detailed WHAT/WHY/WHERE content. **Root Cause:** Python-Markdown footnotes extension requires ALL continuation lines (after the first line) to be indented with exactly 4 spaces. **Example of Broken Markdown:** ```markdown [^15]: ### Side-Channel Attack Mitigations #### WHAT: Requirement Definition Protection against timing attacks... ``` **Result:** Only "### Side-Channel Attack Mitigations" renders in the footnote. **Solution:** Indent ALL continuation lines with 4 spaces: ```markdown [^15]: ### Side-Channel Attack Mitigations #### WHAT: Requirement Definition Protection against timing attacks... #### WHY: Security Rationale - **Timing attacks**: Details here... ``` **Automated Fix:** See `~/.claude/skills/mkdocs-footnotes-formatting.md` for comprehensive guide and automation scripts. **Quick Fix Script:** ```python import re from pathlib import Path def fix_footnote_indentation(content: str) -> str: lines = content.split('\n') result = [] in_footnote = False for line in lines: if re.match(r'^\[\^[0-9]+\]:', line): in_footnote = True result.append(line) continue if in_footnote: if (line.strip() == '---' or re.match(r'^\[\^[0-9]+\]:', line) or re.match(r'^#{1,2}\s+[^#]', line)): in_footnote = False result.append(line) continue if line.startswith(' ') or line.strip() == '': result.append(line) else: result.append(' ' + line) else: result.append(line) return '\n'.join(result) ``` **Impact:** 12 files fixed, 855 lines indented for proper footnote rendering. --- ### Issue 5: WHERE Section Citations Not Using Bullet Points **Problem:** Citation paragraphs in WHERE: Authoritative Sources sections lack visual hierarchy, making it difficult to distinguish individual sources. **Example of Problematic Formatting:** ```markdown #### WHERE: Authoritative Sources Citation text here — Source, Date Another citation — Source, Date ``` **Result:** Renders as plain paragraphs without clear separation between citations. **Solution:** Convert citations to bulleted lists: ```markdown #### WHERE: Authoritative Sources - Citation text here — Source, Date - Another citation — Source, Date ``` **Automated Fix:** ```python import re from pathlib import Path def fix_where_citations(content: str) -> str: """Convert WHERE section citations to bullet points.""" lines = content.split('\n') result = [] in_where_section = False i = 0 while i < len(lines): line = lines[i] if '#### WHERE: Authoritative Sources' in line: in_where_section = True result.append(line) i += 1 # Skip blank line after header if i < len(lines) and lines[i].strip() == '': result.append(lines[i]) i += 1 # Process citations while i < len(lines): current_line = lines[i] # Exit on section boundary if (current_line.strip() == '---' or current_line.strip().startswith('####') or re.match(r'^\[\^[0-9]+\]:', current_line)): in_where_section = False break # Add bullet to citation text if (current_line.strip() != '' and not current_line.lstrip().startswith('—') and not current_line.lstrip().startswith('-')): indent = len(current_line) - len(current_line.lstrip()) citation_text = current_line.strip() result.append(' ' * indent + '- ' + citation_text + ' ') i += 1 # Handle attribution line (starts with —) if i < len(lines) and lines[i].lstrip().startswith('—'): attribution_line = lines[i] attr_indent = len(attribution_line) - len(attribution_line.lstrip()) attribution_text = attribution_line.strip() result.append(' ' * (indent + 2) + attribution_text) i += 1 continue result.append(current_line) i += 1 else: result.append(line) i += 1 return '\n'.join(result) ``` **Impact:** Better visual hierarchy, clearer source attribution, proper HTML `