cta-Block liest die gepflegte CTA-Entitaet (Marker-Weg + Interessens-Automatik)
Der cta-Block war mit 252 Vorkommen der viert-haeufigste Block im Bestand und zugleich der einzige Conversion-Block ohne Entitaets-Bezug: die Plattform pflegt CTAs seit Jahren als eigene Entitaet mit eigenem Ausliefer-Dienst (ctas.ayoune.app) und eigenem Klick-Zaehler (clt.ayoune.app), der Website-Bauer kannte sie nicht und der Redakteur tippte denselben Text ein zweites Mal.
- mapCtasToItems: ICTA -> Block-Inhalt. Strippt HTML (21 von 55 Datensaetzen tragen Markup in h1/h2), behandelt den Platzhalter '-' als leer, leitet den Sprach-Code aus 'locale' ab (NICHT 'lang' wie bei References/Faqs/Quotes - ein lang-Filter saehe undefined und liesse jede Fassung durch) und setzt die Knopf-Adresse auf den Klick-Zaehler. - resolveEntityBlocks: cta-Zweig. Ausdrueckliche Wahl (_cta) schlaegt den Auto-Zug (_ctaAuto + _ctaInterest); der Auto-Zug ist fail-closed und fragt ohne Kriterium die Datenbank gar nicht. WEDER _website- NOCH published-Filter - beide Felder fehlen bei 55/55 Datensaetzen, ein vom popup-Nachbarn abgeschriebener Filter haette jede CTA weggeschnitten. - renderBlock case 'cta': Entitaet schlaegt Inline, Sprachwahl liegt hier (der Resolver kennt die ausgelieferte Sprache nicht). Ohne passende Fassung bleibt der Block still statt auf die alten Inline-Texte zurueckzufallen. - app.ts: aMN.CTAs geladen (marketing-Modul, kein cms-Auto-Load) - ohne den Eintrag waere der ganze Zweig still wirkungslos. - app.ts: aMN.Quotes nachgetragen. NEBENBEFUND derselben Klasse: der quotes-Zweig stand seit der Zitate-Welle im Zeichner, sein Modell wurde nie geladen. Latent - quotes kommt im Seiten-Bestand 0-mal vor (Positiv-Kontrolle: 23 andere Typen).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neupages
cta-Block liest die gepflegte CTA-Entitaet (Marker-Weg + Interessens-Automatik)
Der cta-Block war mit 252 Vorkommen der viert-haeufigste Block im Bestand und zugleich der einzige Conversion-Block ohne Entitaets-Bezug: die Plattform pflegt CTAs seit Jahren als eigene Entitaet mit eigenem Ausliefer-Dienst und Klick-Zaehler, der Website-Bauer kannte sie nicht.
- mapCtasToItems: ICTA -> Block-Inhalt. Strippt HTML (21 von 55 Datensaetzen tragen Markup in h1/h2), behandelt den Platzhalter '-' als leer, leitet den Sprach-Code aus 'locale' ab (NICHT 'lang' wie bei References/Faqs/Quotes) und setzt die Knopf-Adresse auf den Klick-Zaehler clt.ayoune.app/cta/:id. - resolveEntityBlocks: cta-Zweig. Ausdrueckliche Wahl (_cta) schlaegt den Auto-Zug (_ctaAuto + _ctaInterest); der Auto-Zug ist fail-closed und fragt ohne Kriterium die Datenbank gar nicht. WEDER _website- NOCH published-Filter - beide Felder fehlen bei 55/55 Datensaetzen, ein vom popup-Nachbarn abgeschriebener Filter haette jede CTA weggeschnitten. - renderBlock case 'cta': Entitaet schlaegt Inline, Sprachwahl liegt hier (der Resolver kennt die ausgelieferte Sprache nicht). Ohne passende Fassung bleibt der Block still statt auf die alten Inline-Texte zurueckzufallen. - app.ts: aMN.CTAs geladen (marketing-Modul, kein cms-Auto-Load) - ohne den Eintrag waere der ganze Zweig still wirkungslos. - app.ts: aMN.Quotes nachgetragen. NEBENBEFUND derselben Klasse: der quotes-Zweig stand seit der Zitate-Welle im Zeichner, sein Modell wurde nie geladen. Latent - quotes kommt im Seiten-Bestand 0-mal vor (Positiv-Kontrolle: 23 andere Typen).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neupages
Adress-Historie leitet mit 301 statt 404 (formerSlugs, Kaskade 56)
Ein Adress-Wechsel einer CMS-Seite hinterliess bis hierher eine tote Adresse: `/de/alte-adresse` antwortete 404 ohne `location`-Kopf, jeder externe Verweis und jeder Suchmaschinen-Eintrag lief ins Leere, und die aufgebaute Bewertung wanderte nicht mit. Der Fundament-Anteil (`IWebPage.formerSlugs` + Schema- Zwilling + sparse-Index `{_customerID,_website,formerSlugs}`) liegt seit Kaskade 56 vor und hatte bis jetzt keinen Leser.
- `getPageByFormerSlug` — gezielter Nachschlag, mandanten- UND website-gebunden, nur veroeffentlichte Seiten, beidseitig zwischengespeichert (auch das negative Ergebnis: eine frei waehlbare 404-Adresse darf keinen un-gecachten Datenbank-Zugriff ausloesen). Fail-soft. - `matchesFormerPageSlug` / `formerSlugRedirectTarget` — rein + exportiert, damit die Proben die ECHTE Aufloesung pruefen statt einer Kopie. - `resolveAndRender`: der Nachschlag steht VOR dem 404 und **301**, nicht 302 — nur der dauerhafte Code laesst Suchmaschinen die Bewertung uebertragen. Der bestehende Sprach-Rueckfall bleibt bewusst 302.
RANGFOLGE strukturell statt per Vergleich: der Nachschlag laeuft ausschliesslich in einem `if (!page)`-Zweig, eine Historie kann eine lebende Adresse also nicht ueberstimmen. Das ist der Unterschied zum Blog-Vorbild (`resolveBlogDocBySlug`), das alle Beitraege als Liste haelt und deshalb zwei Durchgaenge braucht.
13 Proben, alle fuenf Flip-Proben beissen je genau eine Zusage: 301->302, `if(!page)`-Klammer weg, `slug` aus der Projektion, Selbst-Schleifen-Riegel weg, Trim weg. Volle Reihe 1698/1698. Negativ-Kontrollen belegen, dass der heutige Bestand (0 von 372 Seiten mit Historie) unveraendert ausgeliefert wird.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neupages
UCD-Schlitze landen am Inhalts-Element statt am Abschnitt
Der Klient tauscht dynamische Inhalte per getElementById. Eine gesetzte Block-Kennung ging bisher immer ans <section> — fuer einen UCD-Schlitz die falsche Ebene: der Tausch ersetzt dann das innerHTML des GANZEN Abschnitts samt Innen-Huelle und Knopf-Klassen.
blockIdPlacement() trennt beide Bedeutungen: ein normaler Sprungziel-Anker (#kontakt) bleibt unveraendert am Abschnitt, ein UCD-Schlitz wandert an das inhaltstragende Element — beim Knopf-Schlitz an den Knopf, sonst an die Inhalts-Huelle. h1-h6 stehen bewusst NICHT in der Liste: der Klient findet sie ueber den Element-Namen.
Bewusst KEIN neues settings-Feld: PageBlockSettingsSchema ist strict, ein Feld waere Fundament-Anteil. Getragen wird die Markierung von settings.id (Vertrag + Schema vorhanden). Grenze daraus: ein Block = hoechstens EIN dynamischer Schlitz.
Proben: 886 gruen in der pages-Reihe (11 neu). Flip gefahren — Kante entfernt: 5 rot, die beiden Rueckwaerts-Proben blieben gruen.
Neupages
Galerie-Block speist aus der Mediathek Images (W9 Glied 2)
CEO-Entscheid 2026-08-22 Weg (a): der `gallery`-Block bekommt eine Entitaets-Quelle aus dem LEBENDEN Bild-Bestand statt aus der Leer-Entitaet `Collections` (0 Dokumente ueber alle 90 Mandanten, 0 Verbraucher, kein W0-Kanon). Kein Fundament-Anteil, kein neuer Blocktyp, die Blocktyp-Zahl bleibt unveraendert.
Zwei Wege wie bei references/portfolio/quotes: - ausdrueckliche Auswahl `content._images[]`, Reihenfolge der Redaktion - Auto-Zug `content._imagesAuto` mit Kriterium `_imageTags` / `_imageCategory`
IImage traegt KEINEN W0-Kanon (kein published/_websites/sortOrder, am Vertrag UND am Schema gemessen, Positiv-Kontrolle IReference.published). Daraus folgen die zwei quotes-Vertraege: die ausdrueckliche Auswahl filtert nicht, der Auto-Zug verlangt ein Kriterium und fragt ohne eines die Datenbank gar nicht erst. Der Grund ist hier schaerfer als bei den Zitaten: images ist die MEDIATHEK - bei EINEM Mandanten 1.303 Bilder; ein kriterienloser Zug haette die halbe Mediathek auf eine oeffentliche Website gestellt, ohne dass etwas fehlschlaegt.
Gemessene Achsen (Prod, 3.384 Bilder): tags 459 · category 0 · _contentCategoryID 1. tags ist die einzige lebende Gruppierungs-Achse; category bleibt als Filter angeboten (das Feld existiert), _contentCategoryID bewusst nicht.
Alternativtexte nach der bindenden S8-Semantik (altTranslations[locale] vor alt), inkl. der Mongoose-MAP-Form: auf einem hydrierten Dokument liefert doc.altTranslations['en'] sonst still undefined und der Besucher laese die Vorgabe-Sprache, obwohl die Uebersetzung vorliegt (357 Bilder tragen sie).
Nicht-Brechen: ein Block ohne Entitaets-Bezug laeuft durch den unveraenderten Inline-Zweig - byte-identisch, als Literal festgenagelt, mit Umkehrprobe.
28 neue Proben, alle vier Flip-Proben beissen (Kriterium-Riegel, Map-Form, Inline-Ausgabe, Reihenfolge). Voller Lauf des Repositoriums: 101 Suiten / 2391 Proben.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XVq1MjzHpUt7bUtRzJTny
Fixpages
testimonial-Auto-Zug sortiert `createdAt` statt des schema-losen `created`
`IRating` fuehrt `created`, das `RatingSchema` deklariert es NICHT — `strict` verwirft es beim Schreiben still, es lebt nur in Alt-Dokumenten. Am Bestand gemessen 2026-08-22: `created` in 6 von 46 Dokumenten, `createdAt` in 46/46 (Positiv-Kontrolle im selben Lauf). Fuer 40 Dokumente sortierte MongoDB gegen `null` — der Zug lieferte also NICHT die neuesten `_ratingsLimit`, sondern eine beliebige Auswahl, ohne je fehlzuschlagen. Die vier Alt-Anzeigewege sortieren `createdAt` (`legacy-core-api/services/micro/content/dynamic.ts:315`).
Heute ohne sichtbare Wirkung (plattformweit passieren nur 4 Bewertungen den Filter, die Vorgabe-Grenze ist 12) — der Zweig wird erst mit wachsendem Bestand falsch, und dann lautlos. Genau deshalb war er nicht aufgefallen: der bestehende Fall (c) prueft Filter UND Grenze, die Sortierung nie. Die neue Probe schliesst die Luecke, mit Negativ-Kontrolle auf das Phantom-Feld (rot vor dem Fix belegt, gruen danach).
Dazu die Abgrenzung der ACHT bewertungs-artigen Achsen im Blocktyp-Kanon — welche Entitaet auf eine Website gehoert und welche nicht (`StoreViews` ist gar keine Bewertung; `Settings.rating` ist die achte Achse und steht per Bauart NICHT in `modelsAndRights`). Voller Beleg: `.claude/reports/bewertungs-achsen/abgrenzung-2026-08-22.md`.
`KNOWN_BLOCK_TYPES` unveraendert — kein neuer Blocktyp, die zwei Waechter bleiben gueltig.
Verify: `tsc --noEmit` Exit 0; volle Reihe 2318/2319 (der eine Fehlschlag `__tests__/contents.test.ts:282` ist VORBESTEHEND — mit gestashten Aenderungen identisch rot).
Neupages
Blocktyp `quotes` — zitierte Aussagen Dritter aus `Quotes` (hub), W9
Dritter zitat-naher Block neben `testimonial` (Ratings, ohne Sprache) und `references` (Kundenstimmen des Mandanten). Drei Traeger, drei Quellen — keiner ersetzt einen anderen; die Abgrenzung steht im Kanon-Kommentar.
Gemessen (nicht angenommen): `Quotes` traegt KEINEN W0-Kanon — kein `published`, kein `slug`, kein `_websites`, kein `sortOrder`, am publizierten Vertrag UND am Mongoose-Schema geprueft. Daraus die zwei Vertraege dieses Zweiges:
(1) Die ausdrueckliche Auswahl (`_quotes[]`) filtert nicht — zeichengleich zum `_ratings[]`/`_faqs[]`-Weg. Wer ein Zitat in den Block legt, gibt es frei. (2) Der Auto-Zug verlangt ein Kriterium (`_quoteCategory`/`_quoteLang`) und fragt OHNE Kriterium die Datenbank gar nicht erst. Grund ist gemessen: bei EINEM Mandanten liegen 48.428 importierte Fremdzitate — ein kriterienloser Zug haette daraus zwoelf willkuerliche auf die Website gestellt, ohne dass irgendetwas fehlschlaegt.
Publizier-Zeit statt Marker (EPIC-Regel): ein Zitat aendert sich nur, wenn ein Mensch es aendert — kein Zeitfenster, keine wandernde Adresse.
Alle drei Wachposten bedient: KNOWN_BLOCK_TYPES 39 -> 40, die harte Zahl in renderBlockListBlocks.test.ts, UND der Charakterisierungs-Fall in BLOCK_CASES (den der Auftrag nicht nannte — er verlangt Markup-Belege, keine Zaehlung).
Proben: 13 neu (quotesBlock.test.ts), volle Einheiten-Menge 2312/2313. Die eine rote ist ein Bestandsbefund (contents.test.ts, searchableFields-Vorlage) und von dieser Arbeit unberuehrt — belegt und im bestehenden Brief nachgetragen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PmMj9oQCU9nxusjcbD8Dp8