This repository is the pnpm/Turborepo monorepo for duyet.net public apps, shared packages, data sync jobs, and Cloudflare Pages/Workers deploys.
GitHub repo metadata (description + topics) must match the current stack. When migrating frameworks, update both in the same change: description should name TanStack Start, Cloudflare Workers/Pages, and Rust/WASM; topics should include tanstack-start, cloudflare-workers, cloudflare-pages, vite, wasm, rust, blog, duyet, monorepo (and drop stale ones such as nextjs). gh repo edit / the GitHub settings UI is required — topics are not stored in-repo.
- Read
CLAUDE.mdand this file before making non-trivial changes. - Use semantic commit messages. Prefer a scope from
.commitlintrc.jswhen one exists. - Keep changes surgical. Do not reformat or refactor unrelated code.
- Preserve existing UX and public routes unless the request explicitly changes them.
- Use pnpm commands from the relevant package or app directory. Check the local
package.jsonbefore assuming a script exists. - Verify with the narrowest useful command first, then broaden only when needed.
- If a linked worktree reports
Operation not permittedunder.git/worktrees/..., use the canonical checkout aftergit status --short --branch; stage only touched paths so unrelated local edits stay out of commits. - Public marketing/content routes stay fully static: HTML is produced at build/prerender time. Do not add runtime-required data fetches to those routes. Chat widgets may hydrate, but they must not turn the host page into a client-only shell.
- Shared chrome lives in
packages/components/site-header/as small units (AppSwitcher,GlobalNav,LocalNav,MobileNav,ThemeButton) composed bySiteHeader. Do not growSiteHeader.tsxback into a 800-line file. - UI primitives come from latest shadcn/ui under
packages/components/ui/(registry stylenew-york-v4). Chat conversations use the official June 2026 set:MessageScroller,Message,Bubble,Attachment,Marker. Compose them viaChatTranscript/ChatMessageListinpackages/components/chat/. Do not invent a parallel chat kit. - Ignore generated Next dumps:
.next/,out/,next-env.d.ts. Do not commitapps/agents/scratch or leftover agent worktrees. - Secret scanning:
.gitleaks.toml(custom AnyRoutersk-ar-v1-prefix). CI workflowgitleaks.ymlscans the working tree with--no-gitso historical leaks do not fail the gate. Do not rewrite git history for leaked keys; rotate the live credentials instead. Distinct from.deepsec/(SAST). - Clerk pin: keep
pnpm.overrides["@clerk/shared"]on 3.47.8 (and RenovateallowedVersions: "<4") while apps still depend on@clerk/clerk-react5.x /@clerk/backend2.x. Those packages declare@clerk/shared ^3.47. A v4 override makes Vite/rolldown fail withMISSING_EXPORTforClientContext/OrganizationProvider/SessionContext/UserContextand takes downapps/blogPages deploys. Do not bump shared to v4 until those apps move to@clerk/react6.x.
When this session has HERDR_ENV=1 and the user asks for a Herdr worktree / parallel agent:
- Do not run bare
herdr(that attaches the TUI). Useherdr worktree,herdr agent, orherdr --skill. herdr worktree createonly creates a git worktree + shell pane. It does not start a coding agent.- Required follow-up — parse
.result.root_pane.pane_id, then start and prompt:
CREATE=$(herdr worktree create --cwd "$PWD" --branch feat/<name> --base origin/master --no-focus)
PANE=$(printf '%s' "$CREATE" | jq -r '.result.root_pane.pane_id')
herdr agent start feat-<short> --kind cursor --pane "$PANE"
herdr agent prompt feat-<short> "<full task spec>" --wait --timeout 600000- Agent names:
[a-z][a-z0-9_-]{0,31}, unique among live agents. Prefer--kindmatching the user’s agent (Cursor →cursor, Grok →grok, etc.). - Herdr 0.8
agent startworks (agent_started/interactive_ready). Stopping afterworktree createis the idle-shell failure mode, not a Herdr regression.
apps/blog,apps/home,apps/cv,apps/insights,apps/photos,apps/kb,apps/llm-timeline,apps/ai-percentage,apps/burns,apps/homelab,apps/x-algo,apps/tipemit static HTML for public pages at build time (Vite/TanStack prerender or Pages output).apps/agent-uiis a signed-in chat surface athttps://agents.duyet.net. Itsindex.htmlmust still contain a prerendered chat shell (Ask Duyet anything.). Conversation rows use shared shadcn chat primitives; Clerk/auth and streaming stay client-only.apps/insightsis static HTML plus calls toapps/api. Do not use TanStack Start server functions for runtime data loading.
pnpm run buildbuilds all apps and packages through Turbo.pnpm run devstarts workspace development servers through Turbo.pnpm run lintruns Biome linting.pnpm run fmtformats TypeScript, TSX, and Markdown through Biome.pnpm run testruns Turbo tests.pnpm run configruns workspace config tasks, including app secret syncs where defined.pnpm run deploybuilds deployable apps, then runs workspace config.pnpm run cf:deploydeploys changed Cloudflare Pages apps (same discovery as CI).pnpm run cf:deploy:prodruns production Cloudflare deploy tasks through Turbo.pnpm run cf:deploy -- --forcebypasses git-based change detection and rebuilds all requested Cloudflare Pages apps.pnpm run wasm:buildbuilds cdylib crates to WASM viascripts/wasm-build.ts+wasm-pack. Binary crates (duyet,duyet-cli) are excluded.duyetdepends onrustls→ring→getrandom 0.2, whichcompile_error!s onwasm32-unknown-unknownwithout thejsfeature. Do not enable that feature workspace-wide; the CLI is not a WASM target. A workspace-widecargo build --target wasm32-unknown-unknownis what broke blog Pages CI after #1450.pnpm run wasm:build:releaseis the same graph with optimizations. Pages CI (cf-deploy.yml,cf-deploy-preview.yml) runs this before every app build.pnpm run wasm:testrunscargo testacross the Rust workspace.pnpm run wasm:clippylints Rust code.pnpm run bench:wasmbenchmarks TS vs WASM for all modules.
apps/home: homepage forhttps://duyet.net, deployed to Cloudflare Pages.apps/blog: statically prerendered Vite blog forhttps://blog.duyet.net, Auth0 auth, Vercel KV comments, Markdown posts with KaTeX.apps/cv: CV host forhttps://cv.duyet.net.apps/insights: analytics dashboard forhttps://insights.duyet.net, using Cloudflare Analytics, GitHub, PostHog, WakaTime, ClickHouse, and TanStack Start prerendering.apps/photos: photo gallery forhttps://photos.duyet.net, Unsplash and Cloudinary-related workflows.apps/homelab: homelab docs and resources forhttps://homelab.duyet.net.apps/llm-timeline: LLM release timeline forhttps://llm-timeline.duyet.net, with sync, RSS, sitemap, and llms.txt generation.apps/agent-ui: Cloudflare Pages chat UI forhttps://agents.duyet.net. Clerk auth, AI SDKuseChat,@duyet/componentsChatTranscript(MessageScroller / Message / Bubble / Attachment / Marker) againstapps/agent-api.apps/agent-api: API-only Cloudflare Agents Worker. REST chat isPOST /api/v1/chatwith Clerk bearer auth orAGENT_API_TOKEN. The production hostnameagents-api.duyet.netis bound in the Cloudflare dashboard; theroutesblock inapps/agent-api/wrangler.tomlstays commented sowrangler deploydoes not require Zone DNS edit permission.apps/api: Hono API on Cloudflare Workers forhttps://api.duyet.net.POST /api/contact{name, email, message}: 8 KB body cap, 5 per IP per 10 min,websitehoneypot (non-empty is a silent 202), stored in D1SUBMISSIONS_DBaspending, owner notified via theNOTIFY_EMAILsend_email binding.POST /api/jd{company?, note?, text | url}: 40 KB envelope cap withtext<= 32 KB or an httpsurl, same 5-per-IP-per-10-min limit, honeypot,SUBMISSIONS_DBrow, andNOTIFY_EMAILnotification.POST /api/comments{post, author, email?, body}: 8 KB cap,postmust match a slug inblog.duyet.net/posts-data.json(cached 1 h, unknown slug is 404), same limit, honeypot,SUBMISSIONS_DBrow, andNOTIFY_EMAILnotification.
apps/ai-percentage: AI-written-code dashboard forhttps://ai-percentage.duyet.net; data comes fromapps/data-sync.apps/data-sync: operational CLI for ClickHouse analytics/activity syncs and migrations.apps/agent-assistant: Vite-powered TanStack Start application with assistant-ui + LangGraph serving as a local agent interface. Deployed directly serverless on Cloudflare Workers/Pages (duyet-agent-assistant) forhttps://agent-assistant.duyet.netutilizing a native Cloudflare Durable Object (ThreadStateDO) backed by SQLite for checkpoint persistence.apps/kb: static TanStack Start knowledge base forhttps://kb.duyet.net, bundlingcontent/**/*.mdat build time and generatingllms.txt,llms-full.txt,sitemap.xml,robots.txt, and rawpublic/k/*.mdarticle endpoints.apps/burns: Vite/Cloudflare Pages dashboard (duyet-burns) that prerenders Claude Code usage stats.buildrunsscripts/fetch-burns-data.ts, which pullsccusagedata from MotherDuck (MOTHERDUCK_TOKEN); a daily cron refreshes it. Preview CI uses the GitHubstagingenvironment secret (synced from local.env.production.local). If the token is missing, preview/local builds keep committedpublic/token-data.json; master/cron/manual deploys still require the token.apps/x-algo: static TanStack Start explainer for the open-sourced X For You ranking weights athttps://x-algo.duyet.net. Numbers live insrc/lib/scoring.tsand must matchxai-org/x-algorithmhome-mixer/params/param.rs.apps/tip: Ko-fi tip page on Cloudflare Pages forhttps://tip.duyet.net(Pages projectduyet-tip). A single centered section embeds the official Ko-fi widget; widget URL lives insrc/lib/site.ts. Keep the sharedSiteHeader/SiteFooterchrome and the prerendered static HTML.apps/paid-api: standalone x402 USDC-gated chat Worker forhttps://paid.duyet.net(duyet-paid-api). Payment replaces auth; seeapps/paid-api/README.md.apps/news-tab: Chrome MV3 unpacked new-tab extension forhttps://news.duyet.net. Unzip/news-tab.zipthen Load unpacked thenews-tabfolder that containsmanifest.json— never the.zipitself. Not a Worker/Pages app — nowrangler.toml. FetchesGET /api/public(and, with host_permissions,GET /api/feed?days=3for category/trending/tags). The new-tab UI should match the live homepage AI;DR (layout A: two-column numbered list,topic-coloredhighlights, 40–48px thumbs), not a parallel cream digest. The news Worker build packs that unpacked tree tohttps://news.duyet.net/news-tab.zip(not committed; not the monoreporeleases/latesttag). Guide:/extension. Own release-please component (news-tab-v0.1.x,apps/news-tab/CHANGELOG.md); do not fold into the rootduyetrelease. Humans merge release-please PRs; never auto-merge them.manifest.jsonversiontracks that component via extra-files (monorepo series starts at 0.1.0, not the old standalone 1.0.1).
packages/components: shared React chrome (site-header/), shadcnui/primitives, and official chat components (chat/).packages/libs: shared utility functions.packages/interfaces: shared TypeScript interfaces.packages/config: shared app, API, and UI config.packages/tailwind-config: shared Tailwind config.packages/tsconfig: shared TypeScript config.packages/profileandpackages/urls: shared profile and URL metadata.
- Cloudflare Pages production deploys happen on pushes to
masterormain; PRs receive preview deploys. - Pages CI (
cf-deploy.yml,cf-deploy-preview.yml) discovers deployable apps at runtime viascripts/cf-pages-apps.ts: anyapps/*withpages_build_output_dirinwrangler.tomland acf:deploy:prodscript is included. Do not hardcode the app list in the workflow. New Pages apps (for examplekb) deploy automatically when their tree orpackages/**changes. - A scheduled daily job (cron
0 0 * * *in.github/workflows/cf-deploy.yml) rebuilds and redeploys theburnsapp to refresh its prerendered stats from MotherDuck. - Pages vs Worker topology, lockfile-based rebuilds, and the agent-ui ↔ agent-api race:
docs/ai/deploy-topology.md. - Deploy workflows run type checks, tests, and lint before deploy jobs.
- App-level
cf:deploy:prodscripts are authoritative when present. apps/agent-uideploysdist/clientto theduyet-agentsPages project forhttps://agents.duyet.net.apps/insightsdeploysdist/clientto theduyet-insightsPages project.apps/apiuses Wrangler as a Worker, not a Pages app.apps/agent-apiuses Wrangler as a Worker, not a Pages app.apps/agent-assistantcompiles via Vite/TanStack Start into a unified Worker + Assets bundle and deploys to theduyet-agent-assistantproject. Features an automated deployment processor (deploy.ts) that injectsThreadStateDOSQLite schemas and patches browser-incompatiblecreateRequireandimport.meta.urlhooks in compiled server chunks.- Cloudflare Workers Cache (
[cache] enabled = true) is a Workers-only wrangler key — it is not valid in a Pages config (pages_build_output_dir). It is set on the three real Workers (api,agent-api,agent-assistant) as a safe no-op: they only cache responses with an explicitCache-Control: public. The static Pages apps (agent-ui,blog,cv,home,photos,insights,ai-percentage,homelab,burns,kb,llm-timeline,x-algo,tip) do not get[cache]; their public cache policy lives in each app'spublic/_headers(zone cache + deploy purge). Seedocs/ai/workers-cache.mdfor the per-app matrix, TTL rationale, and how to extend.
apps/agent-ui:pnpm run devuses Vite on port 3008; local chat expectsapps/agent-apion port 8788 unlessVITE_DUYET_AGENTS_API_URLorVITE_AGENT_API_URLis set. Production chat uses the same-origin/api/v1/chatPages Function proxy so browsers do not need to resolveagents-api.duyet.net.apps/agent-api:pnpm run devuses Wrangler on port 8788;pnpm run deploytype-checks then deploys the Worker;pnpm run cf:deploy:prodloads production env files before deploy;pnpm run configsyncsAGENT_API_TOKENplus Clerk verification secrets.apps/api:pnpm run devuses Wrangler;pnpm run deploybuilds then runsscripts/deploy.ts. That helper omits[[d1_databases]]from the production config whendatabase_idis not a UUID, so CI does not auto-provision D1 by name. Afterwrangler d1 create duyet-api-submissions, put the printed UUID inapps/api/wrangler.tomland the same deploy appliesSUBMISSIONS_DBmigrations remotely.apps/cv:pnpm run previewvalidates production output locally.apps/data-sync: usepnpm run sync <name>,pnpm run sync:all,pnpm run migrate:*, andpnpm run cleanup:dry-run.apps/kb:pnpm run buildruns the content prebuild and must preserve articlelinksfrontmatter inpublic/k/*.mdfor knowledge-graph and LLM consumers.apps/llm-timeline: usepnpm run sync,pnpm run sync:dry,pnpm run rss,pnpm run llms-txt, andpnpm run sitemapfor content generation.apps/ai-percentage: refresh data throughapps/data-syncwithpnpm run sync ai-code-percentage.
Rust crates in crates/ serve two purposes:
- Build-time: Native CLI binary (
duyet-cli) for data sync and prerender - Runtime: WASM modules for browser/CF Workers (diff, exif, utils, markdown)
Two binaries share the duyet name and must not be confused. duyet-cli (crates/cli) is the build-time JSON stdin/stdout tool consumed by callCli(); it is unchanged by the CLI program. duyet (crates/duyet) is the user-facing CLI for readers and agents from epic #1440: clap command tree, --json envelope {"ok","schema","data"|"error"}, exit codes 0/1/2/3/4/5/6/10 in one ExitCode enum, TOML config with no secrets under the OS config dir, doctor, completions, man pages, and generated crates/duyet/docs/reference.md. Later-slice commands are present with --help and exit 2 with the tracked issue number. Contracts live in crates/duyet/README.md; drive it with .cursor/skills/verify-duyet-cli/SKILL.md. Cargo.lock is committed because duyet ships as a binary; test.yml caches on it.
pnpm run rust:build— build native CLI binary (target/release/duyet-cli)pnpm run wasm:build— build cdylib runtime crates to WASM (-peach crate withcrate-typecdylib; never--workspace)pnpm run wasm:test—cargo testacross workspacepnpm run wasm:clippy— clippy wasm32 for the workspace exceptduyetandduyet-clipnpm run bench:wasm— run TS vs WASM benchmarks
| Crate | Mode | Function | Consumer |
|---|---|---|---|
crates/cli/ |
Build | Unified CLI: csv, normalize, dedup, markdown subcommands |
JS wrappers via callCli() |
crates/duyet/ |
User CLI | duyet binary: version, config, doctor, completions, docs; content/chat/submission/update groups stubbed until #1443/#1445/#1447/#1448 |
Readers and agents (released in #1444/#1446) |
crates/markdown/ |
Both | markdown_to_html(input) -> String |
WASM for per-page, CLI for batch |
crates/csv-parser/ |
Build | parse_csv(input) -> String |
apps/llm-timeline/lib/csv.ts |
crates/normalizers/ |
Build | 8 normalize functions | apps/llm-timeline/lib/normalizers.ts |
crates/dedup/ |
Build | merge_all_sources(input) -> String |
apps/llm-timeline/lib/deduplicator.ts |
crates/exif/ |
Runtime | extract_exif(data: &[u8]) -> String |
apps/photos/lib/exifExtractor.ts |
crates/diff/ |
Runtime | diff_text, align_blocks |
currently no app consumer after the agents UI removal |
crates/utils/ |
Runtime | escape_reg_exp, slugify |
packages/libs/string.ts |
The duyet-cli binary reads JSON from stdin, writes JSON to stdout:
echo '{"input":"a,b\\n1,2"}' | duyet-cli csv
# {"ok":true,"data":[["a","b"],["1","2"]]}
echo '{"input":[{"fn":"normalize_date","args":["Q1 2024"]}]}' | duyet-cli normalize
# {"ok":true,"data":["2024-01-01"]}JS wrapper: import { callCli } from "@duyet/libs/native-cli"
WASM modules require initSync() before use. In Node/Bun:
import { initSync, extract_exif } from "@duyet/wasm/pkg/exif/exif.js"
import { readFileSync } from "node:fs"
const wasmPath = new URL("exif_bg.wasm", import.meta.url)
initSync({ module: readFileSync(new URL(wasmPath).pathname) })In browser/CF Workers, use the default async export instead of initSync.
Type declarations for WASM modules are in packages/wasm/types.d.ts (committed to git, since pkg/ is gitignored).
WASM modules require initSync() before use. In Node/Bun:
import { initSync, normalize_date } from "@duyet/wasm/pkg/normalizers/normalizers.js"
import { readFileSync } from "node:fs"
const wasmPath = new URL("normalizers_bg.wasm", import.meta.url)
initSync({ module: readFileSync(new URL(wasmPath).pathname) })In browser/CF Workers, use the default async export instead of initSync.
| Module | TS mean | WASM mean | Speedup | Verdict |
|---|---|---|---|---|
| markdown-to-html | 6.3ms | 0.08ms | 79x | WASM wins — heavy compute |
| diff-text (line-level) | 0.11ms | 0.11ms | ~1x | Parity |
| string-utils | 0.14ms | 0.14ms | ~1x | Parity |
| exif-parse | 0.001ms | 0.001ms | ~1x | Parity |
| csv-parse | 0.08ms | 0.16ms | 0.5x | WASM slower → now uses native CLI |
| normalizers | 0.01ms | 0.04ms | 0.23x | WASM slower → now uses native CLI batch |
| dedup | 0.01ms | 0.04ms | 0.29x | WASM slower → now uses native CLI |
Rule: WASM only outperforms TS when compute > JS↔WASM boundary cost (~30-40μs).
- Native CLI (build-time): csv, normalizers, dedup. No boundary cost. Batch mode for normalizers.
- WASM (runtime): markdown (79x win), diff, exif, utils. Used in browser/CF Workers.
- Native binary markdown: CLI provides batch mode for pre-generation, but per-page prerender keeps WASM (3900 spawns would be slower).
commitlint.config.js extends @commitlint/config-conventional only. There is no scope-enum rule, so any scope (or none) is accepted as long as the type and subject follow Conventional Commits.
Prefer a scope from this informal list when one fits: deps, post, blog, cv, home, insights, photos, travel, auth, ci, ui, rust, docs, lib, agents, llm-timeline, kb, burns, api. Use no scope when none of those fit. This list is documentation, not an enforced allowlist.
This file is the root internal knowledge base for AI agents. Keep AGENTS.md and root CLAUDE.md as short entrypoints. Move durable duplicated workflow details here as docs are rewritten.
For Claude Cowork / desktop-agent sessions (non-terminal use), see docs/ai/cowork-instructions.md. For writing blog posts and notes in Duyet's voice, see docs/ai/writing-style.md.
This section is the source of truth for public-app UI. Root DESIGN.md is a shorter companion note for the same direction; if the two disagree, follow this section.
The current public-app visual direction is a Websmith-inspired Duyet system, not a literal clone. Keep Duyet content, routes, data loading, auth, keyboard behavior, and app-specific workflows intact. Copy the design language only: quiet editorial layout, warm surfaces, compact cards, restrained borders, and mobile-safe wrapping.
- Use a white or warm off-white page background. Preferred warm base:
#f8f8f2; white is acceptable for dense data or photo-heavy apps where the user explicitly prefers it. - Use near-black foreground text, usually
#1a1a1aor#1f1f1f, never low-contrast gray for primary content. - Use Inter-first typography for refreshed apps:
Inter, ui-sans-serif, system-ui, sans-serif. If an app already has a deliberate serif/display pairing, keep it only when it serves that app. - Keep headings tight but not oversized. Desktop heroes should feel confident, not billboard-sized. Use around
text-4xltotext-5xlfor primary app heroes, smaller for utility dashboards. - Keep body text relaxed and readable: mostly
text-smandtext-base; avoid giant feature-card copy. - Use a compact radius system:
8pxto12pxfor buttons, panels, cards, inputs, and screenshots. Avoid pill-shaped cards unless the existing control is a badge or status chip. - Primary controls should usually be black or near-black rounded rectangles with white text. Secondary controls are white/warm panels with a single thin border.
- Accent/status orange can use
oklch(70.5% .213 47.604)or a close orange. Use it sparingly for status dots, highlights, or active marks, not as a full-page theme. - Pastel panels should be soft and varied, not a one-hue palette: light blue, emerald, red/coral, stone, and pale orange panels are preferred. Avoid purple-blue gradients as the main theme.
- Prefer sticky, minimal headers with identity, a small route group, and a status indicator.
- Use left-aligned editorial heroes. The first viewport should clearly identify the app or topic without a marketing splash page.
- On laptop and larger screens, use 3+ columns where content naturally supports it. Keep the cards smaller and more relaxed than the original bento refresh.
- Use two-column grids only for large screenshots, long-form editorial sections, or when card content needs width.
- Keep section rhythm simple: full-width bands or unframed constrained sections. Do not nest cards inside cards.
- Cards should be compact: small title, short supporting text, one metadata/status row, and modest padding (
p-4orp-5; rarelyp-6). - Use simple borders (
border-black/10,border-stone-200, or tokenized equivalents), very light shadows if any, and no decorative gradient blobs/orbs. - For visual app showcases, use existing local screenshots/assets. Do not copy Websmith logo, copy, or imagery.
- Test at a real mobile width around 390px and at tablet/laptop widths before finishing visual work.
- No horizontal overflow. Check
document.documentElement.scrollWidth - document.documentElement.clientWidth. - Clamp drawers, sheets, and side panels with
w-[min(360px,calc(100vw-2rem))]or an equivalent max-width pattern. - Long titles, locations, URLs, and metric labels need
min-w-0,break-words,truncate, or hidden decorative dividers on small screens. - Composer or sticky bottom controls must reserve matching scroll padding so content does not sit underneath them.
- Responsive grids should usually be
grid-cols-1, thensm:grid-cols-2ormd:grid-cols-2, thenlg:grid-cols-3or more when the content is compact.
apps/home: editorial homepage with sticky minimal header, oversized but not huge left-aligned hero, relaxed 3+ column project grid on laptop, pastel service tiles, compact black CTAs, and large footer/contact rhythm.apps/agent-ui: keep this a small signed-in chat surface foragents.duyet.net; it should callapps/agent-apiand not duplicate agent logic.apps/agent-api: keep this surface API-only foragents-api.duyet.net. Preserve/api/v1/chat,/agents/ChatAgent/:sessionId, Clerk bearer auth, andAGENT_API_TOKENsupport.apps/blog: keep white background preference. Use compact home cards and mobile-safe archive rows; avoid the old large shared-card padding in 3-column contexts. Newsletter capture hydrates as a small Subscribe button in the post hero plus an inlineGet updatescolumn in the post footer grid (series / related / changelog). Hide that footer column when the row already has 3 cells. It POSTs tohttps://news.duyet.net/api/subscribeand must not turn the host page into a client-only shell.apps/news: mailing list lives in D1subscribers. Daily digest stays automated. Custom notes are composed at/mail(Clerk admin) with templates + AI wrap; HTML is Cursor-like (520px, Inter, black CTA). Do not add a third-party ESP. Cross-originPOST /api/subscribeCORS must be handled insrc/server.ts(handleSubscribeCors) before TanStack Start; the SPA fallback swallows OPTIONS. Public third-party digest isGET /api/public(slim TL;DR + top stories, no auth);handlePublicCorsin the same Worker entry allowschrome-extension://and localhost Origins so the in-repo extension can fetch it. Do not send/api/feedto those clients as the primary path — it is ~360KB and has no CORS. The Chrome new-tab client isapps/news-tab(unpacked MV3, no wrangler).pnpm --filter news buildzips that tree topublic/news-tab.zipsohttps://news.duyet.net/news-tab.ziptracks the deployed site;/extensionis the load-unpacked guide (unzip first, then pick thenews-tabfolder — Chrome cannot Load unpacked the zip file). Hourly ingest is theNewsIngestSchedulerDurable Object alarm plus a GitHub Actions watchdog (.github/workflows/news-ingest.yml); do not add a Worker[triggers]cron (Free 5-cron cap) or Workflowschedules(paid). GitHub scheduled workflows on this repo skip for hours; do not treat Actions SUCCESS as "ingest finished" —POST /api/admin/ingestonly creates a Cloudflare Workflow instance.apps/insights: keep dashboard density. Use the shared warm/near-black tokens and compact operational panels rather than a landing-page composition.apps/insights: architecture is static HTML frontend plus backend Worker API calls. Do not use TanStack Start server functions for runtime data loading; client-side data refreshes should callapps/apiWorker endpoints.apps/photos: keep the photo-first white background. Text metadata such as location must truncate or wrap safely.
The project implements several standards to enable AI agent discovery and capabilities natively:
/auth.md: Serves human-readable and machine-readable agent authentication instructions./.well-known/api-catalog: Lists standard machine-readable links for APIs (status, description, and docs) conforming to RFC 9727./.well-known/oauth-protected-resource: Configures resource server metadata (RFC 9728)./.well-known/oauth-authorization-server&/.well-known/openid-configuration: Serves authorization server metadata including theagent_authblock for auth.md onboarding./.well-known/mcp/server-card.json: Exposes the MCP Server Card configuration for Model Context Protocol integration./.well-known/agent-skills/index.json: Publishes the skills discovery index conforming to Agent Skills Discovery RFC v0.2.0.
- Link Response Headers: Served on the homepage (
/and/*.html) pointing to/.well-known/api-catalog(rel="api-catalog") and/auth.md(rel="describedby"). - Markdown Content Negotiation: Served via Cloudflare Pages middleware (
apps/home/functions/_middleware.ts) which intercepts requests withAccept: text/markdownand serves/llms.txtwith correct MIME types and token estimates. - WebMCP: Exposes browser-level actions to agents by registering
search_projectsandnavigate_tovianavigator.modelContext.provideContext()/registerTool()inside the React root layout (apps/home/src/routes/__root.tsx).
Add the following HTTPS / SVCB DNS records to Cloudflare DNS for DNS-based agent discovery:
_index._agents.duyet.net. 3600 IN HTTPS 1 . alpn="index" port=443 mandatory=alpn,port_a2a._agents.duyet.net. 3600 IN HTTPS 1 agents-api.duyet.net. alpn="a2a" port=443 mandatory=alpn,port