Developers
Start building
Themes/Share images (Open Graph)

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:

WhoWhereWhat
MerchantCustomizer, Share imagesThe design of a page kind: the elements on the image, their order and texts, and the values of the template's settings
DeveloperCode editor, og/<kind>.tsxThe 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.

FilePages
og/default.tsxThe store card: the home page and every page without its own
og/product.tsxProduct pages
og/collection.tsxCategory pages
og/article.tsxBlog posts
og/page.tsxContent 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 default too.
  • Only these five names are drawn. og/ takes .tsx files 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:

ElementDrawsSettings
TextA text: a value of the page, words of your own, or bothText; Style (Heading, Body, Label); Size (Automatic, Small, Medium, Large, Extra large); Color (Text, Muted, Accent); Lines at most (1 to 6)
PriceThe product's price. Product pages only, and one per imageShow the original price, struck through beside the price when the product is discounted; Size (Small, Medium, Large)
BadgeA short text in a pill, on one lineText; Look (Accent, Soft, Outline)
Accent lineA short bar in the accent colour, with the text or along a whole side of the imagePlacement (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)
LogoThe store logo, or the store name when the store has no logo. One per imageHeight (32 to 120 px)
SpaceEmpty room between two elementsHeight (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:

KindElements, from the top
Store cardAccent line along the right edge, the store name as a heading, the store tagline
ProductAccent line, the brand as a label (the category when the product has no brand), the product name as a heading, the price
CategoryAccent line, the category name as a heading, the category description
Blog postAccent line, the blog category as a label, the post title as a heading, the excerpt
PageAccent 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 writeThe 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.publishedAt and 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:

GroupSettingChoicesStarts as
PicturePositionRight, Left, Behind the text, HiddenRight
FitFill the frame, Show all of itShow all of it on products, Fill the frame on categories and posts
LayoutAlignmentLeft, CenterLeft
Vertical positionTop, Middle, BottomMiddle on the store card, Top on the other kinds
Margin40 to 96 px60 px
ColorsColor schemeTheme, Soft, Inverted, Brand color, CustomTheme
Background, Text, AccentA colour eachEmpty
Background picturePictureA picture you upload or pick from the media libraryNone
Overlay0 to 95 %60 %
FooterShow the footerOn or offOn
StoreLogo, or the name without one, Logo only, Name, NothingLogo only on the store card, Logo, or the name without one elsewhere
Web addressOn or offOn
Publication dateOn or offOn
  • 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

PartRule
OgImageRequired named export. A function that takes OgImageProps and returns exactly one root element.
metaOptional named export, an OgTemplateMeta: what a merchant can change. See Declare what a merchant can change.
Importsreact and @poltura/og-image only. No relative imports, no @poltura/theme-sdk, no other package.
ComponentsYou may declare helper components in the same file. They must be plain functions: no hooks, no classes, no async.

@poltura/og-image gives you:

ExportUse
OgImageProps, OgTemplateMetaThe 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>
  );
}
  • OgCard is 1200×630. It draws the page as props.settings and props.elements say, under the ids ogCardMeta declares. Pass it the props unchanged.
  • An id is unique within its list. The card's setting ids are image, image_fit, align, position, padding, scheme, background, text, accent, background_image, overlay, footer, footer_brand, footer_host and footer_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 meta is exactly ogCardMeta(kind), like the first one above. Once the meta differs in anything (a setting added, an element type relabelled), every label is shown as the meta holds 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:

  • meta declares 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.accent is a color: a hex colour, or an empty text while the merchant has chosen none. The template then uses the theme's primary colour.
  • settings.align is a select and holds one of its option values. settings.host is a checkbox and holds true or false.
  • elements is the merchant's list in order, hidden elements left out. element.id is unique on the image and stays the same when the element is moved, so it is the key.
  • content is a dynamic text: it arrives with its {{…}} values filled in. It is empty when the page has no such value, so the template skips an empty text.
  • defaultElements is 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 these defaultElements, 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.

PropertyContent
labelA name for the template. The Customizer names an image after its page kind and does not show it
settingsThe settings of the template: a list of fields, shown in this order
elementsThe element types a merchant can add: each has a type, a label, settings of its own and a limit
defaultElementsThe 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:

