diff --git a/CHANGELOG.md b/CHANGELOG.md index 4676243..3b6708a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Fediverse/Mastodon `fediverse:creator` meta tag — first extension on any CMS to support this (#57) - Live character count indicators on OG title, OG description, SEO title, meta description fields with color-coded warnings (#58) - LinkedIn social preview card in article/menu editor alongside Facebook and Twitter/X previews (#61) +- `og:video` meta tag support with per-article video URL field, auto-detect MIME type for YouTube/Vimeo/direct files (#59) +- Pinterest rich pin tags: `article:tag` from Joomla content tags, `product:availability` from MokoSuiteShop stock (#60) - Site-wide default OG title and description plugin parameters - Discord embed color via `theme-color` meta tag (color picker in plugin config) - LinkedIn article tags: `article:published_time`, `article:modified_time`, `article:author` diff --git a/source/packages/com_mokoog/forms/tag.xml b/source/packages/com_mokoog/forms/tag.xml index 87d8068..7f87824 100644 --- a/source/packages/com_mokoog/forms/tag.xml +++ b/source/packages/com_mokoog/forms/tag.xml @@ -60,6 +60,14 @@ + Music +
diff --git a/source/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini b/source/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini index 1da4c1e..e5859fa 100644 --- a/source/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini +++ b/source/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini @@ -14,6 +14,9 @@ PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing. Recomme PLG_CONTENT_MOKOOG_FIELD_OG_TYPE="OG Type" PLG_CONTENT_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type for this page." +PLG_CONTENT_MOKOOG_FIELD_OG_VIDEO="Video URL" +PLG_CONTENT_MOKOOG_FIELD_OG_VIDEO_DESC="URL of a video to embed in social sharing previews. Supports direct video URLs and YouTube/Vimeo links. Outputs og:video meta tags." + PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL="SEO Meta Tags" PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC="Control search engine meta tags for this page." diff --git a/source/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini b/source/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini index 1da4c1e..e5859fa 100644 --- a/source/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini +++ b/source/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini @@ -14,6 +14,9 @@ PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing. Recomme PLG_CONTENT_MOKOOG_FIELD_OG_TYPE="OG Type" PLG_CONTENT_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type for this page." +PLG_CONTENT_MOKOOG_FIELD_OG_VIDEO="Video URL" +PLG_CONTENT_MOKOOG_FIELD_OG_VIDEO_DESC="URL of a video to embed in social sharing previews. Supports direct video URLs and YouTube/Vimeo links. Outputs og:video meta tags." + PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL="SEO Meta Tags" PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC="Control search engine meta tags for this page." diff --git a/source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php b/source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php index 7bd1b9b..e7362d9 100644 --- a/source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php +++ b/source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php @@ -194,7 +194,7 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface $db = Factory::getDbo(); $query = $db->getQuery(true) ->select($db->quoteName([ - 'og_title', 'og_description', 'og_image', 'og_type', + 'og_title', 'og_description', 'og_image', 'og_type', 'og_video', 'seo_title', 'meta_description', 'robots', 'canonical_url', ])) ->from($db->quoteName('#__mokoog_tags')) @@ -249,6 +249,7 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface 'og_description' => trim($ogData['og_description'] ?? ''), 'og_image' => trim($ogData['og_image'] ?? ''), 'og_type' => trim($ogData['og_type'] ?? 'article'), + 'og_video' => trim($ogData['og_video'] ?? ''), 'seo_title' => trim($ogData['seo_title'] ?? ''), 'meta_description' => trim($ogData['meta_description'] ?? ''), 'robots' => trim($robots), diff --git a/source/packages/plg_system_mokoog/src/Extension/MokoOG.php b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php index b700fdc..760ee77 100644 --- a/source/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -92,7 +92,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface if ($catOg) { // Merge: category fills any gaps in the content-level data - foreach (['og_title', 'og_description', 'og_image', 'og_type', 'seo_title', 'meta_description', 'robots', 'canonical_url'] as $field) { + foreach (['og_title', 'og_description', 'og_image', 'og_type', 'og_video', 'seo_title', 'meta_description', 'robots', 'canonical_url'] as $field) { if (empty($ogData->$field) && !empty($catOg->$field)) { $ogData->$field = $catOg->$field; } @@ -184,6 +184,27 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface $doc->setMetaData('fediverse:creator', $fediverseCreator); } + // og:video tags + $videoUrl = $ogData->og_video ?? ''; + + if ($videoUrl) { + $doc->setMetaData('og:video', $videoUrl, 'property'); + $doc->setMetaData('og:video:secure_url', $videoUrl, 'property'); + + // Detect video type from URL + if (str_contains($videoUrl, 'youtube.com') || str_contains($videoUrl, 'youtu.be') + || str_contains($videoUrl, 'vimeo.com')) { + $doc->setMetaData('og:video:type', 'text/html', 'property'); + } else { + $ext = strtolower(pathinfo(parse_url($videoUrl, PHP_URL_PATH) ?: '', PATHINFO_EXTENSION)); + $mimeMap = ['mp4' => 'video/mp4', 'webm' => 'video/webm', 'ogg' => 'video/ogg']; + $doc->setMetaData('og:video:type', $mimeMap[$ext] ?? 'video/mp4', 'property'); + } + + $doc->setMetaData('og:video:width', '1280', 'property'); + $doc->setMetaData('og:video:height', '720', 'property'); + } + // LinkedIn article tags if ($option === 'com_content' && $view === 'article' && $id > 0) { $doc->setMetaData('article:published_time', $this->getArticleDate($id, 'publish_up'), 'property'); @@ -206,6 +227,39 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface } } + // Pinterest rich pin tags + if ($option === 'com_content' && $view === 'article' && $id > 0) { + $article = $this->loadArticle($id); + + if ($article) { + // Extract Joomla content tags for article:tag (used by Pinterest article pins) + $db = Factory::getDbo(); + $tagQuery = $db->getQuery(true) + ->select($db->quoteName('t.title')) + ->from($db->quoteName('#__tags', 't')) + ->join('INNER', $db->quoteName('#__contentitem_tag_map', 'm') + . ' ON ' . $db->quoteName('m.tag_id') . ' = ' . $db->quoteName('t.id')) + ->where($db->quoteName('m.type_alias') . ' = ' . $db->quote('com_content.article')) + ->where($db->quoteName('m.content_item_id') . ' = ' . $id) + ->where($db->quoteName('t.published') . ' = 1'); + $db->setQuery($tagQuery); + $tags = $db->loadColumn(); + + foreach ($tags as $tag) { + $doc->setMetaData('article:tag', $tag, 'property'); + } + } + } + + if ($option === 'com_mokoshop' && $view === 'product' && $id > 0) { + $productData = $this->loadShopProduct($id); + + if ($productData) { + $availability = ((int) ($productData->stock_qty ?? 0) > 0) ? 'instock' : 'outofstock'; + $doc->setMetaData('product:availability', $availability, 'property'); + } + } + // Fire event so third-party plugins can add custom OG/social tags $eventData = [ 'subject' => $doc, @@ -306,6 +360,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface 'og_description' => '', 'og_image' => '', 'og_type' => '', + 'og_video' => '', 'seo_title' => '', 'meta_description' => '', 'robots' => '',