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;
}