diff --git a/docs/.dockerignore b/docs/.dockerignore new file mode 100644 index 0000000..97aa7fe --- /dev/null +++ b/docs/.dockerignore @@ -0,0 +1,10 @@ +node_modules +dist +.astro +.git +.gitignore +.env +.env.* +!.env.example +*.log +.DS_Store diff --git a/docs/.env.example b/docs/.env.example new file mode 100644 index 0000000..7b11923 --- /dev/null +++ b/docs/.env.example @@ -0,0 +1,18 @@ +# mcaxl docs — local environment. +# +# Copy to .env and adjust. `.env` is gitignored. + +# Distinguishes this compose project from others on the host. +COMPOSE_PROJECT=mcaxl-docs + +# Public hostnames (resolved via the wildcard DNS for *.mcp.l.supported.systems +# that already points at the host's edge caddy-docker-proxy). +# DOMAIN → prod static build +# DEV_DOMAIN → dev server, hot-reload +DOMAIN=mcaxl-docs.mcp.l.supported.systems +DEV_DOMAIN=dev-mcaxl-docs.mcp.l.supported.systems + +# Mode switch — drives which compose profile is active. +# dev → astro dev server + HMR (hot reload) at DEV_DOMAIN +# prod → static build served by in-container Caddy at DOMAIN +COMPOSE_PROFILES=dev diff --git a/docs/Caddyfile b/docs/Caddyfile new file mode 100644 index 0000000..2f5f1a7 --- /dev/null +++ b/docs/Caddyfile @@ -0,0 +1,31 @@ +# Internal Caddy used by the prod container to serve the static site. +# caddy-docker-proxy terminates TLS at the edge, forwards to :80 here. + +:80 { + root * /srv + file_server + encode gzip zstd + + # Long-cache everything Astro puts under /_astro/ (hashed filenames) + @immutable path /_astro/* + header @immutable Cache-Control "public, max-age=31536000, immutable" + + # Short-cache HTML so content updates appear quickly after redeploy. + @html path_regexp htmlrx \.(html)?$ + header @html Cache-Control "public, max-age=60, must-revalidate" + + # Basic security headers + header { + X-Content-Type-Options nosniff + Referrer-Policy strict-origin-when-cross-origin + X-Frame-Options SAMEORIGIN + -Server + } + + # Render 404 for missing pages + handle_errors { + @404 expression {http.error.status_code} == 404 + rewrite @404 /404.html + file_server + } +} diff --git a/docs/Dockerfile b/docs/Dockerfile new file mode 100644 index 0000000..5100086 --- /dev/null +++ b/docs/Dockerfile @@ -0,0 +1,62 @@ +# mcaxl docs — multi-stage build. +# +# Stages: +# deps — install node_modules once, cached by package.json +# dev — runs `astro dev` with the source bind-mounted (compose `dev` profile) +# build — produces the static site in /app/dist +# prod — serves /app/dist with Caddy (static file server, no Node runtime) + +# ---------- Stage: deps -------------------------------------------------- +FROM mirror.gcr.io/library/node:22-alpine AS deps + +ENV ASTRO_TELEMETRY_DISABLED=1 \ + NODE_ENV=development + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN --mount=type=cache,target=/root/.npm \ + npm install --no-audit --no-fund + +# ---------- Stage: dev --------------------------------------------------- +FROM mirror.gcr.io/library/node:22-alpine AS dev + +ENV ASTRO_TELEMETRY_DISABLED=1 \ + NODE_ENV=development \ + HOST=0.0.0.0 + +WORKDIR /app + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +EXPOSE 4321 +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] + +# ---------- Stage: build ------------------------------------------------- +FROM mirror.gcr.io/library/node:22-alpine AS build + +ENV ASTRO_TELEMETRY_DISABLED=1 \ + NODE_ENV=production + +WORKDIR /app + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# DOMAIN must be set at build time so Astro bakes the canonical site URL +# (sitemaps, og: tags, etc.) into the static output. +ARG DOMAIN +ENV DOMAIN=${DOMAIN} + +RUN npm run build + +# ---------- Stage: prod -------------------------------------------------- +# Static site served by Caddy. No Node in the final image. +FROM mirror.gcr.io/library/caddy:2-alpine AS prod + +COPY --from=build /app/dist /srv +COPY Caddyfile /etc/caddy/Caddyfile + +EXPOSE 80 +CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..c347914 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,57 @@ +# mcaxl docs — compose wrapper. +# +# Mode (dev/prod) is driven by COMPOSE_PROFILES in .env. Flip it there. +# `docker compose` honors COMPOSE_PROJECT and COMPOSE_PROFILES automatically. + +COMPOSE ?= docker compose + +.PHONY: help up down restart logs shell ps build clean prod dev net pull-prod pull-dev + +help: ## Show this help + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[36m%-10s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +up: ## Start the active profile (set COMPOSE_PROFILES in .env) + $(COMPOSE) up -d --build + +dev: ## Start dev explicitly (hot reload at https://$${DEV_DOMAIN}) + COMPOSE_PROFILES=dev $(COMPOSE) up -d --build + +prod: ## Start prod explicitly (static site at https://$${DOMAIN}) + COMPOSE_PROFILES=prod $(COMPOSE) up -d --build + +down: ## Stop and remove all containers for this project + COMPOSE_PROFILES=dev $(COMPOSE) down + COMPOSE_PROFILES=prod $(COMPOSE) down + +restart: ## Restart the active profile + $(COMPOSE) restart + +logs: ## Tail logs for active services + $(COMPOSE) logs -f --tail=200 + +shell: ## Shell into the dev container + $(COMPOSE) exec docs-dev sh + +ps: ## Show running containers for this project + $(COMPOSE) ps + +build: ## Build images for both profiles + COMPOSE_PROFILES=dev,prod $(COMPOSE) build + +clean: ## Remove containers, images, and volumes for this project + COMPOSE_PROFILES=dev,prod $(COMPOSE) down --rmi local -v + +net: ## Create the external `caddy` network (one-time, host-wide) + docker network inspect caddy >/dev/null 2>&1 || docker network create caddy + +pull-prod: ## Deploy: git pull, rebuild, and restart the prod container + git pull --ff-only + COMPOSE_PROFILES=prod $(COMPOSE) up -d --build docs-prod + @echo + @COMPOSE_PROFILES=prod $(COMPOSE) ps docs-prod + +pull-dev: ## Deploy: git pull, rebuild, and restart the dev container + git pull --ff-only + COMPOSE_PROFILES=dev $(COMPOSE) up -d --build docs-dev + @echo + @COMPOSE_PROFILES=dev $(COMPOSE) ps docs-dev diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 541b781..d879ef7 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -52,6 +52,7 @@ export default defineConfig({ src: './src/assets/logo.svg', replacesTitle: false, }, + favicon: '/favicon.svg', customCss: ['./src/styles/custom.css'], // Per-page action buttons: "Copy Markdown" + "View in Markdown" diff --git a/docs/docker-compose.yml b/docs/docker-compose.yml new file mode 100644 index 0000000..40b34fe --- /dev/null +++ b/docs/docker-compose.yml @@ -0,0 +1,71 @@ +# mcaxl docs — dev/prod via compose profiles. +# +# Mode is driven by COMPOSE_PROFILES in .env: +# COMPOSE_PROFILES=dev → hot-reload astro dev server at https://${DEV_DOMAIN} +# COMPOSE_PROFILES=prod → static site via in-container Caddy at https://${DOMAIN} +# +# Both profiles sit behind the host's external caddy-docker-proxy on the +# `caddy` network. mcaxl is a public OSS project; no basic auth. +# +# Prerequisite: `docker network create caddy` (one-time, shared with the +# edge caddy-docker-proxy instance). + +services: + docs-dev: + profiles: ["dev"] + build: + context: . + dockerfile: Dockerfile + target: dev + image: ${COMPOSE_PROJECT}-dev + container_name: ${COMPOSE_PROJECT}-dev + restart: unless-stopped + environment: + ASTRO_TELEMETRY_DISABLED: "1" + NODE_ENV: development + DOMAIN: ${DEV_DOMAIN} # Vite HMR binds wss://${DEV_DOMAIN}:443 + volumes: + - .:/app + - /app/node_modules + - /app/.astro + networks: + - caddy + labels: + caddy: ${DEV_DOMAIN} + caddy.reverse_proxy: "{{upstreams 4321}}" + + # ── HMR-through-Caddy streaming tweaks ── + # Vite HMR doesn't send app-level pings; Caddy closes idle WebSockets + # after ~10-15s without these. Refs in ~/.claude/CLAUDE.md. + caddy.reverse_proxy.flush_interval: "-1" + caddy.reverse_proxy.transport: "http" + caddy.reverse_proxy.transport.read_timeout: "0" + caddy.reverse_proxy.transport.write_timeout: "0" + caddy.reverse_proxy.transport.keepalive: "5m" + caddy.reverse_proxy.transport.keepalive_idle_conns: "10" + caddy.reverse_proxy.stream_timeout: "24h" + caddy.reverse_proxy.stream_close_delay: "5s" + + caddy.encode: "gzip zstd" + + docs-prod: + profiles: ["prod"] + build: + context: . + dockerfile: Dockerfile + target: prod + args: + DOMAIN: ${DOMAIN} + image: ${COMPOSE_PROJECT}-prod + container_name: ${COMPOSE_PROJECT}-prod + restart: unless-stopped + networks: + - caddy + labels: + caddy: ${DOMAIN} + caddy.reverse_proxy: "{{upstreams 80}}" + caddy.encode: "gzip zstd" + +networks: + caddy: + external: true diff --git a/docs/public/favicon.svg b/docs/public/favicon.svg new file mode 100644 index 0000000..88c73c1 --- /dev/null +++ b/docs/public/favicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/docs/src/assets/logo.svg b/docs/src/assets/logo.svg index d46845b..d89aa25 100644 --- a/docs/src/assets/logo.svg +++ b/docs/src/assets/logo.svg @@ -1,6 +1,16 @@ -