Bear That Codes

Markdown To Pdf

Published on 4 min read

The problem

I want to have a document which is easily readable by my partner to describe our home network, self hosted services and home automation, presented as a PDF which is accessable on a network share.

I want that document to be easy to create and edit (which means Markdown) and edits to the markdown should automatically generate a new version of the PDF when updates are pushed into my git repo.

I thought this was worth sharing in case anyone else wants to build on this and leverage my approach

My solution

I already have a locally hosted git platform - specifically Forgejo

I already have a forgejo runner VM configured as ubuntu_latest, and a network share to put the files in.

All I needed on top of that was a way to build the Markdown files and convert them to PDF.

At the end of this page is my YAML file which is triggered whenever a change is made to the manual, any images, or the style sheet which defines how the document looks.

The actual process uses Chrome to render the PDF using a node package called md-to-pdf, and it builds the files in the folder in sequential order.

In order to control the order, I prefex each file with a number - more on this later…

Finally the completed PDF files are SFTP’d to my NAS using credentials stored as secrets in the Forgejo actions

 
name: Generate Documentation PDF v2
 
on:
  push:
    branches:
      - main
    paths:
      - 'docs/**' # Trigger when markdown documents change
      - 'styles/**' # Trigger when styles change
  workflow_dispatch: # Allows manual triggering
 
jobs:
  build:
    name: Build Documentation PDFs
    runs-on: ubuntu-latest
    
    steps:
 
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ vars.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
    
      - name: Checkout repository
        uses: actions/checkout@v4
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
 
      - name: Install Chrome dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            ca-certificates \
            fonts-liberation \
            libappindicator3-1 \
            libasound2-dev \
            libatk-bridge2.0-0 \
            libatk1.0-0 \
            libgbm1 \
            libgtk-3-0 \
            libnspr4 \
            libnss3 \
            libx11-xcb1 \
            libxcb1 \
            libxcomposite1 \
            libxcursor1 \
            libxdamage1 \
            libxfixes3 \
            libxi6 \
            libxrandr2 \
            libxrender1 \
            libxss1 \
            libxtst6 \
            xdg-utils \
            lftp \
            zip \
            git
 
        # pdf generator uses md-to-pdf from https://github.com/simonhaenisch/md-to-pdf
      - name: Install md-to-pdf
        run: npm install -g md-to-pdf
 
      - name: Create output directory
        run: mkdir -p output
 
      - name: Generate individual PDFs
        run: |
          # Create output root directory
          mkdir -p output
          
          # Process each markdown file
 
          echo "Generating PDF"
 
          cd docs
 
          for f in *.md; do cat $f; echo; done | md-to-pdf --launch-options='{"headless":"new","args":["--no-sandbox"]}' --stylesheet='../styles/stylesheet.css' > ../output/Manual.pdf
 
          cd ..
 
          echo "Output Folder List"
          # cd ../output
          ls output
 
      - name: Upload Individual PDFs
        uses: actions/upload-artifact@v3
        with:
          name: published-document
          path: output/**/*.pdf
          if-no-files-found: error
      
      - name: Upload via SFTP
        env:
          SFTP_HOST: ${{ secrets.SFTP_HOST}}$
          SFTP_USER: ${{ secrets.SFTP_USER}}$
          SFTP_PASS: ${{ secrets.SFTP_PASS }}
        run: |
          echo "Current Folder"
          cd output
          pwd
          echo "File List"
          ls
          lftp -u "$SFTP_USER","$SFTP_PASS" sftp://$SFTP_HOST << EOF
          set sftp:auto-confirm yes
          put Manual.pdf
          bye
          EOF
 

The Markdown files

As already mentioned, the files are prefixed with a number, which allows me to control the order the pages appear in.

I’ve typically spaced the numbers 10 apart, to allow for inserting sub-sections if required - for example the file structure could look like this

000-Start.md
010-Network.md
020-Truenas.md
030-HomeAssistant.md
031-Zigbee.md
040-Printers.md

Documents with the same number are effectively sections in the manual which will be listed in alphabetical order in that section.

The 000-Start.md file starts with this frontmatter section defining the layout of the file, page size, page footers etc.

 
---
pdf_options:
  format: a4
  margin: 30mm 20mm
  printBackground: false
  footerTemplate: |-
    <section>
      <div>
      Generated <span class="date"></span>
        Page <span class="pageNumber"></span>
        of <span class="totalPages"></span>
      </div>
    </section>
---
 

Each subsequent file then starts with a section break, to force a new page

<div class="page-break"></div>

Then what follows is regular markdown in each file, and can include all common markdown features supported by the md-to-pdf renderer (which uses marked itself) and supports most common Markdown features (headings, lists, tables, images)

The Styling

Finally, in order to improve the printability, I’ve made some small changes to the stylesheet (located in styles/stylesheet.css) to make tables more readable in the printed output and configure the font sizes.

* {
	box-sizing: border-box;
}
 
html {
	font-size: 100%;
}
 
body {
	font-family: 'Ubuntu', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',  'Cantarell',
		'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
	line-height: 1.6;
	font-size: 0.6875em; /* 11 pt */
	color: #111;
	margin: 0;
}
 
table {
	border-spacing: 0;
	border-collapse: collapse;
	display: block;
	margin: 0 0 1em;
	width: 100%;
	overflow: auto;
}
 
table th,
table td {
	padding: 0.5em 1em;
	border: 1px solid gainsboro;
}
 
table th {
	font-weight: 600;
}
 
table tr {
	background-color: white;
	border-top: 1px solid gainsboro;
}
 
table tr:nth-child(2n) {
	background-color: whitesmoke;
}