bensomething / craft-placard
Configurable social images for entries.
Package info
github.com/bensomething/craft-placard
Type:craft-plugin
pkg:composer/bensomething/craft-placard
Requires
- php: ^8.2
- ext-imagick: *
- craftcms/cms: ^5.10
Requires (Dev)
- craftcms/ecs: dev-main
- craftcms/phpstan: dev-main
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-16 11:09:22 UTC
README
Generates social (OG) cards with Imagick for Craft entries. Cards are rendered on demand and cached under storage/runtime/og-images.
A card is a stack of four text elements (label, title, description and meta) over a solid colour, with an optional logo beside the label and an optional background image.
Requirements
- Craft CMS 5.10 or later
- PHP 8.2 or later
- The
imagickPHP extension
Install
Placard is available from Packagist while it's being tested:
composer require bensomething/craft-placard:^1.0@beta php craft plugin/install placard
Configure
Copy example.config.php to config/placard.php. Settings → Plugins → Placard previews the card your config produces.
sections and entryTypes override any setting except defaultImagePath, keyed by handle. Entry types win over sections. styles are named overrides editors choose between per entry (see Styles).
return [ 'position' => 'bottom', 'sections' => [ 'artists' => [ 'position' => 'middle', 'background' => ['colorField' => 'colour'], ], ], 'entryTypes' => [ 'featuredArtist' => ['meta' => false], ], ];
Paths accept aliases (@root, @webroot) and env vars. The canvas is fixed at 1200×630, the size every platform recommends.
No commercial font is bundled. Inter (SIL OFL) is the default. Set fontPath to use your own font throughout, and an element's own fontPath (see Elements) to give it a different font.
Nested fields
Settings that name a field (imageFieldHandles, the background's imageField and every colorField) also take a path into a Matrix or Content Block field. seo.seoImage reads seoImage from the entries in seo and uses the first one with a value. Paths can go more than one level deep.
'imageFieldHandles' => ['seo.seoImage', 'hero'], 'background' => ['colorField' => 'seo.colour'],
Card text templates reach nested fields themselves: '{seo.one().seoTitle ?? title}'.
Templates
{% set ogImage = craft.placard.shareImageUrl(entry ?? null) %}
{% if ogImage %}
<meta property="og:image" content="{{ ogImage }}">
{% endif %}
| Helper | Returns |
|---|---|
shareImageUrl(entry) |
The image the entry shares (its Social Image field's choice, else the first image in imageFieldHandles, else the card). Anything that isn't an entry gets default() |
previewImageUrl(entry) |
As shareImageUrl(), but the card also renders drafts for logged-in users. For the CP, like a field layout's Template element (craft.placard.previewImageUrl(element)), not front-end templates |
entryHasImage(entry) |
Whether that's one of the entry's own images |
url(entry) |
The card's URL. Only live entries are served, so drafts can't be read from it |
default() |
defaultImagePath, for pages with no entry, or null when it isn't set |
With SEOmatic
SEOmatic's image source settings only list Assets fields, so they can't read the Social Image field. Pass the URL to SEOmatic from your layout instead:
{% set ogImage = craft.placard.shareImageUrl(entry ?? null) %}
{% if ogImage %}
{% do seomatic.meta.setAttributes({
seoImage: ogImage,
ogImage: ogImage,
twitterImage: ogImage,
}) %}
{% endif %}
This overrides the section's SEOmatic settings. SEOmatic doesn't transform an image given as a URL, and cards are already 1200×630. Use the summary_large_image Twitter card so they show at full size.
Social Image field
Lets editors choose what each entry shares (the generated card, a custom image, or one of the entry's image fields) with a preview that updates as they edit. Its settings choose which sources are offered, which image fields are listed and the default. Asset fields inside the entry's Matrix and Content Block fields are listed too, as SEO → SEO Image. A layout can only hold one, and entries without URLs show a note instead.
The field must be in the entry type's own layout (a tab is fine), not inside a Matrix or Content Block field.
Styles
With styles in the config, the field also offers a Style dropdown for the generated card. Each style is a set of overrides like sections, applied after the section's and entry type's, so the editor's choice wins. name labels it in the dropdown, else the handle is used.
'styles' => [ 'light' => [ 'name' => 'Light', 'background' => '#ffffff', 'title' => ['color' => '#111111'], ], 'photo' => [ 'name' => 'Photo', 'background' => ['imageField' => 'hero', 'opacity' => 0.6], ], ],
The field's settings choose which styles it offers. The dropdown only shows while the card is what's shared, and Settings → Plugins → Placard previews each style.
Card text
Each element's text is an object template rendered against the entry, given as its text key or as a string in place of the element's array:
'title' => ['text' => '{title}', 'size' => 96], 'label' => '{type.name}', 'description' => '{summary ?: seoDescription}', 'meta' => '{postDate|date("j F Y")}',
HTML is stripped. Description and meta are empty by default, so they don't show until set. A template that errors is logged and leaves that element blank.
DefineCard event
For anything a template can't do:
use bensomething\placard\events\DefineCardEvent; use bensomething\placard\services\Generator; use yii\base\Event; Event::on(Generator::class, Generator::EVENT_DEFINE_CARD, function (DefineCardEvent $e) { if ($e->entry->section?->handle === 'home') { $e->title = 'My Brand'; $e->label = ''; } });
The event has title, label, description, meta, background and backgroundColor, plus the card's resolved settings, which can be changed for that card alone. An empty value drops that element.
Console
php craft placard/preview 123 # an entry's card php craft placard/preview --title="Hello world" # a dummy card
Also takes --label, --description, --meta and --image. Output goes to storage/runtime/og-images-preview/.
Settings
| Setting | Default | Notes |
|---|---|---|
position |
bottom |
top, middle, bottom or between (first and last at the edges) |
align |
left |
left, center or right |
textEdges |
caps |
Place by cap height to baseline (caps) or by the letters' ink (ink) |
padding |
80 |
Inset for the text in px, as CSS shorthand: one value, [vertical, horizontal], [top, horizontal, bottom] or [top, right, bottom, left]. Text wraps at the width between the sides, and center centres within them |
gap |
24 |
Space below each element in px, between line boxes, so the visible space also takes in line height and descenders. With between it's a minimum. Elements can set their own |
fontPath |
Inter | Font for every element, unless it sets its own. Unset, each element uses the bundled Inter in its own weight (Bold title, Medium label, Regular description and meta) |
imageFieldHandles |
['seoImage', 'hero', 'featuredImage'] |
Share image fields for entries without a Social Image field |
defaultImagePath |
img/og-default.png |
Returned by default() |
sections, entryTypes |
[] |
Overrides by handle |
styles |
[] |
Named overrides editors choose per entry, see Styles |
label, title, description, meta |
Each element's settings, see Elements | |
logo, background, border |
See below |
Element, logo, background and border settings are each an array under their name. In sections and entryTypes, an array changes only the keys it names, so 'border' => ['color' => '#ff0000'] keeps the border's widths.
Elements
label, title, description and meta each take an array of their settings. A string sets just the text, and false hides the element.
'title' => [ 'text' => '{title}', 'size' => 96, 'lineHeight' => 1.05, 'maxLines' => 2, ], 'description' => ['text' => '{summary}', 'lineHeight' => 1.3], 'meta' => false,
| Key | Default (label, title, description, meta) | Notes |
|---|---|---|
text |
{type.name}, {title}, empty, empty |
Object template, see Card text |
show |
true |
Turn the element off. Hiding the label keeps the logo |
size |
28, 116, 32, 28 |
Text size in px |
lineHeight |
1.02 |
A multiple of the size, as in CSS. The extra space is shared above and below each line |
maxLines |
1, 3, 2, 1 |
Lines before the text is clamped with an ellipsis. A title needing fewer is balanced across the lines it uses |
uppercase |
false, true, false, false |
Set the text in capitals |
color |
#a3a3a3, #ffffff, #a3a3a3, #a3a3a3 |
Text colour |
colorField |
none | A Color field (or hex text field) to take the colour from, falling back to color |
fontPath |
none | The element's font, over fontPath |
gap |
none | Space below the element, over gap. Not on meta, which is always last |
Logo
'logo' => ['path' => '@webroot/img/logo.svg', 'height' => 40],
A string sets just the path, and false hides the logo.
| Key | Default | Notes |
|---|---|---|
path |
none | PNG or SVG drawn beside the label |
height |
48 |
Height in px. Its width follows its proportions |
show |
true |
Turn the logo off |
Background
'background' => [ 'color' => '#1f1f1f', 'imageField' => 'hero', 'opacity' => 0.4, 'blendMode' => 'multiply', ],
A string sets just the colour. The image is the imageField's, then the share image (with useShareImage), then imagePath. With a Social Image field, the share image is the custom image or image field the editor picked. With the card picked, or no field, it's the first image in imageFieldHandles.
| Key | Default | Notes |
|---|---|---|
color |
#000000 |
Canvas colour |
colorField |
none | A Color field (or hex text field) to take the colour from, falling back to color |
imageField |
none | Asset field for the background image |
useShareImage |
false |
Fall back to the share image as the background: the Social Image field's pick, else the first image in imageFieldHandles |
imagePath |
none | Fixed background image, used last |
fit |
cover |
cover (cropped around the focal point) or contain |
padding, radius |
0 |
Inset and corner radius in px |
grayscale |
false |
Set the image in grayscale |
opacity |
0.25 |
Strength of the image |
blur |
0 |
Blur strength in px, roughly as CSS blur() |
blendMode |
normal |
How the image blends with the colour: multiply, screen, overlay, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference or exclusion, as in CSS |
Border
'border' => ['width' => [8, 0, 0, 0], 'color' => '#f5b400'],
A number or list sets just the widths, so 'border' => [8, 0, 0, 0] is a bar along the top in the default colour.
| Key | Default | Notes |
|---|---|---|
width |
0 |
Widths in px, as CSS shorthand like padding. Drawn over the background image, and the text sits inside it |
color |
#ffffff |
Border colour |
colorField |
none | A Color field (or hex text field) to take the colour from, falling back to color |
Licence
MIT. Bundled Inter fonts are SIL OFL 1.1 (see src/resources/fonts/OFL.txt).