‹ Back homeTechnology

Inside the implementation: static pages and dynamic services, browser-based writing, reliable publishing, notifications, data, performance, and the boundaries of private content.

Oct 25, 2026·17 min read·Reads …

Contents
  1. 1. Is It Actually a Static Website?
  2. 2. Behind the Browser Editor Is Still a Markdown File
  3. 3. Publish Is a Chain, Not a Button
  4. Progress Belongs to the Task, Not the Page
  5. An Old Task Must Not Replace a New Version
  6. 4. Email Is More Than an Extra Request After Publish
  7. What Does “Sent” Mean?
  8. Idempotency Is Not a Magic Word
  9. Free Allowances Are Not Just Display Numbers
  10. 5. More Data Does Not Just Mean a Longer List
  11. Separate Original Records from Query Views
  12. Every Async Request Can Return Too Late
  13. 6. Performance Starts with Asking What Is Busy
  14. Animation Needs an Ending
  15. Load What the Reader Uses
  16. 7. Private Articles Do Not Hide Everything
  17. Attachments Need Their Own Protection
  18. Private Comments Are Not End-to-End Encrypted Chat
  19. 8. A Backup Is More Than an Archive File
  20. 9. A Small Article Number Was Really a Data-Model Question
  21. 10. None of This Makes Me Want a Huge Platform
  22. Next: Making the Room Feel Alive
  23. Technical References
  24. Open-Source Projects and Third-Party Components
  25. Astro and Official Integrations
  26. Marked
  27. Turndown
  28. js-yaml
  29. resvg-js
  30. Node.js
  31. Python
  32. Git
  33. Cloudflare
  34. GitHub and GitHub Actions
  35. Resend
  36. Design Reference
  37. This Project and AI Assistance

Chapter One recorded the journey from an idea to a real website.

It deliberately left out the technical detail. Before discussing frameworks, I wanted to preserve why I started and why I kept improving something that repeatedly felt wrong.

This chapter takes another angle.

How does writing reach the reader’s screen? What happens after Publish? Why can the admin appear successful while the public page remains old? Why can a background made of a few colors spin up a computer’s fans?

Those questions explain the project better than a list of technologies.

Henry’s Room did not begin with a complete architecture. Concrete problems gradually taught it to separate content, state, data, and responsibility.

The account below reflects the implementation through October 24, 2026. Flows are simplified for explanation, not a complete deployment tutorial or a claim that every situation has been verified.

1. Is It Actually a Static Website?

If we mean the public articles and main pages, yes.

Astro generates HTML at build time. Cloudflare Pages distributes it. Reading an article does not require an application server to query a database and assemble its body for every visitor.

But “static website” is incomplete as a description of the whole system.

Comments must persist. Subscriptions require email verification. The admin reads and edits articles. Notifications need coordination. HTML files alone do not do those jobs.

The fuller description is:

A static-first reading front end, plus dynamic services for interaction and administration.

Area Technology Responsibility
Content and pages Astro, Markdown Generate readable articles and site information
Static delivery Cloudflare Pages Serve pages, styles, scripts, and public assets
Interaction and admin Cloudflare Workers Comments, subscriptions, counts, administration, task coordination
Business records Cloudflare KV Comments, subscription relationships, notification and task state
Admin queries Optional D1 projection Rebuildable views for filtering, sorting, and pagination
Versions and releases GitHub, GitHub Actions Content history, tests, builds, deployment
Email Resend Accept verification and notification requests

This is not a choice between entirely static and entirely dynamic. Work follows its nature.

Article bodies change relatively infrequently and can be generated ahead of time. A comment is a live interaction and belongs behind an API. Reading history and bookmarks serve the current device, so they stay in the browser without a new cloud account system.

There are trade-offs.

Editing the home-page copy in the admin does not instantly change every visitor’s page. It still needs a commit, build, and deployment. Avoiding a traditional server does not remove provider failures, quotas, or maintenance.

Static-first removes one kind of complexity. It does not remove complexity itself.

2. Behind the Browser Editor Is Still a Markdown File

At first I only needed a way to put articles online.

