Skip to content

[mirror] Add “Building a federated blog” tutorial (Astro + Bun + SQLite)#6

Open
yashwant86 wants to merge 42 commits intomm-base-695from
mm-pr-695
Open

[mirror] Add “Building a federated blog” tutorial (Astro + Bun + SQLite)#6
yashwant86 wants to merge 42 commits intomm-base-695from
mm-pr-695

Conversation

@yashwant86
Copy link
Copy Markdown

@yashwant86 yashwant86 commented Apr 26, 2026

Mirror of upstream fedify-dev#695 for benchmark. Do not merge.


Summary by MergeMonkey

  • Documentation:
    • Add comprehensive tutorial for building a federated blog with Astro, Bun, and SQLite.
    • Update documentation navigation to include new Astro blog tutorial.
  • Infrastructure:
    • Update deno.json SVG glob pattern configuration.

dahlia added 30 commits April 18, 2026 19:59
Adds the first two chapters of the new "Building a federated blog"
tutorial (docs/tutorial/astro-blog.md):

- Chapter 1 (Introduction): describes what we're building, the
  target audience, and the list of features and limitations.
- Chapter 2 (Setting up): walks through installing Bun and the
  fedify CLI, running fedify init with the Astro + Bun + in-memory
  options, and verifying the project works with fedify lookup.

Also adds the tutorial to the navigation in docs/.vitepress/config.mts
so it appears in the sidebar.

The companion example repository is at:
https://github.com/fedify-dev/astro-blog

Assisted-by: Claude Code:claude-sonnet-4-6
Adds chapter 3 "Building the blog" to docs/tutorial/astro-blog.md.
This chapter walks through:

- Defining a content collection with Astro's Content Layer API
  (defineCollection + glob() loader) and a Zod schema.
- Creating three sample Markdown posts in src/content/posts/.
- Writing a minimal Layout.astro with global CSS that readers can
  copy-paste without needing an external CSS framework.
- Implementing the blog listing page (src/pages/index.astro) using
  getCollection() and the filter/sort pattern.
- Implementing the individual post page using dynamic routing and
  the render() helper.

Also includes two screenshots of the working blog UI taken with
Playwright.

Also excludes .playwright-mcp/ from deno fmt and git tracking.

Assisted-by: Claude Code:claude-sonnet-4-6
…rial

Covers the in-memory key pair store (globalThis trick for Astro HMR),
the actor dispatcher returning a Person with RSA + Ed25519 keys, inbox
and followers stub dispatchers, content negotiation via @fedify/astro
middleware, and the HTML profile page at /users/blog.  Includes a
screenshot of the actor profile page and instructions for verifying with
fedify lookup.

Assisted-by: Claude Code:claude-sonnet-4-6
Covers using fedify tunnel to expose the local dev server publicly,
the X-Forwarded-Proto fix needed so actor URIs use https://, configuring
Vite's allowedHosts for tunnel hostnames, and verifying the actor is
discoverable from Mastodon's search interface.

Assisted-by: Claude Code:claude-sonnet-4-6
Covers Follow/Undo inbox handlers with auto-accept, storing followers in
the in-memory map, returning real follower data from the followers
dispatcher, and displaying the follower count on the home page.
Testing instructions use ActivityPub.Academy.

