Skip to content

Commit

Permalink
Merge pull request #162 from RomainGueffier/feature/161-hreflang-x-de…
Browse files Browse the repository at this point in the history
…fault-tag

Feature add hreflang x-default tag to sitemap
  • Loading branch information
boazpoolman authored Mar 13, 2024
2 parents 109e5a6 + 9fe3b09 commit 116aa77
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 1 deletion.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,14 @@ This setting will add a default `/` entry to the sitemap XML when none is presen

> `required:` NO | `type:` bool | `default:` true
### Default language URL (x-default)

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.

###### Key: `defaultLanguageUrlType`

> `required:` NO | `type:` string | `default:` ''
## 🔧 Config
Config can be changed in the `config/plugins.js` file in your Strapi project.
You can overwrite the config like so:
Expand Down
2 changes: 2 additions & 0 deletions admin/src/config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ export const GET_SITEMAP_INFO_SUCCEEDED = 'Sitemap/ConfigPage/GET_SITEMAP_INFO_S
export const ON_CHANGE_CUSTOM_ENTRY = 'Sitemap/ConfigPage/ON_CHANGE_CUSTOM_ENTRY';
export const GET_ALLOWED_FIELDS_SUCCEEDED = 'Sitemap/ConfigPage/GET_ALLOWED_FIELDS_SUCCEEDED';
export const SET_LOADING_STATE = 'Sitemap/ConfigPage/SET_LOADING_STATE';
export const DEFAULT_LANGUAGE_URL_TYPE_DEFAULT_LOCALE = 'default-locale';
export const DEFAULT_LANGUAGE_URL_TYPE_OTHER = 'other';
45 changes: 45 additions & 0 deletions admin/src/tabs/Settings/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import {
Grid,
GridItem,
TextInput,
SingleSelect,
SingleSelectOption,
useTheme,
} from '@strapi/design-system';

import { onChangeSettings } from '../../state/actions/Sitemap';
import HostnameModal from '../../components/HostnameModal';
import { DEFAULT_LANGUAGE_URL_TYPE_DEFAULT_LOCALE, DEFAULT_LANGUAGE_URL_TYPE_OTHER } from '../../config/constants';

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

const saveHostnameOverrides = (hostnames) => {
dispatch(onChangeSettings('hostname_overrides', hostnames));
setOpen(false);
};

const handleDefaultLanguageUrlTypeChange = (value = '') => {
dispatch(onChangeSettings('defaultLanguageUrlType', value));
if (value === DEFAULT_LANGUAGE_URL_TYPE_OTHER) dispatch(onChangeSettings('defaultLanguageUrl', undefined));
setInputVisible(value === DEFAULT_LANGUAGE_URL_TYPE_OTHER);
};

