Skip to content

feat(tanstack-start): collapse admin root integration to a single shell touch point#17037

Merged
jacobsfletch merged 2 commits into
experiment/framework-adapter-patternfrom
feat/tanstack-app-dir-abstractions
Jun 18, 2026
Merged

feat(tanstack-start): collapse admin root integration to a single shell touch point#17037
jacobsfletch merged 2 commits into
experiment/framework-adapter-patternfrom
feat/tanstack-app-dir-abstractions

Conversation

@jacobsfletch

@jacobsfletch jacobsfletch commented Jun 17, 2026

Copy link
Copy Markdown
Member

Reduces the Payload and TanStack Start integration to a single touch point: a shell component attached to the root route.

Now, we attach a single shellComponent to the root route:

import { withPayloadRoot } from '@payloadcms/tanstack-start/client'

export const Route = createRootRoute({
  head: () => ({ /* ... */ }),
  shellComponent: withPayloadRoot(MarketingRoot), // Single integration point
})

function MarketingRoot() {  // Safe, isolated tree to riff on your app
  return (
    <html>
      <body>
        ...
      </body>
    </html>
  )
}

With this setup, each shell manages its own HTML tree, and Payload-specific implementation details are completely abstracted away.

Behind the scenes the withPayloadRoot HOC wraps the consumer's own root document shell. Within /admin routes it renders Payload's admin document shell; everywhere else it renders the consumer's shell.

Background

We have to remove as many developer touch points as possible, so that installing Payload into your TanStack app is as few steps as possible with clearly defined abstractions that hide away Payload-specific implementation details.

Underpinning this: in TanStack Router, the root route is shared with your public-facing site, so every Payload touch point is exposed to your marketing shell — complicating setup and inviting drift.

This is different from Next.js, which supports multi-root layouts. This setup is very nice, specifically because it allows you to simply "drop-in" a set of static files into your existing app, and it just works.

Collapsing the touch points into one owned abstraction keeps your public site free of Payload concerns and moves the wiring into @payloadcms/tanstack-start, where it is versioned once — nothing is left in the consumer's app ot manage.

Previously a consumer had to wire up 3 specific things:

  1. Call the root layout loader
  2. Subscribe to it in their root component
  3. Thread the results appropriately through the root HTML

Before

export const Route = createRootRoute({
  loader: () => getInitialHtmlAttrsFn(), // Touch point 1
  head: () => ({ /* ... */ }),
  component: RootComponent,
})

function RootComponent() {
  const { dir, languageCode, theme } = Route.useLoaderData() // Touch point 2

  return (
    <html
      data-theme={theme} // Touch point 3
      dir={dir}
      lang={languageCode}
      suppressHydrationWarning
    >
      {/* head + body + Outlet + Scripts */}
    </html>
  )
}

…ll touch point

Adds `withPayloadRoot`, an HOC for a TanStack Start root route's
`shellComponent` that renders Payload's admin document shell on `/admin`
routes and the consumer's own shell everywhere else. This removes the
three prior integration touch points (root loader, loader subscription,
manual html threading) in favor of one:

  shellComponent: withPayloadRoot(MarketingHtml)

`PayloadAdminShell` owns the admin `<html>` chrome (no-flash theme
bootstrap script, `@layer` ordering, HeadContent, Scripts), and
`THEME_INIT_SCRIPT` / `buildThemeInitScript` set `data-theme`/`lang`/`dir`
from the theme/lng cookies before first paint — replacing the server-side
root theme loader. All exported from `@payloadcms/tanstack-start/client`.

Updates the tanstack test app to the new shape: thin `MarketingHtml`
shell, `HydrationMarker` extracted and moved into the `_payload` layout,
and the now-unused root theme loader removed.
@github-actions

Copy link
Copy Markdown
Contributor

Pull Request titles must follow the Conventional Commits specification and have valid scopes.

