Skip to content

Commit 9032ef3

Browse files
Proper hreflang for localized entries in sitemap (#400)
Co-authored-by: Duncan McClean <duncan@duncanmcclean.com>
1 parent 0b3fa80 commit 9032ef3

File tree

9 files changed

+497
-329
lines changed

9 files changed

+497
-329
lines changed
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
{{ xml_header }}
2-
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
2+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
33
{{ pages }}
44
<url>
55
<loc>{{ loc }}</loc>
66
<lastmod>{{ lastmod }}</lastmod>
77
<changefreq>{{ changefreq }}</changefreq>
88
<priority>{{ priority }}</priority>
9+
{{ hreflangs }}
10+
<xhtml:link rel="alternate" hreflang="{{ hreflang }}" href="{{ href }}" />
11+
{{ /hreflangs }}
912
</url>
1013
{{ /pages }}
1114
</urlset>

resources/views/meta.antlers.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,14 @@
9090

9191
{{ if alternate_locales }}
9292
<link rel="alternate" href="{{ canonical_url | striptags | entities | trim }}" hreflang="{{ current_hreflang }}" />
93+
{{ if is_default_site }}
94+
<link rel="alternate" href="{{ canonical_url | striptags | entities | trim }}" hreflang="x-default" />
95+
{{ /if }}
9396
{{ alternate_locales }}
9497
<link rel="alternate" href="{{ url | striptags | entities | trim }}" hreflang="{{ hreflang }}" />
98+
{{ if is_default_site }}
99+
<link rel="alternate" href="{{ url | striptags | entities | trim }}" hreflang="x-default" />
100+
{{ /if }}
95101
{{ /alternate_locales }}
96102
{{ /if }}
97103
{{ /unless }}

src/Cascade.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ public function get()
9898
'home_url' => Str::removeRight(Site::current()?->absoluteUrl() ?? URL::makeAbsolute('/'), '/'),
9999
'humans_txt' => $this->humans(),
100100
'site' => $this->site(),
101+
'is_default_site' => $this->site()->isDefault(),
101102
'alternate_locales' => $alternateLocales = $this->alternateLocales(),
102103
'current_hreflang' => $this->currentHreflang($alternateLocales),
103104
'last_modified' => $this->lastModified(),
@@ -346,7 +347,8 @@ protected function alternateLocales()
346347
->reject(fn ($locale) => collect(config('statamic.seo-pro.alternate_locales.excluded_sites'))->contains($locale))
347348
->map(function ($locale) {
348349
return [
349-
'site' => Config::getSite($locale),
350+
'site' => $site = Site::get($locale),
351+
'is_default_site' => $site->isDefault(),
350352
'url' => $this->model->in($locale)->absoluteUrl(),
351353
];
352354
});

src/Sitemap/Page.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ public function changefreq()
3333
return $this->data->get('change_frequency');
3434
}
3535

36+
public function hrefLangs(): array
37+
{
38+
return $this->data->get('hreflangs', []);
39+
}
40+
3641
public function priority()
3742
{
3843
return $this->data->get('priority');
@@ -46,6 +51,7 @@ public function toArray()
4651
'lastmod' => $this->lastmod(),
4752
'changefreq' => $this->changefreq(),
4853
'priority' => $this->priority(),
54+
'hreflangs' => $this->hrefLangs(),
4955
];
5056
}
5157
}

src/Sitemap/Sitemap.php

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@
44

55
use Illuminate\Support\Collection as IlluminateCollection;
66
use Illuminate\Support\LazyCollection;
7-
use Statamic\Contracts\Entries\QueryBuilder;
7+
use Statamic\Contracts\Entries\Entry;
8+
use Statamic\Contracts\Query\Builder;
9+
use Statamic\Contracts\Taxonomies\Term;
810
use Statamic\Facades\Blink;
911
use Statamic\Facades\Collection;
1012
use Statamic\Facades\Entry as EntryFacade;
13+
use Statamic\Facades\Site as SiteFacade;
1114
use Statamic\Facades\Taxonomy;
1215
use Statamic\SeoPro\Cascade;
1316
use Statamic\SeoPro\GetsSectionDefaults;
1417
use Statamic\SeoPro\SiteDefaults;
18+
use Statamic\Sites\Site;
1519
use Statamic\Support\Traits\Hookable;
1620

1721
class Sitemap
@@ -128,6 +132,8 @@ protected function getPages($items)
128132
->withCurrent($content)
129133
->get();
130134

