Skip to content

Commit 116aa77

Browse files
authored
Merge pull request #162 from RomainGueffier/feature/161-hreflang-x-default-tag
Feature add hreflang x-default tag to sitemap
2 parents 109e5a6 + 9fe3b09 commit 116aa77

File tree

8 files changed

+187
-1
lines changed

8 files changed

+187
-1
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,14 @@ This setting will add a default `/` entry to the sitemap XML when none is presen
208208

209209
> `required:` NO | `type:` bool | `default:` true
210210
211+
### Default language URL (x-default)
212+
213+
This setting will add an additionnal `<link />` tag into each sitemap urls bundles with value `hreflang="x-default"` and the path of your choice. The hreflang x-default value is used to specify the language and region neutral URL for a piece of content when the site doesn't support the user's language and region. For example, if a page has hreflang annotations for English and Spanish versions of a page along with an x-default value pointing to the English version, French speaking users are sent to the English version of the page due to the x-default annotation. The x-default page can be a language and country selector page, the page where you redirect users when you have no content for their region, or just the version of the content that you consider default.
214+
215+
###### Key: `defaultLanguageUrlType`
216+
217+
> `required:` NO | `type:` string | `default:` ''
218+
211219
## 🔧 Config
212220
Config can be changed in the `config/plugins.js` file in your Strapi project.
213221
You can overwrite the config like so:

admin/src/config/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ export const GET_SITEMAP_INFO_SUCCEEDED = 'Sitemap/ConfigPage/GET_SITEMAP_INFO_S
2727
export const ON_CHANGE_CUSTOM_ENTRY = 'Sitemap/ConfigPage/ON_CHANGE_CUSTOM_ENTRY';
2828
export const GET_ALLOWED_FIELDS_SUCCEEDED = 'Sitemap/ConfigPage/GET_ALLOWED_FIELDS_SUCCEEDED';
2929
export const SET_LOADING_STATE = 'Sitemap/ConfigPage/SET_LOADING_STATE';
30+
export const DEFAULT_LANGUAGE_URL_TYPE_DEFAULT_LOCALE = 'default-locale';
31+
export const DEFAULT_LANGUAGE_URL_TYPE_OTHER = 'other';

admin/src/tabs/Settings/index.jsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ import {
1010
Grid,
1111
GridItem,
1212
TextInput,
13+
SingleSelect,
14+
SingleSelectOption,
1315
useTheme,
1416
} from '@strapi/design-system';
1517

1618
import { onChangeSettings } from '../../state/actions/Sitemap';
1719
import HostnameModal from '../../components/HostnameModal';
20+
import { DEFAULT_LANGUAGE_URL_TYPE_DEFAULT_LOCALE, DEFAULT_LANGUAGE_URL_TYPE_OTHER } from '../../config/constants';
1821

