c975l / social-bundle
Symfony bundle for the social side of a c975L site — social links managed in one single place and share buttons for 15 networks, placed anywhere as blocks. Replaces the former ShareButtonsBundle.
Package info
Type:symfony-bundle
pkg:composer/c975l/social-bundle
Requires
- php: >=8.4
- c975l/core-bundle: ^1.12.2
- doctrine/doctrine-bundle: ^3.3
- doctrine/orm: ^3.6
- easycorp/easyadmin-bundle: ^5.1
- symfony/form: ^8.0
- symfony/framework-bundle: ^8.0
Requires (Dev)
- phpunit/phpunit: ^13.2
This package is auto-updated.
Last update: 2026-08-21 12:37:00 UTC
README
Symfony bundle for the social side of a c975L site — social links managed in one single place and share buttons for 15 networks, placed anywhere as blocks. Replaces the former ShareButtonsBundle.
Why SocialBundle
Add SocialBundle on top of the shared UiBundle + ConfigBundle foundation to get social links and sharing — no dependency on SiteBundle, ShopBundle or any other satellite bundle, so it drops into any c975L site that needs one. Its social_links block reuses UiBundle's generic Block entity rather than a dedicated table, following the "singleton CRUD" pattern shared across the ecosystem.
See it in action at bundles.975l.com/pages/social-bundle, and browse every block kind live in the block gallery.
TL;DR — Social links and share buttons for a c975L site. The links are stored as a
social_linksblock reusing UiBundle's genericBlockentity rather than a dedicated table (the "singleton CRUD" pattern), displayed anywhere through asocial_links_displayblock or site-wide. Replaces the former ShareButtonsBundle.
Contents
- Setup — requirements · installation · assets
- Using it — social links block · admin management · rendering · styling · share buttons · site-wide auto-display · customer reviews · admin help procedures · guided projects · AI agent skills
Features
- Social links block: a
ui.blockkind (social_links) storing an ordered list of links (network + url), plus a site-wide icon style (flat/monochrome, colored badge, ring, or text only) and label visibility - no dedicated entity/table - Admin CRUD for the social links block via EasyAdmin, outside of any page's block collection
- Rendering component to display the block wherever it lives, page-attached or not
- Pickable pointer block (
social_links_display) to drop the same site-wide links into any page's block flow, with no data re-entry - Share buttons: a
share_buttons()Twig function to let visitors share the current (or a given) page on 20 social networks, with an independently picked button shape and fill - Share buttons dashboard settings: pick which networks, and which button shape and fill, are used site-wide, plus an
enable-share-buttonsconfig key to auto-display them on every page with no template change - Pickable pointer block (
share_buttons_display) to drop those same site-wide share buttons into any page's block flow, with no data re-entry - Icon picker reusing c975L/UiBundle's searchable
IconPickerType - Stylesheet auto-registration via UiBundle's
BundleStylesheetProviderInterface— no manual<link>needed - Script auto-registration via UiBundle's
BundleScriptProviderInterface— no manual<script>needed - Admin menu entry registered automatically via
MenuProviderInterface - Admin help procedures contributed automatically via
ProcedureProviderInterface - Guided projects contributed automatically via
GuidedProjectProviderInterface— see Guided projects - Customer reviews: imported from the site's own Google Business Profile listing into a
Reviewentity by a cron command, displayed through UiBundle's genericcollectionblock — read-only, the public reply being the only thing the back office writes; see Customer reviews - Pluggable review sources via
ReviewsSourceInterface— auto-discovered by interface, so a site adds its own platform without touching this bundle - A skill for coding agents, shipped in the package and read straight from
vendor/— see AI agent skills
Requirements
- PHP >= 8.4
- Symfony 8
- c975L/CoreBundle, the single package shipping ConfigBundle and UiBundle
- EasyAdmin
Installation
Download
composer require c975l/social-bundle
Install assets
php bin/console assets:install --symlink
This exposes the bundle's compiled stylesheet at public/bundles/c975lsocial/css/styles.min.css.
Two routes to enable, both serving the Google connection (see Routes): the consuming app has to import the bundle's controllers, or the "Connecter Google" dashboard entry breaks every management screen. Everything else the bundle contributes needs no route — EasyAdmin dashboard entries (auto-registered, see Admin management), a Twig component and Twig functions. Its configuration keys (social-enable-share-buttons, see Site-wide auto-display, and the Google ones listed under Connecting the site to Google) are auto-loaded like any other c975L bundle's, via php bin/console c975l:config:load-all.
Share buttons' popup behavior needs its Stimulus controller loaded: as long as your layout renders {{ importmap(['app']|merge(bundle_scripts())) }} (see c975L/UiBundle's bundle_scripts()), it gets auto-registered — no assets/bootstrap.js edit needed.
Symfony's AssetMapper still requires the entrypoint to be declared in your app's importmap.php though, since bundle_scripts() only feeds names to the importmap() Twig function, it doesn't create importmap entries itself:
Add one entry to importmap.php (one-time, at installation):
'@c975l/social-bundle/controllers.js' => [ 'path' => './vendor/c975l/social-bundle/assets/controllers.js', 'entrypoint' => true, ],
Usage
Social links block
Registers a social_links ui.block kind (see c975L/UiBundle's Block system) with a dedicated form (c975L\SocialBundle\Form\Block\SocialLinksType) and template (templates/blocks/SocialLinks.html.twig). Each link is a network (picked from every icon found under public/icons/ and public/bundles/*/icons/) and a url; label and icon are derived from the network at render time, not stored. Pick "Autre" to fall back to a free-text label and UiBundle's IconPickerType for a network with no icon of its own.
Three settings apply to the whole block:
- Introduction text (
intro) - an optional rich-text lead-in (UiBundle'sTrixEditorType, the ecosystem's editor) rendered centered above the icon row (.social-links-intro). Left empty, nothing at all is rendered — no wrapper, no blank space. - Icon style (
iconStyle) -minimal(the flat, monochrome glyph, inheriting the surrounding text color),colored("Version colorée": the same glyph turned white on a solid, brand-colored pill background),outline(a lighter brand-colored ring on a transparent background, filling in on hover) ortext("Texte seul": no glyph at all, the network's name standing as the link - for a footer row set as words, where a row of marks would compete with the site's own). The first three are CSS only (see Styling below), no separate icon asset - same glyph in every case;textprints no icon in the markup at all, and prints the label whatever Display label below says, an entry showing neither having nothing left to click. - Display label (
displayLabel) - whether the network name is shown as text next to the icon (still used asaria-labelregardless).
Unlike most block kinds, social_links is tagged pickable: false and therefore absent from a page's own block picker: it's a singleton, meant to be edited once and rendered wherever needed (see Rendering the block) rather than re-created with duplicate data on every page that wants it.
To insert those same links at a specific spot in a page's block flow (not just the fixed <twig:c975LSocial:SocialLinks/> component placement), pick the social_links_display kind from the page's block picker instead. It's a thin pointer: its own form has no fields and its template just renders <twig:c975LSocial:SocialLinks/> internally, so it always reflects the current site-wide links, edited only from Admin management — no separate data, no duplication, no extra table.
Icons
Ships public/icons/ with flat, single-color 64×64 SVG glyphs (Font Awesome Free 6.5.1 brand icons, default black fill, no explicit fill set) for the 37 social/media networks the picker offers (Facebook, Instagram, Bluesky, LinkedIn, YouTube, TikTok, Pinterest, WhatsApp, Reddit, Discord, Threads, Mastodon, GitHub, Twitch, Spotify, SoundCloud, Flickr, Medium, WeChat, Line, Behance, Dribbble, VK, Xing, Messenger, Snapchat, Telegram, Vimeo, Tumblr, Skype…). Only Font Awesome glyphs are kept here on purpose - no separate, pre-colored "official logo" badge asset: the colored icon style above is achieved entirely in CSS (inverting the glyph to white over a solid brand-colored background, see Styling), so every icon only needs to exist once.
IconServiceInterface::getIcons() merges every bundle's icons/ by filename, walking public/bundles/*/icons/ in alphabetical order before the app's own public/icons/: dropping a {network}.svg in the app overrides the one shipped here, while a same-named file in a package sorting after c975lsocial (c975lui) would silently shadow it.
Icon glyphs are derived from Font Awesome Free (CC BY 4.0) — keep attribution if you redistribute this bundle's icons on their own.
Admin management
Because a Block can normally only be created by attaching it to a Page (there's no page-independent block library in UiBundle), SocialLinksCrudController gives it its own small dashboard entry, scoped to kind = social_links — so it can be created/edited without needing a host page. The menu entry ("Réseaux sociaux") is registered automatically through MenuProvider, under the "Management" section. Access is controlled by the site-role-editor key in ConfigBundle. Each entry also carries a description — the very sentence its own screen shows, not a separate onboarding-only string — which the dashboard's onboarding tour picks up.
The edit form shows a preview of the rendered links below the list. The introduction text and the links themselves are static (reflects the last saved state, not unsaved edits to the form above), but "icon style" and "display label" update it live (see assets/js/social-links-preview.js) as you change them.
Rendering the block
<twig:c975LSocial:SocialLinks/>
Under the hood, this looks up the first social_links block via BlockRepository::findOneByKind() (also exposed as the social_link_block() Twig function) and reuses UiBundle's render_block(). Renders nothing if no social_links block exists yet. Drop it in your footer, navbar, or anywhere else in your layout — it's not tied to any specific location.
Styling
The whole block is wrapped in a .social-links-block (a <div>, not a <section>: it carries no heading of its own, and a headingless <section> is invalid HTML — the same fallback UiBundle's own block components use). That wrapper owns the vertical step above the block, --section-space-tight (UiBundle's page rhythm, so the links are parted from the block above them exactly like any two page sections are), on the top edge only — and none of it inside a footer, where the band already sets its own room.
Ships .social-links / .social-link styles (centered flex row of icon links, wrapping to a second line rather than being clipped on a narrow screen), a .social-links-intro one (the optional introduction text, centered above the row) plus a footer .social-links variant tightening the gap and setting the band's own vertical room when used in a page footer. Loaded automatically via the ui.stylesheet tag — override the classes in your own SCSS if you need a different look.
The list also carries a .social-links--minimal / .social-links--colored / .social-links--outline / .social-links--text modifier class (from the block's icon style setting, see Social links block) and each <li> a .social-link--{network} one — hooks to target from your own SCSS rather than opinions this bundle imposes, except for two, both driven by sass/_social-brand-colors.scss (shared with share_buttons()'s own per-network colors below): under .social-links--colored, each .social-link--{network} gets a solid, brand-colored badge - background + white icon (same $white-icon-filter trick as share_buttons()) + black-or-white text, whichever reads on that background; under .social-links--outline, a brand-colored ring on a transparent background instead, filling in (and turning the icon white) on hover. "Autre" entries keep the default, unstyled look in both cases (no brand color to badge them with). Under .social-links--text the pill goes with the glyph - no background, no padding, no radius, underlined on hover - so the row follows the color and font of whatever it is placed in, a footer among the site's other text links being what it is meant for. Kept deliberately smaller (32px) and visually distinct from share_buttons()'s own badges (50-65px, see below) so the two icon rows don't compete for attention on the same page.
Share buttons
Migrated from the now-abandoned c975L/ShareButtonsBundle. Renders one link per network, each pointing directly at that network's share URL (built server-side from the shared page's URL) — no internal redirect route involved.
{# Full signature #} {{ share_buttons(networks, shape, fill, alignment, displayIcon, displayText, url, id, displayIntro) }} {# Display the main networks at the default shape and fill #} {{ share_buttons() }} {# Custom selection, ellipse-shaped, centered, icon only #} {{ share_buttons(['facebook', 'linkedin', 'email'], 'ellipse') }} {# Round buttons, brand-colored ring instead of a solid fill #} {{ share_buttons('main', 'circle', 'outline') }} {# Override the shared URL (defaults to the current page) #} {{ share_buttons('main', 'wide', 'solid', 'center', true, false, 'https://example.com/my-page') }}
| Parameter | Type | Default | Description |
|---|---|---|---|
networks |
string[]|'main' |
'main' |
Network keys, or 'main' for the default set (facebook, bluesky, linkedin, pinterest, email) |
shape |
string |
'wide' |
wide, ellipse, square, rounded, or circle |
fill |
string |
'solid' |
solid, transparent, outline, or minimal |
alignment |
string |
'center' |
left, center, or right |
displayIcon |
bool |
true |
Show the network icon |
displayText |
bool |
false |
Show the network name |
url |
string|null |
null |
URL to share, defaults to the current page |
id |
string|null |
null |
HTML id set on the band, to link to it from a menu — only printed when set, an empty id="" being invalid and a repeated one worse |
displayIntro |
bool |
false |
Show the invitation line above the buttons (.social-share-intro, wording translated by this bundle). Off here, on for the site-wide band, which reads it from the dashboard instead (see Site-wide auto-display) |
Shape is the button's box and corners, nothing else: wide and ellipse render 65×50 (square and fully round corners respectively), square, rounded and circle render 50×50 (square, 12px and fully round). Fill is what paints that box, whatever its shape: solid is the network's own brand color, outline a brand-colored ring on a transparent background that fills in on hover, minimal the icon alone with no background or border, and transparent one translucent veil for every button instead of the brand colors.
The two are independent, so any of the 20 combinations is reachable — circle + outline and square + minimal are just two of them. Only transparent has an expectation of its own: it carries no color, so it reads as a faint veil of the surrounding text color — mixed off currentColor, which is what makes it darken on a light background and lighten on a dark one without being told which it sits on. It is meant either for a band painted through --social-share-background (see below), where the brand fills of solid would compete with the flat's own color, or for an unpainted band that should barely register.
Upgrading: these two parameters replaced a single
styleone, whose seven values were fixed shape/fill pairs. Those values are gone, not mapped — a call still passing one, or a singleton still carrying one, renders at the defaultswide+solid. See UPGRADE.md.
All networks are supported: facebook, bluesky, linkedin, pinterest, email, blogger, buffer, delicious, evernote, line, reddit, skype, stumbleupon, telegram, threads, tumblr, vk, whatsapp, wordpress, xing. Icons are resolved by network key through UiBundle's IconServiceInterface — the same brand SVGs used by the icon picker (public/icons/facebook.svg and so on), so dropping your own public/icons/{network}.svg in the consuming app overrides a bundle-provided one.
Hidden below 768px (mobile/tablet browsers have their own native share sheet), and clicking a button opens the target in a small centered popup instead of navigating away, via a Stimulus controller (see Install assets).
The band and its buttons are retuned through custom properties rather than by restating the rules — each is read with the value above as its own fallback, so a site setting none of them renders exactly as described:
| Property | Default | Retunes |
|---|---|---|
--page-share-margin-top |
2em |
The gap above the band, which sits between the page's content and the footer |
--social-share-display |
none below 768px, flex above |
The band's visibility, for a design showing it at every width |
--social-share-background |
transparent |
The band as a full-width colored flat, what UiBundle's sections get from their "background" field |
--social-share-padding |
0 |
Its breathing room, once painted |
--social-share-gap |
0.2em |
The space between buttons |
--social-share-btn-width / -height |
65px/50px (shape wide, ellipse), 50px/50px (the other three) |
The button box, whatever shape is picked |
--social-share-btn-margin |
0.2em |
Its own margin, on top of the band's gap |
--social-share-btn-radius |
0 (shape wide, square), 50% (ellipse, circle), 12px (rounded) |
The corners, whatever shape is picked |
--social-share-btn-background / -hover |
the network's brand color, color-mix(in srgb, currentColor 16%, transparent) / 30% (fill transparent) |
One uniform button instead of the brand fill |
--social-share-icon-filter |
none, invert(1) in dark mode |
The glyphs of the fills painting no dark badge under them (transparent, outline, minimal) |
--social-share-preview-background / -padding |
#4a4a4a / 1em |
The stand-in band the transparent fill is previewed over, in the dashboard and the block gallery — never on a real page |
The icons are black Font Awesome SVGs rendered as <img>, which a filter can only leave alone or invert — never tint. That is why the three colorless fills need --social-share-icon-filter where the veil itself needs nothing: currentColor carries the light/dark answer for a background, not for an image. The bundle flips it to invert(1) under :root[data-theme="dark"] and, with no data-theme set at all, under the visitor's OS preference — the same two selectors c975L/SiteBundle's own dark palette uses, so a band follows the site's theme with no wiring. Set it yourself for the one case neither can see: a dark band painted through --social-share-background while the site itself stays light.
Note the four before it have a per-variant default, one value per shape or fill: declaring one of them in :root replaces all of them at once, collapsing every variant into a single look, the shape and fill picked in the dashboard then changing nothing visible. --social-share-btn-background / -hover are offered in the theme file below for exactly that — a row painted one uniform color instead of the brand ones. The button box (--social-share-btn-width / -height / -radius) and --social-share-display are not: a design needing a size or a visibility no variant covers sets them in the app's own app.css, next to the rules it already takes over. --social-share-btn-margin is left out of that file too, the space between buttons already being --social-share-gap's.
scaffold/assets/styles/themes/social.css is the catalogue of the tokens meant to be set site-wide, installed by c975l:scaffold:install (see c975L/SiteBundle's "Themes"). One such file per bundle, each holding what that bundle reads, all concatenated into the single stylesheet the bundles already share — and each token shipped commented out at its own default, so the lines a site leaves active read as exactly what its design decides. --network-color is deliberately absent: it is set per network (.social-share-btn--facebook and its siblings each declare their own brand color), so one value in :root would paint every button alike. ScaffoldThemeTest fails if a themable token is missing from that file, if a value shown there is no longer the one in force, or if a line ships uncommented.
Site-wide auto-display
To show share buttons on every page without touching a single template, two pieces work together:
- "Boutons de partage" in the management menu (
ShareButtonsSettingsCrudController) — a small dashboard singleton (sameBlock-reuse technique as the social links block, no dedicated entity/table) letting you pick which networks, and which button shape and fill, are used site-wide. Networks are a drag-sortable checkbox list (seeassets/js/share-buttons-networks-sort.js) - their order controls the order buttons render in. A live preview (seeassets/js/share-buttons-preview.js) updates as you check/uncheck/reorder networks, change either select or toggle the invitation line below. That line ("Afficher le texte d'invitation",displayIntro, checked by default) is the one shown above the buttons: its wording is the bundle's own, translated in every language it ships (label.share_intro), only its display being a setting — a singleton saved before the setting existed shows it too, and only an explicit uncheck turns it off. social-enable-share-buttons— a boolean c975L/ConfigBundle config key (falseby default), auto-loaded from this bundle'sconfig/configs.json.
This bundle ships the band itself, as templates/shareButtons/default.html.twig — an <aside class="page-share"> wrapping the share_buttons_default() Twig function, already guarded by that config key. It reads those dashboard settings, falling back to share_buttons()'s own defaults ('main' networks, 'wide' shape, 'solid' fill) as long as nothing's been saved yet — and to the main networks again if every one of them is unchecked, social-enable-share-buttons being what hides the band.
c975L/SiteBundle's base layout includes it, outside <main> so the flex column leaves it against the footer:
{{ include('@c975LSocial/shareButtons/default.html.twig', ignore_missing: true) }}
An include resolves at runtime where a function call resolves at compile time, so a layout written that way keeps this bundle optional: ignore_missing renders nothing on a site not installing it, instead of failing on an unknown share_buttons_default(). That template path is a public contract — renaming it is a BC-break — and the markup lives here, the bundle owning the domain owning its fragment.
Flip social-enable-share-buttons to true in the dashboard and every page gets the buttons; leave it false (the default) and nothing changes. Calling share_buttons() directly, anywhere else in your own templates, is unaffected by any of this — it's a separate, always-manual entry point.
Hovering that band as an editor (the site-role-editor role) raises the same floating "Editer" button c975L/UiBundle draws over a block, pointing at the "Boutons de partage" screen — at the creation form as long as the singleton has never been saved. The fragment mounts UiBundle's blockEditOverlay controller itself, since a page composing no block at all renders no .blocks collection to mount it. The url comes from a share_buttons_edit_url() Twig function, usable in your own templates if you display the band some other way.
The "Boutons de partage" screen also carries an anchor: fill it in and the band gets that id on every page, so a navbar or footer entry can link straight to it (/#partage). Left empty — the default — the band renders with no id, exactly as before. It belongs to the site-wide settings rather than to a page, the auto-display being all-pages or nothing. share_buttons_default(id) also takes an optional id of its own, overriding that anchor for a single call.
To insert those same dashboard-defined buttons at a specific spot in a page's block flow (not just the automatic site-wide call above), pick the share_buttons_display kind from the page's block picker instead. Same thin-pointer technique as social_links_display: no display fields of its own, always reflects the current dashboard settings, edited only from the "Boutons de partage" screen.
Its one field is an anchor (same as UiBundle's page-section kinds, see that bundle's README "Anchors"): fill it in and the band gets that id, so a navbar/footer entry can link straight to it — a menu link's target select lists every block carrying an anchor. As with every page-section kind, the block's own id is appended to keep it unique on the page (partage → partage-12). Leave it empty and the band renders with no id: it never inherits the site-wide anchor above, which the layout's own call already uses on that same page.
Customer reviews
The reviews of the site's own Google listing, imported into a Review entity by a cron command and displayed through c975L/UiBundle's generic collection block. No block kind of its own: ReviewCollectionSourceProvider implements UiBundle's CollectionSourceProviderInterface, so an editor picks "Avis clients" as the source of a collection block already on the page, and templates/collection/ReviewItem.html.twig draws each card — the built-in one knowing neither a rating nor a link back to the platform.
What the back office may and may not do
A review is its author's statement, so ReviewCrudController disables new, delete and detail: creating one would be fabricating it, editing its text would falsify it, and hiding the ones that displease is exactly what the French consumer code (art. L111-7-2) forbids — while the review stays published on Google anyway, leaving the site's average visibly apart from the listing's. An abusive review is reported to the platform, where it also has to disappear.
The public reply is the one thing the site writes. Saving it publishes it on the platform first and stores it only then, so a visitor never reads an answer its author never received; emptying the field removes the reply on both sides. Sources able to take a reply implement ReviewsReplySourceInterface on top of ReviewsSourceInterface, so a read-only platform has no method to stub.
The verified flag travels with each review and is printed as a badge, L111-7-2 asking a site to say which of its reviews are verified.
Connecting the site to Google
The reviews endpoints live on the Business Profile API, whose access is not open by default: the Google Cloud project has to be allowlisted (a form in the Business Profile help centre, 7-10 business days) before its quota leaves 0 QPM. The OAuth app also has to be published "in production", or the refresh tokens it issues expire every seven days.
Once that is done, five config keys are auto-loaded from config/configs.json like any other c975L bundle's, via php bin/console c975l:config:load-all:
| Key | Filled by |
|---|---|
social-google-oauth-client-id |
the admin, from the Google Cloud console |
social-google-oauth-client-secret |
the admin — sensitive, so encrypted at rest by ConfigBundle's VaultEncryptor |
social-google-oauth-refresh-token |
the connection itself — sensitive |
social-google-business-account-id |
the connection itself |
social-google-business-location-id |
the connection itself |
The last three are never typed: "Connecter Google", in the sidebar's collapsed "Avancé" submenu (a getLinks() entry tiered advanced, run once when the site is first connected and once more the day the token is revoked), sends the editor to Google's consent screen, and /social/google/callback stores the refresh token, then resolves the account and listing the consenting account holds. A site owning several listings edits the two ids by hand afterwards, a picker for a case most sites never meet being a screen built for nobody.
An agency running several client sites fills the same client id and secret on each, and each client consents with their own Google account — so the token stored on one site only ever reaches that site's own listing.
Routes
This bundle's only routes, and the reason a consuming app now has to import its controllers:
# config/routes.yaml c975l_social: resource: '@c975LSocialBundle/src/Controller/' type: attribute
Doctrine mapping and migration
Review is this bundle's first entity, and like every c975L bundle it ships no migration of its own — the consuming app maps it and generates one:
# config/packages/doctrine.yaml doctrine: orm: mappings: c975LSocialBundle: type: attribute dir: '%kernel.project_dir%/vendor/c975l/social-bundle/src/Entity' prefix: 'c975L\SocialBundle\Entity'
php bin/console make:migration php bin/console doctrine:migrations:migrate
Importing
php bin/console c975l:social:reviews:sync # every configured source
php bin/console c975l:social:reviews:sync --source=google
Meant for cron, never for a page render: platform quotas are counted per call, and a site has to keep serving its reviews while they are down. Each run upserts on (source, external_id), so re-running updates rather than duplicates, and the platform stays authoritative on every field — a reply withdrawn there disappears here too, and a review deleted there is removed here as well. That removal is skipped when a run brings nothing back at all, an empty answer being what a revoked token or an exhausted quota looks like. An unconfigured source is stepped over rather than failing the run.
ReviewCacheInvalidationListener empties the social_reviews cache tag whenever a Review changes, which is the tag the collection source declares — so a sync leaves no stale block behind, with nothing to call by hand.
Tying the site to the listing
SameAsProvider implements c975L/UiBundle's SameAsProviderInterface, so a page carrying a contact_details block publishes, in its sameAs, the Google listing and every social link this bundle already stores — nothing of it is retyped into the contact form.
The listing's public address goes in social-google-listing-url (https://www.google.com/maps?cid=…, the cid being permanent where a Place ID is not). It is listed first, being the profile Google reconciles the site against, where a social account only corroborates it.
sameAs is what states that the site and those profiles are one business; the contact block's own mapUrl field publishes hasMap, which only says a map of the place exists. Both are worth filling, they answer different questions.
Adding another source
Implement ReviewsSourceInterface (getName(), isConfigured(), fetch() yielding ReviewData) anywhere in the app: it is auto-tagged by interface, exactly like MenuProviderInterface and friends, so there is nothing to declare in services.yaml. Add ReviewsReplySourceInterface if the platform takes replies.
Admin help procedures
ProcedureProvider (implements ConfigBundle's ProcedureProviderInterface) reads config/procedures.json and contributes one entry per documented admin workflow (configuring social links, configuring share buttons) to ConfigBundle's ProcedureBuilder, which aggregates every bundle's procedures for the dashboard AI assistant. Each entry ships fr/en/es translations, resolved to the current locale by ProcedureJsonReader.
Guided projects
SocialGuidedProjectProvider (implements ConfigBundle's GuidedProjectProviderInterface, auto-tagged like MenuProviderInterface) contributes three replayable exercises to the /management dashboard's "Guided projects" panel: "Mettre les liens vers vos réseaux" (one list for the whole site, rendered wherever the block is put), "Régler les boutons de partage" (which networks, in which order, and what they look like) and "Afficher les avis Google de votre établissement". They continue the order sequence after ConfigBundle (10-40), SiteBundle (50-80) and UiBundle (90-110), running 130, 135 and 137 — below the 140 GalleryBundle's own first project takes.
The reviews parcours is the only one whose first move happens off the site, and it deliberately doesn't re-document the Google Cloud console: a step's description is inserted as plain text (buildElement('p', …) in ConfigBundle's guided-project.js), so it could carry no link anyway, and a walkthrough of screens Google redesigns would rot silently in every site installing the package. Its first step names the wait and sends the reader to the afficher-avis-google help procedure, which is markdown and links to Google's own pages. It is also the only one opening on another bundle's screen — ConfigBundle's config list, the two OAuth keys being configs — and the only one whose last three steps carry no highlight, consenting leaving the site entirely and coming back through the callback's own redirect.
The share buttons project is contributed only while social-enable-share-buttons is on — the same condition MenuProvider applies to its own entry, since with the feature off that screen isn't in the sidebar either and a parcours walking to an unreachable screen reads as a broken one.
Both projects declare the site-role-editor role their screens demand, rather than the dashboard's own: the two are separate roles, neither implying the other, so GuidedProjectBuilder drops the parcours for an admin lacking it instead of opening on a 403.
Only the opening step of each carries an url: from there the panel walks the screen the user has been sent to, highlighting the button or the field they are meant to use next, in the order the form renders them. The two singleton screens are pointed at with .action-new, .action-edit — the index offers "create" until the row exists and "edit" ever after, and whichever is on screen is the one to click. The settings fields reuse the markers their own JS already reads ([data-share-networks-sortable], [data-share-shape-select], [data-share-fill-select], [data-share-display-intro-checkbox], [data-social-links-icon-style-select]), rather than ids of their own; the two fields with no marker of their own are pointed at with the trix-editor the introduction's textarea is replaced by, and with the anchor field's EasyAdmin id (#Block_data_anchor).
AI agent skills
The package ships a skill of its own, skills/c975l-social/SKILL.md, written for the coding agent of the site installing this bundle rather than for someone modifying it. Point your agent at it:
vendor/c975l/social-bundle/skills/
It holds what an agent gets wrong when left to its own habits — that neither feature has an entity or a table of its own, that a layout includes the share band rather than calling its Twig function, that the old style argument is gone rather than mapped, that an icon dropped in the app overrides the one shipped here — alongside the block kinds, the Twig functions, the config key and the CSS tokens, each named as it actually is in the sources.
Nothing is installed, nothing is copied into your project: the file sits in vendor/ like any other part of the package and follows it at each composer update. A user of Claude Code wanting it to load by itself symlinks it into their own skills directory:
ln -s ../../vendor/c975l/social-bundle/skills/c975l-social .claude/skills/c975l-social
Tests\SkillsTest keeps the file honest: every path, route, config slug, command, class member, Twig function, block kind and component it quotes is checked against the sources, so renaming any of them fails the build rather than leaving an agent confidently wrong.
Tip
If this project helps you save development time:
- star it on GitHub — helps others find it
- open an issue to share how you use it — genuinely useful feedback
And if you'd like to support the work directly, the Sponsor button at the top of the GitHub page is there for that. Thank you!