Later I wanted browser writing, previews, images, and persistent drafts without handling the repository each time.

The articles did not move wholesale into a new database. Their source remains Markdown in Git.

The editor turns writing operations into content. The authorized admin API writes it to GitHub. Marked and Turndown connect the Markdown and visual modes.

An article contains metadata as well as its body:

---
title: "An unfinished article"
description: "Record the question first; organize the answer later."
pubDate: 2026-10-24
category: "技术"
tags: ["实践", "记录"]
cover: c3
draft: true
private: false
---

Here, draft: true is not a decorative badge.

It affects build-time filtering. A file must not enter public lists, search, feeds, or body routes merely because it exists in the repository.

Three states matter:

  • A temporary local draft: helps recover unsaved edits; not a durable backup.
  • A saved article draft: committed to the repository, but not public.
  • A public article: available after the publication chain completes.

Calling all three “saved successfully” is convenient until something goes wrong.

Markdown and Git also mean that writing exists outside one editor. A later front-end replacement would still have reasonably organized content and history.

That does not permit careless conversion. Rich text, code, video, and custom attachments each have structure. Switching modes needs checks for lost content. Looking approximately the same is not proof of preservation.

3. Publish Is a Chain, Not a Button

A recurring bug was “the admin says success, but reality has not caught up.”

The list said published, the progress bar kept running, and the public article was absent. After leaving and returning, progress disappeared while the misleading published label remained.

The cause was treating several different successes as one success.

The current chain can be simplified as:

Editor submits
    ↓
Article is committed to GitHub; a SHA identifies the version
    ↓
The matching GitHub Actions run tests and builds
    ↓
Cloudflare Pages deploys that version
    ↓
The exact version is confirmed live
    ↓
If requested, subscriber notifications continue

A commit SHA identifies the version precisely.

The admin must not ask whether the repository has any recent successful build. It must ask:

Has the version I just submitted completed its own publication?

Otherwise a previous success can be mistaken for the current task.

Uploading a file is not deploying a site. A provider accepting a deployment request is not proof that readers can reach the content.

States are now reconciled around the specific commit. Query failures, missing runs, and builds still in progress must not become “live” merely because a timer expired.

Progress Belongs to the Task, Not the Page

Closing a page should not erase a publication task.

Local recovery state and recent cloud publication records let the admin reconcile work after navigation or on another device.

Placing progress beneath the corresponding article follows the same principle: it belongs to that publication, not a global decorative busy indicator.

Some phases have a reliable percentage. Others have only a state. Those should say “building” or “awaiting verification,” not grow toward 100% according to a clock.

An Old Task Must Not Replace a New Version

An earlier build may finish later.

The deployment flow rechecks the remote main branch before release. A superseded version should not overwrite production.

That does not automatically settle every related email task. Notifications still need their own reconciliation.

A useful publishing UI distinguishes saved, building, live, failed, and awaiting verification. It does not exist to deliver good news at all times.

4. Email Is More Than an Extra Request After Publish

Comments, replies, subscriptions, and friend links turned email into a workflow of its own.

The clearest failure is a subscriber receiving a valid email for an article that is not yet online. Sending succeeded; the product failed.

The current order is therefore: persist the notification intent, wait for the matching deployment, then process sending.

It cannot depend on the browser issuing one more request after saving. The browser may close.

Comment and reply notifications also need the specific record, template, and content for that operation. Retries must not quietly become a different letter or send stale information after a record changes.

What Does “Sent” Mean?

Here it means that the provider returned a successful acceptance response.

It does not prove delivery to the inbox, rather than spam, or that the recipient read it. Full delivery and bounce webhooks were not integrated at this point, so they cannot be described as present.

Retries also need limits.

Temporary timeouts and rate limits may justify bounded retry. Permanent errors, old tasks, exhausted attempts, and interrupted bulk sends need a visible stop and manual review.

Idempotency Is Not a Magic Word

Roughly, idempotency means repeating a request should not repeat its effect.

The implementation reuses a stable request identifier and content for a mail task to reduce duplicate sends. But KV does not provide a cross-instance, strongly consistent task lock, and provider idempotency has its own scope and time limits.

