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 <noreply@anthropic.com>
This commit is contained in:
parent
38dae76cb9
commit
0080d1c54a
6 changed files with 395 additions and 16 deletions
146
.forgejo/workflows/build-blumeops.yaml
Normal file
146
.forgejo/workflows/build-blumeops.yaml
Normal file
|
|
@ -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/<tag>/docs-<version>.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 <<EOF
|
||||
{
|
||||
"tag_name": "$VERSION",
|
||||
"name": "BlumeOps $VERSION",
|
||||
"body": "BlumeOps release $VERSION\n\n## Documentation\n\nDownload \`$TARBALL\` and configure the quartz container with:\n\n\`\`\`\nDOCS_RELEASE_URL=https://forge.ops.eblu.me/eblume/blumeops/releases/download/$VERSION/$TARBALL\n\`\`\`",
|
||||
"draft": false,
|
||||
"prerelease": false
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
RELEASE_RESPONSE=$(curl -sf \
|
||||
-X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$RELEASE_DATA" \
|
||||
"https://forge.ops.eblu.me/api/v1/repos/eblume/blumeops/releases")
|
||||
|
||||
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id')
|
||||
|
||||
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
|
||||
echo "Error: Failed to create release"
|
||||
echo "Response: $RELEASE_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Created release ID: $RELEASE_ID"
|
||||
|
||||
# Upload the asset
|
||||
echo "Uploading $TARBALL..."
|
||||
curl -sf \
|
||||
-X POST \
|
||||
-H "Content-Type: application/gzip" \
|
||||
--data-binary "@$TARBALL" \
|
||||
"https://forge.ops.eblu.me/api/v1/repos/eblume/blumeops/releases/$RELEASE_ID/assets?name=$TARBALL"
|
||||
|
||||
echo ""
|
||||
echo "Release created successfully!"
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
VERSION="${{ inputs.version }}"
|
||||
TARBALL="docs-${VERSION}.tar.gz"
|
||||
echo "================================================"
|
||||
echo "BlumeOps Release: $VERSION"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo "Release URL:"
|
||||
echo " https://forge.ops.eblu.me/eblume/blumeops/releases/tag/$VERSION"
|
||||
echo ""
|
||||
echo "Asset URL (for DOCS_RELEASE_URL ConfigMap):"
|
||||
echo " https://forge.ops.eblu.me/eblume/blumeops/releases/download/$VERSION/$TARBALL"
|
||||
52
containers/quartz/Dockerfile
Normal file
52
containers/quartz/Dockerfile
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Quartz Static Site Server
|
||||
# Downloads and serves a Quartz-built static site from a release bundle
|
||||
#
|
||||
# Configuration (via environment):
|
||||
# DOCS_RELEASE_URL - URL to download the static site tarball
|
||||
#
|
||||
# The container downloads the tarball on startup, extracts it, and serves with nginx.
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
# Install curl for downloading release assets
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
# Copy startup script
|
||||
COPY start.sh /start.sh
|
||||
RUN chmod +x /start.sh
|
||||
|
||||
# Custom nginx config for SPA routing
|
||||
RUN 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"]
|
||||
31
containers/quartz/start.sh
Normal file
31
containers/quartz/start.sh
Normal file
|
|
@ -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;"
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
91
quartz.config.ts
Normal file
91
quartz.config.ts
Normal file
|
|
@ -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
|
||||
54
quartz.layout.ts
Normal file
54
quartz.layout.ts
Normal file
|
|
@ -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: [],
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue