Skip to content

✨ feat: editor-defined OG image and description#650

Merged
jbourdin merged 1 commit into
developfrom
feature/og-image-description
May 29, 2026
Merged

✨ feat: editor-defined OG image and description#650
jbourdin merged 1 commit into
developfrom
feature/og-image-description

Conversation

@jbourdin
Copy link
Copy Markdown
Owner

Summary

Editor-defined Open Graph image and description controls across decks, archetypes, archetype variants, and the Banned/Staple Cards listing pages — bundling F18.30 and F18.31 because they share an OgMetaResolver service and a single PR is browser-validatable end-to-end.

  • Data model. Three new column pairs (all nullable): deck.og_image/og_description (non-translatable); archetype_translation.og_image/og_description (per-locale, kept independent from meta_description so length is tunable for social cards); page_translation.og_image/og_description (per-locale override on top of the existing parent-level Page.ogImage). Migration is hand-pruned to only these six ADD COLUMNs — the auto-generated diff carried 30+ unrelated drift lines that have been stripped.
  • Resolver. New App\Service\Seo\OgMetaResolver (stateless, no injected deps) centralises the fallback chain. For decks: own values → (if isArchetypeVariant()) archetype translation → currentVersion.mosaicImageUrl. For archetypes: translation OG → graceful fallback to localizedMetaDescription. For pages: translation override → parent Page.ogImage → none.
  • Controllers. DeckShowController, ArchetypeDetailController, BannedCardController, StapleCardController inject the resolver and pass the resolved ogImage/ogDescription into their templates. AdminPageController::duplicate() also copies the new per-locale fields when cloning a page.
  • Templates. Existing _partials/opengraph.html.twig already null-guards both fields, so no partial change. staple_card/list.html.twig gains a full {% block opengraph %} block (it had none). The other three templates feed the new vars through their existing block.
  • Admin UX. New ogImage inputs render with the existing ImageUrlField Mantine drag-and-drop component via the admin/_image_url_field.html.twig macro — the same UX as the homepage editor and the parent-level Page.ogImage. The React mount loop is extracted to assets/shared/mount-image-url-field.tsx and reused from page-form, deck-form, and archetype-form Webpack entries (page-form's previously-inline copy was replaced by the same call).
  • Translations. 14 new keys across messages.{en,fr}.xlf (app.form.label.og_*, app.archetype.og_*_label/help, app.cms.form.og_*_localized*). Per-locale og_image_localized is kept distinct from the parent-level app.cms.form.og_image so editors don't see the same label twice on the same admin page.
  • Tests. 13 new unit tests in OgMetaResolverTest cover variant fallback, meta-description graceful fallback, page translation/parent precedence, and missing-translation paths. 2 new functional tests on BannedCardController assert that og:image/og:description render from editor input and degrade gracefully when blank.
  • Docs. docs/features.md (new F18.30 and F18.31 rows), docs/models/deck.md, docs/models/cms.md.

One design call worth flagging

og_title for Banned/Staple keeps the existing 'app.banned_card.public.title'|trans style rather than reusing PageTranslation.title, because PageTranslationFormType hides the title field when is_listing_intro is true (lines 35-39) — so title for listing intros isn't editor-controlled. If editors want a separate social-share title later, the upgrade is PageTranslation.ogTitle.

Test plan

  • Admin: edit a deck → set OG image (drag-and-drop) and OG description → save → view the public deck page source and confirm og:image + og:description reflect the editor values.
  • Admin: edit an archetype's EN translation, set OG fields, save; do the same for FR. Confirm /en/archetypes/<slug> and /fr/archetypes/<slug> each render their locale-specific OG values.
  • Variant fallback: clear a variant deck's own OG fields and confirm the page falls back to the parent archetype's locale-scoped values; set the variant's own values and confirm they win.
  • Listing pages: edit the banned-cards intro Page → set per-locale OG values → confirm /en/banned-cards and /fr/banned-cards emit correct meta. Repeat for staple cards (/en/staple-cards, /fr/staple-cards).
  • Empty-state safety: with all new fields blank, none of the five pages regresses — the deck page still emits the mosaic as og:image, the archetype page still emits localizedMetaDescription as og:description, and the banned/staple pages continue to emit og:title + og:url as before.
  • Drop a JPG, PNG, GIF, and WebP into the drag-and-drop zone on each form and confirm upload → preview → URL persisted.
  • Paste a malformed URL (e.g. not-a-url) into the OG image field and confirm the regex validation (#^(/|https?://)#) rejects on submit.
  • Social validators: paste one URL from each surface into the Facebook Sharing Debugger and the Twitter Card Validator; confirm both EN and FR previews render the expected image + description.

Closes #580, closes #581.

Adds per-entity Open Graph image and description controls so editors can
override how decks, archetypes, archetype variants, and the Banned/Staple
Cards listing pages appear when shared on social platforms.

- New nullable columns: deck.og_image/og_description (non-translatable),
  archetype_translation.og_image/og_description (per-locale, independent
  from meta_description so length can be tuned for social cards),
  page_translation.og_image/og_description (per-locale override on top of
  the existing parent-level Page.ogImage).
- New App\Service\Seo\OgMetaResolver centralises the fallback chain:
  deck own values -> (variant?) archetype translation -> mosaic image;
  archetype translation -> localizedMetaDescription fallback for desc;
  page translation -> parent page -> none.
- DeckShowController, ArchetypeDetailController, BannedCardController,
  StapleCardController inject the resolver and pass the resolved values
  into their templates. AdminPageController.duplicate() now copies the
  per-locale OG fields when cloning a page.
- Templates emit the new vars through the existing
  _partials/opengraph.html.twig (already null-guarded); staple_card/list
  gains a full opengraph block (it had none).
- Admin forms render the new ogImage inputs with the existing
  ImageUrlField drag-and-drop React component via the
  admin/_image_url_field.html.twig macro. The React mount loop is
  extracted to assets/shared/mount-image-url-field.tsx and reused from
  page-form, deck-form, and archetype-form entries.
- 13 new unit tests in OgMetaResolverTest cover variant fallback,
  archetype meta-description fallback, page translation/parent
  precedence, and missing-translation paths. 2 new functional tests on
  BannedCardController assert the og:image/og:description meta tags
  render from editor input and degrade gracefully when blank.
- docs/features.md, docs/models/deck.md, docs/models/cms.md updated.

Closes #580, closes #581.
@sentry
Copy link
Copy Markdown

sentry Bot commented May 29, 2026

Codecov Report

❌ Patch coverage is 93.85965% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/Controller/StapleCardController.php 0.00% 5 Missing ⚠️
src/Controller/AdminPageController.php 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@jbourdin jbourdin merged commit c6091f6 into develop May 29, 2026
6 checks passed
@jbourdin jbourdin deleted the feature/og-image-description branch May 29, 2026 09:17
@jbourdin jbourdin mentioned this pull request May 29, 2026
3 tasks
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