diff --git a/Makefile b/Makefile
index 078ac41..75e1a23 100755
--- a/Makefile
+++ b/Makefile
@@ -16,6 +16,7 @@ CACHE_DIR=_cache
 
 ## Internal directories
 CACHE_SLIDES_DIR=$(CACHE_DIR)/slides
+CACHE_DOCS_DIR=$(CACHE_DIR)/docs
 
 ## Output directories
 BUILD_SLIDES_DIR=$(BUILD_DIR)/slides
diff --git a/tasks/docs.mk b/tasks/docs.mk
index 257d80b..a8ddcf4 100644
--- a/tasks/docs.mk
+++ b/tasks/docs.mk
@@ -7,51 +7,51 @@ prepare-docs: ## install prerequisites for static docs site only
 .PHONY: prepare-docs
 prepare: prepare-docs
 
+.PHONY: sync-docs-internal
 sync-docs-internal:
 	@>&2 echo "ERROR: not implemented"
 	exit 1
 
+.PHONY: clean-docs
 clean-docs: ## remove generated static docs site
 	rm -fr $(BUILD_DOCS_DIR) 
 
 .PHONY: clean-docs
 clean: clean-docs
 
-# deploy-docs: ## deploy static docs site to github
-# 	git push $(DEPLOY_REPO)
-# 	pipenv run mkdocs gh-deploy $(DEPLOY_OPTS)
-
-# .PHONY: deploy-docs
-# deploy: deploy-docs
-
 build-docs-pdf:  ## build pdf docs only
 	mkdir -p $(BUILD_DOCS_DIR)
-	rm -f $(BUILD_DOCS_DIR)/combined.pdf
-	PYTHONUTF8=1 \
-		ENABLE_PDF_EXPORT=1 \
-         pipenv run mkdocs build \
-            --site-dir $(BUILD_DOCS_DIR)
-	pdftk \
-		$$(find -L $(BUILD_DOCS_DIR) -name *.pdf -not -name index.pdf |sort ) \
-        cat output $(BUILD_DOCS_DIR)/docs.pdf
+	mkdir -p $(CACHE_DOCS_DIR)
+	./utils/docs/build_combined.sh $(DOCS_DIR) $(CACHE_DOCS_DIR)/combined.md
+	./utils/docs/build_metadata.sh mkdocs.yml $(CACHE_DOCS_DIR)/metadata.yml
+	./utils/docs/build_pdf.py $(CACHE_DOCS_DIR)/metadata.yml $(CACHE_DOCS_DIR)/combined.md $(BUILD_DOCS_DIR)/docs.pdf
+	# rm -f $(BUILD_DOCS_DIR)/combined.pdf
+	# PYTHONUTF8=1 \
+	# 	ENABLE_PDF_EXPORT=1 \
+    #      pipenv run mkdocs build \
+    #         --site-dir $(BUILD_DOCS_DIR)
+	# pdftk \
+	# 	$$(find -L $(BUILD_DOCS_DIR) -name *.pdf -not -name index.pdf |sort ) \
+    #     cat output $(BUILD_DOCS_DIR)/docs.pdf
 
 .PHONY: build-docs-pdf
 build-docs: build-docs-pdf
 
+.PHONY: build-docs-html
 build-docs-html:  ## build static docs site only
 	mkdir -p $(BUILD_DOCS_DIR)
 	pipenv run mkdocs build \
 		--site-dir $(BUILD_DOCS_DIR)
 
-.PHONY: build-docs-html
 build-docs: build-docs-html
 
 build: build-docs
 
+.PHONY: watch-docs-internal
 watch-docs-internal:
 	pipenv run mkdocs serve --dev-addr 0.0.0.0:$(DOCS_PORT)
-.PHONY: watch-docs-internal
 
+.PHONY: watch-docs
 watch-docs: ## run development server for static docs site
 	pipenv run honcho start watch-docs watch-toc
 
@@ -59,8 +59,7 @@ build-pdf: build-docs-pdf ## build docs as PDF files
 build-html: build-docs-html ## build docs as HTML files
 build-docs: build-docs-pdf build-docs-html  ## build only docs as PDF and HTML
 
+.PHONY: serve-docs
 serve-docs: watch-docs
 
-.PHONY: watch-docs serve-docs
-.PHONY: watch-docs-internal
 
