Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/pull-from-armbian-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,19 @@ jobs:
rsync -avr docs/Network/Network.md ../documentation/docs/User-Guide_Armbian-Config/
rsync -avr docs/System/System.md ../documentation/docs/User-Guide_Armbian-Config/

# the rest goes under armbian-software
# the rest goes under armbian-software (category hubs + section pages)
mkdir -p ../documentation/docs/User-Guide_Armbian-Software/
rsync -avr --exclude="Software.user.md" docs/Software/* ../documentation/docs/User-Guide_Armbian-Software/

# per-app SEO pages get their own top-level /software/<slug>/ URLs
mkdir -p ../documentation/docs/software/
rsync -avr --delete docs/software/. ../documentation/docs/software/

- name: Regenerate the software navigation
# Rebuild the "ARMBIAN SOFTWARE" nav (category -> app pages) from the
# freshly synced app pages, so new/removed apps appear in the left nav.
run: python3 documentation/tools/build-software-nav.py

- name: Create Pull Request to documentation
uses: peter-evans/create-pull-request@v8
with:
Expand Down
2 changes: 1 addition & 1 deletion docs/css/armbian-extra.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
white-space: nowrap;
}

/* Style for only specially tagged bash blocks */
/* The primary "CLI install" block — large, to stand out as the main action. */
pre.custom-bash-block, code.custom-bash-block {
font-size: 24pt;
}
Expand Down
12 changes: 9 additions & 3 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ repo_name: armbian/documentation
exclude_docs: |
/README.md

