Open Graph, Twitter Card, and JSON-LD metadata generation for Shevky.
- Fills
page.og,page.twitter, andpage.structuredData - Supports schema-driven JSON-LD with
schemaType - Runs in both hooks:
content:readyandpage:meta - Produces graph-based structured data for home/about/contact pages
- Supports paginated home listing JSON-LD (
ItemList) - Supports dedicated
schemaType: job-listingpages (separate builder)
npm i @shevky/plugin-open-graphAdd 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"
}
}
}<!-- 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}}Use explicit schemaType in front matter:
post-> graph:WebSite+Person+WebPage+BlogPosting(+ImageObjectwhen present)job-post-> graph:WebSite+ publisher entity +WebPage+JobPosting(job detail)job-listing-> graph:WebSite+ publisher entity +WebPage/CollectionPage+ItemListhome-> graph:WebSite+ (PersonorOrganization) +WebPage/CollectionPage/HomePagecollection+collectionType: home-> paginated home graphabout-> graph:WebSite+ (PersonorOrganization) +AboutPagecontact-> graph:WebSite+ (PersonorOrganization) +ContactPagepress-> graph:WebSite+ (PersonorOrganization) +WebPage/CollectionPage(+ItemListwhen entries exist)help-> graph:WebSite+ (PersonorOrganization) +WebPage(orFAQPagewhen FAQ entries exist)collection-> graph:WebSite+Person+WebPage/CollectionPage(+mainEntity: ItemListwhen entries exist)page-> graph:WebSite+ publisher entity +WebPagepolicy-> graph:WebSite+Person+WebPage(policy-aware genre)
wordCountis included only whenwordCountexists in front mattercategoryUrl+categoryLabeladd:WebPage.breadcrumb(Home -> Category -> Post)BlogPosting.aboutreference to category webpage (<categoryUrl>#webpage)
ImageObject.widthandImageObject.heightare included when one of these exists:primaryImageWidth/primaryImageHeightogImageWidth/ogImageHeightcoverWidth/coverHeight
- Job detail pages emit
JobPostingwith:title,description,datePosted,url- optional
validThrough(or fallback fromjobValidThroughDays) - optional
employmentType,identifier,directApply,baseSalary - optional
hiringOrganizationandjobLocation
- Listing pages (
home,collectionType: home,job-listing) emitItemListentries- list item
@typebecomesJobPostingfor detected job entries
- list item
- Type switch is resolved from listed page data (front matter / derived front matter / header.raw)
employmentTypeis 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:
"-"
- first:
jobLocation.address.addressCountrycomes from i18n map- if country is unknown/unmapped,
addressCountryis omitted from JSON-LD
- if country is unknown/unmapped,
- 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:
jobCategoryTokensjobUrlTokenslocalityCountryTokens
- Example:
"seo": { "jobs": { "countryCodeMap": { "dubai": "AE", "türkiye": "TR", "england": "GB" } } }
jobLocation.address.addressLocalityis normalized to city-only value (e.g.Istanbul, Turkiye->Istanbul)- Optional
identifieris emitted asPropertyValue(fromjobId/jobIdentifier/identifier/id/slug/ URL slug) - Optional
hiringOrganization.urlandhiringOrganization.sameAsare emitted when present jobLocation.address.streetAddresscan be sourced fromaddressText(front matter shortcut)resolveListingPageNameandresolveHomeItemListNameare 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"- Output is
@graph(WebSite+ publisher entity + page node) - Page node uses
name(headlineis not used) and does not includemainEntityOfPage - If FAQ entries are found, page type becomes
["WebPage", "FAQPage"]andmainEntityis emitted asQuestion[] - 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"])
questionCountlimitsmainEntitycount only forschemaType: help(faqtype always lists all questions)- FAQ sources (front matter / derived front matter):
faqs,faq,questions,qna,helpFaqs- each item supports
question|q|name|titleandanswer|a|text|content|description|acceptedAnswer.text
- Output is
@graph(WebSite+ publisher entity +["WebPage","CollectionPage"]) - Page node uses
nameand does not includemainEntityOfPage - 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
- configurable path:
- If valid entries are found,
mainEntityis emitted asItemListwithListItem -> NewsArticleitems - Entries pointing to the press page itself / site home are ignored; if no valid press links remain,
mainEntityis omitted - Item fields:
headline(fromheadline|title|name)url(fromurl|link|href|canonical)- optional
datePublished(fromdatePublished|publishedAt|published|publishDate|date) - optional
publishername (frompublisherName|publisher.name|publisher|sourceName|source)
- ItemList name prefers i18n keys:
seo.press.itemList.name,seo.press.itemList.title
Recommended grouped structure:
seo.collections.tag.descriptionseo.collections.tag.itemList.nameseo.collections.category.descriptionseo.collections.category.itemList.nameseo.collections.series.descriptionseo.collections.series.itemList.nameseo.collections.default.itemList.name(optional fallback)
Templates can include {{label}}.
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>->genrearray for that typeseo.policy.genre.default-> fallbackgenre
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
---twitterSite: X/Twitter handle fortwitter:sitedefaultTwitterCard: default card type (summary_large_image)defaultImage: fallback image when page has no coversiteName: optional override forog:site_namepublisherType:person(default) ororganizationjobCategoryTokens: keyword list for category/type based job detectionjobUrlTokens: keyword list for URL/slug based job detectionfaqItemsPath/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 forschemaType: help(default-1, no limit)jobValidThroughDays: fallback day count forJobPosting.validThroughwhen no explicit end date (default60)localityCountryTokens: tokens removed fromaddressLocalityto keep city-only valueforce: overwrite existing front matter meta fieldsexposePageMeta: expose computed object aspage.pageMetaenableSearchAction: force enable/disableSearchActionon home graphsearchActionTarget: URL template for search actionsearchActionQueryInput: query-input string forSearchAction
For publisherType: organization, source fields are fixed:
Organization.name<-identity.authorOrganization.url<-identity.urlOrganization.logo<-identity.profileImageOrganization.sameAs<-identity.social.*
- 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.
MIT