diff --git a/src/packages/com_mokoog/forms/tag.xml b/src/packages/com_mokoog/forms/tag.xml index 363ef5f..3c1ab5c 100644 --- a/src/packages/com_mokoog/forms/tag.xml +++ b/src/packages/com_mokoog/forms/tag.xml @@ -70,4 +70,37 @@ +
+ + + + +
diff --git a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini index 0e56059..c3f3d80 100644 --- a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini @@ -13,4 +13,29 @@ COM_MOKOOG_HEADING_CONTENT_TYPE="Content Type" COM_MOKOOG_HEADING_CONTENT_ID="Content ID" COM_MOKOOG_HEADING_OG_TITLE="OG Title" COM_MOKOOG_HEADING_IMAGE="Image" +COM_MOKOOG_HEADING_SEO="SEO" COM_MOKOOG_HEADING_MODIFIED="Modified" + +COM_MOKOOG_SEO_OK="OK" +COM_MOKOOG_SEO_MISSING_DESC="No meta description" +COM_MOKOOG_SEO_TITLE_LONG="SEO title too long" +COM_MOKOOG_SEO_NOINDEX="noindex" + +COM_MOKOOG_FIELD_CONTENT_TYPE="Content Type" +COM_MOKOOG_FIELD_CONTENT_ID="Content ID" +COM_MOKOOG_FIELD_OG_TITLE="OG Title" +COM_MOKOOG_FIELD_OG_TITLE_DESC="Custom title for social sharing." +COM_MOKOOG_FIELD_OG_DESCRIPTION="OG Description" +COM_MOKOOG_FIELD_OG_DESCRIPTION_DESC="Custom description for social sharing." +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_FILTER_SEARCH="Search OG titles" +COM_MOKOOG_FILTER_CONTENT_TYPE="Content Type" +COM_MOKOOG_FILTER_SELECT_TYPE="- Select Type -" +COM_MOKOOG_HEADING_OG_TITLE_ASC="OG Title ascending" +COM_MOKOOG_HEADING_OG_TITLE_DESC="OG Title descending" +COM_MOKOOG_HEADING_MODIFIED_ASC="Modified ascending" +COM_MOKOOG_HEADING_MODIFIED_DESC="Modified descending" diff --git a/src/packages/com_mokoog/language/en-US/com_mokoog.ini b/src/packages/com_mokoog/language/en-US/com_mokoog.ini index 0e56059..c3f3d80 100644 --- a/src/packages/com_mokoog/language/en-US/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-US/com_mokoog.ini @@ -13,4 +13,29 @@ COM_MOKOOG_HEADING_CONTENT_TYPE="Content Type" COM_MOKOOG_HEADING_CONTENT_ID="Content ID" COM_MOKOOG_HEADING_OG_TITLE="OG Title" COM_MOKOOG_HEADING_IMAGE="Image" +COM_MOKOOG_HEADING_SEO="SEO" COM_MOKOOG_HEADING_MODIFIED="Modified" + +COM_MOKOOG_SEO_OK="OK" +COM_MOKOOG_SEO_MISSING_DESC="No meta description" +COM_MOKOOG_SEO_TITLE_LONG="SEO title too long" +COM_MOKOOG_SEO_NOINDEX="noindex" + +COM_MOKOOG_FIELD_CONTENT_TYPE="Content Type" +COM_MOKOOG_FIELD_CONTENT_ID="Content ID" +COM_MOKOOG_FIELD_OG_TITLE="OG Title" +COM_MOKOOG_FIELD_OG_TITLE_DESC="Custom title for social sharing." +COM_MOKOOG_FIELD_OG_DESCRIPTION="OG Description" +COM_MOKOOG_FIELD_OG_DESCRIPTION_DESC="Custom description for social sharing." +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_FILTER_SEARCH="Search OG titles" +COM_MOKOOG_FILTER_CONTENT_TYPE="Content Type" +COM_MOKOOG_FILTER_SELECT_TYPE="- Select Type -" +COM_MOKOOG_HEADING_OG_TITLE_ASC="OG Title ascending" +COM_MOKOOG_HEADING_OG_TITLE_DESC="OG Title descending" +COM_MOKOOG_HEADING_MODIFIED_ASC="Modified ascending" +COM_MOKOOG_HEADING_MODIFIED_DESC="Modified descending" diff --git a/src/packages/com_mokoog/sql/install.mysql.sql b/src/packages/com_mokoog/sql/install.mysql.sql index 884d369..306bc3e 100644 --- a/src/packages/com_mokoog/sql/install.mysql.sql +++ b/src/packages/com_mokoog/sql/install.mysql.sql @@ -12,6 +12,10 @@ CREATE TABLE IF NOT EXISTS `#__mokoog_tags` ( `og_description` TEXT NOT NULL, `og_image` VARCHAR(512) NOT NULL DEFAULT '', `og_type` VARCHAR(50) NOT NULL DEFAULT 'article', + `seo_title` VARCHAR(70) NOT NULL DEFAULT '', + `meta_description` VARCHAR(200) NOT NULL DEFAULT '', + `robots` VARCHAR(100) NOT NULL DEFAULT '', + `canonical_url` VARCHAR(512) NOT NULL DEFAULT '', `published` TINYINT(1) NOT NULL DEFAULT 1, `created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', `modified` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', diff --git a/src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql b/src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql new file mode 100644 index 0000000..4f4c433 --- /dev/null +++ b/src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql @@ -0,0 +1,9 @@ +-- +-- MokoOpenGraph 01.01.00 — Add SEO meta management columns +-- + +ALTER TABLE `#__mokoog_tags` + ADD COLUMN `seo_title` VARCHAR(70) NOT NULL DEFAULT '' AFTER `og_type`, + ADD COLUMN `meta_description` VARCHAR(200) NOT NULL DEFAULT '' AFTER `seo_title`, + ADD COLUMN `robots` VARCHAR(100) NOT NULL DEFAULT '' AFTER `meta_description`, + ADD COLUMN `canonical_url` VARCHAR(512) NOT NULL DEFAULT '' AFTER `robots`; diff --git a/src/packages/com_mokoog/tmpl/tags/default.php b/src/packages/com_mokoog/tmpl/tags/default.php index cfec7f7..779746f 100644 --- a/src/packages/com_mokoog/tmpl/tags/default.php +++ b/src/packages/com_mokoog/tmpl/tags/default.php @@ -50,6 +50,9 @@ use Joomla\CMS\Router\Route; + + + @@ -83,6 +86,30 @@ use Joomla\CMS\Router\Route; + + meta_description)) { + $seoIssues[] = Text::_('COM_MOKOOG_SEO_MISSING_DESC'); + } + + if (!empty($item->seo_title) && \strlen($item->seo_title) > 60) { + $seoIssues[] = Text::_('COM_MOKOOG_SEO_TITLE_LONG'); + } + + if (!empty($item->robots) && str_contains($item->robots, 'noindex')) { + $seoIssues[] = Text::_('COM_MOKOOG_SEO_NOINDEX'); + } + + if (empty($seoIssues)) : ?> + + + + + + + published ? Text::_('JPUBLISHED') : Text::_('JUNPUBLISHED'); ?> diff --git a/src/packages/plg_content_mokoog/forms/mokoog.xml b/src/packages/plg_content_mokoog/forms/mokoog.xml index 1e9f026..9e5632e 100644 --- a/src/packages/plg_content_mokoog/forms/mokoog.xml +++ b/src/packages/plg_content_mokoog/forms/mokoog.xml @@ -50,5 +50,48 @@ +
+ + + + + + + + + + + +
diff --git a/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini b/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini index 6ea916c..167fe7e 100644 --- a/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini +++ b/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini @@ -13,3 +13,16 @@ PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE="OG Image" PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing. Recommended: 1200x630px. Leave blank to use the article image." 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_FIELDSET_SEO_LABEL="SEO Meta Tags" +PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC="Control search engine meta tags for this page." + +PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE="SEO Title" +PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC="Custom tag. 50-60 characters recommended. Leave blank to use the default page title." +PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION="Meta Description" +PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC="Custom meta description. 150-160 characters recommended. Leave blank to use the default." +PLG_CONTENT_MOKOOG_FIELD_ROBOTS="Robots Directive" +PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC="Search engine indexing directives for this page. Leave blank for default (index, follow)." +PLG_CONTENT_MOKOOG_ROBOTS_DEFAULT="- Use default (index, follow) -" +PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL="Canonical URL" +PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC="Override the canonical URL for this page. Leave blank to use the current URL." diff --git a/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini b/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini index 6ea916c..167fe7e 100644 --- a/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini +++ b/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini @@ -13,3 +13,16 @@ PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE="OG Image" PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing. Recommended: 1200x630px. Leave blank to use the article image." 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_FIELDSET_SEO_LABEL="SEO Meta Tags" +PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC="Control search engine meta tags for this page." + +PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE="SEO Title" +PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC="Custom <title> tag. 50-60 characters recommended. Leave blank to use the default page title." +PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION="Meta Description" +PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC="Custom meta description. 150-160 characters recommended. Leave blank to use the default." +PLG_CONTENT_MOKOOG_FIELD_ROBOTS="Robots Directive" +PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC="Search engine indexing directives for this page. Leave blank for default (index, follow)." +PLG_CONTENT_MOKOOG_ROBOTS_DEFAULT="- Use default (index, follow) -" +PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL="Canonical URL" +PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC="Override the canonical URL for this page. Leave blank to use the current URL." diff --git a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php index 07c1161..3d70565 100644 --- a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php +++ b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php @@ -176,7 +176,10 @@ 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'])) + ->select($db->quoteName([ + 'og_title', 'og_description', 'og_image', 'og_type', + 'seo_title', 'meta_description', 'robots', 'canonical_url', + ])) ->from($db->quoteName('#__mokoog_tags')) ->where($db->quoteName('content_type') . ' = ' . $db->quote($contentType)) ->where($db->quoteName('content_id') . ' = ' . $contentId); @@ -209,15 +212,26 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface $db->setQuery($query); $existingId = $db->loadResult(); + // Robots may come as array from multi-select, join with comma + $robots = $ogData['robots'] ?? ''; + + if (\is_array($robots)) { + $robots = implode(', ', array_filter($robots)); + } + $record = (object) [ - 'content_type' => $contentType, - 'content_id' => $contentId, - 'og_title' => trim($ogData['og_title'] ?? ''), - 'og_description' => trim($ogData['og_description'] ?? ''), - 'og_image' => trim($ogData['og_image'] ?? ''), - 'og_type' => trim($ogData['og_type'] ?? 'article'), - 'published' => 1, - 'modified' => Factory::getDate()->toSql(), + 'content_type' => $contentType, + 'content_id' => $contentId, + 'og_title' => trim($ogData['og_title'] ?? ''), + 'og_description' => trim($ogData['og_description'] ?? ''), + 'og_image' => trim($ogData['og_image'] ?? ''), + 'og_type' => trim($ogData['og_type'] ?? 'article'), + 'seo_title' => trim($ogData['seo_title'] ?? ''), + 'meta_description' => trim($ogData['meta_description'] ?? ''), + 'robots' => trim($robots), + 'canonical_url' => trim($ogData['canonical_url'] ?? ''), + 'published' => 1, + 'modified' => Factory::getDate()->toSql(), ]; if ($existingId) { diff --git a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php index 818e263..c245c42 100644 --- a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -67,6 +67,9 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface // Try to load custom OG data from the database $ogData = $this->loadOgData($option, $view, $id); + // --- SEO meta tags (set first, before OG) --- + $this->applySeoTags($doc, $ogData); + // Build tag values — custom overrides auto-generated $title = $ogData->og_title ?: $doc->getTitle(); $description = $ogData->og_description ?: $this->buildDescription($doc); @@ -111,6 +114,44 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface } } + /** + * Apply SEO meta tags (title, description, robots, canonical) to the document. + * + * @param \Joomla\CMS\Document\HtmlDocument $doc The document + * @param object $ogData The loaded OG/SEO data + * + * @return void + */ + private function applySeoTags($doc, object $ogData): void + { + // Custom SEO title overrides the page <title> + if (!empty($ogData->seo_title)) { + $doc->setTitle($ogData->seo_title); + } + + // Custom meta description + if (!empty($ogData->meta_description)) { + $doc->setDescription($ogData->meta_description); + } + + // Robots directive + if (!empty($ogData->robots)) { + $doc->setMetaData('robots', $ogData->robots); + } + + // Canonical URL + if (!empty($ogData->canonical_url)) { + // Remove any existing canonical link first + foreach ($doc->_links as $link => $attribs) { + if (isset($attribs['relation']) && $attribs['relation'] === 'canonical') { + unset($doc->_links[$link]); + } + } + + $doc->addHeadLink($ogData->canonical_url, 'canonical'); + } + } + /** * Load custom OG data from the database for the current page. * @@ -123,10 +164,14 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface private function loadOgData(string $option, string $view, int $id): object { $empty = (object) [ - 'og_title' => '', - 'og_description' => '', - 'og_image' => '', - 'og_type' => '', + 'og_title' => '', + 'og_description' => '', + 'og_image' => '', + 'og_type' => '', + 'seo_title' => '', + 'meta_description' => '', + 'robots' => '', + 'canonical_url' => '', ]; if (!$id) {