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

推荐订阅源

Apple Machine Learning Research
Apple Machine Learning Research
Jina AI
Jina AI
博客园_首页
WordPress大学
WordPress大学
罗磊的独立博客
小众软件
小众软件
Last Week in AI
Last Week in AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Hugging Face - Blog
Hugging Face - Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
爱范儿
爱范儿
The Cloudflare Blog
GbyAI
GbyAI
C
Check Point Blog
腾讯CDC
MyScale Blog
MyScale Blog
有赞技术团队
有赞技术团队
博客园 - 聂微东
IT之家
IT之家
雷峰网
雷峰网
H
Help Net Security
博客园 - 叶小钗
美团技术团队
D
DataBreaches.Net

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.