Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Troubleshooting

Organised by what you observe, not by the phase you were in. Every entry is symptom → cause → fix → the page that owns the rule.

Nothing on this page is normative. Every hard number, every enforcement level and every message the platform can emit lives once in Limits and error index; this page routes you into it and into the contract that explains the fix.

Paste your error here

Search this table for a fragment of what you actually saw. {…} marks an interpolated value.

Message containsEmitted byGo to
No matching version found for @homerunner-next/widget-core@ · npm error code ETARGETnpmInstall and scaffold
Incompatible React versionsreact-domInstall and scaffold
Error: directory "{slug}" already exists. · npm install failed — you can run it manually.create-hr-pluginInstall and scaffold · index §11
Error: invalid plugin slug · is reserved — it collides with · Error: unknown template · Error: expected a plugin slug firstcreate-hr-pluginInstall and scaffold · index §11
hr-widget-build: {anything}the SDK build CLIBuild · index §11
[viteHomerunnerWidget] This project declares manifest.widgets[]the Vite presetBuild
Cannot find module '@homerunner-next/widget-core/schema'TypeScriptBuild
__HR_WIDGET_DEV__ is not definedyour bundle at runtimeBuild
dist/manifest.json (build manifest with {buildHash, files}) is missingbuilt-zip auditBuild · index §3
Missing {file} — run `npm run build` first. · a red SSR render failed: panelthe local SSR previewServer rendering
No widget selected — manifest.json declares · Unknown widget "{w}" — manifest.json declares · is CSR-only (`"ssr": false`)the local SSR preview, at bootServer rendering · index §11
ReferenceError: window is not defined · document is not definedthe SSR sandboxServer rendering
ReferenceError: crypto is not defined · TextEncoder is not defined · Response is not definedthe SSR sandboxServer rendering · index §7
Plugin cannot require "{mod}" (not in shared modules whitelist)the SSR sandboxServer rendering · index §7
[plugin] fetch to "{host}" blocked · [plugin] fetch blocked — more than 5 redirects.the SSR sandboxServer rendering · index §7
[plugin] SSR bundle has no default export: {url}the rendererServer rendering
HTTP {status} redirect refused — SSR bundles must be served directly from their published URLthe rendererServer rendering
data-hr-widget-error= in the page sourcethe rendererBlank widgets · index §6
SSR data prep failed: {msg}the rendererServer rendering · index §6
layout SSR data prep failed: {msg} · plugin layout widget rendered as contentthe rendererLayout problems
keyword does not resolve against the plugin manifestthe rendererBlank widgets
Widget ID is requiredmount()Blank widgets
[widget-core] window.HRWidgetRuntime is not available.the SDK runtimeBlank widgets
[hr-widget] "{type}" mount failed for one embed · deferred mount step failedmount()Blank widgets · index §8
[hr-widget] "{type}" render failed · data-hr-error="render-failed"the render boundaryBlank widgets
[widget-core] widget "{type}" did not register within 10000ms · [layout-csr] …a CSR layoutLayout problems
[page-layouts] feed {id} {page}: {reason} — using Automaticthe rendererLayout problems
Unsupported schema type: [object Object]the dashboard config panelConfig panel
No configSchema for this widget. · sub-manifest fetch failed (HTTP {status})the dashboard config panelConfig panel · index §10
Plugin "{slug}" not enabled for this feed. · Widget "{keyword}" is not declared by plugin "{slug}".the dashboardConfig panel
{"error":"Asset not in plugin manifest"} · {"error":"Plugin not found"}the /p/ proxyWorks locally, not live · index §9
VERSION_NOT_GREATER · SLUG_TAKEN · SLUG_RESERVED · INVALID_VERSION · INVALID_MANIFESTcentral, at submitSubmit and publish · index §2
MANIFEST_SHAPE_CHANGED · changes the plugin between single-widget and multi-widget shapescentral, at publishSubmit and publish
MANIFEST_WIDGET_CORE_MISSING · MANIFEST_WIDGET_CORE_TOO_OLD · predates the evergreen runtimepublish gatesSubmit and publish
already exists on the CDN — published versions are immutablethe dashboard uploaderSubmit and publish · index §5

Two attributes are easy to confuse when you grep: data-hr-widget-error is a hidden breadcrumb the server leaves where a widget should have been, and data-hr-error is a marker the browser runtime puts on a live host element. Look for both.


Install and scaffold

