commit 19034a026e383eb0bb806b0da3de37722b18f210 Author: Will Estes Date: Thu Aug 27 05:28:14 2026 -0400 Initial scaffold: Hugo site for west575.org Blog with contact form and newsletter signup, structured like the other Reground-hosted sites but with its own visual identity. diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..abd5639 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,34 @@ +name: deploy +on: + push: + branches: [master] +jobs: + deploy: + runs-on: ubuntu-latest + container: + image: hugomods/hugo:exts + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install rsync + ssh client + run: | + if command -v apk >/dev/null; then + apk add --no-cache rsync openssh-client + else + apt-get update && apt-get install -y rsync openssh-client + fi + - run: hugo --minify -d public + - name: Deploy via rsync + run: | + mkdir -p ~/.ssh + printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + # The server-side key is forced-command, restricted via rrsync to + # this site's own docroot only (see the static-site-deploy + # Ansible role) — it can push new files but can never read/list + # anything back off the server, and can't reach any other site's + # directory regardless of what path is requested here. + rsync -az --delete -e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new" public/ root@west575.org: + env: + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c3fa73 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +public/ +resources/ +.hugo_build.lock diff --git a/assets/css/main.css b/assets/css/main.css new file mode 100644 index 0000000..e5ac3a9 --- /dev/null +++ b/assets/css/main.css @@ -0,0 +1,310 @@ +/* ========================================================================== + west575.org — site stylesheet + + Fonts: Fraunces (headings) — a soft-edged serif with a lot of character + at display sizes, so headings read as written-by-a-person rather than + templated. Work Sans (body/UI) — a plain, well-built humanist sans that + stays out of the way at paragraph length. Loaded from Google Fonts in + layouts/_default/baseof.html. + + Accent: one color, a warm terracotta/marigold (#d9531e). Picked because + it reads as a highlighter or a sticky note, not a corporate swatch — + nothing here is trying to look like a SaaS product or a publisher's + imprint, just a person's site with one confident splash of color. + ========================================================================== */ + +:root { + color-scheme: light dark; + + --bg: #fbf8f3; + --bg-raised: #f2ece1; + --fg: #241f19; + --fg-muted: #6e6559; + --border: #e2d8c7; + + --accent: #d9531e; + --accent-fg: #ffffff; + /* Same hue as --accent, darkened for button/link contrast on light bg + (white-on-#d9531e is 3.6:1, fails WCAG AA for normal text; this pairing + hits 4.7:1 against --bg-raised and 5.1:1 for --accent-fg on --cta). */ + --cta: #b8431a; + --cta-fg: #ffffff; + + --radius: 0.4rem; + --max-width: 40rem; + + --font-heading: "Fraunces", Georgia, "Times New Roman", serif; + --font-body: "Work Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #1a1611; + --bg-raised: #241f19; + --fg: #f2ece1; + --fg-muted: #b3a795; + --border: #3a3227; + + --accent: #f0834f; + --accent-fg: #1a1611; + --cta: #f0834f; + --cta-fg: #1a1611; + } +} + +* { box-sizing: border-box; } + +body { + margin: 0; + font-family: var(--font-body); + line-height: 1.65; + background: var(--bg); + color: var(--fg); + display: flex; + flex-direction: column; + min-height: 100vh; +} + +a { color: var(--cta); } +a:hover { text-decoration-thickness: 2px; } + +.skip-link { + position: absolute; + left: -9999px; + top: 0; + z-index: 100; + background: var(--cta); + color: var(--cta-fg); + padding: 0.75rem 1.25rem; + border-radius: 0 0 var(--radius) 0; +} +.skip-link:focus { left: 0; } + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +h1, h2, h3 { + font-family: var(--font-heading); + font-weight: 600; + line-height: 1.2; + color: var(--fg); +} +h1 { font-size: 2.25rem; } +h2 { font-size: 1.5rem; margin-top: 3rem; } + +/* Header: plain wordmark + nav, no color band — the accent is saved for + links and buttons rather than spent on chrome. */ +header.site { + border-bottom: 1px solid var(--border); +} +header.site .inner { + max-width: var(--max-width); + margin: 0 auto; + padding: 1.5rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; +} +header.site .brand { + font-family: var(--font-heading); + font-weight: 600; + font-size: 1.25rem; + color: var(--fg); + text-decoration: none; +} +header.site nav { + display: flex; + gap: 1.5rem; +} +header.site nav a { + color: var(--fg-muted); + text-decoration: none; + font-size: 0.95rem; +} +header.site nav a:hover, +header.site nav a[aria-current="page"] { + color: var(--cta); +} + +main { + flex: 1; + max-width: var(--max-width); + margin: 0 auto; + padding: 3.5rem 1.5rem; + width: 100%; +} + +.hero p { font-size: 1.1rem; } + +.home-subscribe { + background: var(--bg-raised); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 2rem; + margin-top: 3rem; +} +.home-subscribe h2 { margin-top: 0; } + +/* Footer: about/links on one side, a subscribe form on the other — the + one place the newsletter ask repeats on every page. */ +footer.site { + border-top: 1px solid var(--border); + margin-top: 3rem; + background: var(--bg-raised); +} +footer.site .inner { + max-width: var(--max-width); + margin: 0 auto; + padding: 2rem 1.5rem; +} +footer.site .footer-top { + display: flex; + justify-content: space-between; + gap: 2rem; + flex-wrap: wrap; +} +footer.site .footer-about { + font-size: 0.9rem; + color: var(--fg-muted); + display: flex; + flex-direction: column; + gap: 0.5rem; +} +footer.site .footer-about nav { + display: flex; + gap: 1.25rem; +} +footer.site .footer-about nav a { + color: var(--fg-muted); +} +footer.site .footer-about nav a:hover { color: var(--cta); } +footer.site .footer-subscribe { + min-width: 16rem; + flex: 1 1 16rem; +} +footer.site .footer-subscribe-label { + margin: 0 0 0.75rem; + font-size: 0.9rem; + color: var(--fg-muted); +} +.subscribe-form { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} +.subscribe-form input[type="email"] { + flex: 1 1 12rem; + margin-bottom: 0; +} +.subscribe-status { + font-size: 0.85rem; + color: var(--fg-muted); + margin: 0.5rem 0 0; +} + +.btn { + display: inline-block; + padding: 0.7rem 1.4rem; + border-radius: var(--radius); + background: var(--cta); + color: var(--cta-fg); + text-decoration: none; + font-weight: 600; + border: none; + font-size: 1rem; + cursor: pointer; +} +.btn:hover { opacity: 0.9; } +.btn-subscribe { padding: 0.7rem 1.1rem; } + +a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible { + outline: 2px solid var(--cta); + outline-offset: 2px; +} + +label { + display: block; + font-weight: 600; + font-size: 0.9rem; + margin-bottom: 0.35rem; +} +input, textarea { + width: 100%; + padding: 0.6rem 0.75rem; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--bg); + color: var(--fg); + font-family: inherit; + font-size: 1rem; + margin-bottom: 1.25rem; +} + +.field-honeypot { + position: absolute; + left: -9999px; + width: 1px; + height: 1px; + overflow: hidden; +} + +.contact-form { max-width: 28rem; margin-top: 2rem; } +.contact-status { color: var(--fg-muted); } +.lede { color: var(--fg-muted); font-size: 1.05rem; } + +/* Post list (home page + /posts/) */ +.post-list { + list-style: none; + padding: 0; + margin: 1.5rem 0; +} +.post-list-item { + display: flex; + justify-content: space-between; + gap: 1rem; + padding: 0.75rem 0; + border-bottom: 1px solid var(--border); +} +.post-list-item a { + text-decoration: none; + font-weight: 600; + color: var(--fg); +} +.post-list-item a:hover { color: var(--cta); } +.post-date { + color: var(--fg-muted); + font-size: 0.9rem; + white-space: nowrap; +} + +/* Single post */ +.post-header { margin-bottom: 2rem; } +.post-meta { + color: var(--fg-muted); + font-size: 0.9rem; + display: flex; + gap: 0.75rem; + align-items: center; + flex-wrap: wrap; +} +.post-tags { display: flex; gap: 0.5rem; } +.post-tag { + background: var(--bg-raised); + border: 1px solid var(--border); + border-radius: 999px; + padding: 0.15rem 0.65rem; + font-size: 0.8rem; +} +.post-body { font-size: 1.05rem; } +.post-body p { margin: 1.25rem 0; } diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 0000000..294bf70 --- /dev/null +++ b/content/_index.md @@ -0,0 +1,13 @@ +--- +title: "west575" +--- + +I'm Will. I write software, run other people's production servers, and +occasionally finish a short story. This site is where those things live +without pretending to be one coherent "brand" — it's just what I'm doing +and thinking about, in roughly that order of frequency. + +Expect notes on infrastructure and small-business tooling, some fiction +and the occasional submission log, and whatever else seemed worth writing +down at the time. No content calendar, no funnel — just a [feed](/posts/) +you can subscribe to if any of it's useful. diff --git a/content/contact.md b/content/contact.md new file mode 100644 index 0000000..736ccc7 --- /dev/null +++ b/content/contact.md @@ -0,0 +1,3 @@ +--- +title: "Contact" +--- diff --git a/content/posts/_index.md b/content/posts/_index.md new file mode 100644 index 0000000..2055ae0 --- /dev/null +++ b/content/posts/_index.md @@ -0,0 +1,5 @@ +--- +title: "Posts" +--- + +Notes, essays, and updates — roughly in the order I thought of them. diff --git a/content/posts/hello-world.md b/content/posts/hello-world.md new file mode 100644 index 0000000..8400cb0 --- /dev/null +++ b/content/posts/hello-world.md @@ -0,0 +1,27 @@ +--- +title: "Hello, World" +date: 2026-08-26 +tags: ["meta", "placeholder"] +--- + + + +This is the first post on west575.org, which mostly exists right now to +prove that the blog list and single-post templates render the way I want +them to. Nothing below this line is meant to survive contact with an +actual reader. + +A real first post will probably explain what this site is for: some mix +of infrastructure notes from running Reground's client stack, observations +from freelancing and consulting, and updates on whatever fiction project +is currently eating my evenings. I haven't decided yet whether that's one +blog or three, so for now it's one blog and we'll see what happens. + +In the meantime, here's a paragraph of genuinely placeholder text just to +give the layout something to wrap, indent, and otherwise misbehave around: +long enough to wrap onto a second line on most screens, short enough that +nobody should mistake it for content. If you're reading this on the live +site, something went wrong in the publishing checklist — email me. + +Tags and dates on this post are also placeholders — adjust both when the +real post goes up. diff --git a/hugo.toml b/hugo.toml new file mode 100644 index 0000000..72eaa60 --- /dev/null +++ b/hugo.toml @@ -0,0 +1,41 @@ +baseURL = "https://west575.org/" +locale = "en-US" +title = "west575" + +enableRobotsTXT = true +buildDrafts = false +buildFuture = false +buildExpired = false +disableKinds = ["taxonomy", "term"] + +# RSS/Atom output is on by default (Hugo ships an "index.xml" RSS output +# for home/section/taxonomy pages) — nothing to configure, just don't add +# an `outputs` override that drops "RSS". + +[params] + env = "production" + description = "Will Estes' personal site — DevOps, fiction, and whatever else is on my mind." + author = "Will Estes" + # Placeholder — the eec instance for this domain isn't provisioned yet. + # Update once reground-infrastructure stands up eec-west575. + eec_base_url = "https://eec-west575.reground.org" + +[[menu.main]] + name = "Home" + url = "/" + weight = 10 + +[[menu.main]] + name = "Posts" + url = "/posts/" + weight = 20 + +[[menu.main]] + name = "Contact" + url = "/contact/" + weight = 30 + +[[menu.footer]] + name = "Contact" + url = "/contact/" + weight = 10 diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html new file mode 100644 index 0000000..a10ab37 --- /dev/null +++ b/layouts/_default/baseof.html @@ -0,0 +1,31 @@ + + + + + + {{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} · {{ .Site.Title }}{{ end }} + + + + + + {{ $css := resources.Get "css/main.css" }} + {{ if hugo.IsProduction }} + {{ $css = $css | minify | fingerprint }} + + {{ else }} + + {{ end }} + + + + + {{ partial "header.html" . }} + +
+ {{ block "main" . }}{{ end }} +
+ + {{ partial "footer.html" . }} + + diff --git a/layouts/_default/contact.html b/layouts/_default/contact.html new file mode 100644 index 0000000..e998f35 --- /dev/null +++ b/layouts/_default/contact.html @@ -0,0 +1,60 @@ +{{ define "main" }} +