135+
$data['hreflangs'] = $this->hrefLangs($content);
136+
131137
return (new Page)->with($data);
132138
})
133139
->filter();
@@ -148,7 +154,7 @@ protected function publishedEntriesQuery()
148154
return EntryFacade::query()
149155
->when(
150156
$this->sites->isNotEmpty(),
151-
fn (QueryBuilder $query) => $query->whereIn('site', $this->sites->map->handle()->all())
157+
fn (Builder $query) => $query->whereIn('site', $this->sites->map->handle()->all())
152158
)
153159
->whereIn('collection', $collections)
154160
->whereNotNull('uri')
@@ -182,7 +188,10 @@ protected function publishedTerms()
182188
return Taxonomy::all()
183189
->flatMap(function ($taxonomy) {
184190
return $taxonomy->cascade('seo') !== false
185-
? $taxonomy->queryTerms()->get()
191+
? $taxonomy
192+
->queryTerms()
193+
->when($this->sites->isNotEmpty(), fn (Builder $query) => $query->whereIn('site', $this->sites->map->handle()->all()))
194+
->get()
186195
: collect();
187196
})
188197
->filter
@@ -202,7 +211,10 @@ protected function publishedCollectionTerms()
202211
})
203212
->flatMap(function ($taxonomy) {
204213
return $taxonomy->cascade('seo') !== false
205-
? $taxonomy->queryTerms()->get()->map->collection($taxonomy->collection())
214+
? $taxonomy
215+
->queryTerms()
216+
->when($this->sites->isNotEmpty(), fn (Builder $query) => $query->whereIn('site', $this->sites->map->handle()->all()))
217+
->get()->map->collection($taxonomy->collection())
206218
: collect();
207219
})
208220
->filter
@@ -227,4 +239,54 @@ protected function getSiteDefaults()
227239
return SiteDefaults::load()->all();
228240
});
229241
}
242+
243+
protected function hrefLangs($content): array
244+
{
245+
if (
246+
config('statamic.seo-pro.alternate_locales') === false
247+
|| config('statamic.seo-pro.alternate_locales.enabled') === false
248+
) {
249+
return [];
250+
}
251+
252+
return match (true) {
253+
$content instanceof Entry => $this->hrefLangsForEntry($content),
254+
$content instanceof Term => $this->hrefLangsForTerm($content),
255+
default => [],
256+
};
257+
}
258+
259+
private function hrefLangsForEntry(Entry $entry): array
260+
{
261+
return SiteFacade::all()
262+
->values()
263+
->filter(fn (Site $site) => $entry->in($site->handle()))
264+
->filter(fn (Site $site) => $entry->in($site->handle())->published())
265+
->reject(fn (Site $site) => collect(config('statamic.seo-pro.alternate_locales.excluded_sites'))->contains($site->handle()))
266+
->map(fn (Site $site) => [
267+
'href' => $entry->in($site->handle())->absoluteUrl(),
268+
'hreflang' => strtolower(str_replace('_', '-', $site->locale())),
269+
])
270+
->push([
271+
'href' => $entry->root()->absoluteUrl(),
272+
'hreflang' => 'x-default',
273+
])
274+
->all();
275+
}
276+
277+
private function hrefLangsForTerm(Term $term): array
278+
{
279+
return SiteFacade::all()
280+
->values()
281+
->reject(fn (Site $site) => collect(config('statamic.seo-pro.alternate_locales.excluded_sites'))->contains($site->handle()))
282+
->map(fn (Site $site) => [
283+
'href' => $term->in($site->handle())->absoluteUrl(),
284+
'hreflang' => strtolower(str_replace('_', '-', $site->locale())),
285+
])
286+
->push([
287+
'href' => $term->inDefaultLocale()->absoluteUrl(),
288+
'hreflang' => 'x-default',
289+
])
290+
->all();
291+
}
230292
}

tests/Localized/GraphQLTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public function it_queries_multisite_for_canonical_url_and_alternate_locales_in_
4444
'<link href="http://cool-runnings.com/fr/nectar" rel="canonical" />',
4545
'<link rel="alternate" href="http://cool-runnings.com/fr/nectar" hreflang="fr" />',
4646
'<link rel="alternate" href="http://cool-runnings.com/nectar" hreflang="en" />',
47+
'<link rel="alternate" href="http://cool-runnings.com/nectar" hreflang="x-default" />',
4748
'<link type="text/plain" rel="author" href="http://cool-runnings.com/humans.txt" />',
4849
])->implode('');
4950

tests/Localized/MetaTagTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public function it_generates_multisite_meta($viewType)
4242

4343
$expectedAlternateHreflangMeta = <<<'EOT'
4444
<link rel="alternate" href="http://cool-runnings.com" hreflang="en-us" />
45+
<link rel="alternate" href="http://cool-runnings.com" hreflang="x-default" />
4546
<link rel="alternate" href="http://cool-runnings.com/fr" hreflang="fr" />
4647
<link rel="alternate" href="http://corse-fantastiche.it" hreflang="it" />
4748
<link rel="alternate" href="http://cool-runnings.com/en-gb" hreflang="en-gb" />
@@ -68,6 +69,7 @@ public function it_generates_multisite_meta_for_non_home_page_route($viewType)
6869

