From 0080d1c54a8f4c11b38f7cf0557063a3e757dc92 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Tue, 3 Feb 2026 08:38:12 -0800 Subject: [PATCH] Add Quartz documentation build infrastructure Phase 1a infrastructure for building and hosting BlumeOps docs: - Add quartz.config.ts and quartz.layout.ts for Quartz configuration - Add containers/quartz/ with nginx-based static site server that downloads release bundles on startup via DOCS_RELEASE_URL env var - Add .forgejo/workflows/build-blumeops.yaml workflow (manual trigger) that builds Quartz site and creates Forgejo release with tarball - Update docs/README.md with finalized tooling choice and split Phase 1 into 1a (CI) and 1b (CD/hosting) The architecture separates content versioning from infrastructure: - Releases are versioned BlumeOps releases (v1.0.0, etc.) - Doc tarballs are attached as release assets - The quartz container is a generic static site server Co-Authored-By: Claude Opus 4.5 --- .forgejo/workflows/build-blumeops.yaml | 146 +++++++++++++++++++++++++ containers/quartz/Dockerfile | 52 +++++++++ containers/quartz/start.sh | 31 ++++++ docs/README.md | 37 ++++--- quartz.config.ts | 91 +++++++++++++++ quartz.layout.ts | 54 +++++++++ 6 files changed, 395 insertions(+), 16 deletions(-) create mode 100644 .forgejo/workflows/build-blumeops.yaml create mode 100644 containers/quartz/Dockerfile create mode 100644 containers/quartz/start.sh create mode 100644 quartz.config.ts create mode 100644 quartz.layout.ts diff --git a/.forgejo/workflows/build-blumeops.yaml b/.forgejo/workflows/build-blumeops.yaml new file mode 100644 index 0000000..6d0b110 --- /dev/null +++ b/.forgejo/workflows/build-blumeops.yaml @@ -0,0 +1,146 @@ +# BlumeOps Release Workflow +# +# Creates a versioned release of BlumeOps with all build artifacts. +# Currently includes: +# - Documentation site (Quartz static build) +# +# Future additions may include other release artifacts. +# +# Usage: +# 1. Go to Actions > Build BlumeOps > Run workflow +# 2. Enter a version tag (e.g., v1.2.0) +# 3. The workflow creates a release with attached artifacts +# +# Documentation asset URL: +# https://forge.ops.eblu.me/eblume/blumeops/releases/download//docs-.tar.gz + +name: Build BlumeOps + +on: + workflow_dispatch: + inputs: + version: + description: 'Version tag (e.g., v1.2.0)' + required: true + type: string + +jobs: + build: + runs-on: k8s + steps: + - name: Validate version + run: | + VERSION="${{ inputs.version }}" + if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Version must be in format vX.Y.Z (e.g., v1.0.0)" + exit 1 + fi + echo "Building BlumeOps release: $VERSION" + + - name: Checkout + uses: actions/checkout@v4 + + - name: Build Quartz site + run: | + VERSION="${{ inputs.version }}" + echo "Node version: $(node --version)" + echo "NPM version: $(npm --version)" + + echo "Setting up Quartz..." + # Clone Quartz to a temp directory + git clone --depth 1 https://github.com/jackyzha0/quartz.git /tmp/quartz + cd /tmp/quartz + + # Install dependencies + npm ci + + # Copy our configuration + cp "$GITHUB_WORKSPACE/quartz.config.ts" . + cp "$GITHUB_WORKSPACE/quartz.layout.ts" . + + # Copy docs as content + rm -rf content + cp -r "$GITHUB_WORKSPACE/docs" content + + # Create index page + cat > content/index.md << 'EOF' + --- + title: BlumeOps Documentation + --- + + Welcome to the BlumeOps documentation. + + [[README|Documentation Home]] + EOF + + # Build + echo "Building static site..." + npx quartz build + + # Create tarball + TARBALL="docs-${VERSION}.tar.gz" + echo "Creating tarball: $TARBALL" + tar -czf "$GITHUB_WORKSPACE/$TARBALL" -C public . + + echo "Build complete!" + ls -lh "$GITHUB_WORKSPACE/$TARBALL" + + - name: Create release + run: | + VERSION="${{ inputs.version }}" + TARBALL="docs-${VERSION}.tar.gz" + + echo "Creating release $VERSION..." + + # Use Forgejo API to create release + RELEASE_DATA=$(cat < /etc/nginx/conf.d/default.conf << 'EOF' +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Enable gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # SPA fallback - serve index.html for client-side routing + location / { + try_files $uri $uri/ $uri.html /index.html; + } + + # Health check endpoint + location /healthz { + access_log off; + return 200 "ok\n"; + add_header Content-Type text/plain; + } +} +EOF + +EXPOSE 80 + +CMD ["/start.sh"] diff --git a/containers/quartz/start.sh b/containers/quartz/start.sh new file mode 100644 index 0000000..778eeb1 --- /dev/null +++ b/containers/quartz/start.sh @@ -0,0 +1,31 @@ +#!/bin/sh +set -e + +HTML_DIR="/usr/share/nginx/html" + +# Check for required environment variable +if [ -z "$DOCS_RELEASE_URL" ]; then + echo "Error: DOCS_RELEASE_URL environment variable is required" + echo "Set it to the URL of the static site tarball to serve" + exit 1 +fi + +echo "Downloading docs from: $DOCS_RELEASE_URL" + +# Download the tarball +if ! curl -fsSL "$DOCS_RELEASE_URL" -o /tmp/docs.tar.gz; then + echo "Error: Failed to download docs from $DOCS_RELEASE_URL" + exit 1 +fi + +# Clear existing content and extract +rm -rf "${HTML_DIR:?}"/* +echo "Extracting docs to $HTML_DIR" +tar -xzf /tmp/docs.tar.gz -C "$HTML_DIR" +rm /tmp/docs.tar.gz + +echo "Docs extracted successfully" +echo "Starting nginx..." + +# Start nginx in foreground +exec nginx -g "daemon off;" diff --git a/docs/README.md b/docs/README.md index 8bc69a4..01d1b66 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,30 +28,36 @@ The documentation is being restructured to follow the [Diataxis](https://diataxi - **Output formats**: HTML (for web hosting) and PDF (for offline reference) - **Changelog**: Track significant documentation changes -### Tooling Options (To Evaluate) +### Tooling -Several markdown-to-HTML/PDF tools to consider: +**Selected**: [Quartz](https://quartz.jzhao.xyz/) - A TypeScript-based static site generator designed for Obsidian vaults. Features wiki-link support, backlinks, graph view, and excellent Obsidian compatibility. -| Tool | Language | Obsidian Compatibility | Notes | -|------|----------|------------------------|-------| -| [Quartz](https://quartz.jzhao.xyz/) | TypeScript | Excellent (designed for Obsidian) | Wiki-links, backlinks, graph view | -| [MkDocs](https://www.mkdocs.org/) + [Material](https://squidfunk.github.io/mkdocs-material/) | Python | Good (with plugins) | Very popular, great search | -| [mdBook](https://rust-lang.github.io/mdBook/) | Rust | Limited | Simple, fast, used by Rust docs | -| [Hugo](https://gohugo.io/) | Go | Moderate | Fast builds, flexible | -| [Docusaurus](https://docusaurus.io/) | React | Limited | Feature-rich, heavier | - -**Leaning toward**: Quartz (best Obsidian compatibility) or MkDocs Material (most mature ecosystem). +**Architecture**: +- **Source**: Markdown files in `docs/` with optional YAML frontmatter +- **Build**: Quartz builds static HTML/CSS/JS via Forgejo workflow +- **Release**: Built assets published as Forgejo release attachments +- **Hosting**: `quartz` container downloads release bundle on startup and serves via nginx +- **URL**: `docs.ops.eblu.me` (planned) ## Restructuring Phases -### Phase 1: Foundation (Current) +### Phase 1a: Foundation & CI (Current) - [x] Move existing zk cards to `docs/zk/` - [x] Update `zk-docs` mise task for new path - [x] Create this README with restructuring plan -- [ ] Evaluate and select documentation build tooling -- [ ] Set up basic build pipeline (mise task or CI) +- [x] Select documentation tooling (Quartz) +- [x] Create Quartz configuration (`quartz.config.ts`, `quartz.layout.ts`) +- [x] Create `quartz` container for serving static sites +- [x] Create `build-blumeops` workflow for building releases +- [ ] Test the build workflow and verify release creation - [ ] Create `CHANGELOG.md` +### Phase 1b: CD & Hosting +- [ ] Create ArgoCD manifests for `quartz` deployment +- [ ] Add `docs.ops.eblu.me` to Caddy reverse proxy +- [ ] Configure deployment to pull from release URL +- [ ] Test end-to-end: commit -> build -> release -> deploy + ### Phase 2: Tutorials Learning-oriented content for getting started. @@ -92,8 +98,7 @@ Understanding-oriented discussion of concepts and decisions. - [ ] Migrate remaining useful content from `docs/zk/` - [ ] Decide fate of zk cards (archive, delete, or keep as separate knowledge base) - [ ] Update CLAUDE.md to reference new doc structure -- [ ] Set up automated doc builds (CI/CD) -- [ ] Consider hosting docs publicly (GitHub Pages, etc.) +- [ ] Mirror docs to GitHub Pages for public access (optional) ## Current Directory Layout diff --git a/quartz.config.ts b/quartz.config.ts new file mode 100644 index 0000000..bfdaa22 --- /dev/null +++ b/quartz.config.ts @@ -0,0 +1,91 @@ +import { QuartzConfig } from "./quartz/cfg" +import * as Plugin from "./quartz/plugins" + +/** + * Quartz configuration for BlumeOps documentation + * See https://quartz.jzhao.xyz/configuration + */ +const config: QuartzConfig = { + configuration: { + pageTitle: "BlumeOps Docs", + pageTitleSuffix: "", + enableSPA: true, + enablePopovers: true, + analytics: null, + locale: "en-US", + baseUrl: "docs.ops.eblu.me", + ignorePatterns: ["private", "templates", ".obsidian"], + defaultDateType: "modified", + theme: { + fontOrigin: "googleFonts", + cdnCaching: true, + typography: { + header: "Schibsted Grotesk", + body: "Source Sans Pro", + code: "IBM Plex Mono", + }, + colors: { + lightMode: { + light: "#faf8f8", + lightgray: "#e5e5e5", + gray: "#b8b8b8", + darkgray: "#4e4e4e", + dark: "#2b2b2b", + secondary: "#284b63", + tertiary: "#84a59d", + highlight: "rgba(143, 159, 169, 0.15)", + textHighlight: "#fff23688", + }, + darkMode: { + light: "#161618", + lightgray: "#393639", + gray: "#646464", + darkgray: "#d4d4d4", + dark: "#ebebec", + secondary: "#7b97aa", + tertiary: "#84a59d", + highlight: "rgba(143, 159, 169, 0.15)", + textHighlight: "#fff23688", + }, + }, + }, + }, + plugins: { + transformers: [ + Plugin.FrontMatter(), + Plugin.CreatedModifiedDate({ + priority: ["frontmatter", "git", "filesystem"], + }), + Plugin.SyntaxHighlighting({ + theme: { + light: "github-light", + dark: "github-dark", + }, + keepBackground: false, + }), + Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }), + Plugin.GitHubFlavoredMarkdown(), + Plugin.TableOfContents(), + Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }), + Plugin.Description(), + Plugin.Latex({ renderEngine: "katex" }), + ], + filters: [Plugin.RemoveDrafts()], + emitters: [ + Plugin.AliasRedirects(), + Plugin.ComponentResources(), + Plugin.ContentPage(), + Plugin.FolderPage(), + Plugin.TagPage(), + Plugin.ContentIndex({ + enableSiteMap: true, + enableRSS: true, + }), + Plugin.Assets(), + Plugin.Static(), + Plugin.NotFoundPage(), + ], + }, +} + +export default config diff --git a/quartz.layout.ts b/quartz.layout.ts new file mode 100644 index 0000000..c3f7e6d --- /dev/null +++ b/quartz.layout.ts @@ -0,0 +1,54 @@ +import { PageLayout, SharedLayout } from "./quartz/cfg" +import * as Component from "./quartz/components" + +/** + * Quartz layout configuration for BlumeOps documentation + * See https://quartz.jzhao.xyz/layout + */ + +// Components shared across all pages +export const sharedPageComponents: SharedLayout = { + head: Component.Head(), + header: [], + afterBody: [], + footer: Component.Footer({ + links: { + "GitHub": "https://github.com/eblume/blumeops", + "Forge": "https://forge.ops.eblu.me/eblume/blumeops", + }, + }), +} + +// Components for pages that list posts (folder pages, tag pages) +export const defaultContentPageLayout: PageLayout = { + beforeBody: [ + Component.Breadcrumbs(), + Component.ArticleTitle(), + Component.ContentMeta(), + Component.TagList(), + ], + left: [ + Component.PageTitle(), + Component.MobileOnly(Component.Spacer()), + Component.Search(), + Component.Darkmode(), + Component.DesktopOnly(Component.Explorer()), + ], + right: [ + Component.Graph(), + Component.DesktopOnly(Component.TableOfContents()), + Component.Backlinks(), + ], +} + +export const defaultListPageLayout: PageLayout = { + beforeBody: [Component.Breadcrumbs(), Component.ArticleTitle(), Component.ContentMeta()], + left: [ + Component.PageTitle(), + Component.MobileOnly(Component.Spacer()), + Component.Search(), + Component.Darkmode(), + Component.DesktopOnly(Component.Explorer()), + ], + right: [], +}