1922
const Settings = () => {
2023
const { formatMessage } = useIntl();
@@ -23,13 +26,20 @@ const Settings = () => {
2326
const languages = useSelector((store) => store.getIn(['sitemap', 'languages'], {}));
2427
const settings = useSelector((state) => state.getIn(['sitemap', 'settings'], Map()));
2528
const hostnameOverrides = useSelector((state) => state.getIn(['sitemap', 'settings', 'hostname_overrides'], {}));
29+
const [inputVisible, setInputVisible] = useState(settings.get('defaultLanguageUrlType') === DEFAULT_LANGUAGE_URL_TYPE_OTHER);
2630
const theme = useTheme();
2731

2832
const saveHostnameOverrides = (hostnames) => {
2933
dispatch(onChangeSettings('hostname_overrides', hostnames));
3034
setOpen(false);
3135
};
3236

37+
const handleDefaultLanguageUrlTypeChange = (value = '') => {
38+
dispatch(onChangeSettings('defaultLanguageUrlType', value));
39+
if (value === DEFAULT_LANGUAGE_URL_TYPE_OTHER) dispatch(onChangeSettings('defaultLanguageUrl', undefined));
40+
setInputVisible(value === DEFAULT_LANGUAGE_URL_TYPE_OTHER);
41+
};
42+
3343
return (
3444
<Grid gap={4}>
3545
<GridItem col={6} s={12}>
@@ -88,6 +98,41 @@ const Settings = () => {
8898
onChange={(e) => dispatch(onChangeSettings('excludeDrafts', e.target.checked))}
8999
/>
90100
</GridItem>
101+
<GridItem col={6} s={12}>
102+
<SingleSelect
103+
hint={formatMessage({ id: 'sitemap.Settings.Field.DefaultLanguageUrlType.Description', defaultMessage: 'Generate a link tag and attribute hreflang=x-default with the URL of your choice.' })}
104+
label={formatMessage({ id: 'sitemap.Settings.Field.DefaultLanguageUrlType.Label', defaultMessage: 'Default language URL type' })}
105+
name="defaultLanguageUrlType"
106+
onLabel="on"
107+
offLabel="off"
108+
value={settings.get('defaultLanguageUrlType')}
109+
onChange={handleDefaultLanguageUrlTypeChange}
110+
onClear={handleDefaultLanguageUrlTypeChange}
111+
>
112+
<SingleSelectOption value="">
113+
{formatMessage({ id: 'sitemap.Settings.Field.DefaultLanguageUrlType.Option.Disabled', defaultMessage: 'Disabled' })}
114+
</SingleSelectOption>
115+
<SingleSelectOption value={DEFAULT_LANGUAGE_URL_TYPE_DEFAULT_LOCALE}>
116+
{formatMessage({ id: 'sitemap.Settings.Field.DefaultLanguageUrlType.Option.DefaultLocale', defaultMessage: 'Default language URL of bundles (generated from default locale URL)' })}
117+
</SingleSelectOption>
118+
<SingleSelectOption value={DEFAULT_LANGUAGE_URL_TYPE_OTHER}>
119+
{formatMessage({ id: 'sitemap.Settings.Field.DefaultLanguageUrlType.Option.Other', defaultMessage: 'Other' })}
120+
</SingleSelectOption>
121+
</SingleSelect>
122+
</GridItem>
123+
{inputVisible && (
124+
<GridItem col={12} s={12}>
125+
<TextInput
126+
placeholder="https://www.strapi.io/language-selector"
127+
hint={formatMessage({ id: 'sitemap.Settings.Field.DefaultLanguageUrl.Description', defaultMessage: 'E.g. URL of your website language selector.' })}
128+
label={formatMessage({ id: 'sitemap.Settings.Field.DefaultLanguageUrl.Label', defaultMessage: 'Custom default language URL' })}
129+
name="defaultLanguageUrl"
130+
required
131+
value={settings.get('defaultLanguageUrl')}
132+
onChange={(e) => dispatch(onChangeSettings('defaultLanguageUrl', e.target.value))}
133+
/>
134+
</GridItem>
135+
)}
91136
</Grid>
92137
);
93138
};

admin/src/translations/en.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@
2626
"Settings.Field.IncludeHomepage.Description": "Include a '/' entry when none is present.",
2727
"Settings.Field.ExcludeDrafts.Label": "Exclude drafts",
2828
"Settings.Field.ExcludeDrafts.Description": "Remove all draft entries from the sitemap.",
29+
"Settings.Field.DefaultLanguageUrlType.Label": "Default language URL type",
30+
"Settings.Field.DefaultLanguageUrlType.Description": "Generate a link tag and attribute hreflang=x-default with the URL of your choice.",
31+
"Settings.Field.DefaultLanguageUrlType.Option.Disabled": "Disabled",
32+
"Settings.Field.DefaultLanguageUrlType.Option.DefaultLocale": "Default language URL of bundles (generated from default locale URL)",
33+
"Settings.Field.DefaultLanguageUrlType.Option.Other": "Other",
34+
"Settings.Field.DefaultLanguageUrl.Label": "Custom default language URL.",
35+
"Settings.Field.DefaultLanguageUrl.Description": "E.g. URL of your website language selector.",
2936
"Settings.Field.URL.Label": "Slug",
3037
"Settings.Field.URL.Description": "This field forces the UID type regex",
3138
"Settings.Field.Priority.Label": "Priority",

admin/src/translations/fr.json

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,84 @@
1-
{}
1+
{
2+
"Settings.Configuration.Title": "Configuration",
3+
4+
"Button.Save": "Sauvegarder",
5+
"Button.Cancel": "Annuler",
6+
"Button.Add": "Ajouter",
7+
"Button.AddURL": "Ajouter une URL",
8+
"Button.AddURLBundle": "Ajouter une autre collection d'URLs",
9+
"Button.AddCustomURL": "Ajouter une autre URL",
10+
11+
"Header.Title": "Sitemap",
12+
"Header.Description": "Paramètres pour le sitemap XML",
13+
"Header.Button.Generate": "Générer le sitemap",
14+
"Header.Button.SitemapLink": "Visualiser le sitemap",
15+
"Header.Button.GoToSettings": "Accéder au paramètres",
16+
17+
"Settings.CollectionTitle": "Collections d'URLs",
18+
"Settings.CustomTitle": "URLs personnalisées",
19+
"Settings.SettingsTitle": "Paramètres",
20+
"Settings.Field.Hostname.Label": "Nom de domaine",
21+
"Settings.Field.Hostname.Description": "L'URL de votre site internet",
22+
"Settings.Field.HostnameOverrides.Label": "Domaines personnalisés",
23+
"Settings.Field.HostnameOverrides.Button": "Configurer",
24+
"Settings.Field.HostnameOverrides.Description": "Spécifier le domaine par langue",
25+
"Settings.Field.IncludeHomepage.Label": "Inclure la page d'accueil",
26+
"Settings.Field.IncludeHomepage.Description": "Ajoute une entrée '/' si cette page n'existe pas.",
27+
"Settings.Field.ExcludeDrafts.Label": "Exclure les brouillons",
28+
"Settings.Field.ExcludeDrafts.Description": "Retire tous les brouillons du sitemap.",
29+
"Settings.Field.DefaultLanguageUrlType.Label": "Type d'URL de la langue par défaut",
30+
"Settings.Field.DefaultLanguageUrlType.Description": "Génère une balise link et attribut hreflang=x-default avec l'URL de votre choix.",
31+
"Settings.Field.DefaultLanguageUrl.Label": "URL de la langue par défaut personnalisée",
32+
"Settings.Field.DefaultLanguageUrl.Description": "Ex. URL de la page de sélection de langue.",
33+
"Settings.Field.DefaultLanguageUrlType.Option.Disabled": "Désactivé",
34+
"Settings.Field.DefaultLanguageUrlType.Option.DefaultLocale": "URL par défaut des routes (généré à partir de l'URL de la locale par défaut)",
35+
"Settings.Field.DefaultLanguageUrlType.Option.Other": "Autre",
36+
"Settings.Field.URL.Label": "Slug",
37+
"Settings.Field.URL.Description": "Ce champ requiert une regex de type UID",
38+
"Settings.Field.Priority.Label": "Priorité",
39+
"Settings.Field.Priority.Description": "La priorité des pages.",
40+
"Settings.Field.Changefreq.Label": "Changefreq",
41+
"Settings.Field.Changefreq.Description": "Le fréquence de mise à jour des pages.",
42+
"Settings.Field.IncludeLastmod.Label": "Lastmod",
43+
"Settings.Field.IncludeLastmod.Description": "Ajoute une balise <lastmod> à toutes les URLs de son type.",
44+
"Settings.Field.Pattern.Label": "Modèle",
45+
"Settings.Field.Pattern.DescriptionPart1": "Crée un modèle d'URL dynamique.",
46+
"Settings.Field.Pattern.DescriptionPart2": "utilise",
47+
"Settings.Field.Pattern.DescriptionPart3": "et",
48+
"Settings.Field.Pattern.Error": "Ce modèle n'est pas valide.",
49+
"Settings.Field.SelectContentType.Label": "Type de contenu",
50+
"Settings.Field.SelectContentType.Description": "Sélectionne un type de contenu.",
51+
"Settings.Field.SelectLanguage.Label": "Langue",
52+
"Settings.Field.SelectLanguage.Description": "Sélectionne une langue.",
53+
"Settings.Field.SelectLanguage.SameForAll": "Identique pour toutes les langues",
54+
55+
"Modal.HeaderTitle": "Entrées du Sitemap",
56+
"Modal.Tabs.Basic.Title": "Paramètres",
57+
"Modal.Tabs.Advanced.Title": "Paramètres avancés",
58+
59+
"HostnameOverrides.Label": "Domaines personnalisés",
60+
"HostnameOverrides.Description": "Appliquer un domaine pour les URLs avec la locale {langcode}",
61+
62+
"Info.NoHostname.Title": "Ajouter votre domaine",
63+
"Info.NoHostname.Description": "Avant de générer votre sitemap, vous dever renseigner le domaine de votre site.",
64+
"Info.NoSitemap.Title": "Pas de sitemap XML présent",
65+
"Info.NoSitemap.Description": "Générer votre premier sitemap XML avec le bouton ci-dessous.",
66+
"Info.SitemapIsPresent.Title": "Sitemap XML présent",
67+
"Info.SitemapIsPresent.LastUpdatedAt": "Dernière modification le:",
68+
"Info.SitemapIsPresent.AmountOfURLs": "Nombre d'URLs:",
69+
"Info.SitemapIsPresent.AmountOfSitemaps": "Nombre de sitemaps:",
70+
71+
"EditView.ExcludeFromSitemap": "Exclure du Sitemap",
72+
73+
"Empty.URLBundles.Description": "Aucune collection d'URLs configurée.",
74+
"Empty.URLBundles.Button": "Ajouter la première collection d'URL",
75+
76+
"Empty.CustomURLs.Description": "Aucune URL personnalisée configurée.",
77+
"Empty.CustomURLs.Button": "Ajouter la première URL",
78+
79+
"notification.success.submit": "Les paramètres ont été mis à jour",
80+
"notification.success.generate": "Le sitemap a été généré",
81+
82+
"plugin.name": "Sitemap",
83+
"plugin.name.extended": "Plugin Sitemap"
84+
}

playground/src/api/test/content-types/test/schema.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@
2323
"localized": true
2424
}
2525
}
26+
},
27+
"slug": {
28+
"pluginOptions": {
29+
"i18n": {
30+
"localized": true
31+
}
32+
},
33+
"type": "uid",
34+
"targetField": "title",
35+
"required": true
2636
}
2737
}
2838
}

