Web Platform
Static delivery system reference pattern
A platform-agnostic delivery pattern for publishing technical content with static generation, CI validation, privacy checks, containerized runtime, and separate test and production paths.
A static delivery system is deliberately small: content is edited as structured files, compiled into a static build, packaged into a simple web-serving container, and promoted through a validation-first pipeline.
The important design choice is what the system does not need. For many credibility sites, documentation hubs, and lightweight technical portfolios, the public surface does not require a runtime CMS, database, admin panel, analytics backend, or server-side application. The repository can remain the source of truth, while CI acts as the control surface that decides whether a change is formatted, valid, private enough to publish, search-friendly, and safe to deploy.
The architecture diagram separates the pattern into three layers: source, validation and packaging, and runtime routing. The source layer keeps public copy, diagrams, metadata, and system writeups in content files. The validation layer checks formatting, linting, type safety, content schemas, accessibility, links, SEO artifacts, and privacy boundaries before building the static site. The runtime layer ships the build as an immutable container image, deploys automatically to a noindex test path, and promotes to production through a deliberate release step.
Pattern stages
1. Content packages
The content package is the editable source of truth. YAML files hold small structured sections, while Markdown files hold system cards and long-form detail pages.
content/
profile.yml
cta.yml
themes.yml
patterns.yml
accomplishments.yml
systems/
static-delivery-system-reference-pattern.md
The content schema validates that each system has the fields needed for cards, archive pages, SEO, optional diagrams, and detail pages.
const systems = defineCollection({
loader: glob({
pattern: "systems/*.md",
base: "./content",
}),
schema: z.object({
title: z.string().min(1),
summary: z.string().min(1),
category: z.string().min(1),
tags: z.array(z.string().min(1)).min(1),
featured: z.boolean(),
visibility: visibilitySchema,
diagram: z.string().min(1).optional(),
diagramAlt: z.string().min(1).optional(),
}),
});
2. Astro app
The application layer stays intentionally static. Astro produces files that can be served from any basic web runtime, and Tailwind is integrated at build time.
export default defineConfig({
output: "static",
site: "https://example.org",
vite: {
plugins: [tailwindcss()],
},
});
3. GitLab pipeline
The pipeline separates validation, build, image, and deployment work. That makes formatting, content checks, accessibility, links, SEO, and privacy gates visible before any runtime container is promoted.
stages:
- install
- validate
- build
- test
- image
- deploy_test
- deploy_prod
Node jobs install from the lockfile and run the same commands developers can run locally.
.node-job:
image: node:24-alpine
before_script:
- npm ci --cache "$NPM_CONFIG_CACHE" --prefer-offline
format-check:
extends: .node-job
stage: validate
script:
- npm run format:check
4. Static build
The build script runs an Astro check before emitting static files. The validation script composes the local quality gates into one command that mirrors the pipeline shape.
{
"scripts": {
"build": "ASTRO_TELEMETRY_DISABLED=1 astro check && ASTRO_TELEMETRY_DISABLED=1 astro build",
"validate": "npm run format:check && npm run lint && npm run check && npm run build && npm run validate:a11y && npm run validate:links && npm run validate:seo && npm run validate:privacy"
}
}
5. Nginx image
The image uses a multi-stage build. Node does the static compilation, and the runtime stage only carries the generated files plus a minimal web server configuration.
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.29-alpine AS runtime
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
The runtime configuration can also enforce simple hardening headers and route only files that exist in the static output.
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
server_tokens off;
add_header X-Content-Type-Options "nosniff" always;
location / {
try_files $uri $uri/ =404;
}
}
6. Container registry
The image stage tags the build with the immutable commit SHA and pushes that exact image. Deployment jobs pull the same reference instead of rebuilding or relying on a mutable tag.
IMAGE_COMMIT="${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}"
docker build -t "$IMAGE_COMMIT" .
docker run --detach --rm --name "$SMOKE_CONTAINER" "$IMAGE_COMMIT"
docker push "$IMAGE_COMMIT"
7. Runtime containers
The test deployment path runs automatically after the image passes validation and smoke checks. Production is a manual promotion of the same commit image.
deploy-test:
stage: deploy_test
resource_group: static-site-test
script:
- docker pull "$IMAGE_COMMIT"
- docker rm -f static-site-test >/dev/null 2>&1 || true
- docker run --detach --restart unless-stopped --name static-site-test --publish <test-port>:80 "$IMAGE_COMMIT"
deploy-prod:
stage: deploy_prod
resource_group: static-site-production
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
The test path is intentionally treated differently from production. It exists to prove the deployed container works, while robots metadata and server behavior keep it out of search indexes. Production is the only path meant to be discoverable.
map $host $x_robots_tag {
default "";
test.example.org "noindex, nofollow, noarchive";
}
add_header X-Robots-Tag $x_robots_tag always;
8. Public routing
Public routing is intentionally outside this repository. There is no repo-owned Cloudflare, DNS, tunnel, or reverse-proxy configuration to show here. The pattern boundary is that external routing should point a public hostname at the correct container listener without making the static site responsible for edge configuration.
public hostname
-> edge network / tunnel / reverse proxy
-> test or production container listener
-> static web runtime
The public architecture also avoids exposing operational details that do not help a reviewer understand the system. External routing, registry usage, and container promotion are described at the pattern level, while private hostnames, internal addresses, secrets, and administrative configuration stay outside the published content.