6970
$expectedAlternateHreflangMeta = <<<'EOT'
7071
<link rel="alternate" href="http://cool-runnings.com/about" hreflang="en" />
72+
<link rel="alternate" href="http://cool-runnings.com/about" hreflang="x-default" />
7173
<link rel="alternate" href="http://cool-runnings.com/fr/about" hreflang="fr" />
7274
<link rel="alternate" href="http://corse-fantastiche.it/about" hreflang="it" />
7375
EOT;
@@ -108,6 +110,7 @@ public function it_generates_multisite_meta_for_canonical_url_and_alternate_loca
108110
<link href="http://cool-runnings.com/fr/about" rel="canonical" />
109111
<link rel="alternate" href="http://cool-runnings.com/fr/about" hreflang="fr" />
110112
<link rel="alternate" href="http://cool-runnings.com/about" hreflang="en" />
113+
<link rel="alternate" href="http://cool-runnings.com/about" hreflang="x-default" />
111114
<link rel="alternate" href="http://corse-fantastiche.it/about" hreflang="it" />
112115
EOT;
113116

@@ -133,6 +136,7 @@ public function it_handles_duplicate_alternate_hreflangs($viewType)
133136
<link href="http://cool-runnings.com/fr" rel="canonical" />
134137
<link rel="alternate" href="http://cool-runnings.com/fr" hreflang="fr" />
135138
<link rel="alternate" href="http://cool-runnings.com" hreflang="en-us" />
139+
<link rel="alternate" href="http://cool-runnings.com" hreflang="x-default" />
136140
<link rel="alternate" href="http://corse-fantastiche.it" hreflang="it" />
137141
<link rel="alternate" href="http://cool-runnings.com/en-gb" hreflang="en-gb" />
138142
EOT;
@@ -158,6 +162,7 @@ public function it_handles_duplicate_current_hreflang($viewType)
158162
<link href="http://cool-runnings.com/en-gb" rel="canonical" />
159163
<link rel="alternate" href="http://cool-runnings.com/en-gb" hreflang="en-gb" />
160164
<link rel="alternate" href="http://cool-runnings.com" hreflang="en-us" />
165+
<link rel="alternate" href="http://cool-runnings.com" hreflang="x-default" />
161166
<link rel="alternate" href="http://cool-runnings.com/fr" hreflang="fr" />
162167
<link rel="alternate" href="http://corse-fantastiche.it" hreflang="it" />
163168
EOT;
@@ -203,6 +208,7 @@ public function it_doesnt_generate_multisite_meta_for_excluded_sites($viewType)
203208

204209
$expectedAlternateHreflangMeta = <<<'EOT'
205210
<link rel="alternate" href="http://cool-runnings.com/about" hreflang="en" />
211+
<link rel="alternate" href="http://cool-runnings.com/about" hreflang="x-default" />
206212
<link rel="alternate" href="http://corse-fantastiche.it/about" hreflang="it" />
207213
EOT;
208214

@@ -230,6 +236,7 @@ public function it_doesnt_generate_multisite_meta_for_unpublished_content($viewT
230236

231237
$expectedAlternateHreflangMeta = <<<'EOT'
232238
<link rel="alternate" href="http://cool-runnings.com/about" hreflang="en" />
239+
<link rel="alternate" href="http://cool-runnings.com/about" hreflang="x-default" />
233240
<link rel="alternate" href="http://corse-fantastiche.it/about" hreflang="it" />
234241
EOT;
235242

@@ -261,6 +268,7 @@ public function it_doesnt_generate_multisite_meta_for_scheduled_content($viewTyp
261268

262269
$expectedAlternateHreflangMeta = <<<'EOT'
263270
<link rel="alternate" href="http://cool-runnings.com/about" hreflang="en" />
271+
<link rel="alternate" href="http://cool-runnings.com/about" hreflang="x-default" />
264272
<link rel="alternate" href="http://corse-fantastiche.it/about" hreflang="it" />
265273
EOT;
266274

@@ -292,6 +300,7 @@ public function it_doesnt_generate_multisite_meta_for_expired_content($viewType)
292300

293301
$expectedAlternateHreflangMeta = <<<'EOT'
294302
<link rel="alternate" href="http://cool-runnings.com/about" hreflang="en" />
303+
<link rel="alternate" href="http://cool-runnings.com/about" hreflang="x-default" />
295304
<link rel="alternate" href="http://corse-fantastiche.it/about" hreflang="it" />
296305
EOT;
297306

0 commit comments

Comments
 (0)