diff --git a/.github/workflows/pages.yaml b/.github/workflows/pages.yaml new file mode 100644 index 00000000..07840b41 --- /dev/null +++ b/.github/workflows/pages.yaml @@ -0,0 +1,58 @@ +name: Pages + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + build: + runs-on: ubuntu-latest + env: + HUGO_VERSION: 0.163.3 + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Configure Pages + uses: actions/configure-pages@v6 + + - name: Install Hugo + run: | + mkdir -p "${HOME}/.local/hugo" + curl -sfL --output-dir "${{ runner.temp }}" -O "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_linux-amd64.tar.gz" + tar -C "${HOME}/.local/hugo" -xf "${{ runner.temp }}/hugo_${HUGO_VERSION}_linux-amd64.tar.gz" + echo "${HOME}/.local/hugo" >> "${GITHUB_PATH}" + + - name: Build + run: hugo --cleanDestinationDir --minify --cacheDir "${{ runner.temp }}/hugo_cache" + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v5 + with: + path: ./public + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.gitignore b/.gitignore index 25688d33..da7b5ea5 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,8 @@ acceptance/vendor # Go build and release artifacts /dist/ +/public/ +/.hugo_build.lock # Go binary build artifact in cmd cmd/facts/facts diff --git a/docs/SITE.md b/docs/SITE.md new file mode 100644 index 00000000..ef285d4a --- /dev/null +++ b/docs/SITE.md @@ -0,0 +1,12 @@ +# Documentation Site + +Facts publishes a Hugo documentation site at `https://facts.martinez.io/`. + +Build it from the repository root: + +```sh +hugo --cleanDestinationDir --minify +``` + +The site uses `docs/` as Hugo content, writes generated HTML to `public/`, and +publishes that directory through GitHub Pages. Do not commit `public/`. diff --git a/docs/adr/0013-facts-docs-dedicated-subdomain.md b/docs/adr/0013-facts-docs-dedicated-subdomain.md new file mode 100644 index 00000000..dd8ef001 --- /dev/null +++ b/docs/adr/0013-facts-docs-dedicated-subdomain.md @@ -0,0 +1,9 @@ +# Facts docs use a dedicated custom subdomain + +Facts will publish its documentation site at `https://facts.martinez.io/` from this repository, rather than under the existing `martinez.io` personal site or the default `ncode.github.io/facts` repository Pages URL. The site should stay independently deployable from the Facts repo, with GitHub Pages serving the generated site and a `CNAME` for `facts.martinez.io`. + +## Considered Options + +- **Use `facts.martinez.io`** - chosen: it gives Facts a stable public URL while keeping the project docs independent from the existing personal site. +- **Use `martinez.io/facts`** - rejected: it would couple Facts documentation deployment and routing to the personal website. +- **Use `ncode.github.io/facts`** - rejected: it works as a fallback, but the custom domain is cleaner for public documentation. diff --git a/hugo.toml b/hugo.toml new file mode 100644 index 00000000..7392e3a0 --- /dev/null +++ b/hugo.toml @@ -0,0 +1,14 @@ +baseURL = "https://facts.martinez.io/" +title = "Facts" +contentDir = "docs" +publishDir = "public" +disableKinds = ["rss", "taxonomy", "term"] + +[markup] + [markup.goldmark] + [markup.goldmark.renderer] + unsafe = false + +[caches] + [caches.images] + dir = ":cacheDir/images" diff --git a/layouts/_default/_markup/render-link.html b/layouts/_default/_markup/render-link.html new file mode 100644 index 00000000..28134071 --- /dev/null +++ b/layouts/_default/_markup/render-link.html @@ -0,0 +1,29 @@ +{{- $href := .Destination -}} +{{- if not (or (hasPrefix $href "http://") (hasPrefix $href "https://") (hasPrefix $href "#") (hasPrefix $href "mailto:")) -}} + {{- $clean := replaceRE `^(\./|\.\./)+` "" $href -}} + {{- if eq $clean "adr/" -}} + {{- $href = "/adr/" -}} + {{- else if or (eq $clean "schema/facts.yaml") (eq $clean "docs/schema/facts.yaml") -}} + {{- $href = "https://github.com/ncode/facts/blob/main/docs/schema/facts.yaml" -}} + {{- else if or (eq $clean "supported-facts/") (eq $clean "docs/supported-facts/") -}} + {{- $href = "/supported-facts/" -}} + {{- else if hasSuffix $clean ".md" -}} + {{- $stem := replaceRE `\.md$` "" (path.Base $clean) | lower -}} + {{- if hasPrefix $clean "adr/" -}} + {{- $href = printf "/adr/%s/" $stem -}} + {{- else if eq $stem "history" -}} + {{- $href = "/history/" -}} + {{- else if eq $stem "custom_fact_migration" -}} + {{- $href = "/custom_fact_migration/" -}} + {{- else if eq $stem "facter_conf_compatibility" -}} + {{- $href = "/facter_conf_compatibility/" -}} + {{- else if and (or (eq .Page.Section "supported-facts") (hasPrefix $clean "supported-facts/") (hasPrefix $clean "docs/supported-facts/")) (ne $stem "readme") -}} + {{- $href = printf "/supported-facts/%s/" $stem -}} + {{- else -}} + {{- $href = printf "https://github.com/ncode/facts/blob/main/%s" $clean -}} + {{- end -}} + {{- else if hasPrefix $clean "docs/" -}} + {{- $href = printf "https://github.com/ncode/facts/blob/main/%s" $clean -}} + {{- end -}} +{{- end -}} +{{ .Text | safeHTML }} diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html new file mode 100644 index 00000000..c30028a6 --- /dev/null +++ b/layouts/_default/baseof.html @@ -0,0 +1,31 @@ + + + + + + {{ if .IsHome }}{{ site.Title }}{{ else }}{{ .Title }} | {{ site.Title }}{{ end }} + + + + + + {{ block "main" . }}{{ end }} + + + diff --git a/layouts/_default/list.html b/layouts/_default/list.html new file mode 100644 index 00000000..9f6df695 --- /dev/null +++ b/layouts/_default/list.html @@ -0,0 +1,25 @@ +{{ define "main" }} +
+ {{ partial "docs-nav.html" . }} +
+ {{ if eq .Section "supported-facts" }} + {{ with site.GetPage "/supported-facts/readme" }} + {{ .Content }} + {{ else }} +

