Docs SEO NEO Version 1.x Open Graph & Twitter

Open Graph & Twitter

What SEO NEO emits by default, the og:type=article rules, the four-step Open Graph image resolver, Twitter / X card behaviour, and locale handling.

What SEO NEO emits by default

Open Graph and Twitter card tags are generated automatically from the same data as the core meta tags — you don't need to configure anything separately for most sites. The default output for a page with an image:

<meta property="og:title" content="Page Title">
<meta property="og:description" content="Resolved description.">
<meta property="og:url" content="https://example.com/page/">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Your Site Name">
<meta property="og:image" content="https://example.com/site/assets/files/123/hero.jpg">
<meta property="og:image:width" content="1600">
<meta property="og:image:height" content="900">
<meta property="og:image:secure_url" content="https://example.com/site/assets/files/123/hero.jpg">
<meta property="og:image:type" content="image/jpeg">
<meta property="og:locale" content="en_US">

<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Page Title">
<meta name="twitter:description" content="Resolved description.">
<meta name="twitter:image" content="https://example.com/site/assets/files/123/hero.jpg">

Open Graph tag breakdown

  • og:title — the raw page title, without the separator or site name. Uses the same fallback chain as the <title> tag.
  • og:description — the same resolved description as <meta name="description">.
  • og:url — mirrors the resolved canonical URL, so all three tags stay in sync.
  • og:type — resolves per-page via the seoneo_og_type field (installed by default), then per-template via an og_type= line in per-template defaults, then the site-wide default in module config (website if nothing is set). Hookable via ___getOgType($page). The Default OG type select in module config covers the full set of common Open Graph types: website, article, book, profile, music.song, music.album, music.playlist, music.radio_station, video.movie, video.episode, video.tv_show, video.other, and product.
  • og:site_name — from the Site name setting in module config.
  • og:locale — resolves per language. See Multilingual for the locale map.
  • og:locale:alternate — emitted once per active language other than the current one.

Open Graph image resolver

SEO NEO uses a five-step resolver to find an image for og:image:

  1. Dedicated OG image field — checks seoneo_og_image on the current page first (installed by default with the SEO tab).
  2. Configured image fields — scans the comma-separated list in OG image fields (default: og_image,screenshot,images,image,blog_images). Supports dotted paths: banner.image, gallery.first.
  3. Ancestor walk — if Inherit OG image from closest ancestor is enabled (off by default), walks $page->parents() nearest-first using steps 1 and 2 on each ancestor.
  4. Homepage OG image — if the homepage has seoneo_og_image set, uses that as a site-wide fallback.
  5. Default OG image URL — the fallback image URL from module config. If still empty, the og:image family of tags is omitted.

When the resolved image is a real Pageimage object (i.e. a file in PW's file system, not just a URL string), SEO NEO also emits og:image:width, og:image:height, og:image:secure_url, and og:image:type automatically. Facebook silently rejects images on the first share when dimensions are missing — this is the most common cause of "no image" in link previews.

og:type=article and article tags

When og:type resolves to article, SEO NEO automatically adds three extra tags:

<meta property="article:author" content="Jane Doe">
<meta property="article:published_time" content="2026-05-12T11:49:20+01:00">
<meta property="article:modified_time" content="2026-05-14T15:37:31+01:00">
  • article:author — one tag per author, from the same multi-author resolution as <meta name="author">. Hookable via ___getArticleAuthors($page).
  • article:published_time — defaults to the page's $page->created timestamp as ISO 8601. Hookable via ___getArticlePublishedTime($page).
  • article:modified_time — defaults to $page->modified. Hookable via ___getArticleModifiedTime($page).

These tags are only emitted when og:type=article, so non-article pages stay clean.

To set og:type=article on a template, add this to the per-template defaults in module config:

[blog-post]
og_type=article

Twitter / X cards

Twitter card tags mirror the Open Graph values — they share the same resolved title, description, and image:

  • twitter:card — switches between summary_large_image (when an image resolved) and summary (no image). Automatic.
  • twitter:site — the site's Twitter/X handle from module config. The leading @ is added automatically.
  • twitter:creator — the author's handle from module config. Hook ___getTwitterCreator($page) to return a per-page or per-author value.
  • twitter:title, twitter:description, twitter:image — mirror og:title, og:description, and og:image respectively. Twitter falls back to OG tags by spec, but many scrapers don't, so SEO NEO emits explicit Twitter tags for maximum compatibility.

Locale handling

On single-language sites, og:locale uses the Default OG locale from module config (e.g. en_US).

On multilingual sites, SEO NEO resolves the locale for each active language using a three-step fallback chain:

  1. Locale map — explicit langname=locale entries in Modules > Configure > SeoNeo > Locale map for languages:
    default=en_GB
    de=de_DE
    fi=fi_FI
  2. Auto-derive from language name + country code — if the language has no explicit map entry, SEO NEO derives a locale from the PW language name itself. de becomes de_DE, fr becomes fr_FR, pt becomes pt_PT. Compound names like pt-br map to pt_BR.
  3. Default OG locale — final fallback, used for the default language and as a safety net when neither of the above resolves.

The current language's locale becomes og:locale; all others become og:locale:alternate tags. See Multilingual for full hreflang and locale configuration.

Common gotchas

  • Image not showing in Facebook share preview. The most common cause is missing og:image:width and og:image:height. These are only emitted when the image is a real Pageimage — check the image field is returning a PW file object, not just a URL string. Also confirm the image is at least 200x200px (Facebook's minimum).
  • og:type stays "website" even on blog posts. The type resolves per-page, then per-template. Add blog_post og_type: article to the per-template defaults in module config, or add an seoneo_og_type Text field to the template and set it to article on each page.
  • Twitter card shows "summary" instead of "summary_large_image". No image resolved. Check the OG image fields list in module config and verify the page (or an ancestor) has an image in one of those fields.

See also

  • Configuration — OG image fields, locale map, and Twitter handle settings.
  • Multilingual — locale map, hreflang, and per-language OG locale.
  • Template API — accessing $page->seoneo->ogImage, ->og->render(), and other OG values in templates.

Last updated