typeThe merchant seesValue in the propsOwn properties
textA one-line text inputA text of at most 600 charactersplaceholder, dynamic
textareaA text input of several linesA text of at most 600 charactersplaceholder, dynamic
checkboxA switchtrue or false
selectA list to pick one option fromThe value of one optionoptions: a list of { value, label }
rangeA sliderA number from min to max, on a stepmin, max, step, unit
colorA colour picker with a field for the hex value, and a button that clears itA hex colour (#rgb, #rrggbb, #rrggbbaa) or ''
imageAn 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 addressThe picture's URL, or ''
headerA heading that starts a group of fields: its label, with its info under itNone
paragraphA line of explanation: its labelNone

Every field takes these properties:

PropertyContent
typeOne of the nine types
idThe 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
labelThe name the merchant sees
infoA sentence of help under the field
defaultThe value before the merchant changes anything
visible_ifShows 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:

typeWithout a defaultA default counts when it isNotes
text, textarea''A textCut to 600 characters
checkboxfalsetrue
selectThe first option's valueThe value of one of the optionsNeeds at least one option. An option needs a value; its label is the value when you give none
rangeminA numbermin 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''NeverAlways 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, a null, 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, and Brand: {{product.brand}} becomes Brand.
  • Runs of whitespace become one space, and the finished text is cut to 600 characters.
  • A default, and a value in defaultElements, 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: true the 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}}' } },
  ],
};
PropertyContent
typeThe name of the element type, by the rules of an id. element.type in the template
labelThe name the merchant sees
settingsThe fields of one element, as in meta.settings. An element type may have none
limitHow 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 text arrives with the settings of yours that share an id with the card's (content, for one), and the rest at their defaults. Your defaultElements are then not used.
  • A name of your own (line, emblem) starts the image from your defaultElements: 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:

PropTypeContent
settings{ [id]: string | number | boolean }One entry for every value field of meta.settings
elementsA read-only list of { id, type, settings }The elements to draw, in order
kind, locale, page, store, theme, product, collection, articleSee Page dataThe 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:

  1. The merchant's stored value is used when the field can hold it.
  2. Otherwise the field's default is used.

What a field can hold:

typeA stored value is used when it isOtherwise
text, textareaA text of at most 2,048 characters. It is cut to 600The default
checkboxtrue or falseThe default
selectThe value of an option the field has nowThe default
rangeA number. It is moved to the nearest step and into min to maxThe default
colorA hex colour, or ''The default
imageA text of at most 2,048 characters''
  • A dynamic text is filled in after this, so the template receives the finished text.
  • A header and a paragraph hold no value and have no entry.
  • A stored value whose setting the template does not declare is not passed on. Rename an id and 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 type the template does not declare, elements beyond their type's limit, 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.settings is complete and valid by the same rules as settings, for the fields of the element's type.
  • element.id is 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:

LineCause
meta: must be an objectmeta 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 twiceTwo 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 settingsThe 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 twiceTwo element types share a name
elements: holds more than 16 element typesThe element types after the 16th are left out
defaultElements[0]: "title" is not a declared element typeA 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 meta is annotated with OgTemplateMeta. It cannot see a duplicate id, a malformed id, an empty options list or a default element of an undeclared type.

Limits of a design

LimitValue
Fields in meta.settings, and in one element type40 settings
Element types in meta.elements16 element types
Elements on one image24 elements
Options of one select, values of one visible_if24 options
An id, an element type, a unit48 characters
A label, a placeholder, an option's value or label120 characters
An info300 characters
A text value600 characters, before and after its {{…}} are filled
Any stored value: a text, a colour, the URL of a picture2,048 characters
Pictures of the media library in one design8 pictures, beside the page's own
Mistakes named for one meta50 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 id can hold it. Any other value is ignored.
  • An element stays when its type is still declared. Any other element is left out. When no stored element has a declared type, the template's defaultElements are used; a list the merchant emptied stays empty.
  • Removing og/<kind>.tsx returns 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.