diff --git a/utils/docs/blockquote.tex b/utils/docs/blockquote.tex
new file mode 100644
index 0000000..b53403b
--- /dev/null
+++ b/utils/docs/blockquote.tex
@@ -0,0 +1,7 @@
+% blockquote.tex
+% Stylish blockquote setup
+\usepackage{tcolorbox} % Load the tcolorbox package for creating colored boxes
+ % Define a new tcolorbox environment named 'myquote' with specified colors
+\newtcolorbox{myquote}{colback=red!5!white, colframe=red!75!black}
+% Redefine the standard 'quote' environment to use 'myquote'
+\renewenvironment{quote}{\begin{myquote}}{\end{myquote}} 
diff --git a/utils/docs/build_combined.sh b/utils/docs/build_combined.sh
new file mode 100755
index 0000000..04a7e6f
--- /dev/null
+++ b/utils/docs/build_combined.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+set -ue
+
+INPUT_DIR="$1"
+OUTPUT_FILE="$2"
+
+gx_usage() {
+	echo "Usage: $0 <input_dir> <output_file>"
+	echo ""
+}
+
+if [ -z "$INPUT_DIR" ]; then
+	gx_usage
+	exit 1
+fi
+if [ -z "$OUTPUT_FILE" ]; then
+	gx_usage
+	exit 1
+fi
+
+find -L "$INPUT_DIR" -regextype sed \( -regex '.*/[0-9][^/]*\.md' ! -regex '.*/_.*' \) -print0 \
+	| sort -z \
+	| xargs -0 -iFILE cat FILE \
+	| sed -e 's/^\s*----\s*$//' -e 's,\[\](.*/images/,[](./images/,' \
+	> "$OUTPUT_FILE"
+
diff --git a/utils/docs/build_metadata.sh b/utils/docs/build_metadata.sh
new file mode 100755
index 0000000..b3bcbc8
--- /dev/null
+++ b/utils/docs/build_metadata.sh
@@ -0,0 +1,33 @@
+#!/bin/sh
+
+set -ue
+
+MKDOCS_YML_FILE="${1:-}"
+OUTPUT_FILE="${2:-}"
+
+gx_usage() {
+	echo "Usage: $0 <mkdocs_yml_file> <output_file>"
+	echo ""
+}
+
+if [ -z "$MKDOCS_YML_FILE" ]; then
+	gx_usage
+	exit 1
+fi
+
+if [ -z "$OUTPUT_FILE" ]; then
+	gx_usage
+	exit 1
+fi
+
+TITLE="$(yq -r '.site_name' < "$MKDOCS_YML_FILE")"
+AUTHOR="$(yq -r '.site_author' < "$MKDOCS_YML_FILE")"
+
+cat > "$OUTPUT_FILE" <<-MARK
+---
+title: Formation
+subtitle: "$TITLE"
+author: "$AUTHOR"
+date: \today
+MARK
+
diff --git a/utils/docs/build_pdf.py b/utils/docs/build_pdf.py
new file mode 100755
index 0000000..c497be5
--- /dev/null
+++ b/utils/docs/build_pdf.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+
+# Usage: ./build.py INPUT_DIR OUTPUT_FILE
+
+import os
+# import re
+import sys
+import pypandoc
+
+
+def gx_usage():
+    """Show usage"""
+    print(f"Usage: {sys.argv[0]} METADATA_FILE INPUT_FILE OUTPUT_FILE")
+    print()
+
+
+if len(sys.argv) != 4:
+    gx_usage()
+
+metadata_file = sys.argv[1]
+if not os.path.exists(metadata_file):
+    print(f"Metadata file not found: {metadata_file}")
+    sys.exit(1)
+
+input_file = sys.argv[2]
+if not os.path.exists(input_file):
+    print(f"Input file not found: {input_file}")
+    sys.exit(1)
+
+# Get second
+output_file = sys.argv[3]
+if os.path.exists(output_file):
+    print(f"Output file already exists: {output_file}")
+    sys.exit(1)
+
+# Build the pandoc options as a string
+pandoc_cmd = [
+    "--toc",
+    "--number-sections",
+    "--include-in-header", "utils/docs/main.tex",
+    "--metadata-file", metadata_file,
+    # "-V", "linkcolor:blue",
+    # "-V", "geometry:a4paper",
+    # "-V", "geometry:margin=1.8cm",
+    "-V", "mainfont=DejaVu Serif",
+    "-V", "monofont=SauceCodePro Nerd Font",
+    "--pdf-engine=pdflatex",
+    "--filter=utils/docs/filter-nobg.hs",
+]
+
+# from glob import glob
+# input_files = [os.path.join(dp, f) for dp, dn, filenames in os.walk(input_dir) for f in filenames if re.search(r'^[0-9].*\.md$', f)]
+# input_files.sort()
+
+# Convert all markdown files in the chapters/ subdirectory.
+pypandoc.convert_file(
+    [input_file],
+    "pdf",
+    outputfile=output_file,
+    extra_args=pandoc_cmd,
+)
+
+print(f"Conversion completed. Output saved to: {output_file}")
+
+#
diff --git a/utils/docs/chapter_breaks.tex b/utils/docs/chapter_breaks.tex
new file mode 100644
index 0000000..b8fc881
--- /dev/null
+++ b/utils/docs/chapter_breaks.tex
@@ -0,0 +1,4 @@
+% chapter_breaks.tex
+% Chapter breaks setup
+\usepackage{sectsty}
+\sectionfont{\clearpage}
diff --git a/utils/docs/filter-nobg.hs b/utils/docs/filter-nobg.hs
new file mode 100755
index 0000000..629f753
--- /dev/null
+++ b/utils/docs/filter-nobg.hs
@@ -0,0 +1,23 @@
+#!/usr/bin/env runhaskell
+{-# LANGUAGE OverloadedStrings #-}
+
+import Text.Pandoc.JSON
+import Data.Text (Text, isInfixOf)
+
+-- Function to filter out images with 'bg' in alt text
+filterImage :: Inline -> Inline
+filterImage img@(Image attr alt _) = 
+  if any ("bg" `isInfixOf`) (map stringify alt)
+    then Str ""
+    else img
+filterImage x = x
+
+-- Stringify function to convert inlines to Text
+stringify :: Inline -> Text
+stringify (Str txt) = txt
+stringify _ = ""
+
+-- Main function
+main :: IO ()
+main = toJSONFilter filterImage
+
diff --git a/utils/docs/hyperref_setup.tex b/utils/docs/hyperref_setup.tex
new file mode 100644
index 0000000..4ebfd6e
--- /dev/null
+++ b/utils/docs/hyperref_setup.tex
@@ -0,0 +1,9 @@
+% hyperref_setup.tex
+% Hyperref setup
+\usepackage{hyperref}
+\hypersetup{
+    colorlinks=true,
+    linkcolor=blue,
+    filecolor=magenta,
+    urlcolor=blue,
+}
diff --git a/utils/docs/main.tex b/utils/docs/main.tex
new file mode 100644
index 0000000..a477538
--- /dev/null
+++ b/utils/docs/main.tex
@@ -0,0 +1,30 @@
+% main.tex
+
+% Set the document language to English
+\usepackage[english]{babel}
+
+% Adjust page geometry
+\usepackage[
+	a4paper, inner=1.5cm, outer=1.5cm, top=2cm, bottom=2cm, 
+	% bindingoffset=0.5cm
+]{geometry}
+
+% Configure headers and footers using scrlayer-scrpage
+\usepackage[automark,headsepline,footsepline]{scrlayer-scrpage}
+\clearpairofpagestyles
+\ohead{\headmark}
+\ofoot{\pagemark}
+\pagestyle{scrheadings}
+
+
+% Include additional settings from external files
+% \input{inline_code.tex}
+% \input{bullet_styling.tex}
+\input{chapter_breaks.tex}
+\input{blockquote.tex}
+\input{hyperref_setup.tex}
+
+% Font settings using fontspec
+% \usepackage{fontspec}
+% \setmainfont{DejaVu Serif} % Set your main font
+% \setmonofont{SauceCodePro Nerd Font} % Set your monospace (code) font
diff --git a/utils/docs/metadata.yml b/utils/docs/metadata.yml
new file mode 100644
index 0000000..08526a2
--- /dev/null
+++ b/utils/docs/metadata.yml
@@ -0,0 +1,5 @@
+---
+title: Formation
+subtitle: MariaDB
+author: Glenn Y. Rolland <teaching@glenux.net>
+date: \today