├── .github └── ISSUE_TEMPLATE │ └── bug_report.yaml ├── .gitignore ├── README.md ├── composer.json ├── config └── sitemapamic.php ├── resources └── views │ ├── index.blade.php │ └── sitemap.blade.php ├── routes └── web.php └── src ├── Commands ├── ClearSitemapamicCacheCommand.php └── ListSitemapamicCacheKeysCommand.php ├── Facades └── Sitemapamic.php ├── Http └── Controllers │ └── SitemapamicController.php ├── Listeners ├── ClearSitemapamicCache.php └── ScheduledCacheInvalidated.php ├── Models └── SitemapamicUrl.php ├── ServiceProvider.php ├── Support └── Sitemapamic.php └── UpdateScripts └── v2_0_1 └── MoveConfigFile.php /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | /node_modules 4 | vendor 5 | composer.lock -------------------------------------------------------------------------------- /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 | [![Sitemapamic on Packagist](https://img.shields.io/packagist/v/mitydigital/sitemapamic?style=for-the-badge)](https://packagist.org/packages/mitydigital/sitemapamic/stats) 7 | 8 | --- 9 | 10 | 11 | 12 | > Sitemapamic is a XML sitemap generator for Statamic 13 | 14 | Sitemapamic creates a sitemap.xml file for your Statamic site, and includes: 15 | 16 | - automatic route registration for `sitemap.xml` 17 | - automatic updates as your content changes 18 | - support for entries and taxonomies 19 | - caching for performance (with a set time, or until content changes) 20 | - support for multi-site on the same or different domains 21 | - support for splitting large sites in to multiple smaller sitemaps 22 | - console commands for manual cache clearing 23 | 24 | --- 25 | 26 | Sitemapamic requires: 27 | 28 | - Statamic 5 29 | - PHP 8.2+ 30 | 31 | ## Documentation 32 | 33 | See the [documentation](https://docs.mity.com.au/sitemapamic) for detailed installation, configuration and usage 34 | instructions. 35 | 36 | --- 37 | 38 | ## Support 39 | 40 | We love to share work like this, and help the community. However it does take time, effort and work. 41 | 42 | The best thing you can do is [log an issue](../../issues). 43 | 44 | Please try to be detailed when logging an issue, including a clear description of the problem, steps to reproduce the 45 | issue, and any steps you may have tried or taken to overcome the issue too. This is an awesome first step to helping us 46 | help you. So be awesome - it'll feel fantastic. 47 | 48 | --- 49 | 50 | ## Credits 51 | 52 | - [Marty Friedel](https://github.com/martyf) 53 | - [Jack Sleight](https://github.com/jacksleight) for adding multiple sitemap support 54 | - [Wuif](https://github.com/wuifdesign) for computed configuration support 55 | 56 | ## License 57 | 58 | This addon is licensed under the MIT license. 59 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mitydigital/sitemapamic", 3 | "description": "An XML sitemap generator for Statamic 3, 4 and 5 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" 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resources/views/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 | @foreach ($submaps as $submap) 3 | 4 | {{ ("{$domain}/sitemap_{$submap}.xml") }} 5 | 6 | @endforeach 7 | -------------------------------------------------------------------------------- /resources/views/sitemap.blade.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 | -------------------------------------------------------------------------------- /routes/web.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/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/Commands/ListSitemapamicCacheKeysCommand.php: -------------------------------------------------------------------------------- 1 | table(['Key'], collect(Sitemapamic::getCacheKeys())->map(fn($key) => ['key' => $key])); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Facades/Sitemapamic.php: -------------------------------------------------------------------------------- 1 | map(fn($loader) => $loader()) 42 | ->flatten(1); 43 | return view('mitydigital/sitemapamic::sitemap', [ 44 | 'entries' => $entries 45 | ])->render(); 46 | }; 47 | } elseif ($mode === 'multiple') { 48 | if ($request->submap) { 49 | if (!$loaders->has($request->submap)) { 50 | abort(404); 51 | } 52 | $key .= '.'.$request->submap; 53 | 54 | $generator = function () use ($loaders, $request) { 55 | $entries = $loaders->get($request->submap)(); 56 | return view('mitydigital/sitemapamic::sitemap', [ 57 | 'entries' => $entries 58 | ])->render(); 59 | }; 60 | } else { 61 | $generator = function () use ($loaders) { 62 | // return the view with submaps defined 63 | return view('mitydigital/sitemapamic::index', [ 64 | 'domain' => rtrim(URL::makeAbsolute(Site::current()->url()), '/\\'), 65 | 'submaps' => $loaders->keys() 66 | ])->render(); 67 | }; 68 | } 69 | } 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 | -------------------------------------------------------------------------------- /src/Listeners/ClearSitemapamicCache.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/Support/Sitemapamic.php: -------------------------------------------------------------------------------- 1 | dynamicRoutes[] = $routesClosure; 29 | } 30 | 31 | /** 32 | * Clears the Sitemapamic cache. 33 | * 34 | * Accepts an array of keys when 'mode' is set to 'multiple'. This will allow individual sitemaps to be cleared. 35 | * 36 | * Passing nothing, or an empty array, will clear the entire Sitemapamic cache. 37 | * 38 | * @param array $keys An array of keys, only for "multiple" configuration. 39 | * @return bool 40 | */ 41 | public function clearCache(array $keys = []): bool 42 | { 43 | if (count($keys) === 0) { 44 | // clear everything 45 | foreach ($this->getCacheKeys() as $key) { 46 | Cache::forget($key); 47 | } 48 | } else { 49 | // only clear what was requested 50 | $siteKey = $this->getCacheKey(); 51 | foreach ($keys as $key) { 52 | if (starts_with($key, $siteKey.'.')) { 53 | // we already have the cache key, so exclude it 54 | Cache::forget($key); 55 | } else { 56 | // add the cache key 57 | Cache::forget($this->getCacheKey().'.'.$key); 58 | } 59 | } 60 | } 61 | 62 | return true; 63 | } 64 | 65 | /** 66 | * Get the cache keys used by the current Sitemapamic configuration. 67 | * 68 | * @return array 69 | */ 70 | public function getCacheKeys() 71 | { 72 | $key = $this->getCacheKey(); 73 | $mode = config('sitemapamic.mode', 'single'); 74 | 75 | $keys = []; 76 | $sites = collect(Site::all()->keys()); 77 | 78 | if ($mode === 'single') { 79 | if ($sites->count() > 1) { 80 | // return the single statamic key for each site 81 | return $sites 82 | ->map(fn($site) => $key.'.'.$site) 83 | ->toArray(); 84 | } else { 85 | // just return the key 86 | return [$key.'.'.Site::default()]; 87 | } 88 | } elseif ($mode === 'multiple') { 89 | // get the loaders 90 | $loaders = $this->getLoaders(); 91 | 92 | // get the keys relevant to the setup 93 | if ($sites->count() > 1) { 94 | return $this->getLoaders() 95 | ->keys() 96 | ->map(fn($loader) => $key.'.'.$loader) 97 | ->prepend($key) 98 | ->map(fn($key) => $sites 99 | ->map(fn($site) => $key.'.'.$site)) 100 | ->flatten() 101 | ->toArray(); 102 | } else { 103 | return $this->getLoaders() 104 | ->keys() 105 | ->map(fn($loader) => $key.'.'.$loader) 106 | ->prepend($key) 107 | ->toArray(); 108 | } 109 | } 110 | 111 | return []; 112 | } 113 | 114 | /** 115 | * Returns the root Sitemapamic cache key. 116 | * 117 | * @return string|mixed 118 | */ 119 | public function getCacheKey() 120 | { 121 | return config('sitemapamic.cache', 'sitemapamic'); 122 | } 123 | 124 | /** 125 | * Get all of the content loaders used by the current configuration. 126 | * 127 | * Includes entries, collection terms, global taxonomies and dynamic routes. 128 | * 129 | * @return \Illuminate\Support\Collection 130 | */ 131 | public function getLoaders() 132 | { 133 | $loaders = collect() 134 | ->merge($this->loadEntries()) 135 | ->merge($this->loadCollectionTerms()) 136 | ->merge($this->loadGlobalTaxonomies()); 137 | 138 | // if we have any dynamic routes, let's load those too 139 | if ($this->hasDynamicRoutes()) { 140 | $loaders = $loaders->merge($this->loadDynamicRoutes()); 141 | } 142 | 143 | return $loaders; 144 | } 145 | 146 | /** 147 | * Gets all published entries for all configured collections. 148 | * 149 | * Returns a collection of \MityDigital\Sitemapamic\Models\SitemapamicUrl 150 | * 151 | * @return \Illuminate\Support\Collection 152 | */ 153 | protected function loadEntries(): \Illuminate\Support\Collection 154 | { 155 | return collect(config('sitemapamic.defaults'))->mapWithKeys(function ($properties, $handle) { 156 | return [ 157 | $handle => function () use ($properties, $handle) { 158 | return Collection::findByHandle($handle)->queryEntries()->get()->filter(function ( 159 | \Statamic\Entries\Entry $entry 160 | ) { 161 | // same site? if site is different, remove 162 | if ($entry->site() != Site::current()) { 163 | return false; 164 | } 165 | 166 | // is the entry published? 167 | if (!$entry->published()) { 168 | return false; 169 | } 170 | 171 | // are we an external redirect? 172 | // note the "dirty" trick to make the redirect a string (v4/5 has the ArrayableLink) 173 | if($entry->blueprint()->handle() === 'link' && isset($entry->redirect) && URL::isExternal('' . $entry->redirect)) { 174 | return false; 175 | } 176 | 177 | // if future listings are private or unlisted, do not include 178 | if ($entry->collection()->futureDateBehavior() == 'private' || $entry->collection()->futureDateBehavior() == 'unlisted') { 179 | if ($entry->date() > now()) { 180 | return false; 181 | } 182 | } 183 | 184 | // if past listings are private or unlisted, do not include 185 | if ($entry->collection()->pastDateBehavior() == 'private' || $entry->collection()->pastDateBehavior() == 'unlisted') { 186 | if ($entry->date() < now()) { 187 | return false; 188 | } 189 | } 190 | 191 | // include_xml_sitemap is one of null (when not set, so default to true), then either false or true 192 | $includeKey = config('sitemapamic.mappings.include', 'meta_include_in_xml_sitemap'); 193 | $includeInSitemap = $entry->get($includeKey) ?? $entry->getComputed($includeKey); 194 | if ($includeInSitemap === null || $includeInSitemap == 'default') { 195 | // get the default config, or return true by default 196 | return config('sitemapamic.defaults.'.$entry->collection()->handle().'.include', true); 197 | } elseif ($includeInSitemap == "false" || $includeInSitemap === false) { 198 | // explicitly set to "false" or boolean false, so exclude 199 | return false; 200 | } 201 | 202 | // yep, keep it 203 | return true; 204 | })->map(function ($entry) { 205 | 206 | $changeFreqKey = config('sitemapamic.mappings.change_frequency', 'meta_change_frequency'); 207 | $changeFreq = $entry->get($changeFreqKey) ?? $entry->getComputed($changeFreqKey); 208 | if ($changeFreq == 'default') { 209 | // clear back to use default 210 | $changeFreq = null; 211 | } 212 | 213 | $priorityKey = config('sitemapamic.mappings.priority', 'meta_priority'); 214 | 215 | // return the entry as a Sitemapamic URL 216 | return new SitemapamicUrl( 217 | URL::makeAbsolute($entry->url()), 218 | Carbon::parse($entry->get('updated_at'))->toW3cString(), 219 | $changeFreq ?? config('sitemapamic.defaults.'.$entry->collection()->handle().'.frequency', 220 | false), 221 | $entry->get($priorityKey) ?? $entry->getComputed($priorityKey) ?? config('sitemapamic.defaults.'.$entry->collection()->handle().'.priority', 222 | false) 223 | ); 224 | })->toArray(); 225 | } 226 | ]; 227 | }); 228 | } 229 | 230 | /** 231 | * Gets the Taxonomy pages for the collections where they are used. 232 | * 233 | * lastmod will be set to the Term's updated_at time, or the latest entry's 234 | * updated_at time, whichever is more recent. 235 | * 236 | * Returns a collection of \MityDigital\Sitemapamic\Models\SitemapamicUrl 237 | * 238 | * @return \Illuminate\Support\Collection 239 | */ 240 | protected function loadCollectionTerms(): \Illuminate\Support\Collection 241 | { 242 | // get the current site key based on the url 243 | $site = Site::default(); 244 | foreach (Site::all() as $key => $props) { 245 | if ($props->url() == url('/')) { 246 | $site = $props; 247 | break; 248 | } 249 | } 250 | 251 | return collect(config('sitemapamic.defaults'))->map(function ($properties, $handle) use ($site) { 252 | 253 | // if there is a property called includeTaxonomies, and its false (or the collection is disabled) then exclude it 254 | // this has been added for backwards compatibility 255 | if (isset($properties['includeTaxonomies']) && (!$properties['includeTaxonomies'] || !$properties['include'])) { 256 | return false; 257 | } 258 | 259 | $collection = Collection::findByHandle($handle); 260 | 261 | return $collection->taxonomies()->map->collection($collection)->mapWithKeys(function ($taxonomy) use ( 262 | $properties, 263 | $handle, 264 | $site 265 | ) { 266 | return [ 267 | $handle.'_'.$taxonomy->handle => function () use ($taxonomy, $site) { 268 | 269 | return $taxonomy->queryTerms()->get()->filter(function ($term) use ($site) { 270 | if (!$term->published()) { 271 | return false; 272 | } 273 | 274 | // site is not configured, so exclude 275 | if (!$term->collection()->sites()->contains($site->handle())) { 276 | return false; 277 | } 278 | 279 | // include_xml_sitemap is one of null (when not set, so default to true), then either false or true 280 | $includeInSitemap = $term->get(config('sitemapamic.mappings.include', 'meta_include_in_xml_sitemap')); 281 | if ($includeInSitemap === null) { 282 | // get the default config, or return true by default 283 | return config('sitemapamic.defaults.'.$term->collection()->handle().'.include', true); 284 | } elseif ($includeInSitemap === false) { 285 | // explicitly set to false, so exclude 286 | return false; 287 | } 288 | 289 | return true; // this far, accept it 290 | })->map(function ($term) use ($site) { 291 | // get the term mod date 292 | $lastMod = $term->get('updated_at'); 293 | 294 | // get entries 295 | $termEntries = $term->queryEntries()->orderBy('updated_at', 'desc'); 296 | 297 | // if this term has entries, get the greater of the two updated_at timestamps 298 | if ($termEntries->count() > 0) { 299 | // get the last modified entry 300 | $entryLastMod = $termEntries->first()->get('updated_at'); 301 | 302 | // entry date is after the term's mod date 303 | if ($entryLastMod > $lastMod) { 304 | $lastMod = $entryLastMod; 305 | } 306 | } 307 | 308 | $changeFreq = $term->get(config('sitemapamic.mappings.change_frequency', 'meta_change_frequency')); 309 | if ($changeFreq == 'default') { 310 | // clear back to use default 311 | $changeFreq = null; 312 | } 313 | 314 | 315 | // get the site URL, or the app URL if its "/" 316 | //$siteUrl = config('statamic.sites.sites.'.$term->locale().'.url'); 317 | $siteUrl = $site->url(); 318 | if ($siteUrl == '/') { 319 | $siteUrl = config('app.url'); 320 | } 321 | 322 | return new SitemapamicUrl( 323 | $siteUrl.$term->url(), 324 | Carbon::parse($lastMod)->toW3cString(), 325 | $changeFreq ?? config('sitemapamic.defaults.'.$term->collection()->handle().'.frequency', 326 | false), 327 | $term->get(config('sitemapamic.mappings.priority', 'meta_priority')) ?? config('sitemapamic.defaults.'.$term->collection()->handle().'.priority', 328 | false) 329 | ); 330 | }); 331 | } 332 | ]; 333 | }); 334 | })->flatMap(fn($items) => $items); 335 | } 336 | 337 | protected function loadGlobalTaxonomies(): \Illuminate\Support\Collection 338 | { 339 | // are we configured to load the global taxonomies? 340 | // if so, what? 341 | $taxonomies = config('sitemapamic.globals.taxonomies', []); 342 | 343 | if (empty($taxonomies)) { 344 | // return an empty collection - either set to false, or not set yet 345 | return collect(); 346 | } 347 | 348 | // get the current site key based on the url 349 | $site = Site::default(); 350 | foreach (Site::all() as $key => $props) { 351 | if ($props->url() == url('/')) { 352 | $site = $props; 353 | break; 354 | } 355 | } 356 | 357 | return collect($taxonomies)->mapWithKeys(function ($properties, $handle) use ($site) { 358 | return [ 359 | $handle => function () use ($handle, $site) { 360 | 361 | // get the taxonomy repository 362 | $taxonomy = Taxonomy::find($handle); 363 | 364 | // if the taxonomy isn't configured for the site, get out 365 | if (!$taxonomy->sites()->contains($site)) { 366 | return null; 367 | } 368 | 369 | // does a view exist for this taxonomy? 370 | // if not, it will 404, so let's not do any more 371 | if (!view()->exists($handle.'/show')) { 372 | return null; 373 | } 374 | 375 | // get the terms 376 | return Term::whereTaxonomy($handle) 377 | ->filter(function ($term) { 378 | // should we include this term? 379 | // include_xml_sitemap is one of null (when not set, so default to true), then either false or true 380 | $includeInSitemap = $term->get(config('sitemapamic.mappings.include', 'meta_include_in_xml_sitemap')); 381 | if ($includeInSitemap === "false" || $includeInSitemap === false) { 382 | // explicitly set to "false" or boolean false, so exclude 383 | return false; 384 | } 385 | 386 | // there is no meta field for the term, so include it 387 | // Why? Because if we made it this far, the Taxonomy is part of the global config, so 388 | // we want to include it. So just include it. 389 | return true; 390 | }) 391 | ->map(function (\Statamic\Taxonomies\LocalizedTerm $term) use ($site) { 392 | // get the term mod date 393 | $lastMod = $term->get('updated_at'); 394 | 395 | // get entries 396 | $termEntries = $term->queryEntries()->orderBy('updated_at', 'desc'); 397 | 398 | // if this term has entries, get the greater of the two updated_at timestamps 399 | if ($termEntries->count() > 0) { 400 | // get the last modified entry 401 | $entryLastMod = $termEntries->first()->get('updated_at'); 402 | 403 | // entry date is after the term's mod date 404 | if ($entryLastMod > $lastMod) { 405 | $lastMod = $entryLastMod; 406 | } 407 | } 408 | 409 | $changeFreq = $term->get(config('sitemapamic.mappings.change_frequency', 'meta_change_frequency')); 410 | if ($changeFreq == 'default') { 411 | // clear back to use default 412 | $changeFreq = null; 413 | } 414 | 415 | // get the site URL, or the app URL if its "/" 416 | //$siteUrl = config('statamic.sites.sites.'.$term->locale().'.url'); 417 | $siteUrl = $site->url(); 418 | if ($siteUrl == '/') { 419 | $siteUrl = config('app.url'); 420 | } 421 | 422 | return new SitemapamicUrl( 423 | $siteUrl.$term->url(), 424 | Carbon::parse($lastMod)->toW3cString(), 425 | $changeFreq ?? 426 | config('sitemapamic.globals.taxonomies.'.$term->taxonomy()->handle().'.frequency', 427 | false), 428 | $term->get(config('sitemapamic.mappings.priority', 'meta_priority')) ?? config('sitemapamic.globals.taxonomies.'.$term->taxonomy()->handle().'.priority', 429 | false) 430 | ); 431 | }); 432 | } 433 | ]; 434 | }); 435 | } 436 | 437 | public function hasDynamicRoutes(): bool 438 | { 439 | return (bool) count($this->dynamicRoutes); 440 | } 441 | 442 | protected function loadDynamicRoutes(): \Illuminate\Support\Collection 443 | { 444 | // get the dynamic routes, if any are set, and only return them if they are a SitemapamicUrl 445 | return collect([ 446 | 'dynamic' => function () { 447 | return collect($this->getDynamicRoutes()) 448 | ->flatMap(function ($closure) { 449 | return $closure(); 450 | }) 451 | ->filter(fn($route) => get_class($route) == SitemapamicUrl::class); 452 | } 453 | ]); 454 | } 455 | 456 | /** 457 | * Returns the dynamic routes 458 | * 459 | * @return array|mixed 460 | */ 461 | public function getDynamicRoutes() 462 | { 463 | return $this->dynamicRoutes; 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------