SymptomCauseFix
npm install fails with npm error code ETARGET / No matching version found for @homerunner-next/widget-core@…A pin your project asks for does not exist on the registry — usually a hand-edited range, or a private/stale registry in .npmrc. Every version this book documents is publishedInstall and versions — check npm view @homerunner-next/widget-core versions against your pin
hr-widget-build: command not found, or ls node_modules/.bin/hr-widget-build is emptyYour project resolves a widget-core older than 0.11.0 — before 0.11 the package declared no bin at allBump the pin and reinstall. On a pre-0.11 SDK the scaffold’s three-step build:manifest && build:iife && build:ssr is the only build path
Error: directory "{slug}" already exists. (exit 1)The scaffold refuses to write into an existing directoryRemove it or pick another name
npm install failed — you can run it manually. and the CLI still exits 0Install failure is deliberately non-fatalcd {slug} && npm install
Error: expected a plugin slug first — got "{arg}".argv[0] is always the slug; flags must come after itScaffold a project
Error: invalid plugin slug "{slug}" — lowercase letters, digits and hyphens only… · Error: plugin slug "{slug}" is reserved…The slug goes verbatim into manifest.id, every keyword and every CDN path, so the scaffolder applies the publish audit’s own rule up frontRename — Manifest: root fields
Error: unknown template "{t}" — expected "single" or "suite".--template (or --template=) with anything else, or with nothing usable after itThe two shapes are single and suiteThe two manifest shapes
Incompatible React versions: The "react" and "react-dom" packages must have the exact same version.A caret range on react / react-dom. Your client bundle externalises React against the page’s runtime, and the SSR sandbox hands you the renderer’s copiesPin react, react-dom and @tanstack/react-query exactly — The React-family pins
npm run dev:ssr starts fine but ignores every variable in .env.localNode below 20.12 has no process.loadEnvFile, and the harness swallows the TypeErrorUpgrade Node — Node
You wanted a suite and got one flat widgetYou omitted --template suite (the default is single), or you passed --template=suite to create-hr-plugin 0.8.0, which parsed only the space-separated form and silently scaffolded singleRe-scaffold with --template suite on 0.8.1+, or convert before your first publish — Build a suite. The shape is frozen after that (The two manifest shapes)

Build

Run npm run build. It is two steps — schema injection, then the SDK’s hr-widget-build bin — not the three-step chain older guides describe; hr-widget-build runs the iife and BUILD_SSR=1 passes for you and finalizes exactly once. Single-widget: the scaffold still declares build:iife and build:ssr as escape hatches, and a hand-run pass never reaches the finalize step. Suite: --template suite ships neither script, and a bare vite build fails by design. See Build for the single-widget shape and Build for a suite.

SymptomCauseFix
hr-widget-build: no manifest.json in the current directory.Run from somewhere other than the project rootcd to the root
hr-widget-build: vite is not installed in this project.Missing dev dependencynpm install
hr-widget-build: invalid widget slug in manifest.widgets: {json} · widget slug "{s}" is reserved. · is declared more than once.A widgets[] slug fails the slug pattern, is dist/widgets/media, or repeatsManifest: widget summary
hr-widget-build: missing entry for widget "{s}" ({path}).No src/widgets/{slug}/index.tsx for a declared summaryCreate the entry, or drop the summary — Build a suite
[viteHomerunnerWidget] This project declares manifest.widgets[] — build it with `hr-widget-build` (which loops BUILD_WIDGET per widget), not a bare `vite build`.A suite built through a bare vite build — usually the leftover build:iife / build:ssr scriptsUse npm run build; delete those two scripts from a suite
dist/manifest.json missing, or the publish gate says dist/manifest.json (build manifest with {buildHash, files}) is missing — run the widget-core build.Only the last Vite pass hashes dist/, writes the build manifest and stamps runtime.widgetCore. A hand-run single pass never gets thereRe-run the whole npm run build. Never patch dist/manifest.json by hand
A rebuilt widget still serves last build’s filePer-widget passes run with emptyOutDir: false, so a hand-run pass leaves stale output beside the new filesFull npm run build — it clears dist/ once, up front
manifest.json shows up dirty in git after every buildBy design: schema injection then the runtime.widgetCore stamp both rewrite the tracked fileCommit it. The stamp is a publish gate — Packaging and publishing rules
Cannot find module '@homerunner-next/widget-core/schema' (or /runtime, /plugin-schema, …) from tsc, while Vite builds fineYour tsconfig.json resolves modules the pre-exports way, so the package’s subpath map is invisibleSet "moduleResolution": "bundler" (the scaffold’s value). The subpath list is in SDK reference
__HR_WIDGET_DEV__ is not defined at runtimeThe bundle was built without viteHomerunnerWidget, which defines that literalBuild through the preset — /vite and hr-widget-build
mockServiceWorker.js appears in your published dist/Vite copies publicDir into outDir on every build. Both scaffold templates turn it off for builds since create-hr-plugin 0.8.0; a project scaffolded earlier does not have that lineAdd publicDir: command === "build" ? false : "public" to vite.config.ts — but do not delete the file, the dev sandbox needs it. The worker file
npm run preview shows a heading and an empty boxThe dev page’s only script was the Vite dev entry, which the static server 404s, so nothing ever evaluated. Fixed in create-hr-plugin 0.8.0: the entry now carries an onerror that boots the runtime IIFE and your built bundle insteadRe-scaffold, or copy the bootPreview() block and the /p/{slug}/{file} route out of the current template’s index.html / serve.js. Note preview talks to live Central with no mocks, so point the host id="{widgetId}:{feedId}" at a real pair