Unknown scope "tanstack-start" found in pull request title "feat(tanstack-start): collapse admin root integration to a single shell touch point". Scope must match one of: cpa, claude, codemod, db-*, db-d1-sqlite, db-mongodb, db-postgres, db-vercel-postgres, db-sqlite, db-d1-sqlite, drizzle, email-*, email-nodemailer, email-resend, eslint, evals, graphql, kv, kv-redis, live-preview, live-preview-react, live-preview-vue, next, payload-cloud, plugin-cloud, plugin-cloud-storage, plugin-ecommerce, plugin-form-builder, plugin-import-export, plugin-mcp, plugin-multi-tenant, plugin-nested-docs, plugin-redirects, plugin-search, plugin-sentry, plugin-seo, plugin-stripe, richtext-*, richtext-lexical, sdk, skills, storage-*, storage-azure, storage-gcs, storage-r2, storage-uploadthing, storage-vercel-blob, storage-s3, translations, ui, templates, examples(/(\w|-)+)?, deps.

feat(ui): add Button component
^    ^    ^
|    |    |__ Subject
|    |_______ Scope
|____________ Type

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

📦 esbuild Bundle Analysis for payload

This analysis was generated by esbuild-bundle-analyzer. 🤖

Meta File Out File Size (raw) Note
packages/next/meta_index.json esbuild/index.js 201.84 KB 🆕 Added
packages/payload/meta_index.json esbuild/index.js 1.39 MB 🆕 Added
packages/payload/meta_shared.json esbuild/exports/shared.js 212.71 KB 🆕 Added
packages/richtext-lexical/meta_client.json esbuild/exports/client_optimized/index.js 306.71 KB 🆕 Added
packages/ui/meta_client.json esbuild/exports/client_optimized/index.js 67.97 KB 🆕 Added
packages/ui/meta_shared.json esbuild/exports/shared_optimized/index.js 18.66 KB 🆕 Added
Largest paths These visualization shows top 20 largest paths in the bundle.

Meta file: packages/next/meta_index.json, Out file: esbuild/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ████████████████████████▋ }}}$ 98.9%, 197.86 KB
dist/adapters/router.js ${{\color{Goldenrod}{ }}}$ 0.3%, 663 B
dist/adapters/server.js ${{\color{Goldenrod}{ }}}$ 0.3%, 533 B
dist/adapters/layout.js ${{\color{Goldenrod}{ }}}$ 0.3%, 526 B
dist/adapters/views.js ${{\color{Goldenrod}{ }}}$ 0.2%, 409 B
dist/esbuildEntry.js ${{\color{Goldenrod}{ }}}$ 0.0%, 0 B

Meta file: packages/payload/meta_index.json, Out file: esbuild/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ █████████████████▎ }}}$ 69.3%, 960.50 KB
dist/fields/hooks ${{\color{Goldenrod}{ ▊ }}}$ 3.2%, 43.88 KB
dist/collections/operations ${{\color{Goldenrod}{ ▋ }}}$ 2.9%, 40.22 KB
dist/auth/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 15.63 KB
dist/utilities/configToJSONSchema.js ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 14.93 KB
dist/globals/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 13.36 KB
dist/fields/config ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 13.22 KB
dist/queues/operations ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 12.63 KB
dist/fields/validations.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 10.66 KB
dist/bin/generateImportMap ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 9.53 KB
dist/collections/config ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 9.21 KB
dist/config/orderable ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 8.07 KB
dist/index.js ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.96 KB
dist/uploads/fetchAPI-multipart ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.80 KB
dist/hierarchy/utils ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.64 KB
dist/database/migrations ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 7.55 KB
dist/collections/endpoints ${{\color{Goldenrod}{ }}}$ 0.4%, 6.12 KB
dist/queues/config ${{\color{Goldenrod}{ }}}$ 0.4%, 5.59 KB
dist/auth/strategies ${{\color{Goldenrod}{ }}}$ 0.4%, 5.43 KB
dist/auth/endpoints ${{\color{Goldenrod}{ }}}$ 0.4%, 5.31 KB
(other) ${{\color{Goldenrod}{ ███████▋ }}}$ 30.7%, 425.87 KB

