1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
|
import Script from 'next/script';
import type { FC, ReactElement, ReactNode } from 'react';
import { useIntl } from 'react-intl';
import type { Person, SearchAction, WebSite, WithContext } from 'schema-dts';
import type { NextPageWithLayoutOptions } from '../../../types';
import { CONFIG } from '../../../utils/config';
import { ROUTES } from '../../../utils/constants';
import { ButtonLink, Main } from '../../atoms';
import styles from './layout.module.scss';
import { SiteFooter } from './site-footer';
import { SiteHeader, type SiteHeaderProps } from './site-header';
export type QueryAction = SearchAction & {
'query-input': string;
};
export type LayoutProps = Pick<SiteHeaderProps, 'isHome'> & {
/**
* The layout main content.
*/
children: ReactNode;
};
/**
* Layout component
*
* Render the base layout used by all pages.
*/
export const Layout: FC<LayoutProps> = ({ children, isHome }) => {
const { baseline, copyright, locales, name, url } = CONFIG;
const intl = useIntl();
const messages = {
noScript: intl.formatMessage({
defaultMessage:
'Warning: If you want to benefit from all features (search for example), please activate Javascript.',
description: 'Layout: noscript message',
id: '7jVUT6',
}),
skipToContent: intl.formatMessage({
defaultMessage: 'Skip to content',
description: 'Layout: Skip to content link',
id: 'K4rYdT',
}),
};
const searchActionSchema: QueryAction = {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: `${url}${ROUTES.SEARCH}?s={search_term_string}`,
},
query: 'required',
'query-input': 'required name=search_term_string',
};
const brandingSchema: Person = {
'@type': 'Person',
name,
url,
jobTitle: baseline,
image: '/armand-philippot.jpg',
subjectOf: { '@id': `${url}` },
};
const schemaJsonLd: WithContext<WebSite> = {
'@context': 'https://schema.org',
'@id': `${url}`,
'@type': 'WebSite',
name,
description: baseline,
url,
author: brandingSchema,
copyrightYear: Number(copyright.startYear),
creator: brandingSchema,
editor: brandingSchema,
inLanguage: locales.defaultLocale,
potentialAction: searchActionSchema,
};
const topId = 'top';
const mainId = 'main';
return (
<>
<Script
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
// eslint-disable-next-line react/jsx-no-literals -- Id allowed
id="schema-layout"
type="application/ld+json"
/>
<span id={topId} />
<noscript>
<div className={styles['noscript-spacing']} />
</noscript>
<ButtonLink
// eslint-disable-next-line react/jsx-no-literals
className="screen-reader-text"
// eslint-disable-next-line react/jsx-no-literals
to={`#${mainId}`}
>
{messages.skipToContent}
</ButtonLink>
<SiteHeader className={styles.header} isHome={isHome} />
<Main className={styles.main} id={mainId}>
{children}
</Main>
<SiteFooter topId={topId} />
<noscript>
<div className={styles.noscript}>{messages.noScript}</div>
</noscript>
</>
);
};
/**
* Get the global layout.
*
* @param {ReactElement} page - A page.
* @param {NextPageWithLayoutOptions} props - An object with layout options.
* @returns A page wrapped with the global layout.
*/
export const getLayout = (
page: ReactElement,
props?: NextPageWithLayoutOptions
) => <Layout {...props}>{page}</Layout>;
|