Before you zip, run the publish gates you can run locally: the script in Run the reviewer’s gate yourself checks declared assets, hashed twins, the HRPlugins marker, base-schema extension and declared media/ files.


Dev server

Everything here is npm run dev — the offline Vite sandbox on :3001 with MSW-mocked data. It is documented in Dev sandbox and mocking.

SymptomCauseFix
Vite reports Port 3001 is in use, trying another oneAnother dev server holds the portHarmless — the dev CSS routes are root-relative, so they follow the new origin. Just use the port Vite prints
The widget mounts unstyledYour CSS import is redirected to a no-op virtual module in dev and served at a URL instead; check /dist/{slug}/{slug}.css (single-widget) or /dist/{slug}/{widget}/{widget}.css (suite) actually returns your rulesDev-server URLs
Saving a .css file changes nothingThe server emits a custom hr-widget-css-update event that cache-busts the <link> inside the shadow root; a hard reload confirms whether the build is staleDev-server URLs
Every request escapes to the networkThe MSW worker did not install, usually after an msw upgradenpx msw init public. Never delete public/mockServiceWorker.jsThe worker file
The scenario buttons do nothing and none of them lights up?scenario= is cast, never validated: an unknown value behaves like default and leaves the row unlitScenarios
Scenario buttons switch but the old rows stay on screenThe panel resets every query whose key does not start with "widget". Your own keys starting with "widget" are sparedNamespace query keys with your plugin slug — Query-key discipline
?mock=off gives data-hr-error="widget-fetch-failed"dev-widget:1 exists on no real feedPut a real {widgetId}:{feedId} in index.htmlReal staging data
In a suite, every layout slot child resolves to the layout itselfstartMockWorker is a page singleton bound to ONE keyword, and the built-in /widget/{id} handler echoes it for every idUse the widget fixture factory, keyed by the requested id — what --template suite ships. Suites
In a suite, two widgets show the same value for a settingOne page, one mock settings object — the suite sandbox seeds it with the union of every widget’s schema defaults, so same-named fields collideRename the field, or check that widget on its own in npm run dev:ssr -- --widget {slug}, which seeds only its schema — Suites
The error scenario shows your fixture, not an errorScenario gating short-circuits before your fixture runs, and only the data endpoints are gatedWhy the widget always mounts

Server rendering

The renderer evaluates your built SSR bundle in a node:vm realm with no DOM. The contract is Component and SSR module; the local harness and its divergences are Previewing SSR locally.

ReferenceError: window is not defined (or document, navigator, localStorage). The sandbox seeds those seven globals as inert Proxy stubs, and a property read off a stub returns another stub, which is truthy. So if (document) and if (window.matchMedia) both pass on the server and then blow up on the real call. Only one guard form works:

// Verified in packages/homerunner-renderer/lib/plugin-federation.ts (makeBrowserGlobalStub)
// and the SSR build's typeof rewrite.
if (typeof window === "undefined") return;   // rewritten to "undefined" at build, then DCE'd

The SSR build rewrites typeof on window, document, self, navigator, localStorage and sessionStorage to the literal "undefined", so esbuild deletes the branch and the imports inside it. Truthiness guards keep the dependency in the bundle. Full table: Browser-global stubs, and the guard that actually works.