Assisted-by: Claude Code:claude-sonnet-4-6
Covers migrating the in-memory Map store to bun:sqlite so that key
pairs and followers survive server restarts.  Explains key serialization
(PKCS#8/SPKI via Web Crypto) and the bun-types TypeScript integration.

Assisted-by: Claude Code:claude-sonnet-4-6
Chapter 8 covers the startup post-sync mechanism: compares the Astro
content collection against a SQLite `posts` table and sends
Create(Article), Update(Article), or Delete(Article) activities to
followers.  Also explains the Article object dispatcher and how
@fedify/astro handles HTML/ActivityPub content negotiation on the same URL.

Also adds `twoslash` to all TypeScript code blocks in Chapters 4–7 and
`// @noErrors` where imports cannot be resolved in the VitePress context.

Assisted-by: Claude Code:claude-sonnet-4-6
Covers handling Create/Update/Delete(Note) inbox activities, verifying
the inReplyTo points at a local Article, and displaying stored comments
below each post page.  Includes XSS warning about rendering remote HTML.

Assisted-by: Claude Code:claude-sonnet-4-6
…rial

Chapter 10 covers: HTML sanitization for comment XSS prevention,
Update(Person) for profile changes, Delete(Article) for deleted posts,
image attachments, and Fly.io deployment notes.

Also records the new tutorial in CHANGES.md under the 2.2.0 unreleased
Docs section, referencing issue fedify-dev#691.

Assisted-by: Claude Code:claude-sonnet-4-6
The previous text directed readers to use a Mastodon instance without
specifying one, and the screenshot was taken unauthenticated (so the
searched actor didn't appear in results).  ActivityPub.Academy is a
sandbox Mastodon instance already used in Chapters 6 and 9, so using
it throughout keeps the tutorial consistent.

The screenshot (mastodon-search.png) still needs to be retaken from
ActivityPub.Academy while logged in.

Assisted-by: Claude Code:claude-sonnet-4-6
First-time readers may be confused by the unusual sign-up flow: no email
or password is required, just a checkbox and a button.  Add a brief
explanation and a screenshot of the sign-up page so readers know what to
expect.

Assisted-by: Claude Code:claude-sonnet-4-6
Show what the blog profile looks like on ActivityPub.Academy instead of
a plain-text approximation.

Assisted-by: Claude Code:claude-sonnet-4-6
…API link

Convert three bold paragraph headers (Key serialization, BLOB return type,
Synchronous followers) into a proper list with italic terms and colons.
Also link "Web Crypto API" to its MDN page.

Assisted-by: Claude Code:claude-sonnet-4-6
…Chapter 9

Assisted-by: Claude Code:claude-sonnet-4-6
Briefly explain what Fly.io and Fly Volumes are, and frame the section as
an option rather than an assumption.

Assisted-by: Claude Code:claude-sonnet-4-6
- Add X-Forwarded-Host handling to the middleware code blocks (Chapter 5
  and 8); previously only X-Forwarded-Proto was shown
- Add security.allowedDomains: [{}] to the astro.config.ts code block and
  explain why it is required for X-Forwarded-Host to be trusted
- Remove the ORIGIN env var approach; instead instruct users to open the
  tunnel URL in their browser so the first request already carries the
  correct forwarded headers
- Expand the Chapter 9 "Testing with ActivityPub.Academy" section with
  step-by-step instructions and four new screenshots:
    - activitypub-academy-timeline.png
    - activitypub-academy-reply-box.png
    - activitypub-academy-reply-typed.png
    - post-with-comment.png

Assisted-by: Claude Code:claude-sonnet-4-6
VitePress TOC and heading styles were broken because all chapter headings
were marked as h1 with ==== underlines.  Only the document title should
be h1; chapter-level headings should be h2 with --- underlines, matching
the convention used in docs/tutorial/microblog.md.

Assisted-by: Claude Code:claude-sonnet-4-6
Prevents Hongdown's sentence-case formatter from capitalizing
"bun-types" to "Bun-types" in headings.

Assisted-by: Claude Code:claude-sonnet-4-6
Cross-references like "Chapter 6" are opaque without numbered headings.
Replace each with an italicized, linked section title so readers can
navigate directly to the referenced content.

Assisted-by: Claude Code:claude-sonnet-4-6
- Spell out toLocaleDateString options in the individual post page
  snippet (was shown as { ... } placeholder)
- Correct the limitations list: Update/Delete ARE supported via startup
  sync, just not immediate; clarify accordingly
- Fix Article property description: content is the description wrapped
  in <p> tags, not the full post body
- Fix hashPost to include description alongside title and body; the
  Article's content field is derived from description, so omitting it
  meant description-only changes would not trigger Update(Article)

Assisted-by: Claude Code:claude-sonnet-4-6
- Replace macOS-only `open` shell commands with cross-platform prose
- Fix "attachment" → "attachments" property name in image attachments section
- Clarify that the Mastodon interoperability chapter does include code changes
  (middleware + Astro config updates for tunnel support)

Assisted-by: Claude Code:claude-sonnet-4-6
- Fix source code link to point to fedify-dev/astro-blog instead of
  the fedify examples directory (the tutorial's example project is a
  standalone repo, not under examples/)
- Expand XSS sanitization guidance to explicitly cover the Update(Note)
  handler, not only Create(Note), so readers sanitize both code paths
- Add cross-links to the Deployment manual and the Astro section of the
  Framework integrations manual, as originally specified in issue fedify-dev#691

Assisted-by: Claude Code:claude-sonnet-4-6
The tutorial's in-memory store snippet used `// eslint-disable-line no-var`
on `declare global {}` declarations, but the project uses Biome (not ESLint),
and Biome does not flag `var` inside `declare global {}` blocks anyway since
those are type-only declarations with no runtime effect.

Assisted-by: Claude Code:claude-sonnet-4-6
Activity IDs for Create and Delete activities must be unique across the
lifetime of a blog, not just for a given server run.  The previous
`#create` ID would collide if the same slug were deleted and re-published;
`#delete-${slug}` would collide on a second deletion of the same slug.
Adding `Date.now()` makes each activity ID unique, consistent with the
existing `Update` pattern.

Assisted-by: Claude Code:claude-sonnet-4-6
Two problems with the previous middleware logic:

1. `syncPosts` could fire on the first local request (before any tunnel
   request arrives), causing activities to be sent with localhost URLs
   instead of the public tunnel URL.  Fix: only trigger sync when the
   request includes an `X-Forwarded-Host` header.

2. If `syncPosts` threw, `synced` stayed `true`, permanently suppressing
   future delivery attempts.  Fix: reset `synced = false` in the catch
   handler so the next tunnel request retries.

Assisted-by: Claude Code:claude-sonnet-4-6
- The introduction described posts as "compiled to static HTML at build
  time", but the project uses `output: \"server\"` (no prerendering), so
  posts are actually rendered on the server on each request.  Updated
  both the frontmatter description and the opening paragraph to match
  reality.

- The tunnel NOTE only warned that the URL changes each run; it did not
  mention that after SQLite is introduced, changing the tunnel URL
  without deleting blog.db leaves stale ActivityPub IDs in the database.
  Expanded the NOTE to advise readers to delete blog.db whenever the
  tunnel URL changes.

Assisted-by: Claude Code:claude-sonnet-4-6
`Astro.redirect("/404")` emits a 3xx redirect, so the original URL
receives a redirect status rather than a 404, and the approach silently
relies on a /404 route existing.  Replace it with a direct 404 Response
so crawlers and HTTP clients receive the correct status code without an
extra round-trip.

Assisted-by: Claude Code:claude-sonnet-4-6
dahlia added 12 commits April 19, 2026 02:02
Without checking Follow.objectId, a malicious actor could send
Undo(Follow{object: someoneElse}) to remove an arbitrary follower
from the blog.  Add a guard that compares the undone follow's
object ID against the blog actor URI before acting on it.

Assisted-by: Claude Code:claude-sonnet-4-6
syncPosts() returned immediately when the follower list was empty,
leaving the posts table stale. Posts edited or deleted while follower
count is zero would cause incorrect Create/Update/Delete deliveries
once followers join. Moved the early return inside the sendActivity()
call sites so the DB is always reconciled; outbound activities are
still skipped when recipients is empty.

Assisted-by: Claude Code:claude-sonnet-4-6
The tutorial uses Temporal.Instant from @js-temporal/polyfill but
only relied on it being present transitively through @fedify/vocab.
Transitive dependencies are not guaranteed to stay reachable as
dependency graphs change; added an explicit bun add install step
and listed the package directly.

Assisted-by: Claude Code:claude-sonnet-4-6
Fedify's key validation code (sig/key.ts) throws a TypeError if the
key is not extractable, and it also needs to export the public key
to serialize it in the actor document. Setting extractable: false
breaks federation entirely after the first restart. Reverted both
importKey() calls back to extractable: true.

Assisted-by: Claude Code:claude-sonnet-4-6
Astro.params.slug is typed as string | undefined even after the
404 guard, so passing it directly to getCommentsByPost() produces
a TypeScript type error. post.id is the same value but typed as
string since it comes from the content collection entry.

Assisted-by: Claude Code:claude-sonnet-4-6
Astro 7 will remove the re-export of z from astro:content.
Import z from astro/zod directly as the package itself recommends.

Assisted-by: Claude Code:claude-sonnet-4-6
Vite's dev server does not recognize bun: protocol imports as built-in
modules, so it fails to resolve bun:sqlite when a request first hits
server-side code.  Setting vite.ssr.external tells Vite to skip bundling
this import and let Bun's native module resolver handle it instead.

Assisted-by: Claude Code:claude-sonnet-4-6
- Update fedify init console output to match current prompt order and
  option text (web framework → package manager → message queue →
  key-value store; removes the now-absent runtime question)
- Move the Visual Studio Code section to before the project verification
  steps so readers open their editor before running commands; add
  recommendation for the Astro VS Code extension
- Fix code-highlight range from {9-17} to {9-18} in the astro.config.ts
  example so the full vite block is highlighted
- Add explicit import instructions before the setObjectDispatcher code
  block so readers know to import Article and getCollection

Assisted-by: Claude Code:claude-sonnet-4-6
Fedify CLI 2.1.8 generates Astro+Bun projects using the
@nurodev/astro-bun adapter and bunx --bun scripts instead of
@astrojs/node and plain bunx.  Running Astro under Bun's native
runtime lets Vite resolve bun: imports directly, so the
vite.ssr.external workaround for bun:sqlite added in the previous
commit is no longer needed and has been removed.

Also update the deployment note to refer to the @nurodev/astro-bun
adapter instead of @astrojs/node.

Assisted-by: Claude Code:claude-sonnet-4-6
Astro's hot-module reload does not reliably re-register Fedify's
inbox listeners when src/federation.ts changes.  The first Follow
arriving against a stale federation object gets rejected with
"Unsupported activity type: Follow," even though the code on disk
is correct.  A clean restart rebuilds the federation object and
fixes the issue.

Add a WARNING right after the federation.ts update explaining the
HMR caveat (applying to later chapters too), and a matching
IMPORTANT note at the ActivityPub.Academy testing step so readers
do a clean restart before sending the first Follow.

Assisted-by: Claude Code:claude-sonnet-4-6
The previous wording said \"the LogTape configuration we defined in
Setting up the development environment,\" which suggested the reader
had written it during an earlier step.  In fact, fedify init
generates src/logging.ts with sane defaults, and the setup chapter
never asks the reader to write logging code.  Reword to make it
clear the file was scaffolded by fedify init and drop the misleading
back-link.

Assisted-by: Claude Code:claude-sonnet-4-6
Astro is already defined as a reference link at the top of the
tutorial but was only rendered as plain text on the first Goals
paragraph.  Link it so readers on this page can click through to
astro.build like every other mention.

Assisted-by: Claude Code:claude-sonnet-4-6
@bot-mergemonkey
Copy link
Copy Markdown

bot-mergemonkey Bot commented Apr 26, 2026

Risk AssessmentSAFE · ~15 min review

Focus areas: Code block completeness at lines 844-846, 1353-1355, 1767-1768 · Tutorial accuracy and clarity · Navigation configuration

Assessment: Adds documentation and tutorial content with no code logic changes.

Walkthrough

Users navigate to the new Astro blog tutorial from the documentation sidebar. The tutorial guides them through building a federated blog application using Astro as the frontend framework, Bun as the runtime, and SQLite as the database, with step-by-step code examples and explanations.

Changes

Files Summary
Astro Blog Tutorial Documentation
docs/tutorial/astro-blog.md
Adds comprehensive tutorial documenting how to build a federated blog using Astro, Bun, and SQLite with step-by-step implementation examples.
Documentation Navigation & Metadata
docs/.vitepress/config.mts
CHANGES.md
Registers new Astro blog tutorial in navigation sidebar and updates changelog with documentation additions.
Build Configuration
deno.json
Updates SVG file glob pattern in build configuration.

Dig Deeper With Commands

  • /review <file-path> <function-optional>
  • /chat <file-path> "<question>"
  • /roast <file-path>

Runs only when explicitly triggered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants