- richTextSection on Products now renders inside the same max-w-[48rem]
mx-auto the fact-cards/crossSell card already use — the "Was kann er?"
heading sat edge-to-edge while the cards under it were capped, still
reading as unconstrained (reported 2026-08-30).
- "Vintage-Porzellan"'s medallion and "Für den einen Moment"'s hourglass
still didn't land (reported 2026-08-30) — replaced with a literal
pottery/vase silhouette and a single star, reusing the same 'seal'/
'hourglass' enum keys already assigned to Tasse's blocks so no new
migration is needed, just new icon content behind the same keys.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Eg6h91yngXmSnM51wxXM8
der-eine's original Focus.tsx used max-w-[75rem] — read too wide/loose
for a 3-4 card row once ported to the block system (reported
2026-08-30). Narrowed to max-w-[48rem], matching the blog body column
(RichText.tsx) and the crossSell "Passt dazu" card's own width.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Eg6h91yngXmSnM51wxXM8
Products.ts's admin.livePreview (added earlier this session) only made
the admin UI show a preview iframe — the actual page had no mechanism
to receive postMessage updates and re-render with unsaved draft data,
unlike Pages/Posts which already have this (LivePageContent.tsx/
LivePostContent.tsx). Edits in the backend were invisible in the
preview until an actual save (reported 2026-08-30).
New LiveProductContent.tsx mirrors LivePageContent.tsx's pattern
exactly: useLivePreview<PayloadProduct> + mapPayloadProduct, rendering
via ProductBlocks.tsx's now-exported renderProductBlock (sync) instead
of the full async ProductBlocks wrapper. testimonialsRef/crossSell
skipped in the live subset (need an async fetch a client component
can't perform inline) — same "editable subset only" scope Pages/Posts
already accept. shipping/tax/Kleinunternehmer context is fetched once
via the newly-exported getProductBlockContext() and passed in rather
than re-fetched per keystroke.
/der-eine and /tasse-die-pause now check draftMode() and swap in
LiveProductContent instead of the normal async ProductBlocks when
Payload's Live Preview iframe has it enabled.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Eg6h91yngXmSnM51wxXM8
- pen/sparkle/compact icons were carried over from der-eine's original
32x32-viewBox SVGs but rendered at their native 32px instead of the
64px der-eine's own IconBadge wrapper actually used — half the
intended size on /der-eine's fact cards (reported 2026-08-30).
- The PDP black-icon override only recolored `stroke`; target's/
ProductSpotlight-unrelated icons using a hardcoded `fill="#f6a701"`
center dot (e.g. IconTarget) kept a stray yellow dot inside an
otherwise-black icon on /einfach-anfangen. Added a second override
targeting exactly that hardcoded fill value.
- 3 new icons (seal/droplets/hourglass) for Tasse "Die Pause."'s fact
cards — cup/shield/checkCircle/pause didn't fit "Vintage-Porzellan"/
"Spülmaschinenfest"/"Für den einen Moment" at all.
- ProductSpotlight (homepage) now uses the shared ProductBadge too —
missed in the earlier badge-consolidation pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Eg6h91yngXmSnM51wxXM8
- CrossSellBlock: dropped mode (automatic/manual) + hasMany products
down to one required `product` relationship, same filterOptions-
excludes-self dropdown as Products.relatedProduct — the "automatic,
same category" mode added complexity a plain dropdown already covered
better, and the frontend's card was always designed around one
recommendation, not a grid (reported 2026-08-29).
- New shared ProductBadge component (app/components/ProductBadge.tsx),
used by both ProductBlocks.tsx (PDP) and ProductCard.tsx (shop grid,
cart's RelatedProducts, wishlist grid) — Products.badge ("Neu") only
showed on the PDP before, now consistent everywhere a product's image
renders a badge.
- ProductGallery's arrow/thumbnail buttons get an explicit cursor-pointer
class.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Eg6h91yngXmSnM51wxXM8
ProductCard (price, wishlist heart, add-to-cart button) read as a
commerce grid, too busy for a single lightweight recommendation slot —
der-eine's own hand-coded PasstDazu.tsx (and the blog's "Passend dazu"
card) already had the right, more editorial look: bordered card,
thumbnail + name + description + "Entdecken" arrow, no price/buy
button. Reused that exact markup here instead, stacked rather than a
grid. getWishlistEnabled/ProductCard now unused in this file, removed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Eg6h91yngXmSnM51wxXM8
der-eine was the reference the whole PDP block-builder was designed to
match (gallery sizing, black stepRow-facts icons, dynamic CTA label —
see ProductBlocks.tsx). Migrated it onto its own system: hero body,
usps (the 3 hero features), a "Was kann er?" fact-card section (its own
4 icons — pen/sparkle/compact, new; sunHigh, reused — carried over into
the shared StepIcons set), pricing panel, automatic crossSell.
Testimonials.tsx dropped, not migrated (testimonialsRef removed from
the PDP builder earlier this session). Old Hero/Focus/Testimonials/
Pricing/PasstDazu components left as dead code, same as
/tasse-die-pause's own migration.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Eg6h91yngXmSnM51wxXM8
Product deleted from Payload (no orders referenced it), so its bespoke
page went with it — Navbar's WERKZEUGE_ROUTES and the sitemap no longer
list it. The blog's "3minuten" relatedProduct pointer and the homepage's
"ToDo-Karten" Werkzeuge card were cleared/removed in Payload directly
(no code change needed there — CMS content).
Also fixes a real bug found in the process: ProductBlocks.tsx's
AddToCartButton calls never passed productId, silently falling back to
its "todo-karten" default — every block-driven PDP's buy button (e.g.
"Die Pause.") was adding the wrong product to the cart.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Eg6h91yngXmSnM51wxXM8
der-eine is the reference the whole PDP block system is meant to look
like — three concrete deviations fixed:
- Gallery was full-bleed across the whole lg:col-span-7 column; der-eine
caps it at max-w-[28rem], centered, vertically centered at lg: — same
treatment now, same reasoning (der-eine's own Hero.tsx comment on why).
- Hero CTA was a generic "In den Warenkorb" instead of "${product.name}
bestellen" like der-eine's/todo-cards' own buttons (the closing
ProductPricingPanel button already said the generic label correctly —
matches der-eine's own two-CTA pattern, hero named, pricing-panel
generic).
- stepRow icons render brand-yellow (STEP_ICONS, shared with Pages.layout
content pages that want that) instead of der-eine's black
(Focus.tsx's #231f20). Recolored via a CSS descendant-selector override
scoped to ProductBlocks.tsx's own stepRow case, not touching the shared
icon set or PageBlocks.tsx.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Eg6h91yngXmSnM51wxXM8
Quick-access signup stays in the modal; a small "mehr erfahren" link
under the description sends anyone not ready to type an email straight
to /newsletter (testimonials, how-it-works) instead of only being
reachable via the Navbar — the same gap the blog's newsletter-fallback
card closed for readers, closed here for the modal's own audience.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Eg6h91yngXmSnM51wxXM8
Wires up the new PDP page-builder gaps (Products.layout backend already
built, migrated in a companion payload repo commit):
- storySplit/faq render via PageBlocks.tsx (shared with Pages.layout);
crossSell is Products-only, resolved server-side in ProductBlocks.tsx
(manual: getProductsByIds; automatic: active products sharing a
category, falling back to any other active product).
- ProductGallery gets an optional `badge` overlay slot; ProductHero now
uses it (previously a single plain <Image>, no badge at all) instead
of duplicating a second image element.
- New shared ProductBadge helper (discount % / Ausverkauft / Neu) used
by both ProductHero and ProductPricingPanel — only the pricing panel
showed this before.
- product.usps (max-3 icon+text) renders in the hero, reusing
StepRowBlock's icon set via STEP_ICONS.
- AddToCartButton gets an opt-in showQuantityStepper prop (default off,
no behavior change for existing callers); enabled on both PDP
buy buttons.
- quote gets a PDP-specific full-bleed brand-background treatment,
scoped to ProductBlocks.tsx's own switch case so Pages.layout's
existing quote styling (e.g. /ueber-mich) is untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Eg6h91yngXmSnM51wxXM8
The link/autolink converters never read node.fields.newTab, so setting
"open in new tab" on a link in the Payload editor had no effect.
Needed now that the Die Sieben post's template-download link opens a
PNG in-place instead of navigating away from the article.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Eg6h91yngXmSnM51wxXM8
About section goes back to the original wide banner image (its own
new file, about-banner.jpg) — the new bjoern.png headshot is a square
close-up that never suited this wide/short strip. Blog posts keep the
new photo via about-author.jpg, unaffected.
Also fixes /blog/[slug]'s "Weiterlesen" card: it picked the first of
the 4 most-recent posts that wasn't the current one, so nearly every
post converged on the same one or two most-recent articles. Now picks
the chronologically next-older post (wrapping to the newest from the
oldest), so every post points somewhere different.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Eg6h91yngXmSnM51wxXM8
object-cover on the square about-author.jpg inside this wide/short
banner was scaling the image up to fill the width and cropping most
of the height away, blowing the face up to fill the entire strip.
Verified the exact crop with a sharp simulation before switching.
object-contain shows the whole photo at its real proportions instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Eg6h91yngXmSnM51wxXM8
about-author.jpg became a square close-up portrait; centered object-cover
in this wide, short banner was cropping straight into an extreme
eyes-to-chin close-up. Biasing the crop toward the top keeps more
forehead/hair in frame.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Eg6h91yngXmSnM51wxXM8
Replaces the fixed line-clamp-2 subline cap with real subgrid alignment:
each grid (ProductGrid/RelatedProducts/MerklisteGrid) declares 6 explicit
row-tracks (image/header/price/belowPrice/lowstock/button, last one
minmax(0,1fr)), and every card spans those same 6 tracks via
grid-template-rows: subgrid — so row heights are genuinely shared across
a row of cards. A subline can now wrap to any number of lines (3, 4, ...)
without being truncated, and the price row still starts at the same Y on
every card in that row.
ProductCard.tsx is flattened from nested flex divs into 6 direct grid-row
children so each one can be its own subgrid track; the old flex-1 spacer
is replaced by self-end on the button's row.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J1Hu5bZ1kZUgKhab6yNwCt
The previous equal-height fix (items-stretch) made cards match overall
height, but a subline wrapping to 2 lines still pushed that card's own
price row down relative to its row siblings — the flex-1 spacer only
re-aligns the button at the bottom, not the price above it. Name now
clamps to 1 line, subline always reserves a clamped 2-line slot
(rendered even when empty) — every card's price row starts at the same Y
regardless of title length or whether a subline is set at all.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J1Hu5bZ1kZUgKhab6yNwCt
Reported: einfach-anfangen's block-built PDP looked visually far off from
todo-cards. Root cause: ProductBlocks.tsx wrapped every block (including
the hero) in one generic padded div, so the hero image lost its
edge-to-edge desktop bleed/hover-scale and neither section had its own
Reveal scroll-in animation, unlike every hand-coded PDP. ProductHero and
ProductPricingPanel now render their own full <section> matching
todo-cards' Hero.tsx/Pricing.tsx exactly (own Reveal, own grid, own
padding) instead of relying on the shared per-block wrapper, which now
only applies to genuinely generic content blocks.
Also wires up the testimonialsRef block for Products.layout (added to the
backend in commit 82583a1) — same async-fetch handling as PageBlocks.tsx,
plus the 'einfach-anfangen' TestimonialsPage value.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J1Hu5bZ1kZUgKhab6yNwCt
New ProductBlocks.tsx renders a product's layout — delegates to
PageBlocks.tsx's renderPageBlockSync for the 9 block types shared with
Pages.layout, handles productHero/productPricingPanel itself (need live
product/shipping/tax context a generic content page doesn't have).
Converts /einfach-anfangen to render through this instead of its own
Hero/HowItWorks/Focus/Pricing components (now deleted) — the first PDP
built as CMS blocks end to end. der-eine/todo-cards/tasse-die-pause are
untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J1Hu5bZ1kZUgKhab6yNwCt
app/components/Quote.tsx (the shared component PageBlocks.tsx/Pages.layout
uses) never had a `label` prop at all — dropped when it was extracted from
RichText.tsx's own local Quote. QuoteBlock.label was set correctly in the
admin and mapped through payload.ts, but PageBlocks.tsx's "quote" case
never passed it through, and even if it had, this component had nowhere
to put it. Both fixed; matches RichText.tsx's existing label treatment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J1Hu5bZ1kZUgKhab6yNwCt
Follows Posts.content's BlocksFeature update (docker/payload commit
5426a02) — mirrors PageBlocks.tsx's own JSX for each block field-for-field
since it's the same Block config reused a second time. Works in both the
server-rendered page and the client-side Live Preview renderer (shared
converters), unlike testimonialsRef which needs an async fetch and stays
page-builder only.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J1Hu5bZ1kZUgKhab6yNwCt
Follows Products.subline (docker/payload commit ad7156e). Cards and cart
line items showed nothing but the bare name — added the subline under it.
der-eine and todo-cards' hero taglines were hardcoded copy nearly
identical to this new field — switched both to read subline instead, so
future edits go through the CMS.
Also adds a shared ProductName component: every hand-written PDP headline
already styled a trailing "." in brand color, but the few places that
render product.name as plain text (cards, cart, the Payload-driven
tasse-die-pause hero) had silently lost that styling. Centralizes it so
it can't drift per call site again.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J1Hu5bZ1kZUgKhab6yNwCt
Backend dropped the plain-text description field in favor of a single
richText one (see docker/payload commit 1ba8d07). Renders formatted
(bold/italic/multiple paragraphs) via the shared RichText component on
the PDP (der-eine, tasse-die-pause); everywhere else (Passend-dazu cards,
Product JSON-LD, homepage spotlight fallback) derives plain text at read
time via a new extractPlainText() helper instead of a second field.
Removes the description line from cart line items entirely — it only
bloated the cart with no real benefit there.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J1Hu5bZ1kZUgKhab6yNwCt
The cards already had a hover-lift effect implying the whole thing
was clickable, but only the small arrow line was an actual link —
image and title had pointer-events-none/no href at all. The full
/blog listing page already wraps its own cards in one Link; this
brings the homepage teaser (Blog.tsx) in line with it. The former
inner CTA Link is now a plain span using group-hover for its color
change, since one <a> can't nest another.
SpotlightText's minimal Lexical converter only checked format bit 1
(bold) — bit 2 (italic) was silently ignored, so Tasse "Die Pause"'s
italicized quote ("Nur noch schnell … Nein.") rendered as plain text
on the homepage despite showing correctly in the admin's richText
editor.
app/[slug]/page.tsx and LivePageContent.tsx no longer render a
hardcoded H1 from page.title — that's now the Hero block's job (see
HeroBlock.ts's own comment on why). Breadcrumb stays, still built
from page.title.
Also backfilled /lebensuhr's already-migrated Pages document with a
Hero block (order 0) carrying its real headline — without it the
page would have lost its H1 entirely once the hardcoded wrapper was
removed.
Mirrors the backend schema additions (StepRowBlock.items.subtitle,
new IconBlock) needed for the /lebensuhr Pages migration: PageBlock
type + mapPayloadPageBlock in payload.ts, a "icon" case in
PageBlocks.tsx, and SYMBOLIC_ICONS (the clock face) added alongside
STEP_ICONS in StepIcons.tsx — a distinct registry since these icons
are standalone, not part of an icon-above-text row.
Pages/getPageBySlug/mapPayloadPage in payload.ts follow the exact
getPostBySlug/mapPayloadPost pattern. PageBlocks.tsx renders a Pages
doc's `layout` field, reusing existing components (RichText,
StepArrow, STEP_ICONS, TestimonialsGrid) rather than reinventing per-
block styling — matches what /lebensuhr, /3x3-system, and
/7-tage-klarheits-check already hand-built. LivePageContent.tsx
mirrors LivePostContent.tsx's live-preview pattern, using a synchronous
subset of the block renderer (testimonialsRef needs an async fetch a
client component can't perform inline, so it's skipped in preview
only — same "editable subset" scope-cut LivePostContent.tsx already
makes). buildWebPageSchema in structuredData.ts is the generic
JSON-LD fallback for content types with no bespoke schema (Article/
Product don't fit a page-builder page).
Verified end-to-end: inserted a real Pages test document (SQL, since
no admin auth available here) covering richTextSection/quote/ctaCard,
confirmed /[slug] renders it correctly, confirmed notFound() after
deleting it, then cleaned up the test row.
/lebensuhr, /3x3-system, and /7-tage-klarheits-check each defined
their own local copies of the same hand-drawn brand-line icons.
Moved them into app/components/icons/StepIcons.tsx as a keyed
registry (STEP_ICONS) — also what the new Payload Pages collection's
StepRowBlock select field maps onto by key, so a page-builder step
row can reference these same icons without duplicating SVG paths a
third time.
The period is part of the brand name (see Footer.tsx's own comment on
the two-tone logo treatment) — fixed every place it was cleanly
appendable (string/heading endings, or before a dash, matching the
existing precedent in VertragspartnerBlock.tsx). Left the handful of
genuinely awkward mid-sentence spots alone (separable-verb endings,
"einfach produktiv-Konto" hyphenated compounds) rather than force a
period that breaks the sentence — flagging those separately.
Also: Über-mich's checklist now uses the same brand checkmark as
/lebensuhr and /7-tage-klarheits-check instead of plain bullets.
Nav's "Über Björn" entry pointed to the homepage's #ueber-bjoern
teaser section, not the new bio page — updated the label to "Über
mich" and the href to the real /ueber-mich page. The homepage's own
#ueber-bjoern anchor id (About.tsx) is untouched, it's a separate
section.
Renames the route, its canonical/OG metadata, Navbar's active-route
list, and the testimonials page filter to match. Also updates CTA
copy ("Klarheits-Check starten"), the "So funktioniert" heading, the
"3. Umsetzen" step text, and the breadcrumb label.
Side-by-side kicked in at sm (640px), squeezing the text column hard on
tablet widths. Moved the breakpoint to lg (1024px), matching the
secondary posts' grid below, which already used lg for the same reason.
The card's height was purely driven by the (short, fixed) text column's
content regardless of viewport width. lg:min-h-[26rem] gives it real
height on large screens; the image's sm:h-full and the text column's
justify-between both stretch to match.
Was full-bleed w-full with no cap, so on very wide screens the featured
card (and its now-h-full image column) kept stretching wider while the
image's height stayed pinned to the text column's content height —
increasingly squashed the wider the viewport got.
The 58%-wide image column at a hardcoded 220px tall produced a badly
elongated crop on wide viewports. sm:h-full lets the grid's natural
row-stretch (driven by the text column) size it instead, matching how
/blog's own featured card already does it.
Products.spotlightText switches from a plain textarea to a richText field
(Lexical, same default editor as every other richText field here) so
admins get a formatting toolbar. Rendered via a small dedicated inline
converter (bold + paragraphs only) rather than the full block-oriented
RichText.tsx, which is styled for article-length content (blog posts,
legal pages) and would add heading/paragraph spacing this teaser doesn't
want.
New copy throughout (hero, "Was kann er?", the "Warum Der Eine.?" brand-story
section replacing the old phone-vs-pen scenario grid, final CTA) plus the
Payload product's name/detailHref updated to match (done directly via psql,
no Payload admin API key available — see reference_payload_direct_db_access).
Image sits as a sibling to the badge/WishlistButton overlays, not
wrapping them — nested interactive elements would be invalid HTML and
would fire navigation on a wishlist click.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
py-3 alone didn't match AddToCartInlineButton's real height — that
button's tallest child is its 1.875rem cart-icon image, not its text,
so an input with the same padding but only text content still rendered
shorter. Explicit h-14 matches the button's actual 56px.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
NotifyMeForm's single-row redesign matched AddToCartInlineButton's
button height exactly, which made the sold-out card's earlier
low-stock-line omission overcorrect — it ended up shorter than its
siblings instead of taller. Keeping that line unconditional (like every
other card) is what actually lines them up. Also swapped the generic
"E-Mail-Adresse" placeholder for one that states the purpose.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
NotifyMeForm was two stacked rows (email input + full-width button),
making an out-of-stock ProductCard taller than its in-stock siblings.
Redesigned as a single input with the submit control embedded inside it
(same py-3 as AddToCartInlineButton's own button, so the row height
matches exactly), and ProductCard now skips its reserved-height
low-stock line entirely for a fully-out-of-stock product, since that
line can never apply there.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>