{{ partial "page-title.html" . }}

+ {{ end }} + {{ else if .Content }} + {{ .Content }} + {{ else }} +

{{ partial "page-title.html" . }}

+ {{ end }} +
+ {{ range sort .RegularPages "File.Path" }} + {{ if ne (.File.TranslationBaseName | lower) "readme" }} + {{ partial "page-title.html" . }} + {{ end }} + {{ end }} +
+
+
+{{ end }} diff --git a/layouts/_default/single.html b/layouts/_default/single.html new file mode 100644 index 00000000..025dde9f --- /dev/null +++ b/layouts/_default/single.html @@ -0,0 +1,8 @@ +{{ define "main" }} +
+ {{ partial "docs-nav.html" . }} +
+ {{ .Content }} +
+
+{{ end }} diff --git a/layouts/index.html b/layouts/index.html new file mode 100644 index 00000000..d323a63b --- /dev/null +++ b/layouts/index.html @@ -0,0 +1,79 @@ +{{ define "main" }} +
+
+
+
Facts — a Go port of Puppet Facter
+

Every fact about the machine.

+

Facts discovers hardware, networking, OS, cloud metadata, and operator-supplied facts as one canonical tree: embeddable as a Go library, scriptable as the facts CLI.

+ +
+
$ go get github.com/ncode/facts
+$ brew install ncode/tap/facts
+$ facts --json os.family kernel.version.full
+
+
+
+ +
+
+
+
Library
+

Hermetic by default.

+

An engine is an isolated, immutable unit of discovery configuration. It reads core facts only until you opt into config files, external facts, registered facts, or system defaults.

+
+
+
+

Engine

+

Construct explicit discovery inputs with no package-global collector and no shared mutable state.

+
+
+

Snapshot

+

Discover once, then query and decode an immutable canonical tree as often as needed.

+
+
+

Typed views

+

Decode subtrees into caller-owned Go types and fail loudly when the shape does not match.

+
+
+
+
+ +
+
+
+
CLI
+

Every fact, one command.

+

The facts binary keeps the Ruby Facter process-boundary contract for output formatting, exit status, stderr diagnostics, external facts, environment facts, and config semantics.

+
+
$ facts os.name
+Darwin
+
+$ facts --external-dir ./facts.d site_role
+web
+
+
+ +
+
+
+
Supported facts
+

Tested where it ships.

+

The schema-backed supported fact pages are generated from docs/schema/facts.yaml and rendered here without becoming a second source of truth.