SymptomCauseFix
ReferenceError: crypto is not defined (also TextEncoder, TextDecoder, Buffer, Response, structuredClone, queueMicrotask, performance)Not seeded. There is no Web Crypto and no crypto.randomUUID() in the sandboxBundle a pure-JS replacement, or move the work into the browser. Inventory: Globals
It works in npm run dev:ssr and throws TextEncoder is not defined on the first customer pageYou are on widget-core below 0.12.2, which seeded TextEncoder/TextDecoder in the harness while the renderer never had them. 0.12.2 removed both, so the throw now happens locallyUpgrade the SDK, then bundle a pure-JS replacement — Where the preview differs from production
Plugin cannot require "{mod}" (not in shared modules whitelist)Exactly seven specifiers resolve; everything else must be bundled into your SSR fileShared modules (7)
That same throw for @homerunner-next/widget-core/runtime, but only locallyYou are on widget-core below 0.12.2, which omitted /runtime from the harness’s whitelist while the renderer shared it. Fixed in 0.12.2Upgrade the SDK. Keeping server code off /runtime — a browser surface — is still the safer habit
[plugin] fetch to "{host}" blocked — not declared in the widget's externalFetch manifest field.Server-side fetch to a host outside Central ∪ your declared list. Nothing validates that list at publish, so a typo only shows up on a live pageexternalFetch
[plugin] fetch to "{host}" blocked — private/metadata hosts are never allowed.Loopback, private ranges, *.local, link-local and metadata hosts are refused even when declaredSame
[plugin] fetch blocked — more than 5 redirects. · [plugin] fetch blocked — unparseable URL: {raw}Redirect chain too long, or a non-URL passed to fetchSame
A widget renders stale server data for up to a minuteok GET responses are cached across renders outside development modeBound in Runtime bounds; bypass with ?dev=1
HTTP {status} redirect refused — SSR bundles must be served directly from their published URL · a bare HTTP {status}Your ssr.url 3xx’d or returned non-2xxExecution bounds
[plugin] SSR bundle has no default export: {url}ssr-entry.ts exports no default. Mandatory for every widget, layouts includedThe entry file
SSR bundle failed to load from {url} with nothing else in the logOften the module-scope execution timeout: synchronous top-level work exceeded the vm bound. Async work is not bounded by itMove work into getInitialData; give every request an AbortSignal.timeout(…). Bound: Runtime bounds
SSR data prep failed: {msg}A throw in getInitialData, dehydrateState, getStaticAssets or your component’s server render — all four sit in one tryParse ctx.options with parseWidgetConfig and default everything: Failure semantics
[widget] SSR prefetch FAILED — shipping loading markup. and the widget renders a spinnerprefetchQuery swallows errors by design; the page is marked degradedRender a real empty state, not a spinner, when a query has no data — Prefetch errors only warn
Missing {file} — run `npm run build` first. or a red SSR render failed: panel locallynpm run dev:ssr reads dist/ and compiles nothing. {file} is dist-relative, so a suite’s reads {widget}/{widget}-ssr.umd.jsBuild first — Running it
npm run dev:ssr fails at boot with No widget selected… or Unknown widget "{w}"…A suite has no default widget, and the selection is validated against manifest.json before the server startsName one: npm run dev:ssr -- --widget {slug}Naming a widget
npm run dev:ssr fails at boot saying the widget is CSR-onlyThe correct outcome: a widget whose summary declares "ssr": false has no SSR bundle and no ssr-entry.ts. Exact string in index §11Check it in the browser sandbox instead — Dev sandbox and mocking
A layout previews with all its slots emptyNot a bug and not fixable locally: renderPluginSSR passes no renderedSlots, and only the platform renders a layout’s bound childrenVerify composition on a real feed — Suites: one widget at a time

When a widget is simply absent from a rendered page, view-source and search for data-hr-widget-error. The renderer leaves a hidden node whose HTML comment carries the reason; each reason maps to a cause in index §6. A page containing one is flagged degraded and its cache TTL collapses, so it is not stuck — it re-renders shortly.


Hydration mismatches

React discards the whole server DOM on a mismatch and re-renders client-side. The symptom is almost never an error: it is a visible flash, an interaction that only works after a beat, or SSR markup that is silently thrown away. The contract is The hydration contract.

SymptomCauseFix
Server HTML is replaced wholesale on hydrate; useId-derived ids (Radix aria-*, form ids) differThe server renders each widget as an isolated root inside WidgetHydrationTree with identifierPrefix = the host’s data-hr-ssr-id. Any drift in wrapper shape or prefix regenerates the treeNever call hydrateRoot yourself — Why you must not call hydrateRoot yourself
Output differs between server and client for no obvious reasonprops.widgetType is absent on the server and on the hydrate path, and present only on pure CSR. Same for Math.random(), Date.now(), any window readRender deterministically — What each path actually passes
Dark mode is correct on CSR and light forever on a server-rendered pageresolvedTheme derives from a cookie the edge worker never forwards, so it is "light" on every production SSR — and mount() hydrates with the server’s props, so it stays lightBranch in CSS off the host attribute — The theme contract
Visible server content that never becomes interactive, sitting over an invisible second render — and it looks fine in some browsersThe manifest says shadowDOM: true and your mount() passes false. The browser attaches the server shadow root at parse; your light-DOM render is never displayedMake the two agree — When shadowDOM disagrees
The widget client-renders even though the server clearly emitted markupA component whose entire server output is a hoistable tag (<style>, <link>, <script>, <meta>, <title>, <base>) reads as “no server markup”Nest server output inside a real element — Shadow DOM lifecycle
[widget] SSR state could not be inflated; widgets fetch client-side.The page’s dehydrated-state block was malformed or blocked; the page degrades to client fetching rather than breakingCheck the page’s inline scripts are not being rewritten or CSP-blocked — The DOM contract
data your getInitialData returned arrives mangledIt travels as JSON in an inline script; Date, Map and functions do not surviveReturn JSON-serialisable values only
You cannot reproduce a hydration bug locallynpm run dev never takes the SSR branch at all. npm run dev:ssr in hydrate mode does apply WidgetHydrationTree and the identifier prefix since widget-core 0.12.2 — below that it rendered bare with no prefix, so useId drift was neither reproducible nor trustworthyUpgrade the SDK and run HR_SSR_HYDRATE=1 npm run dev:ssr; the remaining gap is Declarative Shadow DOM and the props registry — Where the preview differs

