Skip to content

Client content sandbox D (do not merge) - #130

Draft
miloshroma wants to merge 11 commits into
mainfrom
sandbox-d
Draft

Client content sandbox D (do not merge)#130
miloshroma wants to merge 11 commits into
mainfrom
sandbox-d

Conversation

@miloshroma

Copy link
Copy Markdown

No description provided.

@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ideal-cms Ready Ready Preview Sep 9, 2026 11:05am UTC

Request Review

- shopifyCarousel: renders several storefront products server-side, CSS-only rail
- sidebarSection: the first two-column text layout in the block set
- /topics/[slug]: a collection item renders from a route, not from a Page document
- drop stale file headers that claimed the source was not yet applied
- generalise comments that described one particular evaluation
…tion tab

- a talk's body is a richText field, so interpolating it printed "[object Object]"
  on every unlocked item; render it through the shared RichText component
- TalkGrid, TopicChips, ShopifyProduct and ShopifyCarousel never declared `section`
  or `id`, so they collected the section tab's settings and ignored them, rendering
  flush to the viewport edge; wrap each in SectionContainer
- drop the outer margins those four carried to compensate for the missing container
- shorten the carousel card: 150px cover, title clamped to two lines
Both routes rendered no header and no footer, so a viewer who opened an
item had no navigation back to the site. Chrome is a per-Page-document
relationship, which is why a hand-written route gets none unless it asks:
these now read the header/footer slots off site settings, copying the blog
route that already does exactly this.

Topic pages move to /browse-topics/[slug], matching the segment a real
content archive of this shape already publishes, and the two hardcoded
links follow. Nothing else needed editing - nested docs, breadcrumbs,
sitemap and static params all derive from slugs.

The Topic doc comment loses a client-identifying catalogue count and two
of their real topic names; the illustration works with invented ones.

importMap.js is regenerated output: the new collections' rich-text fields
pull three lexical feature clients the committed map was missing.
Each carousel card linked to a cart permalink, which lands the buyer on
the store's cart page - one click short of the handoff the block exists to
demonstrate. Every card now posts to a server action that creates the cart
and redirects to the hosted checkout, which is what the single-product
block already did. One module-scope action serves every card: the variant
id travels in a hidden input, so nothing is captured in a closure.

The shared transport stopped caching mutations. Every Storefront call is a
POST, and Next only skips the Data Cache for a POST that carries no
explicit cache config - the transport's blanket revalidate opted the cart
mutation back in, keyed on the request body, so two buyers clicking the
same variant inside the window were handed the same cart. Reads keep their
ISR window; the mutation asks for no-store.

Both Buy actions fall back to the cart permalink when the cart cannot be
created, and log the cause. An unhandled failure would otherwise surface
as an error overlay, which is a worse outcome than the cart page.
The talk and topic routes each carried a hand-rolled generateMetadata that
returned a title and a description and nothing else, so the SEO tab's
stored values only half reached the page: no canonical, no og:*, no
twitter:*, no robots, and a stored meta image was dropped outright. Both
routes now go through generateMeta, the same path a Page document uses,
with their deliberate fallback chains resolved into the document handed to
it rather than lost.

A talk also emits its own structured data - an Article carrying the audio
as an AudioObject with an ISO-8601 duration, breadcrumbs matching a page's,
and Google's paywalled-content pattern: isAccessibleForFree with a hasPart
naming the gated region by class, so a metered item reads as metered rather
than as a truncated page. A gated talk deliberately omits contentUrl,
because applyTier nulls the audio URL before the renderer sees it and
JSON-LD ships to the browser like any other markup.

No FAQPage: the questions field holds questions with no answers, and that
markup requires an acceptedAnswer for each. The author comes from the first
speaker name on a pull quote, which is indirect on purpose - the collection
has no speaker field yet, and adding one means a migration.

buildUrl and generateMeta gained the two new collections in their unions;
both are shared with pages and posts, so the canonical output of an
existing page is worth re-checking after this deploys.
Each pull quote carried a link reading "Listen at 2:55" whose href was the
audio file with a #t= media fragment, and a comment asserting the browser
would seek without JavaScript. It does not: a media fragment applies when
the browser navigates to the media resource, not to an audio element
already on the page. Measured in a browser - the click left currentTime at
zero, and in an ordinary window it navigated away to the bare file.