+
+
+ {{ with site.GetPage "/supported-facts" }} + {{ range sort .RegularPages "File.Path" }} + {{ if ne (.File.TranslationBaseName | lower) "readme" }} +

{{ partial "page-title.html" . }}

Schema-backed reference
+ {{ end }} + {{ end }} + {{ end }} +
+
+
+
+{{ end }} diff --git a/layouts/partials/docs-nav.html b/layouts/partials/docs-nav.html new file mode 100644 index 00000000..ac27040a --- /dev/null +++ b/layouts/partials/docs-nav.html @@ -0,0 +1,33 @@ + diff --git a/layouts/partials/page-title.html b/layouts/partials/page-title.html new file mode 100644 index 00000000..589388eb --- /dev/null +++ b/layouts/partials/page-title.html @@ -0,0 +1,10 @@ +{{- $title := "" -}} +{{- with findRE `(?m)^#\s+(.+)$` .RawContent 1 -}} + {{- $title = replaceRE `^#\s+` "" (index . 0) -}} +{{- else -}} + {{- $title = .Title -}} + {{- if and (not $title) .File -}} + {{- $title = .File.TranslationBaseName | humanize | title -}} + {{- end -}} +{{- end -}} +{{- return $title -}} diff --git a/openspec/changes/add-hugo-github-pages-site/.openspec.yaml b/openspec/changes/add-hugo-github-pages-site/.openspec.yaml new file mode 100644 index 00000000..de73b342 --- /dev/null +++ b/openspec/changes/add-hugo-github-pages-site/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-25 diff --git a/openspec/changes/add-hugo-github-pages-site/design.md b/openspec/changes/add-hugo-github-pages-site/design.md new file mode 100644 index 00000000..e3244c3c --- /dev/null +++ b/openspec/changes/add-hugo-github-pages-site/design.md @@ -0,0 +1,38 @@ +## Context + +Facts already has a polished README, generated supported-fact Markdown, and reference docs under `docs/`, but it does not publish a standalone website. The agreed direction is a Hugo site published from this repository to `https://facts.martinez.io/`, independent from the existing `martinez.io` personal site. + +The current repository has no JavaScript site pipeline. The site should therefore add only Hugo source and a GitHub Pages workflow, keeping generated HTML out of git and keeping `docs/schema/facts.yaml` plus `tools/supportedfacts` as the owners of supported-fact reference content. + +## Goals / Non-Goals + +**Goals:** + +- Publish a static Hugo documentation site for Facts at `https://facts.martinez.io/`. +- Reuse existing Markdown documentation under `docs/` instead of duplicating docs into a second tree. +- Build and deploy with GitHub Actions using the Pages artifact flow. +- Match the README visual language and the Vercel-inspired reference: near-white surfaces, near-black ink, mono technical labels, restrained cards, and a hero-scale mesh gradient. +- Keep v1 dependency-light: no npm, no theme dependency, no client-side JavaScript requirement. + +**Non-Goals:** + +- No docs search, version switcher, analytics, or interactive terminal in v1. +- No committed generated HTML. +- No change to fact schema generation, fact resolution, CLI output, or library API. +- No merge with the existing `martinez.io` website. + +## Decisions + +- **Use Hugo with custom layouts.** Hugo renders the existing Markdown and gives GitHub Pages a plain static artifact. A third-party theme is rejected because it would add dependency churn and fight the README-derived design. +- **Keep Hugo source at the repository root and `docs/` as content.** Root-level `hugo.toml`, `layouts/`, and `static/` keep the site obvious while avoiding a duplicate `site/content` copy of the docs. +- **Use `facts.martinez.io` as the canonical base URL.** This follows ADR-0013 and keeps Facts documentation independent from the personal site. `static/CNAME` owns the Pages custom-domain file. +- **Publish from GitHub Actions artifacts.** The workflow builds Hugo into `public/` and deploys with the GitHub Pages artifact actions. Generated HTML stays out of git. +- **Let the schema generator own supported-fact pages.** Hugo only renders `docs/supported-facts/*.md`; the Go generator and existing tests remain responsible for keeping them in sync with `docs/schema/facts.yaml`. +- **Use system fonts and local CSS only.** The README SVG already uses platform sans and mono stacks. External font packages, npm, and client-side scripts are rejected for v1. + +## Risks / Trade-offs + +- **Markdown without front matter may render with weak titles** -> derive titles in Hugo templates from headings or file names, and add front matter only to hand-authored docs if needed. +- **`docs/` contains non-page assets and generated files** -> configure Hugo/layouts so only intended Markdown pages appear in navigation, while static assets are served from `static/` or copied deliberately. +- **GitHub Pages custom domain needs DNS outside the repo** -> commit `static/CNAME` and document that DNS for `facts.martinez.io` must point at the Pages host. +- **No theme means fewer built-in docs features** -> acceptable for v1; add search or deeper navigation only when docs volume requires it. diff --git a/openspec/changes/add-hugo-github-pages-site/proposal.md b/openspec/changes/add-hugo-github-pages-site/proposal.md new file mode 100644 index 00000000..76a91d81 --- /dev/null +++ b/openspec/changes/add-hugo-github-pages-site/proposal.md @@ -0,0 +1,29 @@ +## Why + +Facts has a strong README and generated reference docs, but no standalone public documentation site. A GitHub Pages site gives Facts a stable project URL at `https://facts.martinez.io/` while keeping the existing Markdown docs as the source of truth. + +## What Changes + +- Add a Hugo-powered documentation site for Facts. +- Publish the site with GitHub Pages from a GitHub Actions build artifact, not committed generated HTML. +- Use the existing `docs/` Markdown and generated supported-fact pages as Hugo content. +- Add a custom domain CNAME for `facts.martinez.io`. +- Style the site from the README visual language: near-white canvas, near-black ink, mono technical labels, and the existing mesh-gradient hero palette. +- Keep v1 static: no npm, no Hugo modules, no theme dependency, no client-side interactivity. + +## Capabilities + +### New Capabilities + +- `facts-documentation-site`: Public Hugo documentation site for Facts, including homepage, docs navigation, custom domain, and GitHub Pages deployment. + +### Modified Capabilities + +(none) + +## Impact + +- **Docs/site**: root Hugo configuration, custom layouts, CSS, static assets, CNAME, and docs content metadata or index pages as needed. +- **CI/deployment**: GitHub Actions workflow for building Hugo and publishing to GitHub Pages. +- **Existing docs**: `docs/supported-facts/*.md` remain generated from `docs/schema/facts.yaml`; Hugo renders them but does not own their content. +- **Product behavior**: No change to fact resolution, CLI behavior, library API, schema contract, or release target support. diff --git a/openspec/changes/add-hugo-github-pages-site/specs/facts-documentation-site/spec.md b/openspec/changes/add-hugo-github-pages-site/specs/facts-documentation-site/spec.md new file mode 100644 index 00000000..39b277a4 --- /dev/null +++ b/openspec/changes/add-hugo-github-pages-site/specs/facts-documentation-site/spec.md @@ -0,0 +1,68 @@ +## ADDED Requirements + +### Requirement: Hugo documentation site +Facts SHALL provide a Hugo-based static documentation site whose source is committed to the repository and whose generated HTML is not committed. + +#### Scenario: Local Hugo build +- **WHEN** a contributor runs the documented Hugo build command from the repository root +- **THEN** Hugo MUST generate the site into the configured output directory +- **AND** the generated output directory MUST remain ignored or otherwise uncommitted + +#### Scenario: No JavaScript package pipeline +- **WHEN** a contributor inspects the site build inputs +- **THEN** the site MUST NOT require npm, a JavaScript bundler, Hugo modules, or a third-party Hugo theme + +### Requirement: Homepage presents Facts +The documentation site SHALL include a homepage that presents Facts as both an embeddable Go library and the `facts` CLI. + +#### Scenario: Homepage primary message +- **WHEN** a visitor opens `https://facts.martinez.io/` +- **THEN** the first screen MUST identify the product as Facts +- **AND** it MUST describe Facts as a Go port of Puppet Facter with both library and CLI usage + +#### Scenario: Homepage install actions +- **WHEN** a visitor scans the homepage +- **THEN** it MUST expose the Go library install command `go get github.com/ncode/facts` +- **AND** it MUST expose the CLI install command `brew install ncode/tap/facts` + +### Requirement: Existing docs are rendered as site content +The documentation site SHALL render the existing Markdown documentation under `docs/` as the source content for documentation pages. + +#### Scenario: Docs navigation +- **WHEN** a visitor uses the site navigation +- **THEN** it MUST expose shallow groups for Start, Library, CLI, Supported facts, and Project +- **AND** those groups MUST link to the relevant existing documentation pages + +#### Scenario: Supported fact pages remain generated +- **WHEN** supported fact reference pages are displayed on the site +- **THEN** they MUST be rendered from `docs/supported-facts/*.md` +- **AND** their content MUST remain owned by the existing schema-driven generation flow + +### Requirement: README-derived visual system +The documentation site SHALL use the visual language already established in the README assets. + +#### Scenario: Palette and typography +- **WHEN** the site is rendered +- **THEN** it MUST use the README palette: near-black ink, near-white canvas surfaces, muted gray text, hairline borders, and the existing blue/cyan/violet/pink/coral/amber accent colors +- **AND** it MUST use system sans-serif and monospace font stacks without remote font dependencies + +#### Scenario: Hero visual treatment +- **WHEN** the homepage first screen is rendered +- **THEN** it MUST include a hero-scale mesh-gradient treatment based on the README hero colors +- **AND** the gradient MUST be used as a large atmospheric element, not as small decorative swatches + +### Requirement: GitHub Pages custom-domain deployment +Facts SHALL deploy the generated documentation site to GitHub Pages at `https://facts.martinez.io/`. + +#### Scenario: Pages artifact deployment +- **WHEN** changes are merged to the default branch +- **THEN** a GitHub Actions workflow MUST build the Hugo site +- **AND** it MUST publish the generated site using GitHub Pages artifact deployment + +#### Scenario: Custom domain file +- **WHEN** the site is built for GitHub Pages +- **THEN** the generated artifact MUST include a `CNAME` file containing `facts.martinez.io` + +#### Scenario: Canonical base URL +- **WHEN** Hugo renders absolute URLs +- **THEN** the configured base URL MUST be `https://facts.martinez.io/` diff --git a/openspec/changes/add-hugo-github-pages-site/tasks.md b/openspec/changes/add-hugo-github-pages-site/tasks.md new file mode 100644 index 00000000..7a5f3849 --- /dev/null +++ b/openspec/changes/add-hugo-github-pages-site/tasks.md @@ -0,0 +1,33 @@ +## 1. Hugo Site Setup + +- [x] 1.1 Add root Hugo configuration with `baseURL = "https://facts.martinez.io/"`, `docs/` as the content source, and no theme. +- [x] 1.2 Add `static/CNAME` containing `facts.martinez.io`. +- [x] 1.3 Ensure Hugo generated output such as `public/` is ignored and not committed. +- [x] 1.4 Document the local Hugo build command in the site or repository docs. + +## 2. Layouts And Styling + +- [x] 2.1 Add minimal custom Hugo layouts for the homepage, single docs pages, docs lists, and shared page chrome. +- [x] 2.2 Add local CSS using the README palette, system sans/mono font stacks, restrained cards, hairline borders, and responsive spacing. +- [x] 2.3 Build the homepage hero with Facts as the first-viewport signal and a large README-color mesh-gradient treatment. +- [x] 2.4 Add static terminal/code blocks for `go get github.com/ncode/facts`, `brew install ncode/tap/facts`, and representative `facts` CLI usage. + +## 3. Documentation Content And Navigation + +- [x] 3.1 Add shallow navigation groups for Start, Library, CLI, Supported facts, and Project. +- [x] 3.2 Map existing `docs/` Markdown pages into those navigation groups without duplicating their content into a second docs tree. +- [x] 3.3 Render `docs/supported-facts/*.md` as supported-fact reference pages while leaving their content owned by the schema generator. +- [x] 3.4 Keep non-page docs assets and schema files out of primary navigation unless explicitly linked. + +## 4. GitHub Pages Deployment + +- [x] 4.1 Add a GitHub Actions workflow that builds Hugo on the default branch and `workflow_dispatch`. +- [x] 4.2 Publish the generated `public/` directory through the GitHub Pages artifact deployment flow. +- [x] 4.3 Configure the workflow permissions and concurrency needed for GitHub Pages deployment. + +## 5. Verification + +- [x] 5.1 Run the local Hugo build and confirm it includes the homepage, docs pages, supported-fact pages, and `CNAME`. +- [x] 5.2 Inspect the generated homepage and at least one supported-fact page for broken links or missing styling. +- [x] 5.3 Run `go test ./...` to catch any generated supported-fact documentation drift. +- [x] 5.4 Run `go vet ./...` before handoff. diff --git a/site_contract_test.go b/site_contract_test.go new file mode 100644 index 00000000..1d30cc72 --- /dev/null +++ b/site_contract_test.go @@ -0,0 +1,111 @@ +package facts + +import ( + "os" + "strings" + "testing" +) + +func TestHugoSiteContract(t *testing.T) { + checkFileContains(t, "hugo.toml", + `baseURL = "https://facts.martinez.io/"`, + `contentDir = "docs"`, + `publishDir = "public"`, + `disableKinds = ["rss", "taxonomy", "term"]`, + `dir = ":cacheDir/images"`, + ) + checkFileExcludes(t, "hugo.toml", "theme =", "[module]") + + checkFileContains(t, "static/CNAME", "facts.martinez.io") + checkFileContains(t, ".gitignore", "/public/", "/.hugo_build.lock") + + checkFileContains(t, "layouts/index.html", + "Facts", + "Go port of Puppet Facter", + "go get github.com/ncode/facts", + "brew install ncode/tap/facts", + "facts --json os.family kernel.version.full", + `site.GetPage "/supported-facts"`, + ".RegularPages", + ) + + checkFileContains(t, "layouts/_default/baseof.html", + "css/site.css", + "Start", + "Library", + "CLI", + "Supported facts", + "Project", + ) + checkFileContains(t, "layouts/_default/single.html", ".Content") + checkFileContains(t, "layouts/_default/list.html", ".RegularPages", `site.GetPage "/supported-facts/readme"`) + checkFileContains(t, "layouts/partials/docs-nav.html", `site.GetPage "/supported-facts"`, ".RegularPages") + checkFileContains(t, "layouts/partials/page-title.html", "RawContent", `findRE`) + checkFileContains(t, "layouts/_default/_markup/render-link.html", + `/supported-facts/%s/`, + `docs/supported-facts/`, + `docs/schema/facts.yaml`, + `github.com/ncode/facts/blob/main`, + ) + checkFileExcludes(t, "layouts/_default/_markup/render-link.html", "safeURL") + checkFileExcludes(t, "layouts/index.html", `RelPermalink "/supported-facts/readme/"`) + + checkFileContains(t, "static/css/site.css", + "#171717", + "#fafafa", + "#4d4d4d", + "#666666", + "#ebebeb", + "#007cf0", + "#00dfd8", + "#7928ca", + "#ff0080", + "#ff4d4d", + "#f9cb28", + "SFMono-Regular", + ":focus-visible", + "scroll-margin-top", + "text-underline-offset", + ) + checkFileExcludes(t, "static/css/site.css", "@import", "https://", "http://") + + checkFileContains(t, ".github/workflows/pages.yaml", + "workflow_dispatch:", + "actions/configure-pages@", + "actions/upload-pages-artifact@", + "actions/deploy-pages@", + "hugo --cleanDestinationDir --minify", + "pages: write", + "id-token: write", + "path: ./public", + ) +} + +func checkFileContains(t *testing.T, path string, needles ...string) { + t.Helper() + body := readTextFile(t, path) + for _, needle := range needles { + if !strings.Contains(body, needle) { + t.Fatalf("%s missing %q", path, needle) + } + } +} + +func checkFileExcludes(t *testing.T, path string, needles ...string) { + t.Helper() + body := readTextFile(t, path) + for _, needle := range needles { + if strings.Contains(body, needle) { + t.Fatalf("%s unexpectedly contains %q", path, needle) + } + } +} + +func readTextFile(t *testing.T, path string) string { + t.Helper() + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + return string(data) +} diff --git a/static/CNAME b/static/CNAME new file mode 100644 index 00000000..df45f8e5 --- /dev/null +++ b/static/CNAME @@ -0,0 +1 @@ +facts.martinez.io diff --git a/static/css/site.css b/static/css/site.css new file mode 100644 index 00000000..c97301b5 --- /dev/null +++ b/static/css/site.css @@ -0,0 +1,501 @@ +:root { + --ink: #171717; + --canvas: #ffffff; + --canvas-soft: #fafafa; + --canvas-soft-2: #f5f5f5; + --body: #4d4d4d; + --mute: #666666; + --hairline: #ebebeb; + --hairline-strong: #a1a1a1; + --blue: #007cf0; + --cyan: #00dfd8; + --mint: #50e3c2; + --violet: #7928ca; + --pink: #ff0080; + --coral: #ff4d4d; + --amber: #f9cb28; + --link: #0070f3; + --shadow: 0 1px 1px rgb(0 0 0 / 3%), 0 8px 24px rgb(0 0 0 / 6%); + color-scheme: light; +} + +* { + box-sizing: border-box; +} + +html { + background: var(--canvas-soft); + color: var(--ink); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; + line-height: 1.5; + text-size-adjust: 100%; +} + +body { + margin: 0; + background: var(--canvas-soft); +} + +a { + color: var(--link); + text-decoration: none; +} + +.doc-content a { + text-decoration: underline; + text-underline-offset: 0.12em; +} + +a:hover { + text-decoration: underline; +} + +img, +svg { + max-width: 100%; +} + +code, +pre, +.eyebrow, +.site-wordmark, +.doc-nav-title, +.platform-count { + font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Monaco, monospace; +} + +code { + border-radius: 4px; + background: var(--canvas-soft-2); + color: var(--ink); + font-size: 0.92em; + padding: 0.12rem 0.28rem; +} + +pre { + margin: 0; + overflow: auto; + border: 1px solid rgb(255 255 255 / 14%); + border-radius: 8px; + background: var(--ink); + color: var(--canvas); + font-size: 0.86rem; + line-height: 1.7; + padding: 1rem; +} + +pre code { + border: 0; + background: transparent; + color: inherit; + padding: 0; +} + +.site-header { + position: sticky; + top: 0; + z-index: 20; + border-bottom: 1px solid var(--hairline); + background: rgb(255 255 255 / 86%); + backdrop-filter: blur(18px); +} + +.site-header-inner, +.site-footer-inner, +.band-inner, +.doc-shell { + width: min(1120px, calc(100% - 32px)); + margin: 0 auto; +} + +.site-header-inner { + display: flex; + min-height: 64px; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.site-wordmark { + color: var(--ink); + font-size: 0.9rem; + font-weight: 600; +} + +.site-nav { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + gap: 0.25rem; +} + +.site-nav a, +.button { + border-radius: 999px; + color: var(--body); + font-size: 0.88rem; + font-weight: 500; + line-height: 1; + padding: 0.58rem 0.78rem; +} + +.site-nav a:hover, +.button:hover { + background: var(--canvas-soft-2); + color: var(--ink); + text-decoration: none; +} + +.site-nav a:focus-visible, +.button:focus-visible, +.doc-nav a:focus-visible, +.doc-list a:focus-visible, +.platform-card:focus-visible { + outline: 2px solid var(--link); + outline-offset: 3px; +} + +.button { + display: inline-flex; + align-items: center; + border: 1px solid var(--hairline); + background: var(--canvas); + color: var(--ink); +} + +.button-primary { + border-color: var(--ink); + background: var(--ink); + color: var(--canvas); +} + +.button-primary:hover { + background: #000000; + color: var(--canvas); +} + +.hero { + position: relative; + min-height: 72vh; + overflow: hidden; + border-bottom: 1px solid var(--hairline); + background: var(--canvas-soft); +} + +.hero::before { + position: absolute; + inset: -18% -12% auto -12%; + height: 58%; + background: + radial-gradient(circle at 14% 34%, rgb(121 40 202 / 38%), transparent 20%), + radial-gradient(circle at 34% 12%, rgb(0 124 240 / 36%), transparent 20%), + radial-gradient(circle at 50% 34%, rgb(0 223 216 / 34%), transparent 21%), + radial-gradient(circle at 66% 15%, rgb(255 0 128 / 32%), transparent 20%), + radial-gradient(circle at 82% 30%, rgb(255 77 77 / 26%), transparent 18%), + radial-gradient(circle at 94% 16%, rgb(249 203 40 / 34%), transparent 18%); + content: ""; + filter: blur(42px); +} + +.hero-content { + position: relative; + display: grid; + min-height: 72vh; + align-content: center; + justify-items: center; + gap: 1.5rem; + padding: 5rem 0 4rem; + text-align: center; +} + +.eyebrow { + color: var(--mute); + font-size: 0.75rem; + letter-spacing: 0; + text-transform: uppercase; +} + +.hero h1 { + max-width: 780px; + margin: 0; + color: var(--ink); + font-size: clamp(2.4rem, 7vw, 4.75rem); + font-weight: 600; + letter-spacing: 0; + line-height: 1; +} + +.hero-lede { + max-width: 700px; + margin: 0; + color: var(--body); + font-size: 1.12rem; +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.75rem; +} + +.command-strip { + width: min(760px, 100%); + border: 1px solid var(--hairline); + border-radius: 8px; + background: rgb(255 255 255 / 78%); + box-shadow: var(--shadow); + padding: 0.7rem; +} + +.command-strip pre { + text-align: left; +} + +.band { + scroll-margin-top: 80px; + border-bottom: 1px solid var(--hairline); + background: var(--canvas); + padding: 4rem 0; +} + +.band-soft { + background: var(--canvas-soft); +} + +.band-dark { + background: var(--ink); + color: var(--canvas); +} + +.section-heading { + max-width: 680px; + margin: 0 0 2rem; +} + +.section-heading h2 { + margin: 0 0 0.75rem; + font-size: clamp(1.8rem, 4vw, 2.55rem); + font-weight: 600; + line-height: 1.1; +} + +.section-heading p { + margin: 0; + color: var(--body); + font-size: 1rem; +} + +.band-dark .section-heading p { + color: var(--hairline-strong); +} + +.feature-grid, +.platform-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; +} + +.feature-card, +.platform-card { + border: 1px solid var(--hairline); + border-radius: 8px; + background: var(--canvas); + padding: 1.25rem; +} + +.feature-card h3, +.platform-card h3 { + margin: 0 0 0.6rem; + font-size: 1rem; +} + +.feature-card p, +.platform-card p { + margin: 0; + color: var(--body); + font-size: 0.94rem; +} + +.platform-card { + display: flex; + min-height: 104px; + flex-direction: column; + justify-content: space-between; +} + +.platform-count { + color: var(--mute); + font-size: 0.75rem; +} + +.doc-shell { + display: grid; + grid-template-columns: 240px minmax(0, 1fr); + gap: 2rem; + padding: 2.5rem 0 4rem; +} + +.doc-nav { + position: sticky; + top: 88px; + align-self: start; + border-right: 1px solid var(--hairline); + padding-right: 1rem; +} + +.doc-nav-group + .doc-nav-group { + margin-top: 1.4rem; +} + +.doc-nav-title { + margin-bottom: 0.55rem; + color: var(--mute); + font-size: 0.72rem; + text-transform: uppercase; +} + +.doc-nav a { + display: block; + border-radius: 6px; + color: var(--body); + font-size: 0.9rem; + padding: 0.28rem 0.4rem; +} + +.doc-nav a:hover { + background: var(--canvas-soft-2); + color: var(--ink); + text-decoration: none; +} + +.doc-content { + min-width: 0; + border: 1px solid var(--hairline); + border-radius: 8px; + background: var(--canvas); + padding: min(5vw, 3rem); +} + +.doc-content h1, +.doc-content h2, +.doc-content h3 { + color: var(--ink); + font-weight: 600; + line-height: 1.16; + scroll-margin-top: 88px; +} + +.doc-content h1 { + margin-top: 0; + font-size: clamp(2rem, 5vw, 3rem); +} + +.doc-content h2 { + margin-top: 2.5rem; + border-top: 1px solid var(--hairline); + padding-top: 1.5rem; + font-size: 1.55rem; +} + +.doc-content h3 { + margin-top: 1.8rem; + font-size: 1.16rem; +} + +.doc-content p, +.doc-content li { + color: var(--body); +} + +.doc-content table { + display: block; + width: 100%; + overflow-x: auto; + border-collapse: collapse; + font-size: 0.9rem; +} + +.doc-content th, +.doc-content td { + border-bottom: 1px solid var(--hairline); + padding: 0.55rem 0.7rem; + text-align: left; + vertical-align: top; +} + +.doc-content th { + background: var(--canvas-soft); + color: var(--ink); +} + +.doc-list { + display: grid; + gap: 0.75rem; + margin-top: 1.5rem; +} + +.doc-list a { + display: block; + border: 1px solid var(--hairline); + border-radius: 8px; + background: var(--canvas-soft); + color: var(--ink); + padding: 1rem; + text-decoration: none; +} + +.site-footer { + border-top: 1px solid var(--hairline); + background: var(--canvas); + color: var(--mute); + font-size: 0.84rem; + padding: 2rem 0; +} + +.site-footer-inner { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 1rem; +} + +@media (max-width: 860px) { + .site-header-inner { + align-items: flex-start; + flex-direction: column; + padding: 0.8rem 0; + } + + .site-nav { + justify-content: flex-start; + } + + .hero, + .hero-content { + min-height: 0; + } + + .hero-content { + padding: 4rem 0 3rem; + } + + .feature-grid, + .platform-grid, + .doc-shell { + grid-template-columns: 1fr; + } + + .doc-nav { + position: static; + border-right: 0; + border-bottom: 1px solid var(--hairline); + padding: 0 0 1.5rem; + } + + .doc-content { + padding: 1.25rem; + } +}