diff --git a/.gitignore b/.gitignore
index 634a57be8..7021fe7e7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@ drizzle/migrations
.tanstack-start/build
.nitro/*
.netlify/*
+.wrangler/*
/build/
/api/
diff --git a/README.md b/README.md
index d747b14ef..f168f491e 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
# TanStack.com
-The home of the TanStack ecosystem. Built with [TanStack Router](https://tanstack.com/router) and deployed automagically with [Netlify](https://netlify.com/).
+The home of the TanStack ecosystem. Built with [TanStack Router](https://tanstack.com/router) and deployed on [Cloudflare Workers](https://workers.cloudflare.com/).
diff --git a/docs/cloudflare-workers-migration.md b/docs/cloudflare-workers-migration.md
new file mode 100644
index 000000000..c8df74cea
--- /dev/null
+++ b/docs/cloudflare-workers-migration.md
@@ -0,0 +1,87 @@
+# Cloudflare Workers Migration Report
+
+TanStack.com is configured as a Cloudflare Workers deployment for this branch. Netlify is no longer part of this site's hosting configuration, but Netlify remains available in builder/deploy-provider UX for users who choose it.
+
+## Files Changed
+
+- `package.json`, `pnpm-lock.yaml`: Cloudflare build/preview/deploy scripts, `@cloudflare/vite-plugin`, `wrangler`, and removal of Netlify hosting packages.
+- `vite.config.ts`: Cloudflare Vite plugin, Worker build constants, opt-in image transformation flag, and server builder-generation disabled for Worker size.
+- `wrangler.jsonc`: Worker name/account, assets binding, `nodejs_compat`, CPU limit, cron triggers, and production `SITE_URL`.
+- `src/server.ts`, `src/server/scheduled.server.ts`: Worker `fetch` and `scheduled` entrypoints, replacing former Netlify scheduled functions.
+- `src/server/runtime/host.server.ts`, `src/utils/hosting-cache.server.ts`: host cache purge adapter using Cloudflare cache-tag purge.
+- `src/components/OptimizedImage.tsx`, `src/utils/optimizedImage.ts`: host-neutral optimized image helper with Cloudflare image transformations behind an explicit build flag.
+- `src/routes/api/builder/*`, `src/components/builder/*`: builder deploy/download path uses browser-generated files; direct server-generation endpoints return explicit 501 on Workers.
+- `src/routes/*`, `src/utils/*`, `src/server/*`: CDN cache headers moved from Netlify-specific headers to portable `CDN-Cache-Control` / `Cache-Tag`.
+- Removed hosting-only Netlify files: `netlify.toml`, `netlify/functions/*`, `scripts/run-built-server.mjs`.
+
+## Commands Used
+
+```bash
+pnpm install --lockfile-only
+pnpm run test:tsc
+pnpm run build:cloudflare
+pnpm test
+pnpm run deploy:cloudflare
+pnpm run preview:cloudflare -- --host 127.0.0.1 --port 3001
+```
+
+Additional checks used `curl` and Playwright with system Chrome against the Workers preview URL.
+
+## Worker
+
+- Account: `8da95258a9c70b54c3e2b374a0079106`
+- Worker: `tanstack-com`
+- URL: `https://tanstack-com.thetanstack.workers.dev`
+- Current version: `ee22ac5c-2681-4b55-acbc-d11ee0adacfc`
+- Upload size: `14872.99 KiB` raw, `4804.71 KiB` gzip
+- Startup time: `27 ms`
+- Note: the secret-bearing `tanstack-com-staging` Worker was renamed to `tanstack-com`, and the older empty `tanstack-com` Worker was removed.
+
+## Passed
+
+- `pnpm run build:cloudflare` passed.
+- `pnpm run test:tsc` passed.
+- `pnpm test` passed with 10 existing oxlint warnings.
+- Cloudflare deploy passed.
+- Local Cloudflare preview started and returned 200 for `/` and `/builder`.
+- `/` returned 200 HTML on the Worker.
+- `/start/latest` returned 200 HTML on the Worker.
+- Browser SPA navigation from `/` to `/start/latest` did not reproduce the `npm-recent-downloads ... data is undefined` error.
+- `/builder` returned 200 HTML with COOP/COEP headers and loaded the client builder/integration surface.
+- Primary homepage images loaded in browser on the Worker.
+- `/api/og/query.png?title=Query&description=Smoke` returned a valid 1200x630 PNG.
+- `/_a/gtag.js` returned Google's JavaScript.
+- `/_a/g/collect` returned 204.
+- `/auth/github/start?returnTo=/account` redirected to GitHub with secure state/return cookies.
+- `/.well-known/oauth-authorization-server` returned OAuth metadata.
+- `/api/mcp/` returned the expected unauthenticated JSON-RPC auth error instead of a runtime failure.
+- `POST /api/application-starter/resolve` returned a Start recipe.
+
+## Failed Or Not Proven
+
+- Direct server-side builder generation endpoints return 501 on Workers:
+ - `/api/builder/features`
+ - `/api/builder/compile`
+ - `/api/builder/compile-attributed`
+ - `/api/builder/download`
+ - `/api/builder/validate`
+ - `/api/builder/feature-artifacts`
+- Full GitHub OAuth callback/account login was not completed.
+- End-to-end GitHub repository deploy was not completed with a logged-in account.
+- Cron trigger behavior was deployed but not manually invoked.
+
+## Builder Generation Note
+
+The released `@tanstack/create@0.68.3` `edge` import is Worker-runtime compatible, but it still statically imports the generated create manifest. That manifest made the Worker upload `11222.23 KiB` gzip, over the paid 10 MiB Worker script limit.
+
+The deployable compromise in this branch keeps dynamic generation in the browser for the builder UI, downloads, and GitHub deploy handoff, and excludes server-side create generation from the Worker bundle. After that change, the Worker upload is `4804.51 KiB` gzip.
+
+## Image Transformation Note
+
+Cloudflare image transformations are disabled by default because `/cdn-cgi/image/*` returned 404 on the Worker preview URL before custom-domain image resizing was proven. Set `TANSTACK_IMAGE_TRANSFORMATIONS=true` during build only after Image Resizing works on the routed `tanstack.com` zone.
+
+## Readiness
+
+Core marketing SSR, docs/start navigation, security headers, static assets, analytics proxying, GitHub auth start, MCP auth rejection, application-starter API, scheduled Worker registration, Cloudflare preview, deploy, and dynamic OG image generation are working on Cloudflare Workers.
+
+Production migration is close, but not fully safe until logged-in OAuth/account flows, cron jobs, and an authenticated builder GitHub deploy are verified. The biggest remaining product parity decision is whether direct server-side builder generation APIs must be supported on the Worker; supporting them requires a smaller create manifest/runtime from `@tanstack/create` or a separate generation service.
diff --git a/docs/perf/lighthouse-shim-vs-react-2026-04-20.md b/docs/perf/lighthouse-shim-vs-react-2026-04-20.md
deleted file mode 100644
index fa6508ba1..000000000
--- a/docs/perf/lighthouse-shim-vs-react-2026-04-20.md
+++ /dev/null
@@ -1,85 +0,0 @@
-# Lighthouse: @tanstack/dom-vite shim vs. real React
-
-**Date:** 2026-04-20
-**tanstack.com commit at time of measurement:** `fb806bb` (shim build with `@tanstack/dom-vite@0.1.0-alpha.5`, pulling `@tanstack/react-dom@0.1.0-alpha.4` — includes the RSC deferred-hydration adoption fix landed the same day).
-**React baseline build:** same source tree with `tanstackDom()` plugin removed from `vite.config.ts` and `serverVariantAliases` dropped — i.e. stock `react@19.2.3` / `react-dom@19.2.3`.
-
-## TL;DR
-
-- **Performance score: parity** (±2 across pages / form factors, within run-to-run noise).
-- **FCP: consistent shim win** everywhere — ~4% on home, ~11–17% on docs / blog. Smaller main-thread parse cost lets first paint land sooner.
-- **LCP: shim regresses on RSC-heavy pages, desktop** — the LCP element on docs / blog pages lives in the Flight-streamed subtree, and the shim's `use(pendingPromise)` + deferred-resume adds latency vs. React's battle-tested RSC client. Mobile is mostly parity (network-bound anyway).
-- **TBT / CLS: effectively zero on both** after the same-day RSC hydration fix — no duplicate DOM, no layout shift from appending.
-- **Bundle (raw JS): −4.7%** on tanstack.com (-980 KB of 21 MB total client JS). Modest because router / store / app code dominates; shim only replaces React's share.
-
-## Methodology
-
-1. `pnpm build` for each variant.
-2. `PORT=4000 pnpm start:prod` to serve from `dist/server/server.js` on `http://localhost:4000`.
-3. **5 trials × 3 URLs × 2 form factors = 30 Lighthouse runs per variant** using `npx lighthouse` v13 with `--only-categories=performance` and headless Chrome.
-4. Mobile runs use Lighthouse's default emulation (slow 4G + 4× CPU slowdown). Desktop uses `--preset=desktop` (no throttling).
-5. Medians reported below.
-
-## Medians
-
-### Performance score
-
-| URL | form | React | Shim | Δ |
-| --------------------------------------------------- | :-----: | ----: | ---: | --: |
-| `/` | desktop | 99 | 99 | 0 |
-| `/` | mobile | 87 | 88 | +1 |
-| `/query/latest/docs/framework/react/guides/queries` | desktop | 96 | 96 | 0 |
-| `/query/latest/docs/framework/react/guides/queries` | mobile | 64 | 66 | +2 |
-| `/blog/react-server-components` | desktop | 98 | 96 | −2 |
-| `/blog/react-server-components` | mobile | 70 | 71 | +1 |
-
-### Web Vitals
-
-| URL | form | FCP (R → S) | LCP (R → S) | TBT (R → S) | CLS (R → S) | TTI (R → S) |
-| ------------------------------- | :-----: | :------------------: | :------------------: | :----------------: | :---------: | :-----------: |
-| `/` | desktop | 0.61s → 0.59s (−4%) | 0.84s → 0.91s (+8%) | 0ms → 0ms | 0 → 0 | 0.84s → 0.92s |
-| `/` | mobile | 2.34s → 2.31s | 3.71s → 3.60s (−3%) | 19ms → 20ms | 0 → 0 | 5.54s → 5.55s |
-| `/query/.../queries` | desktop | 1.05s → 0.92s (−13%) | 1.05s → 1.24s (+18%) | 0ms → 0ms | 0 → 0 | 1.05s → 1.24s |
-| `/query/.../queries` | mobile | 4.66s → 4.13s (−11%) | 6.62s → 6.39s (−3%) | 17ms → 19ms | 0 → 0 | 8.36s → 8.41s |
-| `/blog/react-server-components` | desktop | 0.90s → 0.74s (−17%) | 0.90s → 1.29s (+43%) | 0ms → 0ms | 0 → 0 | 0.90s → 1.29s |
-| `/blog/react-server-components` | mobile | 3.73s → 3.21s (−14%) | 5.32s → 6.23s (+17%) | 34ms → 21ms (−37%) | 0 → 0 | 6.24s → 6.57s |
-
-### Bundle size (uncompressed total client JS)
-
-| Build | Total client JS | Notes |
-| ---------- | --------------: | ---------------------------------------------------------------------------------------------------- |
-| Real React | 21,052 KB | Dedicated `react-*.js` chunk = 176 KB (`manualChunks` splits `node_modules/react{,-dom}/`) |
-| Shim | 20,072 KB | No dedicated react chunk; shim code inlines into `app-shell` (+16 KB there). Net **−980 KB (−4.7%)** |
-
-## Caveats
-
-- **Lab data only.** Chrome origin-level CWV (CrUX) needs ~28 days of real traffic before aggregates stabilize. Since the shim only went live on `2026-04-20`, field data won't be comparable for a month.
-- **`pnpm start:prod` serves from Node locally — no CDN.** Absolute TTFB numbers are dev-machine noise (5ms–1s range depending on cold-cache loader work); anchor on client-side metrics.
-- **Per-page LCP percentages can look dramatic when the absolute value is small.** Blog desktop LCP `0.90s → 1.29s` is +390 ms — real, but a sub-second LCP regression in both states is still a Core Web Vitals "Good" rating (<2.5s).
-- **Single-node prod server — no edge, no warm cache.** Mobile Lighthouse runs with 4× CPU throttling are inherently high-variance.
-
-## Reproduce
-
-```bash
-# React baseline
-# 1) temporarily remove tanstackDom() plugin + serverVariantAliases in vite.config.ts
-pnpm build
-PORT=4000 pnpm start:prod &
-# run 5 trials × 3 URLs × 2 form factors, save JSON to ./react/
-
-# Shim
-# 2) restore tanstackDom() plugin + serverVariantAliases
-pnpm build
-PORT=4000 pnpm start:prod &
-# re-run, save JSON to ./shim/
-
-# Aggregate medians + delta (parse JSON, compute median of numericValues per audit key)
-```
-
-See the shim side for the runner + aggregator scripts used (`/tmp/lh-compare/run.sh`, `/tmp/lh-compare/aggregate.mjs` at measurement time).
-
-## Related shim work shipped with this comparison
-
-- `@tanstack/react-dom@0.1.0-alpha.4`: `renderFunction`'s deferred-hydration branch now mirrors `renderLazy`'s ancestor-Suspense guard (`_awaitingLazyHydration`). Fixes duplicate-markup on RSC pages. Regression test: `tests/rsc-hydration-adopt.test.tsx`.
-- `@tanstack/react-dom-server@0.1.0-alpha.4`: shell-chunk batching in `streamHtml` (reduces Node stream overhead ~3–4% on SSR bench).
-- `@tanstack/dom-vite@0.1.0-alpha.5`: dep bump to pick up react-dom@alpha.4.
diff --git a/docs/performance-plan-home-library-docs.md b/docs/performance-plan-home-library-docs.md
deleted file mode 100644
index 32b505bd2..000000000
--- a/docs/performance-plan-home-library-docs.md
+++ /dev/null
@@ -1,216 +0,0 @@
-# Homepage, Library Landing, and Docs Performance Plan
-
-## Goal
-
-Make the homepage, library landing pages, and docs materially faster, lighter, and cheaper to render without regressing content quality or navigation UX.
-
-## Current Baseline
-
-- Homepage route chunk: `dist/client/assets/index-Bq0A5jmY.js` at `564.79 kB / 172.14 kB gzip`
-- Shared shell chunk: `dist/client/assets/app-shell-BikUtTEO.js` at `349.55 kB / 110.08 kB gzip`
-- Search modal chunk: `dist/client/assets/SearchModal-Bl-tUxqr.js` at `195.54 kB / 54.27 kB gzip`
-- Docs shell chunk: `dist/client/assets/DocsLayout-Bga1-HA9.js` at `17.61 kB / 6.05 kB gzip`
-- Markdown chrome chunk: `dist/client/assets/MarkdownContent-ia2V1dk8.js` at `19.37 kB / 6.39 kB gzip`
-- Global CSS: `dist/client/assets/app-CBMELhsb.css` at `319.24 kB / 40.48 kB gzip`
-
-## Main Problems
-
-- Homepage ships too much in one route chunk.
-- Library landing pages pay docs-shell and docs-config cost before the user asks for docs.
-- Docs still do too much work per request even with GitHub content caching.
-- Hidden docs UI still mounts and runs effects/queries.
-- Anonymous docs users still trigger auth-related client queries for framework preference.
-- Some "lazy" controls are effectively eager.
-
-## Success Targets
-
-- Cut the homepage route chunk hard enough that it is no longer one of the top client payloads.
-- Remove docs-config and docs-layout from the critical path for landing pages.
-- Turn docs page rendering into mostly cached work.
-- Avoid client queries on first paint for content that can be rendered server-side.
-- Reduce hidden-work JS on docs mobile and desktop layouts.
-
-## Workstreams
-
-### 1. Homepage route diet
-
-Targets:
-
-- `src/routes/index.tsx`
-- `src/components/OpenSourceStats.tsx`
-- `src/components/ShowcaseSection.tsx`
-- `src/components/PartnersGrid.tsx`
-- `src/components/MaintainerCard.tsx`
-
-Changes:
-
-- Break below-the-fold homepage sections into viewport-triggered lazy boundaries.
-- Move recent posts off client `useQuery` and into route loader or server-rendered data.
-- Stop client-fetching OSS stats on initial paint. Render a server snapshot first.
-- Keep `DeferredApplicationStarter` deferred by visibility or interaction, not just idle timeout.
-- Avoid eagerly importing large static datasets into the first route chunk where possible.
-- Stop rendering both light and dark hero image variants eagerly.
-
-Expected win:
-
-- Lower homepage JS, lower hydration cost, lower first-load network.
-
-### 2. Dedicated library landing shell
-
-Targets:
-
-- `src/routes/-library-landing.tsx`
-- `src/components/DocsLayout.tsx`
-- landing components under `src/components/landing/`
-
-Changes:
-
-- Introduce a dedicated `LibraryLandingLayout`.
-- Remove `DocsLayout` from landing pages.
-- Stop fetching docs `config.json` in the landing-page critical path unless a landing section actually needs it.
-- Keep framework/version/docs navigation lightweight on landing pages and hand off to docs only when needed.
-
-Expected win:
-
-- Better landing-page TTFB, less landing-page JS, less docs chrome on non-docs surfaces.
-
-### 3. Docs render caching
-
-Targets:
-
-- `src/utils/docs.functions.ts`
-- `src/utils/github-content-cache.server.ts`
-- `src/utils/markdown/renderRsc.tsx`
-- `src/utils/markdown/processor.rsc.tsx`
-- `src/components/markdown/renderCodeBlock.server.tsx`
-
-Changes:
-
-- Cache rendered docs artifacts, not just raw GitHub files.
-- Persist `title`, `description`, `headings`, and rendered output keyed by repo, ref, docs root, and path.
-- Reuse existing docs artifact cache infra instead of adding a second caching path.
-- Make docs requests mostly cache hits unless the source changed.
-
-Expected win:
-
-- Better docs TTFB, less server CPU, fewer repeated markdown and Shiki passes.
-
-### 4. Docs layout mount discipline
-
-Targets:
-
-- `src/components/DocsLayout.tsx`
-- `src/components/RightRail.tsx`
-- `src/components/RecentPostsWidget.tsx`
-
-Changes:
-
-- Do not mount mobile docs menu on desktop.
-- Do not mount desktop docs menu on mobile.
-- Do not mount right rail when hidden by breakpoint.
-- Gate animated partner strip work by actual viewport and reduced-motion preference.
-- Ensure hidden rails do not issue queries or observers.
-
-Expected win:
-
-- Lower docs runtime cost, especially on mobile.
-
-### 5. Remove anonymous auth work from docs and landing
-
-Targets:
-
-- `src/components/FrameworkSelect.tsx`
-- `src/hooks/useCurrentUser.ts`
-- `src/components/SearchModal.tsx`
-- `src/components/NavbarAuthControls.tsx`
-
-Changes:
-
-- Make framework preference local-first for anonymous users.
-- Only sync framework preference to server when user state is already known.
-- Avoid triggering `getCurrentUser` on docs and landing pages just to resolve a preference.
-- Audit other shell components for accidental auth fetches during anonymous browsing.
-
-Expected win:
-
-- Fewer unnecessary client requests, cleaner anonymous docs navigation.
-
-### 6. Shared shell cleanup
-
-Targets:
-
-- `src/routes/__root.tsx`
-- `src/router.tsx`
-- `src/components/Navbar.tsx`
-- `src/components/markdown/MarkdownContent.tsx`
-
-Changes:
-
-- Verify why some intended dynamic imports are not splitting effectively.
-- Trim eager shell work around Sentry boot where possible.
-- Fix `MarkdownContent` so `CopyPageDropdown` only loads on real interaction.
-- Review navbar asset duplication and avoid eager light/dark image duplication where possible.
-
-Expected win:
-
-- Smaller app shell, less global cost paid by every route.
-
-## Suggested Implementation Order
-
-1. Fix obviously accidental eager work.
-2. Make docs layout mount only what is visible.
-3. Remove anonymous auth fetches from docs and landing flows.
-4. Add dedicated library landing shell and remove docs-config from landing critical path.
-5. Move homepage content and stats to server-first data flows and split below-the-fold sections.
-6. Add rendered docs artifact caching.
-7. Rebuild and compare chunks, request timings, and interaction cost.
-
-## PR Breakdown
-
-### PR 1
-
-- Fix `MarkdownContent` eager copy-dropdown load
-- Stop hidden docs rails and menus from mounting
-- Gate mobile partner strip animation correctly
-
-### PR 2
-
-- Remove anonymous auth fetches from framework selection and related docs shell code
-
-### PR 3
-
-- Add `LibraryLandingLayout`
-- Remove `DocsLayout` and docs config dependency from landing critical path
-
-### PR 4
-
-- Split homepage below the fold
-- Server-render recent posts and stats
-- Tighten app-starter deferral
-
-### PR 5
-
-- Cache rendered docs artifacts
-- Measure docs TTFB and server CPU improvement
-
-### PR 6
-
-- Shared shell follow-up: Sentry boot, navbar assets, remaining bundle outliers
-
-## Verification
-
-- Run `pnpm build` after each major phase.
-- Track homepage, a representative library landing page, and a representative docs page.
-- Compare:
-- route chunk size
-- app shell size
-- docs TTFB
-- number of client requests on first load
-- whether anonymous docs visits trigger user/auth requests
-- smoke-check desktop and mobile docs navigation
-
-## Notes
-
-- `LazyLandingCommunitySection` and `LazySponsorSection` already use the right pattern. Reuse that pattern more aggressively.
-- `StackBlitzEmbed` is already `loading="lazy"`, but a poster-plus-click model may still be worth it for landing pages.
-- Do not spend time micro-optimizing heading observers or tiny docs chunks before fixing the homepage and landing-page architecture.
diff --git a/docs/ssr-rsc-migration-findings.md b/docs/ssr-rsc-migration-findings.md
new file mode 100644
index 000000000..3ba8013cf
--- /dev/null
+++ b/docs/ssr-rsc-migration-findings.md
@@ -0,0 +1,149 @@
+# SSR + Hydrate vs RSC findings
+
+Date: 2026-06-20
+
+Scope: local production builds of tanstack.com, with Redact enabled for both variants. The RSC baseline came from the current HEAD before the SSR migration work. The SSR variant is this worktree after removing RSC rendering and adding targeted `Hydrate` timing for below-fold media-heavy sections.
+
+## Current conclusion
+
+SSR + normal hydration + targeted `Hydrate` timing is the stronger direction for tanstack.com.
+
+RSC reduced some client-side rendering responsibility, but it did so by sending rendered payloads, Flight data, extra server-function surfaces, and a lot of architecture-specific indirection. Once SSR is paired with simple visible hydration gates for below-fold media/query-heavy sections, it matches or beats the RSC baseline on first-load production measurements and is smaller during long sessions for docs, examples, and blog navigation.
+
+The important correction from the first SSR pass: plain SSR without timing gates can eagerly schedule too much below-fold media. Lighthouse exposed this immediately on home and Query landing. Adding `Hydrate` around home social proof, home community, landing community, and sponsor pack sections fixed that without bringing back the RSC pipeline.
+
+## Architecture delta
+
+- RSC disabled in `vite.config.ts`; `@vitejs/plugin-rsc` removed.
+- Redact is always enabled for this matrix.
+- Markdown/docs/blog now move raw markdown/source data through normal server functions, then render locally with `@tanstack/markdown` and `@tanstack/highlight`.
+- Deleted RSC markdown/code rendering files, RSC landing-code-example server function, and RSC heading context.
+- Landing code examples are static route/component data, not rendered RSC payloads.
+- Targeted `Hydrate` timing remains only as byte scheduling for below-fold sections.
+
+Code size/complexity:
+
+| Metric | Result |
+| ---------------------------- | -----------------------------------------------: |
+| Source diff | 113 files, +920 / -3946 |
+| Current SSR client dist | 92,258.3 KB |
+| RSC baseline client dist | 92,687.6 KB |
+| Remaining RSC references | none |
+| Remaining `use client` files | `CopyPageDropdown.tsx`, `CopyMarkdownButton.tsx` |
+
+## Markdown and Highlight package story
+
+The original reason RSC helped tanstack.com was real: our docs/blog path was making the browser pay for markdown rendering and Shiki. The earlier performance work measured about 1.1 MiB of script transfer on a docs page, with about 358 KiB clearly tied to syntax highlighting alone: Shiki, its WASM/runtime pieces, themes, and language chunks. The RSC migration solved that by moving markdown and code rendering back to the server, and the RSC launch post recorded the client JS graph dropping about 153 KB gzip on docs/blog pages and about 40 KB gzip on docs example pages.
+
+This SSR pass keeps the same underlying bet, but changes the implementation. Instead of using RSC as the mechanism that hides the markdown/highlight cost from the client, we made the markdown/highlight cost small enough to ship normally, then moved only raw markdown/source data through Start server functions. That is the real byte story: with the custom markdown/highlight libraries in place, SSR is a net negative on first-load gzip across the measured content pages, `Hydrate` fixes the byte scheduling problem for below-fold sections, and long sessions compound the win because each navigation reuses the renderer already sitting in the client bundle instead of transferring rendered Flight output again.
+
+Current packages in this worktree:
+
+| Package | npm unpacked size | Files | Notes |
+| -------------------------------- | ----------------: | ----: | ---------------------------------------------------------------------- |
+| `@tanstack/highlight@0.0.2` | 80.6 KiB | 62 | tiny tokenizer/highlighter, class-based output, isolated theme exports |
+| `@tanstack/markdown@0.0.4` | 79.2 KiB | 44 | parser/rendering path for the markdown subset tanstack.com needs |
+| Combined TanStack packages | 159.8 KiB | 106 | no runtime dependencies, React is a peer for markdown |
+| Shiki 4.0.2 direct package graph | 10.49 MiB | 1,725 | `shiki` plus direct `@shikijs/*` packages and `vscode-textmate` |
+
+That package-size comparison is npm unpacked size, not browser transfer. The browser transfer fact we already have is the old 358 KiB docs-page script cost tied to Shiki. The package fact is still useful for the post because it shows how much general-purpose machinery we stopped depending on: the Shiki package graph is about 67x larger than the two new TanStack packages combined before app bundling even starts.
+
+The current highlight integration is also simpler than the old Shiki path:
+
+- one highlighted HTML tree, not duplicated light/dark markup,
+- no inline token styles,
+- theme CSS generated from `createThemeCss`,
+- light mode uses `githubLightTheme`,
+- dark mode uses `auroraXTheme`,
+- docs smoke test found 20 `pre.th-code` blocks, 381 token spans, 0 `pre.shiki` blocks, and 0 inline token styles.
+
+The blog angle should be framed carefully: AI helped make it practical to write purpose-built packages quickly, but the win is not "AI wrote magic." The win is that we replaced a broad markdown plus Shiki stack with small libraries designed around the exact rendering contract we need, then measured whether that was enough to remove the RSC pipeline without giving back the performance. So far, the answer is yes for the tested pages, with one important caveat: these packages are young enough that conformance matters. We found a tight-list regression in `@tanstack/markdown` where Related Resources rendered as `
...
Route loaders ...
+ {currentCode}
+
+
+ {children}
+
+ )
+ }
+
+ return (
+ ${escapeHtml(
+ return `${escapeHtml(
code,
)}
`
}
export function extractCodeBlockData(props: CodeBlockProps) {
- const rawTitle = ((props as { dataCodeTitle?: string })?.dataCodeTitle ||
- (props as { 'data-code-title'?: string })?.['data-code-title']) as
- | string
- | undefined
+ const rawTitle = props.dataCodeTitle || props['data-code-title']
const title =
rawTitle && rawTitle !== 'undefined' && rawTitle.trim().length > 0
? rawTitle.trim()
: undefined
- const childElement = props.children as
- | undefined
- | { props?: { children?: string; className?: string } }
- const lang =
- childElement?.props?.className?.replace('language-', '') || 'plaintext'
- const code = childElement?.props?.children || ''
+ const codeElement = getCodeElementProps(props.children)
+ const lang = codeElement.className?.replace('language-', '') || 'plaintext'
+ const code = codeElement.children
return {
code,
@@ -82,6 +41,39 @@ export function extractCodeBlockData(props: CodeBlockProps) {
}
}
+function getCodeElementProps(node: React.ReactNode) {
+ if (!React.isValidElement(node)) {
+ return {
+ children: '',
+ className: undefined,
+ }
+ }
+
+ const { props } = node
+
+ if (typeof props !== 'object' || props === null) {
+ return {
+ children: '',
+ className: undefined,
+ }
+ }
+
+ const className =
+ 'className' in props && typeof props.className === 'string'
+ ? props.className
+ : undefined
+
+ const children =
+ 'children' in props && typeof props.children === 'string'
+ ? props.children
+ : ''
+
+ return {
+ children,
+ className,
+ }
+}
+
export function getCodeBlockLanguageFromFilePath(filePath: string) {
const ext = filePath.split('.').pop()?.toLowerCase()
diff --git a/src/components/markdown/index.ts b/src/components/markdown/index.ts
index ca0f7034f..e78381274 100644
--- a/src/components/markdown/index.ts
+++ b/src/components/markdown/index.ts
@@ -1,9 +1,5 @@
export { MarkdownLink } from './MarkdownLink'
export { MarkdownContent } from './MarkdownContent'
-export {
- MarkdownHeadingProvider,
- useMarkdownHeadings,
-} from './MarkdownHeadingContext'
export { CodeBlock } from './CodeBlock'
export { Tabs } from './Tabs'
export { FileTabs } from './FileTabs'
diff --git a/src/components/markdown/renderCodeBlock.server.tsx b/src/components/markdown/renderCodeBlock.server.tsx
deleted file mode 100644
index faaf93d56..000000000
--- a/src/components/markdown/renderCodeBlock.server.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import { transformerNotationDiff } from '@shikijs/transformers'
-import type { HighlighterGeneric } from 'shiki'
-import { createHighlighter } from 'shiki'
-import type { RenderedCodeBlockData } from './codeBlock.shared'
-
-const LANG_ALIASES: Record = {
- cmd: 'bash',
- console: 'bash',
- eslintrc: 'jsonc',
- js: 'javascript',
- json5: 'jsonc',
- md: 'markdown',
- sh: 'bash',
- shell: 'bash',
- text: 'plaintext',
- ts: 'typescript',
- txt: 'plaintext',
- yml: 'yaml',
- zsh: 'bash',
-}
-
-let highlighterPromise: Promise> | null = null
-const failedLanguages = new Set()
-
-async function getHighlighter(language: string) {
- if (!highlighterPromise) {
- highlighterPromise = createHighlighter({
- themes: ['github-light', 'aurora-x'],
- langs: ['plaintext'],
- })
- }
-
- const highlighter = await highlighterPromise
- const normalizedLang = LANG_ALIASES[language] || language
- const langToLoad = normalizedLang === 'mermaid' ? 'plaintext' : normalizedLang
-
- if (failedLanguages.has(langToLoad)) {
- return {
- effectiveLang: 'plaintext',
- highlighter,
- }
- }
-
- if (!highlighter.getLoadedLanguages().includes(langToLoad as any)) {
- try {
- await highlighter.loadLanguage(langToLoad as any)
- } catch {
- failedLanguages.add(langToLoad)
-
- return {
- effectiveLang: 'plaintext',
- highlighter,
- }
- }
- }
-
- return {
- effectiveLang: langToLoad,
- highlighter,
- }
-}
-
-export async function renderCodeBlockData({
- code,
- lang,
- title,
-}: {
- code: string
- lang: string
- title?: string
-}): Promise {
- const trimmedCode = code.trimEnd()
- const { effectiveLang, highlighter } = await getHighlighter(lang)
- const htmlMarkup = ['github-light', 'aurora-x']
- .map((theme) => {
- return highlighter.codeToHtml(trimmedCode, {
- lang: effectiveLang,
- theme,
- transformers: [transformerNotationDiff()],
- })
- })
- .join('')
-
- return {
- copyText: trimmedCode,
- htmlMarkup,
- lang,
- title,
- }
-}
diff --git a/src/components/markdown/usePersistedEnumStore.ts b/src/components/markdown/usePersistedEnumStore.ts
index f9669457d..a246f9683 100644
--- a/src/components/markdown/usePersistedEnumStore.ts
+++ b/src/components/markdown/usePersistedEnumStore.ts
@@ -1,5 +1,3 @@
-'use client'
-
import * as React from 'react'
import { create, type StoreApi, type UseBoundStore } from 'zustand'
diff --git a/src/components/stack/CategoryArticle.tsx b/src/components/stack/CategoryArticle.tsx
index 6550dff7a..3264a2627 100644
--- a/src/components/stack/CategoryArticle.tsx
+++ b/src/components/stack/CategoryArticle.tsx
@@ -23,7 +23,7 @@ import {
Zap,
} from 'lucide-react'
-import { DeferredApplicationStarter } from '~/components/DeferredApplicationStarter'
+import { ApplicationStarter } from '~/components/ApplicationStarter'
import { LibraryWordmark } from '~/components/LibraryWordmark'
import type { LibrarySlim } from '~/libraries'
import { formatPublishedDate, getPostsForLibrary } from '~/utils/blog'
@@ -171,10 +171,10 @@ function FrameworkCategory({
Start-first
+
+ >
+ )
+}
+
export const Route = createFileRoute('/login')({
component: LoginPage,
validateSearch: searchSchema,
@@ -32,37 +49,21 @@ export const Route = createFileRoute('/login')({
})
function SplashImage() {
+ const fallback = (
+
-
-
-
- element.
- */
-function extractCodeBlockData(preNode: HastNode): {
- language: string
- title: string
- code: string
-} | null {
- const codeNode = preNode.children?.find(
- (c: HastNode) => c.type === 'element' && c.tagName === 'code',
- )
-
- if (!codeNode) return null
-
- // Extract language from className
- let language = 'plaintext'
- const className = codeNode.properties?.className
- if (Array.isArray(className)) {
- const langClass = className.find((c) => String(c).startsWith('language-'))
- if (langClass) {
- language = String(langClass).replace('language-', '')
- }
- }
-
- // Extract title from data attributes
- let title = ''
- const props = preNode.properties || {}
- if (typeof props['dataCodeTitle'] === 'string') {
- title = props['dataCodeTitle'] as string
- } else if (typeof props['data-code-title'] === 'string') {
- title = props['data-code-title']
- } else if (typeof props['dataFilename'] === 'string') {
- title = props['dataFilename'] as string
- } else if (typeof props['data-filename'] === 'string') {
- title = props['data-filename']
- }
-
- // Extract code text
- const extractText = (nodes: HastNode[]): string => {
- let text = ''
- for (const node of nodes) {
- if (node.type === 'text' && node.value) {
- text += node.value
- } else if (node.type === 'element' && node.children) {
- text += extractText(node.children)
- }
- }
- return text
- }
- const code = extractText(codeNode.children || [])
-
- return { language, title, code }
-}
-
-function extractFrameworkData(node: HastNode): FrameworkExtraction | null {
- const children = node.children ?? []
- const codeBlocksByFramework: Record = {}
- const contentByFramework: Record = {}
-
- // First pass: find the first H1 to determine the first framework
- let firstFramework: string | null = null
- for (const child of children) {
- if (child.type === 'element' && child.tagName === 'h1') {
- firstFramework = toString(child as any)
- .trim()
- .toLowerCase()
- break
- }
- }
-
- // If no H1 found at all, return null
- if (!firstFramework) {
- return null
- }
-
- // Second pass: collect content
- let currentFramework: string | null = firstFramework // Start with first framework for content before first H1
-
- // Initialize the first framework
- contentByFramework[firstFramework] = []
- codeBlocksByFramework[firstFramework] = []
-
- for (const child of children) {
- // Check if this is an H1 heading (framework divider)
- if (child.type === 'element' && child.tagName === 'h1') {
- // Extract framework name from H1 text
- currentFramework = toString(child as any)
- .trim()
- .toLowerCase()
-
- // Initialize arrays for this framework
- if (currentFramework && !contentByFramework[currentFramework]) {
- contentByFramework[currentFramework] = []
- codeBlocksByFramework[currentFramework] = []
- }
- // Don't include the H1 itself in content - it's just a divider
- continue
- }
-
- if (!currentFramework) continue
-
- // Create a shallow copy of the node
- const contentNode = Object.assign({}, child) as HastNode
-
- // Mark all headings (h2-h6) with framework attribute so they appear in TOC only for this framework
- if (
- contentNode.type === 'element' &&
- contentNode.tagName &&
- /^h[2-6]$/.test(contentNode.tagName)
- ) {
- contentNode.properties = (contentNode.properties || {}) as Record<
- string,
- unknown
- >
- contentNode.properties['data-framework'] = currentFramework
- }
-
- contentByFramework[currentFramework].push(contentNode)
-
- // Extract code blocks for this framework
- if (contentNode.type === 'element' && contentNode.tagName === 'pre') {
- const codeBlockData = extractCodeBlockData(contentNode)
- if (codeBlockData) {
- codeBlocksByFramework[currentFramework].push(codeBlockData)
- }
- }
- }
-
- // Return null if no frameworks found
- if (Object.keys(contentByFramework).length === 0) {
- return null
- }
-
- return { codeBlocksByFramework, contentByFramework }
-}
-
-export function transformFrameworkComponent(node: HastNode) {
- const result = extractFrameworkData(node)
-
- if (!result) {
- return
- }
-
- node.properties = node.properties || {}
- node.properties['data-framework-meta'] = JSON.stringify({
- codeBlocksByFramework: Object.fromEntries(
- Object.entries(result.codeBlocksByFramework).map(([fw, blocks]) => [
- fw,
- blocks.map((b) => ({
- title: b.title,
- code: b.code,
- language: b.language,
- })),
- ]),
- ),
- })
-
- // Store available frameworks for the component
- const availableFrameworks = Object.keys(result.contentByFramework)
- node.properties['data-available-frameworks'] =
- JSON.stringify(availableFrameworks)
-
- node.children = availableFrameworks.map((fw) => {
- const content = result.contentByFramework[fw] || []
- return {
- type: 'element',
- tagName: 'md-framework-panel',
- properties: {
- 'data-framework': fw,
- },
- children: content,
- }
- })
-}
-
-/**
- * Rehype plugin to transform framework components in the AST.
- * Visits the tree and calls transformFrameworkComponent for each framework component found.
- */
-export const rehypeTransformFrameworkComponents = () => {
- return (tree: any) => {
- visit(tree, 'element', (node) => {
- if (node.tagName !== 'md-comment-component') {
- return
- }
-
- const component = String(node.properties?.['data-component'] ?? '')
- if (normalizeComponentName(component) === 'framework') {
- transformFrameworkComponent(node)
- }
- })
- }
-}
diff --git a/src/utils/markdown/plugins/transformTabsComponent.ts b/src/utils/markdown/plugins/transformTabsComponent.ts
deleted file mode 100644
index a5ee89b03..000000000
--- a/src/utils/markdown/plugins/transformTabsComponent.ts
+++ /dev/null
@@ -1,456 +0,0 @@
-import { toString } from 'hast-util-to-string'
-import type { Element, ElementContent } from 'hast'
-
-import { headingLevel, isHeading, slugify } from './helpers'
-import { BUNDLERS, isBundler, type Bundler } from '../bundler'
-
-export type VariantHandler = (
- node: HastNode,
- attributes: Record,
-) => boolean
-
-type InstallMode = 'install' | 'dev-install' | 'local-install'
-
-type HastNode = Element
-
-type TabDescriptor = {
- slug: string
- name: string
-}
-
-type TabExtraction = {
- tabs: TabDescriptor[]
- panels: ElementContent[][]
-}
-
-type PackageManagerExtraction = {
- packagesByFramework: Record
- mode: InstallMode
-}
-
-type FilesExtraction = {
- files: Array<{
- title: string
- code: string
- language: string
- preNode: Element
- }>
-}
-
-function parseAttributes(node: HastNode): Record {
- const rawAttributes = node.properties?.['data-attributes']
- if (typeof rawAttributes === 'string') {
- try {
- return JSON.parse(rawAttributes)
- } catch (error) {
- if (import.meta.env?.DEV) {
- // eslint-disable-next-line no-console
- console.warn(
- '[transformTabsComponent] Failed to parse data-attributes JSON:',
- rawAttributes,
- error,
- )
- }
- return {}
- }
- }
- return {}
-}
-
-function resolveMode(attributes: Record): InstallMode {
- const mode = attributes.mode?.toLowerCase()
- if (mode === 'dev-install') return 'dev-install'
- if (mode === 'local-install') return 'local-install'
- return 'install'
-}
-
-function normalizeFrameworkKey(key: string): string {
- return key.trim().toLowerCase()
-}
-
-// Helper to extract text from nodes (used for code content)
-function extractText(nodes: ReadonlyArray): string {
- let text = ''
- for (const node of nodes) {
- if (node.type === 'text') {
- text += node.value
- } else if (node.type === 'element' && node.children) {
- text += extractText(node.children)
- }
- }
- return text
-}
-
-/**
- * Parse a line like "react: @tanstack/react-query @tanstack/react-query-devtools"
- * Returns { framework: 'react', packages: '@tanstack/react-query @tanstack/react-query-devtools' }
- */
-function parseFrameworkLine(text: string): {
- framework: string
- packages: string[]
-} | null {
- const colonIndex = text.indexOf(':')
- if (colonIndex === -1) {
- return null
- }
-
- const framework = normalizeFrameworkKey(text.slice(0, colonIndex))
- const packagesStr = text.slice(colonIndex + 1).trim()
- const packages = packagesStr.split(/\s+/).filter(Boolean)
-
- if (!framework || packages.length === 0) {
- return null
- }
-
- return { framework, packages }
-}
-
-function extractPackageManagerData(
- node: HastNode,
- mode: InstallMode,
-): PackageManagerExtraction | null {
- const children = node.children ?? []
- const packagesByFramework: Record = {}
-
- const allText = extractText(children)
- const lines = allText.split('\n')
-
- for (const line of lines) {
- const trimmed = line.trim()
- if (!trimmed) continue
-
- const parsed = parseFrameworkLine(trimmed)
- if (parsed) {
- // Each line becomes a separate entry (array of packages)
- // Multiple packages on same line = install together
- // Multiple lines = install separately
- if (packagesByFramework[parsed.framework]) {
- packagesByFramework[parsed.framework].push(parsed.packages)
- } else {
- packagesByFramework[parsed.framework] = [parsed.packages]
- }
- }
- }
-
- if (Object.keys(packagesByFramework).length === 0) {
- return null
- }
-
- return { packagesByFramework, mode }
-}
-
-/**
- * Extract code block data (language, title, code) from a element.
- * Extracts title from data-code-title (set by rehypeCodeMeta).
- */
-function extractCodeBlockData(preNode: HastNode): {
- language: string
- title: string
- code: string
-} | null {
- const codeNode = preNode.children?.find(
- (c): c is Element => c.type === 'element' && c.tagName === 'code',
- )
-
- if (!codeNode) return null
-
- let language = 'plaintext'
- const className = codeNode.properties?.className
- if (Array.isArray(className)) {
- const langClass = className.find((c) => String(c).startsWith('language-'))
- if (langClass) {
- language = String(langClass).replace('language-', '')
- }
- }
-
- let title = ''
- const props = preNode.properties || {}
- if (typeof props['dataCodeTitle'] === 'string') {
- title = props['dataCodeTitle']
- } else if (typeof props['data-code-title'] === 'string') {
- title = props['data-code-title']
- } else if (typeof props['dataFilename'] === 'string') {
- title = props['dataFilename']
- } else if (typeof props['data-filename'] === 'string') {
- title = props['data-filename']
- }
-
- const code = extractText(codeNode.children || [])
-
- return { language, title, code }
-}
-
-/**
- * Extract files data for variant="files" tabs.
- * Parses consecutive code blocks and creates file tabs.
- */
-function extractFilesData(node: HastNode): FilesExtraction | null {
- const children = node.children ?? []
- const files: FilesExtraction['files'] = []
-
- for (const child of children) {
- if (child.type === 'element' && child.tagName === 'pre') {
- const codeBlockData = extractCodeBlockData(child)
- if (!codeBlockData) continue
-
- files.push({
- title: codeBlockData.title || 'Untitled',
- code: codeBlockData.code,
- language: codeBlockData.language,
- preNode: child,
- })
- }
- }
-
- if (files.length === 0) {
- return null
- }
-
- return { files }
-}
-
-/**
- * Extract bundler tab data. Splits children by headings whose text matches a
- * known bundler (e.g. `# Vite`, `## Rsbuild`). Uses the largest heading level
- * present, mirroring `extractTabPanels`. Unknown headings are ignored; content
- * before any recognized heading is dropped.
- */
-function extractBundlerData(node: HastNode): TabExtraction | null {
- const children = node.children ?? []
- const headings = children.filter(isHeading)
-
- if (headings.length === 0) {
- return null
- }
-
- let largestHeadingLevel = Infinity
- for (const heading of headings) {
- largestHeadingLevel = Math.min(largestHeadingLevel, headingLevel(heading))
- }
-
- const panelsByBundler = new Map()
- let currentBundler: Bundler | null = null
-
- for (const child of children) {
- if (isHeading(child) && headingLevel(child) === largestHeadingLevel) {
- const headingText = toString(child).trim().toLowerCase()
- if (isBundler(headingText)) {
- currentBundler = headingText
- if (!panelsByBundler.has(currentBundler)) {
- panelsByBundler.set(currentBundler, [])
- }
- continue
- }
- currentBundler = null
- continue
- }
-
- if (currentBundler) {
- panelsByBundler.get(currentBundler)!.push(child)
- }
- }
-
- if (panelsByBundler.size === 0) {
- return null
- }
-
- const tabs: TabDescriptor[] = []
- const panels: ElementContent[][] = []
- for (const bundler of BUNDLERS) {
- const panel = panelsByBundler.get(bundler)
- if (!panel) continue
- tabs.push({ slug: bundler, name: bundler })
- panels.push(panel)
- }
-
- return { tabs, panels }
-}
-
-function extractTabPanels(node: HastNode): TabExtraction | null {
- const children = node.children ?? []
- const headings = children.filter(isHeading)
-
- let sectionStarted = false
- let largestHeadingLevel = Infinity
- headings.forEach((heading) => {
- largestHeadingLevel = Math.min(largestHeadingLevel, headingLevel(heading))
- })
-
- const tabs: TabDescriptor[] = []
- const panels: ElementContent[][] = []
- let currentPanel: ElementContent[] | null = null
-
- children.forEach((child) => {
- if (isHeading(child)) {
- const level = headingLevel(child)
- if (!sectionStarted) {
- if (level !== largestHeadingLevel) {
- return
- }
- sectionStarted = true
- }
-
- if (level === largestHeadingLevel) {
- if (currentPanel) {
- panels.push(currentPanel)
- }
-
- const headingId =
- typeof child.properties?.id === 'string'
- ? child.properties.id
- : slugify(toString(child), `tab-${tabs.length + 1}`)
-
- tabs.push({
- slug: headingId,
- name: toString(child),
- })
-
- currentPanel = []
- return
- }
- }
-
- if (sectionStarted) {
- if (!currentPanel) {
- currentPanel = []
- }
- currentPanel.push(child)
- }
- })
-
- if (currentPanel) {
- panels.push(currentPanel)
- }
-
- if (!tabs.length) {
- return null
- }
-
- return { tabs, panels }
-}
-
-export function transformTabsComponent(node: HastNode) {
- const attributes = parseAttributes(node)
- const variant = attributes.variant?.toLowerCase()
-
- // Handle package-manager variant
- if (variant === 'package-manager' || variant === 'package-managers') {
- const mode = resolveMode(attributes)
- const result = extractPackageManagerData(node, mode)
-
- if (!result) {
- return
- }
-
- // Remove children so package managers don't show up in TOC
- node.children = []
-
- // Store metadata for the React component
- node.properties = node.properties || {}
- node.properties['data-package-manager-meta'] = JSON.stringify({
- packagesByFramework: result.packagesByFramework,
- mode: result.mode,
- })
- return
- }
-
- // Handle files variant
- if (variant === 'files') {
- const result = extractFilesData(node)
-
- if (!result) {
- return
- }
-
- // Store metadata for the React component (without preNodes to avoid circular refs)
- node.properties = node.properties || {}
- node.properties['data-files-meta'] = JSON.stringify({
- files: result.files.map((f) => ({
- title: f.title,
- code: f.code,
- language: f.language,
- })),
- })
-
- // Create tab headings from file titles
- const tabs = result.files.map((file, index) => ({
- slug: `file-${index}`,
- name: file.title,
- }))
-
- node.properties['data-attributes'] = JSON.stringify({ tabs })
-
- // Create panel elements with original preNodes
- node.children = result.files.map(
- (file, index): Element => ({
- type: 'element',
- tagName: 'md-tab-panel',
- properties: {
- 'data-tab-slug': `file-${index}`,
- 'data-tab-index': String(index),
- },
- // Use the original preNode which already has data-code-title from rehypeCodeMeta
- children: [file.preNode],
- }),
- )
- return
- }
-
- // Handle bundler variant
- if (variant === 'bundler') {
- const result = extractBundlerData(node)
-
- if (!result) {
- return
- }
-
- node.properties = node.properties || {}
- node.properties['data-bundler-meta'] = JSON.stringify({
- bundlers: result.tabs.map((t) => t.slug),
- })
- node.properties['data-attributes'] = JSON.stringify({ tabs: result.tabs })
-
- node.children = result.panels.map((panelChildren, index): Element => {
- const isCodeOnly =
- panelChildren.length === 1 &&
- panelChildren[0]?.type === 'element' &&
- panelChildren[0]?.tagName === 'pre'
-
- return {
- type: 'element',
- tagName: 'md-tab-panel',
- properties: {
- 'data-tab-slug': result.tabs[index]?.slug ?? `bundler-${index + 1}`,
- 'data-tab-index': String(index),
- 'data-content': isCodeOnly ? 'code-only' : 'mixed',
- },
- children: panelChildren,
- }
- })
- return
- }
-
- // Handle default tabs variant
- const result = extractTabPanels(node)
- if (!result) {
- return
- }
-
- const panelElements = result.panels.map(
- (panelChildren, index): Element => ({
- type: 'element',
- tagName: 'md-tab-panel',
- properties: {
- 'data-tab-slug': result.tabs[index]?.slug ?? `tab-${index + 1}`,
- 'data-tab-index': String(index),
- },
- children: panelChildren,
- }),
- )
-
- node.properties = {
- ...node.properties,
- 'data-attributes': JSON.stringify({ tabs: result.tabs }),
- }
- node.children = panelElements
-}
diff --git a/src/utils/markdown/processor.rsc.tsx b/src/utils/markdown/processor.rsc.tsx
deleted file mode 100644
index 6fff14e60..000000000
--- a/src/utils/markdown/processor.rsc.tsx
+++ /dev/null
@@ -1,206 +0,0 @@
-import * as React from 'react'
-import rehypeAutolinkHeadings from 'rehype-autolink-headings'
-import rehypeCallouts from 'rehype-callouts'
-import rehypeRaw from 'rehype-raw'
-import rehypeReact from 'rehype-react'
-import rehypeSlug from 'rehype-slug'
-import * as jsxRuntime from 'react/jsx-runtime'
-import remarkGfm from 'remark-gfm'
-import remarkParse from 'remark-parse'
-import remarkRehype from 'remark-rehype'
-import { unified } from 'unified'
-import { CodeBlock } from '~/components/markdown/CodeBlock.server'
-import {
- MdCommentComponent,
- MdFrameworkPanel,
- MdTabPanel,
-} from '~/components/markdown/MdComponents'
-import { MarkdownLink } from '~/components/markdown/MarkdownLink'
-import { InlineCode, MarkdownImg } from '~/ui'
-import { extractCodeMeta } from '~/utils/markdown/plugins/extractCodeMeta'
-import {
- rehypeCollectHeadings,
- rehypeParseCommentComponents,
- rehypeTransformCommentComponents,
- rehypeTransformFrameworkComponents,
- type MarkdownHeading,
-} from '~/utils/markdown/plugins'
-
-export type { MarkdownHeading } from '~/utils/markdown/plugins'
-
-export type MarkdownJsxResult = {
- content: React.ReactNode
- headings: MarkdownHeading[]
-}
-
-export type MarkdownRenderOptions = {
- preserveTabPanels?: boolean
-}
-
-function createHeadingComponent(
- level: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6',
-) {
- function HeadingComponent({
- children,
- className,
- id,
- ...props
- }: React.HTMLAttributes) {
- const Tag = level
-
- return (
-
- {children}
-
- )
- }
-
- HeadingComponent.displayName = `Heading${level.toUpperCase()}`
-
- return HeadingComponent
-}
-
-function MarkdownIframe(props: React.IframeHTMLAttributes) {
- return
-}
-
-function CodeElement({
- children,
- className,
- ...props
-}: React.HTMLAttributes) {
- if (className?.includes('language-')) {
- return (
-
- {children}
-
- )
- }
-
- return (
-
- {children}
-
- )
-}
-
-function LinkElement(props: React.AnchorHTMLAttributes) {
- if (props.className?.includes('anchor-heading')) {
- // eslint-disable-next-line jsx-a11y/anchor-has-content
- return
- }
-
- return
-}
-
-function TableElement({
- className,
- ...props
-}: React.TableHTMLAttributes) {
- return (
-
-
-
- )
-}
-
-function createMarkdownComponents(options: MarkdownRenderOptions = {}) {
- function MdCommentComponentWithOptions(
- props: React.ComponentProps,
- ) {
- return (
-
- )
- }
-
- return {
- a: LinkElement,
- code: CodeElement,
- h1: createHeadingComponent('h1'),
- h2: createHeadingComponent('h2'),
- h3: createHeadingComponent('h3'),
- h4: createHeadingComponent('h4'),
- h5: createHeadingComponent('h5'),
- h6: createHeadingComponent('h6'),
- iframe: MarkdownIframe,
- img: MarkdownImg,
- 'md-comment-component': MdCommentComponentWithOptions,
- 'md-framework-panel': MdFrameworkPanel,
- 'md-tab-panel': MdTabPanel,
- pre: CodeBlock,
- table: TableElement,
- }
-}
-
-export async function renderMarkdownToJsx(
- content: string,
- options?: MarkdownRenderOptions,
-): Promise {
- const headings: Array = []
-
- const file = await unified()
- .use(remarkParse)
- .use(remarkGfm)
- .use(remarkRehype, { allowDangerousHtml: true })
- .use(extractCodeMeta)
- .use(rehypeRaw)
- .use(rehypeParseCommentComponents)
- .use(rehypeCallouts, {
- theme: 'github',
- props: {
- containerProps: (_node: unknown, type: string) => ({
- className: `markdown-alert markdown-alert-${type}`,
- }),
- contentProps: () => ({
- className: 'markdown-alert-content',
- }),
- titleIconProps: () => ({
- className: 'octicon octicon-info mr-2',
- }),
- titleProps: () => ({
- className: 'markdown-alert-title',
- }),
- titleTextProps: () => ({
- className: 'markdown-alert-title',
- }),
- },
- } as any)
- .use(rehypeSlug)
- .use(rehypeTransformFrameworkComponents)
- .use(rehypeTransformCommentComponents)
- .use(() => rehypeCollectHeadings(headings))
- .use(rehypeAutolinkHeadings, {
- behavior: 'append',
- content: {
- type: 'text',
- value: '#',
- },
- properties: {
- ariaHidden: true,
- className: ['anchor-heading', 'anchor-heading-link'],
- tabIndex: -1,
- },
- })
- .use(rehypeReact, {
- Fragment: jsxRuntime.Fragment,
- jsx: jsxRuntime.jsx,
- jsxs: jsxRuntime.jsxs,
- components: createMarkdownComponents(options),
- } as any)
- .process(content)
-
- return {
- content: file.result as React.ReactNode,
- headings,
- }
-}
diff --git a/src/utils/markdown/processor.ts b/src/utils/markdown/processor.ts
new file mode 100644
index 000000000..289ea2bd2
--- /dev/null
+++ b/src/utils/markdown/processor.ts
@@ -0,0 +1,22 @@
+import { docsMarkdownExtensions } from '@tanstack/markdown/extensions/docs'
+import { parseMarkdown } from '@tanstack/markdown/parser'
+import type { MarkdownDocument, MarkdownHeading } from '@tanstack/markdown'
+
+export type { MarkdownDocument, MarkdownHeading } from '@tanstack/markdown'
+
+export type SiteMarkdownDocument = MarkdownDocument & {
+ headings: Array
+}
+
+export function parseSiteMarkdown(content: string): SiteMarkdownDocument {
+ const document = parseMarkdown(content, {
+ allowHtml: true,
+ extensions: docsMarkdownExtensions(),
+ headingIds: true,
+ })
+
+ return {
+ ...document,
+ headings: document.headings ?? [],
+ }
+}
diff --git a/src/utils/markdown/renderRsc.tsx b/src/utils/markdown/renderRsc.tsx
deleted file mode 100644
index b697499d9..000000000
--- a/src/utils/markdown/renderRsc.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { renderServerComponent } from '@tanstack/react-start/rsc'
-import * as React from 'react'
-import {
- renderMarkdownToJsx,
- type MarkdownRenderOptions,
-} from './processor.rsc'
-
-export async function renderMarkdownToRsc(
- content: string,
- options?: MarkdownRenderOptions,
-) {
- const { content: contentJsx, headings } = await renderMarkdownToJsx(
- content,
- options,
- )
- const contentRsc = await renderServerComponent(
- React.createElement(React.Fragment, null, contentJsx),
- )
-
- return {
- contentRsc,
- headings,
- }
-}
diff --git a/src/utils/netlify-purge.server.ts b/src/utils/netlify-purge.server.ts
deleted file mode 100644
index 22e565e35..000000000
--- a/src/utils/netlify-purge.server.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { purgeCache } from '@netlify/functions'
-import * as Sentry from '@sentry/node'
-
-export type PurgeResult =
- | { purged: true; tags: Array }
- | {
- purged: false
- reason: 'no-tags' | 'no-credentials' | 'error'
- error?: string
- }
-
-export async function purgeNetlifyTags(
- tags: Array,
-): Promise {
- const uniqueTags = Array.from(new Set(tags)).filter((tag) => tag.length > 0)
-
- if (uniqueTags.length === 0) {
- return { purged: false, reason: 'no-tags' }
- }
-
- // SITE_ID + NETLIFY_PURGE_API_TOKEN are auto-injected when running on
- // Netlify. Absent locally — no-op so dev workflows still work.
- if (!process.env.SITE_ID || !process.env.NETLIFY_PURGE_API_TOKEN) {
- return { purged: false, reason: 'no-credentials' }
- }
-
- try {
- await purgeCache({ tags: uniqueTags })
- return { purged: true, tags: uniqueTags }
- } catch (error) {
- const message = error instanceof Error ? error.message : String(error)
- console.error('[netlify-purge] purgeCache failed', {
- tags: uniqueTags,
- message,
- })
- Sentry.captureException(error, {
- tags: { runtime: 'server', context: 'netlify-purge' },
- extra: { tags: uniqueTags },
- })
- return { purged: false, reason: 'error', error: message }
- }
-}
diff --git a/src/utils/netlifyImage.ts b/src/utils/netlifyImage.ts
deleted file mode 100644
index 72e58026b..000000000
--- a/src/utils/netlifyImage.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Build a Netlify Image CDN URL for optimized image delivery.
- * Returns the original src in development or for external URLs, data URIs, and SVGs.
- *
- * @see https://docs.netlify.com/build/image-cdn/overview/
- */
-export function getNetlifyImageUrl(
- src: string,
- options: { width?: number; height?: number; quality?: number } = {},
-): string {
- // Skip in development - Netlify Image CDN only works in production
- if (import.meta.env.DEV) {
- return src
- }
-
- if (
- src.startsWith('http') ||
- src.startsWith('data:') ||
- src.endsWith('.svg')
- ) {
- return src
- }
-
- const { width = 800, quality = 80 } = options
- return `/.netlify/images?url=${encodeURIComponent(src)}&w=${width}&q=${quality}`
-}
diff --git a/src/utils/og.ts b/src/utils/og.ts
index 7dbea4ecd..d6064eb0f 100644
--- a/src/utils/og.ts
+++ b/src/utils/og.ts
@@ -20,13 +20,12 @@ type OgImageOptions = {
* Unlike canonical links (which always point to production), og:image
* URLs MUST be reachable on the same deploy that emitted them — social-
* card validators fetch the URL from the meta tag verbatim, so on a
- * Netlify deploy preview the og:image must point at the preview origin,
- * not at production.
+ * staging deploy the og:image must point at the preview origin, not at
+ * production.
*
- * The incoming request URL is the source of truth. `process.env.URL` /
- * `DEPLOY_PRIME_URL` etc. turned out to be unreliable inside our bundled
- * SSR function, so read the origin from the live request via TanStack
- * Start's `getRequest()`. The server import is referenced only inside
+ * The incoming request URL is the source of truth. Read the origin from the
+ * live request via TanStack Start's `getRequest()`. The server import is
+ * referenced only inside
* `.server()`, which the start compiler treats as a client-safe boundary
* — the import is tree-shaken from the client bundle.
*/
diff --git a/src/utils/optimizedImage.ts b/src/utils/optimizedImage.ts
new file mode 100644
index 000000000..78485d0f4
--- /dev/null
+++ b/src/utils/optimizedImage.ts
@@ -0,0 +1,78 @@
+export type ImageOptimizationOptions = {
+ fit?: 'contain' | 'cover' | 'crop' | 'pad' | 'scale-down'
+ format?: 'auto' | 'avif' | 'webp' | 'json'
+ height?: number
+ quality?: number
+ width?: number
+}
+
+export function getOptimizedImageUrl(
+ src: string,
+ options: ImageOptimizationOptions = {},
+) {
+ if (!shouldTransformImage(src)) {
+ return src
+ }
+
+ const transformOptions = createCloudflareTransformOptions(options)
+ if (!transformOptions) {
+ return src
+ }
+
+ const source = encodeURI(src).replace(/^\//, '')
+
+ return `/cdn-cgi/image/${transformOptions}/${source}`
+}
+
+export function getAbsoluteOptimizedImageUrl(
+ src: string,
+ options: ImageOptimizationOptions = {},
+) {
+ const optimizedSrc = getOptimizedImageUrl(src, options)
+ if (optimizedSrc.startsWith('http')) {
+ return optimizedSrc
+ }
+
+ const origin = __TANSTACK_SITE_URL__.replace(/\/$/, '')
+ const path = optimizedSrc.startsWith('/') ? optimizedSrc : `/${optimizedSrc}`
+
+ return `${origin}${path}`
+}
+
+function shouldTransformImage(src: string) {
+ if (!__TANSTACK_ENABLE_IMAGE_TRANSFORMATIONS__) {
+ return false
+ }
+
+ const normalized = src.toLowerCase()
+
+ return (
+ src.startsWith('/') &&
+ !src.startsWith('/cdn-cgi/image/') &&
+ !normalized.startsWith('data:') &&
+ !normalized.endsWith('.svg')
+ )
+}
+
+function createCloudflareTransformOptions(
+ options: ImageOptimizationOptions,
+): string | undefined {
+ const params: Array = []
+
+ if (options.width) {
+ params.push(`width=${Math.round(options.width)}`)
+ }
+
+ if (options.height) {
+ params.push(`height=${Math.round(options.height)}`)
+ }
+
+ if (options.fit) {
+ params.push(`fit=${options.fit}`)
+ }
+
+ params.push(`quality=${Math.round(options.quality ?? 80)}`)
+ params.push(`format=${options.format ?? 'auto'}`)
+
+ return params.length > 0 ? params.join(',') : undefined
+}
diff --git a/src/utils/partner-pages.ts b/src/utils/partner-pages.ts
index 8cd4f78a6..d0f79855d 100644
--- a/src/utils/partner-pages.ts
+++ b/src/utils/partner-pages.ts
@@ -57,6 +57,12 @@ const partnerGuidance: Record<
whyTanStack:
'That can be a strong fit for TanStack Start apps that want to run close to users on Workers and take advantage of bindings and prerendering support.',
},
+ lovable: {
+ whyGreat:
+ 'Lovable is useful when the shortest path from idea to working app matters, especially because it combines AI-assisted building, visual editing, GitHub sync, and hosted deployment.',
+ whyTanStack:
+ 'Its move to TanStack Start for new SSR projects makes the generated app foundation much closer to the stack TanStack teams already want to own.',
+ },
sentry: {
whyGreat:
'Sentry turns production issues into actionable debugging data instead of just error logs, especially once tracing and replay are in the mix.',
diff --git a/src/utils/partners.tsx b/src/utils/partners.tsx
index 94cf875b8..677377fa8 100644
--- a/src/utils/partners.tsx
+++ b/src/utils/partners.tsx
@@ -6,6 +6,8 @@ import vercelLightSvg from '~/images/vercel-light.svg'
import vercelDarkSvg from '~/images/vercel-dark.svg'
import netlifyLightSvg from '~/images/netlify-light.svg'
import netlifyDarkSvg from '~/images/netlify-dark.svg'
+import lovableBlackSvg from '~/images/lovable-black.svg'
+import lovableWhiteSvg from '~/images/lovable-white.svg'
import convexWhiteSvg from '~/images/convex-white.svg'
import convexColorSvg from '~/images/convex-color.svg'
import clerkLightSvg from '~/images/clerk-logo-light.svg'
@@ -42,6 +44,7 @@ import openrouterWhiteSvg from '~/images/openrouter-white.svg'
import {
getPartnerPlacementContext,
getPartnersForPlacement,
+ type PartnerPlacementContext,
} from '~/utils/partner-placement'
function LearnMoreButton() {
@@ -63,6 +66,9 @@ type PartnerApplicationStarterIcon = {
type ApplicationStarterPartnerTier = 1 | 2 | 3
+export const partnerUniqueConstraints = ['auth-provider', 'hosting'] as const
+export type PartnerUniqueConstraint = (typeof partnerUniqueConstraints)[number]
+
export const partnerTiers = ['gold', 'silver', 'bronze'] as const
export type PartnerTier = (typeof partnerTiers)[number]
@@ -237,6 +243,7 @@ export type Partner = {
startDate?: string
endDate?: string
score: number
+ uniqueConstraints?: Array
tier?: PartnerTier
brandColor?: string // Primary brand color for game elements
tagline?: string // Short tagline for game info cards
@@ -253,6 +260,7 @@ export type ApplicationStarterPartnerSuggestion = {
label: string
tags: Array
tier: ApplicationStarterPartnerTier
+ uniqueConstraints: Array
}
const APPLICATION_STARTER_GUIDANCE_MARKER = 'Starter guidance:'
@@ -405,6 +413,9 @@ const clerk = (() => {
status: 'active' as const,
score: 0.286,
tier: 'silver' as const,
+ uniqueConstraints: [
+ 'auth-provider',
+ ] satisfies Array,
brandColor: '#6C47FF',
tagline: 'Authentication',
image: {
@@ -441,6 +452,9 @@ const workos = (() => {
status: 'active' as const,
score: 0.314,
tier: 'silver' as const,
+ uniqueConstraints: [
+ 'auth-provider',
+ ] satisfies Array,
brandColor: '#6363F1',
tagline: 'Enterprise Auth',
applicationStarterIcon: {
@@ -532,6 +546,7 @@ const netlify = (() => {
status: 'active' as const,
score: 0.343,
tier: 'silver' as const,
+ uniqueConstraints: ['hosting'] satisfies Array,
href,
brandColor: '#00C7B7',
tagline: 'Web Deployment',
@@ -573,6 +588,7 @@ const cloudflare = (() => {
status: 'active' as const,
score: 0.857,
tier: 'gold' as const,
+ uniqueConstraints: ['hosting'] satisfies Array,
startDate: 'Sep 2025',
brandColor: '#F6821F',
tagline: 'Edge Deployment',
@@ -596,6 +612,48 @@ const cloudflare = (() => {
}
})()
+const lovable = (() => {
+ const href = 'https://lovable.dev?utm_source=tanstack'
+
+ return {
+ name: 'Lovable',
+ id: 'lovable',
+ href,
+ libraries: ['start', 'router'] as const,
+ status: 'inactive' as const,
+ score: 0.714,
+ tier: 'gold' as const,
+ uniqueConstraints: ['hosting'] satisfies Array,
+ brandColor: '#FF7EB0',
+ tagline: 'AI App Builder',
+ applicationStarterPromptInstructions: [
+ 'Treat Lovable as the AI app-building and hosting path, not as a TanStack CLI deployment flag or npm package.',
+ 'Keep the generated app portable: start with the TanStack CLI output, preserve GitHub/project ownership notes, and call out any Lovable Cloud setup that cannot be automated from code.',
+ 'When Lovable is selected, do not add a separate Cloudflare, Netlify, or Railway deployment target unless the user explicitly asks for a handoff path.',
+ ],
+ image: {
+ light: lovableBlackSvg,
+ dark: lovableWhiteSvg,
+ },
+ llmDescription:
+ 'AI app-building platform for generating, editing, and shipping web apps from prompts, with GitHub sync, visual editing, Lovable Cloud hosting, and new TanStack Start-powered SSR projects.',
+ category: 'ai',
+ content: (
+ <>
+
+ Lovable helps teams move from prompt to working app with{' '}
+ AI-assisted building, visual editing, GitHub sync,
+ and Lovable Cloud hosting. New Lovable projects are powered by
+ TanStack Start, which makes it especially relevant for teams that want
+ generated apps to keep strong routing, SSR, and type-safety
+ foundations.
+
+
+ >
+ ),
+ }
+})()
+
const sentry = (() => {
const href = 'https://sentry.io?utm_source=tanstack'
@@ -882,6 +940,7 @@ const vercel = (() => {
status: 'inactive' as const,
startDate: 'May 2024',
endDate: 'Oct 2024',
+ uniqueConstraints: ['hosting'] satisfies Array,
image: {
light: vercelLightSvg,
dark: vercelDarkSvg,
@@ -1066,6 +1125,7 @@ const railway = (() => {
status: 'active' as const,
score: 0.145,
tier: 'gold' as const,
+ uniqueConstraints: ['hosting'] satisfies Array,
href,
brandColor: '#0B0D0E',
tagline: 'Instant Deployment',
@@ -1143,6 +1203,7 @@ const openRouter = (() => {
export const partners: Partner[] = [
codeRabbit,
cloudflare,
+ lovable,
agGrid,
serpApi,
netlify,
@@ -1177,6 +1238,164 @@ const applicationStarterPlacementContext = getPartnerPlacementContext({
surface: 'application_starter_suggestions',
})
+function getPartnerUniqueConstraints(
+ partner: Pick,
+) {
+ return partner.uniqueConstraints ?? []
+}
+
+function hasSharedUniqueConstraint(
+ left: Pick,
+ right: Pick,
+) {
+ return left.uniqueConstraints.some((constraint) =>
+ right.uniqueConstraints.includes(constraint),
+ )
+}
+
+function findApplicationStarterPartnerSuggestion(
+ partnerId: string,
+ partnerSuggestions: Array,
+) {
+ return partnerSuggestions.find((partner) => partner.id === partnerId)
+}
+
+export function hasApplicationStarterPartnerConflictWithAny(
+ partnerId: string,
+ partnerIds: Array,
+ partnerSuggestions: Array = applicationStarterPartnerSuggestions,
+) {
+ const partner = findApplicationStarterPartnerSuggestion(
+ partnerId,
+ partnerSuggestions,
+ )
+
+ if (!partner) {
+ return false
+ }
+
+ return partnerIds.some((candidateId) => {
+ if (candidateId === partner.id) {
+ return false
+ }
+
+ const candidate = findApplicationStarterPartnerSuggestion(
+ candidateId,
+ partnerSuggestions,
+ )
+
+ return candidate ? hasSharedUniqueConstraint(partner, candidate) : false
+ })
+}
+
+export function getApplicationStarterConflictingPartnerIds(
+ partner: ApplicationStarterPartnerSuggestion,
+ partnerSuggestions: Array,
+) {
+ return partnerSuggestions.flatMap((candidate) => {
+ if (
+ candidate.id === partner.id ||
+ !hasSharedUniqueConstraint(partner, candidate)
+ ) {
+ return []
+ }
+
+ return [candidate.id]
+ })
+}
+
+export function getApplicationStarterCompatiblePartnerIds(
+ partnerIds: Array,
+ partnerSuggestions: Array = applicationStarterPartnerSuggestions,
+) {
+ const normalized = Array()
+
+ for (const partnerId of partnerIds) {
+ if (normalized.includes(partnerId)) {
+ continue
+ }
+
+ const partner = findApplicationStarterPartnerSuggestion(
+ partnerId,
+ partnerSuggestions,
+ )
+
+ if (!partner) {
+ continue
+ }
+
+ if (partner.uniqueConstraints.length === 0) {
+ normalized.push(partner.id)
+ continue
+ }
+
+ const conflictingPartnerIds = normalized.filter((candidateId) => {
+ const candidate = findApplicationStarterPartnerSuggestion(
+ candidateId,
+ partnerSuggestions,
+ )
+
+ return candidate ? hasSharedUniqueConstraint(partner, candidate) : false
+ })
+
+ const hasSameOrHigherTierConflict = conflictingPartnerIds.some(
+ (candidateId) => {
+ const candidate = findApplicationStarterPartnerSuggestion(
+ candidateId,
+ partnerSuggestions,
+ )
+
+ return candidate ? candidate.tier <= partner.tier : false
+ },
+ )
+
+ if (hasSameOrHigherTierConflict) {
+ continue
+ }
+
+ for (const conflictingPartnerId of conflictingPartnerIds) {
+ const candidate = findApplicationStarterPartnerSuggestion(
+ conflictingPartnerId,
+ partnerSuggestions,
+ )
+
+ if (!candidate || candidate.tier <= partner.tier) {
+ continue
+ }
+
+ const conflictingIndex = normalized.indexOf(conflictingPartnerId)
+
+ if (conflictingIndex >= 0) {
+ normalized.splice(conflictingIndex, 1)
+ }
+ }
+
+ normalized.push(partner.id)
+ }
+
+ return normalized
+}
+
+export function getApplicationStarterVisiblePartnerSuggestions(
+ partnerSuggestions: Array,
+ _selectedPartnerIds: Array,
+) {
+ return partnerSuggestions
+}
+
+export function hasApplicationStarterPartnerUniqueConstraint(
+ partnerId: string,
+ uniqueConstraint: PartnerUniqueConstraint,
+ partnerSuggestions: Array = applicationStarterPartnerSuggestions,
+) {
+ const partner = findApplicationStarterPartnerSuggestion(
+ partnerId,
+ partnerSuggestions,
+ )
+
+ return partner?.uniqueConstraints.includes(uniqueConstraint) ?? false
+}
+
const applicationStarterInferenceRules: Array<{
partnerId: string
patterns: Array
@@ -1197,6 +1416,12 @@ const applicationStarterInferenceRules: Array<{
partnerId: 'cloudflare',
patterns: [/\b(cloudflare|workers?|durable objects|r2|d1|kv)\b/i],
},
+ {
+ partnerId: 'lovable',
+ patterns: [
+ /\b(lovable|lovable cloud|vibe[- ]?coding|ai app builder|visual editor)\b/i,
+ ],
+ },
{
partnerId: 'netlify',
patterns: [
@@ -1303,9 +1528,15 @@ function inferApplicationStarterPartnerIds(
partner.status === 'active' && selectedPartnerIds.includes(partner.id),
)
const blockedCategories = new Set(
- selectedSourcePartners.map((partner) => partner.category),
+ selectedSourcePartners
+ .filter((partner) => getPartnerUniqueConstraints(partner).length === 0)
+ .map((partner) => partner.category),
+ )
+ const blockedUniqueConstraints = new Set(
+ selectedSourcePartners.flatMap(getPartnerUniqueConstraints),
)
const inferredCategories = new Set()
+ const inferredUniqueConstraints = new Set()
const inferredPartnerIds = Array()
for (const rule of applicationStarterInferenceRules) {
@@ -1322,6 +1553,18 @@ function inferApplicationStarterPartnerIds(
continue
}
+ const partnerUniqueConstraints = getPartnerUniqueConstraints(partner)
+ const hasUniqueConstraint = partnerUniqueConstraints.length > 0
+ const isBlockedByUniqueConstraint = partnerUniqueConstraints.some(
+ (uniqueConstraint) =>
+ blockedUniqueConstraints.has(uniqueConstraint) ||
+ inferredUniqueConstraints.has(uniqueConstraint),
+ )
+
+ if (isBlockedByUniqueConstraint) {
+ continue
+ }
+
if (
partner.id === 'clerk' &&
/\bavoid\s+adding\s+auth\s+and\s+api\s+key\s+providers?\s+that\s+overlap\s+in\s+purpose\b/i.test(
@@ -1332,8 +1575,9 @@ function inferApplicationStarterPartnerIds(
}
if (
- blockedCategories.has(partner.category) ||
- inferredCategories.has(partner.category)
+ !hasUniqueConstraint &&
+ (blockedCategories.has(partner.category) ||
+ inferredCategories.has(partner.category))
) {
continue
}
@@ -1343,10 +1587,17 @@ function inferApplicationStarterPartnerIds(
}
inferredPartnerIds.push(partner.id)
- inferredCategories.add(partner.category)
+
+ if (hasUniqueConstraint) {
+ for (const uniqueConstraint of partnerUniqueConstraints) {
+ inferredUniqueConstraints.add(uniqueConstraint)
+ }
+ } else {
+ inferredCategories.add(partner.category)
+ }
}
- return inferredPartnerIds
+ return getApplicationStarterCompatiblePartnerIds(inferredPartnerIds)
}
export function getInferredApplicationStarterPartnerIdsFromUserInput(
@@ -1356,45 +1607,55 @@ export function getInferredApplicationStarterPartnerIdsFromUserInput(
return inferApplicationStarterPartnerIds(input, selectedPartnerIds)
}
-const applicationStarterPartnerSuggestions: Array =
- getPartnersForPlacement(
+function createApplicationStarterPartnerSuggestion(
+ partner: Partner,
+): ApplicationStarterPartnerSuggestion {
+ const tier = getApplicationStarterPartnerTier(partner)
+ const normalizedPartnerKey = normalizeApplicationStarterPartnerKey(partner.id)
+ const iconMode: ApplicationStarterPartnerSuggestion['iconMode'] =
+ tier === 2 ? (partner.applicationStarterIcon?.mode ?? 'contain') : undefined
+
+ return {
+ id: partner.id,
+ label: partner.name,
+ description: getApplicationStarterPartnerDescription(partner),
+ hint: `${partner.name} (${partnerCategoryLabels[partner.category]})`,
+ iconMode,
+ iconSrc:
+ tier === 2
+ ? (partner.applicationStarterIcon?.src ??
+ getApplicationStarterPartnerFaviconUrl(partner.href))
+ : undefined,
+ image: partner.image,
+ tags: getApplicationStarterPartnerTags(partner),
+ brandColor:
+ applicationStarterBrandColorOverrides.get(normalizedPartnerKey) ??
+ partner.brandColor,
+ tier,
+ uniqueConstraints: getPartnerUniqueConstraints(partner),
+ }
+}
+
+function createApplicationStarterPartnerSuggestions(
+ placementContext: PartnerPlacementContext,
+) {
+ return getPartnersForPlacement(
partners.filter((partner) => partner.status === 'active'),
- applicationStarterPlacementContext,
- )
- .map((partner) => {
- const tier = getApplicationStarterPartnerTier(partner)
- const normalizedPartnerKey = normalizeApplicationStarterPartnerKey(
- partner.id,
- )
- const iconMode: ApplicationStarterPartnerSuggestion['iconMode'] =
- tier === 2
- ? (partner.applicationStarterIcon?.mode ?? 'contain')
- : undefined
-
- return {
- id: partner.id,
- label: partner.name,
- description: getApplicationStarterPartnerDescription(partner),
- hint: `${partner.name} (${partnerCategoryLabels[partner.category]})`,
- iconMode,
- iconSrc:
- tier === 2
- ? (partner.applicationStarterIcon?.src ??
- getApplicationStarterPartnerFaviconUrl(partner.href))
- : undefined,
- image: partner.image,
- tags: getApplicationStarterPartnerTags(partner),
- brandColor:
- applicationStarterBrandColorOverrides.get(normalizedPartnerKey) ??
- partner.brandColor,
- tier,
- score: partner.score,
- }
- })
- .map(({ score: _score, ...partner }) => partner)
+ placementContext,
+ ).map(createApplicationStarterPartnerSuggestion)
+}
-export function getApplicationStarterPartnerSuggestions() {
- return applicationStarterPartnerSuggestions
+const applicationStarterPartnerSuggestions =
+ createApplicationStarterPartnerSuggestions(applicationStarterPlacementContext)
+
+export function getApplicationStarterPartnerSuggestions(
+ placementContext: PartnerPlacementContext = applicationStarterPlacementContext,
+) {
+ if (placementContext === applicationStarterPlacementContext) {
+ return applicationStarterPartnerSuggestions
+ }
+
+ return createApplicationStarterPartnerSuggestions(placementContext)
}
export function composeApplicationStarterInput(
@@ -1404,19 +1665,33 @@ export function composeApplicationStarterInput(
options?: { forceRouterOnly?: boolean },
) {
const trimmedInput = input.trim()
+ const compatibleSelectedPartnerIds =
+ getApplicationStarterCompatiblePartnerIds(selectedPartnerIds)
+ const compatibleInferredPartnerIds =
+ getApplicationStarterCompatiblePartnerIds(
+ inferredPartnerIds.filter(
+ (partnerId) =>
+ !hasApplicationStarterPartnerConflictWithAny(
+ partnerId,
+ compatibleSelectedPartnerIds,
+ ),
+ ),
+ )
const selectedPartners = applicationStarterPartnerSuggestions.filter(
- (partner) => selectedPartnerIds.includes(partner.id),
+ (partner) => compatibleSelectedPartnerIds.includes(partner.id),
)
const inferredPartners = applicationStarterPartnerSuggestions.filter(
- (partner) => inferredPartnerIds.includes(partner.id),
+ (partner) => compatibleInferredPartnerIds.includes(partner.id),
)
const selectedSourcePartners = partners.filter(
(partner) =>
- partner.status === 'active' && selectedPartnerIds.includes(partner.id),
+ partner.status === 'active' &&
+ compatibleSelectedPartnerIds.includes(partner.id),
)
const inferredSourcePartners = partners.filter(
(partner) =>
- partner.status === 'active' && inferredPartnerIds.includes(partner.id),
+ partner.status === 'active' &&
+ compatibleInferredPartnerIds.includes(partner.id),
)
if (selectedPartners.length === 0 && inferredPartners.length === 0) {
@@ -1457,7 +1732,10 @@ export function composeApplicationStarterInput(
)
}
- const allPartnerIds = [...selectedPartnerIds, ...inferredPartnerIds]
+ const allPartnerIds = [
+ ...compatibleSelectedPartnerIds,
+ ...compatibleInferredPartnerIds,
+ ]
if (allPartnerIds.includes('coderabbit')) {
partnerInstructions.push(
@@ -1470,8 +1748,8 @@ export function composeApplicationStarterInput(
trimmedInput,
'',
APPLICATION_STARTER_GUIDANCE_MARKER,
- `${APPLICATION_STARTER_SELECTED_PARTNERS_MARKER} ${selectedPartnerIds.join(', ') || 'none'}`,
- `${APPLICATION_STARTER_INFERRED_PARTNERS_MARKER} ${inferredPartnerIds.join(', ') || 'none'}`,
+ `${APPLICATION_STARTER_SELECTED_PARTNERS_MARKER} ${compatibleSelectedPartnerIds.join(', ') || 'none'}`,
+ `${APPLICATION_STARTER_INFERRED_PARTNERS_MARKER} ${compatibleInferredPartnerIds.join(', ') || 'none'}`,
options?.forceRouterOnly
? APPLICATION_STARTER_FORCE_ROUTER_ONLY_MARKER
: null,
diff --git a/src/utils/prod-diagnostics.server.ts b/src/utils/prod-diagnostics.server.ts
index 66bb2dfcd..a3dac98d7 100644
--- a/src/utils/prod-diagnostics.server.ts
+++ b/src/utils/prod-diagnostics.server.ts
@@ -1,6 +1,7 @@
import { randomUUID } from 'node:crypto'
import { readdirSync } from 'node:fs'
import { AsyncLocalStorage } from 'node:async_hooks'
+import { supportsProcessDiagnostics } from '~/server/runtime/host.server'
import { getClientIp } from '~/utils/request.server'
const isProduction = process.env.NODE_ENV === 'production'
@@ -37,9 +38,20 @@ function getFdCount(): number {
function getResourceSummary(): Record {
const summary: Record = {}
- for (const resource of process.getActiveResourcesInfo()) {
- summary[resource] = (summary[resource] ?? 0) + 1
+
+ const getActiveResourcesInfo = process.getActiveResourcesInfo
+ if (typeof getActiveResourcesInfo !== 'function') {
+ return summary
+ }
+
+ try {
+ for (const resource of getActiveResourcesInfo()) {
+ summary[resource] = (summary[resource] ?? 0) + 1
+ }
+ } catch {
+ return summary
}
+
return summary
}
@@ -75,6 +87,10 @@ function logDiagnostic(payload: Record): void {
}
function maybeLogFdHighWatermark(): void {
+ if (!supportsProcessDiagnostics()) {
+ return
+ }
+
const fdCount = getFdCount()
if (fdCount > fdHighWatermark) {
fdHighWatermark = fdCount
@@ -123,13 +139,18 @@ export function installProductionFetchProbe(): void {
}
export function installProductionProcessProbe(): void {
- if (!isProduction || processProbeInstalled) {
+ if (
+ !isProduction ||
+ processProbeInstalled ||
+ !supportsProcessDiagnostics() ||
+ typeof process.on !== 'function'
+ ) {
return
}
processProbeInstalled = true
- setInterval(() => {
+ const heartbeat = setInterval(() => {
maybeLogFdHighWatermark()
logDiagnostic({
event: 'process_heartbeat',
@@ -137,7 +158,16 @@ export function installProductionProcessProbe(): void {
fdCount: getFdCount(),
resourceSummary: getResourceSummary(),
})
- }, 30_000).unref()
+ }, 30_000)
+
+ if (
+ typeof heartbeat === 'object' &&
+ heartbeat &&
+ 'unref' in heartbeat &&
+ typeof heartbeat.unref === 'function'
+ ) {
+ heartbeat.unref()
+ }
process.on('unhandledRejection', (reason) => {
const message =
diff --git a/src/utils/publicImageDimensions.ts b/src/utils/publicImageDimensions.ts
new file mode 100644
index 000000000..1274143c9
--- /dev/null
+++ b/src/utils/publicImageDimensions.ts
@@ -0,0 +1,69 @@
+type ImageDimensions = {
+ height: number
+ width: number
+}
+
+const publicImageDimensions: Record = {
+ '/blog-assets/react-server-components/header.jpg': {
+ height: 701,
+ width: 1200,
+ },
+ '/blog-assets/tanstack-router-signal-graph/bundle-size-history-react.png': {
+ height: 904,
+ width: 2116,
+ },
+ '/blog-assets/tanstack-router-signal-graph/bundle-size-history-solid.png': {
+ height: 904,
+ width: 2116,
+ },
+ '/blog-assets/tanstack-router-signal-graph/bundle-size-history-vue.png': {
+ height: 904,
+ width: 2116,
+ },
+ '/blog-assets/tanstack-router-signal-graph/client-side-nav-react.png': {
+ height: 1096,
+ width: 2688,
+ },
+ '/blog-assets/tanstack-router-signal-graph/client-side-nav-solid.png': {
+ height: 1096,
+ width: 2688,
+ },
+ '/blog-assets/tanstack-router-signal-graph/client-side-nav-vue.png': {
+ height: 1096,
+ width: 2688,
+ },
+ '/blog-assets/tanstack-router-signal-graph/header.jpg': {
+ height: 1024,
+ width: 1536,
+ },
+ '/blog-assets/tanstack-router-signal-graph/store-updates-history-react.png': {
+ height: 828,
+ width: 2122,
+ },
+ '/blog-assets/tanstack-router-signal-graph/store-updates-history-solid.png': {
+ height: 828,
+ width: 2122,
+ },
+ '/blog-assets/tanstack-router-signal-graph/store-updates-history-vue.png': {
+ height: 828,
+ width: 2122,
+ },
+ '/blog-assets/who-owns-the-tree/header.jpg': {
+ height: 801,
+ width: 1200,
+ },
+}
+
+export function getPublicImageDimensions(src: string) {
+ const pathname = getPathname(src)
+
+ return pathname ? publicImageDimensions[pathname] : undefined
+}
+
+function getPathname(src: string) {
+ try {
+ return new URL(src, 'https://tanstack.com').pathname
+ } catch {
+ return undefined
+ }
+}
diff --git a/src/utils/seo.ts b/src/utils/seo.ts
index 11708e128..c995090a5 100644
--- a/src/utils/seo.ts
+++ b/src/utils/seo.ts
@@ -39,6 +39,7 @@ export function canonicalUrl(path: string, search?: string) {
const origin = trimTrailingSlash(
env.URL ||
(import.meta.env.SSR ? env.SITE_URL : undefined) ||
+ env.VITE_SITE_URL ||
DEFAULT_SITE_URL,
)
diff --git a/src/utils/shop.functions.ts b/src/utils/shop.functions.ts
index e5a0d4cc0..5e50fc2d8 100644
--- a/src/utils/shop.functions.ts
+++ b/src/utils/shop.functions.ts
@@ -77,8 +77,7 @@ function setBrowseCacheHeaders() {
setResponseHeaders(
new Headers({
'Cache-Control': 'public, max-age=0, must-revalidate',
- 'Netlify-CDN-Cache-Control':
- 'public, max-age=300, durable, stale-while-revalidate=600',
+ 'CDN-Cache-Control': 'public, max-age=300, stale-while-revalidate=600',
}),
)
}
diff --git a/src/utils/sponsors.functions.ts b/src/utils/sponsors.functions.ts
index 1dd092e02..1f7de10d8 100644
--- a/src/utils/sponsors.functions.ts
+++ b/src/utils/sponsors.functions.ts
@@ -46,8 +46,7 @@ export const getSponsorsForSponsorPack = createServerFn({
setResponseHeaders(
new Headers({
'Cache-Control': 'public, max-age=0, must-revalidate',
- 'Netlify-CDN-Cache-Control':
- 'public, max-age=300, durable, stale-while-revalidate=300',
+ 'CDN-Cache-Control': 'public, max-age=300, stale-while-revalidate=300',
}),
)
diff --git a/src/utils/stats.functions.ts b/src/utils/stats.functions.ts
index 412830364..f858c8e9f 100644
--- a/src/utils/stats.functions.ts
+++ b/src/utils/stats.functions.ts
@@ -1,6 +1,6 @@
/**
* Pure stats functions that can be used by both TanStack Start server functions
- * and Netlify functions. No TanStack Start dependencies.
+ * and Worker cron tasks. No TanStack Start dependencies.
*/
import { envFunctions } from './env.functions'
@@ -239,7 +239,7 @@ async function fetchNpmPackageDownloadsChunked(
let totalDownloadCount = 0
let lastChunkData: { day: string; downloads: number }[] = []
- // Load cache functions (dynamic import for Netlify compatibility)
+ // Load cache functions dynamically to keep the shared stats module light.
const { getCachedNpmDownloadChunk, setCachedNpmDownloadChunk } =
await import('./stats-db.server')
diff --git a/src/utils/stats.server.ts b/src/utils/stats.server.ts
index 034da2d11..88b9dcbba 100644
--- a/src/utils/stats.server.ts
+++ b/src/utils/stats.server.ts
@@ -385,8 +385,7 @@ export async function getOSSStats({
new Headers({
'Cache-Control':
'public, max-age=300, stale-while-revalidate=3600, stale-if-error=3600',
- 'Netlify-CDN-Cache-Control':
- 'public, max-age=300, durable, stale-while-revalidate=3600',
+ 'CDN-Cache-Control': 'public, max-age=300, stale-while-revalidate=3600',
}),
)
@@ -670,8 +669,7 @@ export async function fetchNpmDownloadsBulk({ data }: { data: any }) {
setResponseHeaders(
new Headers({
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=7200',
- 'Netlify-CDN-Cache-Control':
- 'public, max-age=3600, durable, stale-while-revalidate=7200',
+ 'CDN-Cache-Control': 'public, max-age=3600, stale-while-revalidate=7200',
}),
)
@@ -732,10 +730,9 @@ export async function fetchNpmDownloadChunk({ data }: { data: any }) {
setResponseHeaders(
new Headers({
- // Use Netlify-specific header for best performance
- // 'durable' shares cached responses across all edge nodes
- 'Netlify-CDN-Cache-Control': `public, max-age=${cdnMaxAge}, durable${isCurrentYear ? '' : ', stale-while-revalidate=86400'}`,
- // Also set standard Cache-Control for browser caching
+ // Keep CDN and browser TTLs separate so historical data can stay hot at
+ // the edge without forcing longer browser freshness.
+ 'CDN-Cache-Control': `public, max-age=${cdnMaxAge}${isCurrentYear ? '' : ', stale-while-revalidate=86400'}`,
'Cache-Control': `public, max-age=${cacheMaxAge}`,
}),
)
@@ -897,8 +894,7 @@ export async function fetchRecentDownloadStats({
setResponseHeaders(
new Headers({
'Cache-Control': 'public, max-age=300, stale-while-revalidate=600',
- 'Netlify-CDN-Cache-Control':
- 'public, max-age=300, durable, stale-while-revalidate=600',
+ 'CDN-Cache-Control': 'public, max-age=300, stale-while-revalidate=600',
}),
)
diff --git a/src/utils/useDeploymentProviderPlacement.ts b/src/utils/useDeploymentProviderPlacement.ts
index bd46d1427..847c938e2 100644
--- a/src/utils/useDeploymentProviderPlacement.ts
+++ b/src/utils/useDeploymentProviderPlacement.ts
@@ -1,5 +1,3 @@
-'use client'
-
import * as React from 'react'
import { getPartnersForPlacement } from '~/utils/partner-placement'
import { partners, type Partner } from '~/utils/partners'
diff --git a/src/utils/usePartnerPlacementContext.ts b/src/utils/usePartnerPlacementContext.ts
index f33e000ff..c06f205e4 100644
--- a/src/utils/usePartnerPlacementContext.ts
+++ b/src/utils/usePartnerPlacementContext.ts
@@ -1,5 +1,3 @@
-'use client'
-
import * as React from 'react'
import { useLoaderData } from '@tanstack/react-router'
import type { Partner } from '~/utils/partners'
diff --git a/vite.config.ts b/vite.config.ts
index a4dcb3773..0cdc509ab 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,19 +1,25 @@
import { sentryTanstackStart } from '@sentry/tanstackstart-react/vite'
import { defineConfig } from 'vite'
+import type { PluginOption } from 'vite'
import { redact } from '@tanstack/redact/vite'
import contentCollections from '@content-collections/vite'
import { devtools as tanstackDevtools } from '@tanstack/devtools-vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import tailwindcss from '@tailwindcss/vite'
+import { cloudflare } from '@cloudflare/vite-plugin'
import { analyzer } from 'vite-bundle-analyzer'
import viteReact from '@vitejs/plugin-react'
-import rsc from '@vitejs/plugin-rsc'
-import netlify from '@netlify/vite-plugin-tanstack-start'
import fs from 'node:fs'
+import { createRequire } from 'node:module'
import os from 'node:os'
import path from 'node:path'
+const nodeRequire = createRequire(import.meta.url)
const isDev = process.env.NODE_ENV !== 'production'
+const takumiWasmRuntimePath = path.join(
+ path.dirname(path.dirname(nodeRequire.resolve('@takumi-rs/wasm/no-bundler'))),
+ 'bundlers/workerd.js',
+)
const shouldUseRedact = process.env.DISABLE_REDACT !== 'true'
const localRedactPackageRoot = process.env.LOCAL_REDACT_PACKAGE_ROOT
const shouldUseSentryPlugin =
@@ -21,33 +27,10 @@ const shouldUseSentryPlugin =
Boolean(process.env.SENTRY_AUTH_TOKEN)
const shouldBuildSourcemaps =
shouldUseSentryPlugin || process.env.BUILD_SOURCEMAPS === 'true'
-
-const rscSsrExternals = [
- // OpenTelemetry uses require-in-the-middle which is CJS-only and breaks
- // under Vite's ESM module runner during dev SSR.
- 'require-in-the-middle',
- '@opentelemetry/instrumentation',
- // HTML parsing stack has known CJS/ESM interop issues in SSR module runner.
- 'cheerio',
- 'iconv-lite',
- 'encoding-sniffer',
- 'parse5',
- 'parse5-parser-stream',
- // Compression/archive stack has known CJS transform issues in dev SSR.
- 'jszip',
- 'pako',
- // These packages also have known CJS/ESM interop issues in the RSC/SSR path.
- 'discord-interactions',
- // OG image generation: takumi ships a native .node binary that cannot
- // be bundled by rolldown — must be externalized for SSR environments.
- '@takumi-rs/core',
- '@takumi-rs/image-response',
- '@takumi-rs/helpers',
- 'takumi-js',
-]
-
-const sentrySsrExternals = ['@sentry/node', '@sentry/tanstackstart-react']
-const dbSsrExternals = ['drizzle-orm', 'drizzle-orm/postgres-js']
+const siteUrl = process.env.VITE_SITE_URL || process.env.SITE_URL || ''
+const imageTransformationsEnv = process.env.TANSTACK_IMAGE_TRANSFORMATIONS
+const shouldUseCloudflareImageTransformations =
+ imageTransformationsEnv === 'true'
const localEnvPath = path.resolve(__dirname, '.env.local')
const defaultCheckoutEnvDir = path.join(os.homedir(), 'GitHub/tanstack.com')
@@ -57,14 +40,26 @@ const envDir =
? defaultCheckoutEnvDir
: __dirname
+function edgeTakumiWasmImport(): PluginOption {
+ return {
+ name: 'tanstack-edge-takumi-wasm-import',
+ enforce: 'pre',
+ transform(code, id) {
+ if (!id.includes('/node_modules/takumi-js/dist/render-')) return
+
+ return code.replace(
+ /import\(\s*\/\*\s*@vite-ignore\s*\*\/\s*['"]@takumi-rs\/wasm['"]\s*\)/g,
+ 'import("@takumi-rs/wasm/no-bundler")',
+ )
+ },
+ }
+}
+
// Runtime-specific `react-dom/server` variants aren't in @tanstack/redact/vite's
-// default alias map — our shim ships a single universal server build, unlike
-// React which maintains per-runtime forks (edge/node/bun/browser + static.*).
-// @vitejs/plugin-rsc and Netlify's edge adapter import them conditionally, so
-// we funnel them all to `@tanstack/redact/server` at the top-level resolve
-// (Vite 8's `EnvironmentResolveOptions` doesn't accept `alias`, so env-scoped
-// aliasing isn't an option).
+// default alias map. Funnel them all to `@tanstack/redact/server` at the
+// top-level resolve so Workers get a single server implementation.
const serverVariantAliases: Record = {
+ 'react-dom/server': '@tanstack/redact/server',
'react-dom/server.edge': '@tanstack/redact/server',
'react-dom/server.node': '@tanstack/redact/server',
'react-dom/server.bun': '@tanstack/redact/server',
@@ -79,22 +74,63 @@ const useSyncExternalStoreShimIndexAlias = {
replacement: '@tanstack/redact',
}
-// These browser-facing packages are imported by RSC assets. Bundle them into
-// server output so Netlify's Node runtime never loads their raw package entries.
+// These browser-facing packages are imported by SSR assets. Bundle them into
+// Worker server output so the runtime never loads their raw package entries.
const serverBundledClientPackages = [
...(shouldUseRedact ? ['@tanstack/redact'] : []),
+ /^@radix-ui\//,
'@kapaai/react-sdk',
+ '@tanstack/highlight',
+ '@tanstack/markdown',
+ '@tanstack/react-hotkeys',
+ '@tanstack/react-pacer',
+ '@tanstack/react-table',
+ 'lucide-react',
+ 'zustand',
/^@fingerprintjs\//,
]
+const routerSsrPackages = [
+ '@tanstack/history',
+ '@tanstack/query-core',
+ '@tanstack/react-query',
+ '@tanstack/react-router',
+ '@tanstack/react-router-ssr-query',
+ '@tanstack/react-router/ssr',
+ '@tanstack/react-router/ssr/server',
+ '@tanstack/router-core',
+]
+
export default defineConfig({
envDir,
+ define: {
+ __TANSTACK_ENABLE_SERVER_BUILDER_GENERATION__: JSON.stringify(false),
+ __TANSTACK_ENABLE_IMAGE_TRANSFORMATIONS__: JSON.stringify(
+ shouldUseCloudflareImageTransformations,
+ ),
+ __TANSTACK_SITE_URL__: JSON.stringify(siteUrl || 'https://tanstack.com'),
+ },
resolve: {
alias: [
{
find: '~',
replacement: path.resolve(__dirname, './src'),
},
+ {
+ find: 'ejs',
+ replacement: path.resolve(
+ __dirname,
+ './src/server/runtime/ejs-compat.server.ts',
+ ),
+ },
+ {
+ find: 'unicorn-magic',
+ replacement: 'unicorn-magic/node',
+ },
+ {
+ find: '@takumi-rs/wasm/auto',
+ replacement: takumiWasmRuntimePath,
+ },
...(shouldUseRedact
? [
useSyncExternalStoreShimIndexAlias,
@@ -123,37 +159,17 @@ export default defineConfig({
: undefined,
},
environments: {
- rsc: {
- resolve: {
- noExternal: serverBundledClientPackages,
- external: [
- '@tanstack/react-start-server',
- '@tanstack/react-router/ssr/server',
- ],
- },
- },
ssr: {
+ optimizeDeps: {
+ exclude: ['@tanstack/create'],
+ },
resolve: {
- noExternal: serverBundledClientPackages,
- external: [
- ...rscSsrExternals,
- ...sentrySsrExternals,
- ...dbSsrExternals,
- ],
+ noExternal: [...serverBundledClientPackages, ...routerSsrPackages],
},
},
},
ssr: {
- external: [
- 'postgres',
- ...dbSsrExternals,
- // CTA packages use execa which has a broken unicorn-magic dependency
- '@tanstack/create',
- // Externalize CLI so server reloads it on changes
- '@tanstack/cli',
- ...rscSsrExternals,
- ...sentrySsrExternals,
- ],
+ external: [],
noExternal: [
'@uploadthing/react',
'file-selector',
@@ -161,6 +177,7 @@ export default defineConfig({
'@tanstack/react-hotkeys',
'@webcontainer/api',
...serverBundledClientPackages,
+ ...routerSsrPackages,
],
},
optimizeDeps: {
@@ -176,22 +193,16 @@ export default defineConfig({
'takumi-js',
// Don't pre-bundle CLI so we always get fresh changes during dev
...(isDev ? ['@tanstack/cli'] : []),
- // `use client` libraries that plugin-rsc pre-bundles inconsistently
- // across client/ssr/rsc envs when combined with our React shim — each
- // env resolves `react` to a different target, so the optimizer's hash
- // diverges. Excluding from optimize keeps resolution deterministic per
- // env and silences the 50k+ "inconsistently optimized" warning flood.
+ // Lucide can resolve differently across Vite environments when combined
+ // with our React shim. Excluding it keeps resolution deterministic.
'lucide-react',
],
},
build: {
+ minify: 'esbuild',
sourcemap: shouldBuildSourcemaps,
reportCompressedSize: false,
rollupOptions: {
- external: (id) => {
- // Externalize postgres from client bundle
- return id.includes('postgres')
- },
output: {
manualChunks: (id) => {
if (
@@ -243,6 +254,10 @@ export default defineConfig({
},
},
plugins: [
+ edgeTakumiWasmImport(),
+ cloudflare({
+ viteEnvironment: { name: 'ssr' },
+ }),
...(shouldUseRedact
? [
redact(
@@ -278,11 +293,11 @@ export default defineConfig({
: []),
tanstackStart({
rsc: {
- enabled: true,
+ enabled: false,
},
server: {
build: {
- inlineCss: true,
+ inlineCss: false,
},
},
importProtection: {
@@ -311,11 +326,6 @@ export default defineConfig({
},
},
}),
- rsc(),
- // Only enable Netlify plugin during build or when NETLIFY env is set
- ...(process.env.NETLIFY || process.env.NODE_ENV === 'production'
- ? [netlify()]
- : []),
viteReact(),
...(shouldUseSentryPlugin
diff --git a/wrangler.jsonc b/wrangler.jsonc
new file mode 100644
index 000000000..40f718384
--- /dev/null
+++ b/wrangler.jsonc
@@ -0,0 +1,23 @@
+{
+ "$schema": "node_modules/wrangler/config-schema.json",
+ "name": "tanstack-com",
+ "account_id": "8da95258a9c70b54c3e2b374a0079106",
+ "main": "src/server.ts",
+ "compatibility_date": "2026-06-19",
+ "compatibility_flags": ["nodejs_compat"],
+ "observability": {
+ "enabled": true,
+ },
+ "limits": {
+ "cpu_ms": 300000,
+ },
+ "triggers": {
+ "crons": ["0 9 * * *", "0 */6 * * *", "*/15 * * * *"],
+ },
+ "assets": {
+ "binding": "ASSETS",
+ },
+ "vars": {
+ "SITE_URL": "https://tanstack.com",
+ },
+}