# Share images (Open Graph) (/themes/og-images)

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 [#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 `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 [#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 [#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 [#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-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 [#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-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 [#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 [#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 [#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 [#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](#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 [#start-from-the-platform-card]

This is the platform's own product template. For another kind only the name in
`ogCardMeta('product')` differs.

```tsx
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`:

```tsx
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 [#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`.

```tsx
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](#element-types)).

### Declare what a merchant can change [#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 [#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                                                                                                                                                                                                                                                                                                                                            |

```tsx
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 [#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:

```tsx
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 [#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-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 [#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`.

```tsx
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 `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 [#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](#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:

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:

| `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 `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:

```tsx
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 [#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 `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 [#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 [#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 [#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 [#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 [#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 `key`s) and your own function components are fine: they are resolved before the
image is drawn.

### Text [#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 [#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 [#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 [#images]

```tsx
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 [#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 [#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](#limits-of-a-design).

## What a template cannot do [#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 [#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](#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 [#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:

   ```bash
   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 [#next]

- [Theme assets and media](/themes/theme-assets): ship pictures and fonts with the theme.
- [Theme SDK reference](/themes/sdk): every module a theme imports.