It is a client button now, seeking the player by an id the page hands it
rather than by a tag selector, so a second player added later cannot be
seeked by mistake. A rejected play() is swallowed on purpose: the seek has
already landed, and a browser that blocks playback should leave the reader
pressing play rather than facing an unhandled rejection.

Also softens one field description that named the client's org type; the
generated types carry that string, so they are regenerated with it.
The SEO tab on both collections rendered a Generate button that did
nothing at all - no request, no error, no console output. A collection has
to be registered in two places: the plugin's own collections array, which
carries the field paths and the server extractor, and the client-side
extractor registry, whose keys the generate hook looks up by collection
slug. Neither knew about them, so the lookup returned undefined and the
hook bailed before its fetch, surfacing only as a tooltip on a small icon
beside the field label.

Each collection gets a content extractor built from the shared rich-text
flattener rather than a second one. A talk contributes its title, summary,
teaser, body and derived takeaways and questions; the transcript is
deliberately left out, because generation truncates its input and a
transcript would evict everything else. A topic contributes its title and
description.

No schema change: the plugin appends an admin control, a provider and an
endpoint, and stores nothing - so no migration and no regenerated types.
Generation reads OPENAI_API_KEY, the same variable the translator plugin
in this file already uses.
Both documents had a single "SEO" tab hanging below a long root-level
form, where a Page document shows "Content | SEO" at the top. The content
fields move into an unnamed tab, which in Payload is presentational: every
field inside it is still stored at the top level, so this is layout only.

Verified rather than assumed - the config builds and `generate:types`
produces a byte-identical file, so there is no schema delta and no
migration. That check matters here because the failure mode this file
already warns about, DuplicateFieldName from a mis-nested SEO field group,
typechecks clean and only appears when Payload starts.
Both Shopify blocks asked the editor for a product handle as free text -
on a deal whose whole argument is that content should stop being a product
catalogue, that is the wrong thing to ask a person to type. The handle
field now renders a picker: browse the store, filter, and click a product
with its cover and title.

The stored value does not change. It is the same text field holding the
same handle, so there is no schema delta - `generate:types` comes back
byte-identical - no migration, and every handle already in a database
keeps working.

Products deliberately do not become a collection: they live in the store
and the blocks resolve them at render time, which is the separation this
demo exists to show. So the list comes from an admin-only endpoint that
reuses the existing storefront transport, keeping the token on the server.
Every page and block added for this sandbox was written in hand-rolled
inline styles with pixel literals, so the result read as cramped and
unfinished next to the rest of the site: section headings at the same size
and weight as the paragraphs beside them, lists with their markers reset
away, a reading column that jumped 165px sideways on a click, and card
excerpts cut mid-word with no ellipsis. All of it now uses the app's own
type scale, colour tokens and shared components, so no file in this layer
carries a `style={{` object or a hex colour any more.

The product rail becomes a slider. Eight cards in a fixed-basis overflow
rail left three unreachable and sliced the fifth at the container edge;
cards are now whole fractions of the view, one to three by breakpoint,
with prev/next controls that disable at each end. The controls are not
server-rendered, so with no JavaScript the rail still scrolls and nothing
becomes unreachable.

Tier labels get one source of truth. The same document showed "Readable"
on the homepage and "Free" on a topic page, with a lock emoji on one and
not the other. The badge now states what the item requires rather than
what the current reader can see, which is what the field description
already promised, and it means a listing no longer reads the cookie and
can cache for everyone.

The locked notice stops looking like a leak and like a validation error.
It showed 300 characters of a 377-character body, cut mid-word, under
"Needs all-access. You are viewing as visitor" - now a shorter excerpt
ending in an ellipsis, with member-facing copy and the tier named the same
way the switch names it.

Behaviour is unchanged throughout: the gated body, audio and transcript
stay absent from the HTML rather than hidden, the section order stands,
the Buy button still creates a cart and redirects to hosted checkout, and
every class used was machine-checked against the compiled stylesheet.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant