Page history and credits on a static blog
Transparent revision logs, attribution as a first-class concern, and why both matter
- Published
- 8 May 2026
- Read time
- 4 min read
Was this useful?
Most blog posts operate on an implicit contract: once published, they don’t change. Or if they do change, the change is invisible. This is fine for minor edits, but when you correct something meaningful — a wrong date, a misattributed quote, broken code — readers who’ve already seen the post have no way of knowing.
This blog has two optional features that address this: a page history log and a credits section. Both are schema-validated fields in the content collection and rendered at the bottom of post pages.
Architecture overview
UI mockup
The wireframe below shows the presentation intent for both metadata surfaces: a timeline-style history block and compact, pill-style credits.
Page history
The history field in a post’s frontmatter is an optional array of revision entries:
history: - datetime: 2026-03-26T00:00:00 note: Initial publish. - datetime: 2026-03-27T12:00:00 note: >- Corrected the HMAC algorithm description — it's SHA-256, not SHA-1.Each entry has a datetime (coerced to a Date by Zod) and a note string.
Notes support inline HTML, so links to related pages and emphasis are possible.
The Zod schema definition:
history: z .array( z.object({ datetime: z.coerce.date(), note: z.string(), }), ) .optional(),PageHistory.astro renders the entries as an <ol> — a chronological list where
each <li> pairs a <time> element with a <span> for the note:
<div class="page-history"> <p class="page-history__heading">Page history</p> <ol class="page-history__log"> {entries.map((entry) => ( <li class="page-history__entry"> <time class="page-history__time" datetime={entry.datetime.toISOString()} > {formatDatetime(entry.datetime)} </time> <span class="page-history__note" set:html={entry.note} /> </li> ))} </ol></div>The <time> element carries the machine-readable ISO 8601 datetime in its
datetime attribute. The human-readable text is formatted with toLocaleDateString
using the en-GB locale.
set:html is used for the note rather than {entry.note} because notes can
contain inline HTML. This is an intentional tradeoff — the content is
author-controlled in a static repository, not user-submitted, so the XSS risk
is the same as any other HTML in the site.
Visually, the history block is rendered at reduced opacity (0.65) and with a left border — it’s clearly secondary information, present for transparency rather than as a primary content element.
Credits
The credits field follows the same pattern — an optional array, validated by Zod,
with label, text, and an optional URL:
credits: - label: Cover image text: Kelly Sikkema on Unsplash url: https://unsplash.com/@kellysikkema - label: Diagram library text: Mermaid url: https://mermaid.js.org/credits: z .array( z.object({ label: z.string(), text: z.string(), url: z.string().url().optional(), }), ) .optional(),PageCredits.astro renders them as a <ul>. Each <li> pairs the label with
either an anchor or a plain <span> depending on whether a URL is present. URLs
use target="_blank" with rel="noopener noreferrer".
Attribution is a first-class concern here, not an afterthought. Every Unsplash cover image has its photographer credited. Libraries and tools that made a feature possible are listed. When a post is directly inspired by another person’s work, that’s acknowledged explicitly. This isn’t just good etiquette — it’s consistent with how I’d want my own work credited.
Both components share the same visual treatment: muted, compact, below the main content and the share widget. They’re there for the reader who cares about the detail, invisible to the reader who doesn’t.
Why both belong in the schema
It would be easy to treat history and credits as presentational concerns — markdown at the bottom of a post, maintained by hand. Putting them in the schema instead means they’re validated on every build, available to any component or page that needs them, and impossible to malform silently. The discipline of typing them enforces consistency: every credit has a label, every history entry has a datetime.
Neither field is required. A post with no meaningful revision history doesn’t need a history block. A post with no external sources doesn’t need credits. The optionality is intentional — adding boilerplate entries just to fill a section would dilute the signal these features are meant to carry.
Working on something similar?
Need help raising the bar?
I help teams improve engineering practice through hands-on delivery, pragmatic reviews, and mentoring. If you want a second pair of eyes or practical support, let's talk.
- Engineering practice review
- Hands-on delivery
- Team mentoring
If this has been useful, you can back the writing with a one-off tip through a secure Stripe checkout.
Free · Practical · One email per post
Get practical engineering notes
One short email when a new article goes live. Useful if you are breaking into tech, growing as an engineer, or improving engineering practice on your team.
Comments
Loading comments…
Leave a comment