Blank widgets and error states

Start by reading the host element. data-hr-error is set by the browser runtime; its complete vocabulary and what clears each value is The data-hr-error vocabulary.

Value on the hostWhat it meansWhere to look
render-failedYour component threw during render. By far the most common cause is reading props.options without parseWidgetConfigOptions arrive RAW
mount-failedA throw inside mountOne — classically a missing or non-colon-joined id — or a rejected deferred hydrate/render stepIdempotency and per-embed isolation
widget-fetch-failedThe widget-config fetch failed. A non-colon-joined id makes feedId NaN and 404s the fetchAttributes mount reads
rate-limitedThe config fetch returned 429Rate limit
property-not-foundA CSR layout was pointed at a property slug that does not exist on the feedLayout problems

The raw-options trap, spelled out. props.options arrives unparsed on both the server and the client — plugins are deliberately excluded from the renderer’s settings parse. Raw settings carry null for a cleared field, [] for an emptied map, stringified numbers and a legacy width object. Calling configZod.parse() on that throws; parseWidgetConfig strips, coerces and lifts first. Call it in the component and in every SSR export:

// pdp-suite/src/widgets/stay-hero/widget.tsx (real, published)
const cfg = parseWidgetConfig(configZod, props.options ?? {});

Reproduce it locally by clearing a field to null or "" in the dev panel’s settings editor — see Testing options-updated.

SymptomCauseFix
Nothing happens at all: no error, no data-hr-mounted, no console linemount selects on [data-hr-widget="{widgetType}"] inside the container you pass. A wrong keyword matches nothing and returns silentlyCheck the keyword on the host equals config.widgetType exactly — mount(container, config)
[widget-core] window.HRWidgetRuntime is not available. Make sure the runtime.iife.js <script> tag loads before your widget entry.Script order on a hand-written embedRuntime first, then your bundle — Boot order
The bundle 200s in the network tab and never executescrossorigin="anonymous" on a /p/ script tag. /p/ 302s off-host to a target with no ACAO, so the browser rejects the final response in CORS modeDrop the attribute — The crossorigin trap
The bundle executes but the component never rendersThe registry key must be the keyword minus plugin:. The publish audit only greps for the literal HRPlugins, so a bundle registered under the wrong key passes and fails silentlyRegistration
Widget ID is requiredThe host has no id. It must be the colon-joined "{widgetId}:{feedId}"The embed shape
Fixing the DOM and calling mount again does nothingdata-hr-mounted is written before the id check, so a failed embed keeps the markerRemove the attribute, or call el.__hrWidgetUnmount()The unmount handle
The failure node’s reason is keyword does not resolve against the plugin manifestThe stored keyword does not match the manifest shape: a bare plugin:{slug} against a suite, a widget slug against a v1 manifest, or an unknown/kill-switched slugShape mismatches fail closed
A live edit in the dashboard preview does nothingOnly the CSR paths listen for options-updated; a hydrated SSR widget installs no listenerLive preview
One embed broke and the others are fineWorking as designed — each element mounts inside its own tryIdempotency and per-embed isolation
A CSR-only widget leaves a tall gap, or none at allpreloadMinHeightHint returns null for every plugin:* keyword, so nothing is reserved unless you supply a heightSet expectedHeight (Manifest: widget summary) or data-hr-min-heightAnti-CLS preload reservation

Empty or wrong config panel

