Skip to content

shevky/plugin-open-graph

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Shevky Plugin: Open Graph

Open Graph, Twitter Card, and JSON-LD metadata generation for Shevky.

Features

  • Fills page.og, page.twitter, and page.structuredData
  • Supports schema-driven JSON-LD with schemaType
  • Runs in both hooks: content:ready and page:meta
  • Produces graph-based structured data for home/about/contact pages
  • Supports paginated home listing JSON-LD (ItemList)
  • Supports dedicated schemaType: job-listing pages (separate builder)

Installation

npm i @shevky/plugin-open-graph

Usage

Add the plugin in site.json:

{
  "plugins": ["@shevky/plugin-open-graph"],
  "pluginConfigs": {
    "shevky-open-graph": {
      "twitterSite": "@fatihtatoglu",
      "defaultTwitterCard": "summary_large_image",
      "defaultImage": "/assets/images/default-cover.webp",
      "siteName": "Fatih Tatoğlu",
      "publisherType": "person",
      "faqItemsPath": "faq.accordion.items",
      "newsItemsPath": "press.accordion.items",
      "questionCount": 20,
      "force": false,
      "exposePageMeta": true,
      "enableSearchAction": null,
      "searchActionTarget": "/?q={search_term_string}",
      "searchActionQueryInput": "required name=search_term_string"
    }
  }
}

Mustache Example

