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 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
+ 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) {