Get in touch

+

Questions, corrections, or just want to say hi — I read every message myself.

+ +
+ + + + + + + + + + + + +
+

+ + +{{ end }} diff --git a/layouts/_default/list.html b/layouts/_default/list.html new file mode 100644 index 0000000..8372a6c --- /dev/null +++ b/layouts/_default/list.html @@ -0,0 +1,14 @@ +{{ define "main" }} +

{{ .Title }}

+{{ .Content }} + +{{ end }} diff --git a/layouts/_default/single.html b/layouts/_default/single.html new file mode 100644 index 0000000..078684b --- /dev/null +++ b/layouts/_default/single.html @@ -0,0 +1,21 @@ +{{ define "main" }} +
+
+

{{ .Title }}

+ {{ if not .Date.IsZero }} + + {{ end }} +
+ +
+ {{ .Content }} +
+
+{{ end }} diff --git a/layouts/index.html b/layouts/index.html new file mode 100644 index 0000000..3890340 --- /dev/null +++ b/layouts/index.html @@ -0,0 +1,25 @@ +{{ define "main" }} +
+

{{ .Title }}

+ {{ .Content }} +
+ +
+

Recent posts

+ +

All posts →

+
+ +
+

Stay in the loop

+

New posts land here first, and occasionally by email.

+ {{ partial "subscribe-form.html" . }} +
+{{ end }} diff --git a/layouts/partials/footer.html b/layouts/partials/footer.html new file mode 100644 index 0000000..f9e7ea2 --- /dev/null +++ b/layouts/partials/footer.html @@ -0,0 +1,19 @@ + diff --git a/layouts/partials/header.html b/layouts/partials/header.html new file mode 100644 index 0000000..81087ab --- /dev/null +++ b/layouts/partials/header.html @@ -0,0 +1,10 @@ +
+
+ {{ .Site.Title }} + +
+
diff --git a/layouts/partials/subscribe-form.html b/layouts/partials/subscribe-form.html new file mode 100644 index 0000000..2c0271b --- /dev/null +++ b/layouts/partials/subscribe-form.html @@ -0,0 +1,51 @@ +{{/* Standalone newsletter signup — not tied to any ARC/giveaway flow. + Posts a JSON body to {eec_base_url}/subscribe. Include anywhere with + {{ partial "subscribe-form.html" . }}. */}} +{{ $id := printf "subscribe-%d" (now.UnixNano) }} +
+ + + + + + +
+

+ + diff --git a/static/README.md b/static/README.md new file mode 100644 index 0000000..66a17c5 --- /dev/null +++ b/static/README.md @@ -0,0 +1,5 @@ +This directory is intentionally empty for now. + +TODO: add a real favicon (favicon.ico / favicon.svg / apple-touch-icon.png) +before launch. Not fabricating placeholder binary image files here — drop +real ones in and reference them from layouts/_default/baseof.html's .