SymptomCauseFix
An amber banner: No configSchema for this widget. Ask the plugin developer to ship one via zodToManifestSchema(widgetSchema.extend({...})) in the widget's config.ts.The sub-manifest is missing from the CDN, 404’d, timed out, or carries no configSchema. It also appears when the browser cannot rebuild a zod object from your JSON Schema ([plugin-playground] jsonSchemaToZod failed: in the console). This degrades; it is never an errorPer-widget sub-manifests
sub-manifest fetch failed (HTTP {status}) in the consoleThe declared widgets/{slug}.manifest.json is not at that path under the published version prefixIt must be committed and shipped in both zips — What must be committed
The whole config page crashes with Unsupported schema type: [object Object]z.any(), z.unknown(), z.literal(), z.tuple(), z.intersection() or z.discriminatedUnion() survived the JSON-Schema round trip into the dashboard’s unguarded default-value walkerNever ship those six. Model variants as a z.enum() discriminator plus flat optional fields — Breaks the whole panel
Plugin "{slug}" not enabled for this feed.The plugin is not installed on that feedInstall, customers and kill switches
Widget "{keyword}" is not declared by plugin "{slug}".A stored keyword the current manifest no longer resolves — usually a slug you renamed or removedDeprecate instead of deleting — deprecated and replacedBy
A built-in panel (Filter, Spacing, Section…) lost its controlsYou re-declared a base key in your uiSchema. The adapter replaces a top-level key wholesale — it does not deep-mergeNever re-declare a base key
Fields inside a panel are labelled translations.viewDetailsThe default label is the full dotted path; there is no humanisationDeclare label on every nested field — The uiSchema
Your uiSchema key order is ignoredPlugins cannot reorder anything; order is droppedMove the field in the zod schema — The uiSchema
Everything the customer types into a record field fails your runtime parseThe key/value editor writes stringsKeep record values z.string()Degrades silently
A cross-field .refine() never fires in the dashboardzodToJsonSchema erases refinements; the form’s validation is cosmetic anywayEnforce it in parseWidgetConfig and degrade rather than throw — Reading options back
A changed schema default does not reach existing widgetsThe seeded object is autosaved on the customer’s first edit, so your defaults became stored valuesDefaults at widget creation
The panel is fine but standard theme/spacing controls are missingYour configSchema does not extend the base widgetSchema. This is a publish warning, visible only to the reviewerBuild config.ts as widgetSchema.extend({...})The base schema

Layout problems

Layouts are suite-only and need widget-core 0.11.0+. The full contract is Layout widgets.

SymptomCauseFix
Your layout is not offered under Feed → Page URLs → Page layoutsIt appears only when the plugin is approved and enabled on that feed, the widget is not kill-switched, and the summary declares that page in pagespages and page assignment
The customer’s slot picks vanish on saveThere is nowhere to store them: your zod config has no slots object fieldAdd it, keys matching the manifest slot names exactly — The mandatory slots config field
One slot renders and another never doesSlot names must match character for character between the manifest summary, the zod field and (for presets) the preset’s slots keys. Preset slot keys are never validatedsettings.slots · Presets
A slot is empty and renderedSlots.hero is undefinedAn unbound slot is absent, not empty. On an assigned page only the keys present in the stored bindings are rendered — nothing is backfilled for a plugin layoutAlways props.renderedSlots?.[name] ?? nullThe component contract
The layout shell is inert on a server-rendered page: no clicks, no stateKnown gap. The renderer emits no data-hr-ssr-id and no props-registry entry for a shell, and the client mount is a deliberate no-op — this is correct behaviour, not a broken mountPut interactivity in a content widget bound into a slot — SSR: the server composes your children
The whole customer page 500sA layout component that throws during render is not contained: the shell is rendered later, in the page-level renderToString, which has no tryNever throw in a layout’s render path. Parse defensively, default every field and every slot — Failure semantics
A breadcrumb reason of plugin layout widget rendered as content — needs a layout entry with slotsA category: "layout" widget was placed as ordinary contentManifest: category
layout SSR data prep failed: plugin layout widgets require an SSR bundle (`ssr: false` is content-only)The summary declares "ssr": false. The publish audit lets that through; the renderer does notAn SSR bundle is mandatory for a layout — Requirements at a glance
The server render replaces itself with an empty client rendercategory: "layout" was omitted from both registerPluginWidget and mountRegistration and mount
The layout stylesheet restyles the customer’s whole pageOn a server-rendered page the shell is light DOM and its CSS is loaded document-levelClass-namespace every selector, never rely on :host, never paint a background — Layout CSS
A CSR slot child stays empty; [widget-core] widget "{type}" did not register within 10000msIts IIFE 404’d or never registered. The layout still rendersCSR: the layout composes its own children
[layout-csr] Skipping nested plugin layout child "{keyword}" — layouts cannot nest.A layout was bound into a slot. Refused on both render pathsLayouts cannot nest
[layout-csr] Property "{slug}" was not found in feed {id}. and data-hr-error="property-not-found"The one whole-layout failure: a permanent 4xx on the property checkFix the layout’s Property setting or the embed’s data-hr-options filter
accepts: ["layout"] matches nothing in the slot pickerKnown gap — the picker excludes layouts before accepts is evaluated, and accepts is unvalidated at publishKnown gaps, collected
A listings / checkout / confirmation page 404s outrightThose pages pass only a slot named content, and 404 when it resolves to no live widgetName a slot exactly contentWhat the renderer does with an assignment
[page-layouts] feed {id} {page}: {reason} — using AutomaticThe renderer could not honour the assignment and fell back rather than failingRead the reason — Degrading to Automatic
You deprecated an assigned layout and it keeps renderingKnown gap: the dashboard flags it, the renderer does not check deprecatedAsk the customer to switch that page back to Automatic
A dialog inside a layout shell never opensOn a server-rendered page there is no shadow root and no portal container in a shellLayout widgets have no portal container

