feat: implement Sitemapable (#181)

This commit is contained in:
Will Rossiter 2023-10-28 09:23:54 +13:00
parent def6be347c
commit 8a7c8aa9cc
No known key found for this signature in database
GPG Key ID: 7FD2A809B22259EF
10 changed files with 164 additions and 95 deletions

View File

@ -6,13 +6,13 @@
## Maintainer Contact ## Maintainer Contact
* Will Rossiter (Nickname: wrossiter, willr) <will@fullscreen.io> - Will Rossiter (Nickname: wrossiter, willr) <will@fullscreen.io>
## Installation ## Installation
> composer require "wilr/silverstripe-googlesitemaps" > composer require "wilr/silverstripe-googlesitemaps"
If you're using Silverstripe 5 then version 3 or `dev-main` will work. If you're using Silverstripe 5 then version `3` or `dev-main` will work.
For Silverstripe 4 use the `2.x` branch line. For Silverstripe 4 use the `2.x` branch line.
@ -34,4 +34,4 @@ See docs/en for more information about configuring the module.
## Troubleshooting ## Troubleshooting
* Flush this route to ensure the changes take effect (e.g http://yoursite.com/sitemap.xml?flush=1) - Flush this route to ensure the changes take effect (e.g http://yoursite.com/sitemap.xml?flush=1)

10
_config.php Normal file
View File

@ -0,0 +1,10 @@
<?php
use SilverStripe\Core\ClassInfo;
use Wilr\GoogleSitemaps\GoogleSitemap;
if (0 === strpos(ltrim($_SERVER['REQUEST_URI'], '/'), 'sitemap')) {
foreach (ClassInfo::implementorsOf(Sitemapable::class) as $className) {
GoogleSitemap::register_dataobject($className);
}
}

View File

@ -30,42 +30,32 @@ manually, including requesting to have the page excluded from the sitemap.
Most module configuration is done via the SilverStripe Config API. Create a new Most module configuration is done via the SilverStripe Config API. Create a new
config file `mysite/_config/googlesitemaps.yml` with the following outline: config file `mysite/_config/googlesitemaps.yml` with the following outline:
--- ---
Name: customgooglesitemaps Name: customgooglesitemaps
After: googlesitemaps After: googlesitemaps
--- ---
Wilr\GoogleSitemaps\GoogleSitemap: Wilr\GoogleSitemaps\GoogleSitemap:
enabled: true
objects_per_sitemap: 1000 enabled: true
google_notification_enabled: false objects_per_sitemap: 1000
use_show_in_search: true google_notification_enabled: false
use_show_in_search: true
You can now alter any of those properties to set your needs. A popular option You can now alter any of those properties to set your needs. A popular option
is to turn on automatic pinging so that Google is notified of any updates to is to turn on automatic pinging so that Google is notified of any updates to
your page. You can set this in the file we created in the last paragraph by your page. You can set this in the file we created in the last paragraph by
editing the `google_notification_enabled` option to true editing the `google_notification_enabled` option to true
---
Name: customgooglesitemaps
After: googlesitemaps
---
Wilr\GoogleSitemaps\GoogleSitemap:
enabled: true
objects_per_sitemap: 1000
google_notification_enabled: true
use_show_in_search: true
### Bing Ping Support
To ping Bing whenever your sitemap is updated, set `bing_notification_enabled`
--- ---
Name: customgooglesitemaps Name: customgooglesitemaps
After: googlesitemaps After: googlesitemaps
--- ---
Wilr\GoogleSitemaps\GoogleSitemap: Wilr\GoogleSitemaps\GoogleSitemap:
enabled: true
bing_notification_enabled: true enabled: true
objects_per_sitemap: 1000
google_notification_enabled: true
use_show_in_search: true
### Including DataObjects ### Including DataObjects
@ -76,41 +66,40 @@ database as DataObject subclasses.
To include a DataObject instance in the Sitemap it requires that your subclass To include a DataObject instance in the Sitemap it requires that your subclass
defines two functions: defines two functions:
* AbsoluteLink() function which returns the URL for this DataObject - AbsoluteLink() function which returns the URL for this DataObject
* canView() function which returns a boolean value. - canView() function which returns a boolean value.
The following is a barebones example of a DataObject called 'MyDataObject'. It The following is a barebones example of a DataObject called 'MyDataObject'. It
assumes that you have a controller called 'MyController' which has a show method assumes that you have a controller called 'MyController' which has a show method
to show the DataObject by its ID. to show the DataObject by its ID.
<?php <?php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
class MyDataObject extends DataObject { class MyDataObject extends DataObject {
function canView($member = null) { function canView($member = null) {
return true; return true;
} }
function AbsoluteLink() { function AbsoluteLink() {
return Director::absoluteURL($this->Link()); return Director::absoluteURL($this->Link());
} }
function Link() {
return 'MyController/show/'. $this->ID;
}
}
function Link() {
return 'MyController/show/'. $this->ID;
}
}
After those methods have been defined on your DataObject you now need to tell After those methods have been defined on your DataObject you now need to tell
the Google Sitemaps module that it should be listed in the sitemap.xml file. To the Google Sitemaps module that it should be listed in the sitemap.xml file. To
do that, include the following in your _config.php file. do that, include the following in your \_config.php file.
use Wilr\GoogleSitemaps\GoogleSitemap; use Wilr\GoogleSitemaps\GoogleSitemap;
GoogleSitemap::register_dataobject('MyDataObject'); GoogleSitemap::register_dataobject('MyDataObject');
If you need to change the frequency of the indexing, you can pass the change If you need to change the frequency of the indexing, you can pass the change
frequency (daily, weekly, monthly) as a second parameter to register_dataobject(), So frequency (daily, weekly, monthly) as a second parameter to register_dataobject(), So
@ -118,7 +107,7 @@ instead of the previous code you would write:
use Wilr\GoogleSitemaps\GoogleSitemap; use Wilr\GoogleSitemaps\GoogleSitemap;
GoogleSitemap::register_dataobject('MyDataObject', 'daily'); GoogleSitemap::register_dataobject('MyDataObject', 'daily');
See the following blog post for more information: See the following blog post for more information:
@ -133,8 +122,26 @@ urls to include.
use Wilr\GoogleSitemaps\GoogleSitemap; use Wilr\GoogleSitemaps\GoogleSitemap;
GoogleSitemap::register_routes(array( GoogleSitemap::register_routes(array(
'/my-custom-controller/', '/my-custom-controller/',
'/Security/', '/Security/',
'/Security/login/' '/Security/login/'
)); ));
### Sitemapable
For automatic registration of a DataObject subclass, implement the `Sitemapable`
extension
```
<?php
class MyDataObject extends DataObject implements Sitemapable
{
public function AbsoluteLink()
{
// ..
}
}
```

View File

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<ruleset name="SilverStripe"> <ruleset name="SilverStripe">
<description>CodeSniffer ruleset for SilverStripe coding conventions.</description> <description>CodeSniffer ruleset for SilverStripe coding conventions.</description>
<file>src</file>
<file>tests</file>
<!-- base rules are PSR-2 --> <!-- base rules are PSR-2 -->
<rule ref="PSR2" > <rule ref="PSR2">
<!-- Current exclusions --> <!-- Current exclusions -->
<exclude name="PSR1.Methods.CamelCapsMethodName" /> <exclude name="PSR1.Methods.CamelCapsMethodName" />
</rule> </rule>

View File

@ -75,8 +75,7 @@ class GoogleSitemapController extends Controller
} }
} }
if ( if (GoogleSitemap::enabled()
GoogleSitemap::enabled()
&& $class && $class
&& ($page > 0) && ($page > 0)
&& ($class == SiteTree::class || $class == 'GoogleSitemapRoute' || GoogleSitemap::is_registered($class)) && ($class == SiteTree::class || $class == 'GoogleSitemapRoute' || GoogleSitemap::is_registered($class))

View File

@ -90,8 +90,7 @@ class GoogleSitemap
* @var array * @var array
*/ */
private static $search_indexes = [ private static $search_indexes = [
'google' => 'http://www.google.com/webmasters/sitemaps/ping?sitemap=', 'google' => 'http://www.google.com/webmasters/sitemaps/ping?sitemap='
'bing' => 'http://www.bing.com/ping?sitemap=',
]; ];
/** /**

14
src/Sitemapable.php Normal file
View File

@ -0,0 +1,14 @@
<?php
namespace Wilr\GoogleSitemaps;
interface Sitemapable
{
/**
* Return the absolute URL for this object
*
* @return string
*/
public function AbsoluteLink();
}

View File

@ -20,12 +20,22 @@ use Wilr\GoogleSitemaps\Tests\Model\UnviewableDataObject;
class GoogleSitemapTest extends FunctionalTest class GoogleSitemapTest extends FunctionalTest
{ {
protected static $fixture_file = [ protected static $fixture_file = [
'GoogleSitemapTest.yml', 'GoogleSitemapTest.yml'
'GoogleSitemapPageTest.yml',
]; ];
protected $usesDatabase = true; protected $usesDatabase = true;
public static function get_fixture_file()
{
$files = [__DIR__ . '/GoogleSitemapTest.yml'];
if (class_exists('Page')) {
$files[] = __DIR__ . '/GoogleSitemapPageTest.yml';
}
return $files;
}
protected static $extra_dataobjects = [ protected static $extra_dataobjects = [
TestDataObject::class, TestDataObject::class,
OtherDataObject::class, OtherDataObject::class,
@ -55,13 +65,13 @@ class GoogleSitemapTest extends FunctionalTest
public function testCanIncludeInGoogleSitemap(): void public function testCanIncludeInGoogleSitemap(): void
{ {
GoogleSitemap::register_dataobject(TestDataObject::class, ''); GoogleSitemap::register_dataobject(TestDataObject::class, '');
$unused = $this->objFromFixture(TestDataObject::class, 'UnindexedDataObject');
$this->assertFalse($unused->canIncludeInGoogleSitemap());
$used = $this->objFromFixture(TestDataObject::class, 'DataObjectTest2'); $used = $this->objFromFixture(TestDataObject::class, 'DataObjectTest2');
$this->assertTrue($used->canIncludeInGoogleSitemap()); $this->assertTrue($used->canIncludeInGoogleSitemap());
$used->setPrivate();
$this->assertFalse($used->canIncludeInGoogleSitemap());
} }
public function testIndexFileWithCustomRoute(): void public function testIndexFileWithCustomRoute(): void
@ -77,31 +87,29 @@ class GoogleSitemapTest extends FunctionalTest
public function testGetItems(): void public function testGetItems(): void
{ {
GoogleSitemap::register_dataobject(TestDataObject::class, ''); GoogleSitemap::register_dataobject(TestDataObject::class, '');
$google = new GoogleSitemap();
$items = GoogleSitemap::get_items(TestDataObject::class, 1); $items = $google->getItems(TestDataObject::class, 1);
$this->assertEquals(2, $items->count());
$this->assertListEquals(array( $this->assertEquals(3, $items->count());
array("Priority" => "0.2"),
array("Priority" => "0.4")
), $items);
GoogleSitemap::register_dataobject(OtherDataObject::class); GoogleSitemap::register_dataobject(OtherDataObject::class);
$this->assertEquals(1, GoogleSitemap::get_items(OtherDataObject::class, 1)->count()); $this->assertEquals(1, $google->getItems(OtherDataObject::class, 1)->count());
GoogleSitemap::register_dataobject(UnviewableDataObject::class); GoogleSitemap::register_dataobject(UnviewableDataObject::class);
$this->assertEquals(0, GoogleSitemap::get_items(UnviewableDataObject::class, 1)->count()); $this->assertEquals(0, $google->getItems(UnviewableDataObject::class, 1)->count());
} }
public function testGetItemsWithCustomRoutes(): void public function testGetItemsWithCustomRoutes(): void
{ {
GoogleSitemap::register_routes(array( GoogleSitemap::register_routes([
'/test-route/', '/test-route/',
'/someother-route/', '/someother-route/',
'/fake-sitemap-route/' '/fake-sitemap-route/'
)); ]);
$items = GoogleSitemap::get_items('GoogleSitemapRoute', 1); $google = new GoogleSitemap();
$items = $google->getItems('GoogleSitemapRoute', 1);
$this->assertEquals(3, $items->count()); $this->assertEquals(3, $items->count());
} }
@ -110,6 +118,16 @@ class GoogleSitemapTest extends FunctionalTest
GoogleSitemap::register_dataobject(TestDataObject::class); GoogleSitemap::register_dataobject(TestDataObject::class);
GoogleSitemap::register_dataobject(OtherDataObject::class); GoogleSitemap::register_dataobject(OtherDataObject::class);
$obj = $this->objFromFixture(TestDataObject::class, 'DataObjectTest1');
$table = $obj->baseTable();
DB::query("UPDATE \"" . $table . "\" SET \"LastEdited\"='2023-02-13 00:00:00'");
$obj2 = $this->objFromFixture(OtherDataObject::class, 'OtherDataObjectTest2');
$table = $obj2->baseTable();
DB::query("UPDATE \"" . $table . "\" SET \"LastEdited\"='2023-02-13 00:00:00'");
$response = $this->get('sitemap.xml'); $response = $this->get('sitemap.xml');
$body = $response->getBody(); $body = $response->getBody();
@ -153,9 +171,19 @@ class GoogleSitemapTest extends FunctionalTest
Config::inst()->set(GoogleSitemap::class, 'objects_per_sitemap', 1); Config::inst()->set(GoogleSitemap::class, 'objects_per_sitemap', 1);
GoogleSitemap::register_dataobject(TestDataObject::class); GoogleSitemap::register_dataobject(TestDataObject::class);
$obj = $this->objFromFixture(TestDataObject::class, 'DataObjectTest1');
$obj1 = $this->objFromFixture(TestDataObject::class, 'DataObjectTest2');
$obj2 = $this->objFromFixture(TestDataObject::class, 'UnindexedDataObject');
$table = $obj->baseTable();
DB::query("UPDATE \"" . $table . "\" SET \"LastEdited\"='2023-02-13 00:00:00' WHERE \"ID\"='" . $obj->ID . "'");
DB::query("UPDATE \"" . $table . "\" SET \"LastEdited\"='2023-02-13 00:00:00' WHERE \"ID\"='" . $obj1->ID . "'");
DB::query("UPDATE \"" . $table . "\" SET \"LastEdited\"='2023-02-13 00:00:00' WHERE \"ID\"='" . $obj2->ID . "'");
$response = $this->get('sitemap.xml'); $response = $this->get('sitemap.xml');
$body = $response->getBody(); $body = $response->getBody();
$this->assertXmlStringEqualsXmlFile(__DIR__ . '/xml/' . __FUNCTION__ . '.xml', $body); $this->assertXmlStringEqualsXmlFile(__DIR__ . '/xml/' . __FUNCTION__ . '.xml', $body);
Config::inst()->set(GoogleSitemap::class, 'objects_per_sitemap', $original); Config::inst()->set(GoogleSitemap::class, 'objects_per_sitemap', $original);
@ -164,11 +192,11 @@ class GoogleSitemapTest extends FunctionalTest
public function testRegisterRoutesIncludesAllRoutes(): void public function testRegisterRoutesIncludesAllRoutes(): void
{ {
GoogleSitemap::register_route('/test/'); GoogleSitemap::register_route('/test/');
GoogleSitemap::register_routes(array( GoogleSitemap::register_routes([
'/test/', // duplication should be replaced '/test/', // duplication should be replaced
'/unittests/', '/unittests/',
'/anotherlink/' '/anotherlink/'
), 'weekly'); ], 'weekly');
$response = $this->get('sitemap.xml/sitemap/GoogleSitemapRoute/1'); $response = $this->get('sitemap.xml/sitemap/GoogleSitemapRoute/1');
$body = $response->getBody(); $body = $response->getBody();
@ -193,7 +221,7 @@ class GoogleSitemapTest extends FunctionalTest
public function testGetItemsWithPages(): void public function testGetItemsWithPages(): void
{ {
if (!class_exists('Page')) { if (!class_exists(SiteTree::class)) {
$this->markTestIncomplete('No cms module installed, page related test skipped'); $this->markTestIncomplete('No cms module installed, page related test skipped');
} }
@ -205,10 +233,10 @@ class GoogleSitemapTest extends FunctionalTest
$page2->publishSingle(); $page2->publishSingle();
$page2->flushCache(); $page2->flushCache();
$this->assertListContains(array( $this->assertListContains([
array('Title' => 'Testpage1'), ['Title' => 'Testpage1'],
array('Title' => 'Testpage2') ['Title' => 'Testpage2']
), GoogleSitemap::inst()->getItems(SiteTree::class), "There should be 2 pages in the sitemap after publishing"); ], GoogleSitemap::inst()->getItems(SiteTree::class), "There should be 2 pages in the sitemap after publishing");
// check if we make a page readonly that it is hidden // check if we make a page readonly that it is hidden
$page2->CanViewType = 'LoggedInUsers'; $page2->CanViewType = 'LoggedInUsers';
@ -217,9 +245,9 @@ class GoogleSitemapTest extends FunctionalTest
$this->logOut(); $this->logOut();
$this->assertListEquals(array( $this->assertListEquals([
array('Title' => 'Testpage1') ['Title' => 'Testpage1']
), GoogleSitemap::inst()->getItems(SiteTree::class), "There should be only 1 page, other is logged in only"); ], GoogleSitemap::inst()->getItems(SiteTree::class), "There should be only 1 page, other is logged in only");
} }
public function testAccess(): void public function testAccess(): void

View File

@ -1,15 +1,15 @@
Wilr\GoogleSitemaps\Tests\Model\TestDataObject: Wilr\GoogleSitemaps\Tests\Model\TestDataObject:
DataObjectTest1: DataObjectTest1:
Priority: 0.4 Priority: 0.4
DataObjectTest2: DataObjectTest2:
Priority: 0.2 Priority: 0.2
UnindexedDataObject: UnindexedDataObject:
Priority: -1 Priority: -1
Wilr\GoogleSitemaps\Tests\Model\OtherDataObject: Wilr\GoogleSitemaps\Tests\Model\OtherDataObject:
OtherDataObjectTest2: OtherDataObjectTest2:
Priority: 0.3 Priority: 0.3
Wilr\GoogleSitemaps\Tests\Model\UnviewableDataObject: Wilr\GoogleSitemaps\Tests\Model\UnviewableDataObject:
Unviewable1: Unviewable1:
Priority: 0.4 Priority: 0.4

View File

@ -8,6 +8,7 @@ use SilverStripe\Control\Director;
class TestDataObject extends DataObject implements TestOnly class TestDataObject extends DataObject implements TestOnly
{ {
protected $private = false;
private static $db = array( private static $db = array(
'Priority' => 'Varchar(10)' 'Priority' => 'Varchar(10)'
@ -15,9 +16,19 @@ class TestDataObject extends DataObject implements TestOnly
public function canView($member = null) public function canView($member = null)
{ {
if ($this->private) {
return false;
}
return true; return true;
} }
public function setPrivate()
{
$this->private = true;
}
public function AbsoluteLink() public function AbsoluteLink()
{ {
return Director::absoluteBaseURL(); return Director::absoluteBaseURL();