Add the Forward Weaverse contract and section inventory - #66
Merged
Conversation
Defines page-role ownership, theme settings, resource selectors, cache boundaries, and the Shopify-loader versus Weaverse-composition seam for issue #65. The section inventory is derived from Forward's real page composition at main@3972c8d. Documentation only: no dependency, project connection, or Studio composition.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Closes the three open questions on the Weaverse contract. Header and Footer are never Weaverse global sections. They stay theme-owned components configured through theme settings, so their keyboard, focus-trap, inert, body-lock, and fail-soft contracts stay in code and under test rather than behind merchant-editable data. This is a permanent decision, not a deferred slice, so the theme-settings surface is now grouped by global surface instead of listing the six flat ThemeContent fields. Studio owns the content of /about, /materials, and /field-testing, not just their layout. Copy and imagery become section settings and the routes stay in the repo; none is converted into a Shopify page. The Shared Contract version disagreement between builder#2660 and epic #2663 is waived as a stale citation rather than a scope difference.
LEDE_CLASS, SHELL_SECTION_CLASS, and VIEWPORT_SECTION_CLASS were declared per page, so every route that used the same shell repeated the string. They move beside CONTROL_TRANSITION, which is already an exported class constant. The normalized rich-text run renderer existed twice, once in the custom page route and once reimplemented inline in the policy route. FilterSidebar was a local function inside the Shop route but is a component, not a section.
Every route now composes named section components instead of inlining its markup. 35 sections, each imported back into the page it came from. Net -1048 lines, and no rendered output changes. Sections are pure presentation: content and data arrive as props and the pages keep their storefront reads, so Weaverse settings map onto props one-for-one when the schema lands. Extraction found reuse the section inventory had not predicted: - editorial-callout — Materials and Field Testing closed with identical markup - index-header — Shop, Journal, and the policy routes each repeated the same dark masthead - search-empty-state — the no-query and no-match states were one block twice It also corrected the inventory. editorial-hero was recorded as shared by all three editorial routes; About and Materials do share it, but Field Testing's hero is a full-bleed overlay with an absolute image and an after: scrim, so it becomes editorial-overlay-hero. Contract boundaries hold: the PDP buy block, the colorway/size query state, and the collection grid behavior stay theme-owned and were not extracted. Cart already delegates to CartView, and the account routes have no page-level sections, so both are untouched. One source-regex assertion pointed at the article route for the RichTextRuns markup; it now points at src/sections/article-body.tsx, where that markup lives.
Records what the extraction corrected in the section inventory, the reuse it found that the inventory had missed, and the verification state — including that the local Bun is 1.3.6 against the repo's pinned 1.3.14, so the DOM suite's 25 failures are a toolchain mismatch and are identical before and after the refactor.
The connection gate is open, so Weaverse moves from "outside that approval" into the approved set for the current ordered slices. Its guardrails move in with it: install only an exact registry-verified version and never the stale npm latest tag, keep the Shopify data seam and the Weaverse composition seam separate so no credential or raw payload reaches a Studio payload, and leave pageview transport and deduplication to the SDK. Routes now compose named sections from src/sections/ rather than inlining markup, so the rule belongs here: a section is pure presentation, content and data arrive as props, sections never import the data source, and a section used by more than one route takes its variations as props instead of forking into a near-copy. The surfaces that are not sections are named too — the PDP buy block and its query state, the collection and Shop grid behavior, Cart, /account/**, and Header/Footer. The no-Pilot constraint is clarified, not weakened: adopting a documented Weaverse platform pattern is a platform decision, and still does not license reading Pilot source. Verification said `bun test`, which skips the DOM suite's preload; it now says `bun run test`. The Bun expectation is pinned to packageManager with the failure mode an older Bun produces, so the next agent does not chase phantom DOM failures.
The plan described a paper model. It now describes what src/sections/ actually contains, and records the three places the markup disagreed rather than quietly patching them: - editorial-hero was listed as shared by all three editorial routes. About and Materials do share it, differing only in image side, but Field Testing's hero is a full-bleed overlay with an absolute image and an after: scrim, so it is editorial-overlay-hero. - Four kinds of reuse were missed: editorial-callout, index-header, search-empty-state, and the rich-text run renderer. - Per-role counts were wrong. COLLECTION renders four sections not three, PRODUCT has one composable section not five, and ARTICLE has two not three. No section was invented to match the table. article-related stayed out because the route does not render it, and the four PDP disclosure panels stayed inside the buy block because they are one stack, not four sections. Also documents the theme-owned routes extracted for code organization without becoming composable, and reorders the implementation steps so extraction sits before the install, with schema work mapping onto props the components already take.
Extraction is now inside this spec's scope, with the reason it moved ahead of the install: it needs no dependency, no Weaverse project, and no credentials, so nothing about it was blocked. Schema exports, a section registry, and the src/lib/weaverse/ seam are excluded explicitly — they belong to the connection slice, written against the real SDK contract. Records the post-extraction state beside the original baseline, renumbers the phase gates so the completed extraction is Phase 2 and the connection slice is Phase 4, and notes the Bun version requirement in the verification section.
Records what changed in the repository guidance and why, including that the no-Pilot constraint was clarified rather than weakened, and that the verification list had been calling the wrong test command.
The -1048 figure measured only the route side and read as a repo-wide shrink; src/ actually grew 955 lines net as markup moved into 35 section files. Adds the static browser matrix result the extraction entry was missing, and records that the DOM suite passes 66/66 on a matching Bun, so the 25 logged failures were the older local Bun alone.
Derives .env.example from Forward's own env modules, the @weaverse/next source, and the Next POC, and documents each group's fail-closed semantics. Records that the SDK's publicEnv payload would expose PUBLIC_STOREFRONT_API_TOKEN to the browser, which sits outside the current approval.
Corrects the install shape: @weaverse/next alone, since alpha.16 already depends on react and schema. Adds the decision to hand the SDK an allowlisted env object rather than process.env, because the SDK's publicEnv payload is built from PUBLIC_STORE_DOMAIN and PUBLIC_STOREFRONT_API_TOKEN.
The key is not a storefront runtime input; it belongs to a local admin-data seeding script, so it does not belong in the theme's template. Also records that a non-default WEAVERSE_HOST moves the SDK's API base off the production edge proxy rather than only changing the Studio URL.
Exact registry-verified version pinned without a range; the alpha dist-tag resolved to 0.1.0-alpha.16 at install time and npm latest remains a stale 0.1.0-alpha.0. No composition wired yet, so this commit only proves the dependency is inert: check passes and the production audit is clean across 71 packages.
Adds the server-only boundary, the section schemas, and the component registry. The env boundary does not hand process.env to the SDK. The SDK resolves config with readEnv(env, key), which falls back to process.env whenever a key is absent from the object it was given, so a short allowlist would block nothing. Instead every key the SDK reads is supplied explicitly, with PUBLIC_STOREFRONT_API_TOKEN, WEAVERSE_API_KEY, and WEAVERSE_PUBLIC_API_BASE blanked, which keeps the public token out of the SDK's browser-visible publicEnv payload. A test asserts the key list against the installed SDK bundle so a new SDK key cannot slip through unnoticed. Page and theme reads fail soft: an unconfigured project, a network failure, a missing page, or a Builder fallback placeholder all yield null and the route keeps its theme-owned rendering. The registry omits Header, Footer, the PDP buy block, grid behavior, Cart, and account by design.
Transcribes the content the routes render today into scripts/weaverse-seed/*.json and adds bun run seed:weaverse to push it through the Content API. Dry run by default: --apply is required before any write, and only that path reads WEAVERSE_API_KEY. Section types are validated against the component registry before the first request, so a typo cannot create an item the theme has no component for, and item ids are a deterministic digest of page and section key so a second run merges into the same items instead of appending duplicates. Failures report status and endpoint only, never a response body that could echo the key.
Colocates every section schema with its component, splits theme settings into one file per group with types derived from the declarations, and registers Heading, Subheading, Paragraph, and Button as shared Weaverse elements built on the existing presentation recipes. Leo authorized reading Pilot for file organization on 2026-09-08. Only layout conventions were adopted: schema beside component, settings per group, shared text and CTA elements. No Pilot code, markup, or visual design was ported, and AGENTS.md now records the exception and its limit instead of leaving the old blanket prohibition contradicted.
The registry reads default off each module, which the global named-exports rule already allows as a must-use case, so the earlier two-argument entry() workaround was unnecessary. The 21 registered components now default-export and entry() takes the module alone, removing the chance of pairing a schema with the wrong component. The 18 sections that are not Weaverse components keep named exports.
…hname Three defects, all found by calling the live API rather than trusting the code. The editorial routes asked Weaverse for PAGE, which maps to a Shopify page; a theme-owned route at its own path is CUSTOM. PAGE/about returns the project's shared default template with an empty handle, which the empty-page filter then failed to reject because it matched on the id containing fallback — a real default template has an ordinary id and a childless root. Detection now judges the content: no authored child sections means not composed yet. The request context also supplied no pathname, so the SDK would have resolved every CUSTOM lookup against the home page; pathname is now required. The seed script sent only PATCH, but creation and update are separate Content API endpoints and a fresh project has neither page. It now creates first and treats 409 as already-exists. Records that /pages/[pageHandle] and /journal/[articleHandle] are still unwired, and why.
The first apply reported success and still produced empty pages: the Builder creates each page with its own root item, and the script invented a deterministic root id instead of using it. The page kept pointing at the original root, which had no children, so the Content API listed the sections while the storefront rendered nothing. The script now reads the real root id and attaches to it, and theme settings are skipped unless --with-theme is passed, since nothing reads them yet. Verified end-to-end against a production build: /about renders the seeded heading and all four sections, and product-strip resolves Weatherline, Traverse, and Drift through the picker, the loader, and the storefront data source.
A composed page crashed on next/image because Builder stores { url, altText } while the theme renders { src, alt }; the shapes look interchangeable and are not. The seeded payloads also carried no image at all.
The real mistake was making ten sections composable without auditing their required props: a merchant can clear any field in Studio, so every setting being absent is an ordinary state, not an edge case. weaverseImage() normalizes either shape or returns null, and a DOM test now renders all fourteen composed components with no props, plus Builder's image shape and a dimensionless image. Restoring the unnormalized read turns three of those red.
Verified on a production build at the URL that failed: all three routes return 200 with an image and no error page.
The new DOM test imported expect from bun:test, which the repo's minimal ambient declaration does not expose, so typecheck failed. Converted to node:assert like every other DOM suite. This was pushed broken because the verification grep did not match the tsc error line; check now passes in full.
Studio could not connect or outline the page. Four gaps: StudioConnect was mounted in head rather than body; routes never forwarded searchParams, which is how design mode is detected; the client client was built without a request context, so the bridge had no route identity; and no component spread the runtime's data-wv-* props onto its root element. That last one is the dangerous kind: the storefront rendered perfectly while Studio saw a single selectable item instead of five, so nothing storefront-facing could catch it. A real design-mode request now carries five identity attributes per page instead of one. Guarding it exposed a conflict — product-case-study returned null with no product selected, which is correct on the storefront but leaves a merchant unable to select the section to choose one. It now renders a selectable placeholder only when the runtime is rendering it.
…yload Follows the POC rather than deriving from it. The per-item revalidation route had no counterpart here, so a merchant editing a section would see stale loader data until a full reload — a component loader otherwise only runs during a page load. Verified mounted: an empty body returns 400 invalid-payload. The client now prefers the project id the payload was loaded with, falling back to the environment, so the bridge cannot target a different project than the one the page came from. The root theme provider and theme-settings CSS variables stay absent on purpose: nothing reads theme settings yet, and they belong to the slice that wires Header and Footer.
Studio's runtime crashed reading i18n.language and pageId: the server request context supplied only headers, pathname, and searchParams, while the bridge also needs url, i18n, pageType, and handle. Those four travel to the client in configs.requestInfo. i18n reuses the catalog client's CATALOG_I18N rather than declaring a second market table, and url is built from the forwarded host because resolveRequestUrl prefers it and a bare pathname resolves against localhost. Every Studio break today has been a field the bridge needs and the storefront never reads, so no storefront-facing check could catch them.
Six Studio defects shipped today with every gate green, because each was a field the bridge reads and the storefront does not. Sorting them by what would actually catch each showed five were cheap. Extracts page-payload.ts and request-info.ts from server.ts, which imports server-only and calls headers() and so could not be tested outside Next. Neither piece of logic needs a request, so the split stands on its own. The new tests reproduce the exact shipped payloads: a default template with a childless root, a missing i18n, missing pageType and handle, and a bare pathname. Three file-level guards cover the revalidation route, the script's placement in body, and searchParams forwarding; sabotaging each turns exactly one red. The revalidation route is asserted directly rather than added to the route contract, which tracks no API routes and whose 20 + 4 counts other suites pin.
The proxy has to decide before rendering starts whether a path is a custom page: once a dynamic route begins streaming the response is already a 200, so notFound() inside it can only produce a soft 404. Next's documentation says the real status has to be decided in the proxy. Calls the public discovery endpoint, which needs no API key, and stays free of server-only, next/headers, and the SDK because none of them exist in the proxy. Fails open: with no usable listing every path is admitted and the renderer decides, so an outage costs a soft 404 on invented URLs rather than a hard 404 on real pages.
Merchants create custom pages at paths this repository cannot know ahead of time, so there is no longer a route file per page. The renderer deliberately does not sit at the app root. A dynamic root catch-all matches every unclaimed URL, which turned unknown product, collection, article, page, and policy handles from real 404s into soft ones — six route-contract smokes caught that. It lives behind an internal prefix instead, and the proxy rewrites only the paths Weaverse actually publishes, so every other URL keeps Next's ordinary routing and its ordinary 404. The proxy runs again on its own rewrite, so a marker header distinguishes an internal rewrite from a visitor addressing the internal prefix directly; the latter gets the same 404 as any unknown path, leaving each custom page exactly one public URL.
The three editorial pathnames now share one renderer route, so the contract pins them by smoke path rather than by pattern, and the footer and Studio guards follow.
loading.tsx wrapped every route in a Suspense boundary, so Next streamed a shell and committed the response to 200 before any component ran. notFound() could then only produce a soft 404 — which is exactly what the docs describe, and why the Next POC, which has no loading.tsx, returns real 404s from the same call. Six routes call notFound(); none of them could set a status. They answered 404 only because dynamicParams = false rejects unknown params before rendering starts.
With notFound() working again, the renderer no longer needs an invented prefix or a proxy that filters paths against a cached Weaverse listing. A route that declines an unknown param falls through to this catch-all, which answers a real 404, so unknown product, collection, article, page, and policy handles keep their status. Deletes the listing cache and its tests, and returns proxy.ts to being only the account boundary.
The three editorial routes are Weaverse-composed and absent without a configured project, so the credential-free matrix can no longer assert their content. gotoReady waited for the loading text to disappear, which doubled as a hydration signal; with the boundary gone that assertion was vacuous and the mega-panel test began clicking an unwired trigger. The test now retries the click until the panel opens, which is both more honest and faster than waiting for network silence — networkidle took the matrix from 1.4 to 7.4 minutes and still failed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Contract, section extraction, and the first Weaverse connection for #65, tracked by Weaverse/builder#2660 under epic Weaverse/builder#2663.
Studio is working end to end: sections can be selected, edited, saved, and the change survives a reload.
What it decides
Page-role ownership. Weaverse composes the editorial surfaces. The theme keeps
/shop,/journal,/search,/cart,/account/**,/policies/**, and the resource routes, because they own pagination, ranking, server state, security, or verbatim legal text.Header and Footer are never Weaverse global sections. They stay theme-owned components configured through theme settings, so their keyboard, focus-trap, inert-background, body-lock, and fail-soft contracts stay in code and under test.
Home's seven sections stay Server Components.
@weaverse/next@0.1.0-alpha.16cannot render server-side — its renderer holds context and subscribes to a store — so composing a section ships its JavaScript to the browser. On the highest-traffic page that buys nothing a shopper can see.The PDP buy block and the collection grid stay theme-owned, because they own variant identity, the
colorway/sizequery state, and the checkout handoff.What it ships
src/sections/, as a pure refactor ahead of the install.@weaverse/next@0.1.0-alpha.16pinned exactly, with the composition seam beside the storefront seam rather than through it.{ id, handle }; loaders resolve those handles through the storefront data source, never Shopify directly, so static mode still renders./about,/materials,/field-testingcomposed and seeded, plusbun run seed:weaverseand the per-item Studio revalidation route.Two findings worth reading
The env boundary cannot be a short allowlist. The SDK's
readEnvfalls back toprocess.envfor any key missing from the object it is handed, so every key it reads is now supplied explicitly, withPUBLIC_STOREFRONT_API_TOKENblanked. That keeps the public token out of the SDK's browser-visiblepublicEnv. A test asserts the key list against the installed bundle.A root
loading.tsxwas suppressing real 404s. It wrapped every route in a Suspense boundary, so Next streamed a shell and committed the response to200before any component ran;notFound()could then only produce a soft 404. Removing it restored real 404s across seven routes and deleted about 200 lines of machinery that existed only to work around it.Verification
bun install --frozen-lockfileunchanged ·bun run checkat 370 node + 95 DOM tests, 42-page build, compiled theme, route contract 20 + 4 ·smoke:routes35/35 ·test:browser:static146 passed / 10 intentional skips / 0 failures ·bun audit --productionclean.Known gaps, recorded in the spec
/pages/[pageHandle](PAGE) and/journal/[articleHandle](ARTICLE) are not wired: they are resource-backed templates whose content arrives from the route, which needs the renderer'sdataContextseam this PR does not build.presetsare not defined yet, so a section added in Studio starts empty.