I will not describe this as “duplicate email is impossible under every failure.”

Implement the protections that are possible, and state the limits that remain.

Free Allowances Are Not Just Display Numbers

One comment email is small. Verification, moderation, replies, and broadcasts add up.

The admin needs allowance details, reset dates, and alerts. Alert capacity must be reserved before ordinary notifications consume the last available send.

Queue checks should not run at maximum frequency either. The scheduler distinguishes idle from active periods, reducing empty scans and responding more actively when tasks exist.

That controls workload within a free-first approach. It does not eliminate quotas or processing delays, and internal budgets are not provider guarantees.

5. More Data Does Not Just Mean a Longer List

With a handful of comments, subscribers, or friend applications, fetching everything and filtering in the browser seems convenient.

At larger volumes:

  • Opening a page reads too much.
  • Search may cover only the current page.
  • Filter counts may not represent the real total.
  • Failed reads can masquerade as empty lists.

The needed change is not ten rows becoming fifty. It is the read model.

The admin moved toward server-side filtering, pagination, and complete-index checks. A cold index reports progress rather than presenting a partial scan as a full result.

“No records” and “records could not be read” are different states.

Separate Original Records from Query Views

Comments and subscription relationships remain in KV. D1 is an optional administration projection, not a wholesale replacement of those records.

Think of it as a catalog:

  • Original records are the material to preserve.
  • A query projection is a view for finding and sorting that material.
  • A damaged catalog can be rebuilt. Its failure does not make the originals nonexistent.

Sensitive tokens do not belong in a search projection merely because it makes listing convenient.

Caches need the same discipline. They can accelerate reading but must not become a mysterious new authority.

The implementation checks projection versions and age, invalidates views after writes, and returns to the original records when required. Failures and budget exhaustion need defined fallback or explicit failure.

There is no atomic transaction spanning KV and D1, and no promise that every location reads the same state instantly.

Acknowledging those constraints is more useful than saying “we added a database.”

Every Async Request Can Return Too Late

An old request can arrive after I change filters and overwrite the new list.

Likewise, article reads or decryption results must not reopen a discarded editor after logout or navigation.

Cancel obsolete work or check which operation owns the result. Finishing late does not preserve the right to update the UI.

A loading icon alone cannot solve that.

6. Performance Starts with Asking What Is Busy

Two symptoms are easy to confuse:

  1. A page is slow to arrive.
  2. A page is open, but the computer continues working hard.

The first may involve DNS, routing, asset origins, caching, or response times. The second calls for investigation of animation, painting, scripts, and background work.

Without measurement, neither Cloudflare nor the visitor’s computer can be blamed categorically.

I wanted a breathing background: stronger colors, natural movement, varied paths, no dots or rings inside the patches.

Each visual refinement could add continuous rendering work. Most visitors, however, were simply reading.

The eventual direction was not “no motion.” It was “respond when something meaningful happens.”

Animation Needs an Ending

An entrance can be brief while content geometry remains stable. Controls can respond without every card shifting. Observers and polling should stop when their page or function is no longer active.

Reduced-motion preferences matter. Content cannot depend on animation to become readable; failed scripts must not leave text transparent.

The home-page twitch was a reminder that adding a duration is not enough. Initial geometry, fonts, final positions, and timing all affect the result.

Load What the Reader Uses

A reader who never opens the admin should not download its editor. A complex feature should not make every page prepare all its code.

The project separates public interactions, admin modules, and private-content handling, loading them when needed and enforcing project budgets for scripts, styles, and media.

The budget is not a universally correct number. It makes the cost of future features visible.

Likewise, media should load, decrypt, or play after the reader requests it, not merely because it exists in markup.

Build and isolated-browser checks support these changes. They do not replace long-session power measurements or every physical phone and tablet.

“No issue found in this check” is not “no device will ever lag.”

7. Private Articles Do Not Hide Everything

The original requirement was direct: no password, no article body.

A password overlay over plaintext HTML would not satisfy it.

