🚀 Build & Deploy the Docs Site
This page documents docusaurus-deploy.sh, the script that builds a Docusaurus
site inside a Node container and copies the static output to the web server's
document root. It's parameterized by a single PJT_NAME variable, so the same
script deploys any docs project on the org just by changing that one value.
- Updates a persistent local clone of the repo (or clones it the first time).
- Rewrites
urlandbaseUrlin the Docusaurus config for this deployment. - Runs
npm install+npm run buildinside anode:ltscontainer. - Mirrors the built
build/output into the web root withrsync. - Regenerates the landing page listing all deployed projects.
Prerequisites
- Docker installed and running on the host.
- git and rsync on the host.
- Write access to the deployment directory (the script uses
sudo). - Outbound network access from the container to the npm registry. This host
reaches the internet through a resolver passed explicitly as
--dns 8.8.8.8; adjust that value if your network requires a different one.
The build runs in the Debian-based node:lts image, which already bundles git,
Python 3, and a C/C++ toolchain — so Docusaurus's git lookups and any native npm
modules (e.g. better-sqlite3, which compiles with node-gyp) work with no
extra setup.
Configuration
Only PJT_NAME normally changes between projects. Everything else is derived
from it, except SITE_URL, which is fixed to the shared domain.
| Variable | Meaning | Value |
|---|---|---|
PJT_NAME | Project / repo name; drives all the paths below | iiot-docs |
REPO_URL | Git repository to build | https://github.com/ruseleredu/$PJT_NAME |
SITE_URL | Public domain, no trailing slash (shared) | https://docs.mini.pc |
BASE_URL | Sub-path the site is served from, with both slashes | /$PJT_NAME/ |
DEST | Directory the static files are copied to | /var/www/html/docs/$PJT_NAME |
WORKDIR | Persistent working clone on the host | /opt/docusaurus-builder/$PJT_NAME |
BASE_URL must match the deployed pathbaseUrl is the sub-path the browser requests assets from. If it doesn't line
up with where the files actually live (both the DEST copy path and your
nginx/apache location), the page will load but all CSS and JS will 404 and the
site renders unstyled. Because BASE_URL, DEST, and the nginx location are
all derived from PJT_NAME, they stay in sync automatically — but if you
override any of them by hand, override all of them.
SITE_URL stays fixed while everything else keys off PJT_NAME. This assumes
every project shares docs.mini.pc and is distinguished by its baseUrl
sub-path. If a project ever needs its own domain, move SITE_URL into the
per-project variables too.
The script
#!/bin/sh
set -e
PJT_NAME="iiot-docs"
REPO_URL="https://github.com/ruseleredu/$PJT_NAME"
SITE_URL="https://docs.mini.pc"
BASE_URL="/$PJT_NAME/"
DEST="/var/www/html/docs/$PJT_NAME"
WORKDIR="/opt/docusaurus-builder/$PJT_NAME"
# Update in place instead of re-cloning
if [ -d "$WORKDIR/.git" ]; then
git -C "$WORKDIR" fetch origin
git -C "$WORKDIR" reset --hard @{u} # discard last run's sed edits, match remote
else
git clone "$REPO_URL" "$WORKDIR"
fi
cd "$WORKDIR"
# Detect config extension, then re-apply edits (reset --hard wiped them)
CONFIG=$(ls docusaurus.config.ts docusaurus.config.js 2>/dev/null | head -n1)
sed -i "s|url:.*|url: '${SITE_URL}',|g" "$CONFIG"
sed -i "s|baseUrl:.*|baseUrl: '${BASE_URL}',|g" "$CONFIG"
grep -nE "^\s*(url|baseUrl):" "$CONFIG" # sanity check before building
# node_modules lives in $WORKDIR and persists; npm install is incremental.
# node:lts (Debian) ships git + Python + toolchain, so native modules compile.
docker run --rm --dns 8.8.8.8 -v "$PWD":/app -w /app node:lts \
sh -c "npm install && npm run build"
sudo mkdir -p "$DEST"
sudo rsync -a --delete build/ "$DEST"/
# Refresh the landing page that lists every deployed project
sudo DOCS_ROOT=/var/www/html/docs SITE_TITLE="docs.mini.pc" \
sh /opt/docusaurus-builder/generate-index.sh
node:lts and not node:lts-alpineThe Alpine image is ~300 MB smaller but ships neither git nor a build toolchain.
That means installing git for Docusaurus's "last updated" lookups and
compiling any native module from source — extra moving parts that caused DNS and
node-gyp/Python failures earlier. Debian node:lts bundles all of it, so the
run line needs no apk add and no custom image. Docker caches the image after
the first pull, so it's downloaded only once.
Deploy
Place the script somewhere convenient, make it executable once, then run it for every update:
chmod +x docusaurus-deploy.sh # first time only
./docusaurus-deploy.sh
To deploy a different project, change one line:
PJT_NAME="other-project"
The grep line prints the two config values it just set — confirm they read as
expected before trusting the build:
22: url: 'https://docs.mini.pc',
25: baseUrl: '/iiot-docs/',
How the config gets rewritten
The script auto-detects whether the repo uses docusaurus.config.ts or
docusaurus.config.js and edits whichever it finds:
CONFIG=$(ls docusaurus.config.ts docusaurus.config.js 2>/dev/null | head -n1)
The two sed lines then replace everything after url: / baseUrl: up to the
end of the line. This works because Docusaurus's other URL-ish fields
(editUrl, favicon) don't start with a lowercase url:, so they're left
alone. Two things to keep in mind:
- The replacement wipes any trailing comment on those lines.
sedhere uses|as the delimiter, so aSITE_URL/BASE_URLcontaining a literal|or&would break it. Not a concern for these values.
If CONFIG ever comes back empty (neither file found), the grep sanity check
fails immediately under set -e, stopping the run before a bad build.
The build container
The build runs in the stock node:lts image with no customization — no
docker build, no Dockerfile, no image guard. Because the image is used as-is,
Docker pulls it once and caches it; every later run reuses that cached image.
node:lts is Debian-based and built on buildpack-deps, so it already contains
everything the build needs: git (for Docusaurus's last-update lookups), Python 3,
make, and g++ (for compiling native npm modules). Nothing is installed at
run time, which removes the in-container apk add step — and with it the
DNS-at-build-time failures the Alpine approach was prone to.
node:lts tracks the current LTS line but a locally cached copy won't update on
its own. Pull a fresh one when you want the latest patch:
docker pull node:lts
Pin a specific version instead (e.g. node:22-bookworm) if you need
reproducible builds across machines.
Speed: why updates are fast
The script is built to avoid redoing work on every run.
- Persistent clone. After the first run,
git fetch+git reset --hardpull only new commits instead of re-downloading the whole repo. npm install, notnpm ci.npm cideletesnode_modulesand reinstalls all ~1,460 packages every time (~33 s). Because the clone persists,node_modulessurvives, andnpm installagainst an unchanged lockfile is nearly a no-op. It still reconciles correctly when the lockfile changes upstream.- Cached image, no per-run setup.
node:ltsis pulled once and reused, and since it already has git and the build toolchain, there's no in-container install on any run. rsync --delete, notcp -r. rsync copies only changed files and removes files deleted upstream, keepingDESTan exact mirror. Plaincpwould leave stale, deleted pages behind forever.- The persistent
.docusaurus/cache also shaves time off the build step.
A routine update becomes roughly "fetch new commits + build" instead of a full clone, install, and copy.
Troubleshooting
destination path already existsAn earlier version re-cloned into a fixed directory. The current script updates
a persistent WORKDIR instead, so this shouldn't recur. If you still see it,
the working directory exists but has no .git — remove it and rerun:
rm -rf "$WORKDIR" && ./docusaurus-deploy.sh.
This Docusaurus site is outside any Git worktreeThe site has the "last updated" feature enabled, so Docusaurus runs git log
on each .mdx. This fails only if the build image has no git binary —
node:lts-alpine was the culprit. node:lts (Debian) includes git, so the
error shouldn't appear. If you deliberately switch back to an image without git,
either install it in the container or disable the feature in the docs preset
options:
showLastUpdateTime: false,
showLastUpdateAuthor: false,
gyp ERR! find Python / native module fails to buildA dependency ships a native addon (e.g. better-sqlite3) with no prebuilt
binary for this platform, so npm compiles it with node-gyp, which needs Python
and a C/C++ toolchain. node:lts includes both, so this builds cleanly. It only
surfaces on a stripped-down image like node:lts-alpine; if you must use Alpine,
add the toolchain with apk add --no-cache python3 make g++.
git (no such package) / DNS: transient errorAlpine-only: apk couldn't reach its package mirror to install git or the
toolchain. Switching to node:lts removes the apk step entirely, so this can't
occur. If you keep an Alpine-based image and hit it, --dns 8.8.8.8 helps
docker run; a docker build step needs the DNS fix at the daemon level.
Landing page (project index)
With several projects deployed side by side under /var/www/html/docs/, a
landing page at the root ties them together. generate-index.sh scans that
directory and writes an index.html listing every project as a Bootstrap 5
card. The deploy script calls it as its last step, so the index refreshes on
every deploy.
A plain static page can't enumerate folders on its own — a browser has no
filesystem access — which is why this is a generator run on the host rather than
client-side JavaScript. It's also why the alternative, nginx's autoindex, is
avoided: it exposes a raw file listing rather than a curated page.
#!/bin/sh
set -e
# Scans DOCS_ROOT for project subfolders (those containing an index.html),
# pulls each site's hero title/subtitle out of its built homepage, and writes
# a Bootstrap 5 card grid listing them.
DOCS_ROOT="${DOCS_ROOT:-/var/www/html/docs}" # where project subfolders live
OUTPUT="${OUTPUT:-$DOCS_ROOT/index.html}" # landing page to write
LINK_PREFIX="${LINK_PREFIX:-/}" # projects are served at ${LINK_PREFIX}<name>/
SITE_TITLE="${SITE_TITLE:-Documentation}"
BOOTSTRAP_CSS="${BOOTSTRAP_CSS:-https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css}"
FAVICON="${FAVICON:-https://raw.githubusercontent.com/facebook/docusaurus/refs/heads/main/examples/classic/static/img/favicon.ico}"
# Pull the text inside an element carrying a given class. Handles quoted or
# unquoted attributes, extra classes, and a missing close tag (stops at next <).
# Needs GNU grep (-P). Returns empty if not found.
extract() {
grep -oP "$2"'[^>]*>\K[^<]*' "$1" 2>/dev/null | head -n1
}
prettify() {
echo "$1" | tr '_-' ' ' \
| awk '{for(i=1;i<=NF;i++){$i=toupper(substr($i,1,1)) substr($i,2)}}1'
}
# Header (quoted heredoc: nothing expands here; placeholders filled by sed after)
cat > "$OUTPUT" <<'HEAD'
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>__SITE_TITLE__</title>
<link rel="icon" href="__FAVICON__">
<link href="__BOOTSTRAP_CSS__" rel="stylesheet">
</head>
<body class="bg-body-tertiary">
<main class="container py-5">
<h1 class="mb-1">__SITE_TITLE__</h1>
<p class="text-body-secondary mb-4">Available documentation sites</p>
<div class="row row-cols-1 row-cols-md-2 g-4">
HEAD
found=0
for dir in "$DOCS_ROOT"/*/; do
[ -f "${dir}index.html" ] || continue # skip folders with no built site
name=$(basename "$dir")
title=$(extract "${dir}index.html" 'hero__title')
subtitle=$(extract "${dir}index.html" 'hero__subtitle')
[ -n "$title" ] || title=$(prettify "$name") # fall back to folder name
# %s args keep arbitrary title/subtitle text out of the format string
{
printf ' <div class="col">\n'
printf ' <div class="card h-100 shadow-sm">\n'
printf ' <div class="card-body">\n'
printf ' <h5 class="card-title">%s</h5>\n' "$title"
printf ' <p class="card-text text-body-secondary">%s</p>\n' "$subtitle"
printf ' </div>\n'
printf ' <div class="card-footer bg-transparent d-flex align-items-center">\n'
printf ' <a href="%s%s/" class="btn btn-primary btn-sm stretched-link">Abrir</a>\n' "$LINK_PREFIX" "$name"
printf ' <span class="ms-auto small text-body-secondary font-monospace">%s%s/</span>\n' "$LINK_PREFIX" "$name"
printf ' </div>\n'
printf ' </div>\n'
printf ' </div>\n'
} >> "$OUTPUT"
found=$((found + 1))
done
if [ "$found" -eq 0 ]; then
printf ' <div class="col"><div class="alert alert-secondary mb-0">No documentation sites deployed yet.</div></div>\n' >> "$OUTPUT"
fi
cat >> "$OUTPUT" <<TAIL
</div>
<footer class="text-body-secondary small mt-5">Generated $(date -u '+%Y-%m-%d %H:%M UTC') · ${found} site(s)</footer>
</main>
<script>document.documentElement.setAttribute('data-bs-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');</script>
</body>
</html>
TAIL
# Fill placeholders kept out of the heredoc (avoids quoting/escaping headaches)
sed -i "s|__SITE_TITLE__|${SITE_TITLE}|g; s|__BOOTSTRAP_CSS__|${BOOTSTRAP_CSS}|g; s|__FAVICON__|${FAVICON}|g" "$OUTPUT"
echo "Wrote $OUTPUT (${found} site(s))"
Everything is overridable by environment variable, so no edits are needed to retarget it:
DOCS_ROOT=/var/www/html/docs OUTPUT=/var/www/html/docs/index.html \
SITE_TITLE="docs.mini.pc" sh generate-index.sh
Card titles come from each site's hero
Instead of showing folder names, the generator reads each project's built
homepage and pulls the text out of its hero__title and hero__subtitle
elements — the heading and tagline from the Docusaurus landing page. The
extract() helper matches the class, skips the rest of the tag, and captures up
to the next <, so it copes with minified output: unquoted attributes
(class=hero__title), extra classes, and even a missing closing </p>. If a
homepage has no hero, the card falls back to the (prettified) folder name.
extract() uses grep -oP (PCRE), which is GNU grep only. That's fine on the
host, which is Debian-based. Don't run this script inside the Alpine container,
whose BusyBox grep has no -P.
baseUrlCards link to /<name>/ (root-relative), because each project's baseUrl is
/$PJT_NAME/ and the nginx alias serves the site at docs.mini.pc/<name>/ —
the /docs/ in the filesystem path is not part of the URL. If you ever serve
projects under a /docs/ URL prefix instead, set LINK_PREFIX=/docs/ so the
links keep matching.
The generator itself never touches the network, but the page it writes pulls
Bootstrap from cdn.jsdelivr.net and the favicon from raw.githubusercontent.com
in the viewer's browser. If docs.mini.pc is a LAN box whose users have no
internet, neither will load — the page renders unstyled and without an icon.
Vendor both locally once and point the script at the copies:
# one-time, on a machine with internet:
curl -L -o /var/www/html/docs/bootstrap.min.css \
https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css
# one-time, on a machine with internet:
curl -L -o /var/www/html/docs/favicon.ico \
https://raw.githubusercontent.com/facebook/docusaurus/refs/heads/main/examples/classic/static/img/favicon.ico
# then generate against the local files:
BOOTSTRAP_CSS=/bootstrap.min.css FAVICON=/favicon.ico sh generate-index.sh
Deploy everything (deploy-all.sh)
Once several projects each have their own deploy script
(iiot-docs.sh, tcc-docs.sh, …), deploy-all.sh runs them all in one pass.
It lives in the same directory, discovers every *.sh sibling automatically
(skipping itself and generate-index.sh), and needs no edit when you add a new
project.
nano deploy-all.sh
#!/bin/sh
# Runs every per-project deploy script in this directory, in order, continuing
# past any that fail; regenerates the landing page; and writes a Bootstrap-
# styled logs.html next to index.html in the web root for later reference.
# Exit status is non-zero if any project failed.
set -u
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
cd "$SCRIPT_DIR"
SELF=$(basename "$0")
DOCS_ROOT="${DOCS_ROOT:-/var/www/html/docs}"
SITE_TITLE="${SITE_TITLE:-docs.mini.pc}"
SUDO="${SUDO-sudo}" # set SUDO= (empty) when already root
LOG_HTML="${LOG_HTML:-$DOCS_ROOT/logs.html}" # written alongside index.html
BOOTSTRAP_CSS="${BOOTSTRAP_CSS:-https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css}"
FAVICON="${FAVICON:-https://raw.githubusercontent.com/facebook/docusaurus/refs/heads/main/examples/classic/static/img/favicon.ico}"
RUN="$(mktemp -d)"
trap 'rm -rf "$RUN"' EXIT
BODY="$RUN/body.html"
: > "$BODY"
# HTML-escape stdin; strip ANSI colour codes and stray carriage returns first.
clean() {
sed "s/$(printf '\033')\[[0-9;?]*[a-zA-Z]//g" | tr -d '\r' \
| sed -e 's/&/\&/g' -e 's/</\</g' -e 's/>/\>/g'
}
started="$(date '+%Y-%m-%d %H:%M:%S %Z')"
ok_list=""
fail_list=""
total=0
for script in ./*.sh; do
base=$(basename "$script")
[ "$base" = "$SELF" ] && continue
[ "$base" = "generate-index.sh" ] && continue
[ -f "$script" ] || continue
total=$((total + 1))
printf '\n========== [%d] %s ==========\n' "$total" "$base"
log="$RUN/out.log"
start=$(date +%s)
# tee gives live console output; the rc file preserves the child's real
# exit code (the pipeline's own status would be tee's, not the script's).
{ sh "$script"; echo $? > "$RUN/rc"; } 2>&1 | tee "$log"
rc=$(cat "$RUN/rc")
dur=$(( $(date +%s) - start ))
if [ "$rc" -eq 0 ]; then
status="OK"; badge="text-bg-success"; ok_list="$ok_list $base"; openattr=""
else
status="FAIL (exit $rc)"; badge="text-bg-danger"; fail_list="$fail_list $base"; openattr=" open"
fi
printf -- '---------- %s: %s (%ds) ----------\n' "$base" "$status" "$dur"
{
printf '<details class="card mb-2 shadow-sm"%s>' "$openattr"
printf '<summary class="card-header d-flex align-items-center gap-2"><span class="fw-semibold">%s</span><span class="badge %s">%s</span><span class="ms-auto text-body-secondary small">%ds</span></summary>' \
"$base" "$badge" "$status" "$dur"
printf '<pre class="log card-body mb-0 small"><code>'
clean < "$log"
printf '</code></pre></details>\n'
} >> "$BODY"
done
# Regenerate the landing page once, capturing its output into the log too
printf '\n========== landing page ==========\n'
lp="$RUN/lp.log"
if [ -f "$SCRIPT_DIR/generate-index.sh" ]; then
{ $SUDO env DOCS_ROOT="$DOCS_ROOT" SITE_TITLE="$SITE_TITLE" \
BOOTSTRAP_CSS="$BOOTSTRAP_CSS" FAVICON="$FAVICON" \
sh "$SCRIPT_DIR/generate-index.sh"; echo $? > "$RUN/rc"; } 2>&1 | tee "$lp"
lprc=$(cat "$RUN/rc")
else
echo "generate-index.sh not found, skipping" | tee "$lp"; lprc=0
fi
if [ "$lprc" -eq 0 ]; then lpbadge="text-bg-success"; lpstatus="OK"
else lpbadge="text-bg-danger"; lpstatus="FAIL (exit $lprc)"; fi
{
printf '<details class="card mb-2 shadow-sm"><summary class="card-header d-flex align-items-center gap-2"><span class="fw-semibold">landing page</span><span class="badge %s">%s</span></summary>' \
"$lpbadge" "$lpstatus"
printf '<pre class="log card-body mb-0 small"><code>'; clean < "$lp"; printf '</code></pre></details>\n'
} >> "$BODY"
ok_n=$(echo "$ok_list" | wc -w)
fail_n=$(echo "$fail_list" | wc -w)
# --- build logs.html, then place it next to index.html --------------------
{
printf '<!DOCTYPE html>\n<html lang="en">\n<head>\n'
printf '<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n'
printf '<title>Deploy log · %s</title>\n' "$SITE_TITLE"
printf '<link rel="icon" href="%s">\n' "$FAVICON"
printf '<link href="%s" rel="stylesheet">\n' "$BOOTSTRAP_CSS"
cat <<'HTMLCSS'
<style>
details > summary { cursor: pointer; list-style: none; }
details > summary::-webkit-details-marker { display: none; }
pre.log { max-height: 480px; overflow: auto; white-space: pre; }
</style>
</head>
<body class="bg-body-tertiary">
<main class="container py-4">
<h1 class="h4 mb-3">Deploy log</h1>
HTMLCSS
if [ -n "$fail_list" ]; then
printf '<div class="alert alert-danger" role="alert"><strong>%d failed.</strong> Total %d · OK %d · Failed %d<br><span class="small font-monospace">Failed:%s</span></div>\n' \
"$fail_n" "$total" "$ok_n" "$fail_n" "$fail_list"
else
printf '<div class="alert alert-success" role="alert">All %d project(s) built successfully.</div>\n' "$total"
fi
printf '<p class="text-body-secondary small mb-3">Run: %s</p>\n' "$started"
cat "$BODY"
cat <<'HTMLTAIL'
</main>
<script>document.documentElement.setAttribute('data-bs-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');</script>
</body>
</html>
HTMLTAIL
} > "$RUN/logs.html"
$SUDO cp "$RUN/logs.html" "$LOG_HTML"
printf '\n===================== SUMMARY =====================\n'
printf 'Total: %d OK: %d Failed: %d\n' "$total" "$ok_n" "$fail_n"
[ -n "$ok_list" ] && printf 'OK :%s\n' "$ok_list"
[ -n "$fail_list" ] && printf 'FAILED :%s\n' "$fail_list"
printf 'Log written to %s\n' "$LOG_HTML"
[ -n "$fail_list" ] && exit 1
echo "All projects built successfully."
chmod +x deploy-all.sh # first time only
./deploy-all.sh
Its behavior is shaped by two things that matter for an unattended batch:
- It does not stop on the first failure. Each project script runs under its
own
set -e, but the wrapper captures the exit code, records the project as failed, and moves on — so one broken build (a missing native dep, say) doesn't block the other nine. At the end it prints a pass/fail summary and exits non-zero if any project failed, which is what lets you wire it into cron or a health check. - It regenerates the landing page once, at the end, reflecting whatever built successfully.
Environment variables (all overridable) drive the shared configuration:
| Variable | Default | Purpose |
|---|---|---|
DOCS_ROOT | /var/www/html/docs | Where projects and both pages live |
SITE_TITLE | docs.mini.pc | Heading on the landing page and log |
SUDO | sudo | Set SUDO= (empty) when already root |
LOG_HTML | $DOCS_ROOT/logs.html | Where the HTML log is written |
BOOTSTRAP_CSS | jsDelivr CDN URL | Stylesheet for the landing page and log |
FAVICON | Docusaurus favicon URL | Icon for the landing page and log |
Each individual project script also calls generate-index.sh at its own end, so
during a full deploy-all.sh run the index is regenerated several times (once
per project, plus the final explicit one). It's a cheap directory scan, so this
is harmless. If you only ever deploy via deploy-all.sh, you can drop the
generate-index.sh line from the individual scripts and let the wrapper own it.
Build logs (logs.html)
deploy-all.sh writes a Bootstrap-styled logs.html next to index.html in the
web root, so the last run is viewable at docs.mini.pc/logs.html and matches the
landing page's look. Each project's combined stdout and stderr is streamed live
to the console and captured; the page renders one collapsible card per project
with a status badge and build duration, and failed sections open by default
so you land on the errors.
Captured output is ANSI-stripped and HTML-escaped before embedding, so build
noise (colour codes, <>& in log lines) can't corrupt the page. The collapse
uses native <details>, so only the Bootstrap stylesheet is needed — no
bootstrap.bundle.js.
logs.html is served — mind its contentsBecause it sits in the web root, logs.html is reachable by anyone who can load
the site, and it contains full build paths and any error output. That's fine for
a trusted LAN box. If it isn't, keep it out of the web root
(LOG_HTML=/opt/docusaurus-builder/logs.html) or restrict /logs.html in nginx.
Each run overwrites the file rather than keeping history.
logs.html pulls Bootstrap and the favicon from the network just like the
landing page. On an offline LAN, vendor both locally and pass the same
BOOTSTRAP_CSS=/bootstrap.min.css FAVICON=/favicon.ico — deploy-all.sh applies
them to the log and forwards them to generate-index.sh for the landing page, so
one setting covers both pages.
Serving the site (nginx)
The static files at DEST need a web server pointing at them. A minimal nginx
config that serves a project (matching BASE_URL = /iiot-docs/ and
DEST = /var/www/html/docs/iiot-docs), plus the generated landing page and build
log from the docs root:
server {
listen 80;
server_name docs.mini.pc;
# Landing page and build log live in the docs root, served at "/"
location = / { root /var/www/html/docs; try_files /index.html =404; }
location = /logs.html { root /var/www/html/docs; }
# One block per project (all derived from PJT_NAME)
location /iiot-docs/ {
alias /var/www/html/docs/iiot-docs/;
index index.html;
try_files $uri $uri/ =404;
}
}
Drop the location = /logs.html line (or restrict it) if you'd rather not expose
the build log — see the warning above.
The alias path, the location prefix, and BASE_URL all have to agree —
they're all functions of PJT_NAME, so keep them consistent when you add a new
project. The landing page links to those same /<name>/ paths, so a project
only appears (and resolves) once its location block is in place.