2026-06-23 12:53:47 -05:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @package MokoSuiteOpenGraph
|
|
|
|
|
* @subpackage plg_system_mokoog
|
|
|
|
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
|
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
|
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
namespace Joomla\Plugin\System\MokoOG\Helper;
|
|
|
|
|
|
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
|
|
|
|
|
|
use Joomla\CMS\Factory;
|
|
|
|
|
use Joomla\CMS\Uri\Uri;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* XML Sitemap builder.
|
|
|
|
|
*
|
|
|
|
|
* Generates a sitemap.xml containing all published articles, excluding
|
|
|
|
|
* those marked with noindex robots directives in the mokoog_tags table.
|
|
|
|
|
*/
|
|
|
|
|
class SitemapBuilder
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* Generate sitemap XML content.
|
|
|
|
|
*
|
|
|
|
|
* @param string $changefreq Default change frequency for entries
|
|
|
|
|
*
|
|
|
|
|
* @return string Complete sitemap XML
|
|
|
|
|
*/
|
|
|
|
|
public static function generate(string $changefreq = 'weekly'): string
|
|
|
|
|
{
|
2026-06-28 13:54:53 -05:00
|
|
|
$allowed = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'];
|
|
|
|
|
$changefreq = \in_array($changefreq, $allowed, true) ? $changefreq : 'weekly';
|
|
|
|
|
|
2026-06-28 14:47:34 -05:00
|
|
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
2026-06-23 12:53:47 -05:00
|
|
|
|
2026-06-29 09:52:51 -05:00
|
|
|
// Only include content the public (guest, user id 0) can view — never
|
|
|
|
|
// leak registered/special-access articles into the public sitemap.
|
|
|
|
|
$publicLevels = array_map('intval', \Joomla\CMS\Access\Access::getAuthorisedViewLevels(0));
|
|
|
|
|
|
2026-06-23 12:53:47 -05:00
|
|
|
// Get all published articles
|
|
|
|
|
$query = $db->getQuery(true)
|
|
|
|
|
->select($db->quoteName(['a.id', 'a.alias', 'a.catid', 'a.modified', 'a.language']))
|
|
|
|
|
->from($db->quoteName('#__content', 'a'))
|
|
|
|
|
->where($db->quoteName('a.state') . ' = 1');
|
|
|
|
|
|
2026-06-29 09:52:51 -05:00
|
|
|
if (!empty($publicLevels)) {
|
|
|
|
|
$query->where($db->quoteName('a.access') . ' IN (' . implode(',', $publicLevels) . ')');
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 12:53:47 -05:00
|
|
|
$db->setQuery($query);
|
|
|
|
|
$articles = $db->loadObjectList();
|
|
|
|
|
|
|
|
|
|
// Get noindex articles from mokoog_tags
|
|
|
|
|
$noindexQuery = $db->getQuery(true)
|
|
|
|
|
->select($db->quoteName('content_id'))
|
|
|
|
|
->from($db->quoteName('#__mokoog_tags'))
|
|
|
|
|
->where($db->quoteName('content_type') . ' = ' . $db->quote('com_content'))
|
|
|
|
|
->where($db->quoteName('robots') . ' LIKE ' . $db->quote('%noindex%'));
|
|
|
|
|
|
|
|
|
|
$db->setQuery($noindexQuery);
|
2026-06-28 13:54:53 -05:00
|
|
|
$noindexIds = array_map('intval', $db->loadColumn());
|
2026-06-23 12:53:47 -05:00
|
|
|
|
|
|
|
|
$root = rtrim(Uri::root(), '/');
|
|
|
|
|
$xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
|
|
|
|
$xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
|
|
|
|
|
|
|
|
|
|
// Homepage
|
|
|
|
|
$xml .= ' <url>' . "\n";
|
|
|
|
|
$xml .= ' <loc>' . $root . '/</loc>' . "\n";
|
|
|
|
|
$xml .= ' <changefreq>daily</changefreq>' . "\n";
|
|
|
|
|
$xml .= ' <priority>1.0</priority>' . "\n";
|
|
|
|
|
$xml .= ' </url>' . "\n";
|
|
|
|
|
|
|
|
|
|
foreach ($articles as $article) {
|
|
|
|
|
// Skip noindexed
|
2026-06-28 13:54:53 -05:00
|
|
|
if (\in_array((int) $article->id, $noindexIds, true)) {
|
2026-06-23 12:53:47 -05:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 12:17:37 -05:00
|
|
|
$url = self::articleUrl($article, $root);
|
2026-06-23 12:53:47 -05:00
|
|
|
$lastmod = $article->modified && $article->modified !== '0000-00-00 00:00:00'
|
|
|
|
|
? date('Y-m-d', strtotime($article->modified)) : '';
|
|
|
|
|
|
|
|
|
|
$xml .= ' <url>' . "\n";
|
|
|
|
|
$xml .= ' <loc>' . htmlspecialchars($url, ENT_XML1) . '</loc>' . "\n";
|
|
|
|
|
|
|
|
|
|
if ($lastmod) {
|
|
|
|
|
$xml .= ' <lastmod>' . $lastmod . '</lastmod>' . "\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$xml .= ' <changefreq>' . $changefreq . '</changefreq>' . "\n";
|
|
|
|
|
$xml .= ' <priority>0.8</priority>' . "\n";
|
|
|
|
|
$xml .= ' </url>' . "\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$xml .= '</urlset>';
|
|
|
|
|
|
|
|
|
|
return $xml;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 12:17:37 -05:00
|
|
|
/**
|
|
|
|
|
* Build the SEF/canonical site URL for an article, with a safe fallback.
|
|
|
|
|
*
|
|
|
|
|
* Routes through the site router so the sitemap matches the canonical URLs
|
|
|
|
|
* the plugin emits. If routing fails (or SEF is off), falls back to the
|
|
|
|
|
* non-SEF index.php URL — never an empty or broken URL.
|
|
|
|
|
*
|
|
|
|
|
* @param object $article Row with id, alias, catid, language
|
|
|
|
|
* @param string $root Site root without trailing slash
|
|
|
|
|
*
|
|
|
|
|
* @return string Absolute URL
|
|
|
|
|
*/
|
|
|
|
|
private static function articleUrl(object $article, string $root): string
|
|
|
|
|
{
|
|
|
|
|
$fallback = $root . '/index.php?option=com_content&view=article&id=' . (int) $article->id;
|
|
|
|
|
|
|
|
|
|
$internal = 'index.php?option=com_content&view=article&id=' . (int) $article->id
|
|
|
|
|
. (!empty($article->alias) ? ':' . $article->alias : '')
|
|
|
|
|
. (!empty($article->catid) ? '&catid=' . (int) $article->catid : '');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$routed = \Joomla\CMS\Router\Route::link(
|
|
|
|
|
'site',
|
|
|
|
|
$internal,
|
|
|
|
|
false,
|
|
|
|
|
\Joomla\CMS\Router\Route::TLS_IGNORE,
|
|
|
|
|
true
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (\is_string($routed) && $routed !== '') {
|
|
|
|
|
return $routed;
|
|
|
|
|
}
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
// Fall back to the non-SEF URL below.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $fallback;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 12:53:47 -05:00
|
|
|
/**
|
|
|
|
|
* Write sitemap XML to the site root.
|
|
|
|
|
*
|
|
|
|
|
* @param string $xml The sitemap XML content
|
|
|
|
|
*
|
|
|
|
|
* @return bool True on success
|
|
|
|
|
*/
|
|
|
|
|
public static function writeToFile(string $xml): bool
|
|
|
|
|
{
|
|
|
|
|
$path = JPATH_ROOT . '/sitemap.xml';
|
2026-06-29 09:52:51 -05:00
|
|
|
$tmp = $path . '.' . uniqid('tmp', true);
|
|
|
|
|
|
|
|
|
|
if (file_put_contents($tmp, $xml) === false) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Atomic replace so concurrent saves never expose a half-written sitemap.
|
|
|
|
|
if (!@rename($tmp, $path)) {
|
|
|
|
|
@unlink($tmp);
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-06-23 12:53:47 -05:00
|
|
|
|
2026-06-29 09:52:51 -05:00
|
|
|
return true;
|
2026-06-23 12:53:47 -05:00
|
|
|
}
|
|
|
|
|
}
|