The implementation derives a key in the browser with PBKDF2-SHA256 and encrypts the body with AES-GCM. Published files contain ciphertext. Readers decrypt locally; the article password is not uploaded in plaintext.

But the protected scope must remain explicit:

The body is private. The title, summary, date, category, tags, and cover remain public.

Information that should not be public cannot be placed in the summary and left for the body password to protect.

Password strength matters because static ciphertext can be downloaded for offline guessing. noindex is a request to search engines, not access control or encryption.

Attachments Need Their Own Protection

Encrypting the body does not make an existing public image URL private.

Private attachments are encrypted locally with random, independent AES-256-GCM keys. Only ciphertext is uploaded. Keys, filenames, and media types stay in the encrypted article body.

At this point, the per-attachment limit was 8 MiB. That is an implementation boundary, not an assertion that arbitrary file sizes suit this approach.

Even after unlocking, a reader explicitly requests a download and decryption. Relocking or leaving cleans up players and temporary object URLs.

Previously public resources and downloaded copies cannot be recalled. An authorized reader can still copy, screenshot, or download.

Private Comments Are Not End-to-End Encrypted Chat

Private discussions require credentials associated with the article and thread, and must not leak into public lists.

Comments and notification emails still pass through the server and remain manageable by the author. This is not a system readable only by the two people conversing.

Older comments without ownership credentials cannot be rebound by guessing merely to make them reappear. Review tools exist, but historical attribution still requires the author.

Explaining the boundary is part of the security design.

8. A Backup Is More Than an Archive File

Once business records and notification tasks exist, Git no longer represents the entire website.

Source and articles may be in the repository. Runtime configuration, business records, and recovery-sensitive task state are not all restored by having the repository.

The backup process exports relevant data and configuration, verifies integrity, and encrypts the result. The repository holds a public backup key; the private key needs separate care.

At least four questions remain:

  1. Did the workflow finish?
  2. Is the encrypted artifact available, downloadable, and decryptable?
  3. Are records and required metadata complete?
  4. Does restoring to an isolated environment produce the expected result?

One success does not prove the next.

The project has performed offline verification and a recovery rehearsal into fresh, isolated KV, checking values, metadata, and expiration information before removing temporary resources.

That is not restoring over production and does not make production overwrites automatically safe.

Notification state is particularly important. Restoring records must not reactivate old broadcasts, caches, and stale operational tasks. Recovery must distinguish preserved data from runtime state that needs isolation or rebuilding.

Code rollback and data recovery are also different.

An earlier page version may fix an interface. It does not recall an email or undo a previous record mutation.

To make rollback useful, know what is being restored, what is not, and how to verify unrelated work remains intact.

9. A Small Article Number Was Really a Data-Model Question

Cover numbers originally followed page position.

The same article could have one number in the full list and another after filtering. Its apparent identity changed with its location.

Now the number belongs to the article.

Historical numbering uses available dates, relevant Git order, and stable identifiers. New articles follow the first non-draft commits on main. Deleted numbers are not reused. Changing a title, date, or list position does not assign a new identity.

Drafts do not consume numbers, and the author does not need to enter one manually.

There is an important limitation: article dates and Git history do not prove the exact earliest Cloudflare publication timestamp of every old piece.

The initial ordering uses evidence the project has. It does not invent missing logs.

A detail that appears to belong only to the UI may reveal an unresolved data model.

10. None of This Makes Me Want a Huge Platform

There are many technical terms here, but complexity was not the goal.

Readers do not need accounts. Local reading history means no automatic cross-device sync. Static generation means waiting for builds. Free-first hosting means quotas, delays, and bounded degradation.

Some issues are still not settled.

Old private comments and formerly public resources require author review. Future session and credential changes are separate work. Long-term power use, browser differences, and actual email delivery remain questions that tests cannot permanently close.

These limits are part of the system, not details to hide at the end.

If Chapter One asked why I kept building, this chapter answers:

A long-lived website needs more than a happy path. Failures must be visible, states trustworthy, data owned, and future changes possible.

AI makes interfaces appear faster. It does not make the facts behind them true automatically.

