diff --git a/packages/volto/news/6602.feature b/packages/volto/news/6602.feature new file mode 100644 index 0000000000..c0d9292115 --- /dev/null +++ b/packages/volto/news/6602.feature @@ -0,0 +1 @@ +Provide language alternate links @erral diff --git a/packages/volto/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.jsx b/packages/volto/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.jsx new file mode 100644 index 0000000000..9af027a5cf --- /dev/null +++ b/packages/volto/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.jsx @@ -0,0 +1,23 @@ +import config from '@plone/volto/registry'; +import Helmet from '@plone/volto/helpers/Helmet/Helmet'; + +const AlternateHrefLangs = (props) => { + const { content } = props; + return ( + + {config.settings.isMultilingual && + content['@components']?.translations?.items.map((item, key) => { + return ( + + ); + })} + + ); +}; + +export { AlternateHrefLangs }; diff --git a/packages/volto/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.jsx b/packages/volto/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.jsx new file mode 100644 index 0000000000..7ebb9f0aa0 --- /dev/null +++ b/packages/volto/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.jsx @@ -0,0 +1,135 @@ +import React from 'react'; +import Helmet from '@plone/volto/helpers/Helmet/Helmet'; + +import renderer from 'react-test-renderer'; +import configureStore from 'redux-mock-store'; +import { Provider } from 'react-intl-redux'; +import config from '@plone/volto/registry'; + +import { AlternateHrefLangs } from './AlternateHrefLangs'; + +const mockStore = configureStore(); + +describe('AlternateHrefLangs', () => { + beforeEach(() => {}); + it('non multilingual site, renders nothing', () => { + config.settings.isMultilingual = false; + const content = { + '@id': '/', + '@components': {}, + }; + const store = mockStore({ + intl: { + locale: 'en', + messages: {}, + }, + }); + // We need to force the component rendering + // to fill the Helmet + renderer.create( + + + , + ); + + const helmetLinks = Helmet.peek().linkTags; + expect(helmetLinks.length).toBe(0); + }); + it('multilingual site, with some translations', () => { + config.settings.isMultilingual = true; + config.settings.supportedLanguages = ['en', 'es', 'eu']; + + const content = { + '@components': { + translations: { + items: [ + { '@id': '/en', language: 'en' }, + { '@id': '/es', language: 'es' }, + ], + }, + }, + }; + + const store = mockStore({ + intl: { + locale: 'en', + messages: {}, + }, + }); + + // We need to force the component rendering + // to fill the Helmet + renderer.create( + + <> + + + , + ); + const helmetLinks = Helmet.peek().linkTags; + + expect(helmetLinks.length).toBe(2); + + expect(helmetLinks).toContainEqual({ + rel: 'alternate', + href: '/es', + hrefLang: 'es', + }); + expect(helmetLinks).toContainEqual({ + rel: 'alternate', + href: '/en', + hrefLang: 'en', + }); + }); + it('multilingual site, with all available translations', () => { + config.settings.isMultilingual = true; + config.settings.supportedLanguages = ['en', 'es', 'eu']; + const store = mockStore({ + intl: { + locale: 'en', + messages: {}, + }, + }); + + const content = { + '@components': { + translations: { + items: [ + { '@id': '/en', language: 'en' }, + { '@id': '/eu', language: 'eu' }, + { '@id': '/es', language: 'es' }, + ], + }, + }, + }; + + // We need to force the component rendering + // to fill the Helmet + renderer.create( + + + , + ); + + const helmetLinks = Helmet.peek().linkTags; + + // We expect having 3 links + expect(helmetLinks.length).toBe(3); + + expect(helmetLinks).toContainEqual({ + rel: 'alternate', + href: '/eu', + hrefLang: 'eu', + }); + expect(helmetLinks).toContainEqual({ + rel: 'alternate', + href: '/es', + hrefLang: 'es', + }); + expect(helmetLinks).toContainEqual({ + rel: 'alternate', + href: '/en', + hrefLang: 'en', + }); + }); +}); diff --git a/packages/volto/src/components/theme/View/View.jsx b/packages/volto/src/components/theme/View/View.jsx index 902204c869..da8e9a7d82 100644 --- a/packages/volto/src/components/theme/View/View.jsx +++ b/packages/volto/src/components/theme/View/View.jsx @@ -21,6 +21,7 @@ import BodyClass from '@plone/volto/helpers/BodyClass/BodyClass'; import { getBaseUrl, flattenToAppURL } from '@plone/volto/helpers/Url/Url'; import { getLayoutFieldname } from '@plone/volto/helpers/Content/Content'; import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils'; +import { AlternateHrefLangs } from '@plone/volto/components/theme/AlternateHrefLangs/AlternateHrefLangs'; import config from '@plone/volto/registry'; import SlotRenderer from '../SlotRenderer/SlotRenderer'; @@ -234,6 +235,7 @@ class View extends Component { return (
+ {/* Body class if displayName in component is set */}