PHPackages                             c975l/social-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. c975l/social-bundle

ActiveSymfony-bundle[Utility &amp; Helpers](/categories/utility)

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.

v2.3.0(1w ago)2299↑40%1MITPHPPHP &gt;=8.4CI passing

Since Jul 10Pushed 1w ago1 watchersCompare

[ Source](https://github.com/975L/SocialBundle)[ Packagist](https://packagist.org/packages/c975l/social-bundle)[ Docs](https://github.com/975L/SocialBundle)[ Fund](https://buymeacoff.ee/laurentmarquet)[ Fund](https://opencollective.com/laurent-marquet)[ RSS](/packages/c975l-social-bundle/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (21)Versions (39)Used By (1)

SocialBundle
============

[](#socialbundle)

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.

[![GitHub](https://camo.githubusercontent.com/3d0043e812c3392481cf40dc200a55df9aa7e876fbc0b27d5118402be92b1343/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f3937354c2f536f6369616c42756e646c65)](https://github.com/975L/SocialBundle/blob/master/LICENSE)[![Packagist Version](https://camo.githubusercontent.com/ced1e5cac3070597109f2d564748fc60a5d113b66e550c43cb665e1e9d216f45/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f633937356c2f736f6369616c2d62756e646c65)](https://packagist.org/packages/c975l/social-bundle)[![PHP Version](https://camo.githubusercontent.com/417bc05247b91380afe2f168ea0f93074927cb01368e8cc0e3defe1686ef3412/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f633937356c2f736f6369616c2d62756e646c65)](https://packagist.org/packages/c975l/social-bundle)[![Codacy Grade](https://camo.githubusercontent.com/7faf88e8e36d69b1fb678072669df562bbbf4abe750eb407a3dd97f10485fdb8/68747470733a2f2f6170702e636f646163792e636f6d2f70726f6a6563742f62616467652f47726164652f3032356463353436313464363464383061306565343536323632653938623163)](https://app.codacy.com/gh/975L/SocialBundle/dashboard)

Why SocialBundle
----------------

[](#why-socialbundle)

[![SocialBundle](.github/images/SocialBundle.svg)](.github/images/SocialBundle.svg)

Add SocialBundle on top of the shared [UiBundle](https://github.com/975L/UiBundle) + [ConfigBundle](https://github.com/975L/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](https://bundles.975l.com/pages/social-bundle), and browse every block kind live in the [block gallery](https://bundles.975l.com/pages/blocks).

---

> **TL;DR** — Social links and share buttons for a c975L site. The links are stored as a `social_links` block reusing UiBundle's generic `Block` entity rather than a dedicated table (the "singleton CRUD" pattern), displayed anywhere through a `social_links_display` block or site-wide. Replaces the former ShareButtonsBundle.

Contents
--------

[](#contents)

- **Setup** — [requirements](#requirements) · [installation](#installation) · [assets](#install-assets)
- **Using it** — [social links block](#social-links-block) · [admin management](#admin-management) · [rendering](#rendering-the-block) · [styling](#styling) · [share buttons](#share-buttons) · [site-wide auto-display](#site-wide-auto-display) · [customer reviews](#customer-reviews) · [admin help procedures](#admin-help-procedures) · [guided projects](#guided-projects) · [AI agent skills](#ai-agent-skills)

Features
--------

[](#features)

- **Social links block**: a `ui.block` kind (`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-buttons` config 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](https://github.com/975L/UiBundle)'s searchable `IconPickerType`
- **Stylesheet auto-registration** via UiBundle's `BundleStylesheetProviderInterface` — no manual `` needed
- **Script auto-registration** via UiBundle's `BundleScriptProviderInterface` — no manual `` needed
- **Admin menu entry** registered automatically via `MenuProviderInterface`
- **Admin help procedures** contributed automatically via `ProcedureProviderInterface`
- **Guided projects** contributed automatically via `GuidedProjectProviderInterface` — see [Guided projects](#guided-projects)
- **Customer reviews**: imported from the site's own Google Business Profile listing into a `Review` entity by a cron command, displayed through UiBundle's generic `collection` block — read-only, the public reply being the only thing the back office writes; see [Customer reviews](#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](#ai-agent-skills)

---

Requirements
------------

[](#requirements)

- PHP &gt;= 8.4
- Symfony 8
- [c975L/CoreBundle](https://github.com/975L/CoreBundle), the single package shipping ConfigBundle and UiBundle
- EasyAdmin

---

Installation
------------

[](#installation)

### Download

[](#download)

```
composer require c975l/social-bundle
```

### Install assets

[](#install-assets)

```
php bin/console assets:install --symlink
```

This exposes the bundle's compiled stylesheet at `public/bundles/c975lsocial/css/styles.min.css`.

No routes to enable: this bundle only contributes EasyAdmin dashboard entries (auto-registered, see [Admin management](#admin-management)), a Twig component and Twig functions — nothing front-end-routed of its own. Its single configuration key (`social-enable-share-buttons`, see [Site-wide auto-display](#site-wide-auto-display)) is 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](https://github.com/975L/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
-----

[](#usage)

### Social links block

[](#social-links-block)

Registers a `social_links` `ui.block` kind (see [c975L/UiBundle](https://github.com/975L/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's `TrixEditorType`, 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) or `text` ("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](#styling) below), no separate icon asset - same glyph in every case; `text` prints 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 as `aria-label` regardless).

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](#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 `` 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 `` internally, so it always reflects the current site-wide links, edited only from [Admin management](#admin-management) — no separate data, no duplication, no extra table.

#### Icons

[](#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](#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](https://fontawesome.com/) (CC BY 4.0) — keep attribution if you redistribute this bundle's icons on their own.

### Admin management

[](#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

[](#rendering-the-block)

```

```

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

[](#styling)

The whole block is wrapped in a `.social-links-block` (a ``, not a ``: it carries no heading of its own, and a headingless `` 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](#social-links-block)) and each `` 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

[](#share-buttons)

Migrated from the now-abandoned [c975L/ShareButtonsBundle](https://github.com/975L/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') }}
```

ParameterTypeDefaultDescription`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](#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 `style` one, 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 defaults `wide` + `solid`. See [UPGRADE.md](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](#social-links-block) (`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](#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:

PropertyDefaultRetunes`--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` aboveThe 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 modeThe 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 pageThe icons are black Font Awesome SVGs rendered as ``, 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](https://github.com/975L/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](https://github.com/975L/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

[](#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 (same `Block`-reuse technique as the [social links block](#social-links-block), no dedicated entity/table) letting you pick which networks, and which button [shape and fill](#share-buttons), are used site-wide. Networks are a drag-sortable checkbox list (see `assets/js/share-buttons-networks-sort.js`) - their order controls the order buttons render in. A live preview (see `assets/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](https://github.com/975L/ConfigBundle) config key (`false` by default), auto-loaded from this bundle's `config/configs.json`.

This bundle ships the band itself, as `templates/shareButtons/default.html.twig` — an `` 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](https://github.com/975L/SiteBundle)'s base layout includes it, outside `` 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](https://github.com/975L/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`](#social-links-block): 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
----------------

[](#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](https://github.com/975L/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

[](#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

[](#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`:

KeyFilled 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 itselfThe 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

[](#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

[](#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

[](#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. 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

[](#tying-the-site-to-the-listing)

`SameAsProvider` implements [c975L/UiBundle](https://github.com/975L/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

[](#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
---------------------

[](#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
---------------

[](#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
---------------

[](#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](https://github.com/975L/SocialBundle) — helps others find it
- [**open an issue**](https://github.com/975L/SocialBundle/issues/new) 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!

###  Health Score

51

—

FairBetter than 95% of packages

Maintenance98

Actively maintained with recent releases

Popularity20

Limited adoption so far

Community12

Small or concentrated contributor base

Maturity63

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

37

Last Release

10d ago

Major Versions

v0.1 → v1.02026-07-11

v1.4.4 → v2.0.02026-08-03

PHP version history (2 changes)v0.1PHP &gt;=8.1

v1.4.0PHP &gt;=8.4

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/16478286?v=4)[Laurent Marquet](/maintainers/LaurentMarquet)[@LaurentMarquet](https://github.com/LaurentMarquet)

---

Top Contributors

[![LaurentMarquet](https://avatars.githubusercontent.com/u/16478286?v=4)](https://github.com/LaurentMarquet "LaurentMarquet (46 commits)")

---

Tags

symfonybundlesocial

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/c975l-social-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/c975l-social-bundle/health.svg)](https://phpackages.com/packages/c975l-social-bundle)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k18.3M433](/packages/easycorp-easyadmin-bundle)[2lenet/crudit-bundle

The easy like Crud'it Bundle.

1617.3k16](/packages/2lenet-crudit-bundle)[rcsofttech/audit-trail-bundle

Enterprise-grade, high-performance Symfony audit trail bundle. Automatically track Doctrine entity changes with split-phase architecture, multiple transports (HTTP, Queue, Doctrine), and sensitive data masking.

12017.1k](/packages/rcsofttech-audit-trail-bundle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
