...
 
Commits (410)
## CONTENTS OF THIS FILE ##
* Introduction
* Installation
* Configuration
* Usage
* Extending the module
* How Can You Contribute?
* Maintainers
## INTRODUCTION ##
Author and maintainer: Pawel Ginalski (gbyte.co)
* Drupal: https://www.drupal.org/u/gbyte.co
* Personal: https://gbyte.co/
The module generates multilingual XML sitemaps which adhere to Google's new
hreflang standard. Out of the box the sitemaps index most of Drupal's
content entity types including:
* nodes
* taxonomy terms
* menu links
* users
* ...
Contributed entity types like commerce products or media entities can be indexed
as well. On top of that custom links and view pages can be added to sitemaps.
To learn about XML sitemaps, see https://en.wikipedia.org/wiki/Sitemaps.
The module also provides an API allowing to create any type of sitemap (not
necessary an XML one) holding links to a local or remote source.
## INSTALLATION ##
See https://www.drupal.org/documentation/install/modules-themes/modules-8
for instructions on how to install or update Drupal modules.
## CONFIGURATION ##
### PERMISSIONS ###
The module permission 'administer sitemap settings' can be configured under
/admin/people/permissions.
### ENTITIES ###
Initially only the home page is indexed in the default sitemap variant. To
include content into a sitemap, visit
/admin/config/search/simplesitemap/entities to enable support for entity types
of your choosing. Bundleless entity types can be configured right on that page,
for bundles of entity types visit the bundle's configuration pages, e.g.
* /admin/structure/types/manage/[content type] for nodes
* /admin/structure/taxonomy/manage/[taxonomy vocabulary] for taxonomy terms
* /admin/structure/menu/manage/[menu] for menu items
* ...
When including an entity type or bundle into a sitemap, the priority setting
can be set which will set the 'priority' parameter for all entities of that
type. Same goes for the 'changefreq' setting. All Images referenced by the
entities can be indexed as well. See https://en.wikipedia.org/wiki/Sitemaps to
learn more about these parameters.
Inclusion settings of bundles can be overridden on a per-entity
basis. Just head over to a bundle instance edit form (e.g. node/1/edit) to
override its sitemap settings.
If you wish for the sitemap to reflect the new configuration instantly, check
'Regenerate sitemaps after clicking save'. This setting only appears if a change
in the settings has been detected.
Once variants are set up in admin/config/search/simplesitemap/variants, all the
above settings can be configured and overwritten on a per variant basis right
from the UI.
As the sitemaps are accessible to anonymous users, bear in mind that only links
will be included which are accessible to anonymous users. There are no access
checks for links added through the module's hooks (see below).
### VIEWS ###
To index views, enable the included, optional module Simple XML Sitemap (Views)
(simple_sitemap_views). Simple views as well as views with arguments can be
indexed on the view edit page. For views with arguments, links to all view
variants will be included in the sitemap.
### CUSTOM LINKS ###
To include custom links into a sitemap, visit
/admin/config/search/simplesitemap/custom.
### SETTINGS ###
The settings page can be found under admin/config/search/simplesitemap.
Here the module can be configured and the sitemaps can be manually regenerated.
#### VARIANTS ####
It is possible to have several sitemap instances of different sitemap types with
specific links accessible under certain URLs. These sitemap variants can be
configured under admin/config/search/simplesitemap/variants.
#### AUTOMATIC SUBMISSION ####
It is possible to have the module automatically submit specific sitemap
variants to search engines. Google and Bing are preconfigured. This
functionality is available through the included simple_sitemap_engines
submodule. After enabling this module, go to
admin/config/search/simplesitemap/engines/settings to set it up.
## USAGE ##
The sitemaps are accessible to the whole world under [variant name]/sitemap.xml.
In addition to that, the default sitemap is accessible under /sitemap.xml. To
view the XML source, press ctrl+u.
If the cron generation is turned on, the sitemaps will be regenerated according
to the 'Sitemap generation interval' setting.
A manual generation is possible on admin/config/search/simplesitemap. This is
also the place that shows the overall and variant specific generation status.
The sitemap can be also generated via drush: Use the command
'drush simple-sitemap:generate' ('ssg'), or 'drush simple-sitemap:rebuild-queue'
('ssr').
Generation of hundreds of thousands of links can take time. Each variant gets
published as soon as all of its links have been generated. The previous version
of the sitemap variant is accessible during the generation process.
## EXTENDING THE MODULE ##
### API ###
There are API methods for altering stored inclusion settings, status queries and
programmatic sitemap generation. These include:
* getSetting
* saveSetting
* setVariants
* getSitemap
* removeSitemap
* generateSitemap
* rebuildQueue
* enableEntityType
* disableEntityType
* setBundleSettings
* getBundleSettings
* removeBundleSettings
* setEntityInstanceSettings
* getEntityInstanceSettings
* removeEntityInstanceSettings
* bundleIsIndexed
* entityTypeIsEnabled
* addCustomLink
* getCustomLinks
* removeCustomLinks
* getSitemapManager
* getSitemapVariants
* addSitemapVariant
* removeSitemapVariants
* getQueueWorker
* deleteQueue
* rebuildQueue
* getInitialElementCount
* getQueuedElementCount
* getStashedResultCount
* getProcessedElementCount
* generationInProgress
These service methods can be chained like so:
```php
$generator = \Drupal::service('simple_sitemap.generator');
$generator
->getSitemapManager()
->addSitemapVariant('test');
$generator
->saveSetting('remove_duplicates', TRUE)
->enableEntityType('node')
->setVariants(['default', 'test'])
->setBundleSettings('node', 'page', ['index' => TRUE, 'priority' => 0.5])
->removeCustomLinks()
->addCustomLink('/some/view/page', ['priority' => 0.5])
->generateSitemap();
```
See https://gbyte.co/projects/simple-xml-sitemap and code documentation in
Drupal\simple_sitemap\Simplesitemap for further details.
### API HOOKS ###
It is possible to hook into link generation by implementing
`hook_simple_sitemap_links_alter(&$links, $sitemap_variant){}` in a custom module and altering the
link array shortly before it is transformed to XML.
Adding arbitrary links is possible through the use of
`hook_simple_sitemap_arbitrary_links_alter(&$arbitrary_links, $sitemap_variant){}`. There are no
checks performed on these links (i.e. if they are internal/valid/accessible)
and parameters like priority/lastmod/changefreq have to be added manually.
Altering sitemap attributes and sitemap index attributes is possible through the
use of `hook_simple_sitemap_attributes_alter(&$attributes, $sitemap_variant){}` and
`hook_simple_sitemap_index_attributes_alter(&$index_attributes, $sitemap_variant){}`.
Altering URL generators is possible through
the use of `hook_simple_sitemap_url_generators_alter(&$url_generators){}`.
Altering sitemap generators is possible through
the use of `hook_simple_sitemap_sitemap_generators_alter(&$sitemap_generators){}`.
Altering sitemap types is possible through
the use of `hook_simple_sitemap_sitemap_types_alter(&$sitemap_types){}`.
### WRITING PLUGINS ###
There are three types of plugins that allow to create any type of sitemap. See
the generator plugins included in this module and check the API docs
(https://www.drupal.org/docs/8/api/plugin-api/plugin-api-overview) to learn how
to implement plugins.
#### SITEMAP TYPE PLUGINS ####
This plugin defines a sitemap type. A sitemap type consists of a sitemap
generator and several URL generators. This plugin is very simple, as it
only requires some class annotation to define which sitemap/URL plugins to use.
#### SITEMAP GENERATOR PLUGINS ####
This plugin defines how a sitemap type is supposed to look. It handles all
aspects of the sitemap except its links/URLs.
#### URL GENERATOR PLUGINS ####
This plugin defines a way of generating URLs for one or more sitemap types.
Note:
Overwriting the default EntityUrlGenerator for a single entity type is possible
through the flag "overrides_entity_type" = "[entity_type_to_be_overwritten]" in
the settings array of the new generator plugin's annotation. See how the
EntityUrlGenerator is overwritten by the EntityMenuLinkContentUrlGenerator to
facilitate a different logic for menu links.
See https://gbyte.co/projects/simple-xml-sitemap for further details.
## HOW CAN YOU CONTRIBUTE? ##
* Report any bugs, feature or support requests in the issue tracker; if
possible help out by submitting patches.
http://drupal.org/project/issues/simple_sitemap
* Do you know a non-English language? Help translating the module.
https://localize.drupal.org/translate/projects/simple_sitemap
* If you would like to say thanks and support the development of this module, a
donation will be much appreciated.
https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=5AFYRSBLGSC3W
* Feel free to contact me for paid support: https://gbyte.co/contact
## MAINTAINERS ##
Current maintainers:
* Pawel Ginalski (gbyte.co) - https://www.drupal.org/u/gbyte.co
{
"name": "drupal/simple_sitemap",
"description": "Creates a standard conform hreflang XML sitemap of the site content and provides a framework for developing other sitemap types.",
"type": "drupal-module",
"homepage": "https://drupal.org/project/simple_sitemap",
"authors": [
{
"name": "Pawel Ginalski (gbyte.co)",
"email": "contact@gbyte.co",
"homepage": "https://www.drupal.org/u/gbyte.co",
"role": "Maintainer"
}
],
"support": {
"issues": "https://drupal.org/project/issues/simple_sitemap",
"irc": "irc://irc.freenode.org/drupal-contribute",
"source": "https://cgit.drupalcode.org/simple_sitemap"
},
"license": "GPL-2.0+",
"minimum-stability": "dev",
"require": {
"ext-xmlwriter": "*"
},
"extra": {
"drush": {
"services": {
"drush.services.yml": "^9"
}
}
}
}
links:
-
path: '/'
priority: '1.0'
changefreq: 'daily'
max_links: 2000
cron_generate: true
cron_generate_interval: 0
generate_duration: 10000
remove_duplicates: true
skip_untranslated: true
xsl: true
base_url: ''
default_variant: 'default'
custom_links_include_images: false
excluded_languages: []
enabled_entity_types:
- 'node'
- 'taxonomy_term'
- 'menu_link_content'
variants:
default:
label: 'Default'
weight: 0
custom:
-
path: /
priority: 1
entity_types: []
settings:
max_links: 2000
cron_generate: true
simple_sitemap.settings:
type: config_object
mapping:
max_links:
label: 'Max links'
type: integer
cron_generate:
label: 'Cron generation'
type: boolean
cron_generate_interval:
label: 'Cron generation interval'
type: integer
generate_duration:
label: 'Generation duration'
type: integer
remove_duplicates:
label: 'Remove duplicates'
type: boolean
skip_untranslated:
label: 'Skip untranslated'
type: boolean
disable_language_hreflang:
label: 'Disable language hreflang'
type: boolean
xsl:
label: 'Include a stylesheet in the sitemaps for humans'
type: boolean
base_url:
label: 'Base URL'
type: string
default_variant:
label: 'Default variant'
type: string
custom_links_include_images:
label: 'Include images of custom links'
type: boolean
excluded_languages:
label: 'Excluded languages'
type: sequence
sequence:
type: string
enabled_entity_types:
label: 'Enabled entity types'
type: sequence
sequence:
type: string
simple_sitemap.bundle_settings.*.*.*:
label: 'Entity bundle settings'
type: config_object
mapping:
index:
label: 'Index'
type: boolean
priority:
label: 'Priority'
type: string
changefreq:
label: 'Change frequency'
type: string
include_images:
label: 'Include images'
type: boolean
simple_sitemap.custom_links.*:
label: 'Custom links'
type: config_object
mapping:
links:
label: 'Custom links'
type: sequence
sequence:
type: mapping
mapping:
path:
label: 'Path'
type: string
priority:
label: 'Priority'
type: string
changefreq:
label: 'Change frequency'
type: string
simple_sitemap.variants.*:
label: 'Sitemap variants'
type: config_object
mapping:
variants:
label: 'Sitemap variants'
type: sequence
sequence:
type: mapping
mapping:
label:
label: 'Variant label'
type: string
weight:
label: 'Weight'
type: integer
#simple-sitemap-settings-form .progress__bar {
background-image: none;
}
build:
assessment:
validate_codebase:
phplint:
container_composer:
phpcs:
# phpcs will use core's specified version of Coder.
sniff-all-files: true
halt-on-fail: false
testing:
# run_tests task is executed several times in order of performance speeds.
# halt-on-fail can be set on the run_tests tasks in order to fail fast.
# suppress-deprecations is false in order to be alerted to usages of
# deprecated code.
run_tests.standard:
types: 'Simpletest,PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional'
testgroups: '--all'
suppress-deprecations: false
run_tests.js:
types: 'PHPUnit-FunctionalJavascript'
testgroups: '--all'
suppress-deprecations: false
nightwatchjs: { }
services:
simple_sitemap.commands:
class: \Drupal\simple_sitemap\Commands\SimplesitemapCommands
arguments:
- '@simple_sitemap.generator'
tags:
- { name: drush.command }
/**
* @file
* Attaches simple_sitemap behaviors to the entity form.
*/
(function($) {
"use strict";
Drupal.behaviors.simple_sitemapFieldsetSummaries = {
attach: function(context, settings) {
$(context).find('#edit-simple-sitemap').drupalSetSummary(function(context) {
var enabledVariants = [];
$('input:radio.enabled-for-variant').each(function() {
if ($(this).is(':checked') && $(this).val() == 1) {
enabledVariants.push($(this).attr('class').split(' ')[1])
}
});
if (enabledVariants.length > 0) {
return Drupal.t('Included in sitemap variants: ') + enabledVariants.join(', ');
}
else {
return Drupal.t('Excluded from all sitemap variants');
}
});
}
};
})(jQuery);
/**
* @file
* Attaches simple_sitemap behaviors to the sitemap entities form.
*/
(function($) {
"use strict";
Drupal.behaviors.simple_sitemapSitemapEntities = {
attach: function(context, settings) {
$.each(settings.simple_sitemap.all_entities, function(index, entityId) {
var target = '#edit-' + entityId + '-enabled';
triggerVisibility(target, entityId);
$(target).change(function() {
triggerVisibility(target, entityId);
});
});
function triggerVisibility(target, entityId) {
if ($(target).is(':checked')) {
$('#warning-' + entityId).hide();
$('#indexed-bundles-' + entityId).show();
}
else {
$('#warning-' + entityId).show();
$('#indexed-bundles-' + entityId).hide();
}
}
}
};
})(jQuery);
/**
* @file
* Attaches simplesitemap behaviors to the entity form.
*/
(function($) {
"use strict";
Drupal.behaviors.simplesitemapFieldsetSummaries = {
attach: function(context) {
$(context).find('#edit-simplesitemap').drupalSetSummary(function (context) {
var vals = [];
if ($(context).find('#edit-simplesitemap-index-content').is(':checked')) {
// Display summary of the settings in tabs.
vals.push(Drupal.t('Included in sitemap'));
vals.push(Drupal.t('Priority') + ' ' + $('#edit-simplesitemap-priority option:selected', context).text());
}
else {
// Display summary of the settings in tabs.
vals.push(Drupal.t('Excluded from sitemap'));
}
return vals.join('<br />');
});
}
};
})(jQuery);
/**
* @file
* Attaches simplesitemap behaviors to the entity form.
*
* @todo: Tidy up.
*/
(function($) {
"use strict";
// Hide the 'Regenerate sitemap' field to only display it if settings have changed.
$('.form-item-simplesitemap-regenerate-now').hide();
Drupal.behaviors.simplesitemapForm = {
attach: function(context) {
if ($(context).find('#edit-simplesitemap-index-content').is(':checked')) {
// Show 'Priority' field if 'Index sitemap' is ticked.
$('.form-item-simplesitemap-priority').show();
}
else { // Hide 'Priority' field if 'Index sitemap' is unticked.
$('.form-item-simplesitemap-priority').hide();
}
// Show 'Regenerate sitemap' field if setting has changed.
$( "#edit-simplesitemap-index-content" ).change(function() {
$('.form-item-simplesitemap-regenerate-now').show();
if ($(context).find('#edit-simplesitemap-index-content').is(':checked')) {
// Show 'Priority' field if 'Index sitemap' is ticked.
$('.form-item-simplesitemap-priority').show();
}
else { // Hide 'Priority' field if 'Index sitemap' is unticked.
$('.form-item-simplesitemap-priority').hide();
}
});
// Show 'Regenerate sitemap' field if setting has changed.
$( "#edit-simplesitemap-priority" ).change(function() {
$('.form-item-simplesitemap-regenerate-now').show();
});
}
};
})(jQuery);
id: bing
label: 'Bing'
url: http://www.bing.com/ping?sitemap=[sitemap]
sitemap_variants: { }
id: google
label: 'Google'
url: http://www.google.com/ping?sitemap=[sitemap]
sitemap_variants: { }
simple_sitemap_engines.simple_sitemap_engine.*:
type: config_entity
label: 'Search engine'
mapping:
id:
type: string
label: 'Search engine ID'
label:
type: label
label: 'Label'
url:
type: string
label: 'Submission URL'
sitemap_variants:
type: sequence
label: 'Sitemap variants'
sequence:
type: string
label: 'Sitemap variant'
last_submitted:
type: integer
label: 'Last submitted'
simple_sitemap_engines.settings:
type: config_object
label: 'Sitemap search engine submission settings'
mapping:
enabled:
type: boolean
label: 'Sitemap submission enabled'
submission_interval:
type: integer
label: 'Sitemap submission frequency'
name: 'Simple XML Sitemap (Search engines)'
type: module
description: 'Submits sitemaps to search engines.'
configure: simple_sitemap_engines.settings
package: SEO
core: 8.x
dependencies:
- simple_sitemap:simple_sitemap
simple_sitemap_engines:
route_name: entity.simple_sitemap_engine.status
title: 'Search engines'
base_route: simple_sitemap.settings
weight: 5
simple_sitemap_engines.status:
route_name: entity.simple_sitemap_engine.status
title: 'Status'
parent_id: simple_sitemap_engines
weight: 1
simple_sitemap_engines.settings:
route_name: simple_sitemap_engines.settings
title: 'Settings'
parent_id: simple_sitemap_engines
weight: 2
<?php
/**
* @file
* Submits sitemaps to search engines.
*/
/**
* Implements hook_cron().
*
* If the sitemap submission interval has elapsed, adds each search engine to
* the submission queue to be processed.
*
* @see Drupal\simple_sitemap_engines\Plugin\QueueWorker\SitemapSubmitter
*/
function simple_sitemap_engines_cron() {
/** @var \Drupal\Core\Config\Config $config */
$config = \Drupal::config('simple_sitemap_engines.settings');
if ($config->get('enabled')) {
$interval = (int) $config->get('submission_interval') * 60 * 60;
$request_time = \Drupal::service('datetime.time')->getRequestTime();
$state = \Drupal::state();
if ($interval === 0
|| $state->get('simple_sitemap_engines_last_submitted', 0) + $interval <= $request_time) {
/** @var \Drupal\Core\Queue\QueueInterface $queue */
$queue = \Drupal::queue('simple_sitemap_engine_submit');
$state->set('simple_sitemap_engines_last_submitted', $request_time);
foreach (\Drupal::entityTypeManager()
->getStorage('simple_sitemap_engine')
->loadMultiple() as $id => $engine) {
if (!empty($engine->sitemap_variants)) {
$queue->createItem($id);
}
}
}
}
}
entity.simple_sitemap_engine.status:
path: '/admin/config/search/simplesitemap/engines'
defaults:
_entity_list: 'simple_sitemap_engine'
_title: 'Simple XML Sitemap Settings'
requirements:
_permission: 'administer sitemap settings'
simple_sitemap_engines.settings:
path: '/admin/config/search/simplesitemap/engines/settings'
defaults:
_form: '\Drupal\simple_sitemap_engines\Form\SimplesitemapEnginesForm'
_title: 'Simple XML Sitemap Settings'
requirements:
_permission: 'administer sitemap settings'
<?php
namespace Drupal\simple_sitemap_engines\Controller;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\simple_sitemap\Form\FormHelper;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Search engine entity list builder.
*/
class SearchEngineListBuilder extends ConfigEntityListBuilder {
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* SearchEngineListBuilder constructor.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage class.
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, DateFormatterInterface $date_formatter) {
parent::__construct($entity_type, $storage);
$this->dateFormatter = $date_formatter;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.manager')->getStorage($entity_type->id()),
$container->get('date.formatter')
);
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['label'] = $this->t('Name');
$header['url'] = $this->t('Submission URL');
$header['variants'] = $this->t('Sitemap variants');
$header['last_submitted'] = $this->t('Last submitted');
return $header;
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
/** @var \Drupal\simple_sitemap_engines\Entity\SearchEngine $entity */
$row['label'] = $entity->label();
$row['url'] = $entity->url;
$row['variants'] = implode(', ', $entity->sitemap_variants);
$row['last_submitted'] = $entity->last_submitted
? $this->dateFormatter->format($entity->last_submitted, 'short')
: $this->t('Never');
return $row;
}
public function render() {
return ['simple_sitemap_engines' => [
'#prefix' => FormHelper::getDonationText(),
'#title' => $this->t('Submission status'),
'#type' => 'fieldset',
'table' => parent::render(),
]];
}
}
<?php
namespace Drupal\simple_sitemap_engines\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
/**
* Defines the the search engine entity class.
*
* @ConfigEntityType(
* id = "simple_sitemap_engine",
* label = @Translation("Search engine"),
* admin_permission = "administer sitemap settings",
* entity_keys = {
* "id" = "id",
* "label" = "label",
* },
* handlers = {
* "list_builder" = "Drupal\simple_sitemap_engines\Controller\SearchEngineListBuilder",
* },
* links = {
* "collection" = "/admin/config/search/simplesitemap/engines/list",
* },
* config_export = {
* "id",
* "label",
* "url",
* "sitemap_variants",
* "last_submitted",
* }
* )
*/
class SearchEngine extends ConfigEntityBase {
/**
* The search engine ID.
*
* @var string
*/
public $id;
/**
* The search engine label.
*
* @var string
*/
public $label;
/**
* The search engine submission URL.
*
* When submitting to search engines, '[sitemap]' will be replaced with the
* full URL to the sitemap.xml.
*
* @var string
*/
public $url;
/**
* List of sitemap variants to be submitted to this search engine.
*
* @var array
*/
public $sitemap_variants;
/**
* Timestamp when the sitemap was last submitted to this search engine.
*
* @var int
*/
public $last_submitted;
/**
* Implements magic __toString() to simplify checkbox list building.
*
* @return string
* The search engine label.
*/
public function __toString() {
return $this->label();
}
}
<?php
namespace Drupal\simple_sitemap_engines\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\simple_sitemap\Form\FormHelper;
use Drupal\simple_sitemap\SimplesitemapManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form for managing search engine submission settings.
*/
class SimplesitemapEnginesForm extends ConfigFormBase {
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatter
*/
protected $dateFormatter;
/**
* The sitemap manager service.
*
* @var \Drupal\simple_sitemap\SimplesitemapManager
*/
protected $sitemapManager;
/**
* SimplesitemapEnginesForm constructor.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Datetime\DateFormatter $date_formatter
* The date formatter service.
* @param \Drupal\simple_sitemap\SimplesitemapManager $sitemap_manager
* The sitemap manager service.
*/
public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, DateFormatter $date_formatter, SimplesitemapManager $sitemap_manager) {
parent::__construct($config_factory);
$this->entityTypeManager = $entity_type_manager;
$this->dateFormatter = $date_formatter;
$this->sitemapManager = $sitemap_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('entity_type.manager'),
$container->get('date.formatter'),
$container->get('simple_sitemap.manager')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'simple_sitemap_engines_settings_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['simple_sitemap_engines.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('simple_sitemap_engines.settings');
$engines = $this->entityTypeManager->getStorage('simple_sitemap_engine')->loadMultiple();
$variants = array_map(
function ($variant) { return $this->t($variant['label']); },
$this->sitemapManager->getSitemapVariants(NULL, FALSE)
);
$form['#tree'] = TRUE;
$form['engines'] = [
'#type' => 'fieldset',
'#title' => $this->t('Search engines'),
'#markup' => '<div class="description">' . $this->t('Configure sitemap variants to submit to search engines.') . '</div>',
'#prefix' => FormHelper::getDonationText(),
];
foreach ($engines as $engine_id => $engine) {
$form['engines'][$engine_id] = [
'#type' => 'details',
'#title' => $engine->label(),
'#open' => !empty($engine->sitemap_variants) || count($engines) == 1,
];
$form['engines'][$engine_id]['variants'] = [
'#type' => 'select',
'#title' => $this->t('Sitemap variants'),
'#options' => $variants,
'#default_value' => $engine->sitemap_variants,
'#multiple' => TRUE,
];
}
$form['settings'] = [
'#type' => 'fieldset',
'#title' => $this->t('Submission settings'),
];
$form['settings']['enabled'] = [
'#type' => 'checkbox',
'#title' => $this->t('Submit the sitemap to search engines'),
'#default_value' => $config->get('enabled'),
];
$form['settings']['submission_interval'] = [
'#type' => 'select',
'#title' => $this->t('Submission interval'),
'#options' => FormHelper::getCronIntervalOptions(),
'#default_value' => $config->get('submission_interval'),
'#states' => [
'visible' => [':input[name="enabled"]' => ['checked' => TRUE]],
],
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$engines = $this->entityTypeManager->getStorage('simple_sitemap_engine')->loadMultiple();
foreach ($engines as $engine_id => $engine) {
$engine->sitemap_variants = $form_state->getValue(['engines', $engine_id, 'variants']);
$engine->save();
}
$config = $this->config('simple_sitemap_engines.settings');
$config->set('enabled', $form_state->getValue(['settings', 'enabled']));
$config->set('submission_interval', $form_state->getValue(['settings', 'submission_interval']));
$config->save();
}
}
<?php
namespace Drupal\simple_sitemap_engines\Plugin\QueueWorker;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\simple_sitemap\SimplesitemapManager;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Process a queue of search engines to submit sitemaps.
*
* @QueueWorker(
* id = "simple_sitemap_engine_submit",
* title = @Translation("Sitemap search engine submission"),
* cron = {"time" = 30}
* )
*
* @see simple_sitemap_engines_cron()
*/
class SitemapSubmitter extends QueueWorkerBase implements ContainerFactoryPluginInterface {
/**
* The search engine entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $engineStorage;
/**
* The HTTP client service.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $httpClient;
/**
* The sitemap manager service.
*
* @var \Drupal\simple_sitemap\SimplesitemapManager
*/
protected $sitemapManager;
/**
* The simple sitemap logger.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* Constructs a new class instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $engine_storage
* The search engine entity storage.
* @param \GuzzleHttp\ClientInterface $http_client
* The HTTP client service.
* @param \Drupal\simple_sitemap\SimplesitemapManager $sitemap_manager
* The sitemap manager service.
* @param \Psr\Log\LoggerInterface $logger
* The simple sitemap logger.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityStorageInterface $engine_storage, ClientInterface $http_client, SimplesitemapManager $sitemap_manager, LoggerInterface $logger) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->engineStorage = $engine_storage;
$this->httpClient = $http_client;
$this->sitemapManager = $sitemap_manager;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getStorage('simple_sitemap_engine'),
$container->get('http_client'),
$container->get('simple_sitemap.manager'),
$container->get('logger.factory')->get('simple_sitemap')
);
}
/**
* {@inheritdoc}
*/
public function processItem($engine_id) {
/** @var \Drupal\simple_sitemap_engines\Entity\SearchEngine $engine */
if ($engine = $this->engineStorage->load($engine_id)) {
// Gather URLs for all sitemap variants.
$sitemap_urls = [];
foreach ($this->sitemapManager->getSitemapTypes() as $type_name => $type_definition) {
$sitemap_generator = $this->sitemapManager->getSitemapGenerator($type_definition['sitemapGenerator']);
$variants = $this->sitemapManager->getSitemapVariants($type_name, FALSE);
if (!empty($variants)) {
// Submit all variants that are enabled for this search engine.
foreach ($variants as $id => $variant) {
if (in_array($id, $engine->sitemap_variants)) {
$sitemap_urls[$variant['label']] = $sitemap_generator->setSitemapVariant($id)->getSitemapUrl();
}
}
}
}
// Submit all URLs.
foreach ($sitemap_urls as $variant => $sitemap_url) {
$submit_url = str_replace('[sitemap]', $sitemap_url, $engine->url);
try {
$this->httpClient->request('GET', $submit_url);
// Log if submission was successful.
$this->logger->info('Sitemap %sitemap submitted to @url', ['%sitemap' => $variant, '@url' => $submit_url]);
// Record last submission time. This is purely informational; the
// variable that determines when the next submission should be run is
// stored in the global state.
$engine->last_submitted = time();
}
catch (RequestException $e) {
// Catch and log exceptions so this submission gets removed from the
// queue whether or not it succeeded.
// If the error was caused by network failure, it's fine to just wait
// until next time the submission is queued to try again.
// If the error was caused by a malformed URL, keeping the submission
// in the queue to retry is pointless since it will always fail.
watchdog_exception('simple_sitemap', $e);
}
}
$engine->save();
}
}
}
<?php
namespace Drupal\Tests\simple_sitemap_engines\Kernel;
use Drupal\KernelTests\KernelTestBase;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
use Prophecy\Argument;
// phpcs:disable Drupal.Arrays.Array.LongLineDeclaration
/**
* Tests search engine sitemap submission.
*
* @group simple_sitemap_engines
*/
class SubmitSitemapTest extends KernelTestBase {
/**
* The modules to enable.
*
* @var array
*/
public static $modules = ['system', 'simple_sitemap', 'simple_sitemap_engines'];
/**
* The cron service.
*
* @var \Drupal\Core\Cron
*/
protected $cron;
/**
* The search engine entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $engineStorage;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('simple_sitemap_engine');
$this->installConfig('simple_sitemap');
$this->installConfig('simple_sitemap_engines');
$this->cron = \Drupal::service('cron');
$this->engineStorage = \Drupal::entityTypeManager()->getStorage('simple_sitemap_engine');
$this->queue = \Drupal::queue('simple_sitemap_engine_submit');
// Set Google to submit the default sitemap variant. Other search engines
// will not submit anything.
$google = $this->engineStorage->load('google');
$google->sitemap_variants = ['default'];
$google->save();
}
/**
* Tests sitemap submission URLs and last submission status.
*/
public function testSubmission() {
// Create a mock HTTP client.
$http_client = $this->prophesize(ClientInterface::class);
// Make mock HTTP requests always succeed.
$http_client->request('GET', Argument::any())->willReturn(TRUE);
// Replace the default HTTP client service with the mock.
$this->container->set('http_client', $http_client->reveal());
// Run cron to trigger submission.
$this->cron->run();
$google = $this->engineStorage->load('google');
$bing = $this->engineStorage->load('bing');
// Check that Google was marked as submitted and Bing was not.
$this->assertNotEmpty($google->last_submitted);
$this->assertEmpty($bing->last_submitted);
// Check that exactly 1 HTTP request was sent to the correct URL.
$http_client->request('GET', 'http://www.google.com/ping?sitemap=http://localhost/default/sitemap.xml')->shouldBeCalled();
$http_client->request('GET', Argument::any())->shouldBeCalledTimes(1);
}
/**
* Tests that sitemaps are not submitted every time cron runs.
*/
public function testNoDoubleSubmission() {
// Create a mock HTTP client.
$http_client = $this->prophesize(ClientInterface::class);
// Make mock HTTP requests always succeed.
$http_client->request('GET', Argument::any())->willReturn(TRUE);
// Replace the default HTTP client service with the mock.
$this->container->set('http_client', $http_client->reveal());
// Run cron to trigger submission.
$this->cron->run();
// Check that Google was submitted and store its last submitted time.
$google = $this->engineStorage->load('google');
$http_client->request('GET', 'http://www.google.com/ping?sitemap=http://localhost/default/sitemap.xml')->shouldBeCalledTimes(1);
$this->assertNotEmpty($google->last_submitted);
$google_last_submitted = $google->last_submitted;
// Make sure enough time passes between cron runs to guarantee that they
// do not run within the same second, since timestamps are compared below.
sleep(2);
$this->cron->run();
$google = $this->engineStorage->load('google');
// Check that the last submitted time was not updated on the second cron
// run.
$this->assertEquals($google->last_submitted, $google_last_submitted);
// Check that no duplicate request was sent.
$http_client->request('GET', 'http://www.google.com/ping?sitemap=http://localhost/default/sitemap.xml')->shouldBeCalledTimes(1);
}
/**
* Tests that failed sitemap submissions are handled properly.
*/
public function testFailedSubmission() {
// Create a mock HTTP client.
$http_client = $this->prophesize(ClientInterface::class);
// Make mock HTTP requests always fail.
$http_client->request('GET', Argument::any())->willThrow(RequestException::class);
// Replace the default HTTP client service with the mock.
$this->container->set('http_client', $http_client->reveal());
// Run cron to trigger submission.
$this->cron->run();
$google = $this->engineStorage->load('google');
// Check that one request was attempted.
$http_client->request('GET', Argument::any())->shouldBeCalledTimes(1);
// Check the last submission time is still empty.
$this->assertEmpty($google->last_submitted);
// Check that the submission was removed from the queue despite failure.
$this->assertEquals(0, $this->queue->numberOfItems());
}
}
views.display_extender.simple_sitemap_display_extender:
type: views_display_extender
mapping:
index:
label: 'Index'
type: boolean
variant:
label: 'Sitemap variant'
type: string
priority:
label: 'Priority'
type: string
changefreq:
label: 'Change frequency'
type: string
arguments:
label: 'Indexed arguments'
type: sequence
sequence:
type: string
max_links:
label: 'Max links'
type: integer
/**
* @file
* Views UI helpers for Simple XML Sitemap display extender.
*/
(function ($, Drupal) {
Drupal.simpleSitemapViewsUi = {};
Drupal.behaviors.simpleSitemapViewsUiCheckboxify = {
attach: function attach() {
var $button = $('[data-drupal-selector="edit-index-button"]').once('simple-sitemap-views-ui-checkboxify');
if ($button.length) {
new Drupal.simpleSitemapViewsUi.Checkboxifier($button);
}
}
};
Drupal.behaviors.simpleSitemapViewsUiArguments = {
attach: