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 c3f3d80..28aecda 100644 --- a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini @@ -39,3 +39,12 @@ 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" + +COM_MOKOOG_TOOLBAR_BATCH_GENERATE="Batch Generate" +COM_MOKOOG_BATCH_TITLE="Batch OG Tag Generation" +COM_MOKOOG_BATCH_COUNTING="Counting articles without OG tags..." +COM_MOKOOG_BATCH_NONE="All articles already have OG tags." +COM_MOKOOG_BATCH_FOUND="articles found without OG tags." +COM_MOKOOG_BATCH_PROCESSED="processed" +COM_MOKOOG_BATCH_COMPLETE="Batch generation complete!" +COM_MOKOOG_BATCH_ERROR="Error:" 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 c3f3d80..28aecda 100644 --- a/src/packages/com_mokoog/language/en-US/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-US/com_mokoog.ini @@ -39,3 +39,12 @@ 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" + +COM_MOKOOG_TOOLBAR_BATCH_GENERATE="Batch Generate" +COM_MOKOOG_BATCH_TITLE="Batch OG Tag Generation" +COM_MOKOOG_BATCH_COUNTING="Counting articles without OG tags..." +COM_MOKOOG_BATCH_NONE="All articles already have OG tags." +COM_MOKOOG_BATCH_FOUND="articles found without OG tags." +COM_MOKOOG_BATCH_PROCESSED="processed" +COM_MOKOOG_BATCH_COMPLETE="Batch generation complete!" +COM_MOKOOG_BATCH_ERROR="Error:" diff --git a/src/packages/com_mokoog/src/Controller/BatchController.php b/src/packages/com_mokoog/src/Controller/BatchController.php new file mode 100644 index 0000000..909ef78 --- /dev/null +++ b/src/packages/com_mokoog/src/Controller/BatchController.php @@ -0,0 +1,169 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Component\MokoOG\Administrator\Controller; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\BaseController; +use Joomla\CMS\Response\JsonResponse; +use Joomla\CMS\Session\Session; + +class BatchController extends BaseController +{ + /** + * Count the total articles eligible for batch generation. + * + * @return void + */ + public function count(): void + { + Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN')); + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__content', 'c')) + ->leftJoin( + $db->quoteName('#__mokoog_tags', 't') + . ' ON ' . $db->quoteName('t.content_type') . ' = ' . $db->quote('com_content') + . ' AND ' . $db->quoteName('t.content_id') . ' = ' . $db->quoteName('c.id') + ) + ->where($db->quoteName('c.state') . ' = 1') + ->where($db->quoteName('t.id') . ' IS NULL'); + + $db->setQuery($query); + $total = (int) $db->loadResult(); + + echo new JsonResponse(['total' => $total]); + + Factory::getApplication()->close(); + } + + /** + * Process a chunk of articles for batch OG generation. + * + * @return void + */ + public function process(): void + { + Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN')); + + $app = Factory::getApplication(); + $offset = $app->getInput()->getInt('offset', 0); + $limit = $app->getInput()->getInt('limit', 50); + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName([ + 'c.id', 'c.title', 'c.metadesc', 'c.introtext', 'c.fulltext', 'c.images', + ])) + ->from($db->quoteName('#__content', 'c')) + ->leftJoin( + $db->quoteName('#__mokoog_tags', 't') + . ' ON ' . $db->quoteName('t.content_type') . ' = ' . $db->quote('com_content') + . ' AND ' . $db->quoteName('t.content_id') . ' = ' . $db->quoteName('c.id') + ) + ->where($db->quoteName('c.state') . ' = 1') + ->where($db->quoteName('t.id') . ' IS NULL') + ->order($db->quoteName('c.id') . ' ASC'); + + $db->setQuery($query, $offset, $limit); + $articles = $db->loadObjectList(); + + $created = 0; + $now = Factory::getDate()->toSql(); + + foreach ($articles as $article) { + $ogTitle = $article->title; + $ogDescription = $this->extractDescription($article); + $ogImage = $this->extractImage($article); + + $record = (object) [ + 'content_type' => 'com_content', + 'content_id' => (int) $article->id, + 'og_title' => $ogTitle, + 'og_description' => $ogDescription, + 'og_image' => $ogImage, + 'og_type' => 'article', + 'seo_title' => '', + 'meta_description' => $article->metadesc ?: '', + 'robots' => '', + 'canonical_url' => '', + 'published' => 1, + 'created' => $now, + 'modified' => $now, + ]; + + $db->insertObject('#__mokoog_tags', $record); + $created++; + } + + echo new JsonResponse([ + 'created' => $created, + 'offset' => $offset, + 'processed' => $offset + $created, + ]); + + $app->close(); + } + + /** + * Extract a description from article content. + * + * @param object $article Article record + * + * @return string + */ + private function extractDescription(object $article): string + { + // Prefer meta description if set + if (!empty($article->metadesc)) { + return $article->metadesc; + } + + // Fall back to intro text + $text = $article->introtext ?: $article->fulltext; + $text = strip_tags($text); + $text = trim(preg_replace('/\s+/', ' ', $text)); + + if (\strlen($text) > 160) { + $text = mb_substr($text, 0, 157) . '...'; + } + + return $text; + } + + /** + * Extract the best image from article data. + * + * @param object $article Article record + * + * @return string + */ + private function extractImage(object $article): string + { + if (!empty($article->images)) { + $images = json_decode($article->images, true); + + if (!empty($images['image_fulltext'])) { + return $images['image_fulltext']; + } + + if (!empty($images['image_intro'])) { + return $images['image_intro']; + } + } + + return ''; + } +} diff --git a/src/packages/com_mokoog/src/View/Tags/HtmlView.php b/src/packages/com_mokoog/src/View/Tags/HtmlView.php index 14ff7cb..0a189b4 100644 --- a/src/packages/com_mokoog/src/View/Tags/HtmlView.php +++ b/src/packages/com_mokoog/src/View/Tags/HtmlView.php @@ -65,6 +65,7 @@ class HtmlView extends BaseHtmlView protected function addToolbar(): void { ToolbarHelper::title(Text::_('COM_MOKOOG_TAGS_TITLE'), 'bookmark'); + ToolbarHelper::custom('batch.generate', 'refresh', '', 'COM_MOKOOG_TOOLBAR_BATCH_GENERATE', false); ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'tags.delete'); ToolbarHelper::preferences('com_mokoog'); } diff --git a/src/packages/com_mokoog/tmpl/tags/default.php b/src/packages/com_mokoog/tmpl/tags/default.php index 779746f..94ad6cf 100644 --- a/src/packages/com_mokoog/tmpl/tags/default.php +++ b/src/packages/com_mokoog/tmpl/tags/default.php @@ -14,9 +14,11 @@ use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\LayoutHelper; use Joomla\CMS\Router\Route; +use Joomla\CMS\Session\Session; /** @var \Joomla\Component\MokoOG\Administrator\View\Tags\HtmlView $this */ +$token = Session::getFormToken(); ?>
+ + + + +