sitemap.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. <?php
  2. namespace Grav\Plugin;
  3. use Composer\Autoload\ClassLoader;
  4. use Grav\Common\Cache;
  5. use Grav\Common\Grav;
  6. use Grav\Common\Data;
  7. use Grav\Common\Language\Language;
  8. use Grav\Common\Page\Interfaces\PageInterface;
  9. use Grav\Common\Page\Page;
  10. use Grav\Common\Plugin;
  11. use Grav\Common\Twig\Twig;
  12. use Grav\Common\Uri;
  13. use Grav\Common\Page\Pages;
  14. use Grav\Common\Utils;
  15. use Grav\Plugin\Sitemap\SitemapEntry;
  16. use RocketTheme\Toolbox\Event\Event;
  17. use Twig\TwigFunction;
  18. class SitemapPlugin extends Plugin
  19. {
  20. /**
  21. * @var array
  22. */
  23. protected $sitemap = false;
  24. protected $route_data = [];
  25. protected $multilang_skiplang_prefix = null;
  26. protected $multilang_include_fallbacks = false;
  27. protected $multilang_enabled = true;
  28. protected $datetime_format = null;
  29. protected $include_change_freq = true;
  30. protected $default_change_freq = null;
  31. protected $include_priority = true;
  32. protected $default_priority = null;
  33. protected $ignores = null;
  34. protected $ignore_external = true;
  35. protected $ignore_protected = true;
  36. protected $ignore_redirect = true;
  37. /**
  38. * @return array
  39. */
  40. public static function getSubscribedEvents()
  41. {
  42. return [
  43. 'onPluginsInitialized' => [
  44. ['autoload', 100000], // TODO: Remove when plugin requires Grav >=1.7
  45. ['onPluginsInitialized', 0],
  46. ],
  47. 'onBlueprintCreated' => ['onBlueprintCreated', 0]
  48. ];
  49. }
  50. /**
  51. * Composer autoload.
  52. *is
  53. * @return ClassLoader
  54. */
  55. public function autoload(): ClassLoader
  56. {
  57. return require __DIR__ . '/vendor/autoload.php';
  58. }
  59. /**
  60. * Enable sitemap only if url matches to the configuration.
  61. */
  62. public function onPluginsInitialized()
  63. {
  64. if ($this->isAdmin()) {
  65. $this->active = false;
  66. return;
  67. }
  68. /** @var Uri $uri */
  69. $uri = $this->grav['uri'];
  70. $route = $this->config->get('plugins.sitemap.route');
  71. if ($route && $route == $uri->path()) {
  72. $this->enable([
  73. 'onTwigInitialized' => ['onTwigInitialized', 0],
  74. 'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0],
  75. 'onPagesInitialized' => ['onPagesInitialized', 0],
  76. 'onPageInitialized' => ['onPageInitialized', 0],
  77. 'onTwigSiteVariables' => ['onTwigSiteVariables', 0]
  78. ]);
  79. }
  80. }
  81. /**
  82. * Generate data for the sitemap.
  83. */
  84. public function onPagesInitialized()
  85. {
  86. /** @var Cache $cache */
  87. $cache = $this->grav['cache'];
  88. /** @var Pages $pages */
  89. $pages = $this->grav['pages'];
  90. $cache_id = md5('sitemap-data-'.$pages->getPagesCacheId());
  91. $this->sitemap = $cache->fetch($cache_id);
  92. if ($this->sitemap === false) {
  93. $this->multilang_enabled = $this->config->get('plugins.sitemap.multilang_enabled');
  94. /** @var Language $language */
  95. $language = $this->grav['language'];
  96. $default_lang = $language->getDefault() ?: 'en';
  97. $active_lang = $language->getActive() ?? $default_lang;
  98. $languages = $this->multilang_enabled && $language->enabled() ? $language->getLanguages() : [$default_lang];
  99. $this->multilang_skiplang_prefix = $this->config->get('system.languages.include_default_lang') ? '' : $language->getDefault();
  100. $this->multilang_include_fallbacks = $this->config->get('system.languages.pages_fallback_only') || !empty($this->config->get('system.languages.content_fallback'));
  101. $this->datetime_format = $this->config->get('plugins.sitemap.short_date_format') ? 'Y-m-d' : 'Y-m-d\TH:i:sP';
  102. $this->include_change_freq = $this->config->get('plugins.sitemap.include_changefreq');
  103. $this->default_change_freq = $this->config->get('plugins.sitemap.changefreq');
  104. $this->include_priority = $this->config->get('plugins.sitemap.include_priority');
  105. $this->default_priority = $this->config->get('plugins.sitemap.priority');
  106. $this->ignores = (array) $this->config->get('plugins.sitemap.ignores');
  107. $this->ignore_external = $this->config->get('plugins.sitemap.ignore_external');
  108. $this->ignore_protected = $this->config->get('plugins.sitemap.ignore_protected');
  109. $this->ignore_redirect = $this->config->get('plugins.sitemap.ignore_redirect');
  110. // Gather data for all languages
  111. foreach ($languages as $lang) {
  112. $language->init();
  113. $language->setActive($lang);
  114. $pages->reset();
  115. $this->addRouteData($pages, $lang);
  116. }
  117. // Reset back to active language
  118. if ($language->enabled() && $language->getActive() !== $active_lang) {
  119. $language->init();
  120. $language->setActive($active_lang);
  121. $pages->reset();
  122. }
  123. // Build sitemap
  124. foreach ($languages as $lang) {
  125. foreach($this->route_data as $route => $route_data) {
  126. if ($data = $route_data[$lang] ?? null) {
  127. $entry = new SitemapEntry();
  128. $entry->setData($data);
  129. if ($language->enabled()) {
  130. foreach ($route_data as $l => $l_data) {
  131. $entry->addHreflangs(['hreflang' => $l, 'href' => $l_data['location']]);
  132. if ($l === $default_lang) {
  133. $entry->addHreflangs(['hreflang' => 'x-default', 'href' => $l_data['location']]);
  134. }
  135. }
  136. }
  137. $this->sitemap[$data['route']] = $entry;
  138. }
  139. }
  140. }
  141. $additions = (array) $this->config->get('plugins.sitemap.additions');
  142. foreach ($additions as $addition) {
  143. if (isset($addition['location'])) {
  144. $location = Utils::url($addition['location'], true);
  145. $entry = new SitemapEntry($location,$addition['lastmod'] ?? null,$addition['changefreq'] ?? null, $addition['priority'] ?? null);
  146. $this->sitemap[$location] = $entry;
  147. }
  148. }
  149. $cache->save($cache_id, $this->sitemap);
  150. }
  151. $this->grav->fireEvent('onSitemapProcessed', new Event(['sitemap' => &$this->sitemap]));
  152. }
  153. public function onPageInitialized($event)
  154. {
  155. $page = $event['page'] ?? null;
  156. $route = $this->config->get('plugins.sitemap.route');
  157. if (is_null($page) || $page->route() !== $route) {
  158. $html_support = $this->config->get('plugins.sitemap.html_support', false);
  159. $extension = $this->grav['uri']->extension() ?? ($html_support ? 'html': 'xml');
  160. // set a dummy page
  161. $page = new Page;
  162. $page->init(new \SplFileInfo(__DIR__ . '/pages/sitemap.md'));
  163. $page->templateFormat($extension);
  164. unset($this->grav['page']);
  165. $this->grav['page'] = $page;
  166. $twig = $this->grav['twig'];
  167. $twig->template = "sitemap.$extension.twig";
  168. }
  169. }
  170. // Access plugin events in this class
  171. public function onTwigInitialized()
  172. {
  173. $this->grav['twig']->twig()->addFunction(
  174. new TwigFunction('sort_sitemap_entries_by_language', [$this, 'sortSitemapEntriesByLanguage'])
  175. );
  176. }
  177. /**
  178. * Add current directory to twig lookup paths.
  179. */
  180. public function onTwigTemplatePaths()
  181. {
  182. $this->grav['twig']->twig_paths[] = __DIR__ . '/templates';
  183. }
  184. /**
  185. * Set needed variables to display the sitemap.
  186. */
  187. public function onTwigSiteVariables()
  188. {
  189. $twig = $this->grav['twig'];
  190. $twig->twig_vars['sitemap'] = $this->sitemap;
  191. }
  192. /**
  193. * Extend page blueprints with feed configuration options.
  194. *
  195. * @param Event $event
  196. */
  197. public function onBlueprintCreated(Event $event)
  198. {
  199. static $inEvent = false;
  200. /** @var Data\Blueprint $blueprint */
  201. $blueprint = $event['blueprint'];
  202. if (!$inEvent && $blueprint->get('form/fields/tabs', null, '/')) {
  203. if (!in_array($blueprint->getFilename(), array_keys($this->grav['pages']->modularTypes()))) {
  204. $inEvent = true;
  205. $blueprints = new Data\Blueprints(__DIR__ . '/blueprints/');
  206. $extends = $blueprints->get('sitemap');
  207. $blueprint->extend($extends, true);
  208. $inEvent = false;
  209. }
  210. }
  211. }
  212. public function sortSitemapEntriesByLanguage()
  213. {
  214. $entries = [];
  215. foreach ((array) $this->sitemap as $route => $entry) {
  216. $lang = $entry->getLang();
  217. unset($entry->hreflangs);
  218. unset($entry->image);
  219. if ($lang === null) {
  220. $lang = $this->grav['language']->getDefault() ?: 'en';
  221. }
  222. $entries[$lang][$route] = $entry;
  223. }
  224. return $entries;
  225. }
  226. protected function addRouteData($pages, $lang)
  227. {
  228. $routes = array_unique($pages->routes());
  229. ksort($routes);
  230. foreach ($routes as $route => $path) {
  231. /** @var PageInterface $page */
  232. $page = $pages->get($path);
  233. $header = $page->header();
  234. $external_url = $this->ignore_external ? isset($header->external_url) : false;
  235. $protected_page = $this->ignore_protected ? isset($header->access) : false;
  236. $redirect_page = $this->ignore_redirect ? isset($header->redirect) : false;
  237. $config_ignored = preg_match(sprintf("@^(%s)$@i", implode('|', $this->ignores)), $page->route());
  238. $page_ignored = $protected_page || $external_url || $redirect_page || (isset($header->sitemap['ignore']) ? $header->sitemap['ignore'] : false);
  239. if ($page->routable() && $page->published() && !$config_ignored && !$page_ignored) {
  240. $page_languages = array_keys($page->translatedLanguages());
  241. $include_lang = $this->multilang_skiplang_prefix !== $lang;
  242. $location = $page->canonical($include_lang);
  243. $page_route = $page->url(false, $include_lang);
  244. $lang_route = [
  245. 'title' => $page->title(),
  246. 'route' => $page_route,
  247. 'lang' => $lang,
  248. 'translated' => in_array($lang, $page_languages),
  249. 'location' => $location,
  250. 'lastmod' => date($this->datetime_format, $page->modified()),
  251. ];
  252. if ($this->include_change_freq) {
  253. $lang_route['changefreq'] = $header->sitemap['changefreq'] ?? $this->default_change_freq;
  254. }
  255. if ($this->include_priority) {
  256. $lang_route['priority'] = $header->sitemap['priority'] ?? $this->default_priority;
  257. }
  258. // optional add image
  259. $images = $header->sitemap['images'] ?? $this->config->get('plugins.sitemap.images') ?? [];
  260. if (isset($images)) {
  261. foreach ($images as $image => $values) {
  262. if (isset($values['loc'])) {
  263. $images[$image]['loc'] = $page->media()[$values['loc']]->url();
  264. } else {
  265. unset($images[$image]);
  266. }
  267. }
  268. $lang_route['images'] = $images;
  269. }
  270. $this->route_data[$route][$lang] = $lang_route;
  271. }
  272. }
  273. }
  274. }