<!-- Open Graph -->
<meta property="og:type" content="{{page.og.type}}">
<meta property="og:site_name" content="{{site.title}}">
<meta property="og:title" content="{{page.og.title}}">
<meta property="og:description" content="{{page.og.description}}">
<meta property="og:image" content="{{{page.og.image}}}">
<meta property="og:image:secure_url" content="{{{page.og.image}}}">
<meta property="og:url" content="{{{page.og.url}}}">
<meta property="og:locale" content="{{page.og.locale}}">
{{#page.og.altLocale}}
<meta property="og:locale:alternate" content="{{.}}">
{{/page.og.altLocale}}

<!-- Twitter -->
<meta name="twitter:card" content="{{page.twitter.card}}">
<meta name="twitter:title" content="{{page.twitter.title}}">
<meta name="twitter:description" content="{{page.twitter.description}}">
<meta name="twitter:image" content="{{{page.twitter.image}}}">
<meta name="twitter:url" content="{{{page.twitter.url}}}">
{{#page.twitter.site}}
<meta name="twitter:site" content="{{.}}">
{{/page.twitter.site}}

{{#page.structuredData}}
<script type="application/ld+json">
  {{{page.structuredData}}}
</script>
{{/page.structuredData}}

schemaType

Use explicit schemaType in front matter:

  • post -> graph: WebSite + Person + WebPage + BlogPosting (+ ImageObject when present)
  • job-post -> graph: WebSite + publisher entity + WebPage + JobPosting (job detail)
  • job-listing -> graph: WebSite + publisher entity + WebPage/CollectionPage + ItemList
  • home -> graph: WebSite + (Person or Organization) + WebPage/CollectionPage/HomePage
  • collection + collectionType: home -> paginated home graph
  • about -> graph: WebSite + (Person or Organization) + AboutPage
  • contact -> graph: WebSite + (Person or Organization) + ContactPage
  • press -> graph: WebSite + (Person or Organization) + WebPage/CollectionPage (+ ItemList when entries exist)
  • help -> graph: WebSite + (Person or Organization) + WebPage (or FAQPage when FAQ entries exist)
  • collection -> graph: WebSite + Person + WebPage/CollectionPage (+ mainEntity: ItemList when entries exist)
  • page -> graph: WebSite + publisher entity + WebPage
  • policy -> graph: WebSite + Person + WebPage (policy-aware genre)

Post extras (for schemaType: post)

  • wordCount is included only when wordCount exists in front matter
  • categoryUrl + categoryLabel add:
    • WebPage.breadcrumb (Home -> Category -> Post)
    • BlogPosting.about reference to category webpage (<categoryUrl>#webpage)
  • ImageObject.width and ImageObject.height are included when one of these exists:
    • primaryImageWidth / primaryImageHeight
    • ogImageWidth / ogImageHeight
    • coverWidth / coverHeight

Job extras (detail + listing)

  • Job detail pages emit JobPosting with:
    • title, description, datePosted, url
    • optional validThrough (or fallback from jobValidThroughDays)
    • optional employmentType, identifier, directApply, baseSalary
    • optional hiringOrganization and jobLocation
  • Listing pages (home, collectionType: home, job-listing) emit ItemList entries
    • list item @type becomes JobPosting for detected job entries
  • Type switch is resolved from listed page data (front matter / derived front matter / header.raw)
  • employmentType is read directly from front matter:
    • first: schemaEmploymentType / employmentTypeSchema / employmentSchema / jobEmploymentType
    • then: employmentType / employment / workType / jobType / contractType / positionType
    • numeric values are supported and can be resolved from i18n key: seo.home.jobs.employmentTypes.<value>
    • if missing: "-"
  • jobLocation.address.addressCountry comes from i18n map
    • if country is unknown/unmapped, addressCountry is omitted from JSON-LD
  • Country names are normalized to ISO alpha-2 when possible (e.g. Türkiye -> TR, Dubai -> AE, England -> GB)
  • Country code mapping comes from i18n key seo.jobs.countryCodeMap
  • Job detection tokens and locality country-strip tokens are configurable from plugin config:
    • jobCategoryTokens
    • jobUrlTokens
    • localityCountryTokens
  • Example:
    • "seo": { "jobs": { "countryCodeMap": { "dubai": "AE", "türkiye": "TR", "england": "GB" } } }
  • jobLocation.address.addressLocality is normalized to city-only value (e.g. Istanbul, Turkiye -> Istanbul)
  • Optional identifier is emitted as PropertyValue (from jobId / jobIdentifier / identifier / id / slug / URL slug)
  • Optional hiringOrganization.url and hiringOrganization.sameAs are emitted when present
  • jobLocation.address.streetAddress can be sourced from addressText (front matter shortcut)
  • resolveListingPageName and resolveHomeItemListName are i18n-driven; fallback is always -
  • Suggested keys: seo.home.page.name, seo.home.page.pageName, seo.home.jobs.itemList.name, seo.home.jobs.itemList.pageName, seo.home.itemList.name, seo.home.itemList.pageName

Example front matter for schemaType: job-post:

schemaType: job-post
description: "Kısa ama net ilan açıklaması"
jobLocation:
  address:
    addressLocality: "İstanbul"
    addressRegion: "İstanbul"
    addressCountry: "TR"
    addressText: "Levent, Büyükdere Cad. No: 123"

Help / FAQ extras (for schemaType: help, schemaType: faq, /help/, /faq/)

  • Output is @graph (WebSite + publisher entity + page node)
  • Page node uses name (headline is not used) and does not include mainEntityOfPage
  • If FAQ entries are found, page type becomes ["WebPage", "FAQPage"] and mainEntity is emitted as Question[]
  • FAQ source path can be configured from plugin config:
    • faqItemsPath: single dot-path (example: faq.accordion.items)
    • faqItemsPaths: multiple dot-paths (example: ["faq.accordion.items", "blocks.faq.items"])
  • questionCount limits mainEntity count only for schemaType: help (faq type always lists all questions)
  • FAQ sources (front matter / derived front matter):
    • faqs, faq, questions, qna, helpFaqs
    • each item supports question|q|name|title and answer|a|text|content|description|acceptedAnswer.text

Press extras (for schemaType: press)

  • Output is @graph (WebSite + publisher entity + ["WebPage","CollectionPage"])
  • Page node uses name and does not include mainEntityOfPage
  • Press entries are read from front matter / derived front matter sources:
    • configurable path: newsItemsPath / newsItemsPaths (example: press.accordion.items)
    • pressItems, pressList, press, mediaMentions, mediaCoverage, news
  • If valid entries are found, mainEntity is emitted as ItemList with ListItem -> NewsArticle items
  • Entries pointing to the press page itself / site home are ignored; if no valid press links remain, mainEntity is omitted
  • Item fields:
    • headline (from headline|title|name)
    • url (from url|link|href|canonical)
    • optional datePublished (from datePublished|publishedAt|published|publishDate|date)
    • optional publisher name (from publisherName|publisher.name|publisher|sourceName|source)
  • ItemList name prefers i18n keys: seo.press.itemList.name, seo.press.itemList.title

Collection i18n keys

Recommended grouped structure:

  • seo.collections.tag.description
  • seo.collections.tag.itemList.name
  • seo.collections.category.description
  • seo.collections.category.itemList.name
  • seo.collections.series.description
  • seo.collections.series.itemList.name
  • seo.collections.default.itemList.name (optional fallback)

Templates can include {{label}}.

Policy Type and i18n (for schemaType: policy)

You can classify policy pages in two ways:

  • Explicit: set policyType (recommended)
  • Automatic: configure keyword hints in i18n and let plugin infer from title/slug/category

Supported i18n keys:

  • seo.policy.hints -> object map (<type> -> list of match tokens)
  • seo.policy.genre.<type> -> genre array for that type
  • seo.policy.genre.default -> fallback genre

Example:

{
  "seo": {
    "policy": {
      "hints": {
        "cookie": {
          "tr": ["çerez", "cookies"],
          "en": ["cookie", "cookies"]
        },
        "terms": {
          "tr": ["kullanım koşulları", "kullanim kosullari"],
          "en": ["terms of use", "terms"]
        },
        "disclaimer": {
          "tr": ["sorumluluk reddi"],
          "en": ["disclaimer"]
        }
      },
      "genre": {
        "cookie": {
          "tr": ["Çerez Politikası", "Hukuki"],
          "en": ["Cookie Policy", "Legal"]
        },
        "terms": {
          "tr": ["Kullanım Koşulları", "Hukuki"],
          "en": ["Terms of Use", "Legal"]
        },
        "disclaimer": {
          "tr": ["Sorumluluk Reddi", "Hukuki"],
          "en": ["Disclaimer", "Legal"]
        },
        "default": {
          "tr": ["Politika", "Hukuki"],
          "en": ["Policy", "Legal"]
        }
      }
    }
  }
}

Example:

---
lang: en
title: NVIDIA Jetson Orin Nano Setup
slug: nvidia-jetson-orin-nano-setup
schemaType: post
template: post
cover: /assets/images/jetson-orin-nano-super-developer-kit.webp
description: Step-by-step setup guide.
date: 2026-01-14
updated: 2026-01-15
keywords:
  - jetson
  - jetpack
---

Config Options

  • twitterSite: X/Twitter handle for twitter:site
  • defaultTwitterCard: default card type (summary_large_image)
  • defaultImage: fallback image when page has no cover
  • siteName: optional override for og:site_name
  • publisherType: person (default) or organization
  • jobCategoryTokens: keyword list for category/type based job detection
  • jobUrlTokens: keyword list for URL/slug based job detection
  • faqItemsPath / faqItemsPaths: dot-path to FAQ item list in front matter (faq.accordion.items)
  • newsItemsPath / newsItemsPaths: dot-path to press/news item list in front matter (press.accordion.items)
  • questionCount: max FAQ item count for schemaType: help (default -1, no limit)
  • jobValidThroughDays: fallback day count for JobPosting.validThrough when no explicit end date (default 60)
  • localityCountryTokens: tokens removed from addressLocality to keep city-only value
  • force: overwrite existing front matter meta fields
  • exposePageMeta: expose computed object as page.pageMeta
  • enableSearchAction: force enable/disable SearchAction on home graph
  • searchActionTarget: URL template for search action
  • searchActionQueryInput: query-input string for SearchAction

For publisherType: organization, source fields are fixed:

  • Organization.name <- identity.author
  • Organization.url <- identity.url
  • Organization.logo <- identity.profileImage
  • Organization.sameAs <- identity.social.*

Notes

  • Core should stay focused on markdown-to-html. This plugin handles SEO meta generation.
  • If plugin is not used, only front-matter-driven meta fields are available.

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published