Skip to content

Commit 35774d9

Browse files
committed
Publish Elixir chapter as standalone book at /elixir/book/
The Elixir chapter of this book — 30+ pages covering data types, control flow, operators, recursion, Mix — now also stands alone at https://wintermeyer-consulting.de/elixir/book/ with its own sidebar, per-topic pagination, and data-book-current="elixir" stamp. How it hangs together: - elixir-book/scripts/sync.py mirrors modules/ROOT/pages/elixir/* into elixir-book/modules/ROOT/pages/, dropping the `elixir/` path prefix so URLs land at /elixir/book/iex.html instead of /elixir/book/elixir/iex.html. xref:elixir/... and include:elixir/... references are rewritten to drop the prefix too. - The main book's elixir/index.adoc is skipped (it's an include:: merge of every sub-chapter for single-page reading); the mini-book's index.adoc is a minimal hand-written landing. - antora-elixir-playbook.yml is a second Antora playbook with site.url=/elixir and output.dir=build/elixir-site. It consumes the same shared UI bundle as the main book (wincon-antora-ui release). - scripts/deploy.sh now renders both books in the same run, each to its own timestamped release under its own /var/www/ dir. - scripts/fetch-partials.sh stamps two ui-supplemental dirs (phoenix + elixir). Build-generated paths — elixir-book/modules/ROOT/pages/ and elixir-book/ui-supplemental/ — are gitignored. The one source of truth for Elixir content remains modules/ROOT/pages/elixir/; edits there flow into both surfaces on the next deploy. Requires a matching nginx config + /var/www/elixir-book/ setup on bremen2 (both done as part of this work).
1 parent c1d6ec5 commit 35774d9

7 files changed

