This post is the operational guide for edgeblog — how to add a post, edit an existing one, manage drafts, and push the site live. If you’ve forked the repo or you’re a future version of me who’s forgotten the details, this is for you.
The short version
Writing a post is three steps:
- Create
content/posts/YYYY-MM-DD-some-slug.mdwith TOML front-matter and Markdown body. - Preview locally with
fastly compute serve. git commitandgit push. GitHub Actions does the rest.
Total wall-clock time from “I have an idea” to “the world can read it”: about two minutes of CI once the post is written.
File layout and naming
Posts live in a single directory:
content/
└── posts/
├── 2026-04-24-hello-edge.md
├── 2026-04-25-how-i-built-edgeblog.md
└── 2026-04-26-writing-on-edgeblog.md
The filename convention is YYYY-MM-DD-slug.md. The build script strips
the date prefix when deriving the URL, so the file above becomes
/posts/writing-on-edgeblog. The date prefix only exists so that your
filesystem listing is in chronological order; it’s not visible anywhere
else.
If you name a file without the date prefix (e.g. about.md), the whole
filename-stem becomes the slug. Either works.
Front-matter
Every post starts with a TOML block, fenced by +++ on lines by themselves:
+++
title = "Writing on edgeblog"
date = "2026-04-26"
summary = "A user guide for authoring on edgeblog."
tags = ["edgeblog", "guide"]
draft = false
+++
This is the post body, written in Markdown.
The fields:
| Field | Required | What it does |
|---|---|---|
title | Yes | Page <title>, <h1>, feed entry title. |
date | Yes | Sort order (ISO format, YYYY-MM-DD). Shown in the post meta line. Used as published / updated in the Atom feed. |
summary | No | Short description shown in the home-page post list and in the Atom <summary>. Leave empty and those sections collapse. |
tags | No | Array of strings. Displayed as chips under the post title. Not currently linked (tag pages are a TODO). |
draft | No | If true, post only appears when the show_drafts KV flag is on. Drafts are always excluded from feed and sitemap. |
If title or date are missing, the build fails loudly. Everything else
defaults sensibly.
Markdown support
The renderer is pulldown-cmark with these extensions enabled:
- Tables — standard pipe syntax.
- Footnotes —
[^1]in text,[^1]: explanationon its own line. - Strikethrough —
~~like this~~. - Task lists —
- [ ]and- [x]. - Smart punctuation — straight quotes become curly ones,
--becomes an en-dash,...becomes an ellipsis. If you want to avoid this (e.g. in a code block showing literal--), use fenced code — smart punctuation doesn’t apply insidecode spansor code blocks.
Code blocks
Use triple backticks with a language hint for the CSS class, which future syntax highlighting will hook into:
```rust
fn main() {
println!("hello");
}
```
Currently styling is minimal (dark background, mono font); real syntax highlighting is planned for a later turn.
Images
Standard Markdown for now: . In the
next turn I’m adding Fastly’s Image Optimizer, at which point I’ll
replace this with a helper that emits <picture> with AVIF → WebP → JPEG
fallback and automatic resizing.
For now, if you need to include images:
- Drop the file in
assets/images/(create the directory if needed). - Add a line to
main.rsexposing it as a static asset. This is a bit awkward and will go away when IO lands; treat it as temporary.
Links and references
- External links:
[link text](https://example.com)— no special treatment. - Internal links:
[the hello post](/posts/hello-edge)— just use the URL path. - Anchor links: put
<span id="heading-slug"></span>near the heading and link with#heading-slug. The renderer doesn’t auto-slug headings yet.
Drafts
A draft post sets draft = true in the front-matter. It is compiled into
the Wasm binary alongside published posts, but hidden at runtime unless
the show_drafts KV flag is true.
This is intentional. It means previewing a draft in production is a KV flip away, without a redeploy, without exposing the post to crawlers.
Workflow for a draft
- Create the post with
draft = true. - Commit and push. CI builds, deploy publishes.
- Flip the flag:
fastly kv-store-entry create \ --store-id=qi1qyf29g17wcyacup1fr4 \ --key=show_drafts \ --value=true - Wait ~10 seconds for KV propagation, then visit https://blog.alpagot.net/ — the draft appears with a badge.
- When happy, set
draft = false, commit, push. - Flip the flag back:
fastly kv-store-entry create \ --store-id=qi1qyf29g17wcyacup1fr4 \ --key=show_drafts \ --value=false
The reason for step 6 is that once a post is draft = false, it’s public
via the feed and sitemap anyway — keeping show_drafts=true only helps
future unpublished drafts leak.
Drafts are not secret. The post body is in the Wasm binary, which is public the moment it’s deployed. Anyone who knows the URL of a draft can access it directly regardless of the flag. If you need genuine confidentiality, don’t commit the post until you’re ready to publish.
Previewing locally
From the repo root:
fastly compute serve
This compiles the Wasm, boots Viceroy
(the local ABI implementation), and serves on
http://127.0.0.1:7676. Viceroy reads the [local_server] section of
fastly.toml for KV mocks — the two flag values are set to false by
default. Edit them in fastly.toml and restart to preview different flag
states locally.
The --watch flag re-compiles on file change, but the cold re-compile is
slow enough that I usually just restart manually.
For a faster feedback loop during code changes:
cargo check # typecheck only, no link, ~3 seconds on a warm cache
cargo clippy # linting with the same warnings the CI enforces
cargo fmt --all # format everything; CI runs --check, so run this before commit
Publishing
git add content/posts/2026-04-26-writing-on-edgeblog.md
git commit -m "Add writing guide"
git push
GitHub Actions picks up the push. The deploy workflow takes about 1m 15s
warm: build Wasm, upload package, activate new service version, smoke-test.
You can watch it at
https://github.com/alpagot/edgeblog/actions. If the smoke test fails —
which checks /healthz, /, and /feed.xml for 200s — the deploy job
fails but the previous version stays live. Fastly’s service activation is
atomic; there’s no half-deployed state.
Editing an existing post
Edit the .md file, commit, push. The ETag hash in the build-generated
POSTS array changes because the rendered HTML changes, so every browser
gets a fresh copy (their cached If-None-Match no longer matches). The
surrogate key also updates, so any purge of page html or post:<slug>
would invalidate the edge cache — though in practice the new deploy purges
the whole service cache automatically.
If you just want to tweak the date on an existing post (say, to pin it to the top or to correct a typo in the URL):
- Changing the
datefront-matter field changes ordering but not the URL. - Changing the filename changes the URL. If the old URL has been shared, add a redirect — though we haven’t built a redirect mechanism yet. For now, don’t rename files.
Unpublishing
Three ways, in increasing severity:
- Set
draft = true— post is no longer public, but the URL still exists in case you have a direct link stored somewhere. - Delete the
.mdfile and redeploy — post vanishes entirely. Visitors to the URL get a clean 404. - As above, plus purge the feed reader cache — feed readers may have
already cached the entry. Most honour the next Atom update that no
longer contains the entry. You can speed this up by purging the feed
surrogate key:
fastly purge --service-id=SXOpETWAC1hmiiJ57GSNui --key=feed
Maintenance mode
If something is on fire and you want every request to get a “back shortly” page:
fastly kv-store-entry create \
--store-id=qi1qyf29g17wcyacup1fr4 \
--key=maintenance_mode \
--value=true
/healthz stays 200 so uptime monitors don’t fire. Everything else returns
503 with Retry-After: 60.
Turn it off the same way:
fastly kv-store-entry create \
--store-id=qi1qyf29g17wcyacup1fr4 \
--key=maintenance_mode \
--value=false
KV propagation is eventually consistent — a few seconds to see the change globally.
Monitoring a deploy or diagnosing a regression
Live tail the logs:
fastly log-tail --service-id=SXOpETWAC1hmiiJ57GSNui
This streams log::info! output from all production POPs in real time. We
don’t have persistent log streaming set up yet — that’s in a later turn —
but log-tail is perfect for “is this request reaching my handler?”
diagnostics.
The Fastly real-time dashboard at https://manage.fastly.com/observability/dashboard/service/SXOpETWAC1hmiiJ57GSNui shows request rate, status codes, cache hit ratio, and compute time at one-second granularity. Bookmark it.
Things I’m deliberately not doing
- Analytics. No Google Analytics, no Plausible, no anything. Fastly’s dashboards show traffic shape; that’s enough for a personal blog. At least for now.
- Comments. Hosting user input would mean a database, auth, spam filtering, content moderation. Not worth it. If you want to respond, email me. richard [@] alpagot.net.
- Search. Static sites don’t need server-side search when the whole site fits in one Atom feed. I might add a tiny client-side search over the feed later; tab-complete your browser’s “find in page” for the time being.
- Pagination. The post list shows everything. When it gets unwieldy, I’ll add pagination. We’re a long way from unwieldy.
- Categories distinct from tags. One classification axis is plenty.
Things on the roadmap
- Tag index pages —
/tags/rustlisting everyrust-tagged post. - Image Optimizer integration — responsive
<picture>with format negotiation. - Next-Gen WAF — mostly for the demo value since edgeblog has no attackable forms, but the setup is worth documenting.
- Syntax highlighting — build-time with
syntect, emitting pre-styled HTML so no runtime CPU is spent. - Structured log streaming — Better Stack or Datadog free tier, plus a log format that lets me filter by path / status / POP.
- A test suite —
cargo testrunning handler functions under Viceroy so CI can assert behavioural properties, not just “it compiles”.
Each of these is a post in its own right. See you there.