{"id":30615627,"url":"https://github.com/alexfalkowski/web","last_synced_at":"2026-07-04T01:05:03.377Z","repository":{"id":247582955,"uuid":"826246685","full_name":"alexfalkowski/web","owner":"alexfalkowski","description":"A website lean-thoughts.com.","archived":false,"fork":false,"pushed_at":"2026-06-08T11:33:13.000Z","size":2064,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-06-08T13:22:14.029Z","etag":null,"topics":["cucumber","golang","htmx","make","ruby"],"latest_commit_sha":null,"homepage":"https://alexfalkowski.github.io/web","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alexfalkowski.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2024-07-09T10:42:51.000Z","updated_at":"2026-06-08T11:30:31.000Z","dependencies_parsed_at":"2026-02-26T11:06:16.766Z","dependency_job_id":null,"html_url":"https://github.com/alexfalkowski/web","commit_stats":null,"previous_names":["alexfalkowski/web"],"tags_count":765,"template":false,"template_full_name":"alexfalkowski/go-service-template","purl":"pkg:github/alexfalkowski/web","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexfalkowski%2Fweb","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexfalkowski%2Fweb/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexfalkowski%2Fweb/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexfalkowski%2Fweb/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alexfalkowski","download_url":"https://codeload.github.com/alexfalkowski/web/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexfalkowski%2Fweb/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34078019,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-08T02:00:07.615Z","response_time":111,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cucumber","golang","htmx","make","ruby"],"created_at":"2025-08-30T08:06:07.212Z","updated_at":"2026-07-04T01:05:03.371Z","avatar_url":"https://github.com/alexfalkowski.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![CircleCI](https://circleci.com/gh/alexfalkowski/web.svg?style=svg)](https://circleci.com/gh/alexfalkowski/web)\n[![codecov](https://codecov.io/gh/alexfalkowski/web/graph/badge.svg?token=S9SPVVYQAY)](https://codecov.io/gh/alexfalkowski/web)\n[![Go Report Card](https://goreportcard.com/badge/github.com/alexfalkowski/web)](https://goreportcard.com/report/github.com/alexfalkowski/web)\n[![Go Reference](https://pkg.go.dev/badge/github.com/alexfalkowski/web.svg)](https://pkg.go.dev/github.com/alexfalkowski/web)\n[![Stability: Active](https://masterminds.github.io/stability/active.svg)](https://masterminds.github.io/stability/active.html)\n\n# 🌐 Web\n\nA small Go service that serves the website at:\n\n- \u003chttps://web.lean-thoughts.com/\u003e\n\nThe service is built on top of the [`mvc`](https://github.com/alexfalkowski/go-service/tree/master/net/http/mvc) package from `go-service` and ships as a single binary with server-side templates, content, the favicon, and static site assets embedded.\n\n## 🧭 Background\n\nThis project is an implementation playground for the ideas outlined in:\n\n- \u003chttps://alejandrofalkowski.substack.com/p/hyperprogress\u003e\n\n## ✨ What it does\n\nAt a high level the service:\n\n- serves the home page (`/`)\n- serves a books page (`/books`)\n- serves `robots.txt` (`/robots.txt`)\n- serves `sitemap.xml` (`/sitemap.xml`)\n- serves the favicon (`/favicon.ico`)\n- renders a custom not-found page for missing routes\n- adds browser security headers to site responses\n- exposes health, liveness/readiness, and metrics endpoints\n\nThe HTML templates, error templates, books YAML data, favicon image, robots file, and sitemap are embedded into the binary using `go:embed`. Full-page browser rendering also loads HTMX and Pico CSS from jsDelivr, with those origins allowed by the response CSP.\n\n## 🏗️ Architecture overview\n\n### 🗂️ Project layout\n\nThis repo follows the structure described in:\n\n- \u003chttps://github.com/golang-standards/project-layout\u003e\n\nKey directories:\n\n- `main.go`: entrypoint for the `web` binary\n- `internal/`: application code (not importable from other modules)\n- `test/`: acceptance/system tests and supporting Ruby test client\n- `bin/` + `Makefile`: build/dev/test automation\n\n### 🧩 Dependency injection and modules\n\nThe service is wired with dependency injection using `go-service/v2/di`. The top-level module that assembles the server is:\n\n- `internal/cmd.Module`\n\nIt pulls in configuration, health, and site modules.\n\n### 🛣️ MVC routing and rendering\n\nRouting and rendering are handled using:\n\n- `go-service/v2/net/http/mvc`\n\nFeature modules (e.g. books/root/robots/sitemap) register their routes during DI wiring.\n\n### 📦 Embedded assets\n\nThe site package embeds:\n\n- templates for layout, pages, and not-found errors\n- the books YAML file used to render the books page\n- the favicon PNG\n- `robots.txt`\n- `sitemap.xml`\n\nSee:\n\n- `internal/site/site.go`\n\n## 🔌 Endpoints\n\n### 📄 Pages\n\n- `GET /` renders the home page\n- `PUT /` renders a partial/fragment version of the home page (used for incremental updates)\n- `GET /books` renders the books page\n- `PUT /books` renders a partial/fragment version of the books page\n- `GET /robots.txt` serves the robots file as a static asset\n- `GET /sitemap.xml` serves the sitemap file as a static asset\n- `GET /favicon.ico` serves the browser favicon\n- missing routes render a `404` not-found page\n\n\u003e [!NOTE]\n\u003e The `PUT` endpoints exist to support partial rendering patterns, for example HTMX-style incremental updates. The exact response shape depends on the templates/layout configured in the MVC layer.\n\n\u003e [!TIP]\n\u003e Use `GET` when checking complete pages in a browser and `PUT` when checking fragment rendering.\n\n### 🫀 Health and observability\n\nThe HTTP transport registers service-prefixed health and observability routes.\nWith the local service name `web`, the endpoints are:\n\n- `/web/healthz` (overall health / online)\n- `/web/livez` (liveness)\n- `/web/readyz` (readiness)\n- `/web/metrics` (Prometheus metrics)\n\nHealth timings are configured via the service config under the `health` section.\n\n`/web/healthz` uses the default `go-health/v2` online registration, so it can depend on public connectivity. `/web/livez` and `/web/readyz` use noop checks.\n\n### 🛡️ Response headers\n\nSite responses include browser security headers such as `Content-Security-Policy`, `X-Content-Type-Options`, `Referrer-Policy`, `X-Frame-Options`, `Permissions-Policy`, and `Strict-Transport-Security`.\n\nThe CSP intentionally permits jsDelivr for HTMX and Pico CSS, plus Cloudflare Insights script and beacon origins (`static.cloudflareinsights.com` and `cloudflareinsights.com`) for production browser analytics.\n\nEmbedded static assets also include `Cache-Control` and `ETag` headers, and matching `If-None-Match` requests return `304 Not Modified`.\n\n\u003e [!NOTE]\n\u003e The acceptance suite verifies these headers for the page, robots, sitemap, favicon, and not-found responses.\n\n## 🧰 Development\n\n### ✅ Prerequisites\n\nInstall:\n\n- [Go](https://go.dev/) (see `go.mod`; check locally with `go version`)\n- [Ruby](https://www.ruby-lang.org/en/)\n- Bundler for the Ruby test harness\n\nIf you are cloning the repo, initialize submodules before relying on Make targets:\n\n```sh\ngit clone --recurse-submodules https://github.com/alexfalkowski/web.git\n```\n\nFor an existing checkout where `bin/` may be absent or stale:\n\n```sh\ngit submodule sync\ngit submodule update --init\n```\n\nThen install dependencies:\n\n```sh\nmake dep\n```\n\n\u003e [!IMPORTANT]\n\u003e The root `Makefile` includes shared build fragments from `bin/`, so a missing submodule can prevent `make` from parsing at all. Once `bin/` is present, `make submodule` can refresh it through the normal repo target.\n\n\u003e [!WARNING]\n\u003e Some targets require external tools in addition to Go and Ruby. For example, `make dev` uses `air`, Go checks may use `gotestsum`, `golangci-lint`, and `govulncheck`, and security checks may use Trivy.\n\n### 🧾 Useful Make targets\n\nThis repo relies on `make` for a consistent developer experience.\n\nList all available commands:\n\n```sh\nmake help\n```\n\nCommon workflows:\n\n```sh\n# Install dependencies (Go + Ruby)\nmake dep\n\n# Run linters\nmake lint\n\n# Auto-fix lint where possible\nmake fix-lint\n\n# Format code\nmake format\n\n# Run the repo-defined Go specs wrapper\nmake specs\n\n# Run Cucumber acceptance tests\nmake features\n\n# Run Cucumber benchmark scenarios\nmake benchmarks\n```\n\n\u003e [!TIP]\n\u003e `make help` is the best way to discover the current command surface because most project workflows come from the shared `bin/` Make fragments.\n\n### 🚀 Running locally\n\nThere are two common ways to run the service:\n\n#### 🔁 1) Dev mode\n\nUse the dev target (recommended while iterating):\n\n```sh\nmake dev\n```\n\nThis runs the service with `test/.config/server.yml`.\n\n#### 🧱 2) Build and run the binary\n\nBuild a local binary:\n\n```sh\nmake build\n```\n\nThen run it:\n\n```sh\n./web server -config file:test/.config/server.yml\n```\n\n\u003e [!IMPORTANT]\n\u003e The current config flag is `-config`; `-c` is the short form. The CLI command is `server`, registered in `internal/cmd`, and starts the HTTP server using the DI module graph.\n\n### 🔎 Example: verifying endpoints\n\nOnce the server is running, you can verify key endpoints.\n\nIf you started the service with `make dev` or with `-config file:test/.config/server.yml`,\nthe HTTP server listens on `localhost:11000`.\n\nPages:\n\n```sh\ncurl -i http://localhost:11000/\ncurl -i http://localhost:11000/books\ncurl -i http://localhost:11000/robots.txt\ncurl -i http://localhost:11000/sitemap.xml\ncurl -i http://localhost:11000/favicon.ico\n```\n\nPartial renders (PUT):\n\n```sh\ncurl -i -X PUT http://localhost:11000/\ncurl -i -X PUT http://localhost:11000/books\n```\n\nHealth:\n\n```sh\ncurl -i http://localhost:11000/web/healthz\ncurl -i http://localhost:11000/web/livez\ncurl -i http://localhost:11000/web/readyz\ncurl -i http://localhost:11000/web/metrics\n```\n\nNot found:\n\n```sh\ncurl -i http://localhost:11000/not-a-real-page\n```\n\n\u003e [!CAUTION]\n\u003e Ports, TLS, telemetry, and other server settings come from configuration. Do not treat `test/.config/server.yml` as a production configuration.\n\n## ⚙️ Configuration\n\nThe service config model lives in:\n\n- `internal/config.Config`\n\nIt embeds the shared base config from `go-service` and adds a `health` section.\n\nThe canonical local example is:\n\n- `test/.config/server.yml`\n\nThe local development config in `test/.config/server.yml` includes this\nfirst-use excerpt:\n\n```yaml\nhealth:\n  duration: 1s\n  timeout: 1s\ntransport:\n  http:\n    address: tcp://:11000\n```\n\nThe service-specific `health` section is required. `health.duration` must be a\npositive Go duration and controls how often health registrations are evaluated;\n`health.timeout` may be zero or greater and controls the online health check\ntimeout.\n\n\u003e [!NOTE]\n\u003e The full local config also sets the environment, UUID generation, tint logging, Prometheus metrics, an OTLP tracer endpoint, HTTP limiter tokens/interval, and HTTP timeout. The wider configuration shape comes from shared `go-service` sections such as environment, telemetry, transport, and version metadata.\n\n## 🧪 Testing\n\nThe primary behavioral checks are the Ruby/Cucumber suites:\n\n```sh\nmake features\nmake benchmarks\n```\n\nThese targets run the acceptance harness from `test/`. Nonnative loads\n`test/nonnative.yml`, starts `../web server -config file:.config/server.yml` on\n`localhost:11000`, and writes Nonnative, server, and Cucumber output under\n`test/reports/`. Stop any local service already using `11000` before running the\nacceptance suites.\n\nTo run a narrower acceptance scope while iterating, pass feature paths relative\nto `test/`:\n\n```sh\nmake features feature=features/site/site.feature\nmake features feature=features/health/observability.feature\nmake benchmarks feature=features/site/benchmark.feature\n```\n\nThe main CircleCI service build also runs:\n\n```sh\nmake lint\nmake sec\nmake analyse\nmake coverage\nmake codecov-upload\n```\n\n`make codecov-upload` is CI upload behavior, not a read-only local validation\nstep.\n\nThe full CircleCI workflow additionally runs Docker image checks and the\nsubmodule sync/push job on non-`master` branches:\n\n```sh\nmake platform=amd64 test-docker\nmake platform=arm64 test-docker\nmake sync push\n```\n\nOn `master`, CircleCI also runs versioning, Docker release/manifest, and deploy jobs.\n\nGo support checks are still available:\n\n```sh\nmake specs\ngo test ./...\n```\n\n\u003e [!NOTE]\n\u003e Treat `make features` and `make benchmarks` as the authoritative product behavior checks. The Go tests support build/tooling confidence but are not the main product signal.\n\n### 💎 Ruby acceptance test client\n\nThe Ruby test helper client is in:\n\n- `test/lib/web.rb`\n- `test/lib/web/v1/http.rb`\n\nIt provides a small wrapper around HTTP calls used by the acceptance tests.\n\n## 🎨 Style\n\nGo code generally follows:\n\n- \u003chttps://github.com/uber-go/guide/blob/master/style.md\u003e\n\n## 🚢 Changes and releases\n\nReleases are handled through CI and GoReleaser configuration:\n\n- `.circleci/config.yml`\n- `.goreleaser.yml`\n\nGenerated changelog text is part of the GoReleaser release flow.\n\n\u003e [!CAUTION]\n\u003e Release, Docker publishing, deployment, and GitHub PR targets can push to external systems. Use the read-only validation targets unless you intend to publish or update remote state.\n\n## 📜 License\n\nSee:\n\n- `LICENSE`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexfalkowski%2Fweb","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falexfalkowski%2Fweb","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexfalkowski%2Fweb/lists"}