├── .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 | 
6 | 
7 | [](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 |
--------------------------------------------------------------------------------