Lines changed: 259 additions & 58 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ sandbox/*
99
/ignore/
1010
# Generated at deploy time by scripts/fetch-partials.sh.
1111
/ui-supplemental/
12+
/elixir-book/ui-supplemental/
13+
# Generated at deploy time by elixir-book/scripts/sync.py from
14+
# modules/ROOT/pages/elixir/.
15+
/elixir-book/modules/ROOT/pages/

antora-elixir-playbook.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
site:
2+
title: Elixir
3+
url: https://wintermeyer-consulting.de/elixir
4+
start_page: book::index.adoc
5+
content:
6+
sources:
7+
- url: .
8+
branches: HEAD
9+
start_path: elixir-book
10+
ui:
11+
bundle:
12+
url: https://github.com/wintermeyer/wincon-antora-ui/releases/download/latest/ui-bundle.zip
13+
snapshot: true
14+
supplemental_files: ./elixir-book/ui-supplemental
15+
output_dir: antora-assets
16+
output:
17+
dir: build/elixir-site
18+
runtime:
19+
fetch: true

elixir-book/antora.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
name: book
2+
title: Elixir
3+
version: ~
4+
start_page: ROOT:index.adoc
5+
nav:
6+
- modules/ROOT/nav.adoc

elixir-book/modules/ROOT/nav.adoc

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
* xref:index.adoc[Welcome]
2+
3+
.First Steps
4+
* xref:elixir-version.adoc[Elixir Version]
5+
* xref:iex.adoc[iex]
6+
* xref:hello-world.adoc[Hello World!]
7+
* xref:basic-calculations.adoc[Basic Calculations]
8+
* xref:basic-debugging.adoc[Basic Debugging]
9+
10+
.Language Essentials
11+
* xref:modules-and-functions.adoc[Modules and Functions]
12+
* xref:higher-order-functions.adoc[Higher-Order Functions]
13+
* xref:variables.adoc[Variables]
14+
* xref:variable-scopes.adoc[Variable Scopes]
15+
* xref:immutability.adoc[Immutability]
16+
17+
.Control Flow
18+
* xref:control-structures/index-control-structures.adoc[Control Structures]
19+
** xref:control-structures/if-and-unless.adoc[if and unless]
20+
** xref:control-structures/case.adoc[case]
21+
** xref:control-structures/cond.adoc[cond]
22+
** xref:control-structures/with-and-for.adoc[with and for]
23+
** xref:control-structures/variable-assignment-with-control-structures.adoc[Variable Assignment with Control Structures]
24+
25+
.Data Types
26+
* xref:data-types/index-data-types.adoc[Data Types]
27+
** xref:data-types/atom.adoc[Atom]
28+
** xref:data-types/boolean.adoc[Boolean]
29+
** xref:data-types/integers-and-floats.adoc[Integers and Floats]
30+
** xref:data-types/strings.adoc[Strings]
31+
** xref:data-types/lists-and-tuples.adoc[Lists and Tuples]
32+
** xref:data-types/keyword-list.adoc[Keyword List]
33+
** xref:data-types/map.adoc[Map]
34+
** xref:data-types/struct.adoc[Struct]
35+
** xref:data-types/type-conversions.adoc[Type Conversions]
36+
37+
.Operators
38+
* xref:operators/index-operators.adoc[Operators]
39+
** xref:operators/match-operator.adoc[Match Operator]
40+
** xref:operators/pipe-operator.adoc[Pipe Operator]
41+
** xref:operators/capture-operator.adoc[Capture Operator]
42+
** xref:operators/arithmetic-operator.adoc[Arithmetic]
43+
** xref:operators/boolean-operator.adoc[Boolean]
44+
** xref:operators/comparison-operator.adoc[Comparison]
45+
** xref:operators/range-operator.adoc[Range]
46+
** xref:operators/cons-operator.adoc[Cons]
47+
** xref:operators/string-concatenation-operator.adoc[String Concatenation]
48+
** xref:operators/interpolation-operator.adoc[Interpolation]
49+
50+
.More
51+
* xref:logical-expressions.adoc[Logical Expressions]
52+
* xref:enumerables.adoc[Enumerables]
53+
* xref:enum.adoc[Enum]
54+
* xref:stream.adoc[Stream]
55+
* xref:sigils.adoc[Sigils]
56+
* xref:recursion.adoc[Recursion]
57+
* xref:mix.adoc[Mix]

elixir-book/scripts/sync.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Mirror modules/ROOT/pages/elixir/ (the Elixir chapter of the main
4+
book) into elixir-book/modules/ROOT/pages/ so Antora can render it
5+
as a standalone site at /elixir/book/.
6+
7+
- Drops the leading `elixir/` path prefix. Files at
8+
`pages/elixir/iex.adoc` land as `pages/iex.adoc` in the mini-book,
9+
so URLs end up at /elixir/book/iex.html instead of the awkward
10+
/elixir/book/elixir/iex.html.
11+
- Rewrites `xref:elixir/<x>.adoc` → `xref:<x>.adoc` in all copied
12+
files, preserving internal links between chapters.
13+
- Skips `index.adoc` (it's a one-page include:: merge of every
14+
sub-chapter — the mini-book uses a sidebar instead, so we write
15+
our own minimal landing).
16+
17+
Generated files are gitignored; the one source of truth is the main
18+
Elixir chapter under modules/ROOT/pages/elixir/.
19+
"""
20+
from __future__ import annotations
21+
22+
import re
23+
import shutil
24+
from pathlib import Path
25+
26+
SRC = Path("modules/ROOT/pages/elixir")
27+
DST = Path("elixir-book/modules/ROOT/pages")
28+
29+
XREF_RE = re.compile(r"(xref|include):elixir/")
30+
31+
32+
def main() -> None:
33+
# Wipe prior output so deleted source files don't linger.
34+
for existing in DST.glob("*"):
35+
if existing.is_dir():
36+
shutil.rmtree(existing)
37+
else:
38+
existing.unlink()
39+
DST.mkdir(parents=True, exist_ok=True)
40+
41+
for src_file in SRC.rglob("*.adoc"):
42+
rel = src_file.relative_to(SRC)
43+
# Skip the main-book landing — it's an include::-heavy single
44+
# page; the mini-book's landing is written by hand below.
45+
if str(rel) == "index.adoc":
46+
continue
47+
dst_file = DST / rel
48+
dst_file.parent.mkdir(parents=True, exist_ok=True)
49+
content = src_file.read_text()
50+
content = XREF_RE.sub(r"\1:", content)
51+
dst_file.write_text(content)
52+
53+
# Copy any non-adoc assets (rare here, but robust).
54+
for src_file in SRC.rglob("*"):
55+
if src_file.is_file() and src_file.suffix != ".adoc":
56+
rel = src_file.relative_to(SRC)
57+
dst_file = DST / rel
58+
dst_file.parent.mkdir(parents=True, exist_ok=True)
59+
shutil.copy2(src_file, dst_file)
60+
61+
(DST / "index.adoc").write_text(LANDING)
62+
print(f"synced Elixir chapter -> {DST}/")
63+
64+
65+
LANDING = """\
66+
= Welcome
67+
Stefan Wintermeyer <sw@wintermeyer-consulting.de>
68+
69+
This is the Elixir chapter from the
70+
link:https://wintermeyer-consulting.de/phoenix/book/[_Elixir, Phoenix and
71+
Ash Beginner's Guide_], split page-per-topic so each concept has its
72+
own URL. If you want the full guide (Elixir, Phoenix, Ash, and
73+
recipes), read it in the parent book.
74+
75+
Pick a topic from the sidebar.
76+
77+
If this is your first functional programming language you might want
78+
to get a cup of coffee first. It might take a while to get used to
79+
the functional programming paradigm. **It took me a long time too!**
80+
"""
81+
82+
83+
if __name__ == "__main__":
84+
main()

scripts/deploy.sh

Lines changed: 58 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,98 @@
11
#!/usr/bin/env bash
2-
# Deploy the Antora site to bremen2 under
3-
# /var/www/elixir-phoenix-ash/releases/<timestamp>/
4-
# and atomically swap the `current` symlink.
2+
# Deploy two Antora sites from this repo:
53
#
6-
# Runs on the `eliph` self-hosted GitHub Actions runner from the
7-
# actions/checkout workdir. Invoked as `./scripts/deploy.sh`.
4+
# - main Phoenix book -> /var/www/elixir-phoenix-ash/
5+
# - Elixir mini-book -> /var/www/elixir-book/
86
#
9-
# The Antora build needs Node (provided via mise under ~eliph).
7+
# Both land via timestamped releases and an atomic `current`
8+
# symlink swap. Runs on the `eliph` self-hosted GitHub Actions
9+
# runner from the actions/checkout workdir.
10+
#
11+
# Node is provided via mise under ~eliph.
1012

1113
set -euo pipefail
1214

13-
# Activate mise so node/npm/npx resolve on the non-interactive shell.
1415
if command -v mise >/dev/null 2>&1; then
1516
eval "$(mise activate bash)"
1617
elif [ -x "$HOME/.local/bin/mise" ]; then
1718
eval "$("$HOME/.local/bin/mise" activate bash)"
1819
fi
1920

20-
APP_DIR="/var/www/elixir-phoenix-ash"
21-
RELEASES_DIR="${APP_DIR}/releases"
22-
CURRENT_LINK="${APP_DIR}/current"
23-
SHARED_DIR="${APP_DIR}/shared"
24-
LOCK_FILE="${SHARED_DIR}/.deploy.lock"
21+
PHOENIX_APP_DIR="/var/www/elixir-phoenix-ash"
22+
ELIXIR_APP_DIR="/var/www/elixir-book"
2523
KEEP_RELEASES=5
2624
TIMESTAMP="$(date +%Y%m%d%H%M%S)"
27-
RELEASE_DIR="${RELEASES_DIR}/${TIMESTAMP}"
25+
26+
PHOENIX_RELEASE_DIR="${PHOENIX_APP_DIR}/releases/${TIMESTAMP}"
27+
ELIXIR_RELEASE_DIR="${ELIXIR_APP_DIR}/releases/${TIMESTAMP}"
28+
LOCK_FILE="${PHOENIX_APP_DIR}/shared/.deploy.lock"
2829

2930
log() { echo "[$(date '+%H:%M:%S')] $*"; }
3031

31-
mkdir -p "${SHARED_DIR}"
32+
mkdir -p "${PHOENIX_APP_DIR}/shared"
3233
exec 9>"${LOCK_FILE}"
3334
flock -n 9 || { log "ERROR: another deploy is running"; exit 1; }
3435

3536
REPO_DIR="$(pwd)"
3637
log "Repo: ${REPO_DIR}"
3738

39+
publish_release() {
40+
local app_dir="$1"
41+
local release_dir="$2"
42+
local source_dir="$3"
43+
44+
log "Publishing ${release_dir}..."
45+
mkdir -p "${release_dir}"
46+
cp -a "${source_dir}/." "${release_dir}/"
47+
chmod -R a+rX "${release_dir}"
48+
49+
local current_link="${app_dir}/current"
50+
log "Atomic swap ${current_link} -> ${release_dir}"
51+
ln -sfn "${release_dir}" "${current_link}.new"
52+
mv -fT "${current_link}.new" "${current_link}"
53+
54+
log "Pruning ${app_dir}/releases (keeping last ${KEEP_RELEASES})..."
55+
mapfile -t _old < <(
56+
find "${app_dir}/releases" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' \
57+
| sort | head -n "-${KEEP_RELEASES}"
58+
)
59+
for r in "${_old[@]}"; do
60+
[ -n "${r}" ] && rm -rf "${app_dir}/releases/${r:?}"
61+
done
62+
}
63+
3864
log "Fetching latest nav + footer partials from wincon..."
3965
./scripts/fetch-partials.sh
4066

67+
log "Syncing Elixir chapter into elixir-book/modules/ROOT/pages/..."
68+
python3 elixir-book/scripts/sync.py
69+
4170
log "Installing Antora..."
4271
( cd "${REPO_DIR}" && npm ci --no-audit --no-fund )
4372

44-
log "Rendering site..."
45-
# --fetch refreshes both the content source (this repo) and the UI
46-
# bundle (wincon-antora-ui/releases/latest/ui-bundle.zip). The UI
47-
# bundle's snapshot:true in the playbook tells Antora not to cache
48-
# across runs, so a fresh bundle is always pulled.
73+
log "Rendering Phoenix book..."
74+
# --fetch refreshes the content source and the shared UI bundle
75+
# (wincon-antora-ui/releases/latest/ui-bundle.zip). snapshot:true
76+
# in the playbook tells Antora not to cache across runs.
4977
( cd "${REPO_DIR}" && npx antora --fetch antora-playbook.yml )
5078

5179
if [ ! -d "${REPO_DIR}/build/site/book" ]; then
5280
log "ERROR: expected build/site/book/ not found"
5381
exit 1
5482
fi
5583

56-
log "Publishing release ${TIMESTAMP}..."
57-
mkdir -p "${RELEASE_DIR}"
58-
cp -a "${REPO_DIR}/build/site/." "${RELEASE_DIR}/"
59-
chmod -R a+rX "${RELEASE_DIR}"
84+
log "Rendering Elixir mini-book..."
85+
( cd "${REPO_DIR}" && npx antora antora-elixir-playbook.yml )
6086

61-
log "Atomic swap..."
62-
ln -sfn "${RELEASE_DIR}" "${CURRENT_LINK}.new"
63-
mv -fT "${CURRENT_LINK}.new" "${CURRENT_LINK}"
87+
if [ ! -d "${REPO_DIR}/build/elixir-site/book" ]; then
88+
log "ERROR: expected build/elixir-site/book/ not found"
89+
exit 1
90+
fi
6491

65-
log "Pruning old releases (keeping last ${KEEP_RELEASES})..."
66-
mapfile -t _old < <(
67-
find "${RELEASES_DIR}" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' \
68-
| sort | head -n "-${KEEP_RELEASES}"
69-
)
70-
for r in "${_old[@]}"; do
71-
[ -n "${r}" ] && rm -rf "${RELEASES_DIR:?}/${r}"
72-
done
92+
publish_release "${PHOENIX_APP_DIR}" "${PHOENIX_RELEASE_DIR}" "${REPO_DIR}/build/site"
93+
mkdir -p "${ELIXIR_APP_DIR}/shared"
94+
publish_release "${ELIXIR_APP_DIR}" "${ELIXIR_RELEASE_DIR}" "${REPO_DIR}/build/elixir-site"
7395

7496
log "Deploy complete: ${TIMESTAMP}"
75-
log " Active: ${CURRENT_LINK} -> $(readlink -f "${CURRENT_LINK}")"
97+
log " Phoenix: ${PHOENIX_APP_DIR}/current -> $(readlink -f "${PHOENIX_APP_DIR}/current")"
98+
log " Elixir: ${ELIXIR_APP_DIR}/current -> $(readlink -f "${ELIXIR_APP_DIR}/current")"

scripts/fetch-partials.sh

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,29 @@
11
#!/usr/bin/env bash
2-
# Fetch the canonical nav + footer partials from wincon and write them
3-
# into ./ui-supplemental/partials/ so Antora overrides the default
4-
# header-content.hbs / footer-content.hbs from the shared UI bundle
5-
# (wintermeyer/wincon-antora-ui) with book-specific content.
2+
# Fetch the canonical nav + footer partials from wincon and write
3+
# them into the ui-supplemental/ directories so Antora overrides
4+
# the shared UI bundle's header-content.hbs / footer-content.hbs
5+
# with book-specific content.
66
#
7-
# Source of truth: https://wintermeyer-consulting.de/partials/
8-
# (served from wincon/priv/static/partials/).
7+
# This repo publishes TWO Antora sites:
8+
# - main Phoenix book -> ./ui-supplemental/ (phoenix)
9+
# - Elixir mini-book -> ./elixir-book/ui-supplemental/ (elixir)
910
#
10-
# Falls back to the GitHub raw URL when production is unreachable.
11+
# Each site gets the same fetched footer but its own header with
12+
# a different data-book-current stamp, which drives which stack
13+
# link gets highlighted in the top nav.
1114
#
12-
# On total failure the supplemental files are left at whatever state
13-
# the previous run put them in (or absent on first deploy), so Antora
14-
# silently falls back to the bundle defaults. Deploys never break on
15-
# a transient wincon outage.
15+
# Source of truth: https://wintermeyer-consulting.de/partials/.
16+
# Falls back to the GitHub raw URL when production is unreachable.
17+
# On total failure the supplemental files are left as-is; Antora
18+
# falls back to the UI bundle defaults and the deploy still works.
1619

1720
set -u
1821

1922
PROD_BASE="https://wintermeyer-consulting.de/partials"
2023
GH_BASE="https://raw.githubusercontent.com/wintermeyer/wincon/main/priv/static/partials"
21-
BOOK_CURRENT="phoenix"
2224

2325
cd "$(dirname "$0")/.."
2426

25-
PARTIALS_DIR="ui-supplemental/partials"
26-
mkdir -p "${PARTIALS_DIR}"
27-
2827
try_fetch() {
2928
local source_name="$1"
3029
local dest="$2"
@@ -46,12 +45,21 @@ try_fetch() {
4645
return 1
4746
}
4847

49-
try_fetch footer.html "${PARTIALS_DIR}/footer-content.hbs" || true
48+
stamp_book_for() {
49+
local partials_dir="$1"
50+
local book_current="$2"
51+
52+
mkdir -p "${partials_dir}"
53+
54+
try_fetch footer.html "${partials_dir}/footer-content.hbs" || true
55+
56+
if try_fetch book-nav.html "${partials_dir}/header-content.hbs"; then
57+
local tmp="${partials_dir}/header-content.hbs.tmp"
58+
sed -e "s/data-book-current=\"\"/data-book-current=\"${book_current}\"/" \
59+
"${partials_dir}/header-content.hbs" > "${tmp}"
60+
mv "${tmp}" "${partials_dir}/header-content.hbs"
61+
fi
62+
}
5063

51-
if try_fetch book-nav.html "${PARTIALS_DIR}/header-content.hbs"; then
52-
# Highlight the Phoenix link in the shared book nav.
53-
tmp="${PARTIALS_DIR}/header-content.hbs.tmp"
54-
sed -e "s/data-book-current=\"\"/data-book-current=\"${BOOK_CURRENT}\"/" \
55-
"${PARTIALS_DIR}/header-content.hbs" > "$tmp"
56-
mv "$tmp" "${PARTIALS_DIR}/header-content.hbs"
57-
fi
64+
stamp_book_for "ui-supplemental/partials" "phoenix"
65+
stamp_book_for "elixir-book/ui-supplemental/partials" "elixir"

0 commit comments

Comments
 (0)