Styling and dark mode

SymptomCauseFix
The widget renders with no styles at all on a live pageYour assets.css never resolved — a /p/ 404, a wrong dist-relative path, or a crossorigin attribute on the tagTwo serving paths · The crossorigin trap
A widget briefly renders unstyled, then stylesThe shadow CSS gate resolves regardless after its timeout; CSS never blocks hydration. A failed sheet logs [HRWidget] Failed to load CSS in shadow DOM: <href>Shadow DOM lifecycle
The customer’s page loses all its margins, padding and bordersTailwind preflight. On a server-rendered page your stylesheet is also appended to <head>, so its universal selector resets their whole documentImport Tailwind without preflight — The Tailwind stack
Your .card / .title rules collide with the host pageSame reason: on a composed page every selector you ship is a global selectorNamespace every class — Where your CSS actually lands
Dark mode follows the visitor’s OS instead of the configured colour schemeTailwind’s dark: variant compiles to @media (prefers-color-scheme: dark)Key off the host attribute instead — The theme contract
Your dark arm silently never matches:host([data-hr-scheme="auto"]):not([data-hr-theme]) parses fine and matches no shadow host. The :not() must be inside :host(...)Four footguns
The dark rule loses to the light rule:where() holds specificity at zero, so the dark selector has to string-match the light oneSame
auto still flashes light before hydratingYou did not declare mediaSafeAuto, or the request was not in a DSD bucketDark at first paint · mediaSafeAuto
A dialog opens as unstyled browser-default HTML over the pageThe portal target fell through to document.body, which is outside your shadow rootTwo portal targets
A dialog is almost right — a white panel over a dark widgetThe elevated portal clones only link[rel="stylesheet"]. Everything you rendered as an inline <style> (accent, font, customCss) is absent, so every token falls backBuild the token string once and pass it as cssString too — The token trap
Your @font-face is ignored@font-face does not work inside a shadow rootDeclare the stylesheet in assets.fonts, which is injected at document level — The two injection paths
Declared fonts arrive after first paint on a server-rendered pageYou export getStaticAssets, and declared fonts are appended only on the manifest pathOmit getStaticAssets unless you truly need it — Component and SSR module
A font you edited into manifest.json is not loadingThe build bakes the font list into the client banner at config timeRebuild after editing — Web fonts
An imported image ships as a huge data: URLVite library mode inlines everything unless tagged ?no-inline; the preset only tags listed extensionsKeep unusual asset types off the import graph — Bundled assets
spacing, font, accent colours or customCss have no effectThe platform applies none of the base fields for youRender them yourself — Rendering the base fields yourself

Submit and publish rejections

Three gates, and you only watch two of them: Preflight and submit walks all three, and every code and message is in index §1–§5.

