From bdcad1321f245d9ad72466b456370dfd2d6d350e Mon Sep 17 00:00:00 2001 From: Labyricorn Date: Tue, 11 Aug 2026 21:40:42 -0700 Subject: [PATCH] Add example blog and article entries --- .../example-information-card/contents.lr | 105 +++ .../example-deployment-runbook/contents.lr | 620 ++++++++++++++++++ 2 files changed, 725 insertions(+) create mode 100644 content/articles/example-information-card/contents.lr create mode 100644 content/blog/example-deployment-runbook/contents.lr diff --git a/content/articles/example-information-card/contents.lr b/content/articles/example-information-card/contents.lr new file mode 100644 index 0000000..ff17aeb --- /dev/null +++ b/content/articles/example-information-card/contents.lr @@ -0,0 +1,105 @@ +_model: entry +--- +title: Example Article: Anatomy of an Information Card +--- +date: 2026-08-11 +--- +updated: 2026-08-11 +--- +author: Example Article Author +--- +tags: + +Lorem Ipsum +Information Card +Field Mapping +Example +--- +kicker: Article +--- +summary: A fully populated example article showing how every information-card value maps to a Lektor content field. +--- +published_urls: + +https://example.com/article +https://publication.example.org/features/anatomy-of-an-article-information-card-demonstration +--- +body: +## Information card field map + +Every available row in the article information card is intentionally visible +on this example page. The values map to this record's `contents.lr` file as +follows: + +`content/articles/example-information-card/contents.lr` + +- **Author** is read from the `author` field. +- **Published** is read from the `date` field. +- **Updated** is read from the optional `updated` field. +- **Reading time** is calculated automatically from the words in the `body` + field, using approximately 200 words per minute. +- **Topics** are read from the multiline `tags` field. +- **Published online** loops over every item in the multiline `published_urls` + field. + +The first publication URL stays below 40 characters and is displayed in full. +The second is deliberately longer than 40 characters, so its visible text uses +a middle ellipsis while its link destination, tooltip, and accessible label +retain the complete URL. + +## Lorem ipsum demonstration + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere, +libero non posuere luctus, neque tellus cursus sapien, vitae tincidunt erat +metus et velit. Sed dignissim sem vel augue fermentum, vitae tristique lorem +facilisis. Praesent commodo, mauris id faucibus malesuada, augue justo +sollicitudin nibh, vitae efficitur magna arcu sed neque. Nulla facilisi. Donec +tempor turpis sed felis interdum, quis consequat nibh consequat. + +Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere +cubilia curae; Etiam id lectus vel ipsum placerat pellentesque. Morbi vulputate +semper dolor, in facilisis justo volutpat sed. Suspendisse potenti. Fusce +ultricies ipsum at arcu aliquet, a feugiat augue suscipit. Nam interdum +fermentum enim, et commodo sapien pharetra sit amet. Curabitur vitae urna vitae +mauris volutpat consequat non non augue. + +Aliquam erat volutpat. Proin vitae metus eget nunc porttitor elementum. Mauris +euismod, sem sed tincidunt tincidunt, sapien velit consectetur neque, at +viverra nibh eros nec urna. Duis malesuada sapien in velit feugiat, sed +ullamcorper massa tincidunt. Cras condimentum risus at neque vestibulum, quis +tempor justo tristique. Pellentesque habitant morbi tristique senectus et netus +et malesuada fames ac turpis egestas. + +Donec eu feugiat neque. Quisque ac velit vitae odio volutpat vulputate. Nunc +faucibus eros non dolor tempor, nec posuere lacus tristique. Sed euismod +convallis nulla, quis feugiat dolor vulputate sit amet. Aenean commodo feugiat +ipsum, vel dictum sapien ullamcorper quis. Vivamus id enim nec nibh eleifend +convallis. In hac habitasse platea dictumst. Maecenas cursus orci eget dui +aliquet, vitae feugiat elit rhoncus. + +Phasellus dignissim, metus ac tristique convallis, purus massa pulvinar enim, +eu facilisis justo sem sit amet nibh. Nullam gravida interdum quam, a luctus +turpis finibus sed. Sed at magna vel lectus faucibus malesuada. Integer +facilisis quam non purus volutpat, quis tristique nisl blandit. Ut pellentesque +ligula vitae purus porttitor, vitae scelerisque justo feugiat. Duis luctus +metus quis orci egestas, id pellentesque mi faucibus. + +Morbi sit amet nibh et justo sagittis viverra. Nulla vitae odio ac tortor +tempor pretium. Curabitur dignissim massa at justo eleifend, vel feugiat neque +facilisis. Praesent sed tellus quis augue vulputate malesuada. Sed tincidunt +nisi in risus dignissim, a hendrerit ipsum volutpat. Integer faucibus metus et +turpis sodales, sed convallis lectus feugiat. Vivamus finibus, tortor id +efficitur feugiat, odio mauris imperdiet justo, quis pulvinar erat sem at arcu. + +Etiam consequat ligula sed sem faucibus, eu feugiat leo posuere. Pellentesque +congue mauris sit amet sapien vestibulum, quis vulputate nulla malesuada. +Suspendisse vitae ipsum ut sapien consequat tempus. Aenean et lectus in justo +scelerisque feugiat. Cras sed posuere ex. Nam non egestas risus. Vestibulum +mollis mauris eget nisi efficitur, vitae sollicitudin tortor imperdiet. Donec +at consequat velit, non cursus risus. + +Sed vulputate justo at elit gravida, a tristique mauris blandit. In hac +habitasse platea dictumst. Mauris at erat luctus, volutpat risus vitae, +fermentum ipsum. Proin malesuada nibh et orci tristique, eget efficitur nibh +interdum. Quisque feugiat sem at purus pretium, id sodales nunc ullamcorper. +Integer accumsan urna sit amet mi placerat, vitae suscipit tortor lacinia. diff --git a/content/blog/example-deployment-runbook/contents.lr b/content/blog/example-deployment-runbook/contents.lr new file mode 100644 index 0000000..6422efd --- /dev/null +++ b/content/blog/example-deployment-runbook/contents.lr @@ -0,0 +1,620 @@ +_model: entry +--- +title: Example Blog Entry: Labyricorn Deployment Runbook +--- +date: 2026-08-11 +--- +author: Example Blog Author +--- +tags: + +Lektor +Deployment +Operations +Example +--- +kicker: Blog +--- +summary: A complete example blog record whose body mirrors the repository runbook and documents how its information card is populated. +--- +body: +## Information card field map + +This example deliberately exposes how the visible post-information card maps +back to this record's `contents.lr` file: + +`content/blog/example-deployment-runbook/contents.lr` + +- **Author** is read from the `author` field. +- **Published** is read from the `date` field. +- **Tags** are read from the multiline `tags` field. + +The title, summary, and article body are supplied by `title`, `summary`, and +`body`, respectively, but those values appear outside the information card. + +## Labyricorn website and deployment runbook + +This repository is the canonical source for `www.labyricorn.com`. It contains +the editable Lektor project—not generated production files—and is also the +primary operations guide for building, deploying, verifying, rolling back, and +troubleshooting the site. + +## Quick reference + +| Item | Value | +| --- | --- | +| Production URL | `https://www.labyricorn.com` | +| Canonical Git repository | `https://git.labyricorn.com/Labyricorn/labyricorn-site.git` | +| Production branch | `main` | +| Production host | `ubuntu2-47` (`10.138.2.47`) | +| Repository checkout | `/srv/labyricorn/repo` | +| Active document root | `/srv/labyricorn/current` | +| Release storage | `/srv/labyricorn/releases` | +| Deployment command | `sudo deploy-labyricorn` | +| Rollback listing | `sudo rollback-labyricorn` | +| Rollback command | `sudo rollback-labyricorn RELEASE_NAME` | +| Deployment log | `/srv/labyricorn/logs/deploy.log` | +| nginx site config | `/etc/nginx/sites-available/labyricorn` | +| Lektor executable | `/srv/labyricorn/.local/bin/lektor` | +| Deployment account | `labyricorn-deploy` | + +## Architecture + +```text +Gitea: Labyricorn/labyricorn-site (main) + | + | authenticated Git fetch + v + /srv/labyricorn/repo + | + | Lektor static build + v + /srv/labyricorn/releases/- + | + | validated, atomic symlink switch + v + /srv/labyricorn/current + | + | read-only static files + v + nginx :80 + | + | Cloudflare proxy and public TLS + v + https://www.labyricorn.com +``` + +Gitea is the source of truth. The production checkout is disposable state and +must not contain unpublished edits during deployment. Lektor's development +server is not part of production serving; nginx serves generated files only. + +## Repository contents + +```text +Labyricorn.lektorproject Lektor project definition +content/ Editable site content (`contents.lr` files) +models/ Lektor content models +templates/ Jinja templates +assets/static/ CSS, favicon, and other static source assets +.gitignore Excludes generated and local files +README.md This runbook +``` + +Generated output such as `build/`, `dist/`, and `.lektor/` is intentionally +ignored and must not be committed. + +## Routine content workflow + +Edit in a normal Git checkout, review the changes, and push them to `main`. +A successful push triggers the repository's Gitea Action, which builds and +atomically activates that exact revision on the production host. + +```bash +git switch main +git pull --ff-only origin main + +# Edit content, templates, models, or assets. + +lektor build --output-path build +git status +git add +git commit -m "Describe the site change" +git push origin main +``` + +Watch the run under **Actions** in Gitea. Manual deployment remains available +for recovery or controlled operations: + +```bash +sudo deploy-labyricorn +``` + +Do not edit generated files under `/srv/labyricorn/current` or +`/srv/labyricorn/releases`. They will be replaced by deployment and are not +source-controlled. + +## Local development + +Install Lektor in an isolated Python environment. For example: + +```bash +python3 -m venv .venv +. .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install lektor +``` + +Run the local editor and preview server: + +```bash +lektor server +``` + +Run a production-style build: + +```bash +lektor build --output-path build +test -f build/index.html +``` + +The project currently has no third-party Lektor packages or plugins. + +## Production filesystem and permissions + +```text +/srv/labyricorn/ +├── repo/ Git working copy (not web-accessible) +├── releases/ Successful immutable builds +├── current -> releases/ +├── logs/ +│ ├── deploy.log +│ └── deploy.lock +├── .local/bin/lektor pipx-installed Lektor +├── .netrc Git HTTPS credential (0600; secret) +├── .ssh/ Reserved deployment SSH material +└── README-DEPLOYMENT.md Supplemental host-local notes +``` + +The `labyricorn-deploy` account owns the checkout, releases, logs, Lektor +environment, and Git credential. nginx's `www-data` account can traverse the +base path and read active generated files, but cannot read the source checkout +or credential. nginx cannot modify either source or releases. + +## Deployment behavior + +`/usr/local/bin/deploy-labyricorn` performs the following sequence: + +1. Changes from root to the unprivileged `labyricorn-deploy` account. +2. Acquires an exclusive `flock` lock to prevent concurrent deployments. +3. Refuses to proceed if the working tree contains any tracked or untracked + changes. +4. Fetches and prunes `origin`. +5. Verifies that `origin/main` exists. +6. Switches to local `main` and resets it exactly to `origin/main`. +7. Locates exactly one `.lektorproject` file instead of assuming its name. +8. Creates a unique release named with UTC time and the 12-character commit ID. +9. Runs Lektor with an explicit output directory. +10. Requires the build to succeed and contain `index.html`. +11. Writes the full commit ID to `.labyricorn-commit` in the release. +12. Atomically replaces `/srv/labyricorn/current` with a relative symlink to + the new release. +13. Keeps the active release plus recent prior releases, targeting five total. +14. Logs start, failure, success, release name, and commit. + +Run it with: + +```bash +sudo deploy-labyricorn +``` + +A failed build is removed before activation. The current symlink is not changed, +so the previously working site remains online. + +### Deployment preflight + +```bash +sudo -u labyricorn-deploy env HOME=/srv/labyricorn \ + git -C /srv/labyricorn/repo status --short --branch + +sudo -u labyricorn-deploy env HOME=/srv/labyricorn \ + git -C /srv/labyricorn/repo fetch origin +``` + +The status must be clean. Do not bypass a dirty-tree refusal. Determine whether +the files are legitimate unpublished admin edits, then commit and push them or +remove them deliberately before deployment. + +## Verify a deployment + +```bash +readlink -f /srv/labyricorn/current +cat /srv/labyricorn/current/.labyricorn-commit + +sudo -u labyricorn-deploy env HOME=/srv/labyricorn \ + git -C /srv/labyricorn/repo rev-parse HEAD + +sudo -u labyricorn-deploy env HOME=/srv/labyricorn \ + git -C /srv/labyricorn/repo ls-remote origin refs/heads/main + +nginx -t +systemctl is-active nginx + +curl -I -H 'Host: www.labyricorn.com' \ + -H 'X-Forwarded-Proto: https' http://127.0.0.1/ + +curl -I https://www.labyricorn.com/ +``` + +The commit in the active release, local `HEAD`, and remote `main` should match. +The local nginx request with `X-Forwarded-Proto: https` should return `200`. +Public HTTP should redirect to HTTPS, and public HTTPS should return `200`. + +## Rollback + +List releases and identify the current one: + +```bash +sudo rollback-labyricorn +``` + +Switch atomically to a previous successful release: + +```bash +sudo rollback-labyricorn 20260812T024742Z-92475d2ec759 +``` + +Use a release name printed by the listing command. Rollback does not build, +fetch, or alter Git. It only changes the `current` symlink and records the event +in the deployment log. + +Verify afterward: + +```bash +readlink -f /srv/labyricorn/current +cat /srv/labyricorn/current/.labyricorn-commit +curl -I -H 'Host: www.labyricorn.com' \ + -H 'X-Forwarded-Proto: https' http://127.0.0.1/ +``` + +A normal deployment after rollback rebuilds and reactivates the latest remote +`main` commit. + +## nginx and URL handling + +The site configuration is: + +```text +/etc/nginx/sites-available/labyricorn +/etc/nginx/sites-enabled/labyricorn -> ../sites-available/labyricorn +``` + +nginx serves `/srv/labyricorn/current` and supports: + +- `index.html` at directory-style URLs; +- direct static assets; +- optional `.html` fallback; +- generated `/404/index.html` with an HTTP `404` response; +- disabled directory browsing; +- rejection of hidden paths and common executable-script extensions; +- dedicated access and error logs. + +Always validate before reloading nginx: + +```bash +nginx -t +systemctl reload nginx +``` + +nginx logs: + +```text +/var/log/nginx/labyricorn-access.log +/var/log/nginx/labyricorn-error.log +``` + +## HTTPS, Cloudflare, and DNS + +Current production HTTPS terminates at Cloudflare. The origin nginx process +currently listens on port `80`, not `443`. For `www.labyricorn.com`, nginx uses +Cloudflare's `X-Forwarded-Proto` header to distinguish public HTTP from HTTPS: + +- public HTTP redirects to `https://www.labyricorn.com`; +- Cloudflare-originated HTTPS requests are served without a redirect loop; +- `labyricorn.com` is configured at nginx to redirect to the canonical HTTPS + `www` hostname. + +The apex `labyricorn.com` did not resolve during initial setup. Its redirect will +not work publicly until DNS/Cloudflare has an apex record routed to this origin. + +Certbot and the nginx integration are installed, and `certbot.timer` is enabled, +but no origin certificate was issued during setup. Do not run Certbot blindly +behind Cloudflare. First decide whether the desired origin mode is a Cloudflare +Origin CA certificate, Let's Encrypt with a compatible challenge, or direct DNS +without the proxy. After changing TLS, test both origin and public behavior for +redirect loops. + +Useful checks: + +```bash +getent ahosts www.labyricorn.com +getent ahosts labyricorn.com +curl -I http://www.labyricorn.com/ +curl -I https://www.labyricorn.com/ +systemctl status certbot.timer +certbot certificates +``` + +## Firewall and exposed services + +UFW is active with default incoming denial. Allowed services are: + +- OpenSSH: TCP 22 +- nginx HTTP/HTTPS profile: TCP 80 and 443 + +Inspect current state: + +```bash +ufw status verbose +ss -lntup +``` + +Lektor's development port must never be opened in UFW or bound to `0.0.0.0`. + +## Optional server-side Lektor admin + +An optional unit exists at: + +```text +/etc/systemd/system/lektor-admin.service +``` + +Current state: **disabled and stopped**. When started, it runs as +`labyricorn-deploy` and binds only to `127.0.0.1:5000`. It is not proxied by +nginx and production remains independent of it. + +Start it temporarily: + +```bash +systemctl start lektor-admin +systemctl status lektor-admin +ss -lntp | grep ':5000' +``` + +Access it securely through an SSH tunnel from an administrator workstation: + +```bash +ssh -L 5000:127.0.0.1:5000 root@10.138.2.47 +``` + +Then browse to `http://127.0.0.1:5000` on that workstation. + +Stop it when finished: + +```bash +systemctl stop lektor-admin +``` + +### Committing admin edits made on the server + +Lektor can edit files, but it does not automatically create Git commits or push +them. The deployment account intentionally has authenticated Git write access so +a controlled admin workflow can publish server-side edits. + +Review everything before committing: + +```bash +sudo -u labyricorn-deploy env HOME=/srv/labyricorn bash +cd /srv/labyricorn/repo +git status +git diff +git add +git commit -m "Describe the admin edit" +git push origin main +exit +``` + +Then run `sudo deploy-labyricorn`. The deployment script will refuse to run +while uncommitted admin edits remain. This is deliberate protection against +silently discarding edits when it resets to `origin/main`. + +Before enabling persistent or public admin access, add authentication and make a +separate security decision. Do not publish port 5000 directly and do not proxy +an unauthenticated editor through nginx. + +## Git authentication and secrets + +The private Gitea repository is accessed over HTTPS because the Gitea SSH port +was not reachable through `git.labyricorn.com` during setup. + +Credential purpose and location: + +| Purpose | Location | Permissions | +| --- | --- | --- | +| Gitea HTTPS fetch/push | `/srv/labyricorn/.netrc` | `0600`, owned by `labyricorn-deploy` | +| Reserved SSH identity | `/srv/labyricorn/.ssh/id_ed25519` | `0600`, owned by `labyricorn-deploy` | + +Never place token contents, passwords, or private keys in this README, Git URLs, +shell scripts, logs, commits, issue trackers, or command history. + +Check credential-file metadata without printing its contents: + +```bash +stat -c '%A %U:%G %n' /srv/labyricorn/.netrc +``` + +If the Gitea token is rotated, replace `.netrc` as `labyricorn-deploy`, preserve +mode `0600`, and verify both fetch and push access before revoking the old token. +Avoid printing either credential during rotation. + +## Logs and diagnostics + +```bash +# Deployment and rollback history +tail -n 100 /srv/labyricorn/logs/deploy.log + +# nginx service and request errors +journalctl -u nginx --since today +tail -n 100 /var/log/nginx/labyricorn-error.log + +# Optional admin service +journalctl -u lektor-admin --since today + +# Repository state +sudo -u labyricorn-deploy env HOME=/srv/labyricorn \ + git -C /srv/labyricorn/repo status + +# Active release +readlink -f /srv/labyricorn/current +cat /srv/labyricorn/current/.labyricorn-commit + +# Services and sockets +systemctl status nginx +systemctl status certbot.timer +ss -lntup +``` + +## Troubleshooting + +### Deployment says the repository is dirty + +Inspect as the deployment account: + +```bash +sudo -u labyricorn-deploy env HOME=/srv/labyricorn \ + git -C /srv/labyricorn/repo status --short +``` + +If these are intended admin edits, review, commit, and push them. Otherwise, +identify their origin before removing anything. Never use a destructive reset +until you are certain no work needs to be preserved. + +### Git fetch or push fails + +Check DNS, HTTPS, the remote URL, and credential permissions without displaying +the secret: + +```bash +getent ahosts git.labyricorn.com +curl -I https://git.labyricorn.com/ +sudo -u labyricorn-deploy env HOME=/srv/labyricorn \ + git -C /srv/labyricorn/repo remote -v +stat -c '%A %U:%G %n' /srv/labyricorn/.netrc +``` + +An authentication failure usually means the token was revoked, expired, or +lacks repository access. + +### Lektor build fails + +Run a disposable diagnostic build; do not build into `current`: + +```bash +test_dir=$(mktemp -d /srv/labyricorn/releases/.diagnostic.XXXXXX) +chown labyricorn-deploy:labyricorn-deploy "$test_dir" +sudo -u labyricorn-deploy env HOME=/srv/labyricorn \ + /srv/labyricorn/.local/bin/lektor \ + --project /srv/labyricorn/repo/Labyricorn.lektorproject \ + build --output-path "$test_dir" +``` + +Inspect the error and source. Remove the diagnostic directory only after +confirming its exact path and that it is not referenced by `current`. + +### nginx returns an error + +```bash +nginx -t +systemctl status nginx +readlink -f /srv/labyricorn/current +test -f /srv/labyricorn/current/index.html +tail -n 100 /var/log/nginx/labyricorn-error.log +``` + +Do not point nginx at the Git checkout or start Lektor as a production server. + +### Public HTTPS loops or behaves differently from local HTTP + +Check Cloudflare proxy mode and the forwarded protocol behavior: + +```bash +curl -I -H 'Host: www.labyricorn.com' http://127.0.0.1/ +curl -I -H 'Host: www.labyricorn.com' \ + -H 'X-Forwarded-Proto: https' http://127.0.0.1/ +curl -I http://www.labyricorn.com/ +curl -I https://www.labyricorn.com/ +``` + +The first origin request should redirect; the simulated Cloudflare HTTPS request +should return the site. A different result indicates a proxy/header or nginx +configuration problem. + +## Automatic deployment + +Pushes to `main` automatically deploy through Gitea Actions. The workflow is +versioned with the site at `.gitea/workflows/deploy.yml` and has one job: + +1. Run on the repository-scoped runner labelled `labyricorn-deploy`. +2. Execute the existing root-owned `deploy-labyricorn` command. +3. Confirm `/srv/labyricorn/current/.labyricorn-commit` equals the pushed commit. +4. Request the site through nginx on the local origin and fail if it is unhealthy. + +The action intentionally does not check out the repository into its own +workspace. The deployment script fetches `origin/main` using the dedicated +`labyricorn-deploy` account, validates a clean checkout, builds a new release, +and atomically changes the `current` symlink. Concurrent action runs share the +`labyricorn-production` concurrency group, and the deployment script also uses +a filesystem lock. + +### Runner installation + +The production host runs official Gitea Runner 3.0.0 directly on Linux; Docker +is not installed or required for this workflow. + +| Item | Value | +|---|---| +| systemd unit | `gitea-runner.service` | +| service account | `gitea-runner` | +| runner name | `labyricorn-production` | +| registration scope | `Labyricorn/labyricorn-site` repository | +| execution label | `labyricorn-deploy:host` | +| runner binary | `/usr/local/bin/gitea-runner` | +| runner configuration | `/etc/gitea-runner/config.yaml` | +| registration state | `/var/lib/gitea-runner/.runner` | +| job workspace | `/var/lib/gitea-runner/work` | +| sudo policy | `/etc/sudoers.d/gitea-runner-labyricorn` | + +The runner account has no general administrative access. Its sudo policy allows +only this exact command without a password: + +```text +/usr/local/bin/deploy-labyricorn +``` + +Useful checks: + +```bash +systemctl status gitea-runner +journalctl -u gitea-runner -n 100 --no-pager +sudo -l -U gitea-runner +``` + +If the runner must be replaced, delete or disable it in **Repository Settings → +Actions → Runners**, generate a fresh repository registration token, and +register the replacement. Registration tokens and `.runner` contents are +credentials: never commit or print them. A push can still be deployed manually +with `sudo deploy-labyricorn` while the runner is unavailable. + +## Change-control checklist + +Before changing deployment, nginx, TLS, firewall, credentials, or admin access: + +1. Inspect the live state and preserve unrelated configuration. +2. Back up the specific file being changed. +3. Validate syntax before reload or restart. +4. Keep SSH reachable before firewall changes. +5. Verify local origin behavior and public Cloudflare behavior separately. +6. Record operational changes in this README. +7. Commit and push source documentation to `main`. + +This README describes the current verified installation on `ubuntu2-47`. Update +it whenever the operational architecture changes.