Hi RolandD
Having your custom exporter, you also probably changed the website dropdown with your own option(s), am I right?
Yes—this issue mainly concerns the pre-made template for Google Merchant. Having to modify the google.php file to suit your needs after every component update is a bit tedious and troublesome. That’s why I’ve created my own solution in a separate file (Merchant.php).
My custom sitemap code for Virtuemart is working fine. The only thing I have to keep in mind is that the filename must be capitalized exactly the same way as the class name—starting with a capital letter. for example, like this: Sitemap.php
My modification completely solves this problem—see
https://piekielko.com/sitemap.xml
<?php
namespace Rolandd\Component\Rocsvi\Site\File\Export\Xml;
defined('_JEXEC') or die;
use Rolandd\Component\Rocsvi\Site\Entity\Template as TemplateEntity;
use Rolandd\Component\Rocsvi\Site\File\Export\FileExportInterface;
class Sitemap implements FileExportInterface
{
private TemplateEntity $template;
private bool $open = false;
private array $row = [];
public function __construct(TemplateEntity $template)
{
$this->template = $template;
}
public function headerText($exportFields): string
{
if ($this->open)
{
return '';
}
$this->open = true;
return
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" .
"<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" " .
"xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\" " .
"xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">";
}
public function footerText(): string
{
return "</urlset>";
}
public function nodeStart(int $productId = 0, string $type = ''): string
{
$this->row = [];
return "<url>\n";
}
public function nodeEnd(int $productId = 0): string
{
if (empty($this->row['product_url']))
{
return "</url>\n";
}
$productPL = $this->row['product_url'];
$productEN = str_replace('piekielko.com/', 'piekielko.com/en/', $productPL);
$lastmod = $this->row['lastmod'] ?? '';
$priority = $this->row['priority'] ?? '';
$changefreq = $this->row['changefreq'] ?? '';
$xml = '';
$xml .= '<xhtml:link rel="alternate" hreflang="pl" href="' . htmlspecialchars($productPL, ENT_XML1) . "\" />\n";
$xml .= '<xhtml:link rel="alternate" hreflang="en" href="' . htmlspecialchars($productEN, ENT_XML1) . "\" />\n";
$xml .= '<xhtml:link rel="alternate" hreflang="x-default" href="' . htmlspecialchars($productPL, ENT_XML1) . "\" />\n";
$xml .= "</url>\n";
$xml .= "<url>\n";
$xml .= "<loc>" . htmlspecialchars($productEN, ENT_XML1) . "</loc>\n";
if ($lastmod)
{
$xml .= "<lastmod>" . htmlspecialchars($lastmod, ENT_XML1) . "</lastmod>\n";
}
if ($priority)
{
$xml .= "<priority>" . htmlspecialchars($priority, ENT_XML1) . "</priority>\n";
}
if ($changefreq)
{
$xml .= "<changefreq>" . htmlspecialchars($changefreq, ENT_XML1) . "</changefreq>\n";
}
$xml .= '<xhtml:link rel="alternate" hreflang="pl" href="' . htmlspecialchars($productPL, ENT_XML1) . "\" />\n";
$xml .= '<xhtml:link rel="alternate" hreflang="en" href="' . htmlspecialchars($productEN, ENT_XML1) . "\" />\n";
$xml .= '<xhtml:link rel="alternate" hreflang="x-default" href="' . htmlspecialchars($productPL, ENT_XML1) . "\" />\n";
$xml .= "</url>";
return $xml;
}
public function contentText($content, $columnHeader, $fieldName, $cdata = false): string
{
$this->row[$fieldName] = trim((string) $content);
$xml = '';
switch ($fieldName)
{
case 'product_url':
$xml .= '<loc>' . htmlspecialchars(trim((string) $content), ENT_XML1) . "</loc>\n";
break;
case 'file_url':
$parts = explode('^', (string) ($content ?? ''));
$imagePart = $parts[0] ?? '';
$title = trim($parts[1] ?? '');
$caption = trim($parts[2] ?? '');
$caption = strip_tags(html_entity_decode($caption));
if (mb_strlen($caption) > 200)
{
$caption = mb_substr($caption, 0, 200) . '…';
}
$caption = htmlspecialchars($caption, ENT_XML1);
$images = strpos($imagePart, '|') !== false ? explode('|', $imagePart) : [$imagePart];
foreach ($images as $img)
{
$img = trim($img);
if (!$img)
{
continue;
}
if (strpos($img, 'http') !== 0)
{
$img = 'https://piekielko.com/' . ltrim($img, '/');
}
$xml .= "<image:image>\n";
$xml .= "<image:geo_location>Polska</image:geo_location>\n";
$xml .= "<image:loc>" . htmlspecialchars($img, ENT_XML1) . "</image:loc>\n";
if ($title)
{
$xml .= "<image:title>" . htmlspecialchars($title, ENT_XML1) . "</image:title>\n";
}
if ($caption)
{
$xml .= "<image:caption>{$caption}</image:caption>\n";
}
$xml .= "<image:license>https://piekielko.com/licencja</image:license>\n";
$xml .= "</image:image>\n";
}
break;
default:
if (!empty($columnHeader))
{
$xml .= '<' . $columnHeader . '>' . htmlspecialchars(trim((string) $content), ENT_XML1) . '</' . $columnHeader . ">\n";
}
break;
}
return $xml;
}
public function prepareContent(array $contents, array $fields, bool $lastRecord): string
{
return implode('', $contents);
}
}
Overall, I think your component is the best solution for building custom site maps. I created separate indexes for products and articles, and it works perfectly. I don’t need any additional third-party solutions.
Kind regards
Chris