Share images (Open Graph)
The image a link to the store shows when it is shared. What a merchant designs in the Customizer, how a developer writes the TSX template and declares what a merchant can change, the layout rules of the renderer, limits and how to check the result.
On this page
When someone shares a link to the store, the social network or messenger shows a preview card:
a title, a description and an image. That image is the page's share image (og:image).
The platform draws one for every page from a TSX template, fills it with the page's own data
(the product name and price, the post title, the store logo) and serves it as a 1200×630 JPEG
from the store's own domain.
Facebook, LinkedIn, WhatsApp, X, Slack, Discord, Telegram and iMessage all read the same image.
Two people work on a share image, each in their own place:
| Who | Where | What |
|---|---|---|
| Merchant | Customizer, Share images | The design of a page kind: the elements on the image, their order and texts, and the values of the template's settings |
| Developer | Code editor, og/<kind>.tsx | The template: the code that draws the image, and its meta export, which declares the settings and elements of a design |
A theme needs no template. A page kind without one draws the platform card, the platform's own template, and the merchant designs that.
One template per page kind
A template is a file in the theme's og/ folder, named after the kind of page it draws.
| File | Pages |
|---|---|
og/default.tsx | The store card: the home page and every page without its own |
og/product.tsx | Product pages |
og/collection.tsx | Category pages |
og/article.tsx | Blog posts |
og/page.tsx | Content pages |
- A kind without a file draws the platform card. A new theme has no
og/folder at all, and every page still gets an image. - A page that is none of these (the blog index, search results) uses
defaulttoo. - Only these five names are drawn.
og/takes.tsxfiles with a lowercase name, directly in the folder: no subfolders and no helper modules. - Each kind has one design, stored in the theme next to its other settings.
Design an image in the Customizer
Open Share images in the structure tree and pick a page kind: Store card, Product, Category, Blog post or Page. Labels are quoted here as the Customizer shows them in English.
- The stage shows the image inside a link preview card, with the page's title, its description and the store's address, the way a network shows a shared link. A chip beside the kind's name says who draws the image: Platform design or Theme template.
- The tree lists the elements of the image under the kind, in the order they are drawn, with Add element as the last row.
- The inspector holds the image as a whole: whether it is generated, the page it is previewed with, who draws it and the settings of the template. With an element selected it holds that element's settings.
Every change is part of the theme draft. It is saved automatically, undo takes it back, and shoppers and crawlers see it after you Publish.
Elements of an image
An element is one thing on the image: a text, the price, a badge, the logo.
- Select an element in the tree to change its settings in the inspector.
- Move it by dragging its row, or with Move up and Move down.
- Hide takes it off the image and keeps its settings. Show brings it back.
- Duplicate puts a copy right behind it. Remove deletes it.
Add element offers two lists:
- Values adds a text that already holds a value of the page: the product name, the price, the store name. On the platform card a title or the store name is added as a heading, and any other value as muted body text.
- Elements adds an empty element of one of the template's element types.
The platform card has these element types:
| Element | Draws | Settings |
|---|---|---|
| Text | A text: a value of the page, words of your own, or both | Text; Style (Heading, Body, Label); Size (Automatic, Small, Medium, Large, Extra large); Color (Text, Muted, Accent); Lines at most (1 to 6) |
| Price | The product's price. Product pages only, and one per image | Show the original price, struck through beside the price when the product is discounted; Size (Small, Medium, Large) |
| Badge | A short text in a pill, on one line | Text; Look (Accent, Soft, Outline) |
| Accent line | A short bar in the accent colour, with the text or along a whole side of the image | Placement (With the text, Along the right edge, Along the left edge: a full-height bar outside the stack); Width (24 to 400 px, with the text only) |
| Logo | The store logo, or the store name when the store has no logo. One per image | Height (32 to 120 px) |
| Space | Empty room between two elements | Height (8 to 160 px) |
- With Size on Automatic, a heading is set smaller the longer its text is, and headings and body text are set smaller beside a picture.
- A text takes the lines its words need, never more than its Lines at most. A longer text is cut with an ellipsis.
- An element with nothing to show is left out together with its spacing: a Text or a Badge whose text comes out empty, a Price on a product without a price.
- The card spaces the elements itself. Space adds room where you want more.
- The elements share the column above the footer, or the whole height inside the margin when the footer is off. When they do not all fit, the card shortens texts before it leaves anything out. Texts give up lines one at a time: texts in the Label style first, then Body, and Heading last. Of two texts of the same style the one with more lines gives up first, and of two with as many lines the later one. A shortened text ends with an ellipsis. Once every text is down to one line, elements are left out from the end of the list. Nothing is drawn over the footer, and no text is cut through a line.
- The card works out the lines of a text from its size and the width of the column, before the image is drawn. Where room is left in the column, it goes back to the texts, headings first.
- The Price stays on one line. A size that would run past the column is brought down until the price fits, and the original price is left out where there is no room for both.
- An image holds at most 24 elements, hidden ones counted. An element type that allows one per image cannot be added or duplicated a second time.
Each kind starts with these elements:
| Kind | Elements, from the top |
|---|---|
| Store card | Accent line along the right edge, the store name as a heading, the store tagline |
| Product | Accent line, the brand as a label (the category when the product has no brand), the product name as a heading, the price |
| Category | Accent line, the category name as a heading, the category description |
| Blog post | Accent line, the blog category as a label, the post title as a heading, the excerpt |
| Page | Accent line, the page title as a heading, the page description |
Values in a text
The text of a Text or a Badge can mix your own words with values of the page. Insert value under the text field writes a value where the cursor last stood. A value is a name between double braces, and the image shows the page's own value in its place:
| You write | The image shows |
|---|---|
{{product.title}} | Washed linen throw in oat |
New: {{product.title}} | New: Washed linen throw in oat |
{{product.brand}} / {{product.category}} | Fjord & Field / Throws & blankets |
From {{product.price.amount}} at {{store.name}} | From €89.00 at Fjord & Field |
- Every text and number of the page is a value. Insert value lists the texts a page of
the kind has. The names of all values are in the table under Page data:
page.title,store.name,product.price.amount,article.publishedAtand the rest. - A value the page does not have is left out.
{{product.title}}is empty on a blog post, and{{product.brand}}is empty on a product without a brand. - Give a fallback with a bar.
{{product.brand | product.category}}shows the brand, or the category when the product has no brand. The first value that is not empty wins. - A value that is left out takes one separator along: the one before it, or the one after
it when none stands before it. On a product without a brand,
{{product.brand}} / {{product.category}}shows the category alone, without the slash, and{{product.category}} / {{product.brand}} / {{store.name}}shows the category and the store name with one slash between them. A separator is a slash, a vertical bar, a comma, a semicolon, a colon, a hyphen, a dash, a middle dot or a bullet, with the spaces around it. - One design serves every language of the store. A value shows the page's text in the page's language. Words you type are not translated: they read the same on every language.
- A text holds at most 600 characters, and the image shows at most 600.
Settings of the platform card
With the kind selected, the inspector shows the settings of the template. The platform card has these:
| Group | Setting | Choices | Starts as |
|---|---|---|---|
| Picture | Position | Right, Left, Behind the text, Hidden | Right |
| Fit | Fill the frame, Show all of it | Show all of it on products, Fill the frame on categories and posts | |
| Layout | Alignment | Left, Center | Left |
| Vertical position | Top, Middle, Bottom | Middle on the store card, Top on the other kinds | |
| Margin | 40 to 96 px | 60 px | |
| Colors | Color scheme | Theme, Soft, Inverted, Brand color, Custom | Theme |
| Background, Text, Accent | A colour each | Empty | |
| Background picture | Picture | A picture you upload or pick from the media library | None |
| Overlay | 0 to 95 % | 60 % | |
| Footer | Show the footer | On or off | On |
| Store | Logo, or the name without one, Logo only, Name, Nothing | Logo only on the store card, Logo, or the name without one elsewhere | |
| Web address | On or off | On | |
| Publication date | On or off | On |
- Picture is there on the kinds whose page has a picture of its own: the product photo, the category image, the post cover. The store card and content pages do not have the group. A page without a picture draws its text across the full width.
- Fit applies to a picture on the right or the left. Fill the frame crops the picture to the frame. Show all of it draws the whole picture inside a bordered box.
- Margin is the space kept clear on every side. Some apps crop the edges of a share image: below 60 px, text may be cut.
- Color scheme takes its colours from the theme, so the image follows the brand. Theme is the theme's background and text colour. Soft uses the theme's muted background. Inverted swaps background and text; its accent is the theme's primary colour, or the text colour when the primary could not be told from the new background. Brand color puts the image on the theme's primary colour. Custom shows Background, Text and Accent: a colour you leave empty stays the theme's.
- Background picture fills the image behind the text. Overlay tints it with the background colour so the text stays readable: 0 % leaves the picture as it is, 95 % nearly covers it. With Position on Behind the text, the page's own picture is the picture behind the text, and the one chosen here is used on a page without a picture.
- Store, Web address and Publication date are shown while Show the footer is on. Logo only draws nothing for a store without a logo. Publication date is there on blog posts.
- A setting you changed is marked and can be reset on its own.
Preview, safe area and reset
- Preview with picks the page the stage draws: a real product, category, post or page of the store. A store with nothing published for the kind sees sample data on its own name, logo, colours and fonts. The store card has one subject, the store, and no picker.
- Safe area on the stage shades the bands X cuts off when it crops the image to 2:1, and frames the margin. Keep text and logos inside the frame.
- Reset design drops the design of the kind after a confirmation. Every setting and element goes back to what the template starts with.
Which pages get a generated image
The switch under Share image turns a kind off. A page of a kind that is off links, in this order: its own picture (the product photo, the category image, the post cover); else the store card, when the store card is on; else the platform's static image, the same for every store. A page that is not public (a draft or scheduled post or page, anything seen in a preview) never links a generated image either.
When the theme has a template
A kind whose theme has og/<kind>.tsx is designed the same way, with what that template
declares in place of the platform card's elements and settings.
- The chip on the stage and the row in the tree say Theme template.
- Add element offers the element types of the template, and Values when one of them has a text that takes values. The inspector shows the template's settings. A template that declares neither has nothing to adjust: its code decides everything.
- The labels are the ones the developer wrote in the template. A template that declares exactly what the platform card declares is worded like the platform card, in your language.
- Open in code editor opens the file. For a kind without a file the button reads Edit the code: it opens the code editor, which offers to create the file.
Write a template
Template code is written in the code editor. og/ is listed there with the other content
folders, and the editor type-checks the templates as a project of their own: it suggests only
react and @poltura/og-image, knows no browser globals, and flags an import that is not
allowed.
Theme: New Share Image Template… in the command palette creates the file for a kind that has none, from one of two starting points: Platform card or Own layout. Both are below.
Saving writes the theme draft. Shoppers and crawlers see the new image after you Publish.
The file
| Part | Rule |
|---|---|
OgImage | Required named export. A function that takes OgImageProps and returns exactly one root element. |
meta | Optional named export, an OgTemplateMeta: what a merchant can change. See Declare what a merchant can change. |
| Imports | react and @poltura/og-image only. No relative imports, no @poltura/theme-sdk, no other package. |
| Components | You may declare helper components in the same file. They must be plain functions: no hooks, no classes, no async. |
@poltura/og-image gives you:
| Export | Use |
|---|---|
OgImageProps, OgTemplateMeta | The types of the props and of meta |
OgCard, ogCardMeta(kind) | The platform card and its meta |
clampText(text, max) | Cuts a text at a word boundary and adds an ellipsis when it is longer than max characters |
isHexColor(value) | Whether a text is a hex colour |
mixHex(from, to, share) | The hex colour share (0 to 1) of the way from one hex colour to another |
inkOn(background) | White or near-black, whichever reads better on a hex colour |
contrastOf(first, second) | How far apart two hex colours are in lightness, as WCAG measures contrast: from 1 (none) to 21 (black on white) |
Start from the platform card
This is the platform's own product template. For another kind only the name in
ogCardMeta('product') differs.
import { OgCard, ogCardMeta, type OgImageProps } from '@poltura/og-image';
export const meta = ogCardMeta('product');
export function OgImage(props: OgImageProps) {
return <OgCard {...props} />;
}ogCardMeta(kind) declares every setting and element of the platform card, and OgCard draws
them. A template like this one changes nothing yet: it is the place to start when you want the
platform's design with something added. Draw around the card, and add settings of your own to
its meta:
import { OgCard, ogCardMeta, type OgImageProps, type OgTemplateMeta } from '@poltura/og-image';
const card = ogCardMeta('product');
export const meta: OgTemplateMeta = {
...card,
settings: [
...(card.settings ?? []),
{ type: 'header', id: 'ribbon_header', label: 'Sold out ribbon' },
{ type: 'text', id: 'ribbon', label: 'Text', default: 'Sold out' },
],
};
export function OgImage(props: OgImageProps) {
const { product, settings, theme } = props;
const ribbon = product?.available === false ? String(settings.ribbon) : '';
return (
<div
style={{
position: 'relative',
display: 'flex',
width: 1200,
height: 630,
fontFamily: theme.fonts.body,
}}
>
<OgCard {...props} />
{ribbon ? (
<div
style={{
position: 'absolute',
top: 0,
right: 0,
padding: '12px 28px',
fontSize: 24,
fontWeight: 700,
color: theme.colors.primaryForeground,
backgroundColor: theme.colors.primary,
}}
>
{ribbon}
</div>
) : null}
</div>
);
}OgCardis 1200×630. It draws the page asprops.settingsandprops.elementssay, under the idsogCardMetadeclares. Pass it the props unchanged.- An
idis unique within its list. The card's setting ids areimage,image_fit,align,position,padding,scheme,background,text,accent,background_image,overlay,footer,footer_brand,footer_hostandfooter_date: a setting of yours that reuses one is left out, and the Customizer reports it. - The card changes with the platform. A template that draws it gets the platform's later improvements, and settings the card gains appear in the Customizer without an edit.
- The Customizer words the platform card itself, in the merchant's language. It does so for a
kind without a template file, and for a template whose
metais exactlyogCardMeta(kind), like the first one above. Once themetadiffers in anything (a setting added, an element type relabelled), every label is shown as themetaholds it, the card's own included, and those are English. The template with the ribbon is such a one.
Start from your own layout
This is the Own layout the code editor creates for the store card. For another kind only
the label differs (Product, Collection, Article, Page): the body works for every
page kind, because every kind has a page, a store and a theme.
import type { OgImageProps, OgTemplateMeta } from '@poltura/og-image';
// What a merchant can change in the Customizer. props.settings and props.elements carry the
// result: every declared setting is there, and a value that is missing or invalid is its default.
export const meta: OgTemplateMeta = {
label: 'Store card',
settings: [
{ type: 'color', id: 'accent', label: 'Accent color' },
{
type: 'select',
id: 'align',
label: 'Alignment',
default: 'left',
options: [
{ value: 'left', label: 'Left' },
{ value: 'center', label: 'Center' },
],
},
{ type: 'checkbox', id: 'host', label: 'Show the web address', default: true },
],
elements: [
{
type: 'line',
label: 'Line of text',
settings: [
{ type: 'textarea', id: 'content', label: 'Text', dynamic: true },
{ type: 'checkbox', id: 'heading', label: 'Heading' },
],
},
],
defaultElements: [
{ type: 'line', settings: { content: '{{page.title}}', heading: true } },
{ type: 'line', settings: { content: '{{page.description}}' } },
],
};
// The canvas is 1200×630 px. Keep content 60 px from the edges: X crops the image to 2:1.
// Lay out with flexbox: give every div that holds other elements display: 'flex'.
// Style with inline style objects; className and stylesheets are ignored.
export function OgImage({ settings, elements, store, theme }: OgImageProps) {
const { colors, fonts } = theme;
const accent = String(settings.accent) || colors.primary;
const centered = settings.align === 'center';
return (
<div
style={{
width: 1200,
height: 630,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: centered ? 'center' : 'flex-start',
padding: 60,
backgroundColor: colors.background,
color: colors.foreground,
fontFamily: fonts.body,
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: centered ? 'center' : 'flex-start',
}}
>
<div style={{ width: 64, height: 8, borderRadius: 4, backgroundColor: accent }} />
{elements.map((element) => {
const text = String(element.settings.content);
const heading = element.settings.heading === true;
if (text === '') return null;
return (
<div
key={element.id}
style={{
display: 'block',
lineClamp: heading ? 3 : 2,
overflow: 'hidden',
marginTop: 24,
fontFamily: heading ? fonts.heading : fonts.body,
fontSize: heading ? 72 : 32,
fontWeight: heading ? 700 : 400,
lineHeight: heading ? 1.1 : 1.35,
color: heading ? colors.foreground : colors.mutedForeground,
textAlign: centered ? 'center' : 'left',
}}
>
{text}
</div>
);
})}
</div>
{settings.host === true ? (
<div style={{ fontSize: 24, color: colors.mutedForeground }}>{store.host}</div>
) : null}
</div>
);
}What it shows:
metadeclares three settings and one element type. The Customizer builds its inspector and its Add element list from them, and shows the labels as written here.settings.accentis acolor: a hex colour, or an empty text while the merchant has chosen none. The template then uses the theme's primary colour.settings.alignis aselectand holds one of its option values.settings.hostis acheckboxand holdstrueorfalse.elementsis the merchant's list in order, hidden elements left out.element.idis unique on the image and stays the same when the element is moved, so it is thekey.contentis adynamictext: it arrives with its{{…}}values filled in. It is empty when the page has no such value, so the template skips an empty text.defaultElementsis the list an image starts with: the page title as a heading and the page description under it.- The element type is called
line, a name the platform card does not use. So the image starts from thesedefaultElements, and not from the elements a merchant arranged on the card (Element types).
Declare what a merchant can change
meta is a plain object. Annotate it with OgTemplateMeta and the editor checks it as you
type.
| Property | Content |
|---|---|
label | A name for the template. The Customizer names an image after its page kind and does not show it |
settings | The settings of the template: a list of fields, shown in this order |
elements | The element types a merchant can add: each has a type, a label, settings of its own and a limit |
defaultElements | The elements an image starts with: each names a type and may give settings values |
Every property is optional. A template without meta draws from the page data alone, and the
Customizer has nothing to adjust for it.
Fields
A field is one entry of a settings list. Seven types hold a value and two lay the form out:
type | The merchant sees | Value in the props | Own properties |
|---|---|---|---|
text | A one-line text input | A text of at most 600 characters | placeholder, dynamic |
textarea | A text input of several lines | A text of at most 600 characters | placeholder, dynamic |
checkbox | A switch | true or false | |
select | A list to pick one option from | The value of one option | options: a list of { value, label } |
range | A slider | A number from min to max, on a step | min, max, step, unit |
color | A colour picker with a field for the hex value, and a button that clears it | A hex colour (#rgb, #rrggbb, #rrggbbaa) or '' | |
image | An upload or a pick from the media library, shown as a thumbnail with a button that removes it. There is no field to type an address | The picture's URL, or '' | |
header | A heading that starts a group of fields: its label, with its info under it | None | |
paragraph | A line of explanation: its label | None |
Every field takes these properties:
| Property | Content |
|---|---|
type | One of the nine types |
id | The name of the value in props.settings. It starts with a letter, holds letters, digits, _ and -, is at most 48 characters long and is unique within its list. A name every object already has (constructor, toString) is refused. A header and a paragraph take an id by the same rule, and theirs may start with _; one that is missing or does not fit is replaced by a generated id |
label | The name the merchant sees |
info | A sentence of help under the field |
default | The value before the merchant changes anything |
visible_if | Shows the field only while another setting holds a given value |
import type { OgTemplateMeta } from '@poltura/og-image';
export const meta: OgTemplateMeta = {
label: 'Product',
settings: [
{ type: 'header', id: 'look', label: 'Look' },
{ type: 'paragraph', id: 'look_note', label: 'Colors you leave empty follow the theme.' },
{ type: 'color', id: 'accent', label: 'Accent color', default: '#b4532a' },
{
type: 'select',
id: 'layout',
label: 'Layout',
default: 'split',
options: [
{ value: 'split', label: 'Picture beside the text' },
{ value: 'stacked', label: 'Text only' },
],
},
{
type: 'range',
id: 'picture_width',
label: 'Picture width',
min: 320,
max: 560,
step: 20,
unit: 'px',
default: 480,
visible_if: { field: 'layout', value: 'split' },
},
{ type: 'image', id: 'pattern', label: 'Background pattern', info: 'Drawn behind the text.' },
{ type: 'checkbox', id: 'price', label: 'Show the price', default: true },
{
type: 'text',
id: 'eyebrow',
label: 'Line above the title',
placeholder: '{{product.brand}}',
default: '{{product.brand | product.category}}',
dynamic: true,
},
{ type: 'textarea', id: 'note', label: 'Note under the price' },
],
};What each type starts with and accepts:
type | Without a default | A default counts when it is | Notes |
|---|---|---|---|
text, textarea | '' | A text | Cut to 600 characters |
checkbox | false | true | |
select | The first option's value | The value of one of the options | Needs at least one option. An option needs a value; its label is the value when you give none |
range | min | A number | min is 0, step is 1 and max is min + 100 unless you set them. unit is shown beside the number |
color | '' | A hex colour | '' means no colour chosen: fall back to a colour of the theme |
image | '' | Never | Always starts empty |
Show a field only when it applies
visible_if names another setting of the same list and the value, or the values, it must hold:
import type { OgField } from '@poltura/og-image';
export const colorFields: OgField[] = [
{
type: 'select',
id: 'scheme',
label: 'Colors',
default: 'theme',
options: [
{ value: 'theme', label: 'Theme' },
{ value: 'dark', label: 'Dark' },
{ value: 'custom', label: 'Custom' },
],
},
{
type: 'color',
id: 'background',
label: 'Background',
visible_if: { field: 'scheme', value: 'custom' },
},
{
type: 'checkbox',
id: 'border',
label: 'Border',
visible_if: { field: 'scheme', value: ['theme', 'dark'] },
},
];visible_if only hides the field in the form. The setting keeps its value and still arrives in
props.settings, so check the controlling setting in the template as well:
settings.scheme === 'custom' before you use settings.background.
Texts that take values of the page
A text or textarea with dynamic: true may hold values of the page between double braces.
The platform fills them in before the template runs: the template receives the finished text.
In the Customizer such a field has an Insert value button that lists the page's values, so
a merchant never types the braces.
- A value is the path of a text or a number in the page data:
{{page.title}},{{product.price.amount}},{{theme.radius}}. A path that names nothing, anull, a boolean or an object is empty. {{product.brand | product.category}}takes the first value that is not empty.- A value that is empty takes one separator along: the one before it, or the one after it when
none stands before it. A separator is a slash, a vertical bar, a comma, a semicolon, a colon,
a hyphen, a dash, a middle dot or a bullet, with the spaces around it. So
{{product.brand}} / {{product.category}}reads well with either one missing,{{product.category}} / {{product.brand}} / {{store.name}}keeps one slash between the category and the store name on a product without a brand, andBrand: {{product.brand}}becomesBrand. - Runs of whitespace become one space, and the finished text is cut to 600 characters.
- A
default, and a value indefaultElements, may hold values too. The words around the values are the same on every language of the store, like the words a merchant types. - Without
dynamic: truethe braces are ordinary characters and are drawn as written.
A finished text can be empty. Draw nothing for it, as the own layout above does.
Element types
elements declares what a merchant can add to the image and arrange. Use it for the parts of
a layout whose number and order are the merchant's choice. A part that is always there is
better drawn by the template, with settings in meta.settings.
import type { OgTemplateMeta } from '@poltura/og-image';
export const meta: OgTemplateMeta = {
elements: [
{
type: 'line',
label: 'Line of text',
settings: [
{ type: 'text', id: 'content', label: 'Text', dynamic: true },
{ type: 'checkbox', id: 'strong', label: 'Bold' },
],
},
{ type: 'emblem', label: 'Logo', limit: 1 },
],
defaultElements: [
{ type: 'emblem' },
{ type: 'line', settings: { content: '{{page.title}}', strong: true } },
{ type: 'line', settings: { content: '{{page.description}}' } },
],
};| Property | Content |
|---|---|
type | The name of the element type, by the rules of an id. element.type in the template |
label | The name the merchant sees |
settings | The fields of one element, as in meta.settings. An element type may have none |
limit | How many elements of this type an image may hold. Elements beyond it are left out. Without it, the only bound is the 24 elements of an image |
defaultElements lists the elements of an image nobody has arranged yet, in order. An entry
names a declared type and may give values for that type's settings; a setting it does not
name starts with its default. An entry whose type is not declared is left out and reported.
Choose the names of your element types with the platform card in mind. A stored design is
read by type name, also across a change of template. The card's element types are text,
price, badge, accent, logo and space.
- A type of yours with one of these names takes over the elements a merchant arranged on the
card. They are read against your fields: a card
textarrives with the settings of yours that share anidwith the card's (content, for one), and the rest at their defaults. YourdefaultElementsare then not used. - A name of your own (
line,emblem) starts the image from yourdefaultElements: a stored list in which no element has a type you declare was arranged for another template. - A list the merchant emptied stays empty under every template.
The same holds for the id of a setting: the own layout above calls its settings accent and
align as the card does, so a merchant's accent colour and alignment carry over, and gives its
element type a name of its own.
Read the design in the template
OgImage receives the page data and the design in one object:
| Prop | Type | Content |
|---|---|---|
settings | { [id]: string | number | boolean } | One entry for every value field of meta.settings |
elements | A read-only list of { id, type, settings } | The elements to draw, in order |
kind, locale, page, store, theme, product, collection, article | See Page data | The page the image is for |
settings is always complete and every value is one its field can hold. The template never
has to check for a missing setting or an impossible value. For each value field of
meta.settings, in this order:
- The merchant's stored value is used when the field can hold it.
- Otherwise the field's default is used.
What a field can hold:
type | A stored value is used when it is | Otherwise |
|---|---|---|
text, textarea | A text of at most 2,048 characters. It is cut to 600 | The default |
checkbox | true or false | The default |
select | The value of an option the field has now | The default |
range | A number. It is moved to the nearest step and into min to max | The default |
color | A hex colour, or '' | The default |
image | A text of at most 2,048 characters | '' |
- A
dynamictext is filled in after this, so the template receives the finished text. - A
headerand aparagraphhold no value and have no entry. - A stored value whose setting the template does not declare is not passed on. Rename an
idand the merchant's value for the old name is ignored.
elements is the list to draw, in order.
- Until the merchant changes an element of the kind, it is
defaultElements. From the first change on it is the merchant's own list, and an empty list means no element at all. - Hidden elements are left out. So are elements whose
typethe template does not declare, elements beyond their type'slimit, and everything after the 24th. - A stored list that holds elements, none of them of a type the template declares, was made
for another template. The template then gets its
defaultElements. element.settingsis complete and valid by the same rules assettings, for the fields of the element's type.element.idis unique on the image and stays with the element when it is moved.
The values are typed string | number | boolean, because the type of a setting is declared in
meta, not in TypeScript. Narrow each value where you read it:
import type { OgImageProps, OgTemplateMeta } from '@poltura/og-image';
export const meta: OgTemplateMeta = {
label: 'Page',
settings: [
{ type: 'color', id: 'accent', label: 'Accent color' },
{ type: 'range', id: 'bar', label: 'Bar width', min: 8, max: 48, step: 4, default: 16 },
{
type: 'select',
id: 'direction',
label: 'Lines',
options: [
{ value: 'column', label: 'Stacked' },
{ value: 'row', label: 'Side by side' },
],
},
{ type: 'checkbox', id: 'bold', label: 'Bold', default: true },
],
elements: [
{
type: 'line',
label: 'Line',
settings: [{ type: 'text', id: 'content', label: 'Text', dynamic: true }],
},
],
defaultElements: [
{ type: 'line', settings: { content: '{{page.title}}' } },
{ type: 'line', settings: { content: '{{store.name}}' } },
],
};
export function OgImage({ settings, elements, theme }: OgImageProps) {
// color: a hex colour, or '' while none is chosen
const accent = String(settings.accent) || theme.colors.primary;
// range: a number
const bar = Number(settings.bar);
// select: the value of one of its options
const row = settings.direction === 'row';
// checkbox: true or false
const bold = settings.bold === true;
return (
<div
style={{
width: 1200,
height: 630,
display: 'flex',
flexDirection: row ? 'row' : 'column',
justifyContent: 'center',
alignItems: row ? 'center' : 'flex-start',
gap: 24,
padding: 60,
borderLeft: `${bar}px solid ${accent}`,
backgroundColor: theme.colors.background,
color: theme.colors.foreground,
fontFamily: theme.fonts.heading,
fontSize: 56,
fontWeight: bold ? 700 : 400,
}}
>
{elements.map((element) => (
<div key={element.id}>{String(element.settings.content)}</div>
))}
</div>
);
}When meta has a mistake
The platform reads meta leniently. What is well formed is kept; a field, an element type or a
default element that is not is left out, and each one is reported in a line that names it:
| Line | Cause |
|---|---|
meta: must be an object | meta is not a plain object |
settings[2]: "type" must be one of header, paragraph, text, … | An unknown field type |
settings[3]: "id" must start with a letter and hold letters, digits, "_" or "-" | A missing, malformed or refused id, or one longer than 48 characters |
settings[4]: the id "accent" is used twice | Two fields of one list share an id |
settings[5]: a select needs "options", each with a "value" | A select without a usable option |
settings: holds more than 40 settings | The fields after the 40th are left out |
elements[1]: needs a "type" of letters, digits, "_" or "-" | An element type without a usable name |
elements[2]: the type "text" is declared twice | Two element types share a name |
elements: holds more than 16 element types | The element types after the 16th are left out |
defaultElements[0]: "title" is not a declared element type | A default element of a type elements does not declare |
- The Customizer lists these lines at the foot of the kind's inspector, under
For the template’s developer. At most 50 mistakes are named for one
meta; what is wrong beyond them goes unnamed until the first ones are fixed. - The image is still drawn. A setting that was left out has no entry in
props.settings, so fix the line instead of working around it. - A template that cannot be loaded at all (it does not compile, it throws while it loads, or loading takes longer than 2 seconds) has no settings to show. The inspector then says The settings of this template could not be read. with the error, and Open at line N opens the code editor at the line the error names.
- One mistake never takes the rest down: the other settings and elements keep working.
- The editor's type-check finds most of them first when
metais annotated withOgTemplateMeta. It cannot see a duplicateid, a malformedid, an emptyoptionslist or a default element of an undeclared type.
Limits of a design
| Limit | Value |
|---|---|
Fields in meta.settings, and in one element type | 40 settings |
Element types in meta.elements | 16 element types |
| Elements on one image | 24 elements |
Options of one select, values of one visible_if | 24 options |
An id, an element type, a unit | 48 characters |
A label, a placeholder, an option's value or label | 120 characters |
An info | 300 characters |
| A text value | 600 characters, before and after its {{…}} are filled |
| Any stored value: a text, a colour, the URL of a picture | 2,048 characters |
| Pictures of the media library in one design | 8 pictures, beside the page's own |
Mistakes named for one meta | 50 lines |
A label, an info, a placeholder and a unit longer than the limit are cut. An option with
a longer value, the options after the 24th and the default elements after the 24th are left
out without a report.
Change or remove a template
The design of a kind is stored per kind, not per template. When the template of a kind changes, or the file is removed, the stored design is read against what is declared now:
- A stored value applies wherever a setting with the same
idcan hold it. Any other value is ignored. - An element stays when its
typeis still declared. Any other element is left out. When no stored element has a declared type, the template'sdefaultElementsare used; a list the merchant emptied stays empty. - Removing
og/<kind>.tsxreturns the kind to the platform card. The card reads the stored design by the same rules: it keeps the elements whose type names it shares with your template and starts from its own when there are none. Reset design in the Customizer drops what is left of a design.
So keep an id and a type stable once merchants use the template, and pick a new id when a
setting changes its meaning.
Page data
Every text is plain text in the page's language and already cut to a sensible length (titles
to 160 characters, descriptions to 300, short labels such as a brand or a category to 80). A
missing text is an empty string, never null. A missing image is null.
| Prop | Type | Content |
|---|---|---|
kind | string | default, product, collection, article or page |
locale | string | Language of the page, for example en or hu |
page.title | string | Title of the page. On default it is the store name |
page.description | string | Description of the page. Empty when the page has none |
page.url | string | Address of the page, as the storefront links it |
store.name | string | Store name |
store.tagline | string | Store tagline. Empty when the store has none |
store.host | string | Domain of the store, for example shop.example |
store.logo | string or null | Logo image URL. null when the store has no logo |
product.title | string | Product name |
product.description | string | Product description. Empty when the product has none |
product.image | string or null | Main product image URL. null when the product has no image |
product.price.amount | string | What a shopper pays in the store's default region, formatted for display (€89.00, 34 990 Ft): tax is added where the region stores prices without it. product.price is null without a price |
product.price.compareAt | string | Original price, formatted. Empty when the product is not discounted |
product.price.currency | string | Currency code, for example EUR |
product.brand | string | Brand. Empty when the product has none |
product.category | string | Category name. Empty when the product is in none |
product.available | boolean | false when the product is sold out |
product.ribbon | string | "Pre-order" or "Coming soon" in the page's language. Empty for any other product |
collection.title | string | Category name |
collection.description | string | Category description. Empty when the category has none |
collection.image | string or null | Category image URL. null when the category has no image |
article.title | string | Post title |
article.excerpt | string | Post excerpt. Empty when the post has none |
article.image | string or null | Cover image URL. null when the post has no cover |
article.category | string | Blog category. Empty when the post is in none |
article.publishedAt | string | Publication date in the store's time zone, written out in the page's language (5 October 2026, 2026. október 5.). Empty when the post has none |
theme.colors.* | string | Colours of the theme, as hex or rgb() values: see the list below |
theme.fonts.heading | string | Font family for headings |
theme.fonts.body | string | Font family for body text |
theme.radius | number | Corner radius of the theme, in pixels |
product, collection and article are objects that exist only on their own kind. product
is null on every kind but product, collection is null on every kind but collection,
and article is null on every kind but article. Read them with ?. and fall back to
page (product?.title || page.title), so the same file also draws a sensible image if the
data is missing.
The ten colours are theme.colors.background, theme.colors.foreground,
theme.colors.primary, theme.colors.primaryForeground, theme.colors.muted,
theme.colors.mutedForeground, theme.colors.accent, theme.colors.accentForeground,
theme.colors.card and theme.colors.border. They follow the theme settings, so a merchant
who changes the brand colour gets new images without touching the template.
The texts and numbers of this table are the values a dynamic text can hold between double
braces.
Layout and styling
The image is not drawn by a browser. It supports a subset of HTML and CSS:
- Lay out with flexbox. An element is a flex container unless its style says otherwise.
Position things with
display: 'flex',flexDirection,justifyContent,alignItems,gap,flexGrowand fixed sizes. The default direction is a row, so addflexDirection: 'column'to stack.display: 'grid'works too; tables do not, and fail the render. - Give a
divthat holds other elementsdisplay: 'flex'. Adivwith several children and nodisplayis set as running text, the way a browser sets a paragraph: its texts and inline elements follow each other on lines. Use that for a sentence with a bold word in it, not for layout. - Inline
styleobjects only.classNameis ignored and there are no stylesheets. Write numbers for pixels (fontSize: 48) and strings for everything else ('1px solid #e2dcd3'). - Size the root. Give the root element
width: 1200andheight: 630. - A size holds the padding and the border. A root of
width: 1200,height: 630andpadding: 60is still 1200 by 630. SetboxSizing: 'content-box'on an element to size its content instead. - Later elements are painted over earlier ones.
position: 'absolute'places an element from the edges of its parent, andzIndexchanges the order. - No
var(). A style that usesvar()is refused.calc()works; 3D transforms are drawn flat.
Elements
| Group | Elements |
|---|---|
| Layout and text | div, span, p, h1 to h6, b, strong, i, em, u, s, small, br |
| Images | img, svg |
Inside an svg | g, path, circle, ellipse, rect, line, polyline, polygon, defs, linearGradient, radialGradient, stop, clipPath, mask, title |
Any other element fails the render (<a> is not an allowed element). Event handlers, ref,
href and dangerouslySetInnerHTML are refused on every element. Fragments, arrays (a
.map() with keys) and your own function components are fine: they are resolved before the
image is drawn.
Text
- Text wraps inside its box. To cut it after a number of lines, use
display: 'block',lineClamp: 2andoverflow: 'hidden'on adivthat holds only the text. The cut ends with an ellipsis. - To cut by characters instead (an eyebrow, a badge), use
clampText(text, 48). - Set
fontFamily,fontSize,fontWeight,lineHeightandletterSpacingexplicitly on the root or on the text. There are no browser defaults for headings: anh1is not bigger than adivunless you say so.
Fonts
Use theme.fonts.heading and theme.fonts.body. They are always families the renderer has.
- Eight families are available: Inter, Poppins, Roboto, Open Sans, Lato, Montserrat, Playfair Display and Merriweather.
- Each comes in two weights,
400and700, upright only. Any other weight is drawn with one of the two. - Any other family name is drawn with Inter.
- Latin, Latin Extended (Hungarian, Polish, Czech and other European languages), Cyrillic and Greek are covered where the family has them. Emoji and Chinese, Japanese or Korean text are not drawn: a character no font has becomes an empty box.
Colours
Use theme.colors so the image follows the store's brand. Hex, rgb(), rgba() and named
colours work in any colour property. Linear and radial gradients work in backgroundImage.
The image is a JPEG: it has no transparency, and a transparent area comes out white.
A color setting holds a hex colour or ''. mixHex, inkOn and contrastOf work on hex
colours: a muted tone is mixHex(text, background, 0.4), inkOn(accent) is the text colour
for a filled badge, and contrastOf(accent, background) tells whether an accent can be told
from its background (the platform card asks for at least 1.6). A theme colour may be an
rgb() value. Given a colour that is not hex, mixHex returns its first argument unchanged,
inkOn returns white and contrastOf returns 1.
Images
const logo = store.logo ? (
<img src={store.logo} width={240} height={56} alt='' style={{ objectFit: 'contain' }} />
) : null;
const pattern = String(settings.pattern);
const backdrop = pattern ? (
<img
src={pattern}
width={1200}
height={630}
alt=''
style={{ position: 'absolute', top: 0, left: 0, objectFit: 'cover' }}
/>
) : null;- Always give an
imgawidthand aheight.objectFit: 'contain'or'cover'decides how the picture fills that box. - Only three kinds of image source are drawn: an image URL that came in the page data
(
store.logo,product.image,collection.image,article.image), the picture of animagesetting, and a small inlinedata:image (at most 512 KB as text). The platform reads the first two from the store's own media. Nothing is ever fetched from the network. - Any other URL is dropped: the
imgleaves an empty box of the same size, and abackgroundImage: 'url(…)'layer is removed. The rest of the image is still drawn. - Check for
nullbefore you draw an image of the page, and for''before you draw animagesetting, and give the layout a version without it. The page's image isnullwhen the page has none, and also when the image it has cannot be shown (the file is not an image, or it is larger than an upload may be). Animagesetting is''while no picture is chosen, and also when the chosen one cannot be shown. Your template then draws its layout without the image instead of a frame around nothing. - A design can use eight pictures of the media library beside the page's own. A ninth arrives
as
''.
Inline SVG
An svg element with shapes is the way to draw an icon or a decoration. Use the elements
listed above. text, image, use and foreignObject are not available, and a url(…)
inside an svg may only point at an element of the same svg (fill='url(#fade)').
Limits
| Limit | Value |
|---|---|
| Canvas | 1200×630 pixels |
| Safe margin | 60 pixels on every side. X crops the image to 2:1 and rounds the corners |
| Output | JPEG, at most about 300 KB. The quality is lowered step by step until it fits |
| Elements and text nodes | 2,000 |
| Nesting | 48 levels |
| Text | 20,000 characters in total |
| One attribute or style | 64,000 characters |
| Style properties | 96 on one element |
Inline data: image | 512 KB each |
| Pictures | 32 uses in one image; every <img> and every url() layer counts |
| Template file | The size limit of any theme file, 1 MB by default |
| Render time | 2 seconds, in the Customizer preview and on the live store alike |
| Drawing time | 4 seconds for the finished layout; a picture that takes over 3 seconds to read is left out |
A photograph that fills the whole canvas is what usually approaches the 300 KB limit. Flat colours and text stay far below it.
The limits of meta and of a design are under Limits of a design.
What a template cannot do
Your template runs once per image, as a plain function call. It is not mounted as a React app.
- No network. A
fetchcall is there, and it rejects. Everything the image needs is in the props. - No hooks.
useState,useEffect,useMemoand context throw. Compute values in the function body instead. - No async. A component that returns a promise is refused.
- No other imports. Only
reactand@poltura/og-image. Storefront code cannot import a template either. - Do not draw the time or a random value. Nothing stops
Date.now()orMath.random(), but the platform draws an image once and serves the stored copy until the page's data, the design or the template changes. A date or a random value is frozen into that copy.
When something is wrong
- In the code editor the type-check marks a prop that does not exist, a
metathat does not fitOgTemplateMetaand an import that is not allowed, as you type. - On save an import that is not allowed is refused. A template that does not compile or
has no
OgImageexport is saved all the same, like any half-written draft file, and the Customizer marks the kind Cannot be published. - In the Customizer a strip under the image shows the error of a template that cannot be
drawn, with its line, and Open at line N opens the code editor there. The stage keeps the
last image that worked. A template that takes longer than 2 seconds to draw is an error there
too. Mistakes in
metaare listed in the inspector (Whenmetahas a mistake). - On publish a template that does not compile, imports something it may not or has no
OgImageexport is refused, and the message names the file. - On the live store a template that fails while it runs (it throws, returns an element that is not allowed, or draws something that cannot be drawn) never breaks the page. The platform draws the platform card for that page instead and keeps that image, with the error. The card is drawn as it starts out: the design made for your template does not apply to it. Your failing template is not run again for that page until the template, the design or the page's data changes.
- A design can fail too. A design of the platform card that cannot be drawn, for example because its pictures are too large to draw together, shows its error under the image in the Customizer. On the live store the page gets the platform card as it starts out, kept with the error, until the design or the page's data changes.
- When an image cannot be drawn for a moment, the page shows the image it had before, or the platform card as it starts out. Nothing is kept, and a later request tries your template again.
Check the result
-
Customizer. The stage under Share images draws the draft's template and the unsaved design with a real product, category, post or page, or with sample data when the store has none yet. Pick the page under Preview with. Safe area shades what X crops.
-
The page head. After publishing, open a page of the store and look at its
og:imagetag:curl -s https://shop.example/products/linen-throw | grep -o '<meta property="og:image[^>]*>'The address has the form
https://shop.example/og/product/<id>.jpg?l=en&v=<version>. Open it in a browser to see the image a crawler gets. -
The networks. A network keeps the image it fetched for a link it has already seen. The address changes when the template, the design or the page's data does, so a new share picks up the new image. To refresh an old link, use the network's own tool (the Facebook Sharing Debugger, the LinkedIn Post Inspector).
Next
- Theme assets and media: ship pictures and fonts with the theme.
- Theme SDK reference: every module a theme imports.