diff --git a/source/packages/com_mokoog/forms/tag.xml b/source/packages/com_mokoog/forms/tag.xml index 7f87824..83f8f93 100644 --- a/source/packages/com_mokoog/forms/tag.xml +++ b/source/packages/com_mokoog/forms/tag.xml @@ -63,8 +63,8 @@ diff --git a/source/packages/com_mokoog/language/en-GB/com_mokoog.ini b/source/packages/com_mokoog/language/en-GB/com_mokoog.ini index f970f01..e8db069 100644 --- a/source/packages/com_mokoog/language/en-GB/com_mokoog.ini +++ b/source/packages/com_mokoog/language/en-GB/com_mokoog.ini @@ -32,6 +32,8 @@ COM_MOKOOG_FIELD_OG_IMAGE="OG Image" COM_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing." COM_MOKOOG_FIELD_OG_TYPE="OG Type" COM_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type." +COM_MOKOOG_FIELD_OG_VIDEO="Video URL" +COM_MOKOOG_FIELD_OG_VIDEO_DESC="URL of a video for social sharing previews. Supports direct video URLs and YouTube/Vimeo links." COM_MOKOOG_FILTER_SEARCH="Search OG titles" COM_MOKOOG_FILTER_CONTENT_TYPE="Content Type" diff --git a/source/packages/com_mokoog/language/en-US/com_mokoog.ini b/source/packages/com_mokoog/language/en-US/com_mokoog.ini index f970f01..e8db069 100644 --- a/source/packages/com_mokoog/language/en-US/com_mokoog.ini +++ b/source/packages/com_mokoog/language/en-US/com_mokoog.ini @@ -32,6 +32,8 @@ COM_MOKOOG_FIELD_OG_IMAGE="OG Image" COM_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing." COM_MOKOOG_FIELD_OG_TYPE="OG Type" COM_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type." +COM_MOKOOG_FIELD_OG_VIDEO="Video URL" +COM_MOKOOG_FIELD_OG_VIDEO_DESC="URL of a video for social sharing previews. Supports direct video URLs and YouTube/Vimeo links." COM_MOKOOG_FILTER_SEARCH="Search OG titles" COM_MOKOOG_FILTER_CONTENT_TYPE="Content Type" diff --git a/source/packages/com_mokoog/sql/install.mysql.sql b/source/packages/com_mokoog/sql/install.mysql.sql index 420d9a0..1bce7f2 100644 --- a/source/packages/com_mokoog/sql/install.mysql.sql +++ b/source/packages/com_mokoog/sql/install.mysql.sql @@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS `#__mokoog_tags` ( `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, - `content_type` VARCHAR(100) NOT NULL DEFAULT '' COMMENT 'e.g. com_content, menu, com_virtuemart', + `content_type` VARCHAR(100) NOT NULL DEFAULT '' COMMENT 'e.g. com_content, menu, com_mokoshop', `content_id` INT(11) UNSIGNED NOT NULL DEFAULT 0, `og_title` VARCHAR(255) NOT NULL DEFAULT '', `og_description` TEXT NOT NULL, diff --git a/source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php b/source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php index e7362d9..7500169 100644 --- a/source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php +++ b/source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php @@ -249,7 +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'] ?? ''), + 'og_video' => $this->sanitizeUrl($ogData['og_video'] ?? ''), 'seo_title' => trim($ogData['seo_title'] ?? ''), 'meta_description' => trim($ogData['meta_description'] ?? ''), 'robots' => trim($robots), @@ -267,6 +267,28 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface } } + /** + * Sanitize a URL to only allow http/https schemes. + * + * @param string $url Raw URL value + * + * @return string Sanitized URL or empty string + */ + private function sanitizeUrl(string $url): string + { + $url = trim($url); + + if ($url === '') { + return ''; + } + + if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) { + return ''; + } + + return $url; + } + /** * Extract the language tag from content data. * diff --git a/source/packages/plg_system_mokoog/src/Extension/MokoOG.php b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php index 760ee77..e65971f 100644 --- a/source/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -189,20 +189,24 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface 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')) { + if (str_starts_with($videoUrl, 'https://')) { + $doc->setMetaData('og:video:secure_url', $videoUrl, 'property'); + } + + // Detect video type from URL — embeds vs direct files + $isEmbed = str_contains($videoUrl, 'youtube.com') || str_contains($videoUrl, 'youtu.be') + || str_contains($videoUrl, 'vimeo.com'); + + if ($isEmbed) { $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'); } - - $doc->setMetaData('og:video:width', '1280', 'property'); - $doc->setMetaData('og:video:height', '720', 'property'); } // LinkedIn article tags @@ -217,49 +221,37 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface } } - // MokoSuiteShop product meta tags + // MokoSuiteShop product meta tags (pricing + Pinterest availability) if ($option === 'com_mokoshop' && $view === 'product' && $id > 0) { $productData = $this->loadShopProduct($id); if ($productData) { $doc->setMetaData('product:price:amount', number_format((float) $productData->price, 2, '.', ''), 'property'); $doc->setMetaData('product:price:currency', $productData->currency ?: 'USD', 'property'); - } - } - - // 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'); } } + // Pinterest article:tag rich pins (from Joomla content tags) + if ($option === 'com_content' && $view === 'article' && $id > 0) { + $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'); + } + } + // Fire event so third-party plugins can add custom OG/social tags $eventData = [ 'subject' => $doc,