PropTypeContent
kindstringdefault, product, collection, article or page
localestringLanguage of the page, for example en or hu
page.titlestringTitle of the page. On default it is the store name
page.descriptionstringDescription of the page. Empty when the page has none
page.urlstringAddress of the page, as the storefront links it
store.namestringStore name
store.taglinestringStore tagline. Empty when the store has none
store.hoststringDomain of the store, for example shop.example
store.logostring or nullLogo image URL. null when the store has no logo
product.titlestringProduct name
product.descriptionstringProduct description. Empty when the product has none
product.imagestring or nullMain product image URL. null when the product has no image
product.price.amountstringWhat 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.compareAtstringOriginal price, formatted. Empty when the product is not discounted
product.price.currencystringCurrency code, for example EUR
product.brandstringBrand. Empty when the product has none
product.categorystringCategory name. Empty when the product is in none
product.availablebooleanfalse when the product is sold out
product.ribbonstring"Pre-order" or "Coming soon" in the page's language. Empty for any other product
collection.titlestringCategory name
collection.descriptionstringCategory description. Empty when the category has none
collection.imagestring or nullCategory image URL. null when the category has no image
article.titlestringPost title
article.excerptstringPost excerpt. Empty when the post has none
article.imagestring or nullCover image URL. null when the post has no cover
article.categorystringBlog category. Empty when the post is in none
article.publishedAtstringPublication 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.*stringColours of the theme, as hex or rgb() values: see the list below
theme.fonts.headingstringFont family for headings
theme.fonts.bodystringFont family for body text
theme.radiusnumberCorner 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, flexGrow and fixed sizes. The default direction is a row, so add flexDirection: 'column' to stack. display: 'grid' works too; tables do not, and fail the render.
  • Give a div that holds other elements display: 'flex'. A div with several children and no display is 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 style objects only. className is 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: 1200 and height: 630.
  • A size holds the padding and the border. A root of width: 1200, height: 630 and padding: 60 is still 1200 by 630. Set boxSizing: '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, and zIndex changes the order.
  • No var(). A style that uses var() is refused. calc() works; 3D transforms are drawn flat.

Elements

GroupElements
Layout and textdiv, span, p, h1 to h6, b, strong, i, em, u, s, small, br
Imagesimg, svg
Inside an svgg, 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: 2 and overflow: 'hidden' on a div that 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, lineHeight and letterSpacing explicitly on the root or on the text. There are no browser defaults for headings: an h1 is not bigger than a div unless 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, 400 and 700, 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 img a width and a height. 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 an image setting, and a small inline data: 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 img leaves an empty box of the same size, and a backgroundImage: 'url(…)' layer is removed. The rest of the image is still drawn.
  • Check for null before you draw an image of the page, and for '' before you draw an image setting, and give the layout a version without it. The page's image is null when 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). An image setting 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

LimitValue
Canvas1200×630 pixels
Safe margin60 pixels on every side. X crops the image to 2:1 and rounds the corners
OutputJPEG, at most about 300 KB. The quality is lowered step by step until it fits
Elements and text nodes2,000
Nesting48 levels
Text20,000 characters in total
One attribute or style64,000 characters
Style properties96 on one element
Inline data: image512 KB each
Pictures32 uses in one image; every <img> and every url() layer counts
Template fileThe size limit of any theme file, 1 MB by default
Render time2 seconds, in the Customizer preview and on the live store alike
Drawing time4 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 fetch call is there, and it rejects. Everything the image needs is in the props.
  • No hooks. useState, useEffect, useMemo and context throw. Compute values in the function body instead.
  • No async. A component that returns a promise is refused.
  • No other imports. Only react and @poltura/og-image. Storefront code cannot import a template either.
  • Do not draw the time or a random value. Nothing stops Date.now() or Math.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 meta that does not fit OgTemplateMeta and 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 OgImage export 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 meta are listed in the inspector (When meta has a mistake).
  • On publish a template that does not compile, imports something it may not or has no OgImage export 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

  1. 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.

  2. The page head. After publishing, open a page of the store and look at its og:image tag:

    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.

  3. 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

Last updated