-
Notifications
You must be signed in to change notification settings - Fork 0
Creating designs
Themes and plugins can ship complete Open Graph designs as design packs.
A pack is one directory with a declarative design.json manifest and
its files; registration is one explicit function call. Nothing is ever
scanned, and nothing runs on the front-end/crawler path: manifests are
read (and cached) only during generation jobs and on admin/editor
screens.
If you just want to add a design programmatically without files, the
image_socialiser_templates filter still works exactly as before —
packs are a declarative layer on top of it.
wp image-socialiser scaffold-design acme-poster
writes a starter pack (a valid, token-driven design.json plus a
README). Then register it from your theme's functions.php or your
plugin:
add_action( 'image_socialiser_register_designs', static function (): void {
image_socialiser_register_design( __DIR__ . '/acme-poster/design.json' );
} );image_socialiser_register_designs is the canonical hook; registering
directly on init or after_setup_theme works too. The function
returns the registered design id, or a WP_Error explaining what was
rejected. The design appears in the template selectors (the Design Center gallery,
per-post-type/taxonomy settings, gallery) automatically.
To ship several packs, list their manifests in one index.json
(a JSON array of paths relative to the index file) and call
image_socialiser_register_design_collection( $path_to_index_json ) —
still one explicit entry point, still no globbing.
The id must be lowercase and namespaced with your theme/plugin
slug — it must contain at least one hyphen (acme-poster, not
poster). A collision with a built-in design or an earlier
registration is rejected (built-ins/earlier registrations win) and
reported via _doing_it_wrong().
Bump version whenever you change the design or its files. The
resolved template array — including the version and every baked-in
value — is part of the content hash of every generated image, so a new
pack version mints new filenames automatically. A theme switch
schedules a bulk regeneration; the manifest cache invalidates by file
modification time on its own.
Layers use the same schema as the built-in templates, drawn bottom to
top. Colors are hex strings, boxes are pixel rectangles on the canvas
({ x, y, w, h }), and everything is sanitized at render time exactly
like internal templates. Unknown or malformed layers are dropped.
Fills the whole canvas.
{ "type": "background", "fill": { "kind": "solid", "color": "#102a43" } }
{ "type": "background", "fill": { "kind": "gradient", "direction": "vertical",
"stops": [ { "color": "token:background_from", "offset": 0 },
{ "color": "token:background_to", "offset": 1 } ] } }direction is vertical (default) or horizontal; gradients use the
first and last stop.
{ "type": "image", "source": "featured", "box": { … },
"fit": "cover", "opacity": 0.25,
"frame": { "color": "#0f172a", "offset": 12 } }-
source—featured,logo,cover_art,brand_background, ortemplate_asset(with anassetkey naming a declared asset). Additional sources bound viaimage_socialiser_binding_image_pathcan be allowed with theimage_socialiser_pack_image_sourcesfilter. Empty sources skip the layer. -
fit—cover(default, center-cropped) orcontain(letterboxed;alignsteers the horizontal position:left/center/right). -
opacity— 0–1. -
frame— optional offset "sticker" frame: a solid rect behind the image, shifted down-right byoffsetpixels (default 12). Pairs with cover-fitted images; skipped when the image source is empty.
Never put file paths in a layer. asset_path/asset_url are
stripped from manifests and injected only from the validated assets
declaration.
{ "type": "rect", "box": { … }, "fill": "#0f172a", "opacity": 0.55, "radius": 8 }
{ "type": "rect", "box": { … }, "fill": "none",
"stroke": { "color": "token:text_color", "width": 3 } }-
fill— a color, or"none"(or omitted) for stroke-only rects. -
stroke—{ color, width }, centered on the box edge (SVG semantics). -
radius— corner radius in pixels. -
opacity— applies to fill and stroke.
GD fallback fidelity: the radius is ignored, and the stroke is approximated with four straight bars.
{ "type": "text", "source": "title", "font": "token:heading_font",
"box": { … }, "align": "left", "maxLines": 3, "lineHeight": 1.15,
"size": { "min": 48, "max": 84 }, "color": "token:text_color",
"backing": { "color": "token:text_color", "padding_x": 20,
"padding_y": 10, "radius": 0, "opacity": 1 } }-
source—title,subtitle,site_name,author,date,category, or anything bound viaimage_socialiser_binding_text. Empty sources skip the layer. -
size— a fixed size, or{ min, max }for auto-fitting: the text is shrunk within the range, then word-wrapped, then ellipsized. -
align—left,center,right.maxLinescaps wrapping,lineHeightis a multiplier (default 1.2). -
backing— optional content-fitted backing boxes: one panel per fitted line, hugging the measured line width, contiguous across lines with the vertical padding applied to the first/last line only. GD ignores the radius.
Declare fonts once under fonts (id → TTF/OTF path relative to the
manifest) and reference them from text layers by id. They are
registered into the plugin's font registry, so they also appear in the
settings dropdowns and the editor preview, and their file hashes join
the content hash — replacing a font file regenerates images that use
it.
Only TTF and OTF are accepted: the server renderers rasterize through
FreeType, which cannot read WOFF/WOFF2, and browsers load TTF/OTF fine
for the preview. Every declared font is validated by its sfnt magic
bytes before FreeType ever sees it; an invalid font rejects the whole
pack. Font ids are global — prefix them like the design id
(acme-display); a taken id keeps its earlier registration.
You are responsible for the font's license permitting embedding and redistribution.
Declare image files once under assets (name → file relative to the
manifest) and reference them from image layers with
"source": "template_asset", "asset": "name". PNG, JPEG, GIF, and
WebP are accepted.
Every declared file — fonts and assets — must resolve inside the
pack directory: paths are realpath()-checked, so ../ traversal,
symlinks escaping the directory, and stream wrappers are rejected (the
whole pack fails to register). Files outside wp-content render fine
on the server but get no URL for the browser preview.
Colors and fonts may be literals or token: references, so a pack can
either pin its own values or inherit the site's brand tokens (and
thereby every design change the user, theme, or network makes):
| Reference | Resolves to |
|---|---|
token:background_from |
primary background color |
token:background_to |
secondary background color |
token:text_color |
text color |
token:muted_color |
muted color |
token:heading_font |
heading font id |
token:body_font |
body font id |
Token references are resolved on every template build — not cached — so they always reflect the current effective tokens.
-
supportslists the features whose settings controls stay active when your design is the site default — declarelayoutonly if your layers actually read the layout tokens (packs are static layer stacks, so most packs won't),background_imageif you use thebrand_backgroundsource,cover_artfor the cover art source. -
pinsdeclares layout tokens your design fixes on purpose (e.g.{ "text_align": "center" }); the matching controls are shown greyed out when your design is the default.
The editor panel and the settings gallery render your design live —
including pack fonts and assets — through the same layer model the
server uses. The fastest loop: register the pack, open the settings
page, and watch the gallery tile while you edit design.json (the
cache invalidates on every file save via mtime).
For pixel-accurate output, render a real PNG before committing:
wp image-socialiser render <post-id>
The project's own rule of thumb applies to packs too: render mockups with the actual engine and review them before you ship a design.
The parsed, validated manifest is cached in a transient keyed by the manifest path, invalidated by file modification time and plugin version. Registration costs one cached read per request on admin/ editor screens and inside generation jobs; a visitor or crawler hitting the site never touches any of this — that path reads a meta value and serves a static file.
{ "$schema": "https://simon.blog/image-socialiser/design-schema/v1.json", "id": "acme-poster", // required — see "Identifiers" "label": "Acme Poster", // optional, defaults to the title-cased id "version": 2, // participates in the content hash "canvas": { "w": 1200, "h": 630 },// optional, defaults to 1200×630 "supports": [ "layout" ], // honored features (see below) "pins": { "text_align": "center" }, "fonts": { // id → file, relative to the manifest "acme-display": "fonts/AcmeDisplay.ttf" }, "assets": { // name → file, relative to the manifest "grain": "assets/grain-bg.png" }, "layers": [ { "type": "image", "source": "template_asset", "asset": "grain", "box": { "x": 0, "y": 0, "w": 1200, "h": 630 }, "fit": "cover" }, { "type": "text", "source": "title", "font": "acme-display", "box": { "x": 100, "y": 180, "w": 1000, "h": 250 }, "align": "center", "maxLines": 3, "size": { "min": 56, "max": 104 }, "color": "token:text_color" } ] }