return (
<Grid gap={4}>
<GridItem col={6} s={12}>
Expand Down Expand Up @@ -88,6 +98,41 @@ const Settings = () => {
onChange={(e) => dispatch(onChangeSettings('excludeDrafts', e.target.checked))}
/>
</GridItem>
<GridItem col={6} s={12}>
<SingleSelect
hint={formatMessage({ id: 'sitemap.Settings.Field.DefaultLanguageUrlType.Description', defaultMessage: 'Generate a link tag and attribute hreflang=x-default with the URL of your choice.' })}
label={formatMessage({ id: 'sitemap.Settings.Field.DefaultLanguageUrlType.Label', defaultMessage: 'Default language URL type' })}
name="defaultLanguageUrlType"
onLabel="on"
offLabel="off"
value={settings.get('defaultLanguageUrlType')}
onChange={handleDefaultLanguageUrlTypeChange}
onClear={handleDefaultLanguageUrlTypeChange}
>
<SingleSelectOption value="">
{formatMessage({ id: 'sitemap.Settings.Field.DefaultLanguageUrlType.Option.Disabled', defaultMessage: 'Disabled' })}
</SingleSelectOption>
<SingleSelectOption value={DEFAULT_LANGUAGE_URL_TYPE_DEFAULT_LOCALE}>
{formatMessage({ id: 'sitemap.Settings.Field.DefaultLanguageUrlType.Option.DefaultLocale', defaultMessage: 'Default language URL of bundles (generated from default locale URL)' })}
</SingleSelectOption>
<SingleSelectOption value={DEFAULT_LANGUAGE_URL_TYPE_OTHER}>
{formatMessage({ id: 'sitemap.Settings.Field.DefaultLanguageUrlType.Option.Other', defaultMessage: 'Other' })}
</SingleSelectOption>
</SingleSelect>
</GridItem>
{inputVisible && (
<GridItem col={12} s={12}>
<TextInput
placeholder="https://www.strapi.io/language-selector"
hint={formatMessage({ id: 'sitemap.Settings.Field.DefaultLanguageUrl.Description', defaultMessage: 'E.g. URL of your website language selector.' })}
label={formatMessage({ id: 'sitemap.Settings.Field.DefaultLanguageUrl.Label', defaultMessage: 'Custom default language URL' })}
name="defaultLanguageUrl"
required
value={settings.get('defaultLanguageUrl')}
onChange={(e) => dispatch(onChangeSettings('defaultLanguageUrl', e.target.value))}
/>
</GridItem>
)}
</Grid>
);
};
Expand Down
7 changes: 7 additions & 0 deletions admin/src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
"Settings.Field.IncludeHomepage.Description": "Include a '/' entry when none is present.",
"Settings.Field.ExcludeDrafts.Label": "Exclude drafts",
"Settings.Field.ExcludeDrafts.Description": "Remove all draft entries from the sitemap.",
"Settings.Field.DefaultLanguageUrlType.Label": "Default language URL type",
"Settings.Field.DefaultLanguageUrlType.Description": "Generate a link tag and attribute hreflang=x-default with the URL of your choice.",
"Settings.Field.DefaultLanguageUrlType.Option.Disabled": "Disabled",
"Settings.Field.DefaultLanguageUrlType.Option.DefaultLocale": "Default language URL of bundles (generated from default locale URL)",
"Settings.Field.DefaultLanguageUrlType.Option.Other": "Other",
"Settings.Field.DefaultLanguageUrl.Label": "Custom default language URL.",
"Settings.Field.DefaultLanguageUrl.Description": "E.g. URL of your website language selector.",
"Settings.Field.URL.Label": "Slug",
"Settings.Field.URL.Description": "This field forces the UID type regex",
"Settings.Field.Priority.Label": "Priority",
Expand Down
85 changes: 84 additions & 1 deletion admin/src/translations/fr.json
Original file line number Diff line number Diff line change
@@ -1 +1,84 @@
{}
{
"Settings.Configuration.Title": "Configuration",

"Button.Save": "Sauvegarder",
"Button.Cancel": "Annuler",
"Button.Add": "Ajouter",
"Button.AddURL": "Ajouter une URL",
"Button.AddURLBundle": "Ajouter une autre collection d'URLs",
"Button.AddCustomURL": "Ajouter une autre URL",

"Header.Title": "Sitemap",
"Header.Description": "Paramètres pour le sitemap XML",
"Header.Button.Generate": "Générer le sitemap",
"Header.Button.SitemapLink": "Visualiser le sitemap",
"Header.Button.GoToSettings": "Accéder au paramètres",

"Settings.CollectionTitle": "Collections d'URLs",
"Settings.CustomTitle": "URLs personnalisées",
"Settings.SettingsTitle": "Paramètres",
"Settings.Field.Hostname.Label": "Nom de domaine",
"Settings.Field.Hostname.Description": "L'URL de votre site internet",
"Settings.Field.HostnameOverrides.Label": "Domaines personnalisés",
"Settings.Field.HostnameOverrides.Button": "Configurer",
"Settings.Field.HostnameOverrides.Description": "Spécifier le domaine par langue",
"Settings.Field.IncludeHomepage.Label": "Inclure la page d'accueil",
"Settings.Field.IncludeHomepage.Description": "Ajoute une entrée '/' si cette page n'existe pas.",
"Settings.Field.ExcludeDrafts.Label": "Exclure les brouillons",
"Settings.Field.ExcludeDrafts.Description": "Retire tous les brouillons du sitemap.",
"Settings.Field.DefaultLanguageUrlType.Label": "Type d'URL de la langue par défaut",
"Settings.Field.DefaultLanguageUrlType.Description": "Génère une balise link et attribut hreflang=x-default avec l'URL de votre choix.",
"Settings.Field.DefaultLanguageUrl.Label": "URL de la langue par défaut personnalisée",
"Settings.Field.DefaultLanguageUrl.Description": "Ex. URL de la page de sélection de langue.",
"Settings.Field.DefaultLanguageUrlType.Option.Disabled": "Désactivé",
"Settings.Field.DefaultLanguageUrlType.Option.DefaultLocale": "URL par défaut des routes (généré à partir de l'URL de la locale par défaut)",
"Settings.Field.DefaultLanguageUrlType.Option.Other": "Autre",
"Settings.Field.URL.Label": "Slug",
"Settings.Field.URL.Description": "Ce champ requiert une regex de type UID",
"Settings.Field.Priority.Label": "Priorité",
"Settings.Field.Priority.Description": "La priorité des pages.",
"Settings.Field.Changefreq.Label": "Changefreq",
"Settings.Field.Changefreq.Description": "Le fréquence de mise à jour des pages.",
"Settings.Field.IncludeLastmod.Label": "Lastmod",
"Settings.Field.IncludeLastmod.Description": "Ajoute une balise <lastmod> à toutes les URLs de son type.",
"Settings.Field.Pattern.Label": "Modèle",
"Settings.Field.Pattern.DescriptionPart1": "Crée un modèle d'URL dynamique.",
"Settings.Field.Pattern.DescriptionPart2": "utilise",
"Settings.Field.Pattern.DescriptionPart3": "et",
"Settings.Field.Pattern.Error": "Ce modèle n'est pas valide.",
"Settings.Field.SelectContentType.Label": "Type de contenu",
"Settings.Field.SelectContentType.Description": "Sélectionne un type de contenu.",
"Settings.Field.SelectLanguage.Label": "Langue",
"Settings.Field.SelectLanguage.Description": "Sélectionne une langue.",
"Settings.Field.SelectLanguage.SameForAll": "Identique pour toutes les langues",

"Modal.HeaderTitle": "Entrées du Sitemap",
"Modal.Tabs.Basic.Title": "Paramètres",
"Modal.Tabs.Advanced.Title": "Paramètres avancés",

"HostnameOverrides.Label": "Domaines personnalisés",
"HostnameOverrides.Description": "Appliquer un domaine pour les URLs avec la locale {langcode}",

"Info.NoHostname.Title": "Ajouter votre domaine",
"Info.NoHostname.Description": "Avant de générer votre sitemap, vous dever renseigner le domaine de votre site.",
"Info.NoSitemap.Title": "Pas de sitemap XML présent",
"Info.NoSitemap.Description": "Générer votre premier sitemap XML avec le bouton ci-dessous.",
"Info.SitemapIsPresent.Title": "Sitemap XML présent",
"Info.SitemapIsPresent.LastUpdatedAt": "Dernière modification le:",
"Info.SitemapIsPresent.AmountOfURLs": "Nombre d'URLs:",
"Info.SitemapIsPresent.AmountOfSitemaps": "Nombre de sitemaps:",

"EditView.ExcludeFromSitemap": "Exclure du Sitemap",

"Empty.URLBundles.Description": "Aucune collection d'URLs configurée.",
"Empty.URLBundles.Button": "Ajouter la première collection d'URL",

"Empty.CustomURLs.Description": "Aucune URL personnalisée configurée.",
"Empty.CustomURLs.Button": "Ajouter la première URL",

"notification.success.submit": "Les paramètres ont été mis à jour",
"notification.success.generate": "Le sitemap a été généré",

"plugin.name": "Sitemap",
"plugin.name.extended": "Plugin Sitemap"
}
10 changes: 10 additions & 0 deletions playground/src/api/test/content-types/test/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@
"localized": true
}
}
},
"slug": {
"pluginOptions": {
"i18n": {
"localized": true
}
},
"type": "uid",
"targetField": "title",
"required": true
}
}
}
29 changes: 29 additions & 0 deletions server/services/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,29 @@ const { isEmpty } = require('lodash');

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