# Software category hub pages (e.g. Monitoring.md) are kept for SEO and the
# existing /User-Guide_Armbian-Software/<Category>/ URLs, but are reached via
# search and the app pages' back-link rather than the nav, so building them
# outside the nav must not warn (matters under `mkdocs build --strict`).
not_in_nav: |
/User-Guide_Armbian-Software/*.md

theme:
name: material
custom_dir: overrides
Expand Down Expand Up @@ -43,8 +50,6 @@ theme:
- content.code.copy
- navigation.tabs
- navigation.top
- navigation.expand
- toc.integrate

extra_css:
- css/armbian-extra.css
Expand Down Expand Up @@ -143,6 +148,7 @@ nav:
- 'Localisation': 'User-Guide_Armbian-Config/Localisation.md'
- 'ARMBIAN SOFTWARE':
- 'Overview': 'User-Guide_Armbian-Software.md'
# BEGIN software-nav — generated by tools/build-software-nav.py (do not edit by hand)
- 'Armbian': 'User-Guide_Armbian-Software/Armbian.md'
- 'Backup': 'User-Guide_Armbian-Software/Backup.md'
- 'Containers': 'User-Guide_Armbian-Software/Containers.md'
Expand All @@ -155,11 +161,11 @@ nav:
- 'Management': 'User-Guide_Armbian-Software/Management.md'
- 'Media': 'User-Guide_Armbian-Software/Media.md'
- 'Monitoring': 'User-Guide_Armbian-Software/Monitoring.md'
- 'Music': 'User-Guide_Armbian-Software/Music.md'
- 'Netconfig': 'User-Guide_Armbian-Software/Netconfig.md'
- 'Printing': 'User-Guide_Armbian-Software/Printing.md'
- 'VPN': 'User-Guide_Armbian-Software/VPN.md'
- 'Web hosting': 'User-Guide_Armbian-Software/WebHosting.md'
# END software-nav
# - 'Desktops': 'User-Guide_Armbian-Software/Desktops.md'
- 'ARMBIAN BUILD FRAMEWORK' :
- 'Overview' : 'Developer-Guide_Overview.md'
Expand Down
24 changes: 24 additions & 0 deletions overrides/main.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
{% extends "base.html" %}

<!-- Open Graph / Twitter card tags, per page, from front-matter meta.
Material already emits <meta name="description"> from page.meta.description;
this adds the social equivalents plus og:image (the app logo shipped as
`image:` in front-matter). Falls back to the site defaults elsewhere. -->
{% block extrahead %}
{{ super() }}
{% set _title = (page.title ~ " - " ~ config.site_name) if page and page.title else config.site_name %}
{% set _desc = (page.meta.description if page and page.meta and page.meta.description else config.site_description) %}
<meta property="og:type" content="website">
<meta property="og:title" content="{{ _title }}">
<meta property="og:description" content="{{ _desc }}">
<meta name="twitter:title" content="{{ _title }}">
<meta name="twitter:description" content="{{ _desc }}">
{% if page and page.meta and page.meta.image %}
{% set _img = page.meta.image %}
{% if _img[:4] != "http" %}{% set _img = (config.site_url | default("", true)).rstrip("/") ~ "/" ~ _img.lstrip("/") %}{% endif %}
<meta property="og:image" content="{{ _img }}">
<meta name="twitter:image" content="{{ _img }}">
<meta name="twitter:card" content="summary_large_image">
{% else %}
<meta name="twitter:card" content="summary">
{% endif %}
{% endblock %}

<!-- Announcement bar -->
{% block announce %}
<a href="https://www.armbian.com">
Expand Down
78 changes: 78 additions & 0 deletions serve-docs-local.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# Offline preview of the docs, reproducing the "Pull from Armbian config" CI job
# locally: generate the software pages in configng, copy them into this repo,
# then serve with live reload.
#
# ./serve-docs-local.sh generate + stage + serve at http://127.0.0.1:8000
# ./serve-docs-local.sh build generate + stage + one-shot build into ./site
# ./serve-docs-local.sh clean revert the staged generated pages, leave tree clean
#
# Override repo locations with CONFIGNG=/path DOCS=/path if they aren't the
# defaults below.
set -euo pipefail

DOCS="${DOCS:-$(cd "$(dirname "$0")" && pwd)}"
CONFIGNG="${CONFIGNG:-$(cd "$DOCS/../configng" && pwd)}"
VENV="${VENV:-$DOCS/.venv-docs}"
cmd="${1:-serve}"

# Revert exactly what the staging block below writes: the generated hub/config
# pages and images (restore tracked ones, drop untracked ones), and the wholly
# generated docs/software tree. Only touches those paths.
stage_clean() {
for d in docs/User-Guide_Armbian-Software docs/User-Guide_Armbian-Config docs/images; do
git -C "$DOCS" checkout -- "$d" 2>/dev/null || true # restore tracked files
git -C "$DOCS" clean -fdq "$d" 2>/dev/null || true # drop untracked generated files
done
rm -rf "$DOCS/docs/software" # wholly generated
echo "reverted generated pages/images/software; check 'git status'"
}

if [[ "$cmd" == "clean" ]]; then stage_clean; exit 0; fi

# 1) pick a mkdocs that actually has this site's plugins. A bare PATH mkdocs may
# lack mkdocs-material / mkdocs-redirects / mdx_truly_sane_lists and fail
# confusingly, so use it only if those import; otherwise a local venv built
# from requirements.txt (first run needs pip/network).
has_deps() { "$1" -c 'import material, mkdocs_redirects, mdx_truly_sane_lists' >/dev/null 2>&1; }
if command -v mkdocs >/dev/null 2>&1 && has_deps "$(command -v python3)"; then
MK=mkdocs
else
[[ -x "$VENV/bin/mkdocs" ]] && has_deps "$VENV/bin/python" || {
echo ">> creating venv at $VENV and installing docs requirements (one-time, needs network)"
python3 -m venv "$VENV"
"$VENV/bin/pip" install -q --disable-pip-version-check -r "$DOCS/requirements.txt" \
|| "$VENV/bin/pip" install -q mkdocs mkdocs-material mkdocs-redirects mdx_truly_sane_lists
}
MK="$VENV/bin/mkdocs"
fi

# 2) generate the markdown from configng (uses the committed config.jobs.json).
echo ">> generating software pages in $CONFIGNG"
( cd "$CONFIGNG" && python3 tools/config-markdown.py -u )

# 3) stage into the docs tree exactly like pull-from-armbian-config.yml.
echo ">> staging generated pages into $DOCS/docs"
mkdir -p "$DOCS/docs/images" "$DOCS/docs/User-Guide_Armbian-Config" \
"$DOCS/docs/User-Guide_Armbian-Software" "$DOCS/docs/software"
rsync -a "$CONFIGNG/tools/include/images/." "$DOCS/docs/images/"
for p in Localisation Network System; do
rsync -a "$CONFIGNG/docs/$p/$p.md" "$DOCS/docs/User-Guide_Armbian-Config/"
done
rsync -a --exclude="Software.user.md" "$CONFIGNG/docs/Software/"* "$DOCS/docs/User-Guide_Armbian-Software/"
rsync -a --delete "$CONFIGNG/docs/software/." "$DOCS/docs/software/"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# 3b) rebuild the ARMBIAN SOFTWARE nav from the staged app pages, exactly as the
# pull-from-armbian-config workflow does, so the local preview matches CI.
echo ">> rebuilding software navigation"
python3 "$DOCS/tools/build-software-nav.py"

# 4) serve or build.
cd "$DOCS"
if [[ "$cmd" == "build" ]]; then
"$MK" build --clean
echo ">> built into $DOCS/site — open site/software/netdata/index.html"
else
echo ">> serving at http://127.0.0.1:8000 (Ctrl-C to stop, then: ./serve-docs-local.sh clean)"
"$MK" serve
fi
127 changes: 127 additions & 0 deletions tools/build-software-nav.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
# Rebuild the "ARMBIAN SOFTWARE" nav in mkdocs.yml so each category is a section
# (its hub page as the section index, via navigation.indexes) with the per-app
# pages nested beneath it. App pages and their `category:` come from the configng
# generator (docs/software/<slug>.md); this only groups them for the left nav —
# the /software/<slug>/ URLs are unaffected.
#
# Idempotent: rewrites only the region between the BEGIN/END markers. Category
# labels + order are curated here (rarely change); a new category needs one line.
import re
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
MKDOCS = ROOT / "mkdocs.yml"
APPS_DIR = ROOT / "docs" / "software"
HUB_DIR = "User-Guide_Armbian-Software"

BEGIN = "# BEGIN software-nav"
END = "# END software-nav"
IND = " " * 8 # category entries sit 8 spaces in, under 'ARMBIAN SOFTWARE'

# Curated label + order, mapped to the category id used by the hub file name and
# the app pages' `category:` field.
CATEGORIES = [
("Armbian", "Armbian"),
("Backup", "Backup"),
("Containers", "Containers"),
("Database", "Database"),
("Development tools", "DevTools"),
("DNS blockers", "DNS"),
("Downloaders", "Downloaders"),
("Finance", "Finance"),
("Home automation", "HomeAutomation"),
("Management", "Management"),
("Media", "Media"),
("Monitoring", "Monitoring"),
("Netconfig", "Netconfig"),
("Printing", "Printing"),
("VPN", "VPN"),
("Web hosting", "WebHosting"),
]


def read_front_matter(md_path):
"""Return (title, category) from a page's YAML front-matter, or (None, None)."""
text = md_path.read_text(encoding="utf-8")
m = re.match(r"^---\n(.*?)\n---\n", text, re.DOTALL)
if not m:
return None, None
fm = m.group(1)
title = re.search(r'^title:\s*"?(.*?)"?\s*$', fm, re.MULTILINE)
cat = re.search(r'^category:\s*"?(.*?)"?\s*$', fm, re.MULTILINE)
return (title.group(1) if title else None,
cat.group(1) if cat else None)
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def collect_apps():
"""category_id -> sorted list of (title, slug)."""
by_cat = {}
for md in sorted(APPS_DIR.glob("*.md")):
title, cat = read_front_matter(md)
if not title or not cat:
continue
by_cat.setdefault(cat, []).append((title, md.stem))
for cat in by_cat:
by_cat[cat].sort(key=lambda t: t[0].lower())
return by_cat


def yq(s):
return "'" + s.replace("'", "''") + "'"


def build_block(by_cat):
lines = []
seen = set()
for label, cat_id in CATEGORIES:
seen.add(cat_id)
apps = by_cat.get(cat_id, [])
if not apps:
# Defunct/phantom category (no app pages generated) — skip it, so a
# category that was removed or merged upstream can't linger in the nav.
continue
# Category is a collapsible toggle with its apps under it. The hub page
# (kept for SEO / the old URL) is intentionally NOT listed here — it is
# redundant with the app list and is marked not_in_nav in mkdocs.yml.
lines.append(f"{IND}- {yq(label)}:")
for title, slug in apps:
lines.append(f"{IND} - {yq(title)}: {yq(f'software/{slug}.md')}")
# Any category with app pages but no curated label still gets rendered (with
# a prettified id as the label) so new upstream categories never silently
# vanish; warn so a nicer label can be added.
for cat in sorted(set(by_cat) - seen):
label = re.sub(r'(?<=[a-z])(?=[A-Z])', ' ', cat)
print(f"::warning:: software-nav: category {cat!r} has app pages but no "
f"curated label — using {label!r}; add one in tools/build-software-nav.py",
file=sys.stderr)
lines.append(f"{IND}- {yq(label)}:")
for title, slug in by_cat[cat]:
lines.append(f"{IND} - {yq(title)}: {yq(f'software/{slug}.md')}")
return "\n".join(lines)


def main():
text = MKDOCS.read_text(encoding="utf-8")
if BEGIN not in text or END not in text:
sys.exit(f"markers {BEGIN!r}/{END!r} not found in {MKDOCS}")
by_cat = collect_apps()
if not by_cat:
# No per-app pages present (e.g. the configng generator that emits
# docs/software/<slug>.md with title/category hasn't run/synced yet).
# Leave the existing nav block untouched rather than blanking it.
print(f"software-nav: no app pages found under {APPS_DIR} — nav left unchanged.")
return
block = build_block(by_cat)
new = re.sub(
rf"({re.escape(IND)}{re.escape(BEGIN)}[^\n]*\n).*?(\n{re.escape(IND)}{re.escape(END)})",
lambda m: m.group(1) + block + m.group(2),
text, count=1, flags=re.DOTALL)
MKDOCS.write_text(new, encoding="utf-8")
total = sum(len(v) for v in by_cat.values())
print(f"software-nav: {total} app pages grouped under {len(CATEGORIES)} categories.")


if __name__ == "__main__":
main()
Loading