Skip to content

Commit

Permalink
Support of multi-language sitemaps with alternative links for each lo…
Browse files Browse the repository at this point in the history
…cation (#50)

* Added support of multi-language sitemaps with alternative links for each location

* Fixed letter case in phpdoc and README.md
  • Loading branch information
rdeanar authored and samdark committed Nov 24, 2017
1 parent 01c653c commit 6b7eed7
Show file tree
Hide file tree
Showing 5 changed files with 2,436 additions and 7 deletions.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Features
--------

- Create sitemap files.
- Create multi-language sitemap files.
- Create sitemap index files.
- Automatically creates new file if 50000 URLs limit is reached.
- Memory efficient buffer of configurable size.
Expand Down Expand Up @@ -78,6 +79,42 @@ foreach ($staticSitemapUrls as $sitemapUrl) {
$index->write();
```

Multi-language sitemap
----------------------

```php
use samdark\sitemap\Sitemap;

// create sitemap
// be sure to pass `true` as second parameter to specify XHTML namespace
$sitemap = new Sitemap(__DIR__ . '/sitemap_multi_language.xml', true);

// Set URL limit to fit in default limit of 50000 (default limit / number of languages)
$sitemap->setMaxUrls(25000);

// add some URLs
$sitemap->addItem('http://example.com/mylink1');

$sitemap->addItem([
'ru' => 'http://example.com/ru/mylink2',
'en' => 'http://example.com/en/mylink2',
], time());

$sitemap->addItem([
'ru' => 'http://example.com/ru/mylink3',
'en' => 'http://example.com/en/mylink3',
], time(), Sitemap::HOURLY);

$sitemap->addItem([
'ru' => 'http://example.com/ru/mylink4',
'en' => 'http://example.com/en/mylink4',
], time(), Sitemap::DAILY, 0.3);

// write it
$sitemap->write();

```

Options
-------

Expand Down
115 changes: 110 additions & 5 deletions Sitemap.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ class Sitemap
*/
private $useIndent = true;

/**
* @var bool if should XHTML namespace be specified
* Useful for multi-language sitemap to point crawler to alternate language page via xhtml:link tag.
* @see https://support.google.com/webmasters/answer/2620865?hl=en
*/
private $useXhtml = false;

/**
* @var array valid values for frequency parameter
*/
Expand Down Expand Up @@ -88,9 +95,11 @@ class Sitemap

/**
* @param string $filePath path of the file to write to
* @param bool $useXhtml is XHTML namespace should be specified
*
* @throws \InvalidArgumentException
*/
public function __construct($filePath)
public function __construct($filePath, $useXhtml = false)
{
$dir = dirname($filePath);
if (!is_dir($dir)) {
Expand All @@ -100,6 +109,7 @@ public function __construct($filePath)
}

$this->filePath = $filePath;
$this->useXhtml = $useXhtml;
}

/**
Expand Down Expand Up @@ -136,6 +146,9 @@ private function createNewFile()
$this->writer->setIndent($this->useIndent);
$this->writer->startElement('urlset');
$this->writer->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
if ($this->useXhtml) {
$this->writer->writeAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml');
}
}

/**
Expand Down Expand Up @@ -240,7 +253,7 @@ protected function validateLocation($location) {
/**
* Adds a new item to sitemap
*
* @param string $location location item URL
* @param string|array $location location item URL
* @param integer $lastModified last modification timestamp
* @param float $changeFrequency change frequency. Use one of self:: constants here
* @param string $priority item's priority (0.0-1.0). Default null is equal to 0.5
Expand All @@ -259,10 +272,36 @@ public function addItem($location, $lastModified = null, $changeFrequency = null
if ($this->urlsCount % $this->bufferSize === 0) {
$this->flush();
}
$this->writer->startElement('url');

if (is_array($location)) {
$this->addMultiLanguageItem($location, $lastModified, $changeFrequency, $priority);
} else {
$this->addSingleLanguageItem($location, $lastModified, $changeFrequency, $priority);
}

$this->urlsCount++;
}


/**
* Adds a new single item to sitemap
*
* @param string $location location item URL
* @param integer $lastModified last modification timestamp
* @param float $changeFrequency change frequency. Use one of self:: constants here
* @param string $priority item's priority (0.0-1.0). Default null is equal to 0.5
*
* @throws \InvalidArgumentException
*
* @see addItem
*/
private function addSingleLanguageItem($location, $lastModified, $changeFrequency, $priority)
{
$this->validateLocation($location);



$this->writer->startElement('url');

$this->writer->writeElement('loc', $location);

if ($lastModified !== null) {
Expand Down Expand Up @@ -291,10 +330,76 @@ public function addItem($location, $lastModified = null, $changeFrequency = null
}

$this->writer->endElement();
}

$this->urlsCount++;
/**
* Adds a multi-language item, based on multiple locations with alternate hrefs to sitemap
*
* @param array $locations array of language => link pairs
* @param integer $lastModified last modification timestamp
* @param float $changeFrequency change frequency. Use one of self:: constants here
* @param string $priority item's priority (0.0-1.0). Default null is equal to 0.5
*
* @throws \InvalidArgumentException
*
* @see addItem
*/
private function addMultiLanguageItem($locations, $lastModified, $changeFrequency, $priority)
{
foreach ($locations as $language => $url) {
$this->validateLocation($url);

$this->writer->startElement('url');

$this->writer->writeElement('loc', $url);

if ($lastModified !== null) {
$this->writer->writeElement('lastmod', date('c', $lastModified));
}

if ($changeFrequency !== null) {
if (!in_array($changeFrequency, $this->validFrequencies, true)) {
throw new \InvalidArgumentException(
'Please specify valid changeFrequency. Valid values are: '
. implode(', ', $this->validFrequencies)
. "You have specified: {$changeFrequency}."
);
}

$this->writer->writeElement('changefreq', $changeFrequency);
}

if ($priority !== null) {
if (!is_numeric($priority) || $priority < 0 || $priority > 1) {
throw new \InvalidArgumentException(
"Please specify valid priority. Valid values range from 0.0 to 1.0. You have specified: {$priority}."
);
}
$this->writer->writeElement('priority', number_format($priority, 1, '.', ','));
}

foreach ($locations as $hreflang => $href) {

$this->writer->startElement('xhtml:link');
$this->writer->startAttribute('rel');
$this->writer->text('alternate');
$this->writer->endAttribute();

$this->writer->startAttribute('hreflang');
$this->writer->text($hreflang);
$this->writer->endAttribute();

$this->writer->startAttribute('href');
$this->writer->text($href);
$this->writer->endAttribute();
$this->writer->endElement();
}

$this->writer->endElement();
}
}


/**
* @return string path of currently opened file
*/
Expand Down
64 changes: 62 additions & 2 deletions tests/SitemapTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ class SitemapTest extends \PHPUnit_Framework_TestCase
/**
* Asserts validity of simtemap according to XSD schema
* @param string $fileName
* @param bool $xhtml
*/
protected function assertIsValidSitemap($fileName)
protected function assertIsValidSitemap($fileName, $xhtml = false)
{
$xsdFileName = $xhtml ? 'sitemap_xhtml.xsd' : 'sitemap.xsd';

$xml = new \DOMDocument();
$xml->load($fileName);
$this->assertTrue($xml->schemaValidate(__DIR__ . '/sitemap.xsd'));
$this->assertTrue($xml->schemaValidate(__DIR__ . '/' . $xsdFileName));
}

protected function assertIsOneMemberGzipFile($fileName)
Expand Down Expand Up @@ -74,6 +77,37 @@ public function testMultipleFiles()
$this->assertContains('http://example.com/sitemap_multi_10.xml', $urls);
}


public function testMultiLanguageSitemap()
{
$fileName = __DIR__ . '/sitemap_multi_language.xml';
$sitemap = new Sitemap($fileName, true);
$sitemap->addItem('http://example.com/mylink1');

$sitemap->addItem([
'ru' => 'http://example.com/ru/mylink2',
'en' => 'http://example.com/en/mylink2',
], time());

$sitemap->addItem([
'ru' => 'http://example.com/ru/mylink3',
'en' => 'http://example.com/en/mylink3',
], time(), Sitemap::HOURLY);

$sitemap->addItem([
'ru' => 'http://example.com/ru/mylink4',
'en' => 'http://example.com/en/mylink4',
], time(), Sitemap::DAILY, 0.3);

$sitemap->write();

$this->assertTrue(file_exists($fileName));
$this->assertIsValidSitemap($fileName, true);

unlink($fileName);
}


public function testFrequencyValidation()
{
$this->setExpectedException('InvalidArgumentException');
Expand Down Expand Up @@ -122,6 +156,32 @@ public function testLocationValidation()
$this->assertTrue($exceptionCaught, 'Expected InvalidArgumentException wasn\'t thrown.');
}

public function testMultiLanguageLocationValidation()
{
$fileName = __DIR__ . '/sitemap.xml';
$sitemap = new Sitemap($fileName);


$sitemap->addItem([
'ru' => 'http://example.com/mylink1',
'en' => 'http://example.com/mylink2',
]);

$exceptionCaught = false;
try {
$sitemap->addItem([
'ru' => 'http://example.com/mylink3',
'en' => 'notlink',
], time());
} catch (\InvalidArgumentException $e) {
$exceptionCaught = true;
}

unlink($fileName);

$this->assertTrue($exceptionCaught, 'Expected InvalidArgumentException wasn\'t thrown.');
}

public function testWritingFileGzipped()
{
$fileName = __DIR__ . '/sitemap_gzipped.xml.gz';
Expand Down
16 changes: 16 additions & 0 deletions tests/sitemap_xhtml.xsd
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns="http://symfony.com/schema"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://symfony.com/schema"
elementFormDefault="qualified">
<!--
The Sitemap schema does not include the link element that is
utilized by Google for multi-language Sitemaps. Hence, we need
to combine the two schemas for automated validation in a dedicated
XSD.
-->
<xsd:import namespace="http://www.sitemaps.org/schemas/sitemap/0.9"
schemaLocation="sitemap.xsd"/>
<xsd:import namespace="http://www.w3.org/1999/xhtml"
schemaLocation="xhtml1-strict.xsd"/>
</xsd:schema>
Loading

0 comments on commit 6b7eed7

Please sign in to comment.