Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/syhily/yufan.me/llms.txt

Use this file to discover all available pages before exploring further.

yufan.me follows a strict five-layer architecture with a one-way import graph that prevents coupling between SSR, client, and shared code. Each layer has a defined set of modules it may import from; violations are caught by the linter and enforced in code review. This separation ensures that server-only code never leaks into client bundles, and that isomorphic utilities stay free of environment-specific dependencies.

Five source layers

src/
├── routes/      Route modules (public/, auth/, admin/)
├── server/      SSR-only: infra/, domains/, http/, render/
├── client/      Browser hooks, oRPC client, browser-only code
├── ui/          Pure-props React components (public, admin, shadcn, PortableText renderer)
├── shared/      Isomorphic config, contracts, DTO types, PT schema, utils
├── assets/      Fonts, icons, global CSS (Tailwind v4)
└── server.ts    Hono entry / SSR adapter
There are no barrel index.ts files anywhere in the codebase. Every import targets a specific module path, keeping bundle analysis tractable and preventing accidental cross-layer leakage (bundle-barrel-imports).

Import rules

These rules are strict and enforced across the entire codebase:
LayerMay import fromMust NOT import from
routes/*Any layer; route components must accept plain props
server/*shared/*, other server/*client/*, ui/*
client/*shared/*, ui/*, client/*server/*, .server.* files
ui/*shared/*, ui/*, client/*server/*, .server.* files
shared/*shared/* onlyEverything else

The server/ sub-tree

The server/ directory is itself organized into four layers with a strict one-way import order (infra → domains → http, domains → render → http):
server/
├── infra/      # Technical primitives — zero business knowledge
├── domains/    # Self-contained business modules (one folder per domain)
├── http/       # HTTP perimeter (oRPC + Hono)
└── render/     # SSR output products (HTML, RSS/Atom, OG, calendar, avatar, SEO)

infra/

Pure primitives with zero business knowledge. Contains the Drizzle pool, Redis storage (unstorage + ioredis), generic HTTP vocabulary (etag, headers, status, errors with DomainError / ActionFailure), email sender, search client, env.ts, logger.ts, rate-limit.ts, and the slug derivation pipeline. infra/ imports nothing from domains/, http/, or render/.

domains/

One folder per business domain — 15 in total: analytics, auth, backup, comments, content, friends, images, music, pages, posts, pt, settings, taxonomies, users, and audit. Each domain has a locked file vocabulary:
FileResponsibility
schema.tsDrizzle table definitions
repo.tsRaw database queries
service.tsBusiness logic
projection.tsData transformation for API responses
cache.tsRedis caching strategy
Domains may import from shared/, infra/, and other domains/. Business logic stays inside service.ts; controllers and loaders orchestrate only.

http/

The HTTP perimeter. Contains the Hono entry (app.ts), oRPC procedure base (orpc-base.ts), composed router (api-router.ts), error hook (errors.ts), and OpenAPI export (openapi.ts). Sub-directories:
  • middlewares/ — session, CSRF, install-gate, rate-limit, trailing-slash, visitor-cookie, wp-decoy, RBAC
  • controllers/ — per-domain <name>.controller.ts; admin controllers under controllers/admin/
  • resources/ — native Hono routes for non-JSON output: feed, sitemap, images, redirects, analytics events
  • loaders/ — React Router data orchestrators: detail, listing, search, comments, sidebar, pagination, revalidate
Controllers and loaders orchestrate only — no business rules live here.

render/

SSR output products that never persist. Produces strings, Buffers, or Responses; caching is the caller’s responsibility. Includes: seo/ (meta tags), feed/ (RSS/Atom with PortableText feed renderer), og/ (Open Graph images), calendar/ (SVG generation), avatar/ (Gravatar/QQ fetcher with Redis cache), react-prerender.ts, and image post-processing helpers.

The PortableText pipeline

Posts and pages are stored as PortableText JSON arrays in Postgres JSONB columns. The schema is defined in @/shared/pt/schema with Zod validation, making it the single wire format for both authoring and rendering. The Tiptap editor in the admin console converts ProseMirror state to and from PortableText via @/shared/pt/bridge — a single file that maps standard blocks to Tiptap built-ins, and custom blocks (image, code, mathBlock, mermaid, musicPlayer, solution, footnoteDefinition, table) to a generic blockCard node. Round-trip fidelity is contract-tested. On the server, @portabletext/react renders PortableText to HTML during SSR. Custom block components live in @/ui/pt/blocks/ and handle images, syntax-highlighted code (Shiki), math (KaTeX), Mermaid diagrams, and the music player.

Tech stack

LayerChoice
App routerReact Router 7 framework mode, SSR (react-router.config.ts)
HTTP hostHono via react-router-hono-server — perimeter middlewares, resource routers, oRPC mount
APIoRPC (@orpc/server + @orpc/client) at /rpc/*, Zod input/output, OpenAPI export in dev
UIReact 19, TSX only, shadcn/ui (Base UI variant) under src/ui/components/
StylingTailwind CSS v4 (src/assets/styles/tailwind.css), one token cascade for public + admin
EditorTiptap (ProseMirror) ↔ PortableText bridge; SSR via @portabletext/react
DataPostgres (Drizzle), Redis (sessions, rate limits, generated-image caches)
AssetsS3-compatible bucket, opt-in per blog
BuildVite+ (vp) — Vite, Rolldown, Vitest, Oxlint, Oxfmt (viteplus.dev)

Build docs developers (and LLMs) love