Skip to content

Creating designs

Simon Kraft edited this page Jul 27, 2026 · 1 revision

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.

Quick start

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 design.json manifest (schema v1)

{
	"$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" }
	]
}

Identifiers

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().

Versioning and invalidation

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

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.

background

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.

image

{ "type": "image", "source": "featured", "box": {  },
	"fit": "cover", "opacity": 0.25,
	"frame": { "color": "#0f172a", "offset": 12 } }
  • sourcefeatured, logo, cover_art, brand_background, or template_asset (with an asset key naming a declared asset). Additional sources bound via image_socialiser_binding_image_path can be allowed with the image_socialiser_pack_image_sources filter. Empty sources skip the layer.
  • fitcover (default, center-cropped) or contain (letterboxed; align steers the horizontal position: left/center/right).
  • opacity — 0–1.
  • frame — optional offset "sticker" frame: a solid rect behind the image, shifted down-right by offset pixels (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.

rect

{ "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.

text

{ "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 } }
  • sourcetitle, subtitle, site_name, author, date, category, or anything bound via image_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.
  • alignleft, center, right. maxLines caps wrapping, lineHeight is 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.

Fonts

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.

Assets

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.

Tokens

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.

supports and pins

  • supports lists the features whose settings controls stay active when your design is the site default — declare layout only if your layers actually read the layout tokens (packs are static layer stacks, so most packs won't), background_image if you use the brand_background source, cover_art for the cover art source.
  • pins declares 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.

Previewing your design

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.

Performance and caching

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.

Clone this wiki locally