惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
Netflix TechBlog - Medium
I
InfoQ
Engineering at Meta
Engineering at Meta
Jina AI
Jina AI
Recent Announcements
Recent Announcements
T
The Blog of Author Tim Ferriss
P
Proofpoint News Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
D
Docker
Microsoft Security Blog
Microsoft Security Blog
宝玉的分享
宝玉的分享
Last Week in AI
Last Week in AI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
GbyAI
GbyAI
博客园 - Franky
博客园 - 聂微东
Microsoft Azure Blog
Microsoft Azure Blog
博客园 - 叶小钗
酷 壳 – CoolShell
酷 壳 – CoolShell
B
Blog RSS Feed
WordPress大学
WordPress大学
MyScale Blog
MyScale Blog
月光博客
月光博客
罗磊的独立博客

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Page history and credits on a static blog
Roger Rajara · 2026-05-14 · via DEV Community

Roger Rajaratnam

Original post: Page history and credits on a static blog

Series: Part 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

Mermaid diagram

Diagram fallback for Dev.to. View the canonical article for the full version: https://sourcier.uk/blog/page-history-credits

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

Diagram fallback for Dev.to. View the canonical article for the original SVG: https://sourcier.uk/blog/page-history-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.

Enter fullscreen mode Exit fullscreen mode

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(),

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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/

Enter fullscreen mode Exit fullscreen mode

credits: z
  .array(
    z.object({
      label: z.string(),
      text: z.string(),
      url: z.string().url().optional(),
    }),
  )
  .optional(),

Enter fullscreen mode Exit fullscreen mode

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.