server/services/core.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,29 @@ const { isEmpty } = require('lodash');
1010

1111
const { logMessage, getService, formatCache, mergeCache } = require('../utils');
1212

13+
/**
14+
* Add link x-default url to url bundles from strapi i18n plugin default locale.
15+
*
16+
* @param {object} config - The config object.
17+
* @param {object} links - The language links.
18+
*
19+
* @returns {object | undefined} The default language link.
20+
*/
21+
const getDefaultLanguageLink = async (config, links) => {
22+
if (config.defaultLanguageUrlType === 'default-locale') {
23+
const { getDefaultLocale } = strapi.plugin('i18n').service('locales');
24+
const defaultLocale = await getDefaultLocale();
25+
26+
// find url with default locale in generated bundle
27+
const url = links.find((link) => link.lang === defaultLocale)?.url;
28+
if (url) return { lang: 'x-default', url };
29+
}
30+
31+
if (config.defaultLanguageUrlType === 'other' && config.defaultLanguageUrl) {
32+
return { lang: 'x-default', url: config.defaultLanguageUrl };
33+
}
34+
};
35+
1336
/**
1437
* Get a formatted array of different language URLs of a single page.
1538
*
@@ -52,6 +75,12 @@ const getLanguageLinks = async (config, page, contentType, defaultURL) => {
5275
});
5376
}));
5477

78+
// add optional x-default link url
79+
if (config.defaultLanguageUrlType) {
80+
const defaultLink = await getDefaultLanguageLink(config, links);
81+
if (defaultLink) links.push(defaultLink);
82+
}
83+
5584
return links;
5685
};
5786

server/services/settings.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const createDefaultConfig = async () => {
1919
hostname: '',
2020
includeHomepage: true,
2121
excludeDrafts: true,
22+
defaultLanguageUrlType: '',
23+
defaultLanguageUrl: '',
2224
hostname_overrides: {},
2325
contentTypes: Map({}),
2426
customEntries: Map({}),

0 commit comments

Comments
 (0)