One-off tip

Support checkout

  1. 1 Choose amount
  2. 2 Payment
  3. 3 Thank you

Choose amount

Pick the level of support that feels right. You can keep it simple or enter a custom amount, then continue to secure payment.

Choose a one-off amount
Page history and credits on a static blog
Cover image: An at symbol is seen on a reflective surface.
astro engineering meta

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
SeriesPart of How this blog was built: documenting every decision that shaped this site.

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.

Wireframe mockup showing the page history timeline and credits chip list rendered below a blog post, including annotation callouts for semantic tags and styling intent

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:

src/content.config.ts
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:

src/components/PageHistory.astro
<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/
src/content.config.ts
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
Get guidance

If this has been useful, you can back the writing with a one-off tip through a secure Stripe checkout.

Comments

Loading comments…

Leave a comment

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.