Compare commits

..

13 Commits

Author SHA1 Message Date
38b0bcf11d Merge pull request '#41: add cibuild and precommit make shims (closes #34)'
All checks were successful
check / check (push) Successful in 11s
Build and Deploy to Cloudflare Pages / build (push) Successful in 45s
Build and Deploy to Cloudflare Pages / deploy (push) Successful in 18s
2026-08-10 16:14:53 +02:00
clawbot
fd3cd4c18c Add Makefile shims for cibuild and precommit (closes #34)
All checks were successful
check / check (push) Successful in 16s
script/cibuild and script/precommit both existed and were already the
documented CI and pre-commit entrypoints, but neither had a Makefile
target, so the standing rule to drive the repo through make targets
rather than the underlying tool could not be followed for either.

It matters most for the build. A bare `docker build .` fails closed on
the CHECK_EPOCH guard by design, so script/cibuild is one of only three
supported ways to build an image here, and it was the only one of the
three without a target while `make docker` had one.

The two targets are thin shims in the same style as every other target
and change nothing about what the scripts do. .PHONY was already
complete for the targets that existed and now lists both new ones.

README.md's Entrypoints section gains the script-to-target mapping so
the two documents agree, including the two names that do not match:
script/install-precommit is `make hooks`, and script/precommit is
`make precommit`. It also notes that `make cibuild` is the slowest
target, because it is the only one that runs two container builds --
while still taking seconds once the shared script/bootstrap layer is
cached, since Dockerfile.lint's first four instructions are
byte-identical to the main Dockerfile's and the pinned-Hugo compile is
therefore paid once per machine rather than twice.

Two accuracy fixes to text the same section already carried. The
script/install-precommit bullet said the installed hook runs
script/check; the script writes script/precommit into
.git/hooks/pre-commit, and its own header comment says so. And the
Makefile is described as listing the operations you are expected to run
rather than as the authoritative list of everything the repo can do,
which is not literally true: script/projectname is an internal helper
that script/docker calls to compute a tag, and it has no target
deliberately -- a target for it would be noise in the `make<tab>`
listing this change exists to make useful.

TODO.md also loses the stale Future Step asking someone to confirm the
static/_headers file took effect in production. That was confirmed live
on both hostnames on 2026-08-10 and recorded at
#14 , so the item is work
already done. The Status paragraph's matching "unverified in production
until the next deploy" clause is corrected for the same reason: a commit
that edits TODO.md should not leave a known-false statement in it.
2026-08-10 13:57:34 +00:00
910f343263 Merge pull request '#39: MIT LICENSE and containerised lint (closes #10, closes #38)'
All checks were successful
check / check (push) Successful in 24s
Build and Deploy to Cloudflare Pages / build (push) Successful in 50s
Build and Deploy to Cloudflare Pages / deploy (push) Successful in 18s
2026-08-10 15:35:21 +02:00
clawbot
25b6c0a9de Run the lint inside Docker via Dockerfile.lint (closes #38)
All checks were successful
check / check (push) Successful in 1m23s
Add a root Dockerfile.lint that runs `hugo --minify --printPathWarnings`
as a build step, so a successful build IS a clean lint, and reduce
script/lint to building that file. There is no host lint path and
deliberately no "am I already inside a container?" branch, which would
be a host lint path in disguise.

The containerisation boundary is lint only, per the owner ruling on the
issue: formatting is not a lint, so script/fmt and script/fmt-check stay
on the host, unchanged in version, scope and flags. That also removes
the forced duplication of prettier's settings between a script and a
Dockerfile, and with it the keep-in-sync notes that duplication needed.

Dockerfile.lint has exactly one stage on purpose. A whole-file
`docker build -f Dockerfile.lint .` builds only the file's last stage,
and sibling stages off a shared base carry no ordering edge, so a second
stage beside the lint would be silently skipped by exactly the
invocation the canonical org-wide script/lint uses -- a green that
linted nothing, which the per-stage CHECK_EPOCH guard cannot catch
because the stage that did run satisfies it. With one stage there is
nothing to skip and script/lint needs no --target. A comment in the file
says that any second check added here must be chained or carry an
explicit ordering edge, never left as a sibling.

Its first four instructions are byte-identical to the main Dockerfile's
and in the same order, so the expensive `RUN script/bootstrap` layer
that compiles the pinned Hugo from source is shared between the two
images rather than paid twice.

Resolve the recursion by direction, not detection. `make check` calls
script/lint, and script/lint is now a `docker build`, so `RUN make
check` in an image would attempt a docker build inside a build step
where there is no daemon. The main Dockerfile therefore runs the
individual non-lint checks -- script/test and script/fmt-check, as
separate RUN lines under the CHECK_EPOCH guard -- matching the canonical
shape, and only the lint is absent from it. script/cibuild runs
script/lint first, for fail-fast feedback: on a runner with no cached
bootstrap layer a lint failure should not wait behind a Hugo build from
source. CI coverage is therefore unchanged, and it runs the same scripts
a developer runs.

Caching is waived for the lint in the shape this repo already settled:
ARG CHECK_EPOCH with no default, guarded with
`[ -n "$CHECK_EPOCH" ] || exit 1`, and the value expanded into the
linted command as well as the guard, so invalidation never rests on
BuildKit's treatment of an unreferenced ARG. Every image-building
entrypoint generates and passes it -- script/cibuild, script/docker,
script/lint -- each as a whole assignment rather than inline, for the
`set -e` reason script/cibuild documents.

script/lint builds with `--output type=cacheonly`: the build is run for
its exit status, not for an image, and because the lint layer is
cache-busted on every invocation an exporting build leaves one dangling
image per lint run. On a host shared with other work that accumulates.
The build cache is unaffected, so script/bootstrap still hits, and
failures still propagate.

Two divergences from REPO_POLICIES.md, stated rather than buried:

  - REPO_POLICIES.md:92, "all Dockerfiles must run `make check`". That
    rule and "every lint run happens in Docker" cannot both hold once
    `make check` contains the lint.
  - REPO_POLICIES.md:102-168, which requires a separate lint stage whose
    result the build stage depends on through
    `COPY --from=lint /src/go.sum /dev/null`, on the stated grounds that
    without the edge "the build stage would not wait for lint to finish
    and a lint failure might not fail the overall build". No such edge
    exists here: the lint is its own file and its own build, sequenced
    by script/cibuild rather than by BuildKit. Both sections are
    superseded upstream by 12e8db8 in sneak/prompts, which deletes the
    Go multistage lint stage and its ordering trick for the same reason
    -- that stage ran `make lint`, which is now a docker build.

Verified: two consecutive script/lint runs on an unchanged tree both
executed hugo for real, distinct epochs echoed, script/bootstrap CACHED,
second run 0.85s; a whole-file `docker build -f Dockerfile.lint .` with
the argument and no --target ran the lint for real; a bare build with no
argument failed closed on the guard; a planted template error failed the
lint with hugo's own render error and made script/cibuild exit non-zero
in 0.6s with the main image build never starting; a planted over-long
line failed the host script/fmt-check; both reverted and re-run clean;
`make check`, script/docker and script/cibuild all green with every
check layer observed executing rather than served from cache, and the
bootstrap layer CACHED in both images. The deploy path is byte-identical
to main: .gitea/, script/bootstrap, script/test and .dockerignore are
untouched.
2026-08-10 13:20:40 +00:00
clawbot
407b0a0d79 Add the MIT LICENSE and state it in the README (closes #10)
All checks were successful
check / check (push) Successful in 11s
The repo had no LICENSE, which REPO_POLICIES.md lists as a mandatory
minimum file, and the README's License section said "Content is provided
as-is for community use." That granted nothing explicitly and matched no
committed file.

The repo is public, verified on the Gitea API rather than assumed, so
the standing policy applies: MIT on any public repo lacking a license.
LICENSE is byte-identical to the canonical sneak/homoicon copy (same git
blob, 3274443) and its body is word-for-word the SPDX MIT text, with
only the line wrapping differing.

The README License section now reads "MIT. See LICENSE.", and says
explicitly that the licence covers content/ as well as the code: this
repo carries both a Hugo site and its community content, while MIT's own
text speaks only of "the Software". The Description first line gains the
licence, which the README requirements call for and which was the one
field it was missing.

Nothing published contradicts the choice. The built public/ tree carries
no copyright, all-rights-reserved or terms-of-use string in index.html,
css/style.css, index.xml or sitemap.xml; the rendered footer names
@sneak and links the repo but reserves no rights, and the RSS carries no
copyright element. The content is factual mesh channel data asserting no
licence of its own.

LICENSE needed no .prettierignore entry, measured rather than assumed:
script/fmt passes prettier the explicit globs '**/*.md' and '**/*.css',
and an extensionless root file matches neither. A script/fmt run leaves
the file's hash unchanged, and a counterfactual LICENSE.md copy was
reflowed by that same run, which is the direct evidence that the
extension is what excludes it and not an ignore rule.

Per-file licence headers and SPDX identifiers are deliberately omitted;
no org standard mandates them.

Nothing on the deploy path is touched.
2026-08-10 12:31:30 +00:00
7d7bec526c Merge pull request '#37: add Cloudflare Pages _headers (closes #14)'
All checks were successful
Build and Deploy to Cloudflare Pages / build (push) Successful in 47s
Build and Deploy to Cloudflare Pages / deploy (push) Successful in 21s
check / check (push) Successful in 10s
2026-08-09 19:08:55 +02:00
clawbot
5f998c6e70 Add a Cloudflare Pages _headers file with security headers (closes #14)
All checks were successful
check / check (push) Successful in 9s
Hugo copies static/ verbatim into public/, so static/_headers lands at
the deploy output root, which is where Pages reads it from. This is the
first root-level static/ in the repo; Hugo unions it with the theme's
static/ per path rather than shadowing it, and the built tree confirms
that: public/css/style.css and public/index.html are byte-identical to
the previous build and the static file count goes from 1 to 2.

The live "before" was measured rather than assumed. Cloudflare already
sends X-Content-Type-Options and Referrer-Policy by default, so those
two lines are restatements; the substance is Strict-Transport-Security,
Content-Security-Policy, X-Frame-Options and Permissions-Policy, none of
which the site sends today.

Every value is checked against the built page, which loads nothing: no
script, img, link, iframe, form or media element, no style= and no on*=
attribute. It has exactly one inline <style> block, filled by readFile
in baseof.html. So default-src 'none' with style-src 'unsafe-inline' is
both achievable and tight, and 'unsafe-inline' is required by, and only
by, that deliberate inlining. There is no script-src allowance because
there are no scripts. X-Frame-Options: DENY and frame-ancestors 'none'
agree.

HSTS carries neither preload nor includeSubDomains. www.lora.vegas is
the only other name in DNS and it is served by this same Pages project,
so this file sets HSTS on its responses directly; includeSubDomains
would instead bind every future subdomain for a year, with no way to
walk it back inside the max-age window without also dropping the apex
protection.

Verified in a headless Chrome against a local server that parses the
committed _headers and applies it as real response headers: zero CSP
violations, the inlined stylesheet parses to 17 rules with the computed
body padding, tagline colour and link colour all coming from the theme
CSS, framing from another origin refused by frame-ancestors, and all
five named outbound links still navigating with status 200.

Whether Pages actually parses the file cannot be verified from here.
Pages silently ignores a malformed _headers, so the green build proves
nothing about it; that check belongs after the next deploy and must be
made on Strict-Transport-Security or Content-Security-Policy, since
X-Content-Type-Options would pass either way. It has to be run against
both lora.vegas and www.lora.vegas: dropping includeSubDomains rests on
www being served by this same Pages project, which was established from
identical response bodies rather than from the Cloudflare dashboard.
2026-08-09 17:01:59 +00:00
821a293391 Merge pull request '#35: restructure README into canonical sections (closes #11)'
All checks were successful
check / check (push) Successful in 35s
Build and Deploy to Cloudflare Pages / build (push) Successful in 1m24s
Build and Deploy to Cloudflare Pages / deploy (push) Successful in 29s
2026-08-09 18:40:49 +02:00
9bfc37bb76 Restructure README.md into the canonical section set (closes #11)
All checks were successful
check / check (push) Successful in 9s
REPO_POLICIES.md mandates a fixed set of README sections; this README
predated the standard being applied here and had About / Contributing /
Technical Details / Entrypoints / License instead. It is now a
Description first line followed by Getting Started, Entrypoints,
Rationale, Design, TODO, License, Author, with Author last.

Nothing the old headings held was dropped: the list of what the site
publishes moved under the Description, and the contribute contact and
the local-preview instructions moved into Getting Started.

Getting Started was written against the current Makefile rather than
carried over from the old prose, which had drifted. There is no
`make build` target, so the old "Build: `hugo`" instruction is now
`make test`; "Local Development: `hugo server`" is now `make setup`
then `make serve`, and `make setup` is what makes a fresh clone
buildable at all since it installs the pinned Hugo.

Two stale claims are fixed. The site is deployed by Gitea Actions to
Cloudflare Pages, not "automatically via GitHub Actions". And the
Entrypoints bullet for `script/fmt` still described the
top-level-markdown-only scope that #12 replaced with `'**/*.md'` and
`'**/*.css'`; the rest of that section was verified accurate against the
scripts, including the `script/check` order and the `CHECK_EPOCH` guard
that makes a bare `docker build .` fail closed.

The License section body is deliberately untouched and no LICENSE file
is added: that is #10's, which is blocked on the owner's choice of
license. For the same reason the Description sentence omits the license
clause the policy asks for; #10 completes both.

The Design section's claims were checked against the tree rather than
assumed: the vendored theme, the `readFile` inline of style.css in
baseof.html, the `hugo --minify` output to `public/`, and the deploy
workflow.
2026-08-09 16:34:16 +00:00
0070fdb589 Merge pull request '#32: widen prettier scope to CSS and all Markdown (closes #12)'
All checks were successful
check / check (push) Successful in 12s
Build and Deploy to Cloudflare Pages / build (push) Successful in 49s
Build and Deploy to Cloudflare Pages / deploy (push) Successful in 21s
2026-08-09 18:30:35 +02:00
3e0694e6e0 Widen the prettier gate to markdown and CSS everywhere (closes #12)
All checks were successful
check / check (push) Successful in 59s
REPO_POLICIES.md scopes prettier to JS/CSS/Markdown/HTML, but script/fmt
and script/fmt-check covered only '*.md' - top-level markdown. The
canonical scripts use '**/*.md'. Both now run over '**/*.md' and
'**/*.css', and both header comments, which still described the old
top-level-only scope, were rewritten.

That brings themes/loravega/static/css/style.css into the gate. It is
inlined into every page by baseof.html via readFile, and its formatting
is whitespace-only: the minified <style> block in the built
public/index.html is byte-identical across the reformat, which is the
preceding commit.

Two paths are excluded, each with the reason recorded in
.prettierignore so the exclusion reads as a decision rather than an
oversight:

themes/loravega/layouts/ - these are not HTML. They are Go templates
carrying {{ define }}, {{ block }}, {{ .Content }} and
{{ readFile ... | safeCSS }}, and prettier has no Go-template parser; it
would fail or reflow the delimiters into markup Hugo cannot parse.
Covering them needs an out-of-tree plugin and therefore a package.json,
which this repo deliberately does not have.

content/ - excluded on measurement, not on the earlier assumption. With
content/ in scope, prettier re-wrapped one list item in
content/_index.md, and the rendered public/index.html changed with it:
the wrap landed as a literal newline between "7 PM at" and the following
<a> tag. HTML collapses that newline to a space, so the page looks the
same, but the published bytes do not match, and this content carries raw
div/span/br blocks that goldmark passes through verbatim because
hugo.toml sets markup.goldmark.renderer.unsafe = true. A formatter that
can silently change a published page is not worth the consistency.
archetypes/ stays covered - it is a template for new content, not
published output, and prettier leaves it unchanged.

Verified by extracting public/ from the built image before and after.
With the final scope, index.html, index.xml and sitemap.xml are
byte-identical; only the verbatim-copied public/css/style.css differs,
in whitespace. Each commit on this branch passes make check on its own -
the reformat lands first, under the old narrow glob that does not look
at CSS, so the widening commit arrives on an already-clean tree and no
merge commit is required to land it.
2026-08-09 16:19:54 +00:00
f3176a1121 Reformat style.css with the repo prettier settings
Pure formatting churn, no functional change. Kept as its own commit
ahead of the script change so the widened gate lands on an already-clean
tree, per REPO_POLICIES.md: formatting diffs are large and must not be
mixed with functional changes.

The file is inlined verbatim into every page by baseof.html via
readFile, so this was checked rather than assumed: the minified <style>
block in the built public/index.html is byte-identical before and after.
Prettier CSS formatting is whitespace-only and cannot reach the rendered
page.
2026-08-09 16:19:15 +00:00
7dea8373d3 Merge pull request '#31: add canonical policy dotfiles (closes #8)'
All checks were successful
check / check (push) Successful in 9s
Build and Deploy to Cloudflare Pages / build (push) Successful in 46s
Build and Deploy to Cloudflare Pages / deploy (push) Successful in 18s
2026-08-09 18:12:07 +02:00
14 changed files with 648 additions and 141 deletions

View File

@@ -1,2 +1,25 @@
node_modules/ node_modules/
yarn.lock yarn.lock
# Site content. Measured, not assumed: formatting content/_index.md
# re-wrapped one list item, and the rendered public/index.html changed
# with it - the wrap landed as a literal newline between "7 PM at" and
# the following <a> tag. HTML collapses that newline to a space, so the
# page looks the same, but the published bytes are not the same, and
# this content carries raw HTML blocks (div/span/br) that goldmark
# passes through verbatim because hugo.toml sets
# markup.goldmark.renderer.unsafe = true. A formatter that can silently
# change a published page is not worth the consistency, so content/ is
# formatted by hand.
content/
# Hugo layout templates. REPO_POLICIES.md puts HTML in prettier's
# scope, but these files are not HTML: they are Go templates carrying
# {{ define }}, {{ block }}, {{ .Content }} and
# {{ readFile ... | safeCSS }}. Prettier has no Go-template parser, so
# it would either fail outright or reflow the delimiters into markup
# Hugo can no longer parse. Formatting them would require an
# out-of-tree prettier plugin, which in turn requires a package.json -
# deliberately out of scope for this repo. Excluded on purpose, not by
# oversight.
themes/loravega/layouts/

View File

@@ -1,8 +1,16 @@
# Hugo static-site build image. The build runs `make check` (a clean # Hugo static-site build image. The build runs the individual non-lint
# `hugo --minify` production build, the `--printPathWarnings` lint # checks -- script/test, a clean `hugo --minify` production build, and
# build, then the read-only prettier docs check), so the image build # script/fmt-check, the read-only prettier check -- so the image build
# fails on any formatting or Hugo build error. This is what CI # fails on any template, content, config or formatting error.
# (script/cibuild) runs on every push. #
# It deliberately does NOT run `make check`, and only the lint is
# missing from what it does run. `make check` calls script/lint, and
# script/lint is a `docker build` of Dockerfile.lint, so `RUN make
# check` here would attempt a docker build inside a build step, in a
# bare alpine with no docker client and no daemon socket. Putting
# `make check` (or a `make lint`) back reintroduces exactly that
# recursion. The lint is not skipped: script/cibuild runs script/lint
# first, in its own container, before this build starts.
# #
# Build this only via script/cibuild or script/docker: both pass the # Build this only via script/cibuild or script/docker: both pass the
# CHECK_EPOCH build argument that this file requires, and a bare # CHECK_EPOCH build argument that this file requires, and a bare
@@ -41,5 +49,8 @@ COPY . .
ARG CHECK_EPOCH ARG CHECK_EPOCH
RUN [ -n "$CHECK_EPOCH" ] || exit 1 RUN [ -n "$CHECK_EPOCH" ] || exit 1
# Run all checks - build fails if any check fails. # The individual non-lint checks - build fails if either fails. Invoked
RUN echo "check epoch: ${CHECK_EPOCH}" && make check # as script/ entrypoints rather than `make check` for the reason in the
# header comment above.
RUN echo "check epoch: ${CHECK_EPOCH}" && script/test
RUN script/fmt-check

61
Dockerfile.lint Normal file
View File

@@ -0,0 +1,61 @@
# Lint-only image. `script/lint` builds this file and nothing else: the
# lint runs as a build step, so a successful build IS a clean lint.
#
# One stage, deliberately. A whole-file `docker build -f Dockerfile.lint .`
# builds only the file's LAST stage, and sibling stages off a shared base
# have no ordering edge between them, so a second stage sitting beside
# this one would be silently skipped by exactly the invocation the
# canonical org-wide `script/lint` uses -- a green that linted nothing,
# which is the failure mode this file exists to prevent. With a single
# stage there is nothing to skip and `script/lint` needs no `--target`.
# If a second check is ever added here it must be chained (`FROM lint AS
# ...`) or carry an explicit ordering edge, never left as a sibling.
#
# Only linting is containerised (owner ruling, 2026-08-10: "fmt and fmt
# check arent docker, just linting"). script/fmt and script/fmt-check run
# on the host, and the main Dockerfile runs the production build and the
# format check directly -- see the comment there.
#
# The lint is invoked directly below rather than through `make lint` or
# `script/lint`. That is not a style choice: `script/lint` IS this build,
# so calling it from inside would recurse into a docker build with no
# daemon.
#
# This repo's lint is a clean Hugo build that surfaces broken internal
# links and template path problems: `hugo` fails on build errors and
# --printPathWarnings reports render-target collisions.
# alpine 3.21, 2026-02-28
FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
WORKDIR /src
# Keep these four instructions byte-identical to the main Dockerfile's,
# in the same order: Docker keys layers on the instruction chain, not on
# the file they live in, so an identical prefix means this build is a
# cache hit against the main image's layers. script/bootstrap compiles
# the pinned Hugo from source, which is by far the most expensive step
# here, and it must not be paid twice. The dependency layer is also
# deliberately above the ARG below, so it stays cached and only the lint
# step re-runs on every invocation.
COPY script/ script/
RUN script/bootstrap
COPY . .
# CHECK_EPOCH is a per-invocation nonce supplied by script/lint. Without
# it an unchanged tree serves the lint layer from cache: the lint never
# executes and the build still exits 0, which is precisely the false
# green this repo already fixed once in the main Dockerfile. Caching is
# explicitly waived for lint, so the value is expanded into the linted
# command as well as the guard -- two independent value-keyed
# invalidation points, so a cache miss never depends on BuildKit's
# treatment of an unreferenced ARG, and the epoch is visible in the build
# log. Declared with no default: a default is a constant, and a constant
# is a stable cache key. The guard makes a bare
# `docker build -f Dockerfile.lint .` fail loudly instead of silently
# reusing the empty (and therefore stable) cache key. Keep both
# references.
ARG CHECK_EPOCH
RUN [ -n "$CHECK_EPOCH" ] || exit 1
RUN echo "lint epoch: ${CHECK_EPOCH}" && hugo --minify --printPathWarnings

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Jeffrey Paul <sneak@sneak.berlin>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,4 +1,4 @@
.PHONY: bootstrap setup test lint fmt fmt-check check docker hooks serve .PHONY: bootstrap setup test lint fmt fmt-check check docker cibuild precommit hooks serve
bootstrap: bootstrap:
@script/bootstrap @script/bootstrap
@@ -24,6 +24,12 @@ check:
docker: docker:
@script/docker @script/docker
cibuild:
@script/cibuild
precommit:
@script/precommit
hooks: hooks:
@script/install-precommit @script/install-precommit

152
README.md
View File

@@ -1,43 +1,55 @@
# lora.vegas # lora.vegas
Las Vegas Meshtastic and LoRa community website. `lora.vegas` is the website of the Las Vegas Meshtastic and LoRa community: an
MIT-licensed single-page static site, built with Hugo, by
[@sneak](https://sneak.berlin).
## About It publishes what the local mesh needs in one linkable place:
This site provides information about the Las Vegas mesh networking community,
including:
- Mesh channel configurations - Mesh channel configurations
- Community coordination (Discord, Signal) - Community coordination links (Discord, Signal)
- Meetup information - Meetup information
- Local resources - Local resources
## Contributing ## Getting Started
From a fresh clone, `make setup` installs every build dependency (git, make, go,
the pinned Hugo, node/npm) and the git pre-commit hook, and `make serve` starts
the Hugo development server:
```bash
git clone git@git.eeqj.de:sneak/lora.vegas.git
cd lora.vegas
make setup
make serve
```
Then open <http://localhost:1313> to preview the site.
To produce the production build, which writes the rendered site to `public/`:
```bash
make test
```
Before committing, run the full check suite — the production build, the lint
build, and the formatting check:
```bash
make check
```
The lint runs inside Docker, so `make check` needs a working Docker daemon;
there is no host fallback. On a machine that has never built the image, the
first run compiles the pinned Hugo from source, which takes minutes; later runs
reuse that cached layer.
`make fmt` rewrites the repo's markdown and CSS to the project's prettier
settings; run it if `make check` fails on formatting.
To contribute to this site, contact **sneak@sneak.berlin** for git repository To contribute to this site, contact **sneak@sneak.berlin** for git repository
access. access.
## Technical Details
This is a static site built with Hugo. The site is deployed automatically via
GitHub Actions.
### Local Development
```bash
hugo server
```
Visit http://localhost:1313 to preview.
### Build
```bash
hugo
```
Output will be in the `public/` directory.
## Entrypoints ## Entrypoints
This repository adheres to the This repository adheres to the
@@ -54,24 +66,88 @@ provide:
git pre-commit hook git pre-commit hook
- `script/test` — the correctness check: a clean `hugo --minify` production - `script/test` — the correctness check: a clean `hugo --minify` production
build build
- `script/lint` — a clean build that surfaces broken links and path collisions - `script/lint` — a clean build that surfaces broken links and path collisions,
- `script/fmt` — format the repo's own top-level markdown docs with prettier run inside Docker: it builds `Dockerfile.lint`, where the lint is a build
step, so a successful build is a clean lint
- `script/fmt` — format every markdown and CSS file in the repo with prettier;
the exclusions live in `.prettierignore` with the reason for each
- `script/fmt-check` — check that formatting (read-only) - `script/fmt-check` — check that formatting (read-only)
- `script/check` — run `script/test`, `script/lint`, then `script/fmt-check`; - `script/check` — run `script/test`, `script/lint`, then `script/fmt-check`;
modifies no tracked files modifies no tracked files
- `script/docker` — build the Docker image tagged with the project name - `script/docker` — build the Docker image tagged with the project name
- `script/cibuild` — the CI build; the Dockerfile runs `make check` - `script/cibuild` — the CI build: `script/lint` first, for fail-fast feedback,
then the main image, which runs the non-lint checks
- `script/install-precommit` — install the git pre-commit hook that runs - `script/install-precommit` — install the git pre-commit hook that runs
`script/check` `script/precommit`
Build the image through `script/cibuild` or `script/docker` only. Both pass a Each of those has a Makefile shim of the same name — `make bootstrap`,
per-invocation `CHECK_EPOCH` build argument that the Dockerfile requires, so the `make setup`, `make test`, `make lint`, `make fmt`, `make fmt-check`,
`make check` layer can never be served from cache — without it Docker returns a `make check`, `make docker`, `make cibuild` — with one exception:
green it did not earn. A bare `docker build .` fails closed on the Dockerfile's `script/install-precommit` is `make hooks`. `script/precommit`, which is what
`CHECK_EPOCH` guard rather than caching its way to a false success. the installed hook runs, is `make precommit`. Prefer the make targets; the
`Makefile` lists the operations you are expected to run.
`make cibuild` is the slowest target: it is the only one that runs two container
builds, the lint image first and then the main image. The two share the
`script/bootstrap` layer byte-for-byte, so the pinned-Hugo compile described
above is paid once per machine rather than twice, and once that layer is cached
a full `make cibuild` takes seconds. That is the cost of the CI build, not a
sign of a problem.
Every lint run for this repo happens inside a container, and only the lint does.
`script/lint` has no host path and no "already inside a container?" branch, so
what a developer runs and what CI runs are the same build. `script/fmt` and
`script/fmt-check` run on the host: a formatting check is not a lint.
That is also why the main `Dockerfile` runs `script/test` and `script/fmt-check`
rather than `make check`. `make check` calls `script/lint`, which is itself a
`docker build`, so a `make check` inside an image would attempt a docker build
in a bare Alpine with no docker client and no daemon socket. The lint is not
skipped — `script/cibuild` runs it first, in its own container, before the main
image build starts.
Build any image through `script/cibuild`, `script/docker` or `script/lint` only.
All three pass a per-invocation `CHECK_EPOCH` build argument that the
Dockerfiles require, so a check layer can never be served from cache — without
it Docker returns a green it did not earn. A bare `docker build` fails closed on
the `CHECK_EPOCH` guard rather than caching its way to a false success.
A convenience `make serve` target runs `hugo server` for local preview. A convenience `make serve` target runs `hugo server` for local preview.
## Rationale
The Las Vegas Meshtastic and LoRa community needs one durable, linkable place
for its channel configurations and group links. Those details otherwise live
inside a Discord or Signal thread, where they scroll away, cannot be linked to
from outside, are invisible to anyone who has not already joined, and quietly go
stale. A static site at a stable domain is the opposite of that: one URL to hand
to a newcomer, and one place to correct when a channel changes.
## Design
The site is a single page. All of its content is one Hugo content file,
`content/_index.md`, rendered by a minimal theme vendored in-repo at
`themes/loravega/` — there is no upstream theme dependency and no submodule.
The theme's `layouts/_default/baseof.html` inlines
`themes/loravega/static/css/style.css` into a `<style>` block with Hugo's
`readFile`, so the whole site ships as a single HTML document with no external
CSS request and no second round trip.
`hugo --minify` builds the site into `public/`. Deployment is automatic: on push
to `main`, the Gitea Actions workflow `.gitea/workflows/deploy.yml` builds the
site and publishes `public/` to Cloudflare Pages.
## TODO
The live task list is in [TODO.md](TODO.md).
## License ## License
Content is provided as-is for community use. MIT. See [LICENSE](LICENSE). This covers everything in the repository — the Hugo
configuration, the `script/` entrypoints, the vendored `themes/loravega/`
templates and CSS, and the site content in `content/`.
## Author
[@sneak](https://sneak.berlin)

233
TODO.md
View File

@@ -13,21 +13,194 @@
pre-1.0 pre-1.0
No git tags. The site is live and now has the scripts-to-rule-them-all scaffold No git tags. The site is live and now has the scripts-to-rule-them-all scaffold
(`Makefile`, `script/`, `Dockerfile`, `check.yml`) and the canonical policy (`Makefile`, `script/`, `Dockerfile`, `check.yml`), the canonical policy
dotfiles; `LICENSE` is the only mandated file still missing. Every external dotfiles and `LICENSE`, so the mandated minimum file list is complete. Every
reference in the repo is now pinned by cryptographic hash (or, for the wrangler external reference in the repo is now pinned by cryptographic hash (or, for the
CLI install, an exact version), and the Hugo that builds the published site is a wrangler CLI install, an exact version), and the Hugo that builds the published
deliberate pinned version rather than whatever the base image's package repo site is a deliberate pinned version rather than whatever the base image's
serves. package repo serves. The site now ships a Cloudflare Pages `_headers` file, so
its response security headers are declared in the repo instead of being whatever
the edge defaults to, and confirmed live in production on both hostnames. The
lint now runs inside a container and nowhere else: `script/lint` is a build of
`Dockerfile.lint`, with no host path to fall back to. Formatting is not a lint
and stays on the host. Every entrypoint the README documents now has a
`Makefile` target, so the org-wide "use make targets, never the underlying tool"
rule is satisfiable for the CI build as well as for the everyday checks.
# Next Step # Next Step
Add `LICENSE` (#10) and replace the README's "content is provided as-is" note Make the prettier scope's exclusion of dot-directories explicit instead of
with the committed license. Blocked on the owner's choice of license — the leaning on `.gitignore` (https://git.eeqj.de/sneak/lora.vegas/issues/33).
remaining policy scaffold is otherwise complete.
# Completed Steps # Completed Steps
- 2026-08-10: added the `cibuild` and `precommit` targets to the `Makefile`
(https://git.eeqj.de/sneak/lora.vegas/issues/34). Both scripts already
existed, were already documented, and are the two entrypoints a contributor is
most likely to be told to run — the CI build and what the pre-commit hook runs
— yet neither had a make target, so the standing rule to use make targets
rather than the underlying tool could not be followed for either. It bit
reviewers twice, most recently on
https://git.eeqj.de/sneak/lora.vegas/pulls/32, and it bit hardest for the
build: since https://git.eeqj.de/sneak/lora.vegas/issues/30 a bare
`docker build .` fails closed on the `CHECK_EPOCH` guard, so `script/cibuild`
is one of only three supported ways to build an image and was the only one
without a target. The two targets are thin shims in the existing style and
change nothing about what the scripts do. `.PHONY` was already complete and
now lists both. `README.md`'s Entrypoints section gained the script-to-target
mapping, including the two names that do not match —
`script/install-precommit` is `make hooks`, and `script/precommit` is
`make precommit` — plus a note that `make cibuild` is the slowest target
because it is the only one that runs two container builds, while still taking
seconds once the shared `script/bootstrap` layer is cached. The section's
pre-existing `script/install-precommit` bullet, which claimed the installed
hook runs `script/check`, now says `script/precommit`, which is what the
script actually writes into `.git/hooks/pre-commit`. The stale Future Step
asking for post-deploy confirmation of `static/_headers` is dropped here: it
was confirmed live on both hostnames
(https://git.eeqj.de/sneak/lora.vegas/issues/14). Verified that `make cibuild`
earns its green rather than replaying a warm cache: one invocation ran both
builds for real in sequence, each with its own distinct `CHECK_EPOCH`, with
`RUN script/bootstrap` `CACHED` above and no check layer cached below it — the
`Dockerfile.lint` build echoed its epoch and printed hugo's own build table,
then the main image build echoed a different epoch, ran `script/test` for the
production build and `script/fmt-check` for the formatting check.
`make precommit` passes on a clean tree, and `make check` passes
- 2026-08-10: moved the lint into Docker
(https://git.eeqj.de/sneak/lora.vegas/issues/38). A new root `Dockerfile.lint`
runs `hugo --minify --printPathWarnings` as a build step, so a successful
build is a clean lint, and `script/lint` is nothing but a build of that file —
no host path and deliberately no "already inside a container?" branch, which
would be a host lint path in disguise. The containerisation boundary is lint
only, per the owner ruling of the same day: formatting is not a lint, so
`script/fmt` and `script/fmt-check` stay on the host. `Dockerfile.lint` has
exactly one stage on purpose. A whole-file `docker build -f Dockerfile.lint .`
builds only the file's last stage, and sibling stages off a shared base have
no ordering edge, so any second stage beside the lint would be silently
skipped by the invocation the canonical org-wide `script/lint` uses — a green
that linted nothing. With one stage there is nothing to skip and `script/lint`
needs no `--target`. Its first four instructions are byte-identical to the
main `Dockerfile`'s, so the expensive `RUN script/bootstrap` layer that
compiles Hugo from source is shared between the two images rather than paid
twice. The recursion this creates was resolved by direction, not detection:
`make check` calls `script/lint`, so the main `Dockerfile` can no longer
`RUN make check` — that would attempt a docker build inside a build step, in a
bare Alpine with no docker client and no daemon socket. It runs the individual
non-lint checks instead, `script/test` and `script/fmt-check`, matching the
canonical shape upstream, and `script/cibuild` runs `script/lint` first for
fail-fast feedback before the main image build starts. So CI still covers all
three checks and cannot drift from what a developer runs. Caching is waived
for the lint exactly as the main `Dockerfile` already does it:
`ARG CHECK_EPOCH` with no default, guarded with
`[ -n "$CHECK_EPOCH" ] || exit 1`, and the value expanded into the linted
command as well as the guard, so invalidation never rests on BuildKit's
handling of an unreferenced `ARG`. Every image-building entrypoint generates
and passes it — `script/cibuild`, `script/docker`, `script/lint` — which is
the failure mode this repo already hit once, a Dockerfile guard asserting a
property one entrypoint did not supply. `script/lint` builds with
`--output type=cacheonly`: the build is run for its exit status, not for an
image, and since the lint layer is cache-busted every run an exporting build
would leave one dangling image per lint on a host shared with other work.
Verified rather than assumed: two consecutive `script/lint` runs on an
unchanged tree both executed hugo for real (second run 0.85s wall,
`RUN script/bootstrap` `CACHED`, distinct epochs echoed, real build tables
printed); a whole-file `docker build -f Dockerfile.lint .` with the argument
and no `--target` ran the lint for real, which is the regression test for the
skipped-sibling hazard; a bare build with no argument failed closed on the
guard; a planted template error failed the lint with hugo's own render error
and made `script/cibuild` exit in 0.6s without the main image build starting
at all; a planted over-long line failed the host `script/fmt-check` with
`[warn] README.md`; both violations were reverted and re-run clean. Not
changed here, and still true: the lint fails on hugo build errors but not on
render-target collisions, which `--printPathWarnings` only prints
(https://git.eeqj.de/sneak/lora.vegas/issues/25) — containerising the run
neither fixes nor worsens that
- 2026-08-10: added the `LICENSE` file and made the README say what it says
(closes #10). The repo is public (`private: false` on the Gitea API, verified
rather than assumed), so the owner's standing policy — MIT on any public repo
lacking a license — applies. `LICENSE` is byte-identical to the canonical
`sneak/homoicon` copy, confirmed by git blob hash rather than by eye
(`3274443`), and its body is word-for-word the SPDX MIT text with only the
line wrapping differing. The README's "Content is provided as-is for community
use." — which granted nothing and matched no committed file — is replaced by
`MIT. See [LICENSE](LICENSE).` plus an explicit statement that the licence
covers the content in `content/` as well as the code, since this repo carries
both and MIT names only "the Software". The Description first line now carries
the licence, which `REPO_POLICIES.md` requires and which was the one field it
was missing. Nothing published contradicts the choice: the built `public/`
tree carries no copyright, all-rights-reserved or terms-of-use string
anywhere, in `index.html`, `css/style.css`, `index.xml` or `sitemap.xml` — the
footer `baseof.html` renders names `@sneak` and links the repo but asserts no
reservation of rights, and the content is factual mesh channel data with no
licence claim of its own. The fmt gate cannot reach `LICENSE` and needed no
`.prettierignore` entry: `script/fmt` passes prettier the explicit globs
`'**/*.md'` and `'**/*.css'`, and an extensionless root file matches neither.
Measured, not assumed — a `script/fmt` run leaves the file's hash unchanged,
and a counterfactual `LICENSE.md` copy was reflowed by the same run, which is
the direct evidence that it is the extension and not an ignore rule doing the
excluding. Deliberately not done, per the issue: per-file licence headers and
SPDX identifiers, which no org standard mandates
- 2026-08-09: added `static/_headers` so Cloudflare Pages serves baseline
response security headers (closes #14). Hugo copies `static/` verbatim into
`public/`, which is the deploy root Pages reads the file from; this is the
first root-level `static/` in the repo, and the built tree confirms it unions
with the theme's rather than shadowing it — `public/css/style.css` and
`public/index.html` are byte-identical to the previous build and the static
file count goes 1 to 2. The live "before" was measured, not assumed:
Cloudflare already sends `X-Content-Type-Options` and `Referrer-Policy` by
default, so the substance here is `Strict-Transport-Security`,
`Content-Security-Policy`, `X-Frame-Options` and `Permissions-Policy`. The CSP
is `default-src 'none'` with `style-src 'unsafe-inline'`, which the built page
supports exactly: it has no script, img, link, iframe, form or media element
and no `style=`/`on*=` attribute, only the one inline `<style>` block
`baseof.html` fills by `readFile`. Verified in a headless Chrome against a
local server that parses the committed `_headers` and applies it as real
response headers: zero CSP violations, the inlined stylesheet parses to 17
rules and the computed body padding, tagline colour and link colour all come
from the theme CSS, framing the page from another origin is refused by
`frame-ancestors 'none'` (consistent with `X-Frame-Options: DENY`), and all
five named outbound links still navigate with status 200. HSTS carries neither
`preload` nor `includeSubDomains`: `www.lora.vegas` is the only other name in
DNS and it is served by this same Pages project, so this file sets HSTS on its
responses directly, and `includeSubDomains` would instead bind every future
subdomain for a year with no way to walk it back inside the max-age window
without also dropping the apex protection.
- 2026-08-09: restructured `README.md` into the canonical section set (closes
#11): a Description first line, then Getting Started, Entrypoints, Rationale,
Design, TODO, License, Author. The non-standard About / Contributing /
Technical Details headings are gone, but nothing they held was dropped — the
bullet list of what the site publishes moved under the Description, the
contribute contact and the local-preview instructions moved into Getting
Started. Getting Started was written against the current `Makefile` rather
than the old prose: there is no `make build` target, so the former "Build:
`hugo`" instruction is now `make test`, and the former "Local Development:
`hugo server`" is `make setup` then `make serve`. Two stale claims fixed: the
site is deployed by Gitea Actions to Cloudflare Pages, not "GitHub Actions",
and the Entrypoints bullet for `script/fmt` still described the
top-level-markdown-only scope that #12 replaced with `'**/*.md'` and
`'**/*.css'`. The License section body is deliberately untouched — it is owned
by #10, which is blocked on the owner's choice of license, and the Description
sentence is likewise missing the license clause the policy calls for until #10
lands. Design section claims were verified against the tree, not assumed
- 2026-08-09: widened the prettier gate from top-level markdown to `'**/*.md'`
and `'**/*.css'` (closes #12). `themes/loravega/static/css/style.css` was
never formatted or gated even though it is inlined into every page; it is now
both, and the reformat landed as its own commit ahead of the script change so
no commit in the branch is red. `content/` is excluded in `.prettierignore`,
and that exclusion is measured rather than assumed: with `content/` in scope,
prettier re-wrapped one list item in `content/_index.md` and the rendered
`public/index.html` changed with it (the wrap became a literal newline between
`7 PM at` and the following `<a>`). HTML collapses that newline to a space so
the page looks identical, but the published bytes are not, and this content
carries raw HTML that goldmark passes through verbatim under `unsafe = true`.
`themes/loravega/layouts/` is excluded too, with the reason recorded: those
files are Go templates, not HTML, and prettier has no parser for `{{ ... }}`
covering them would need a plugin and therefore a `package.json`. Verified by
extracting `public/` from the built image before and after: with the final
scope, `index.html`, `index.xml` and `sitemap.xml` are byte-identical and only
the verbatim-copied `public/css/style.css` changes, in whitespace only — the
minified `<style>` block inlined into `index.html` is unchanged, which is the
direct evidence that CSS formatting cannot reach the rendered page
- 2026-08-09: added the canonical policy dotfiles and hardened both ignore files - 2026-08-09: added the canonical policy dotfiles and hardened both ignore files
(closes #8). `REPO_POLICIES.md` is a byte-identical copy of the canonical (closes #8). `REPO_POLICIES.md` is a byte-identical copy of the canonical
`prompts` file, front matter intact; `.editorconfig`, `.prettierrc` and `prompts` file, front matter intact; `.editorconfig`, `.prettierrc` and
@@ -137,16 +310,40 @@ remaining policy scaffold is otherwise complete.
# Future Steps # Future Steps
- Move the artifact actions to v4 once this Gitea Actions instance serves the v4 Startable work first. Everything under "Blocked" waits on somebody or something
artifact protocol; they are pinned on the deprecated v3 line because v4 fails outside this repo, so nothing there may be picked up as the Next Step.
here (#20)
- Move the deploy container to a pinned node 22 so the wrangler pin can advance - Fix the README's SSH-only clone URL, and add the two entrypoints the
past 4.86.0 (#21) Entrypoints section omits, `script/precommit` and `script/projectname`
- Rework README.md into the standard sections: Description, Getting Started, (https://git.eeqj.de/sneak/lora.vegas/issues/36)
Rationale, Design, TODO, License, Author (currently About, Contributing, - Drop the Go toolchain and module cache from the check image's final layer;
Technical Details, License) they are needed to build hugo and dead weight afterwards
(https://git.eeqj.de/sneak/lora.vegas/issues/28)
- Add a timeout guard to `script/test` and `script/lint` so a wedged build fails
instead of hanging (https://git.eeqj.de/sneak/lora.vegas/issues/16)
- Sync the reformat of `REPO_POLICIES.md` back upstream to `prompts` so the - Sync the reformat of `REPO_POLICIES.md` back upstream to `prompts` so the
canonical copy is clean under the shared prettier settings and future syncs canonical copy is clean under the shared prettier settings and future syncs
are a straight byte copy are a straight byte copy
- Verify the Cloudflare Pages deploy still works after the workflow changes
- Keep mesh channel and signal group listings current - Keep mesh channel and signal group listings current
## Blocked
- Decide whether `script/lint` should fail on render-target collisions rather
than only print them; `--printPathWarnings` exits 0 today, so the signal is
reported and not enforced. Owner call, since it changes what the gate rejects
(https://git.eeqj.de/sneak/lora.vegas/issues/25)
- Move the artifact actions in `.gitea/workflows/deploy.yml` to v4 once this
Gitea Actions instance serves the v4 artifact protocol; they are pinned on the
deprecated v3 line because v4 fails here
(https://git.eeqj.de/sneak/lora.vegas/issues/20). This touches the live deploy
path, so it needs a real workflow run to verify rather than a local check
- Move the deploy container to a pinned node 22 so the wrangler pin can advance
past 4.86.0 (https://git.eeqj.de/sneak/lora.vegas/issues/21)
- Delete the stale remote branches `feat/initial-site` and `security-audit`;
only the owner can remove them
(https://git.eeqj.de/sneak/lora.vegas/issues/15)
- Decide the HSTS `includeSubDomains` and `preload` posture for `lora.vegas`.
Both are owner calls: neither can be walked back inside the max-age window,
and `includeSubDomains` binds hostnames this repo does not control
(https://git.eeqj.de/sneak/lora.vegas/issues/14)
- Verify the Cloudflare Pages deploy still works after the workflow changes

View File

@@ -3,6 +3,14 @@
# scripts-to-rule-them-all. Must not modify any tracked files. Runs the # scripts-to-rule-them-all. Must not modify any tracked files. Runs the
# canonical order: the clean production build, then the lint build that # canonical order: the clean production build, then the lint build that
# reports path warnings, then the read-only formatting check. # reports path warnings, then the read-only formatting check.
#
# The lint - and only the lint - runs inside Docker: it is a build of
# Dockerfile.lint, so this script needs a working docker daemon and has
# no host fallback to drop back to. Budget for the cold case: the first
# lint on a machine with no cached script/bootstrap layer compiles the
# pinned Hugo from source, which takes minutes. That cost falls on the
# pre-commit hook too, since it runs this script. Every later run reuses
# that layer and only the lint step re-executes.
set -eu set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"

View File

@@ -1,20 +1,39 @@
#!/bin/sh #!/bin/sh
# script/cibuild: run the CI build. The Dockerfile runs `make check`, # script/cibuild: run the CI build. The Gitea workflow runs this on
# so a successful build implies all checks pass. The Gitea workflow # push, and it is the single entrypoint that covers everything. Two
# runs this on push. # container builds, in order:
# #
# That implication only holds because of CHECK_EPOCH. Docker keys the # 1. script/lint, which builds Dockerfile.lint -- the lint runs as a
# `RUN make check` layer on content, so on an unchanged tree it is # build step there
# served from cache: the checks never execute and the build still exits # 2. the main Dockerfile, which runs the non-lint checks: the clean
# 0. Passing a value that differs on every invocation invalidates that # `hugo --minify` production build (script/test) and the read-only
# layer and everything below it, while the script/bootstrap toolchain # prettier check (script/fmt-check)
# layer above it keeps caching. #
# Lint goes first, for fail-fast feedback: on a runner with no cached
# script/bootstrap layer the main image compiles Hugo from source, and a
# lint failure should not wait behind that. It is a separate build
# rather than a step inside the main image because script/lint is itself
# a `docker build`, and a docker build cannot run a docker build. See
# Dockerfile.lint for the full reasoning.
#
# The lint is delegated to the same script a developer runs, so CI
# cannot drift from `make check`.
#
# Neither build implies a passing check without CHECK_EPOCH. Docker keys
# the check layers on content, so on an unchanged tree they are served
# from cache: nothing executes and the build still exits 0. Passing a
# value that differs on every invocation invalidates those layers and
# everything below them, while the script/bootstrap toolchain layer
# above keeps caching. script/lint does the same for its own build.
set -eu set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
"$SCRIPT_DIR/lint"
# Assigned to a variable rather than substituted inline in the # Assigned to a variable rather than substituted inline in the
# argument list: a command substitution that fails inside an # argument list: a command substitution that fails inside an
# argument does not trip `set -e`, so the inline form would quietly # argument does not trip `set -e`, so the inline form would quietly

View File

@@ -1,8 +1,16 @@
#!/bin/sh #!/bin/sh
# script/fmt: format the repo's own top-level markdown docs (README.md, # script/fmt: format this repo's markdown and CSS with prettier, using
# TODO.md, ...) with prettier, using our standard settings. Scope is # our standard settings. Scope is every markdown and CSS file in the
# deliberately limited to top-level docs: site content under content/ # tree, per REPO_POLICIES.md ("prettier for JS/CSS/Markdown/HTML").
# is left untouched so rendered output cannot change. # What is deliberately NOT covered is recorded in .prettierignore, with
# the reason next to each entry: the Hugo layout templates, which are
# Go templates and not HTML, and content/, whose reformatting was
# measured to change the rendered page.
#
# Both prettier entrypoints run on the host: only linting is
# containerised (owner ruling, 2026-08-10), and formatting is not a
# lint. Keep the version, scope and flags here in sync with
# script/fmt-check.
set -eu set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
@@ -12,7 +20,7 @@ PRETTIER_VERSION="3.4.2"
main() { main() {
cd "$ROOT" cd "$ROOT"
npx --yes "prettier@${PRETTIER_VERSION}" --write \ npx --yes "prettier@${PRETTIER_VERSION}" --write \
'*.md' --tab-width 4 --prose-wrap always '**/*.md' '**/*.css' --tab-width 4 --prose-wrap always
} }
main "$@" main "$@"

View File

@@ -1,7 +1,12 @@
#!/bin/sh #!/bin/sh
# script/fmt-check: check the formatting of the repo's own top-level # script/fmt-check: check the formatting of this repo's markdown and
# markdown docs (read-only). Same scope as script/fmt, but fails # CSS (read-only). Same scope and same settings as script/fmt - keep
# instead of writing. # the two in sync - but fails instead of writing.
#
# This runs on the host, not in a container: only linting is
# containerised (owner ruling, 2026-08-10), and a formatting check is
# not a lint. Running here also keeps it usable offline, since npx
# reuses ~/.npm/_npx after the first run.
set -eu set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
@@ -11,7 +16,7 @@ PRETTIER_VERSION="3.4.2"
main() { main() {
cd "$ROOT" cd "$ROOT"
npx --yes "prettier@${PRETTIER_VERSION}" --check \ npx --yes "prettier@${PRETTIER_VERSION}" --check \
'*.md' --tab-width 4 --prose-wrap always '**/*.md' '**/*.css' --tab-width 4 --prose-wrap always
} }
main "$@" main "$@"

View File

@@ -1,15 +1,40 @@
#!/bin/sh #!/bin/sh
# script/lint: this Hugo site has no dedicated linter, so the lint gate # script/lint: run the lint. This Hugo site has no dedicated linter, so
# is a clean build that surfaces broken internal links and template # the lint gate is a clean build that surfaces broken internal links and
# path problems. It is a real check: `hugo` fails on build errors, and # template path problems -- but where it runs is not negotiable: every
# --printPathWarnings reports render-target collisions. # lint run happens inside a Docker container, so this script does
# nothing except build Dockerfile.lint. The lint is a build step there,
# so a successful build is a clean lint. There is deliberately no host
# fallback and no "already inside a container?" branch: either would be
# a host lint path wearing a disguise.
#
# No --target: Dockerfile.lint has exactly one stage, so the whole-file
# build IS the lint. See that file for why a second, sibling stage would
# be a silent skip.
#
# Dockerfile.lint requires the CHECK_EPOCH build argument, generated
# here exactly as script/cibuild generates it -- see that script for why
# the lint layer must not be allowed to cache, and why the value is
# built in an assignment rather than inline.
set -eu set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
hugo --minify --printPathWarnings epoch="$(date +%s%N)$$"
# --output type=cacheonly: this build is run for its exit status,
# not for an image. Because the lint layer is cache-busted on every
# invocation the result is a new image every time, and an untagged
# build would leave one dangling image per lint run on a host shared
# with other work. cacheonly keeps the build cache (so
# script/bootstrap still hits) and exports nothing. Failures still
# propagate: an empty CHECK_EPOCH or a failing lint exits non-zero.
docker build \
--build-arg CHECK_EPOCH="$epoch" \
--output type=cacheonly \
-f Dockerfile.lint \
.
} }
main "$@" main "$@"

42
static/_headers Normal file
View File

@@ -0,0 +1,42 @@
# Cloudflare Pages response headers.
#
# Hugo copies static/ verbatim into public/, so this file lands at the
# deploy output root, which is where Pages reads it from. Pages consumes
# the file rather than publishing it. Values here override what
# Cloudflare would otherwise send.
#
# Every value below was checked against the built public/index.html, not
# copied from a template. That page loads nothing: no script, img, link,
# iframe, form, video, audio, object or embed element, no style= or on*=
# attribute. It has exactly one inline <style> block, which
# themes/loravega/layouts/_default/baseof.html fills with the whole of
# themes/loravega/static/css/style.css via readFile. That inlining is a
# deliberate theme design choice, and it is the sole reason style-src
# needs 'unsafe-inline'.
#
# img-src 'self' is kept even though the page has no images. Browsers
# request /favicon.ico unprompted and that fetch is governed by img-src;
# measured in Chrome, with this allowance the request is made and 404s,
# and without it the request is suppressed outright. Neither hurts
# today, but same-origin images are the one resource class this site
# would plausibly grow, and 'self' loosens nothing cross-origin.
#
# X-Content-Type-Options and Referrer-Policy are already sent by
# Cloudflare by default and are restated here on purpose. They are a
# default, not a guarantee, and this file is where the site's header
# posture is declared.
#
# Strict-Transport-Security deliberately carries neither preload nor
# includeSubDomains. preload is effectively irreversible and is the
# owner's call. includeSubDomains would bind every hostname under
# lora.vegas for a year, and it buys nothing today: www.lora.vegas is
# the only other name in DNS, it is served by this same Pages project,
# so this block sets HSTS on its responses directly.
/*
Strict-Transport-Security: max-age=31536000
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
X-Frame-Options: DENY
Permissions-Policy: geolocation=(), microphone=(), camera=()
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src 'self'; form-action 'none'; frame-ancestors 'none'; base-uri 'none'

View File

@@ -1,101 +1,106 @@
:root { :root {
--bg: #ffffff; --bg: #ffffff;
--fg: #1a1a1a; --fg: #1a1a1a;
--accent: #0066cc; --accent: #0066cc;
--muted: #666; --muted: #666;
--max-w: 600px; --max-w: 600px;
} }
* { margin: 0; padding: 0; box-sizing: border-box; } * {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body { body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
background: var(--bg); "Helvetica Neue", Arial, sans-serif;
color: var(--fg); background: var(--bg);
line-height: 1.6; color: var(--fg);
padding: 2rem 1rem; line-height: 1.6;
max-width: 90%; padding: 2rem 1rem;
margin: 0 auto; max-width: 90%;
margin: 0 auto;
} }
h1 { h1 {
font-size: 1.8rem; font-size: 1.8rem;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
.tagline { .tagline {
color: var(--muted); color: var(--muted);
margin-bottom: 2rem; margin-bottom: 2rem;
} }
h2 { h2 {
font-size: 1.2rem; font-size: 1.2rem;
margin-top: 2rem; margin-top: 2rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0;
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
} }
h3 { h3 {
font-size: 1rem; font-size: 1rem;
margin-top: 1.5rem; margin-top: 1.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
font-weight: 600; font-weight: 600;
color: var(--muted); color: var(--muted);
} }
ul { ul {
list-style: none; list-style: none;
padding: 0; padding: 0;
} }
li { li {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
a { a {
color: var(--accent); color: var(--accent);
text-decoration: none; text-decoration: none;
} }
a:hover { a:hover {
text-decoration: underline; text-decoration: underline;
} }
.settings { .settings {
background: #f8f8f8; background: #f8f8f8;
border: 1px solid #e0e0e0; border: 1px solid #e0e0e0;
border-radius: 4px; border-radius: 4px;
padding: 1rem; padding: 1rem;
margin-top: 0.5rem; margin-top: 0.5rem;
font-family: "SF Mono", "Fira Code", "Consolas", monospace; font-family: "SF Mono", "Fira Code", "Consolas", monospace;
font-size: 0.9rem; font-size: 0.9rem;
line-height: 1.8; line-height: 1.8;
overflow-wrap: break-word; overflow-wrap: break-word;
word-break: break-all; word-break: break-all;
} }
.settings .label { .settings .label {
color: var(--muted); color: var(--muted);
} }
.settings .value { .settings .value {
color: var(--accent); color: var(--accent);
} }
.mono-link { .mono-link {
font-family: "SF Mono", "Fira Code", "Consolas", monospace; font-family: "SF Mono", "Fira Code", "Consolas", monospace;
font-size: 0.85rem; font-size: 0.85rem;
} }
footer { footer {
margin-top: 3rem; margin-top: 3rem;
color: var(--muted); color: var(--muted);
font-size: 0.8rem; font-size: 0.8rem;
border-top: 1px solid #e0e0e0; border-top: 1px solid #e0e0e0;
padding-top: 1rem; padding-top: 1rem;
} }
.contribute-link { .contribute-link {
font-size: 0.7rem; font-size: 0.7rem;
} }