Compare commits
258 Commits
Author | SHA1 | Date |
---|---|---|
Will Rossiter | 794778ae20 | |
Sukhwinder Singh | 05e6963f04 | |
Will Rossiter | 3f75c06a77 | |
jareddreyerss | a13d3dd9de | |
Will Rossiter | 59acd419ef | |
Sunny Side Up | aeeb960566 | |
Will Rossiter | c7f260bb33 | |
Brooke Lord | c2f5162e97 | |
Brooke Lord | 59e56c3b69 | |
Brooke Lord | d9da059a83 | |
Brooke Lord | 54ddc6ccf7 | |
Will Rossiter | cbccf88a62 | |
Will Rossiter | 8a7c8aa9cc | |
Will Rossiter | def6be347c | |
Will Rossiter | 523d196d12 | |
Mohamed Alsharaf | f064330acd | |
Will Rossiter | f1bddb626d | |
Luke Fromhold | 77a366871e | |
Will Rossiter | 81b8d870a4 | |
Will Rossiter | 9b0e0f1064 | |
Will Rossiter | 7370c85dae | |
Tyler | bf6156c103 | |
Will Rossiter | a29de80408 | |
Will Rossiter | 0fbfec9f66 | |
Will Rossiter | 54e5043622 | |
Mohamed Alsharaf | 286115174d | |
Will Rossiter | cfcb002cf3 | |
Jan Metzger | 0e965ecd46 | |
Will Rossiter | cb56ac11d9 | |
Will Rossiter | 6adb11b5bf | |
sinan-evanshunt | 8d321bd0a1 | |
terry | 35665402cb | |
Will Rossiter | 7326028c7c | |
Will Rossiter | 401f723e7e | |
Will Rossiter | 71e62966d0 | |
Lukas Erni | 1915f9ab59 | |
Lukas Erni | 1d535ade02 | |
Will Rossiter | 56b25a424f | |
Will Rossiter | 67e8d601d5 | |
Will Rossiter | 67c8ad044e | |
Will Rossiter | a333b56711 | |
Will Rossiter | 745de7a7af | |
Will Rossiter | b5f322646f | |
Will Rossiter | 312b78f88b | |
Florian Thoma | f026001f8b | |
Florian Thoma | ced2213be4 | |
Will Rossiter | 17b21683a2 | |
Tomasz Pirc | 7668ba8f48 | |
Will Rossiter | 4214c091e5 | |
Christopher Darling | 799f669da2 | |
Will Rossiter | 982d96c203 | |
Pavol Tuka | 56ef63f6c3 | |
Will Rossiter | c1fc291e59 | |
Will Rossiter | 36c9b9f1d4 | |
Will Rossiter | 1397fa0e30 | |
Will Rossiter | 473b88ee05 | |
Andreas Piening | 2caacf8e86 | |
Will Rossiter | 7f8937dd1f | |
Will Rossiter | 764a8f5bb2 | |
Will Rossiter | d8beb33d86 | |
Will Rossiter | 2860c390b0 | |
Jarkko Linnanvirta | 41390002d5 | |
Will Rossiter | 710b8c3518 | |
Will Rossiter | 4d131b06ac | |
Andy Karwal | f7b48e2b99 | |
Will Rossiter | 71c662e4d1 | |
Will Rossiter | 64ead69f4e | |
Will Rossiter | 572da5fa8a | |
Will Rossiter | a0a8c34d2f | |
Will Rossiter | adb3ed94fc | |
Daniel Hensby | c3fcd84542 | |
Daniel Hensby | 776e6bd9d9 | |
Daniel Hensby | 36091bc840 | |
Daniel Hensby | f343f312eb | |
Daniel Hensby | 6e49a3e01e | |
Daniel Hensby | b42305dfef | |
Daniel Hensby | 729311249c | |
Daniel Hensby | 326b1ce4b0 | |
Daniel Hensby | 98f65de824 | |
Will Rossiter | 628dbd8404 | |
Nivanka Fonseka | c5388b6e5b | |
Nivanka Fonseka | 29be1bc504 | |
Will Rossiter | c00f420a8b | |
Will Rossiter | 36ef636249 | |
zanderwar | e51dd3877f | |
Daniel Hensby | f51c9e12bf | |
Daniel Hensby | 64a9465d7e | |
Daniel Hensby | bfaaa42e10 | |
Robbie Averill | d5b35cf36c | |
Robbie Averill | b11c6d771e | |
Robbie Averill | 450283bd1b | |
Robbie Averill | 1d8c6faa2e | |
Robbie Averill | d1d5c2b268 | |
Will Rossiter | 9e7aed0f2d | |
Will Rossiter | 742724dad4 | |
Will Rossiter | 38c489c931 | |
Will Rossiter | 0daa163ec1 | |
Will Rossiter | c8e142718d | |
Will Rossiter | 2699956b23 | |
Danae Miller-Clendon | 27898476d8 | |
Will Rossiter | 71567fec58 | |
Will Rossiter | d88ed57a07 | |
tim | 030dd83613 | |
Will Rossiter | b40a66422b | |
Will Rossiter | 313e4649c1 | |
Will Rossiter | 840f664cd9 | |
Will Rossiter | 56b23dbdf8 | |
Will Rossiter | 77f6a02ae7 | |
Will Rossiter | 526a0eec4d | |
Will Rossiter | e0821f8f03 | |
Will Rossiter | 830abcc4f9 | |
Matt Peel | fa92f0d13f | |
Will Rossiter | 8307b56f3f | |
digitall-it | d8f8cafd2d | |
Mike Cochrane | b8766edbee | |
Mike Cochrane | 9f845f5af7 | |
Mike Cochrane | 4333bb6777 | |
Will Rossiter | 51fa6f4c25 | |
Loz Calver | 671522de5f | |
Loz Calver | 286c79160a | |
Will Rossiter | 552684ce12 | |
Will Rossiter | 930fd7f9ac | |
Will Rossiter | e607ea9fc4 | |
Will Rossiter | 31f9182e95 | |
Will Rossiter | 31a5bab455 | |
Will Rossiter | c031c58774 | |
Will Rossiter | 083251bd6d | |
Will Rossiter | bb7fbb8308 | |
UndefinedOffset | cc0c20b3a8 | |
UndefinedOffset | 26838c70d0 | |
Will Rossiter | 49053f7d8f | |
Taras Yemtsov | 02197a35b0 | |
Will Rossiter | 11a894a10e | |
Mike Cochrane | 4701bd985a | |
Will Rossiter | 6fd2901aaf | |
Nik Rolls | ecd3551ec0 | |
Will Rossiter | 356350ede5 | |
Will Rossiter | 9f461b8d7c | |
Will Rossiter | 90ddb6c98f | |
Daniel Hensby | b7da114f99 | |
Patrick Nelson | 5228024ba5 | |
helpfulrobot | 7e42cbef76 | |
Will Rossiter | 99282db32a | |
helpfulrobot | 9c4e8be4bd | |
Will Rossiter | 9f6e1bab41 | |
helpfulrobot | 0cfd3d80f5 | |
Damian Mooyman | 289ed77139 | |
helpfulrobot | 7af5077d99 | |
Damian Mooyman | 5d10786341 | |
helpfulrobot | 41238f5186 | |
Will Rossiter | dd41e39484 | |
Patrick Nelson | a641ded926 | |
Patrick Nelson | e2a1dc5250 | |
Will Rossiter | c549ca4d3f | |
Ed Linklater | 4e1db4d3f9 | |
Ed Linklater | 58376e277c | |
Will Rossiter | a1ac8d229a | |
Will Rossiter | 82778473a8 | |
Daniel Hensby | 0ffc6ac2be | |
Damian Mooyman | 26f1bbefdd | |
Daniel Hensby | 40a3a0adeb | |
Will Rossiter | f9202c28ad | |
Kirk Mayo | 3a3de779af | |
Will Rossiter | 092b2e10ae | |
Simon Welsh | 91a543ea63 | |
Will Rossiter | 6d530af1a2 | |
Elvinas L. | 744d879efb | |
Will Rossiter | d3a12fe225 | |
bob (Plastyk) | 5c2547a3a3 | |
Will Rossiter | 05e62cbbc8 | |
Max Girkens | a87116b3c3 | |
Will Rossiter | 5f26b65061 | |
Nick Spiel | 096138d594 | |
Will Rossiter | edfcd22021 | |
Nick | 92bcb27cac | |
Will Rossiter | e67f807f8a | |
Lukas | eeac25c93e | |
Sean Harvey | 27e79a4e02 | |
Lukas | bbd5e29630 | |
Will Rossiter | 198a850f8d | |
Will Rossiter | 12e46e5b2b | |
Will Rossiter | 8ebf4e9f22 | |
Will Rossiter | 3b3742a1bc | |
Damian Mooyman | 5f06483b73 | |
Jyrki Lilja | 708cb5d166 | |
Jyrki Lilja | ba69d670e5 | |
Will Rossiter | a2297c43ef | |
ARNHOE | 3a1a99cbd5 | |
Will Rossiter | 1416ad24ae | |
Thomas Speak | e385b2f80c | |
Thomas Speak | 9dcc00bafd | |
Will Rossiter | 8403cb5773 | |
Thomas Speak | b0cc13c9ed | |
Ingo Schommer | 7ba2e67a17 | |
Loz Calver | 48b5c0db70 | |
Will Rossiter | affee1788f | |
g4b0 | 9b7c14abc4 | |
Will Rossiter | 92615299b7 | |
Mark-M | cc215ace29 | |
Ingo Schommer | a233c2d514 | |
Will Rossiter | eadc960341 | |
Damian Mooyman | 1f7c2e7b73 | |
Ingo Schommer | 0b832f968e | |
Ingo Schommer | 9a95e58d6e | |
Ingo Schommer | ac1e61faae | |
Will Rossiter | c6119d3de9 | |
Will Rossiter | c69a4a58ae | |
Will Rossiter | a015e227d9 | |
Ryan Wachtl | 7c3bde4d9f | |
Julian Seidenberg | de283133fe | |
ARNHOE | 37e8101c04 | |
Will Rossiter | d42fba7906 | |
Will Rossiter | 1c5e737d06 | |
Ingo Schommer | 747059b6b7 | |
Will Rossiter | 8bbc14ee23 | |
Ingo Schommer | 90f3c82660 | |
Ingo Schommer | d5f673272e | |
Will Rossiter | 6cc800a0db | |
hdrlab | 620e09bd7c | |
Will Rossiter | 3785409b3d | |
Will Rossiter | f41fd87624 | |
Will Rossiter | 706c06e8a0 | |
Will Rossiter | a20e84ca9b | |
Fred Condo | bc1f3b3a81 | |
Will Rossiter | c9b4c3f88e | |
Fred Condo | 378258b0e3 | |
Will Rossiter | 079b949bf2 | |
Hendrik Schaper | 41ce3396fa | |
Will Rossiter | 6ce0524d4c | |
Andrew O'Neil | 8d787c486e | |
Will Rossiter | d67270fe7a | |
Will Rossiter | 929e97accf | |
Will Rossiter | 5bc3e07be6 | |
Will Rossiter | df9b224937 | |
Will Rossiter | c5c92abad2 | |
Will Rossiter | a72dd1a094 | |
Jörn Röder | 8a41b6fdf2 | |
Jörn Röder | e98598ac79 | |
Will Rossiter | 39828c215d | |
UndefinedOffset | e5e2ab57f2 | |
Will Rossiter | a813a471f9 | |
Will Rossiter | 872fd9b258 | |
Will Rossiter | 295aba26e6 | |
Ed | 05d7bb8f82 | |
Roland Lehmann | e801e91636 | |
Will Rossiter | 65c59fc020 | |
Roland Lehmann | 805c7e2ee4 | |
Roland Lehmann | 74de83227a | |
Roland Lehmann | a9019b9099 | |
Roland Lehmann | c841987427 | |
Roland Lehmann | 54929f3e50 | |
rlehmann | 9c0d50e726 | |
Roland Lehmann | 4ca8402508 | |
Will Rossiter | c62538a860 | |
Will Rossiter | ef019199c4 | |
Blaž | f1129a7d2d | |
wrossiter | cb812b5c98 | |
wrossiter | 8886348de0 |
|
@ -0,0 +1,17 @@
|
|||
# For more information about the properties used in this file,
|
||||
# please see the EditorConfig documentation:
|
||||
# http://editorconfig.org
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[{*.yml,package.json}]
|
||||
indent_size = 2
|
||||
|
||||
# The indent size used in the package.json file cannot be changed:
|
||||
# https://github.com/npm/npm/pull/3180#issuecomment-16336516
|
|
@ -0,0 +1,4 @@
|
|||
/tests export-ignore
|
||||
/docs export-ignore
|
||||
/.gitignore export-ignore
|
||||
/.travis.yml export-ignore
|
|
@ -0,0 +1,11 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
name: CI
|
||||
uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1
|
|
@ -0,0 +1,4 @@
|
|||
.DS_Store
|
||||
/vendor
|
||||
/public
|
||||
/composer.lock
|
|
@ -0,0 +1,2 @@
|
|||
mappings:
|
||||
GoogleSitemap: Wilr\GoogleSitemaps\GoogleSitemap
|
37
LICENSE
37
LICENSE
|
@ -1,24 +1,13 @@
|
|||
* Copyright (c) 2008, Silverstripe Ltd.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name of the <organization> nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY Silverstripe Ltd. ``AS IS'' AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL Silverstripe Ltd. BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2020, Fullscreen Interactive All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
22
README
22
README
|
@ -1,22 +0,0 @@
|
|||
###############################################
|
||||
Google Sitemaps Module
|
||||
###############################################
|
||||
|
||||
Maintainer Contact
|
||||
-----------------------------------------------
|
||||
Will Rossiter
|
||||
<will (at) silverstripe (dot) com>
|
||||
|
||||
Requirements
|
||||
-----------------------------------------------
|
||||
SilverStripe 2.4 or newer
|
||||
|
||||
Documentation
|
||||
-----------------------------------------------
|
||||
http://doc.silverstripe.com/doku.php?id=modules:googlesitemaps
|
||||
|
||||
Installation Instructions
|
||||
-----------------------------------------------
|
||||
|
||||
Usage Overview
|
||||
-----------------------------------------------
|
|
@ -0,0 +1,37 @@
|
|||
# Google Sitemaps Module
|
||||
|
||||
[![Build Status](http://img.shields.io/travis/wilr/silverstripe-googlesitemaps.svg?style=flat-square)](http://travis-ci.org/wilr/silverstripe-googlesitemaps)
|
||||
[![Version](http://img.shields.io/packagist/v/wilr/silverstripe-googlesitemaps.svg?style=flat-square)](https://packagist.org/packages/wilr/silverstripe-googlesitemaps)
|
||||
[![License](http://img.shields.io/packagist/l/wilr/silverstripe-googlesitemaps.svg?style=flat-square)](LICENSE.md)
|
||||
|
||||
## Maintainer Contact
|
||||
|
||||
- Will Rossiter (Nickname: wrossiter, willr) <will@fullscreen.io>
|
||||
|
||||
## Installation
|
||||
|
||||
> composer require "wilr/silverstripe-googlesitemaps"
|
||||
|
||||
If you're using Silverstripe 5 then version `3` or `dev-main` will work.
|
||||
|
||||
For Silverstripe 4 use the `2.x` branch line.
|
||||
|
||||
## Documentation
|
||||
|
||||
Provides support for the [Sitemaps XML Protocol](http://www.sitemaps.org/protocol.html),
|
||||
enabling Google, Bing and other search engines to index the web pages on your
|
||||
site. This helps your SilverStripe website rank well in search engines, and to
|
||||
encourage the information on your site to be discovered by Google quickly.
|
||||
|
||||
Any new pages published or unpublished on your website automatically update the
|
||||
Sitemap.
|
||||
|
||||
The XML Sitemap can be accessed by going to http://yoursite.com/sitemap.xml
|
||||
|
||||
## Usage Overview
|
||||
|
||||
See docs/en for more information about configuring the module.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Flush this route to ensure the changes take effect (e.g http://yoursite.com/sitemap.xml?flush=1)
|
14
_config.php
14
_config.php
|
@ -1,10 +1,10 @@
|
|||
<?php
|
||||
|
||||
// adds a rule to make www.site.com/sitemap.xml work
|
||||
Director::addRules(10, array(
|
||||
'sitemap.xml' => 'GoogleSitemap',
|
||||
));
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use Wilr\GoogleSitemaps\GoogleSitemap;
|
||||
|
||||
// add the extension
|
||||
Object::add_extension('SiteTree', 'GoogleSitemapDecorator');
|
||||
?>
|
||||
if (0 === strpos(ltrim($_SERVER['REQUEST_URI'], '/'), 'sitemap')) {
|
||||
foreach (ClassInfo::implementorsOf(Sitemapable::class) as $className) {
|
||||
GoogleSitemap::register_dataobject($className);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
Name: googlesitemaps
|
||||
---
|
||||
Wilr\GoogleSitemaps\GoogleSitemap:
|
||||
enabled: true
|
||||
objects_per_sitemap: 1000
|
||||
use_show_in_search: true
|
||||
---
|
||||
Only:
|
||||
classexists: SilverStripe\CMS\Model\SiteTree
|
||||
---
|
||||
SilverStripe\CMS\Model\SiteTree:
|
||||
extensions:
|
||||
- Wilr\GoogleSitemaps\Extensions\GoogleSitemapSiteTreeExtension
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
Name: googlesitemaproutes
|
||||
---
|
||||
SilverStripe\Control\Director:
|
||||
rules:
|
||||
'sitemap.xml': 'Wilr\GoogleSitemaps\Control\GoogleSitemapController'
|
|
@ -0,0 +1 @@
|
|||
When having discussions about this module in issues or pull request please adhere to the [SilverStripe Community Code of Conduct](https://docs.silverstripe.org/en/contributing/code_of_conduct).
|
|
@ -1,159 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Initial implementation of Sitemap support.
|
||||
* GoogleSitemap should handle requests to 'sitemap.xml'
|
||||
* the other two classes are used to render the sitemap.
|
||||
*
|
||||
* You can notify ("ping") Google about a changed sitemap
|
||||
* automatically whenever a new page is published or unpublished.
|
||||
* By default, Google is not notified, and will pick up your new
|
||||
* sitemap whenever the GoogleBot visits your website.
|
||||
*
|
||||
* Enabling notification of Google after every publish (in your _config.php):
|
||||
* <example
|
||||
* GoogleSitemap::enable_google_notificaton();
|
||||
* </example>
|
||||
*
|
||||
* @see http://www.google.com/support/webmasters/bin/answer.py?hl=en&answer=34609
|
||||
*
|
||||
* @package googlesitemaps
|
||||
*/
|
||||
class GoogleSitemap extends Controller {
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
protected static $enabled = true;
|
||||
|
||||
/**
|
||||
* @var DataObjectSet
|
||||
*/
|
||||
protected $Pages;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
protected static $google_notification_enabled = false;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
protected static $use_show_in_search = true;
|
||||
|
||||
public function Items() {
|
||||
$filter = '';
|
||||
|
||||
$bt = defined('DB::USE_ANSI_SQL') ? "\"" : "`";
|
||||
if(self::$use_show_in_search) {
|
||||
$filter = "{$bt}ShowInSearch{$bt} = 1";
|
||||
}
|
||||
|
||||
$this->Pages = Versioned::get_by_stage('SiteTree', 'Live', $filter);
|
||||
|
||||
$newPages = new DataObjectSet();
|
||||
if($this->Pages) {
|
||||
foreach($this->Pages as $page) {
|
||||
// Only include pages from this host and pages which are not an instance of ErrorPage
|
||||
// We prefix $_SERVER['HTTP_HOST'] with 'http://' so that parse_url to help parse_url identify the host name component; we could use another protocol (like
|
||||
// 'ftp://' as the prefix and the code would work the same.
|
||||
if(parse_url($page->AbsoluteLink(), PHP_URL_HOST) == parse_url('http://' . $_SERVER['HTTP_HOST'], PHP_URL_HOST) && !($page instanceof ErrorPage)) {
|
||||
|
||||
// If the page has been set to 0 priority, we set a flag so it won't be included
|
||||
if($page->canView() && (!isset($page->Priority) || $page->Priority > 0)) {
|
||||
// The one field that isn't easy to deal with in the template is
|
||||
// Change frequency, so we set that here.
|
||||
$properties = $page->toMap();
|
||||
$created = new SS_Datetime();
|
||||
$created->value = $properties['Created'];
|
||||
$now = new SS_Datetime();
|
||||
$now->value = date('Y-m-d H:i:s');
|
||||
$versions = $properties['Version'];
|
||||
$timediff = $now->format('U') - $created->format('U');
|
||||
|
||||
// Check how many revisions have been made over the lifetime of the
|
||||
// Page for a rough estimate of it's changing frequency.
|
||||
|
||||
$period = $timediff / ($versions + 1);
|
||||
|
||||
if($period > 60*60*24*365) { // > 1 year
|
||||
$page->ChangeFreq='yearly';
|
||||
} elseif($period > 60*60*24*30) { // > ~1 month
|
||||
$page->ChangeFreq='monthly';
|
||||
} elseif($period > 60*60*24*7) { // > 1 week
|
||||
$page->ChangeFreq='weekly';
|
||||
} elseif($period > 60*60*24) { // > 1 day
|
||||
$page->ChangeFreq='daily';
|
||||
} elseif($period > 60*60) { // > 1 hour
|
||||
$page->ChangeFreq='hourly';
|
||||
} else { // < 1 hour
|
||||
$page->ChangeFreq='always';
|
||||
}
|
||||
|
||||
$newPages->push($page);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $newPages;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies Google about changes to your sitemap.
|
||||
* Triggered automatically on every publish/unpublish of a page.
|
||||
* This behaviour is disabled by default, enable with:
|
||||
* GoogleSitemap::enable_google_notificaton();
|
||||
*
|
||||
* If the site is in "dev-mode", no ping will be sent regardless wether
|
||||
* the Google notification is enabled.
|
||||
*
|
||||
* @return string Response text
|
||||
*/
|
||||
static function ping() {
|
||||
if(!self::$enabled) return false;
|
||||
|
||||
//Don't ping if the site has disabled it, or if the site is in dev mode
|
||||
if(!GoogleSitemap::$google_notification_enabled || Director::isDev())
|
||||
return;
|
||||
|
||||
$location = urlencode(Director::absoluteBaseURL() . '/sitemap.xml');
|
||||
|
||||
$response = HTTP::sendRequest("www.google.com", "/webmasters/sitemaps/ping",
|
||||
"sitemap=" . $location);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable pings to google.com whenever sitemap changes.
|
||||
*/
|
||||
public static function enable_google_notification() {
|
||||
self::$google_notification_enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables pings to google when the sitemap changes.
|
||||
*/
|
||||
public static function disable_google_notification() {
|
||||
self::$google_notification_enabled = false;
|
||||
}
|
||||
|
||||
function index($url) {
|
||||
if(self::$enabled) {
|
||||
SSViewer::set_source_file_comments(false);
|
||||
$this->getResponse()->addHeader('Content-Type', 'application/xml; charset="utf-8"');
|
||||
|
||||
// But we want to still render.
|
||||
return array();
|
||||
} else {
|
||||
return new SS_HTTPResponse('Not allowed', 405);
|
||||
}
|
||||
}
|
||||
|
||||
public static function enable() {
|
||||
self::$enabled = true;
|
||||
}
|
||||
|
||||
public static function disable() {
|
||||
self::$enabled = false;
|
||||
}
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Decorate the page object to provide google sitemaps with
|
||||
* additionally options and configuration.
|
||||
*
|
||||
* @package googlesitemaps
|
||||
*/
|
||||
class GoogleSitemapDecorator extends SiteTreeDecorator {
|
||||
|
||||
function extraStatics() {
|
||||
return array(
|
||||
'db' => array(
|
||||
"Priority" => "Varchar(5)",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function updateCMSFields(&$fields) {
|
||||
$pagePriorities = array(
|
||||
'' => _t('SiteTree.PRIORITYAUTOSET','Auto-set based on page depth'),
|
||||
'-1' => _t('SiteTree.PRIORITYNOTINDEXED', "Not indexed"), // We set this to -ve one because a blank value implies auto-generation of Priority
|
||||
'1.0' => '1 - ' . _t('SiteTree.PRIORITYMOSTIMPORTANT', "Most important"),
|
||||
'0.9' => '2',
|
||||
'0.8' => '3',
|
||||
'0.7' => '4',
|
||||
'0.6' => '5',
|
||||
'0.5' => '6',
|
||||
'0.4' => '7',
|
||||
'0.3' => '8',
|
||||
'0.2' => '9',
|
||||
'0.1' => '10 - ' . _t('SiteTree.PRIORITYLEASTIMPORTANT', "Least important")
|
||||
);
|
||||
|
||||
$tabset = $fields->findOrMakeTab('Root.Content');
|
||||
$tabset->push(
|
||||
$addTab = new Tab(
|
||||
'GoogleSitemap',
|
||||
_t('SiteTree.TABGOOGLESITEMAP', 'Google Sitemap'),
|
||||
new LiteralField(
|
||||
"GoogleSitemapIntro",
|
||||
"<p>" .
|
||||
sprintf(
|
||||
_t(
|
||||
'SiteTree.METANOTEPRIORITY',
|
||||
"Manually specify a Google Sitemaps priority for this page (%s)"
|
||||
),
|
||||
'<a href="http://www.google.com/support/webmasters/bin/answer.py?hl=en&answer=71936#prioritize" target="_blank">?</a>'
|
||||
) .
|
||||
"</p>"
|
||||
),
|
||||
new DropdownField("Priority", $this->owner->fieldLabel('Priority'), $pagePriorities)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function updateFieldLabels(&$labels) {
|
||||
parent::updateFieldLabels($labels);
|
||||
|
||||
$labels['Priority'] = _t('SiteTree.METAPAGEPRIO', "Page Priority");
|
||||
}
|
||||
|
||||
function onAfterPublish() {
|
||||
GoogleSitemap::ping();
|
||||
}
|
||||
|
||||
function onAfterUnpublish() {
|
||||
GoogleSitemap::ping();
|
||||
}
|
||||
|
||||
/**
|
||||
* The default value of the priority field depends on the depth of the page in
|
||||
* the site tree, so it must be calculated dynamically.
|
||||
*/
|
||||
function getPriority() {
|
||||
if(!$this->owner->getField('Priority')) {
|
||||
$parentStack = $this->owner->parentStack();
|
||||
$numParents = is_array($parentStack) ? count($parentStack) - 1: 0;
|
||||
return max(0.1, 1.0 - ($numParents / 10));
|
||||
} elseif($this->owner->getField('Priority') == -1) {
|
||||
return 0;
|
||||
} else {
|
||||
return $this->owner->getField('Priority');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"name": "wilr/silverstripe-googlesitemaps",
|
||||
"description": "SilverStripe support for the Google Sitemaps XML, enabling Google and other search engines to see all urls on your site. This helps your SilverStripe website rank well in search engines, and to encourage the information on your site to be discovered quickly.",
|
||||
"type": "silverstripe-vendormodule",
|
||||
"keywords": [
|
||||
"silverstripe",
|
||||
"googlesitemaps",
|
||||
"seo"
|
||||
],
|
||||
"homepage": "https://github.com/wilr/silverstripe-googlesitemaps",
|
||||
"license": "BSD-3-Clause",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Will Rossiter",
|
||||
"email": "will@fullscreen.io"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"silverstripe/framework": "^5"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"squizlabs/php_codesniffer": "^3"
|
||||
},
|
||||
"replace": {
|
||||
"silverstripe/googlesitemaps": "*"
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "3.x-dev"
|
||||
},
|
||||
"expose": [
|
||||
"images",
|
||||
"css"
|
||||
]
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Wilr\\GoogleSitemaps\\": "src/",
|
||||
"Wilr\\GoogleSitemaps\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"prefer-stable": true,
|
||||
"minimum-stability": "dev",
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"composer/installers": true,
|
||||
"silverstripe/vendor-plugin": true,
|
||||
"silverstripe/recipe-plugin": true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
:root {
|
||||
--color-black: #292B2E;
|
||||
--color-blue-bright: #005AE1;
|
||||
--color-blue-dark: #003E94;
|
||||
--color-blue-light: #E6EDF4;
|
||||
--color-grey-dark: #595D64;
|
||||
--color-grey-light: #f7f8f8;
|
||||
--color-grey-medium: #DCDEE0;
|
||||
--color-white: #fff;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 10px;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body, table {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--color-white);
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
color: var(--color-black);
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 0 auto;
|
||||
max-width: 122rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.content__title {
|
||||
align-items: center;
|
||||
color: var(--color-black);
|
||||
display: inline-flex;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 3rem;
|
||||
}
|
||||
|
||||
.content__title::before {
|
||||
content: "";
|
||||
background: url("../images/cms_logo.svg") left center no-repeat;
|
||||
border-right: 0.1rem solid var(--color-grey-medium);
|
||||
display: inline-block;
|
||||
height: 3.2rem;
|
||||
margin-right: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
width: 3.2rem;
|
||||
}
|
||||
|
||||
.content__text {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.content__text a {
|
||||
color: var(--color-blue-bright);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.content__text a:hover {
|
||||
color: var(--color-blue-dark);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
border-top: 0.1rem solid var(--color-grey-medium);
|
||||
margin: 3rem 0 5rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.table__cell {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.table__cell--w-10 {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.table__cell--w-15 {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.table__cell--w-65 {
|
||||
width: 65%;
|
||||
}
|
||||
|
||||
.table__cell--w-85 {
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(odd) td {
|
||||
background-color: var(--color-grey-light);
|
||||
}
|
||||
|
||||
.table tbody tr:hover td {
|
||||
background-color: var(--color-blue-light);
|
||||
}
|
||||
|
||||
.table a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.image-table__cell {
|
||||
padding: 1rem 1rem 0 0;
|
||||
}
|
||||
|
||||
.image-table__cell--image {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.image-table__cell--text {
|
||||
width: calc(100% - 6rem);
|
||||
}
|
||||
|
||||
.image-table__image {
|
||||
height: 4rem;
|
||||
object-fit: cover;
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.image-table__title {
|
||||
display: block;
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
# Google Sitemaps Module
|
||||
|
||||
Silverstripe CMS provides support for the Google Sitemaps XML system, enabling
|
||||
Google and other search engines to see all pages on your site. This helps
|
||||
your Silverstripe CMS website rank well in search engines, and to encourage the
|
||||
information on your site to be discovered by Google quickly.
|
||||
|
||||
Therefore, all Silverstripe CMS websites contain a special controller which can
|
||||
be visited: http://yoursite.com/sitemap.xml. This is not a file directly, but
|
||||
rather a custom route which points to the GoogleSitemap controller.
|
||||
|
||||
See http://en.wikipedia.org/wiki/Sitemaps for info on the Google Sitemap
|
||||
format.
|
||||
|
||||
Whenever you publish a new or republish an existing page, Silverstripe CMS can
|
||||
automatically inform Google of the change, encouraging a Google to take notice.
|
||||
If you install the Silverstripe CMS Google Analytics module, you can see if Google
|
||||
has updated your page as a result.
|
||||
|
||||
By default, Silverstripe CMS informs Google that the importance of a page depends
|
||||
on its position of in the sitemap. "Top level" pages are most important, and
|
||||
the deeper a page is nested, the less important it is. (For each level,
|
||||
Importance drops from 1.0, to 0.9, to 0.8, and so on, until 0.1 is reached).
|
||||
|
||||
In the CMS, in the Settings tab for each page, you can set the importance
|
||||
manually, including requesting to have the page excluded from the sitemap.
|
||||
|
||||
## Configuration
|
||||
|
||||
Most module configuration is done via the Silverstripe CMS Config API. Create a
|
||||
new config file `mysite/_config/googlesitemaps.yml` with the following outline:
|
||||
|
||||
```yml
|
||||
---
|
||||
Name: customgooglesitemaps
|
||||
After: googlesitemaps
|
||||
---
|
||||
Wilr\GoogleSitemaps\GoogleSitemap:
|
||||
enabled: true
|
||||
objects_per_sitemap: 1000
|
||||
use_show_in_search: true
|
||||
```
|
||||
|
||||
You can now alter any of those properties to set your needs.
|
||||
|
||||
```yml
|
||||
---
|
||||
Name: customgooglesitemaps
|
||||
After: googlesitemaps
|
||||
---
|
||||
Wilr\GoogleSitemaps\GoogleSitemap:
|
||||
enabled: true
|
||||
objects_per_sitemap: 1000
|
||||
use_show_in_search: true
|
||||
```
|
||||
|
||||
### Including DataObjects
|
||||
|
||||
The module provides support for including DataObject subclasses as pages in the
|
||||
SiteTree such as comments, forum posts and other pages which are stored in your
|
||||
database as DataObject subclasses.
|
||||
|
||||
To include a DataObject instance in the Sitemap it requires that your subclass
|
||||
defines two functions:
|
||||
|
||||
- AbsoluteLink() function which returns the URL for this DataObject
|
||||
- canView() function which returns a boolean value.
|
||||
|
||||
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
|
||||
to show the DataObject by its ID.
|
||||
|
||||
<?php
|
||||
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Control\Director;
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
|
||||
function canView($member = null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function AbsoluteLink() {
|
||||
return Director::absoluteURL($this->Link());
|
||||
}
|
||||
|
||||
function Link() {
|
||||
return 'MyController/show/'. $this->ID;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
do that, include the following in your \_config.php file.
|
||||
|
||||
use Wilr\GoogleSitemaps\GoogleSitemap;
|
||||
|
||||
GoogleSitemap::register_dataobject('MyDataObject');
|
||||
|
||||
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
|
||||
instead of the previous code you would write:
|
||||
|
||||
use Wilr\GoogleSitemaps\GoogleSitemap;
|
||||
|
||||
GoogleSitemap::register_dataobject('MyDataObject', 'daily');
|
||||
|
||||
See the following blog post for more information:
|
||||
|
||||
http://www.silvercart.org/blog/dataobjects-and-googlesitemaps/
|
||||
|
||||
### Including custom routes
|
||||
|
||||
Occasionally you may have a need to include custom URLs in your sitemap for
|
||||
your Controllers and other pages which don't exist in the database. To update
|
||||
the sitemap to include those links call register_routes() with your array of
|
||||
URLs to include.
|
||||
|
||||
use Wilr\GoogleSitemaps\GoogleSitemap;
|
||||
|
||||
GoogleSitemap::register_routes(array(
|
||||
'/my-custom-controller/',
|
||||
'/Security/',
|
||||
'/Security/login/'
|
||||
));
|
||||
|
||||
### Sitemapable
|
||||
|
||||
For automatic registration of a DataObject subclass, implement the `Sitemapable`
|
||||
extension.
|
||||
|
||||
```
|
||||
<?php
|
||||
|
||||
|
||||
class MyDataObject extends DataObject implements Sitemapable
|
||||
{
|
||||
public function AbsoluteLink()
|
||||
{
|
||||
// ..
|
||||
}
|
||||
}
|
||||
```
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="32" height="19" viewBox="0 0 32 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.832 1.85439L9.09495 6.56978C8.16307 7.22084 7.93658 8.50206 8.58764 9.43478C9.2387 10.3667 10.5199 10.5931 11.4526 9.94209L18.1897 5.2267C20.9787 3.27352 24.8307 3.95216 26.7772 6.74111C28.7304 9.53005 28.0518 13.3821 25.2628 15.3286L23.145 16.8087C24.8993 19.7899 28.8675 18.5354 30.6702 15.3353C32.5549 11.9905 32.4863 7.72146 30.1428 4.37673C26.8926 -0.263432 20.4714 -1.39422 15.832 1.85439Z" fill="#005AE1"/>
|
||||
<path d="M16.1671 16.6776L22.9042 11.9622C23.836 11.3112 24.0625 10.03 23.4115 9.09724C22.7604 8.16536 21.4792 7.93887 20.5465 8.58993L13.8094 13.3053C11.0204 15.2585 7.1684 14.5799 5.22191 11.7909C3.26873 9.00196 3.94737 5.14992 6.73631 3.20343L8.85414 1.72329C7.1007 -1.25705 3.1325 -0.00256691 1.32975 3.19758C-0.554895 6.54231 -0.486362 10.8114 1.85712 14.1561C5.10574 18.7955 11.5269 19.9338 16.1671 16.6785V16.6776Z" fill="#005AE1"/>
|
||||
</svg>
|
After Width: | Height: | Size: 970 B |
|
@ -0,0 +1,9 @@
|
|||
de:
|
||||
GoogleSitemaps:
|
||||
METANOTEPRIORITY: 'Manuelle Google Sitemaps-Priorität für diese Seite (%s)'
|
||||
METAPAGEPRIO: 'Page Priorität'
|
||||
PRIORITYAUTOSET: 'Automatisch basierend auf Verschachtelung im Seitenbaum'
|
||||
PRIORITYLEASTIMPORTANT: 'Am wenigsten wichtig'
|
||||
PRIORITYMOSTIMPORTANT: 'Am wichtigsten'
|
||||
PRIORITYNOTINDEXED: 'Nicht indexiert'
|
||||
TABGOOGLESITEMAP: 'Google Sitemap'
|
|
@ -0,0 +1,9 @@
|
|||
en:
|
||||
GoogleSitemaps:
|
||||
METANOTEPRIORITY: 'Manually specify a Google Sitemaps priority for this page (%s)'
|
||||
METAPAGEPRIO: 'Page Priority'
|
||||
PRIORITYAUTOSET: 'Auto-set based on page depth'
|
||||
PRIORITYLEASTIMPORTANT: 'Least important'
|
||||
PRIORITYMOSTIMPORTANT: 'Most important'
|
||||
PRIORITYNOTINDEXED: 'Not indexed'
|
||||
TABGOOGLESITEMAP: 'Google Sitemap'
|
|
@ -0,0 +1,9 @@
|
|||
fi:
|
||||
GoogleSitemaps:
|
||||
METANOTEPRIORITY: 'Määritä Google Sitemaps -prioriteetti tälle sivulle (%s)'
|
||||
METAPAGEPRIO: 'Sivun prioriteetti'
|
||||
PRIORITYAUTOSET: 'Päättele automaattisesti valikkorakenteen mukaan'
|
||||
PRIORITYLEASTIMPORTANT: 'Vähiten tärkein'
|
||||
PRIORITYMOSTIMPORTANT: 'Kaikista tärkein'
|
||||
PRIORITYNOTINDEXED: 'Pyydä olemaan näyttämättä hakukoneissa'
|
||||
TABGOOGLESITEMAP: 'Google Sitemap'
|
|
@ -0,0 +1,9 @@
|
|||
it:
|
||||
GoogleSitemaps:
|
||||
METANOTEPRIORITY: 'Specifica manualmente una priorità Sitemaps di Google per questa pagina (%s)'
|
||||
METAPAGEPRIO: 'Priorità Pagina'
|
||||
PRIORITYAUTOSET: 'Imposta automaticamente in base alla profondità della pagina'
|
||||
PRIORITYLEASTIMPORTANT: 'Meno importante'
|
||||
PRIORITYMOSTIMPORTANT: 'Più importante'
|
||||
PRIORITYNOTINDEXED: 'Non indicizzato'
|
||||
TABGOOGLESITEMAP: 'Sitemap di Google'
|
|
@ -0,0 +1,9 @@
|
|||
lt:
|
||||
GoogleSitemaps:
|
||||
METANOTEPRIORITY: 'Šio puslapio Google svetainės medžio prioriteto nustatymas (%s)'
|
||||
METAPAGEPRIO: 'Puslapio prioritetas'
|
||||
PRIORITYAUTOSET: 'Automatiškai nustatyti prioritetą pagal puslapio gylį'
|
||||
PRIORITYLEASTIMPORTANT: 'Mažiausiai svarbus'
|
||||
PRIORITYMOSTIMPORTANT: 'Labiausiai svarbus'
|
||||
PRIORITYNOTINDEXED: 'Neindeksuojamas'
|
||||
TABGOOGLESITEMAP: 'Google svetainės medis'
|
|
@ -0,0 +1,9 @@
|
|||
nl:
|
||||
GoogleSitemaps:
|
||||
METANOTEPRIORITY: 'Handmatig de prioriteit van Google Sitemaps aanpassen voor deze pagina (%s)'
|
||||
METAPAGEPRIO: 'Pagina prioriteit'
|
||||
PRIORITYAUTOSET: 'Automatisch de pagina diepte maken'
|
||||
PRIORITYLEASTIMPORTANT: 'Minst belangrijke'
|
||||
PRIORITYMOSTIMPORTANT: 'Meest belangrijke'
|
||||
PRIORITYNOTINDEXED: 'niet geïndexeerd'
|
||||
TABGOOGLESITEMAP: 'Google Sitemap'
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
ru:
|
||||
GoogleSitemaps:
|
||||
METANOTEPRIORITY: 'Установить вручную Google Sitemaps приоритет для этой страшицы (%s)'
|
||||
METAPAGEPRIO: 'Приоритет страницы'
|
||||
PRIORITYAUTOSET: 'Автоматичисеки на основании глубины просмотра'
|
||||
PRIORITYLEASTIMPORTANT: 'Наименее важная'
|
||||
PRIORITYMOSTIMPORTANT: 'Наиболее Важная'
|
||||
PRIORITYNOTINDEXED: 'Не индексировать'
|
||||
TABGOOGLESITEMAP: 'Google Sitemap'
|
|
@ -0,0 +1,9 @@
|
|||
sl:
|
||||
GoogleSitemaps:
|
||||
METANOTEPRIORITY: 'Ročno nastavite (Google Sitemaps priority) pomembnost trenutne strani (%s)'
|
||||
METAPAGEPRIO: 'Pomembnost strani'
|
||||
PRIORITYAUTOSET: 'Samodejno glede na nivo v strukturi'
|
||||
PRIORITYLEASTIMPORTANT: 'Manj pomembna'
|
||||
PRIORITYMOSTIMPORTANT: 'Bolj pomembna'
|
||||
PRIORITYNOTINDEXED: 'Ne indeksiraj'
|
||||
TABGOOGLESITEMAP: 'Google Sitemap'
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ruleset name="SilverStripe">
|
||||
<description>CodeSniffer ruleset for SilverStripe coding conventions.</description>
|
||||
<file>src</file>
|
||||
<file>tests</file>
|
||||
<!-- base rules are PSR-2 -->
|
||||
<rule ref="PSR2">
|
||||
<!-- Current exclusions -->
|
||||
<exclude name="PSR1.Methods.CamelCapsMethodName" />
|
||||
</rule>
|
||||
</ruleset>
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="vendor/silverstripe/framework/tests/bootstrap.php" colors="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
|
||||
<coverage includeUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">src/</directory>
|
||||
</include>
|
||||
<exclude>
|
||||
<directory suffix=".php">tests/</directory>
|
||||
</exclude>
|
||||
</coverage>
|
||||
<testsuite name="Default">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</phpunit>
|
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
|
||||
namespace Wilr\GoogleSitemaps\Control;
|
||||
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
use Wilr\GoogleSitemaps\GoogleSitemap;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
/**
|
||||
* Controller for displaying the sitemap.xml. The module displays an index
|
||||
* sitemap at the sitemap.xml level, then outputs the individual objects
|
||||
* at a second level.
|
||||
*
|
||||
* <code>
|
||||
* http://site.com/sitemap.xml/
|
||||
* http://site.com/sitemap.xml/sitemap/$ClassName-$Page.xml
|
||||
* </code>
|
||||
*
|
||||
* @package googlesitemaps
|
||||
*/
|
||||
class GoogleSitemapController extends Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private static $allowed_actions = [
|
||||
'index',
|
||||
'sitemap',
|
||||
'styleSheetIndex',
|
||||
'styleSheet'
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Default controller action for the sitemap.xml file. Renders a index
|
||||
* file containing a list of links to sub sitemaps containing the data.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function index($url)
|
||||
{
|
||||
if (GoogleSitemap::enabled()) {
|
||||
$this->getResponse()->addHeader('Content-Type', 'application/xml; charset="utf-8"');
|
||||
$this->getResponse()->addHeader('X-Robots-Tag', 'noindex');
|
||||
|
||||
$sitemaps = GoogleSitemap::inst()->getSitemaps();
|
||||
$this->extend('updateGoogleSitemaps', $sitemaps);
|
||||
|
||||
return $this->customise(new ArrayData([
|
||||
'Sitemaps' => $sitemaps,
|
||||
]))->renderWith(__CLASS__);
|
||||
} else {
|
||||
return new HTTPResponse('Page not found', 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific controller action for displaying a particular list of links
|
||||
* for a class
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function sitemap()
|
||||
{
|
||||
$class = $this->unsanitiseClassName($this->request->param('ID'));
|
||||
$page = intval($this->request->param('OtherID'));
|
||||
|
||||
if ($page) {
|
||||
if (!is_numeric($page)) {
|
||||
return new HTTPResponse('Page not found', 404);
|
||||
}
|
||||
}
|
||||
|
||||
if (GoogleSitemap::enabled()
|
||||
&& $class
|
||||
&& ($page > 0)
|
||||
&& ($class == SiteTree::class || $class == 'GoogleSitemapRoute' || GoogleSitemap::is_registered($class))
|
||||
) {
|
||||
$this->getResponse()->addHeader('Content-Type', 'application/xml; charset="utf-8"');
|
||||
$this->getResponse()->addHeader('X-Robots-Tag', 'noindex');
|
||||
|
||||
$items = GoogleSitemap::inst()->getItems($class, $page);
|
||||
$this->extend('updateGoogleSitemapItems', $items, $class, $page);
|
||||
|
||||
return array(
|
||||
'Items' => $items
|
||||
);
|
||||
}
|
||||
|
||||
return new HTTPResponse('Page not found', 404);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsanitise a namespaced class' name from a URL param
|
||||
* @return string
|
||||
*/
|
||||
protected function unsanitiseClassName($class)
|
||||
{
|
||||
return str_replace('-', '\\', (string) $class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the stylesheet for the sitemap index
|
||||
*
|
||||
* @return DBHTMLText
|
||||
*/
|
||||
public function styleSheetIndex()
|
||||
{
|
||||
$html = $this->renderWith('xml-sitemapindex');
|
||||
$this->getResponse()->addHeader('Content-Type', 'text/xsl; charset="utf-8"');
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the stylesheet for the sitemap
|
||||
*
|
||||
* @return DBHTMLText
|
||||
*/
|
||||
public function styleSheet()
|
||||
{
|
||||
$html = $this->renderWith('xml-sitemap');
|
||||
$this->getResponse()->addHeader('Content-Type', 'text/xsl; charset="utf-8"');
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
public function AbsoluteLink($action = null)
|
||||
{
|
||||
return rtrim(Controller::join_links(Director::absoluteBaseURL(), 'sitemap.xml', $action), '/');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
<?php
|
||||
|
||||
namespace Wilr\GoogleSitemaps\Extensions;
|
||||
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\ORM\DataExtension;
|
||||
use SilverStripe\ORM\FieldType\DBDatetime;
|
||||
use SilverStripe\Subsites\Model\Subsite;
|
||||
use Wilr\GoogleSitemaps\GoogleSitemap;
|
||||
|
||||
/**
|
||||
* Decorate the page object to provide google sitemaps with additional options
|
||||
* and configuration.
|
||||
*/
|
||||
class GoogleSitemapExtension extends DataExtension
|
||||
{
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function canIncludeInGoogleSitemap()
|
||||
{
|
||||
$can = true;
|
||||
|
||||
if ($this->owner->hasMethod('AbsoluteLink')) {
|
||||
$hostHttp = parse_url(Director::protocolAndHost(), PHP_URL_HOST);
|
||||
|
||||
// Subsite support
|
||||
if (class_exists(Subsite::class)) {
|
||||
// Subsite will have a different domain from Director::protocolAndHost
|
||||
if ($subsite = Subsite::currentSubsite()) {
|
||||
$hostHttp = parse_url(Director::protocol() . $subsite->getPrimaryDomain(), PHP_URL_HOST);
|
||||
}
|
||||
}
|
||||
|
||||
$objHttp = parse_url($this->owner->AbsoluteLink() ?? '', PHP_URL_HOST);
|
||||
|
||||
if ($objHttp != $hostHttp) {
|
||||
$can = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($can) {
|
||||
$can = $this->owner->canView();
|
||||
}
|
||||
|
||||
if ($can) {
|
||||
$can = ($this->owner->getGooglePriority() !== false);
|
||||
}
|
||||
|
||||
if ($can === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow override. invokeWithExtensions will either return a single result (true|false) if defined on the object
|
||||
// or an array if on extensions.
|
||||
$override = $this->owner->invokeWithExtensions('alterCanIncludeInGoogleSitemap', $can);
|
||||
|
||||
if ($override !== null) {
|
||||
if (is_array($override)) {
|
||||
if (!empty($override)) {
|
||||
$can = min($override, $can);
|
||||
}
|
||||
} else {
|
||||
$can = $override;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($can) && isset($can[0])) {
|
||||
return $can[0];
|
||||
}
|
||||
|
||||
return $can;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default value of the priority field depends on the depth of the page in
|
||||
* the site tree, so it must be calculated dynamically.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getGooglePriority()
|
||||
{
|
||||
$field = $this->owner->hasField('Priority');
|
||||
|
||||
if ($field) {
|
||||
$priority = $this->owner->getField('Priority');
|
||||
|
||||
return ($priority < 0) ? false : $priority;
|
||||
}
|
||||
|
||||
return GoogleSitemap::get_priority_for_class($this->owner->ClassName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a pages change frequency calculated by pages age and number of
|
||||
* versions. Google expects always, hourly, daily, weekly, monthly, yearly
|
||||
* or never as values.
|
||||
*
|
||||
* @see http://support.google.com/webmasters/bin/answer.py?hl=en&answer=183668&topic=8476&ctx=topic
|
||||
*
|
||||
* @return DBDatetime
|
||||
*/
|
||||
public function getChangeFrequency()
|
||||
{
|
||||
if ($freq = GoogleSitemap::get_frequency_for_class($this->owner->ClassName)) {
|
||||
return $freq;
|
||||
}
|
||||
|
||||
$date = date('Y-m-d H:i:s');
|
||||
|
||||
$created = new DBDatetime();
|
||||
$created->value = ($this->owner->Created) ? $this->owner->Created : $date;
|
||||
|
||||
$now = new DBDatetime();
|
||||
$now->value = $date;
|
||||
|
||||
$versions = ($this->owner->Version) ? $this->owner->Version : 1;
|
||||
|
||||
if ($now && $created) {
|
||||
$timediff = $now->getTimestamp() - $created->getTimestamp();
|
||||
|
||||
// Check how many revisions have been made over the lifetime of the
|
||||
// Page for a rough estimate of it's changing frequency.
|
||||
$period = $timediff / ($versions + 1);
|
||||
} else {
|
||||
$period = 0;
|
||||
}
|
||||
|
||||
if ($period > 60 * 60 * 24 * 365) {
|
||||
$freq = 'yearly';
|
||||
} elseif ($period > 60 * 60 * 24 * 30) {
|
||||
$freq = 'monthly';
|
||||
} elseif ($period > 60 * 60 * 24 * 7) {
|
||||
$freq = 'weekly';
|
||||
} elseif ($period > 60 * 60 * 24) {
|
||||
$freq = 'daily';
|
||||
} elseif ($period > 60 * 60) {
|
||||
$freq = 'hourly';
|
||||
} else {
|
||||
$freq = 'always';
|
||||
}
|
||||
|
||||
return $freq;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
<?php
|
||||
|
||||
namespace Wilr\GoogleSitemaps\Extensions;
|
||||
|
||||
use SilverStripe\Assets\Image;
|
||||
use SilverStripe\ErrorPage\ErrorPage;
|
||||
use SilverStripe\Forms\DropdownField;
|
||||
use SilverStripe\Forms\LiteralField;
|
||||
use SilverStripe\Forms\Tab;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use Throwable;
|
||||
|
||||
class GoogleSitemapSiteTreeExtension extends GoogleSitemapExtension
|
||||
{
|
||||
private static $db = [
|
||||
"Priority" => "Varchar(5)"
|
||||
];
|
||||
|
||||
public function updateSettingsFields(&$fields)
|
||||
{
|
||||
$prorities = array(
|
||||
'-1' => _t('GoogleSitemaps.PRIORITYNOTINDEXED', "Not indexed"),
|
||||
'1.0' => '1 - ' . _t('GoogleSitemaps.PRIORITYMOSTIMPORTANT', "Most important"),
|
||||
'0.9' => '2',
|
||||
'0.8' => '3',
|
||||
'0.7' => '4',
|
||||
'0.6' => '5',
|
||||
'0.5' => '6',
|
||||
'0.4' => '7',
|
||||
'0.3' => '8',
|
||||
'0.2' => '9',
|
||||
'0.1' => '10 - ' . _t('GoogleSitemaps.PRIORITYLEASTIMPORTANT', "Least important")
|
||||
);
|
||||
|
||||
$tabset = $fields->findOrMakeTab('Root.Settings');
|
||||
|
||||
$message = "<p>";
|
||||
$message .= sprintf(
|
||||
_t(
|
||||
'GoogleSitemaps.METANOTEPRIORITY',
|
||||
"Manually specify a Google Sitemaps priority for this page (%s)"
|
||||
),
|
||||
'<a href="http://www.google.com/support/webmasters/bin/answer.py?hl=en&answer=71936#prioritize" '
|
||||
. 'target="_blank">?</a>'
|
||||
);
|
||||
$message .= "</p>";
|
||||
|
||||
$tabset->push(new Tab(
|
||||
'GoogleSitemap',
|
||||
_t('GoogleSitemaps.TABGOOGLESITEMAP', 'Google Sitemap'),
|
||||
LiteralField::create("GoogleSitemapIntro", $message),
|
||||
$priority = DropdownField::create(
|
||||
"Priority",
|
||||
$this->owner->fieldLabel('Priority'),
|
||||
$prorities,
|
||||
$this->owner->Priority
|
||||
)
|
||||
));
|
||||
|
||||
$priority->setEmptyString(_t('GoogleSitemaps.PRIORITYAUTOSET', 'Auto-set based on page depth'));
|
||||
}
|
||||
|
||||
public function updateFieldLabels(&$labels)
|
||||
{
|
||||
parent::updateFieldLabels($labels);
|
||||
|
||||
$labels['Priority'] = _t('GoogleSitemaps.METAPAGEPRIO', "Page Priority");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that all parent pages of this page (if any) are published
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasPublishedParent()
|
||||
{
|
||||
|
||||
// Skip root pages
|
||||
if (empty($this->owner->ParentID)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ensure direct parent exists
|
||||
$parent = $this->owner->Parent();
|
||||
if (empty($parent) || !$parent->exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check ancestry
|
||||
return $parent->hasPublishedParent();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function canIncludeInGoogleSitemap()
|
||||
{
|
||||
|
||||
// Check that parent page is published
|
||||
if (!$this->owner->hasPublishedParent()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = parent::canIncludeInGoogleSitemap();
|
||||
$result = ($this->owner instanceof ErrorPage) ? false : $result;
|
||||
|
||||
if (is_array($result) && isset($result[0])) {
|
||||
return $result[0];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getGooglePriority()
|
||||
{
|
||||
setlocale(LC_ALL, "en_US.UTF8");
|
||||
$priority = $this->owner->getField('Priority');
|
||||
|
||||
if (!$priority) {
|
||||
$parentStack = $this->owner->getAncestors();
|
||||
$numParents = $parentStack->count();
|
||||
|
||||
$num = max(0.1, 1.0 - ($numParents / 10));
|
||||
$result = str_replace(",", ".", $num);
|
||||
|
||||
return $result;
|
||||
} elseif ($priority == -1) {
|
||||
return false;
|
||||
} else {
|
||||
return (is_numeric($priority) && $priority <= 1.0) ? $priority : 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
public function ImagesForSitemap()
|
||||
{
|
||||
$list = new ArrayList();
|
||||
$cachedImages = [];
|
||||
|
||||
foreach ($this->owner->hasOne() as $field => $type) {
|
||||
if (strpos($type, '.') !== false) {
|
||||
$type = explode('.', $type)[0];
|
||||
}
|
||||
|
||||
if (singleton($type) instanceof Image) {
|
||||
$image = $this->owner->getComponent($field);
|
||||
|
||||
try {
|
||||
if ($image && $image->exists() && !isset($cachedImages[$image->ID])) {
|
||||
$cachedImages[$image->ID] = true;
|
||||
|
||||
$list->push($image);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->owner->hasMany() as $field => $type) {
|
||||
if (singleton($type) instanceof Image) {
|
||||
$images = $this->owner->getComponents($field);
|
||||
|
||||
foreach ($images as $image) {
|
||||
try {
|
||||
if ($image && $image->exists() && !isset($cachedImages[$image->ID])) {
|
||||
$cachedImages[$image->ID] = true;
|
||||
|
||||
$list->push($image);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->owner->manyMany() as $field => $type) {
|
||||
$image = false;
|
||||
|
||||
if (is_array($type) && isset($type['through'])) {
|
||||
if (singleton($type['through']) instanceof Image) {
|
||||
$image = true;
|
||||
}
|
||||
} else {
|
||||
if (strpos($type, '.') !== false) {
|
||||
$type = explode('.', $type)[0];
|
||||
}
|
||||
|
||||
if (singleton($type) instanceof Image) {
|
||||
$image = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($image) {
|
||||
$images = $this->owner->$field();
|
||||
|
||||
foreach ($images as $image) {
|
||||
try {
|
||||
if ($image && $image->exists() && !isset($cachedImages[$image->ID])) {
|
||||
$cachedImages[$image->ID] = true;
|
||||
|
||||
$list->push($image);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->owner->extend('updateImagesForSitemap', $list);
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,466 @@
|
|||
<?php
|
||||
|
||||
namespace Wilr\GoogleSitemaps;
|
||||
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\DataList;
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
use SilverStripe\View\ArrayData;
|
||||
use SilverStripe\Core\Extensible;
|
||||
use SilverStripe\Core\Injector\Injectable;
|
||||
use SilverStripe\Core\Config\Configurable;
|
||||
use Wilr\GoogleSitemaps\Extensions\GoogleSitemapExtension;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
|
||||
/**
|
||||
* Sitemaps are a way to tell Google about pages on your site that they might
|
||||
* not otherwise discover. In its simplest terms, a XML Sitemap usually called
|
||||
* a Sitemap, with a capital S—is a list of the pages on your website.
|
||||
*
|
||||
* Creating and submitting a Sitemap helps make sure that Google knows about
|
||||
* all the pages on your site, including URLs that may not be discoverable by
|
||||
* Google's normal crawling process.
|
||||
*
|
||||
* The GoogleSitemap handle requests to 'sitemap.xml' the other two classes are
|
||||
* used to render the sitemap.
|
||||
*
|
||||
* The config file is usually located in the _config folder of your project folder.
|
||||
* e.g. app/_config/googlesitemaps.yml
|
||||
*
|
||||
* <example>
|
||||
* ---
|
||||
* Name: customgooglesitemaps
|
||||
* After: googlesitemaps
|
||||
* ---
|
||||
* Wilr\GoogleSitemaps\GoogleSitemap:
|
||||
* enabled: true
|
||||
* objects_per_sitemap: 1000
|
||||
* use_show_in_search: true
|
||||
* </example>
|
||||
*
|
||||
* @see http://www.google.com/support/webmasters/bin/answer.py?hl=en&answer=34609
|
||||
*/
|
||||
class GoogleSitemap
|
||||
{
|
||||
use Extensible;
|
||||
use Injectable;
|
||||
use Configurable;
|
||||
|
||||
/**
|
||||
* List of {@link DataObject} class names to include. As well as the change
|
||||
* frequency and priority of each class.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $dataobjects = [];
|
||||
|
||||
/**
|
||||
* List of custom routes to include in the sitemap (such as controller
|
||||
* subclasses) as well as the change frequency and priority.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $routes = [];
|
||||
|
||||
/**
|
||||
* @config
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private static $exclude_redirector_pages = true;
|
||||
|
||||
/**
|
||||
* Decorates the given DataObject with {@link GoogleSitemapDecorator}
|
||||
* and pushes the class name to the registered DataObjects.
|
||||
* Note that all registered DataObjects need the method AbsoluteLink().
|
||||
*
|
||||
* @param string $className name of DataObject to register
|
||||
* @param string $changeFreq how often is this DataObject updated?
|
||||
* Possible values:
|
||||
* always, hourly, daily, weekly, monthly, yearly, never
|
||||
* @param string $priority How important is this DataObject in comparison to other urls?
|
||||
* Possible values: 0.1, 0.2 ... , 0.9, 1.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_dataobject($className, $changeFreq = 'monthly', $priority = '0.6')
|
||||
{
|
||||
if (!self::is_registered($className)) {
|
||||
$className::add_extension(GoogleSitemapExtension::class);
|
||||
|
||||
self::$dataobjects[$className] = [
|
||||
'frequency' => ($changeFreq) ? $changeFreq : 'monthly',
|
||||
'priority' => ($priority) ? $priority : '0.6'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers multiple dataobjects in a single line. See {@link register_dataobject}
|
||||
* for the heavy lifting
|
||||
*
|
||||
* @param array $dataobjects array of class names of DataObject to register
|
||||
* @param string $changeFreq how often is this DataObject updated?
|
||||
* Possible values:
|
||||
* always, hourly, daily, weekly, monthly, yearly, never
|
||||
* @param string $priority How important is this DataObject in comparison to other urls?
|
||||
* Possible values: 0.1, 0.2 ... , 0.9, 1.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_dataobjects($dataobjects, $changeFreq = 'monthly', $priority = '0.6')
|
||||
{
|
||||
foreach ($dataobjects as $obj) {
|
||||
self::register_dataobject($obj, $changeFreq, $priority);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given class name is already registered or not.
|
||||
*
|
||||
* @param string $className Name of DataObject to check
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_registered($className)
|
||||
{
|
||||
if (!isset(self::$dataobjects[$className])) {
|
||||
$lowerKeys = array_change_key_case(self::$dataobjects);
|
||||
|
||||
return isset($lowerKeys[$className]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a class from the sitemap. Mostly used for the test suite
|
||||
*
|
||||
* @param string
|
||||
*/
|
||||
public static function unregister_dataobject($className)
|
||||
{
|
||||
unset(self::$dataobjects[$className]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears registered {@link DataObjects}. Useful for unit tests.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function clear_registered_dataobjects()
|
||||
{
|
||||
self::$dataobjects = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a given route to the sitemap list
|
||||
*
|
||||
* @param string
|
||||
* @param string
|
||||
* @param string
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_route($route, $changeFreq = 'monthly', $priority = '0.6')
|
||||
{
|
||||
self::$routes = array_merge(self::$routes, array(
|
||||
$route => array(
|
||||
'frequency' => ($changeFreq) ? $changeFreq : 'monthly',
|
||||
'priority' => ($priority) ? $priority : '0.6'
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a given list of relative urls. Will be merged with the current
|
||||
* registered routes. If you want to replace them, please call {@link clear_routes}
|
||||
*
|
||||
* @param array
|
||||
* @param string
|
||||
* @param string
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_routes($routes, $changeFreq = 'monthly', $priority = '0.6')
|
||||
{
|
||||
foreach ($routes as $route) {
|
||||
self::register_route($route, $changeFreq, $priority);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears registered routes
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function clear_registered_routes()
|
||||
{
|
||||
self::$routes = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the list of data to include in the rendered sitemap. Links
|
||||
* can include pages from the website, dataobjects (such as forum posts)
|
||||
* as well as custom registered paths.
|
||||
*
|
||||
* @param string
|
||||
* @param int
|
||||
*
|
||||
* @return ArrayList
|
||||
*/
|
||||
public function getItems($class, $page = 1)
|
||||
{
|
||||
$page = (int) $page;
|
||||
|
||||
try {
|
||||
$reflectionClass = new ReflectionClass($class);
|
||||
$class = $reflectionClass->getName();
|
||||
} catch (ReflectionException $e) {
|
||||
// this can happen when $class is GoogleSitemapRoute
|
||||
//we should try to carry on
|
||||
}
|
||||
|
||||
$output = new ArrayList();
|
||||
$count = (int) Config::inst()->get(__CLASS__, 'objects_per_sitemap');
|
||||
$filter = Config::inst()->get(__CLASS__, 'use_show_in_search');
|
||||
$redirector = Config::inst()->get(__CLASS__, 'exclude_redirector_pages');
|
||||
|
||||
// todo migrate to extension hook or DI point for other modules to
|
||||
// modify state filters
|
||||
if (class_exists('Translatable')) {
|
||||
Translatable::disable_locale_filter();
|
||||
}
|
||||
|
||||
if ($class == 'SilverStripe\CMS\Model\SiteTree') {
|
||||
$instances = Versioned::get_by_stage('SilverStripe\CMS\Model\SiteTree', 'Live');
|
||||
|
||||
if ($filter) {
|
||||
$instances = $instances->filter('ShowInSearch', 1);
|
||||
}
|
||||
|
||||
if ($redirector) {
|
||||
foreach (ClassInfo::subclassesFor('SilverStripe\\CMS\\Model\\RedirectorPage') as $redirectorClass) {
|
||||
$instances = $instances->exclude('ClassName', $redirectorClass);
|
||||
}
|
||||
}
|
||||
} elseif ($class == "GoogleSitemapRoute") {
|
||||
$instances = array_slice(self::$routes, ($page - 1) * $count, $count);
|
||||
$output = new ArrayList();
|
||||
|
||||
if ($instances) {
|
||||
foreach ($instances as $route => $config) {
|
||||
$output->push(new ArrayData(array(
|
||||
'AbsoluteLink' => Director::absoluteURL($route),
|
||||
'ChangeFrequency' => $config['frequency'],
|
||||
'GooglePriority' => $config['priority']
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
} else {
|
||||
$instances = new DataList($class);
|
||||
}
|
||||
|
||||
$this->extend("alterDataList", $instances, $class);
|
||||
|
||||
$instances = $instances->limit(
|
||||
$count,
|
||||
($page - 1) * $count
|
||||
);
|
||||
|
||||
if ($instances) {
|
||||
foreach ($instances as $obj) {
|
||||
if ($obj->canIncludeInGoogleSitemap()) {
|
||||
$output->push($obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static interface to instance level ->getItems() for backward compatibility.
|
||||
*
|
||||
* @param string
|
||||
* @param int
|
||||
*
|
||||
* @return ArrayList
|
||||
* @deprecated Please create an instance and call ->getSitemaps() instead.
|
||||
*/
|
||||
public static function get_items($class, $page = 1)
|
||||
{
|
||||
return static::inst()->getItems($class, $page);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the string frequency of edits for a particular dataobject class.
|
||||
*
|
||||
* Frequency for {@link SiteTree} objects can be determined from the version
|
||||
* history.
|
||||
*
|
||||
* @param string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_frequency_for_class($class)
|
||||
{
|
||||
foreach (self::$dataobjects as $type => $config) {
|
||||
if ($class == $type) {
|
||||
return $config['frequency'];
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default priority of edits for a particular dataobject class.
|
||||
*
|
||||
* @param string
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public static function get_priority_for_class($class)
|
||||
{
|
||||
foreach (self::$dataobjects as $type => $config) {
|
||||
if ($class == $type) {
|
||||
return $config['priority'];
|
||||
}
|
||||
}
|
||||
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
/**
|
||||
* The google site map is broken down into multiple smaller files to
|
||||
* prevent overbearing a server. By default separate {@link DataObject}
|
||||
* records are keep in separate files and broken down into chunks.
|
||||
*
|
||||
* @return ArrayList
|
||||
*/
|
||||
public function getSitemaps()
|
||||
{
|
||||
$countPerFile = Config::inst()->get(__CLASS__, 'objects_per_sitemap');
|
||||
$sitemaps = new ArrayList();
|
||||
$filter = Config::inst()->get(__CLASS__, 'use_show_in_search');
|
||||
|
||||
if (class_exists('SilverStripe\CMS\Model\SiteTree')) {
|
||||
// move to extension hook. At the moment moduleexists config hook
|
||||
// does not work.
|
||||
if (class_exists('Translatable')) {
|
||||
Translatable::disable_locale_filter();
|
||||
}
|
||||
|
||||
$filter = ($filter) ? "\"ShowInSearch\" = 1" : "";
|
||||
$class = 'SilverStripe\CMS\Model\SiteTree';
|
||||
$instances = Versioned::get_by_stage($class, 'Live', $filter);
|
||||
$this->extend("alterDataList", $instances, $class);
|
||||
$count = $instances->count();
|
||||
|
||||
$neededForPage = ceil($count / $countPerFile);
|
||||
|
||||
for ($i = 1; $i <= $neededForPage; $i++) {
|
||||
$lastEdited = $instances
|
||||
->limit($countPerFile, ($i - 1) * $countPerFile)
|
||||
->sort(null)
|
||||
->max('LastEdited');
|
||||
|
||||
$lastModified = ($lastEdited) ? date('Y-m-d', strtotime($lastEdited)) : date('Y-m-d');
|
||||
|
||||
$sitemaps->push(new ArrayData(array(
|
||||
'ClassName' => $this->sanitiseClassName('SilverStripe\CMS\Model\SiteTree'),
|
||||
'LastModified' => $lastModified,
|
||||
'Page' => $i
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
if (count(self::$dataobjects) > 0) {
|
||||
foreach (self::$dataobjects as $class => $config) {
|
||||
$list = new DataList($class);
|
||||
$list = $list->sort('LastEdited ASC');
|
||||
$this->extend("alterDataList", $list, $class);
|
||||
$neededForClass = ceil($list->count() / $countPerFile);
|
||||
|
||||
for ($i = 1; $i <= $neededForClass; $i++) {
|
||||
// determine the last modified date for this slice
|
||||
$sliced = $list
|
||||
->limit($countPerFile, ($i - 1) * $countPerFile)
|
||||
->last();
|
||||
|
||||
$lastModified = ($sliced) ? date('Y-m-d', strtotime($sliced->LastEdited)) : date('Y-m-d');
|
||||
|
||||
$sitemaps->push(new ArrayData(array(
|
||||
'ClassName' => $this->sanitiseClassName($class),
|
||||
'Page' => $i,
|
||||
'LastModified' => $lastModified
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count(self::$routes) > 0) {
|
||||
$needed = ceil(count(self::$routes) / $countPerFile);
|
||||
|
||||
for ($i = 1; $i <= $needed; $i++) {
|
||||
$sitemaps->push(new ArrayData(array(
|
||||
'ClassName' => 'GoogleSitemapRoute',
|
||||
'Page' => $i
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
return $sitemaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static interface to instance level ->getSitemaps() for backward compatibility.
|
||||
*
|
||||
* @return ArrayList
|
||||
* @deprecated Please create an instance and call ->getSitemaps() instead.
|
||||
*/
|
||||
public static function get_sitemaps()
|
||||
{
|
||||
return static::inst()->getSitemaps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is GoogleSitemap enabled?
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function enabled()
|
||||
{
|
||||
return (Config::inst()->get(__CLASS__, 'enabled'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convenience method for manufacturing an instance for hew instance-level
|
||||
* methods (and for easier type definition).
|
||||
*
|
||||
* @return GoogleSitemap
|
||||
*/
|
||||
public static function inst()
|
||||
{
|
||||
return GoogleSitemap::create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitise a namespaced class' name for inclusion in a link
|
||||
* @return string
|
||||
*/
|
||||
protected function sanitiseClassName($class)
|
||||
{
|
||||
return str_replace('\\', '-', (string) $class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace Wilr\GoogleSitemaps;
|
||||
|
||||
interface Sitemapable
|
||||
{
|
||||
/**
|
||||
* Return the absolute URL for this object
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function AbsoluteLink();
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<% control Items %>
|
||||
<url>
|
||||
<loc>$AbsoluteLink</loc>
|
||||
<lastmod>$LastEdited.Format(c)</lastmod>
|
||||
<% if ChangeFreq %><changefreq>$ChangeFreq</changefreq><% end_if %>
|
||||
<% if Priority %><priority>$Priority</priority><% end_if %>
|
||||
</url>
|
||||
<% end_control %>
|
||||
</urlset>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml-stylesheet type='text/xsl' href='{$AbsoluteLink('styleSheetIndex')}'?>
|
||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><% loop Sitemaps %>
|
||||
<sitemap>
|
||||
<loc>{$Up.AbsoluteLink('sitemap')}/{$ClassName}/{$Page.xml}</loc>
|
||||
<% if $LastModified %><lastmod>{$LastModified}</lastmod><% end_if %>
|
||||
</sitemap><% end_loop %>
|
||||
</sitemapindex>
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml-stylesheet type='text/xsl' href='{$AbsoluteLink('styleSheet')}'?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
|
||||
<% loop $Items %>
|
||||
<url>
|
||||
<loc>{$AbsoluteLink}</loc>
|
||||
<% if $LastEdited %><lastmod>{$LastEdited.Rfc3339()}</lastmod><% end_if %>
|
||||
<% if $ChangeFrequency %><changefreq>$ChangeFrequency</changefreq><% end_if %>
|
||||
<% if $GooglePriority %><priority>$GooglePriority</priority><% end_if %>
|
||||
<% if $ImagesForSitemap %><% loop $ImagesForSitemap %>
|
||||
<image:image>
|
||||
<image:loc>{$AbsoluteLink}</image:loc>
|
||||
<image:title>{$Title}</image:title>
|
||||
</image:image>
|
||||
<% end_loop %><% end_if %>
|
||||
</url>
|
||||
<% end_loop %>
|
||||
</urlset>
|
|
@ -0,0 +1,91 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xsl:stylesheet version="2.0" xmlns:html="http://www.w3.org/TR/REC-html40" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
|
||||
<xsl:template match="/">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>XML Sitemap</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<link rel="stylesheet" href="$resourceURL(wilr/silverstripe-googlesitemaps:css/style.css)" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<h1 class="content__title">
|
||||
<a href="https://www.silverstripe.org" target="_blank" rel="noopener noreferrer">XML Sitemap</a>
|
||||
</h1>
|
||||
<p class="content__text">
|
||||
This sitemap contains <xsl:value-of select="count(sitemap:urlset/sitemap:url)"/> URLs.
|
||||
</p>
|
||||
<div class="table-wrapper">
|
||||
<table id="sitemap" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="table__cell table__cell--w-65">URL</th>
|
||||
<th class="table__cell table__cell--w-10">Priority</th>
|
||||
<th class="table__cell table__cell--w-10">Change Freq.</th>
|
||||
<th class="table__cell table__cell--w-15">Last Change</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'"/>
|
||||
<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
|
||||
<xsl:for-each select="sitemap:urlset/sitemap:url">
|
||||
<tr>
|
||||
<td class="table__cell">
|
||||
<xsl:variable name="itemURL">
|
||||
<xsl:value-of select="sitemap:loc"/>
|
||||
</xsl:variable>
|
||||
<xsl:variable name="imagesCount" select="count(image:image)"/>
|
||||
<a href="{\$itemURL}">
|
||||
<xsl:value-of select="sitemap:loc"/>
|
||||
</a>
|
||||
<xsl:if test="\$imagesCount > 0">
|
||||
<table class="image-table">
|
||||
<tr>
|
||||
<th class="image-table__cell">Images</th>
|
||||
</tr>
|
||||
<xsl:for-each select="image:image">
|
||||
<xsl:variable name="imageURL">
|
||||
<xsl:value-of select="image:loc"/>
|
||||
</xsl:variable>
|
||||
<tr>
|
||||
<td class="image-table__cell image-table__cell--image">
|
||||
<img class="image-table__image" src="{\$imageURL}" width="60" height="40" />
|
||||
</td>
|
||||
<td class="image-table__cell image-table__cell--text">
|
||||
<span class="image-table__title"><xsl:value-of select="image:title"/></span>
|
||||
<a href="{\$imageURL}">
|
||||
<xsl:value-of select="image:loc"/>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</xsl:for-each>
|
||||
</table>
|
||||
</xsl:if>
|
||||
</td>
|
||||
<td class="table__cell">
|
||||
<xsl:value-of select="concat(sitemap:priority*100,'%')"/>
|
||||
</td>
|
||||
<td class="table__cell">
|
||||
<xsl:value-of select="concat(translate(substring(sitemap:changefreq, 1, 1),concat(\$lower, \$upper),concat(\$upper, \$lower)),substring(sitemap:changefreq, 2))"/>
|
||||
</td>
|
||||
<td class="table__cell">
|
||||
<xsl:value-of select="concat(substring(sitemap:lastmod,0,11),concat(' ', substring(sitemap:lastmod,12,5)))"/>
|
||||
</td>
|
||||
</tr>
|
||||
</xsl:for-each>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="content__text">
|
||||
Generated by the Silverstripe CMS
|
||||
<a href="https://github.com/wilr/silverstripe-googlesitemaps" target="_blank" rel="noopener noreferrer" title="Silverstripe CMS Google Sitemaps module on Github">Google Sitemaps Module</a>
|
||||
</p>
|
||||
<p class="content__text">
|
||||
More information about XML sitemaps on <a href="https://sitemaps.org" target="_blank" rel="noopener noreferrer">sitemaps.org</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>
|
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xsl:stylesheet version="2.0" xmlns:html="http://www.w3.org/TR/REC-html40" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
|
||||
<xsl:template match="/">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>XML Sitemap</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<link rel="stylesheet" href="$resourceURL(wilr/silverstripe-googlesitemaps:css/style.css)" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<h1 class="content__title">
|
||||
<a href="https://www.silverstripe.org" target="_blank" rel="noopener noreferrer">XML Sitemap</a>
|
||||
</h1>
|
||||
<p class="content__text">
|
||||
This sitemap consists of <xsl:value-of select="count(sitemap:sitemapindex/sitemap:sitemap)"/> part(s).
|
||||
</p>
|
||||
<div class="table-wrapper">
|
||||
<table id="sitemapindex" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="table__cell table__cell--w-85">URL</th>
|
||||
<th class="table__cell table__cell--w-15">Last Change</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'"/>
|
||||
<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
|
||||
<xsl:for-each select="sitemap:sitemapindex/sitemap:sitemap">
|
||||
<tr>
|
||||
<td class="table__cell">
|
||||
<xsl:variable name="itemURL">
|
||||
<xsl:value-of select="sitemap:loc"/>
|
||||
</xsl:variable>
|
||||
<a href="{\$itemURL}">
|
||||
<xsl:value-of select="sitemap:loc"/>
|
||||
</a>
|
||||
</td>
|
||||
<td class="table__cell">
|
||||
<xsl:value-of select="concat(substring(sitemap:lastmod,0,11),concat(' ', substring(sitemap:lastmod,12,5)))"/>
|
||||
</td>
|
||||
</tr>
|
||||
</xsl:for-each>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="content__text">
|
||||
Generated by the Silverstripe CMS
|
||||
<a href="https://github.com/wilr/silverstripe-googlesitemaps" target="_blank" rel="noopener noreferrer" title="SilverStripe Google Sitemaps module on Github">Google Sitemaps Module</a>
|
||||
</p>
|
||||
<p class="content__text">
|
||||
More information about XML sitemaps on <a href="https://sitemaps.org" target="_blank" rel="noopener noreferrer">sitemaps.org</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>
|
|
@ -0,0 +1,6 @@
|
|||
Page:
|
||||
Page1:
|
||||
Title: Testpage1
|
||||
Priority: 0.2
|
||||
Page2:
|
||||
Title: Testpage2
|
|
@ -0,0 +1,348 @@
|
|||
<?php
|
||||
|
||||
namespace Wilr\GoogleSitemaps\Tests;
|
||||
|
||||
use Exception;
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Dev\FunctionalTest;
|
||||
use SilverStripe\Forms\DropdownField;
|
||||
use SilverStripe\Forms\LiteralField;
|
||||
use SilverStripe\Forms\Tab;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
use Wilr\GoogleSitemaps\Extensions\GoogleSitemapExtension;
|
||||
use Wilr\GoogleSitemaps\GoogleSitemap;
|
||||
use Wilr\GoogleSitemaps\Tests\Model\OtherDataObject;
|
||||
use Wilr\GoogleSitemaps\Tests\Model\TestDataObject;
|
||||
use Wilr\GoogleSitemaps\Tests\Model\UnviewableDataObject;
|
||||
|
||||
class GoogleSitemapTest extends FunctionalTest
|
||||
{
|
||||
protected static $fixture_file = [
|
||||
'GoogleSitemapTest.yml'
|
||||
];
|
||||
|
||||
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 = [
|
||||
TestDataObject::class,
|
||||
OtherDataObject::class,
|
||||
UnviewableDataObject::class
|
||||
];
|
||||
|
||||
protected static $extra_extensions = [
|
||||
GoogleSitemapExtension::class
|
||||
];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
GoogleSitemap::clear_registered_dataobjects();
|
||||
GoogleSitemap::clear_registered_routes();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
|
||||
GoogleSitemap::clear_registered_dataobjects();
|
||||
GoogleSitemap::clear_registered_routes();
|
||||
}
|
||||
|
||||
public function testCanIncludeInGoogleSitemap(): void
|
||||
{
|
||||
GoogleSitemap::register_dataobject(TestDataObject::class, '');
|
||||
$used = $this->objFromFixture(TestDataObject::class, 'DataObjectTest2');
|
||||
|
||||
$this->assertTrue($used->canIncludeInGoogleSitemap());
|
||||
|
||||
$used->setPrivate();
|
||||
|
||||
$this->assertFalse($used->canIncludeInGoogleSitemap());
|
||||
}
|
||||
|
||||
public function testIndexFileWithCustomRoute(): void
|
||||
{
|
||||
GoogleSitemap::register_route('/test/');
|
||||
|
||||
$response = $this->get('sitemap.xml');
|
||||
$body = $response->getBody();
|
||||
|
||||
$this->assertXmlStringEqualsXmlFile(
|
||||
__DIR__ . '/xml/' . __FUNCTION__ . '.xml',
|
||||
$body,
|
||||
'A link to the custom routes exists'
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetItems(): void
|
||||
{
|
||||
GoogleSitemap::register_dataobject(TestDataObject::class, '');
|
||||
$google = new GoogleSitemap();
|
||||
|
||||
$items = $google->getItems(TestDataObject::class, 1);
|
||||
|
||||
$this->assertEquals(3, $items->count());
|
||||
|
||||
GoogleSitemap::register_dataobject(OtherDataObject::class);
|
||||
$this->assertEquals(1, $google->getItems(OtherDataObject::class, 1)->count());
|
||||
|
||||
GoogleSitemap::register_dataobject(UnviewableDataObject::class);
|
||||
$this->assertEquals(0, $google->getItems(UnviewableDataObject::class, 1)->count());
|
||||
}
|
||||
|
||||
public function testGetItemsWithCustomRoutes(): void
|
||||
{
|
||||
GoogleSitemap::register_routes([
|
||||
'/test-route/',
|
||||
'/someother-route/',
|
||||
'/fake-sitemap-route/'
|
||||
]);
|
||||
|
||||
$google = new GoogleSitemap();
|
||||
$items = $google->getItems('GoogleSitemapRoute', 1);
|
||||
$this->assertEquals(3, $items->count());
|
||||
}
|
||||
|
||||
public function testAccessingSitemapRootXMLFile(): void
|
||||
{
|
||||
GoogleSitemap::register_dataobject(TestDataObject::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');
|
||||
$body = $response->getBody();
|
||||
|
||||
$this->assertXmlStringEqualsXmlFile(__DIR__ . '/xml/' . __FUNCTION__ . '.xml', $body);
|
||||
}
|
||||
|
||||
public function testLastModifiedDateOnRootXML(): void
|
||||
{
|
||||
Config::inst()->set(GoogleSitemap::class, 'enabled', true);
|
||||
|
||||
if (!class_exists('Page')) {
|
||||
$this->markTestIncomplete('No cms module installed, page related test skipped');
|
||||
}
|
||||
|
||||
$page = $this->objFromFixture('Page', 'Page1');
|
||||
$page->publishSingle();
|
||||
$page->flushCache();
|
||||
|
||||
$p2 = $this->objFromFixture('Page', 'Page2');
|
||||
$p2->publishSingle();
|
||||
$p2->flushCache();
|
||||
|
||||
DB::query("UPDATE \"SiteTree_Live\" SET \"LastEdited\"='2014-03-14 00:00:00' WHERE \"ID\"='" . $page->ID . "'");
|
||||
DB::query("UPDATE \"SiteTree_Live\" SET \"LastEdited\"='2014-01-01 00:00:00' WHERE \"ID\"='" . $p2->ID . "'");
|
||||
|
||||
$response = $this->get('sitemap.xml');
|
||||
$body = $response->getBody();
|
||||
|
||||
$expected = '<lastmod>2014-03-14</lastmod>';
|
||||
|
||||
$this->assertEquals(
|
||||
1,
|
||||
substr_count($body, $expected),
|
||||
'The last mod date should use most recent LastEdited date'
|
||||
);
|
||||
}
|
||||
|
||||
public function testIndexFilePaginatedSitemapFiles(): void
|
||||
{
|
||||
$original = Config::inst()->get('GoogleSitemap', 'objects_per_sitemap');
|
||||
Config::inst()->set(GoogleSitemap::class, 'objects_per_sitemap', 1);
|
||||
GoogleSitemap::register_dataobject(TestDataObject::class);
|
||||
|
||||
|
||||
$obj = $this->objFromFixture(TestDataObject::class, 'DataObjectTest1');
|
||||
$obj1 = $this->objFromFixture(TestDataObject::class, 'DataObjectTest2');
|
||||
$obj2 = $this->objFromFixture(TestDataObject::class, 'UnindexedDataObject');
|
||||
|
||||
$t = $obj->baseTable();
|
||||
DB::query("UPDATE \"" . $t . "\" SET \"LastEdited\"='2023-02-13 00:00:00' WHERE \"ID\"='" . $obj->ID . "'");
|
||||
DB::query("UPDATE \"" . $t . "\" SET \"LastEdited\"='2023-02-13 00:00:00' WHERE \"ID\"='" . $obj1->ID . "'");
|
||||
DB::query("UPDATE \"" . $t . "\" SET \"LastEdited\"='2023-02-13 00:00:00' WHERE \"ID\"='" . $obj2->ID . "'");
|
||||
|
||||
|
||||
$response = $this->get('sitemap.xml');
|
||||
$body = $response->getBody();
|
||||
$this->assertXmlStringEqualsXmlFile(__DIR__ . '/xml/' . __FUNCTION__ . '.xml', $body);
|
||||
|
||||
Config::inst()->set(GoogleSitemap::class, 'objects_per_sitemap', $original);
|
||||
}
|
||||
|
||||
public function testRegisterRoutesIncludesAllRoutes(): void
|
||||
{
|
||||
GoogleSitemap::register_route('/test/');
|
||||
GoogleSitemap::register_routes([
|
||||
'/test/', // duplication should be replaced
|
||||
'/unittests/',
|
||||
'/anotherlink/'
|
||||
], 'weekly');
|
||||
|
||||
$response = $this->get('sitemap.xml/sitemap/GoogleSitemapRoute/1');
|
||||
$body = $response->getBody();
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), 'successful loaded nested sitemap');
|
||||
$this->assertEquals(3, substr_count($body, "<loc>"));
|
||||
}
|
||||
|
||||
public function testAccessingNestedSiteMap(): void
|
||||
{
|
||||
$original = Config::inst()->get('GoogleSitemap', 'objects_per_sitemap');
|
||||
Config::inst()->set(GoogleSitemap::class, 'objects_per_sitemap', 1);
|
||||
GoogleSitemap::register_dataobject(TestDataObject::class);
|
||||
|
||||
$response = $this->get('sitemap.xml/sitemap/Wilr-GoogleSitemaps-Tests-Model-TestDataObject/1');
|
||||
$body = $response->getBody();
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), 'successful loaded nested sitemap');
|
||||
|
||||
Config::inst()->set(GoogleSitemap::class, 'objects_per_sitemap', $original);
|
||||
}
|
||||
|
||||
public function testGetItemsWithPages(): void
|
||||
{
|
||||
if (!class_exists(SiteTree::class)) {
|
||||
$this->markTestIncomplete('No cms module installed, page related test skipped');
|
||||
}
|
||||
|
||||
$page = $this->objFromFixture('Page', 'Page1');
|
||||
$page->publishSingle();
|
||||
$page->flushCache();
|
||||
|
||||
$page2 = $this->objFromFixture('Page', 'Page2');
|
||||
$page2->publishSingle();
|
||||
$page2->flushCache();
|
||||
|
||||
$this->assertListContains([
|
||||
['Title' => 'Testpage1'],
|
||||
['Title' => 'Testpage2']
|
||||
], 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
|
||||
$page2->CanViewType = 'LoggedInUsers';
|
||||
$page2->write();
|
||||
$page2->publishSingle();
|
||||
|
||||
$this->logOut();
|
||||
|
||||
$this->assertListEquals([
|
||||
['Title' => 'Testpage1']
|
||||
], GoogleSitemap::inst()->getItems(SiteTree::class), "There should be only 1 page, other is logged in only");
|
||||
}
|
||||
|
||||
public function testAccess(): void
|
||||
{
|
||||
Config::inst()->set(GoogleSitemap::class, 'enabled', true);
|
||||
|
||||
$response = $this->get('sitemap.xml');
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), 'Sitemap returns a 200 success when enabled');
|
||||
$this->assertEquals('application/xml; charset="utf-8"', $response->getHeader('Content-Type'));
|
||||
|
||||
GoogleSitemap::register_dataobject(TestDataObject::class);
|
||||
$response = $this->get('sitemap.xml/sitemap/Wilr-GoogleSitemaps-Tests-Model-TestDataObject/1');
|
||||
$this->assertEquals(200, $response->getStatusCode(), 'Sitemap returns a 200 success when enabled');
|
||||
$this->assertEquals('application/xml; charset="utf-8"', $response->getHeader('Content-Type'));
|
||||
|
||||
Config::inst()->set(GoogleSitemap::class, 'enabled', false);
|
||||
|
||||
$response = $this->get('sitemap.xml');
|
||||
$this->assertEquals(404, $response->getStatusCode(), 'Sitemap index returns a 404 when disabled');
|
||||
|
||||
$response = $this->get('sitemap.xml/sitemap/Wilr-GoogleSitemaps-Tests-Model-TestDataObject/1');
|
||||
$this->assertEquals(404, $response->getStatusCode(), 'Sitemap file returns a 404 when disabled');
|
||||
}
|
||||
|
||||
public function testDecoratorAddsFields(): void
|
||||
{
|
||||
if (!class_exists("Page")) {
|
||||
$this->markTestIncomplete('No cms module installed, page related test skipped');
|
||||
}
|
||||
|
||||
$page = $this->objFromFixture('Page', 'Page1');
|
||||
|
||||
$fields = $page->getSettingsFields();
|
||||
$tab = $fields->fieldByName('Root')->fieldByName('Settings')->fieldByName('GoogleSitemap');
|
||||
|
||||
$this->assertInstanceOf(Tab::class, $tab);
|
||||
$this->assertInstanceOf(DropdownField::class, $tab->fieldByName('Priority'));
|
||||
$this->assertInstanceOf(LiteralField::class, $tab->fieldByName('GoogleSitemapIntro'));
|
||||
}
|
||||
|
||||
public function testGetPriority(): void
|
||||
{
|
||||
if (!class_exists("Page")) {
|
||||
$this->markTestIncomplete('No cms module installed, page related test skipped');
|
||||
}
|
||||
|
||||
$page = $this->objFromFixture('Page', 'Page1');
|
||||
|
||||
// invalid field doesn't break google
|
||||
$page->Priority = 'foo';
|
||||
$this->assertEquals(0.5, $page->getGooglePriority());
|
||||
|
||||
// custom value (set as string as db field is varchar)
|
||||
$page->Priority = '0.2';
|
||||
$this->assertEquals(0.2, $page->getGooglePriority());
|
||||
|
||||
// -1 indicates that we should not index this
|
||||
$page->Priority = -1;
|
||||
$this->assertFalse($page->getGooglePriority());
|
||||
}
|
||||
|
||||
public function testUnpublishedPage(): void
|
||||
{
|
||||
if (!class_exists('SilverStripe\CMS\Model\SiteTree')) {
|
||||
$this->markTestSkipped('Test skipped; CMS module required for testUnpublishedPage');
|
||||
}
|
||||
|
||||
$orphanedPage = new \SilverStripe\CMS\Model\SiteTree();
|
||||
$orphanedPage->ParentID = 999999; // missing parent id
|
||||
$orphanedPage->write();
|
||||
$orphanedPage->publishSingle();
|
||||
|
||||
$rootPage = new \SilverStripe\CMS\Model\SiteTree();
|
||||
$rootPage->ParentID = 0;
|
||||
$rootPage->write();
|
||||
$rootPage->publishSingle();
|
||||
|
||||
$oldMode = Versioned::get_reading_mode();
|
||||
Versioned::set_reading_mode('Live');
|
||||
|
||||
try {
|
||||
$this->assertEmpty($orphanedPage->hasPublishedParent());
|
||||
$this->assertEmpty($orphanedPage->canIncludeInGoogleSitemap());
|
||||
$this->assertNotEmpty($rootPage->hasPublishedParent());
|
||||
$this->assertNotEmpty($rootPage->canIncludeInGoogleSitemap());
|
||||
} catch (Exception $ex) {
|
||||
Versioned::set_reading_mode($oldMode);
|
||||
throw $ex;
|
||||
} // finally {
|
||||
Versioned::set_reading_mode($oldMode);
|
||||
// }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
Wilr\GoogleSitemaps\Tests\Model\TestDataObject:
|
||||
DataObjectTest1:
|
||||
Priority: 0.4
|
||||
DataObjectTest2:
|
||||
Priority: 0.2
|
||||
UnindexedDataObject:
|
||||
Priority: -1
|
||||
|
||||
Wilr\GoogleSitemaps\Tests\Model\OtherDataObject:
|
||||
OtherDataObjectTest2:
|
||||
Priority: 0.3
|
||||
|
||||
Wilr\GoogleSitemaps\Tests\Model\UnviewableDataObject:
|
||||
Unviewable1:
|
||||
Priority: 0.4
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Wilr\GoogleSitemaps\Tests\Model;
|
||||
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\Control\Director;
|
||||
|
||||
class OtherDataObject extends DataObject implements TestOnly
|
||||
{
|
||||
private static $db = array(
|
||||
'Priority' => 'Varchar(10)'
|
||||
);
|
||||
|
||||
public function canView($member = null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function AbsoluteLink()
|
||||
{
|
||||
return Director::absoluteBaseURL();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace Wilr\GoogleSitemaps\Tests\Model;
|
||||
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\Control\Director;
|
||||
|
||||
class TestDataObject extends DataObject implements TestOnly
|
||||
{
|
||||
protected $private = false;
|
||||
|
||||
private static $db = array(
|
||||
'Priority' => 'Varchar(10)'
|
||||
);
|
||||
|
||||
public function canView($member = null)
|
||||
{
|
||||
if ($this->private) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public function setPrivate()
|
||||
{
|
||||
$this->private = true;
|
||||
}
|
||||
|
||||
public function AbsoluteLink()
|
||||
{
|
||||
return Director::absoluteBaseURL();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace Wilr\GoogleSitemaps\Tests\Model;
|
||||
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\Control\Director;
|
||||
|
||||
class UnviewableDataObject extends DataObject implements TestOnly
|
||||
{
|
||||
|
||||
private static $db = array(
|
||||
'Priority' => 'Varchar(10)'
|
||||
);
|
||||
|
||||
public function canView($member = null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function AbsoluteLink()
|
||||
{
|
||||
return Director::absoluteBaseURL();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml-stylesheet type='text/xsl' href='http://localhost/sitemap.xml/styleSheetIndex'?>
|
||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<sitemap>
|
||||
<loc>http://localhost/sitemap.xml/sitemap/Wilr-GoogleSitemaps-Tests-Model-TestDataObject/1</loc>
|
||||
<lastmod>2023-02-13</lastmod>
|
||||
</sitemap>
|
||||
<sitemap>
|
||||
<loc>http://localhost/sitemap.xml/sitemap/Wilr-GoogleSitemaps-Tests-Model-OtherDataObject/1</loc>
|
||||
<lastmod>2023-02-13</lastmod>
|
||||
</sitemap>
|
||||
</sitemapindex>
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml-stylesheet type='text/xsl' href='http://localhost/sitemap.xml/styleSheetIndex'?>
|
||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<sitemap>
|
||||
<loc>http://localhost/sitemap.xml/sitemap/Wilr-GoogleSitemaps-Tests-Model-TestDataObject/1</loc>
|
||||
<lastmod>2023-02-13</lastmod>
|
||||
</sitemap>
|
||||
<sitemap>
|
||||
<loc>http://localhost/sitemap.xml/sitemap/Wilr-GoogleSitemaps-Tests-Model-TestDataObject/2</loc>
|
||||
<lastmod>2023-02-13</lastmod>
|
||||
</sitemap>
|
||||
<sitemap>
|
||||
<loc>http://localhost/sitemap.xml/sitemap/Wilr-GoogleSitemaps-Tests-Model-TestDataObject/3</loc>
|
||||
<lastmod>2023-02-13</lastmod>
|
||||
</sitemap>
|
||||
</sitemapindex>
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml-stylesheet type='text/xsl' href='http://localhost/sitemap.xml/styleSheetIndex'?>
|
||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<sitemap>
|
||||
<loc>http://localhost/sitemap.xml/sitemap/GoogleSitemapRoute/1</loc>
|
||||
</sitemap>
|
||||
</sitemapindex>
|
Loading…
Reference in New Issue