SymptomCauseFix
The uploader lists zip problems and never transmitsThe source-zip audit runs in your browser: structure, entries, sizes, and the manifest’s shapeThe source zip · index §1
VERSION_NOT_GREATER after a clean browser auditThe browser checks the version’s shape, never its ordering against the live versionVersion rules
INVALID_VERSION on something a looser parser accepts, e.g. 1.0Central demands exactly three numeric partsSame
SLUG_TAKEN · SLUG_RESERVEDAnother author owns the slug, or it is an unowned legacy rowChecked after the bytes are in the bucket
INVALID_MANIFEST for a manifest your browser acceptedCentral applies rules the browser does not — length caps on id, name, version, description, author, plus the widget count ceilingManifest: root fields
MANIFEST_SHAPE_CHANGED — at publish, after review passedA live plugin can never flip between the single-widget and suite shapes. It is not checked at submit or at reviewPublish the new shape under a new slug — The manifest shape is frozen at first publish
MANIFEST_WIDGET_CORE_MISSING / _TOO_OLD, or manifest has no "runtime.widgetCore" stamp — this build predates the evergreen runtime.The build’s stamp is missing or below the floor. It is written by the last build pass; never by handUpdate widget-core, rebuild, commit manifest.jsonruntime
Declared icon {ref} is missing from the zip. (also cover, screenshot, README, sub-manifest)media/ and widgets/*.manifest.json are authored content. Nothing generates them, and the reviewer only runs your buildCommit them into the source zip — Media references
{slug}@{version} already exists on the CDN — published versions are immutable.Version prefixes can never be overwrittenResubmit with a bumped version — Immutability and retry safety
A file you shipped is simply not on the CDNAnything more than one directory deep under dist/, widgets/ or media/ is dropped silently; an unusual file name is dropped with a reviewer-only warningKeep the SDK’s output layout — The publishable surface
Your plugin shipped with an empty settings form and nobody told youKnown gap: built-zip warnings are visible only in the reviewer’s consoleRun the local gate script before you zip — Run the reviewer’s gate yourself
Silence after submittingKnown gap: there is no notification of any review outcome, everPoll Dashboard ▸ Plugins ▸ My plugins — From your laptop to a customer page
Everything published, and every cache optimisation is goneYou published a pre-release version like 1.0.0-rc.1. Every immutability optimisation keys off a bare X.Y.Z segmentUse plain three-part versions — Known gaps

It works locally but not on the customer site

The single most productive question: which of the two harnesses did you verify in, and what does it not simulate?

It works in……and breaks live becauseRead
npm run devThe sandbox seeds settings with configZod.parse({}), so every field is present and typed. Customers’ stored settings arrive rawOptions arrive RAW
npm run devIt never converts your configSchema back to zod, so a round-trip breaker looks fine here and empties the customer’s panelWhat survives the manifest round-trip
npm run devIt always takes the CSR branch: no DSD, no props registry, no hydrationWhat the sandbox does not cover
npm run dev on a suiteIt composes a layout’s slots the client way, from bindings in mocks/fixtures.ts. A server-rendered page composes them in the renderer and hands the shell renderedSlots in light DOMSuites
npm run dev:ssrThe harness pre-parses options, points __HR_PLUGIN_ASSET_BASE__ at localhost, emits props as an attribute rather than the page registry, and never composes a layout’s slots. On widget-core below 0.12.2, add: seeded TextEncoder/TextDecoder, no /runtime, no externalFetch enforcement, no execution timeoutWhere the preview differs from production
Either harnessNeither emits Declarative Shadow DOM or the page-level props registryHydration mismatches
A CSR embedOn a composed page your stylesheet is also in <head>, unisolatedWhere your CSS actually lands
A CSR embed of a layoutThe CSR layout gets a shadow root and full providers; the server-rendered shell gets neither, and its mount no-opsHow a layout renders

And when the plugin is live but a page still disagrees with you:

SymptomCauseFix
{"error":"Asset not in plugin manifest"} from /p/The file is neither a manifest-declared asset nor a key of your dist/manifest.json. In a suite it must also sit under a directory named for a declared widget slugThe allow-list
{"error":"Plugin not found"} from /p/Unknown slug, or the plugin is not approved — a suspension 404s every fileThe three kill switches
One widget of your suite stopped rendering everywhere, with no message anywhereA platform per-widget kill switch strips its summary from every public surface and 404s its entire dist/{widget}/ directorySame page
One widget stopped rendering on one feed onlyThe customer’s per-feed toggle. Assets keep serving; only that feed’s instances stop resolvingSame page
A widget you retired keeps rendering on existing pagesThat is what deprecated does — it hides the widget from the picker and leaves placements alonedeprecated is not a kill switch
You published and customers still see the old buildCache, not publishing. The publish fan-out purges what it can reach inside its budget; anything it misses waits out the page TTLHow an update reaches a customer
A page mixes new markup with an old bundleIt cannot. Server-rendered pages embed content-addressed URLs, so a cached page keeps loading exactly the bundles it was rendered withA stale page is stale consistently
A live page broke after old version prefixes were prunedDeleting an old prefix is the one operation that can break a cached pageWhat holds a copy of your old build
A 429, or data that stops arriving under loadThe public API is rate limited per IP, shared across every endpointRate limit

Older versions of this guide told you to call a refresh_manifest API to clear a stale manifest. Do not look for it: it is an administrator-only action with no author-facing surface. Publishing records the manifest and triggers the warm-and-purge fan-out; there is nothing for you to invalidate.


Seeing what the server actually did

Four signals, all owned by Seeing what the server actually did on a real page: development mode (?dev=1, which bypasses caching for a feed), the <!-- hr-render … --> stamp at the end of the page source, the x-hr-render-* headers, and view-source for data-hr-widget-error.

Two more things worth knowing while you read a live page:

  • Your own console.* calls inside the SSR bundle go to the renderer’s stdout, not to the browser. Ask an operator to grep the [plugin], [widget] and [layout] prefixes listed in index §8.
  • Check what is actually live before you debug anything else: Checking a release yourself reads your published manifest.json and dist/manifest.json straight off the CDN.

Still stuck