Those facts still require reading code, checking documentation, inspecting outcomes, and separating implementation from verification.

Henry’s Room is not a universal architecture. It is a set of choices for my writing and readers. Scale and requirements may change them.

The technical work matters when content is not trapped, reading is not distracted, publishing is understandable, and recovery remains possible.

Chapter One ended with responsibility. Here I want to make that concrete:

Do not let the page promise what the system cannot yet do.

Stay curious. Keep making progress.

Next: Making the Room Feel Alive

Once the system grew steadier, I returned to the page itself: a tool-guided redesign that I rejected, motion discovered through repeated attempts, and details that technically worked but still felt wrong.

If this chapter concerns maintaining a website, the next concerns making it somewhere I actually want to stay.

Continue with Chapter Three: From Working to Feeling Right.


Technical References

The site’s description follows its implementation and verification records. These official references explain underlying behavior, not provider guarantees for this project:

Open-Source Projects and Third-Party Components

These credits distinguish actual upstream work from this site’s own implementation.

Astro and Official Integrations

  • Projects: Astro, @astrojs/rss, @astrojs/sitemap.
  • Authors: Fred K. Schott, the Astro team, and contributors.
  • Source: withastro/astro.
  • License: MIT.
  • Use: page generation, content collections, RSS, and sitemaps.

Marked

Turndown

  • Author: Dom Christie and contributors.
  • Source: mixmark-io/turndown.
  • License: MIT.
  • Use: visual content to Markdown conversion.

js-yaml

  • Authors: Vitaly Puzrin, Nodeca, and contributors.
  • Source: nodeca/js-yaml.
  • License: MIT.
  • Use: build-time article metadata and stable numbering.

resvg-js

  • Maintainers: the project and contributors.
  • Source: thx/resvg-js, the destination of the former yisibl/resvg-js repository.
  • License: MPL-2.0.
  • Use: SVG-to-bitmap share images.

Node.js

  • Maintainers: the Node.js project and contributors.
  • Website: nodejs.org.
  • License: MIT for the core, with separate third-party notices in the official license.
  • Use: builds, tests, and tooling.

Python

  • Maintainers: Python Software Foundation and contributors.
  • Website: python.org.
  • License: primarily PSF License Version 2.
  • Use: deployment guards, backup verification, and recovery automation.

Git

  • Maintainers: the Git project and contributors.
  • Website: git-scm.com.
  • License: GPL-2.0.
  • Use: source and writing history.

Transitive dependencies have their own versions, copyright, and licenses. This is not a complete software bill of materials or a single license applied to all components.

Cloudflare

  • Provider: Cloudflare.
  • Source: cloudflare.com.
  • Services: Pages, Workers, Workers KV, D1, Turnstile, and associated network capabilities.
  • Note: hosting and security services, not original site code. Actual account and provider terms govern quotas and charges.

GitHub and GitHub Actions

  • Provider: GitHub.
  • Source: GitHub Actions documentation.
  • Use: repositories, history, tests, and deployment.
  • Note: the service, execution environment, and individual Actions are distinct; each Action retains its own license.

Resend

  • Provider: Resend.
  • Source: Resend documentation.
  • Use: subscription verification, moderation, replies, and other notification email.
  • Note: provider acceptance, delivery, and reading are different states.

Design Reference

Apple products inform the whitespace, hierarchy, and interaction order. This is not an Apple product and has no official affiliation or endorsement.

This Project and AI Assistance

Henry initiated and maintains the project. OpenAI Codex assisted development and article preparation. Assistance does not change upstream attribution or licensing and does not replace verification.

The repository was private at writing, with no project-wide open-source license. Dependencies being open source does not grant redistribution rights to all site code, writing, or media.

This article contains no passwords, tokens, backup keys, subscriber addresses, or visitor data.


Project and article by Henry. Implementation recorded through October 24, 2026.

Previous: Chapter One: From an Idea to a Real Website.

H
Henry

Notes on technology, design, and the slower observations of everyday life.

About the author
DISCUSSION

Comments 0

Leave a thought or add to the conversation. Comments appear after moderation.

Loading comments…