/**
* Add link x-default url to url bundles from strapi i18n plugin default locale.
*
* @param {object} config - The config object.
* @param {object} links - The language links.
*
* @returns {object | undefined} The default language link.
*/
const getDefaultLanguageLink = async (config, links) => {
if (config.defaultLanguageUrlType === 'default-locale') {
const { getDefaultLocale } = strapi.plugin('i18n').service('locales');
const defaultLocale = await getDefaultLocale();

// find url with default locale in generated bundle
const url = links.find((link) => link.lang === defaultLocale)?.url;
if (url) return { lang: 'x-default', url };
}

if (config.defaultLanguageUrlType === 'other' && config.defaultLanguageUrl) {
return { lang: 'x-default', url: config.defaultLanguageUrl };
}
};

/**
* Get a formatted array of different language URLs of a single page.
*
Expand Down Expand Up @@ -52,6 +75,12 @@ const getLanguageLinks = async (config, page, contentType, defaultURL) => {
});
}));

// add optional x-default link url
if (config.defaultLanguageUrlType) {
const defaultLink = await getDefaultLanguageLink(config, links);
if (defaultLink) links.push(defaultLink);
}

return links;
};

Expand Down
2 changes: 2 additions & 0 deletions server/services/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const createDefaultConfig = async () => {
hostname: '',
includeHomepage: true,
excludeDrafts: true,
defaultLanguageUrlType: '',
defaultLanguageUrl: '',
hostname_overrides: {},
contentTypes: Map({}),
customEntries: Map({}),
Expand Down

0 comments on commit 116aa77

Please sign in to comment.