reference => * resolved item {url, mtime, alternates: [[locale, url], ...], items}), * settings (Sitemap settings values; absent keys take the PHP defaults), * manifest (the updates manifest object), skip (URLs a beforeAddItem * listener drops) and extra (URL => text of an element an addItem * listener appends). * * A registry type's resolved item is given in the Go shape (one item per * record with ordered alternates); this script turns it into what the PHP * headless resolvers returned: a list type (no url) yields one entry per * alternate for each child (a child without alternates once), any other type * its children with their default URL. */ $vendor = getenv('WINTER_VENDOR'); if (!$vendor || !is_file($vendor . '/autoload.php')) { fwrite(STDERR, "set WINTER_VENDOR to the vendor directory of a WinterCMS application\n"); exit(2); } require $vendor . '/autoload.php'; use Illuminate\Http\Request; use Illuminate\Routing\RouteCollection; use Winter\Storm\Router\UrlGenerator; date_default_timezone_set('UTC'); if ($argc !== 3) { fwrite(STDERR, "usage: php gen-goldens.php INPUT.json OUTPUT.golden\n"); exit(2); } $input = json_decode(file_get_contents($argv[1]), true); if (!is_array($input)) { fwrite(STDERR, "invalid JSON input\n"); exit(1); } function urlGenerator(string $root): UrlGenerator { return new UrlGenerator(new RouteCollection, Request::create(rtrim($root, '/') . '/sitemap.xml')); } if (isset($input['urls'])) { $url = urlGenerator($input['root']); $out = ''; foreach ($input['urls'] as $path) { $out .= $url->to($path) . "\n"; } file_put_contents($argv[2], $out); exit(0); } if (isset($input['case'])) { file_put_contents($argv[2], (new GoldenSitemap($input['case']))->render()); exit(0); } $out = ''; foreach ($input['cases'] as $case) { $out .= '=== ' . $case['name'] . "\n" . (new GoldenSitemap($case))->render(); } file_put_contents($argv[2], $out); /** * Golem15\Sitemap\Classes\DefinitionItem, unchanged properties. */ class GoldenDefinitionItem { public $changefreq; public $priority; public $nesting = false; public $type = 'url'; public $url = null; public $code = null; public $reference = null; public $cmsPage = null; public $urlPattern = null; public $exists = false; public static function initFromArray(array $items): array { $result = []; foreach ($items as $itemData) { $obj = new self; foreach ($itemData as $name => $value) { if (property_exists($obj, $name)) { $obj->$name = $value; } } $result[] = $obj; } return $result; } } /** * The generation code of Golem15\Sitemap\Models\Definition and routes.php. */ class GoldenSitemap { const MAX_URLS = 50000; protected $case; protected $url; protected $items; protected $updatesManifest = null; protected $urlCount = 0; protected $urlSet; protected $xmlObject; public function __construct(array $case) { $this->case = $case; $this->url = urlGenerator($case['root']); $this->items = GoldenDefinitionItem::initFromArray($case['items'] ?? []); } protected function setting(string $key, $default) { $settings = $this->case['settings'] ?? []; return array_key_exists($key, $settings) ? $settings[$key] : $default; } /** routes.php */ public function render(): string { $xml = $this->generateSitemap(); $frontendUrl = $this->setting('frontend_url', null); if ($frontendUrl) { $frontendUrl = rtrim($frontendUrl, '/'); $backendUrl = rtrim($this->url->to('/'), '/'); $xml = preg_replace_callback('/(.*?)<\/loc>/', function ($m) use ($backendUrl, $frontendUrl) { return '' . str_replace($backendUrl, $frontendUrl, $m[1]) . ''; }, $xml); $xml = preg_replace_callback('/(]*href=")([^"]*)(")/', function ($m) use ($backendUrl, $frontendUrl) { return $m[1] . str_replace($backendUrl, $frontendUrl, $m[2]) . $m[3]; }, $xml); $xml = str_replace( 'href="' . $backendUrl . '/sitemap.xsl"', 'href="' . $frontendUrl . '/sitemap.xsl"', $xml ); } return $xml; } /** * What the PHP headless resolvers returned for a resolved item in the * Go shape. */ protected function phpResolved(array $r, bool $listChildren): array { $info = []; if (!empty($r['url'])) { $info['url'] = $r['url']; } if (array_key_exists('mtime', $r)) { $info['mtime'] = $r['mtime'] === null ? null : new DateTime($r['mtime']); } $links = []; foreach ($r['alternates'] ?? [] as [$locale, $href]) { $links[$locale] = $href; } $info['alternateLinks'] = $links; if (isset($r['items'])) { $info['items'] = []; foreach ($r['items'] as $child) { $childInfo = $this->phpResolved($child, false); if ($listChildren && $childInfo['alternateLinks']) { foreach ($childInfo['alternateLinks'] as $locale => $href) { $perLocale = $childInfo; $perLocale['url'] = $href; $info['items'][] = $perLocale; } } else { $info['items'][] = $childInfo; } } } return $info; } protected function resolveItem(GoldenDefinitionItem $item) { $types = $this->case['types'] ?? []; $ref = (string) $item->reference; if (!isset($types[$item->type][$ref])) { return null; } $r = $types[$item->type][$ref]; return $this->phpResolved($r, empty($r['url'])); } public function generateSitemap(): string { if (!$this->items) { return ''; } foreach ($this->items as $item) { if ($item->type === 'url') { $this->addItemToSet($item); } else { $menuItemInfo = $this->resolveItem($item); if (!is_array($menuItemInfo)) { continue; } if (isset($menuItemInfo['url'])) { $this->addItemToSet($item, $menuItemInfo); } if (isset($menuItemInfo['items'])) { $menuItemIterator = function ($menuItems) use (&$menuItemIterator, $item) { foreach ($menuItems as $menuItem) { if (isset($menuItem['url'])) { $this->addItemToSet($item, $menuItem); } if (isset($menuItem['items'])) { $menuItemIterator($menuItem['items']); } } }; $menuItemIterator($menuItemInfo['items']); } } } $urlSet = $this->getUrlSet(); $xml = $this->getXmlObject(); $xml->appendChild($urlSet); return $xml->saveXML(); } protected function getXmlObject(): DOMDocument { if ($this->xmlObject !== null) { return $this->xmlObject; } $xml = new DOMDocument; $xml->encoding = 'UTF-8'; $xss = $xml->createProcessingInstruction('xml-stylesheet', 'type="text/xsl" href="' . UrlGenerator::buildUrl($this->url->current(), ['path' => 'sitemap.xsl']) . '"' ); $xml->appendChild($xss); return $this->xmlObject = $xml; } protected function getUrlSet(): DOMElement { if ($this->urlSet !== null) { return $this->urlSet; } $xml = $this->getXmlObject(); $urlSet = $xml->createElement('urlset'); $urlSet->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9'); $urlSet->setAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml'); $urlSet->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); $urlSet->setAttribute('xsi:schemaLocation', 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd'); return $this->urlSet = $urlSet; } protected function addItemToSet(GoldenDefinitionItem $item, ?array $itemInfo = null): void { $xml = $this->getXmlObject(); $urlSet = $this->getUrlSet(); if ($this->urlCount >= self::MAX_URLS) { return; } if (!isset($itemInfo['url'])) { $itemInfo['url'] = $this->url->to($item->url); } $lastModified = $itemInfo['mtime'] ?? null; if ($lastModified === null) { $lastModified = $this->lookupManifestLastmod($itemInfo['url']); } if ($lastModified instanceof \DateTime) { $lastModified = $lastModified->getTimestamp(); } $itemInfo['lastModified'] = $lastModified ? date('c', $lastModified) : null; // golem15.sitemap.beforeAddItem: a listener that drops the case's // skip URLs. if (in_array($itemInfo['url'], $this->case['skip'] ?? [], true)) { return; } if (empty($itemInfo['url'])) { return; } else { $itemInfo['url'] = $this->url->to($itemInfo['url']); } $this->urlCount++; $tierPriority = $this->getPriorityForUrl($itemInfo['url']); $effectivePriority = $tierPriority ?? $item->priority; $urlElement = $xml->createElement('url'); $loc = $xml->createElement('loc'); $loc->appendChild($xml->createTextNode($itemInfo['url'])); $urlElement->appendChild($loc); if (!empty($itemInfo['lastModified'])) { $lastmod = $xml->createElement('lastmod'); $lastmod->appendChild($xml->createTextNode((string) $itemInfo['lastModified'])); $urlElement->appendChild($lastmod); } $changefreq = $xml->createElement('changefreq'); $changefreq->appendChild($xml->createTextNode((string) $item->changefreq)); $urlElement->appendChild($changefreq); $priority = $xml->createElement('priority'); $priority->appendChild($xml->createTextNode((string) $effectivePriority)); $urlElement->appendChild($priority); if (!empty($itemInfo['alternateLinks']) && count($itemInfo['alternateLinks']) > 1) { foreach ($itemInfo['alternateLinks'] as $hreflang => $href) { $link = $xml->createElement('xhtml:link'); $link->setAttribute('rel', 'alternate'); $link->setAttribute('hreflang', $hreflang); $link->setAttribute('href', $this->url->to($href)); $urlElement->appendChild($link); } } $urlSet->appendChild($urlElement); // golem15.sitemap.addItem: a listener that appends the case's extra // element to the URL. $extra = $this->case['extra'] ?? []; if (isset($extra[$itemInfo['url']])) { $element = $xml->createElement('extra'); $element->appendChild($xml->createTextNode($extra[$itemInfo['url']])); $urlElement->appendChild($element); } } protected function lookupManifestLastmod(string $url): ?int { $manifest = $this->getUpdatesManifest(); if (!$manifest) { return null; } $path = parse_url($url, PHP_URL_PATH) ?: '/'; $path = $this->normalizeManifestPath($path); if (!isset($manifest[$path])) { return null; } $timestamp = strtotime((string) $manifest[$path]); return $timestamp ?: null; } protected function getUpdatesManifest(): array { if ($this->updatesManifest !== null) { return $this->updatesManifest; } $decoded = $this->case['manifest'] ?? null; if (!is_array($decoded)) { return $this->updatesManifest = []; } $normalized = []; foreach ($decoded as $key => $value) { $normalized[$this->normalizeManifestPath((string) $key)] = $value; } return $this->updatesManifest = $normalized; } protected function normalizeManifestPath(string $path): string { $path = '/' . ltrim($path, '/'); return $path === '/' ? '/' : rtrim($path, '/'); } protected function getPriorityForUrl(string $url): ?string { if (!$this->setting('enable_priority_tiers', true)) { return null; } $path = parse_url($url, PHP_URL_PATH) ?: '/'; $path = rtrim($path, '/'); if ($path === '' || $path === '/en') { return $this->setting('priority_homepage', '1.0'); } if (preg_match('#^/(uslugi|en/services|realizacje|en/work)/[^/]+$#', $path)) { return $this->setting('priority_services', '0.8'); } if (preg_match('#^/(blog|en/blog)/[^/]+/[^/]+$#', $path)) { return $this->setting('priority_blog', '0.6'); } if (preg_match('#^/(uslugi|en/services|realizacje|en/work|blog|en/blog)(/[^/]+)?$#', $path)) { return $this->setting('priority_categories', '0.4'); } $legalPaths = [ '/polityka-prywatnosci', '/en/privacy-policy', '/regulamin', '/en/terms', '/o-nas', '/en/about', '/kontakt', '/en/contact', '/laboratorium-ai', '/en/ai-lab', '/laboratorium-ai/zabawa-animacja', '/en/ai-lab/blob-playground', ]; if (in_array($path, $legalPaths)) { return $this->setting('priority_legal', '0.2'); } return null; } }