├── .gitignore ├── resources └── views │ ├── index.blade.php │ └── sitemap.blade.php ├── src ├── Models │ └── SitemapamicUrl.php ├── Listeners │ ├── ClearSitemapamicCache.php │ └── ScheduledCacheInvalidated.php ├── Facades │ └── Sitemapamic.php ├── Commands │ ├── ListSitemapamicCacheKeysCommand.php │ └── ClearSitemapamicCacheCommand.php ├── UpdateScripts │ └── v2_0_1 │ │ └── MoveConfigFile.php ├── ServiceProvider.php ├── Http │ └── Controllers │ │ └── SitemapamicController.php └── Support │ └── Sitemapamic.php ├── routes └── web.php ├── composer.json ├── .github └── ISSUE_TEMPLATE │ └── bug_report.yaml ├── README.md └── config └── sitemapamic.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | /node_modules 4 | vendor 5 | composer.lock -------------------------------------------------------------------------------- /resources/views/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 | @foreach ($submaps as $submap) 3 | 4 | {{ ("{$domain}/sitemap_{$submap}.xml") }} 5 | 6 | @endforeach 7 | -------------------------------------------------------------------------------- /src/Models/SitemapamicUrl.php: -------------------------------------------------------------------------------- 1 | 2 | @foreach ($entries as $entry) 3 | 4 | {{ $entry->loc }} 5 | {{ $entry->lastmod }} 6 | @if ($entry->changefreq){{ $entry->changefreq }}@endif 7 | 8 | @if ($entry->priority){{ number_format($entry->priority, 1) }}@endif 9 | 10 | 11 | @endforeach 12 | -------------------------------------------------------------------------------- /src/Listeners/ClearSitemapamicCache.php: -------------------------------------------------------------------------------- 1 | pluck('url') 7 | ->map(fn($url) => \Statamic\Facades\URL::makeRelative($url)) 8 | ->unique() 9 | ->each(fn($site) => Route::prefix($site)->group(function () { 10 | // add the standard sitemap.xml 11 | Route::get('sitemap.xml', [SitemapamicController::class, 'show']); 12 | 13 | // add the submap xml if multiple mode is enabled 14 | if (config('sitemapamic.mode', 'single') === 'multiple') { 15 | Route::get('sitemap_{submap}.xml', [SitemapamicController::class, 'show']); 16 | } 17 | })); 18 | -------------------------------------------------------------------------------- /src/Facades/Sitemapamic.php: -------------------------------------------------------------------------------- 1 | table(['Key'], collect(Sitemapamic::getCacheKeys())->map(fn($key) => ['key' => $key])); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mitydigital/sitemapamic", 3 | "description": "An XML sitemap generator for Statamic that includes all collections and related taxonomy pages.", 4 | "type": "statamic-addon", 5 | "keywords": [ 6 | "statamic", 7 | "sitemap" 8 | ], 9 | "autoload": { 10 | "psr-4": { 11 | "MityDigital\\Sitemapamic\\": "src" 12 | } 13 | }, 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Marty Friedel" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.2", 22 | "statamic/cms": "^5.0|^6.0" 23 | }, 24 | "extra": { 25 | "statamic": { 26 | "name": "Sitemapamic", 27 | "description": "Adds an XML sitemap to a Statamic site." 28 | }, 29 | "laravel": { 30 | "providers": [ 31 | "MityDigital\\Sitemapamic\\ServiceProvider" 32 | ] 33 | } 34 | }, 35 | "config": { 36 | "allow-plugins": { 37 | "pixelfear/composer-dist-plugin": true 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Fill out a bug report to help us improve. 3 | body: 4 | - type: textarea 5 | attributes: 6 | label: Bug description 7 | description: Please provide a detatiled description of what happened - and if needed, screenshots can help too. 8 | validations: 9 | required: true 10 | - type: textarea 11 | attributes: 12 | label: Steps to reproduce 13 | description: Provide clear, concise steps to reproduce the issue. A step-by-step list is a great start. 14 | validations: 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: Environment and versions 19 | description: | 20 | Details about specific vesions for Statamic, any addons and your PHP version. 21 | (The easiest thing to do is paste the output of the `php please support:details` command.) 22 | render: yaml # the format of the command is close to yaml and gets highlighted nicely 23 | validations: 24 | required: true 25 | - type: textarea 26 | attributes: 27 | label: Additional details 28 | description: Anything else you would like to add? 29 | -------------------------------------------------------------------------------- /src/UpdateScripts/v2_0_1/MoveConfigFile.php: -------------------------------------------------------------------------------- 1 | isUpdatingTo('2.0.1'); 13 | } 14 | 15 | public function update() 16 | { 17 | // check if the config is cached 18 | if ($configurationIsCached = app()->configurationIsCached()) { 19 | Artisan::call('config:clear'); 20 | } 21 | 22 | // clear Sitemapamic cache 23 | Artisan::call('statamic:sitemapamic:clear'); 24 | 25 | // if the config file exists within the 'config/statamic' path, move it just to 'config' 26 | if (file_exists(config_path('statamic/sitemapamic.php'))) { 27 | if (file_exists(config_path('sitemapamic.php'))) { 28 | // cannot copy 29 | $this->console()->alert('The Sitemapamic config file could not be moved to `config/sitemapamic.php` - it already exists!'); 30 | $this->console()->alert('You will need to manually make sure your `config/sitemapamic.php` file is correctly configured.'); 31 | } else { 32 | // move the config file 33 | rename(config_path('statamic/sitemapamic.php'), config_path('sitemapamic.php')); 34 | 35 | // output 36 | $this->console()->info('Sitemapamic config file has been moved to `config/sitemapamic.php`!'); 37 | } 38 | } 39 | 40 | // re-cache config if it was cached 41 | if ($configurationIsCached) { 42 | Artisan::call('config:cache'); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sitemapamic 2 | 3 | 4 | 5 | ![Statamic 5.0](https://img.shields.io/badge/Statamic-5.0-FF269E?style=for-the-badge&link=https://statamic.com) 6 | ![Statamic 6.0](https://img.shields.io/badge/Statamic-6.0-FF269E?style=for-the-badge&link=https://statamic.com) 7 | [![Sitemapamic on Packagist](https://img.shields.io/packagist/v/mitydigital/sitemapamic?style=for-the-badge)](https://packagist.org/packages/mitydigital/sitemapamic/stats) 8 | 9 | --- 10 | 11 | 12 | 13 | > Sitemapamic is a XML sitemap generator for Statamic 14 | 15 | Sitemapamic creates a sitemap.xml file for your Statamic site, and includes: 16 | 17 | - automatic route registration for `sitemap.xml` 18 | - automatic updates as your content changes 19 | - support for entries and taxonomies 20 | - caching for performance (with a set time, or until content changes) 21 | - support for multi-site on the same or different domains 22 | - support for splitting large sites in to multiple smaller sitemaps 23 | - console commands for manual cache clearing 24 | 25 | --- 26 | 27 | Sitemapamic requires: 28 | 29 | - Statamic 5 or 6 30 | - PHP 8.2+ 31 | 32 | ## Documentation 33 | 34 | See the [documentation](https://docs.mity.com.au/sitemapamic) for detailed installation, configuration and usage 35 | instructions. 36 | 37 | --- 38 | 39 | ## Support 40 | 41 | We love to share work like this, and help the community. However it does take time, effort and work. 42 | 43 | The best thing you can do is [log an issue](../../issues). 44 | 45 | Please try to be detailed when logging an issue, including a clear description of the problem, steps to reproduce the 46 | issue, and any steps you may have tried or taken to overcome the issue too. This is an awesome first step to helping us 47 | help you. So be awesome - it'll feel fantastic. 48 | 49 | --- 50 | 51 | ## Credits 52 | 53 | - [Marty Friedel](https://github.com/martyf) 54 | - [Jack Sleight](https://github.com/jacksleight) for adding multiple sitemap support 55 | - [Wuif](https://github.com/wuifdesign) for computed configuration support 56 | - [Philipp Daun](https://github.com/daun) for performance and bug fixes 57 | 58 | ## License 59 | 60 | This addon is licensed under the MIT license. 61 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | __DIR__.'/../routes/web.php', 30 | ]; 31 | 32 | protected $listen = [ 33 | CollectionDeleted::class => [ 34 | ClearSitemapamicCache::class, 35 | ], 36 | CollectionSaved::class => [ 37 | ClearSitemapamicCache::class, 38 | ], 39 | EntryDeleted::class => [ 40 | ClearSitemapamicCache::class, 41 | ], 42 | EntrySaved::class => [ 43 | ClearSitemapamicCache::class, 44 | ], 45 | TaxonomyDeleted::class => [ 46 | ClearSitemapamicCache::class, 47 | ], 48 | TaxonomySaved::class => [ 49 | ClearSitemapamicCache::class, 50 | ], 51 | TermDeleted::class => [ 52 | ClearSitemapamicCache::class, 53 | ], 54 | TermSaved::class => [ 55 | ClearSitemapamicCache::class, 56 | ], 57 | \MityDigital\StatamicScheduledCacheInvalidator\Events\ScheduledCacheInvalidated::class => [ 58 | ScheduledCacheInvalidated::class 59 | ] 60 | ]; 61 | 62 | protected $updateScripts = [ 63 | // v2.0.1 64 | \MityDigital\Sitemapamic\UpdateScripts\v2_0_1\MoveConfigFile::class 65 | ]; 66 | 67 | public function bootAddon() 68 | { 69 | $this->publishes([ 70 | __DIR__.'/../resources/views' => resource_path('views/vendor/mitydigital/sitemapamic'), 71 | ], 'sitemapamic-views'); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Commands/ClearSitemapamicCacheCommand.php: -------------------------------------------------------------------------------- 1 | argument('sitemaps'); 37 | 38 | if (count($sitemaps) == 0) 39 | { 40 | // default cache clearing behaviour 41 | // clear it all 42 | if (Sitemapamic::clearCache()) { 43 | $this->info('Snip snip and whoosh, it\'s all gone.'); 44 | } 45 | else { 46 | $this->error('Uh oh... Sitemapamic could not clear the entire cache.'); 47 | } 48 | } 49 | else { 50 | // make a neat array 51 | $keys = []; 52 | foreach ($sitemaps as $key) { 53 | if (!in_array('"'.$key.'"', $keys)) { 54 | $keys[] = '"'.$key.'"'; 55 | } 56 | } 57 | 58 | // make it prettier 59 | // Arr::join came in Laravel 9 - so do it manually for L8 support 60 | if (count($keys) > 1) { 61 | $lastKey = array_pop($keys); 62 | $keys = implode(', ', $keys).' and '.$lastKey; 63 | } else { 64 | $keys = end($keys); 65 | } 66 | 67 | // clear specific sitemaps only 68 | if (Sitemapamic::clearCache($sitemaps)) { 69 | $this->info('Snip snip and whoosh, sitemaps for '.$keys.' are gone.'); 70 | } else { 71 | $this->error('Sitemaps for '.$keys.' could not be cleared.'); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Http/Controllers/SitemapamicController.php: -------------------------------------------------------------------------------- 1 | map(fn($loader) => $loader()) 39 | ->flatten(1); 40 | return view('mitydigital/sitemapamic::sitemap', [ 41 | 'entries' => $entries 42 | ])->render(); 43 | }; 44 | } elseif ($mode === 'multiple') { 45 | if ($request->submap) { 46 | if (!$loaders->has($request->submap)) { 47 | abort(404); 48 | } 49 | $key .= '.'.$request->submap; 50 | 51 | $generator = function () use ($loaders, $request) { 52 | $entries = $loaders->get($request->submap)(); 53 | return view('mitydigital/sitemapamic::sitemap', [ 54 | 'entries' => $entries 55 | ])->render(); 56 | }; 57 | } else { 58 | $generator = function () use ($loaders) { 59 | // return the view with submaps defined 60 | return view('mitydigital/sitemapamic::index', [ 61 | 'domain' => rtrim(URL::makeAbsolute(Site::current()->url()), '/\\'), 62 | 'submaps' => $loaders->keys() 63 | ])->render(); 64 | }; 65 | } 66 | } 67 | 68 | // add site to key 69 | $key .= '.'.Site::current(); 70 | 71 | // if the ttl is strictly 'forever', do just that 72 | if ($ttl == 'forever') { 73 | $xml = Cache::rememberForever($key, $generator); 74 | } else { 75 | $xml = Cache::remember($key, $ttl, $generator); 76 | } 77 | 78 | // add the XML header 79 | $xml = ''.$xml; 80 | 81 | return response($xml, 200, ['Content-Type' => 'application/xml']); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /config/sitemapamic.php: -------------------------------------------------------------------------------- 1 | 'single', 17 | 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Cache Key 22 | |-------------------------------------------------------------------------- 23 | | 24 | | The key used to store the output. Will be cached forever until EventSaved or TermSaved is fired. 25 | | 26 | */ 27 | 28 | 'cache' => 'sitemapamic', 29 | 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Cache Duration 34 | |-------------------------------------------------------------------------- 35 | | 36 | | The number of seconds for how long the Sitemapamic Cache be held for. 37 | | 38 | | Can be an integer or DateInterval - the same options that Laravel's Cache accepts. 39 | | 40 | | Or set to 'forever' to remember forever (don't worry, it will get cleared when an Entry, 41 | | Term, Taxonomy or Collection is saved or deleted. 42 | | 43 | */ 44 | 45 | 'ttl' => 'forever', 46 | 47 | 48 | /* 49 | |-------------------------------------------------------------------------- 50 | | Defaults 51 | |-------------------------------------------------------------------------- 52 | | 53 | | Sets defaults for different collections. 54 | | 55 | | The key is the collection handle, and the array includes default configurations. 56 | | Set "include" to true to either include or exclude without explicitly setting per article 57 | | Frequency and Priority are standard for an XML sitemap 58 | | 59 | | 'includeTaxonomies' enables (or disables) whether taxonomy URLs will be generated, if used, 60 | | for the collection. Only applies to Collections that actually use Taxonomies. 61 | | 62 | */ 63 | 64 | 'defaults' => [ 65 | /*'blog' => [ 66 | 'include' => true, 67 | 'frequency' => 'weekly', 68 | 'priority' => '0.7' 69 | ],*/ 70 | 71 | 'pages' => [ 72 | 'include' => true, 73 | 'frequency' => 'yearly', 74 | 'priority' => '0.5', 75 | 'includeTaxonomies' => true, 76 | ] 77 | ], 78 | 79 | 80 | /* 81 | |-------------------------------------------------------------------------- 82 | | Globals 83 | |-------------------------------------------------------------------------- 84 | | 85 | | Sets global behaviour for items like taxonomies. Currently that's all that is supported. 86 | | 87 | | The 'globals.taxonomies' key expects an array of Taxonomy handles, each with an optional 88 | | priority and frequency, just like the Defaults section. This means your Taxonomy blueprint 89 | | can also take advantage of Term-specific 'meta_change_frequency' and 'meta_priority' fields, 90 | | or fall back to these defaults when not set (or present). 91 | | 92 | | If you don't want the Taxonomy included in the sitemap, simply exclude it from the array. 93 | | 94 | */ 95 | 'globals' => [ 96 | 'taxonomies' => [ 97 | /*'tags' => [ 98 | 'frequency' => 'yearly', 99 | 'priority' => '0.5', 100 | ], 101 | 102 | 'categories' => []*/ 103 | ] 104 | ], 105 | 106 | 107 | /* 108 | |-------------------------------------------------------------------------- 109 | | Field mappings 110 | |-------------------------------------------------------------------------- 111 | | 112 | | Allows you to map your blueprint fields with what Sitemapamic is expecting for controlling 113 | | the change frequency, inclusion and priority. 114 | | 115 | | The key is the purpose (i.e. don't change this) and the value is the field handle in your blueprints. 116 | | 117 | */ 118 | 'mappings' => [ 119 | 'include' => 'meta_include_in_xml_sitemap', 120 | 'change_frequency' => 'meta_change_frequency', 121 | 'priority' => 'meta_priority', 122 | ] 123 | ]; 124 | -------------------------------------------------------------------------------- /src/Support/Sitemapamic.php: -------------------------------------------------------------------------------- 1 | dynamicRoutes[] = $routesClosure; 30 | } 31 | 32 | /** 33 | * Clears the Sitemapamic cache. 34 | * 35 | * Accepts an array of keys when 'mode' is set to 'multiple'. This will allow individual sitemaps to be cleared. 36 | * 37 | * Passing nothing, or an empty array, will clear the entire Sitemapamic cache. 38 | * 39 | * @param array $keys An array of keys, only for "multiple" configuration. 40 | * @return bool 41 | */ 42 | public function clearCache(array $keys = []): bool 43 | { 44 | if (count($keys) === 0) { 45 | // clear everything 46 | foreach ($this->getCacheKeys() as $key) { 47 | Cache::forget($key); 48 | } 49 | } else { 50 | // only clear what was requested 51 | $siteKey = $this->getCacheKey(); 52 | foreach ($keys as $key) { 53 | if (starts_with($key, $siteKey.'.')) { 54 | // we already have the cache key, so exclude it 55 | Cache::forget($key); 56 | } else { 57 | // add the cache key 58 | Cache::forget($this->getCacheKey().'.'.$key); 59 | } 60 | } 61 | } 62 | 63 | return true; 64 | } 65 | 66 | /** 67 | * Get the cache keys used by the current Sitemapamic configuration. 68 | * 69 | * @return array 70 | */ 71 | public function getCacheKeys() 72 | { 73 | $key = $this->getCacheKey(); 74 | $mode = config('sitemapamic.mode', 'single'); 75 | 76 | $keys = []; 77 | $sites = collect(Site::all()->keys()); 78 | 79 | if ($mode === 'single') { 80 | if ($sites->count() > 1) { 81 | // return the single statamic key for each site 82 | return $sites 83 | ->map(fn($site) => $key.'.'.$site) 84 | ->toArray(); 85 | } else { 86 | // just return the key 87 | return [$key.'.'.Site::default()]; 88 | } 89 | } elseif ($mode === 'multiple') { 90 | // get the loaders 91 | $loaders = $this->getLoaders(); 92 | 93 | // get the keys relevant to the setup 94 | if ($sites->count() > 1) { 95 | return $this->getLoaders() 96 | ->keys() 97 | ->map(fn($loader) => $key.'.'.$loader) 98 | ->prepend($key) 99 | ->map(fn($key) => $sites 100 | ->map(fn($site) => $key.'.'.$site)) 101 | ->flatten() 102 | ->toArray(); 103 | } else { 104 | return $this->getLoaders() 105 | ->keys() 106 | ->map(fn($loader) => $key.'.'.$loader) 107 | ->prepend($key) 108 | ->toArray(); 109 | } 110 | } 111 | 112 | return []; 113 | } 114 | 115 | /** 116 | * Returns the root Sitemapamic cache key. 117 | * 118 | * @return string|mixed 119 | */ 120 | public function getCacheKey() 121 | { 122 | return config('sitemapamic.cache', 'sitemapamic'); 123 | } 124 | 125 | /** 126 | * Get all of the content loaders used by the current configuration. 127 | * 128 | * Includes entries, collection terms, global taxonomies and dynamic routes. 129 | * 130 | * @return \Illuminate\Support\Collection 131 | */ 132 | public function getLoaders() 133 | { 134 | $loaders = collect() 135 | ->merge($this->loadEntries()) 136 | ->merge($this->loadCollectionTerms()) 137 | ->merge($this->loadGlobalTaxonomies()); 138 | 139 | // if we have any dynamic routes, let's load those too 140 | if ($this->hasDynamicRoutes()) { 141 | $loaders = $loaders->merge($this->loadDynamicRoutes()); 142 | } 143 | 144 | return $loaders; 145 | } 146 | 147 | /** 148 | * Gets all published entries for all configured collections. 149 | * 150 | * Returns a collection of \MityDigital\Sitemapamic\Models\SitemapamicUrl 151 | * 152 | * @return \Illuminate\Support\Collection 153 | */ 154 | protected function loadEntries(): \Illuminate\Support\Collection 155 | { 156 | return collect(config('sitemapamic.defaults'))->mapWithKeys(function ($properties, $handle) { 157 | return [ 158 | $handle => function () use ($properties, $handle) { 159 | return Collection::findByHandle($handle)->queryEntries()->lazy(100)->filter(function ( 160 | \Statamic\Entries\Entry $entry 161 | ) { 162 | // same site? if site is different, remove 163 | if ($entry->site() != Site::current()) { 164 | return false; 165 | } 166 | 167 | // is the entry published? 168 | if (!$entry->published()) { 169 | return false; 170 | } 171 | 172 | // are we an external redirect? 173 | // note the "dirty" trick to make the redirect a string (v4/5 has the ArrayableLink) 174 | if ($entry->blueprint()->handle() === 'link' && isset($entry->redirect) && ( 175 | (get_class($entry->redirect) === ArrayableLink::class && URL::isExternal($entry->redirect->url())) 176 | || 177 | (property_exists($entry->redirect, 'url') && URL::isExternal($entry->redirect->url)) 178 | )) { 179 | return false; 180 | } 181 | 182 | // if future listings are private or unlisted, do not include 183 | if ($entry->collection()->futureDateBehavior() == 'private' || $entry->collection()->futureDateBehavior() == 'unlisted') { 184 | if ($entry->date() > now()) { 185 | return false; 186 | } 187 | } 188 | 189 | // if past listings are private or unlisted, do not include 190 | if ($entry->collection()->pastDateBehavior() == 'private' || $entry->collection()->pastDateBehavior() == 'unlisted') { 191 | if ($entry->date() < now()) { 192 | return false; 193 | } 194 | } 195 | 196 | // include_xml_sitemap is one of null (when not set, so default to true), then either false or true 197 | $includeKey = config('sitemapamic.mappings.include', 'meta_include_in_xml_sitemap'); 198 | $includeInSitemap = $entry->get($includeKey) ?? $entry->getComputed($includeKey); 199 | if ($includeInSitemap === null || $includeInSitemap == 'default') { 200 | // get the default config, or return true by default 201 | return config('sitemapamic.defaults.'.$entry->collection()->handle().'.include', true); 202 | } elseif ($includeInSitemap == "false" || $includeInSitemap === false) { 203 | // explicitly set to "false" or boolean false, so exclude 204 | return false; 205 | } 206 | 207 | // yep, keep it 208 | return true; 209 | })->map(function ($entry) { 210 | 211 | $changeFreqKey = config('sitemapamic.mappings.change_frequency', 'meta_change_frequency'); 212 | $changeFreq = $entry->get($changeFreqKey) ?? $entry->getComputed($changeFreqKey); 213 | if ($changeFreq == 'default') { 214 | // clear back to use default 215 | $changeFreq = null; 216 | } 217 | 218 | $priorityKey = config('sitemapamic.mappings.priority', 'meta_priority'); 219 | 220 | // return the entry as a Sitemapamic URL 221 | return new SitemapamicUrl( 222 | URL::makeAbsolute($entry->url()), 223 | Carbon::parse($entry->get('updated_at'))->toW3cString(), 224 | $changeFreq ?? config('sitemapamic.defaults.'.$entry->collection()->handle().'.frequency', 225 | false), 226 | $entry->get($priorityKey) ?? $entry->getComputed($priorityKey) ?? config('sitemapamic.defaults.'.$entry->collection()->handle().'.priority', 227 | false) 228 | ); 229 | })->toArray(); 230 | } 231 | ]; 232 | }); 233 | } 234 | 235 | /** 236 | * Gets the Taxonomy pages for the collections where they are used. 237 | * 238 | * lastmod will be set to the Term's updated_at time, or the latest entry's 239 | * updated_at time, whichever is more recent. 240 | * 241 | * Returns a collection of \MityDigital\Sitemapamic\Models\SitemapamicUrl 242 | * 243 | * @return \Illuminate\Support\Collection 244 | */ 245 | protected function loadCollectionTerms(): \Illuminate\Support\Collection 246 | { 247 | // get the current site key based on the url 248 | $site = Site::default(); 249 | foreach (Site::all() as $key => $props) { 250 | if ($props->url() == url('/')) { 251 | $site = $props; 252 | break; 253 | } 254 | } 255 | 256 | return collect(config('sitemapamic.defaults'))->map(function ($properties, $handle) use ($site) { 257 | 258 | // if there is a property called includeTaxonomies, and its false (or the collection is disabled) then exclude it 259 | // this has been added for backwards compatibility 260 | if (isset($properties['includeTaxonomies']) && (!$properties['includeTaxonomies'] || !$properties['include'])) { 261 | return false; 262 | } 263 | 264 | $collection = Collection::findByHandle($handle); 265 | 266 | return $collection->taxonomies()->map->collection($collection)->mapWithKeys(function ($taxonomy) use ( 267 | $properties, 268 | $handle, 269 | $site 270 | ) { 271 | return [ 272 | $handle.'_'.$taxonomy->handle => function () use ($taxonomy, $site) { 273 | 274 | return $taxonomy->queryTerms()->get()->filter(function ($term) use ($site) { 275 | if (!$term->published()) { 276 | return false; 277 | } 278 | 279 | // site is not configured, so exclude 280 | if (!$term->collection()->sites()->contains($site->handle())) { 281 | return false; 282 | } 283 | 284 | // include_xml_sitemap is one of null (when not set, so default to true), then either false or true 285 | $includeInSitemap = $term->get(config('sitemapamic.mappings.include', 'meta_include_in_xml_sitemap')); 286 | if ($includeInSitemap === null) { 287 | // get the default config, or return true by default 288 | return config('sitemapamic.defaults.'.$term->collection()->handle().'.include', true); 289 | } elseif ($includeInSitemap === false) { 290 | // explicitly set to false, so exclude 291 | return false; 292 | } 293 | 294 | return true; // this far, accept it 295 | })->map(function ($term) use ($site) { 296 | // get the term mod date 297 | $lastMod = $term->get('updated_at'); 298 | 299 | // get entries 300 | $termEntries = $term->queryEntries()->orderBy('updated_at', 'desc'); 301 | 302 | // if this term has entries, get the greater of the two updated_at timestamps 303 | if ($termEntries->count() > 0) { 304 | // get the last modified entry 305 | $entryLastMod = $termEntries->first()->get('updated_at'); 306 | 307 | // Check if the $lastMod is being returned as a Carbon instance instead of an int 308 | if ($lastMod && !is_int($lastMod)) { 309 | // Convert to a timestamp if it's a Carbon instance 310 | $lastMod = $lastMod->timestamp; 311 | } 312 | 313 | // entry date is after the term's mod date 314 | if ($entryLastMod > $lastMod) { 315 | $lastMod = $entryLastMod; 316 | } 317 | } 318 | 319 | $changeFreq = $term->get(config('sitemapamic.mappings.change_frequency', 'meta_change_frequency')); 320 | if ($changeFreq == 'default') { 321 | // clear back to use default 322 | $changeFreq = null; 323 | } 324 | 325 | 326 | // get the site URL, or the app URL if its "/" 327 | //$siteUrl = config('statamic.sites.sites.'.$term->locale().'.url'); 328 | $siteUrl = $site->url(); 329 | if ($siteUrl == '/') { 330 | $siteUrl = config('app.url'); 331 | } 332 | 333 | return new SitemapamicUrl( 334 | $siteUrl.$term->url(), 335 | Carbon::parse($lastMod)->toW3cString(), 336 | $changeFreq ?? config('sitemapamic.defaults.'.$term->collection()->handle().'.frequency', 337 | false), 338 | $term->get(config('sitemapamic.mappings.priority', 'meta_priority')) ?? config('sitemapamic.defaults.'.$term->collection()->handle().'.priority', 339 | false) 340 | ); 341 | }); 342 | } 343 | ]; 344 | }); 345 | })->flatMap(fn($items) => $items); 346 | } 347 | 348 | protected function loadGlobalTaxonomies(): \Illuminate\Support\Collection 349 | { 350 | // are we configured to load the global taxonomies? 351 | // if so, what? 352 | $taxonomies = config('sitemapamic.globals.taxonomies', []); 353 | 354 | if (empty($taxonomies)) { 355 | // return an empty collection - either set to false, or not set yet 356 | return collect(); 357 | } 358 | 359 | // get the current site key based on the url 360 | $site = Site::default(); 361 | foreach (Site::all() as $key => $props) { 362 | if ($props->url() == url('/')) { 363 | $site = $props; 364 | break; 365 | } 366 | } 367 | 368 | return collect($taxonomies)->mapWithKeys(function ($properties, $handle) use ($site) { 369 | return [ 370 | $handle => function () use ($handle, $site) { 371 | 372 | // get the taxonomy repository 373 | $taxonomy = Taxonomy::find($handle); 374 | 375 | // if the taxonomy isn't configured for the site, get out 376 | if (!$taxonomy->sites()->contains($site)) { 377 | return null; 378 | } 379 | 380 | // does a view exist for this taxonomy? 381 | // if not, it will 404, so let's not do any more 382 | if (!view()->exists($handle.'/show')) { 383 | return null; 384 | } 385 | 386 | // get the terms 387 | return Term::whereTaxonomy($handle) 388 | ->filter(function ($term) { 389 | // should we include this term? 390 | // include_xml_sitemap is one of null (when not set, so default to true), then either false or true 391 | $includeInSitemap = $term->get(config('sitemapamic.mappings.include', 'meta_include_in_xml_sitemap')); 392 | if ($includeInSitemap === "false" || $includeInSitemap === false) { 393 | // explicitly set to "false" or boolean false, so exclude 394 | return false; 395 | } 396 | 397 | // there is no meta field for the term, so include it 398 | // Why? Because if we made it this far, the Taxonomy is part of the global config, so 399 | // we want to include it. So just include it. 400 | return true; 401 | }) 402 | ->map(function (\Statamic\Taxonomies\LocalizedTerm $term) use ($site) { 403 | // get the term mod date 404 | $lastMod = $term->get('updated_at'); 405 | 406 | // get entries 407 | $termEntries = $term->queryEntries()->orderBy('updated_at', 'desc'); 408 | 409 | // if this term has entries, get the greater of the two updated_at timestamps 410 | if ($termEntries->count() > 0) { 411 | // get the last modified entry 412 | $entryLastMod = $termEntries->first()->get('updated_at'); 413 | 414 | // entry date is after the term's mod date 415 | if ($entryLastMod > $lastMod) { 416 | $lastMod = $entryLastMod; 417 | } 418 | } 419 | 420 | $changeFreq = $term->get(config('sitemapamic.mappings.change_frequency', 'meta_change_frequency')); 421 | if ($changeFreq == 'default') { 422 | // clear back to use default 423 | $changeFreq = null; 424 | } 425 | 426 | // get the site URL, or the app URL if its "/" 427 | //$siteUrl = config('statamic.sites.sites.'.$term->locale().'.url'); 428 | $siteUrl = $site->url(); 429 | if ($siteUrl == '/') { 430 | $siteUrl = config('app.url'); 431 | } 432 | 433 | return new SitemapamicUrl( 434 | $siteUrl.$term->url(), 435 | Carbon::parse($lastMod)->toW3cString(), 436 | $changeFreq ?? 437 | config('sitemapamic.globals.taxonomies.'.$term->taxonomy()->handle().'.frequency', 438 | false), 439 | $term->get(config('sitemapamic.mappings.priority', 'meta_priority')) ?? config('sitemapamic.globals.taxonomies.'.$term->taxonomy()->handle().'.priority', 440 | false) 441 | ); 442 | }); 443 | } 444 | ]; 445 | }); 446 | } 447 | 448 | public function hasDynamicRoutes(): bool 449 | { 450 | return (bool) count($this->dynamicRoutes); 451 | } 452 | 453 | protected function loadDynamicRoutes(): \Illuminate\Support\Collection 454 | { 455 | // get the dynamic routes, if any are set, and only return them if they are a SitemapamicUrl 456 | return collect([ 457 | 'dynamic' => function () { 458 | return collect($this->getDynamicRoutes()) 459 | ->flatMap(function ($closure) { 460 | return $closure(); 461 | }) 462 | ->filter(fn($route) => get_class($route) == SitemapamicUrl::class); 463 | } 464 | ]); 465 | } 466 | 467 | /** 468 | * Returns the dynamic routes 469 | * 470 | * @return array|mixed 471 | */ 472 | public function getDynamicRoutes() 473 | { 474 | return $this->dynamicRoutes; 475 | } 476 | } 477 | --------------------------------------------------------------------------------