Meta file: packages/payload/meta_shared.json, Out file: esbuild/exports/shared.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ██████████████████ }}}$ 72.1%, 150.13 KB
dist/fields/validations.js ${{\color{Goldenrod}{ █▎ }}}$ 5.1%, 10.66 KB
dist/fields/config ${{\color{Goldenrod}{ ▋ }}}$ 2.8%, 5.81 KB
dist/utilities/traverseFields.js ${{\color{Goldenrod}{ ▌ }}}$ 2.1%, 4.44 KB
dist/collections/config ${{\color{Goldenrod}{ ▍ }}}$ 1.5%, 3.21 KB
dist/config/orderable ${{\color{Goldenrod}{ ▍ }}}$ 1.5%, 3.13 KB
dist/fields/baseFields ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 2.79 KB
dist/utilities/deepCopyObject.js ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 2.69 KB
dist/config/client.js ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 2.68 KB
dist/auth/cookies.js ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 1.55 KB
dist/utilities/flattenTopLevelFields.js ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 1.41 KB
dist/utilities/getVersionsConfig.js ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 1.04 KB
dist/globals/config ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 939 B
dist/utilities/flattenAllFields.js ${{\color{Goldenrod}{ }}}$ 0.4%, 793 B
dist/utilities/unflatten.js ${{\color{Goldenrod}{ }}}$ 0.4%, 779 B
dist/utilities/sanitizeUserDataForEmail.js ${{\color{Goldenrod}{ }}}$ 0.3%, 713 B
dist/auth/extractJWT.js ${{\color{Goldenrod}{ }}}$ 0.3%, 696 B
dist/utilities/getFieldPermissions.js ${{\color{Goldenrod}{ }}}$ 0.3%, 651 B
dist/errors/ValidationError.js ${{\color{Goldenrod}{ }}}$ 0.3%, 577 B
dist/bin/generateImportMap ${{\color{Goldenrod}{ }}}$ 0.3%, 561 B
(other) ${{\color{Goldenrod}{ ██████▉ }}}$ 27.9%, 58.16 KB

Meta file: packages/richtext-lexical/meta_client.json, Out file: esbuild/exports/client_optimized/index.js

Path Size
dist/features/blocks ${{\color{Goldenrod}{ ███ }}}$ 12.2%, 37.13 KB
dist/lexical/ui ${{\color{Goldenrod}{ ██▊ }}}$ 11.3%, 34.16 KB
dist/lexical/plugins ${{\color{Goldenrod}{ ██▋ }}}$ 10.8%, 32.88 KB
dist/features/experimental_table ${{\color{Goldenrod}{ ██▏ }}}$ 8.9%, 27.16 KB
dist/packages/@lexical ${{\color{Goldenrod}{ █▌ }}}$ 6.3%, 18.99 KB
dist/features/link ${{\color{Goldenrod}{ █▌ }}}$ 6.2%, 18.83 KB
dist/features/toolbars ${{\color{Goldenrod}{ █▍ }}}$ 5.5%, 16.58 KB
dist/features/upload ${{\color{Goldenrod}{ █▏ }}}$ 4.6%, 14.09 KB
dist/features/textState ${{\color{Goldenrod}{ ▉ }}}$ 3.7%, 11.08 KB
dist/features/relationship ${{\color{Goldenrod}{ ▊ }}}$ 3.2%, 9.61 KB
dist/lexical/utils ${{\color{Goldenrod}{ ▋ }}}$ 2.9%, 8.79 KB
dist/features/converters ${{\color{Goldenrod}{ ▋ }}}$ 2.8%, 8.36 KB
dist/utilities/fieldsDrawer ${{\color{Goldenrod}{ ▋ }}}$ 2.7%, 8.12 KB
dist/features/debug ${{\color{Goldenrod}{ ▌ }}}$ 2.4%, 7.40 KB
dist/lexical/config ${{\color{Goldenrod}{ ▍ }}}$ 1.7%, 5.14 KB
dist/features/lists ${{\color{Goldenrod}{ ▍ }}}$ 1.6%, 5.00 KB
dist/lexical/LexicalEditor.js ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 3.55 KB
dist/features/format ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 3.46 KB
dist/features/horizontalRule ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 3.18 KB
dist/field/Field.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 2.83 KB
(other) ${{\color{Goldenrod}{ █████████████████████▉ }}}$ 87.8%, 266.32 KB

