aYOUne

Changelog

Was sich verändert hat — gruppiert nach Release, filterbar nach Komponente und Repository.

Ohne Meilenstein 7
Neu pages

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).

24 Proben mit Negativ-Kontrollen; drei Flip-Proben belegt (Sprachwahl entfernt -> 2 rot,
fail-closed entfernt -> 2 rot, HTML-Strip entfernt -> 2 rot). Regression cta-nah 312/312.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neu pages

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).

24 Proben mit Negativ-Kontrollen; drei Flip-Proben belegt (Sprachwahl entfernt -> 2 rot,
fail-closed entfernt -> 2 rot, HTML-Strip entfernt -> 2 rot). Regression cta-nah
312/312 gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neu pages

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>
Neu pages

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.
Neu pages

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
Fix pages

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).
Neu pages

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