Meta file: packages/ui/meta_client.json, Out file: esbuild/exports/client_optimized/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ █████████████▊ }}}$ 55.3%, 32.35 KB
dist/exports/client ${{\color{Goldenrod}{ ███████████▏ }}}$ 44.7%, 26.20 KB

Meta file: packages/ui/meta_shared.json, Out file: esbuild/exports/shared_optimized/index.js

Path Size
dist/graphics/Logo ${{\color{Goldenrod}{ ███████▋ }}}$ 30.9%, 5.57 KB
../../node_modules ${{\color{Goldenrod}{ ███▋ }}}$ 14.7%, 2.65 KB
dist/graphics/Icon ${{\color{Goldenrod}{ ██▏ }}}$ 8.5%, 1.52 KB
dist/utilities/formatDocTitle ${{\color{Goldenrod}{ █▊ }}}$ 7.3%, 1.32 KB
dist/providers/TableColumns ${{\color{Goldenrod}{ █▏ }}}$ 4.8%, 866 B
dist/utilities/getGlobalData.js ${{\color{Goldenrod}{ █ }}}$ 4.2%, 762 B
dist/utilities/api.js ${{\color{Goldenrod}{ █ }}}$ 4.2%, 756 B
dist/utilities/groupNavItems.js ${{\color{Goldenrod}{ █ }}}$ 4.1%, 745 B
dist/elements/Translation ${{\color{Goldenrod}{ ▋ }}}$ 2.7%, 493 B
dist/utilities/handleTakeOver.js ${{\color{Goldenrod}{ ▌ }}}$ 2.4%, 440 B
dist/utilities/traverseForLocalizedFields.js ${{\color{Goldenrod}{ ▌ }}}$ 2.3%, 419 B
dist/elements/withMergedProps ${{\color{Goldenrod}{ ▍ }}}$ 1.9%, 339 B
dist/utilities/getNavGroups.js ${{\color{Goldenrod}{ ▍ }}}$ 1.9%, 338 B
dist/utilities/getVisibleEntities.js ${{\color{Goldenrod}{ ▍ }}}$ 1.8%, 329 B
dist/elements/WithServerSideProps ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 232 B
dist/utilities/handleGoBack.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 180 B
dist/fields/mergeFieldStyles.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 157 B
dist/utilities/handleBackToDashboard.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 152 B
dist/forms/Form ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 148 B
dist/utilities/abortAndIgnore.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 146 B
(other) ${{\color{Goldenrod}{ █████████████████▎ }}}$ 69.1%, 12.47 KB
Details

Next to the size is how much the size has increased or decreased compared with the base branch of this PR.

  • ‼️: Size increased by 20% or more. Special attention should be given to this.
  • ⚠️: Size increased in acceptable range (lower than 20%).
  • ✅: No change or even downsized.
  • 🗑️: The out file is deleted: not found in base branch.
  • 🆕: The out file is newly found: will be added to base branch.

Comment thread packages/tanstack-start/src/layouts/Root/withPayloadRoot.tsx Outdated
`startsWith(adminRoute)` matched sibling paths like `/admin-root`. Match
exact `/admin` or `/admin/` prefix instead.
@jacobsfletch jacobsfletch merged commit 688c4d0 into experiment/framework-adapter-pattern Jun 18, 2026
260 of 274 checks passed
@jacobsfletch jacobsfletch deleted the feat/tanstack-app-dir-abstractions branch June 18, 2026 13:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants