summaryrefslogtreecommitdiffstats
path: root/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'src/pages')
-rw-r--r--src/pages/404.tsx165
-rw-r--r--src/pages/_app.tsx10
-rw-r--r--src/pages/article/[slug].tsx424
-rw-r--r--src/pages/blog/index.tsx366
-rw-r--r--src/pages/blog/page/[id].tsx205
-rw-r--r--src/pages/blog/page/[number].tsx237
-rw-r--r--src/pages/contact.tsx231
-rw-r--r--src/pages/cv.tsx276
-rw-r--r--src/pages/index.tsx327
-rw-r--r--src/pages/mentions-legales.tsx185
-rw-r--r--src/pages/projet/[slug].tsx186
-rw-r--r--src/pages/projets.tsx128
-rw-r--r--src/pages/projets/[slug].tsx241
-rw-r--r--src/pages/projets/index.tsx123
-rw-r--r--src/pages/recherche/index.tsx348
-rw-r--r--src/pages/sujet/[slug].tsx360
-rw-r--r--src/pages/thematique/[slug].tsx331
17 files changed, 2173 insertions, 1970 deletions
diff --git a/src/pages/404.tsx b/src/pages/404.tsx
index 24c6951..c3a5cac 100644
--- a/src/pages/404.tsx
+++ b/src/pages/404.tsx
@@ -1,30 +1,89 @@
-import { getLayout } from '@components/Layouts/Layout';
-import PostHeader from '@components/PostHeader/PostHeader';
-import styles from '@styles/pages/Page.module.scss';
-import { NextPageWithLayout } from '@ts/types/app';
-import { settings } from '@utils/config';
-import { getIntlInstance, loadTranslation } from '@utils/helpers/i18n';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
+import Link from '@components/atoms/links/link';
+import SearchForm from '@components/organisms/forms/search-form';
+import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout from '@components/templates/page/page-layout';
+import {
+ getThematicsPreview,
+ getTotalThematics,
+} from '@services/graphql/thematics';
+import { getTopicsPreview, getTotalTopics } from '@services/graphql/topics';
+import { type NextPageWithLayout } from '@ts/types/app';
+import {
+ type RawThematicPreview,
+ type RawTopicPreview,
+} from '@ts/types/raw-data';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import {
+ getLinksListItems,
+ getPageLinkFromRawData,
+} from '@utils/helpers/pages';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useSettings from '@utils/hooks/use-settings';
+import { GetStaticProps } from 'next';
import Head from 'next/head';
-import Link from 'next/link';
-import { FormattedMessage, useIntl } from 'react-intl';
+import { ReactNode } from 'react';
+import { useIntl } from 'react-intl';
-const Error404: NextPageWithLayout = () => {
- const intl = useIntl();
+type Error404PageProps = {
+ thematicsList: RawThematicPreview[];
+ topicsList: RawTopicPreview[];
+ translation: Messages;
+};
+/**
+ * Error 404 page.
+ */
+const Error404Page: NextPageWithLayout<Error404PageProps> = ({
+ thematicsList,
+ topicsList,
+}) => {
+ const intl = useIntl();
+ const { website } = useSettings();
+ const title = intl.formatMessage({
+ defaultMessage: 'Page not found',
+ description: 'Error404Page: page title',
+ id: 'KnWeKh',
+ });
+ const body = intl.formatMessage(
+ {
+ defaultMessage:
+ 'Sorry, it seems that the page your are looking for does not exist. If you think this path should work, feel free to <link>contact me</link> with the necessary information so that I can fix the problem.',
+ id: '9sGNKq',
+ description: 'Error404Page: page body',
+ },
+ {
+ link: (chunks: ReactNode) => <Link href="/contact">{chunks}</Link>,
+ }
+ );
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: `/404`,
+ });
const pageTitle = intl.formatMessage(
{
defaultMessage: 'Error 404: Page not found - {websiteName}',
description: '404Page: SEO - Page title',
id: '310o3F',
},
- { websiteName: settings.name }
+ { websiteName: website.name }
);
const pageDescription = intl.formatMessage({
defaultMessage: 'Page not found.',
description: '404Page: SEO - Meta description',
id: '48Ww//',
});
+ const thematicsListTitle = intl.formatMessage({
+ defaultMessage: 'Thematics',
+ description: 'Error404Page: thematics list widget title',
+ id: 'HohQPh',
+ });
+
+ const topicsListTitle = intl.formatMessage({
+ defaultMessage: 'Topics',
+ description: 'Error404Page: topics list widget title',
+ id: 'GVpTIl',
+ });
return (
<>
@@ -32,54 +91,64 @@ const Error404: NextPageWithLayout = () => {
<title>{pageTitle}</title>
<meta name="description" content={pageDescription} />
</Head>
- <div className={`${styles.article} ${styles['article--no-comments']}`}>
- <PostHeader
- title={intl.formatMessage({
- defaultMessage: 'Page not found',
- description: '404Page: page title',
- id: 'OccTWi',
+ <PageLayout
+ title={title}
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ widgets={[
+ <LinksListWidget
+ key="thematics-list"
+ items={getLinksListItems(
+ thematicsList.map((thematic) =>
+ getPageLinkFromRawData(thematic, 'thematic')
+ )
+ )}
+ title={thematicsListTitle}
+ level={2}
+ />,
+ <LinksListWidget
+ key="topics-list"
+ items={getLinksListItems(
+ topicsList.map((topic) => getPageLinkFromRawData(topic, 'topic'))
+ )}
+ title={topicsListTitle}
+ level={2}
+ />,
+ ]}
+ >
+ {body}
+ <p>
+ {intl.formatMessage({
+ defaultMessage: 'You can also try a search:',
+ description: 'Error404Page: try a search message',
+ id: 'XKy7rx',
})}
- />
- <div className={styles.body}>
- <FormattedMessage
- defaultMessage="Sorry, it seems that the page your are looking for does not exist. If you think this path should work, feel free to <link>contact me</link> with the necessary information so that I can fix the problem."
- description="404Page: page body"
- id="ZWh78Y"
- values={{
- link: (chunks: string) => (
- <Link href="/contact/">
- <a>{chunks}</a>
- </Link>
- ),
- }}
- />
- </div>
- </div>
+ </p>
+ <SearchForm hideLabel={true} searchPage="/recherche/" />
+ </PageLayout>
</>
);
};
-Error404.getLayout = getLayout;
+Error404Page.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const intl = await getIntlInstance();
- const breadcrumbTitle = intl.formatMessage({
- defaultMessage: 'Error 404',
- description: '404Page: breadcrumb item',
- id: 'ywkCsK',
- });
- const { locale } = context;
+export const getStaticProps: GetStaticProps<Error404PageProps> = async ({
+ locale,
+}) => {
+ const totalThematics = await getTotalThematics();
+ const thematics = await getThematicsPreview({ first: totalThematics });
+ const totalTopics = await getTotalTopics();
+ const topics = await getTopicsPreview({ first: totalTopics });
const translation = await loadTranslation(locale);
return {
props: {
- breadcrumbTitle,
- locale,
+ thematicsList: thematics.edges.map((edge) => edge.node),
+ topicsList: topics.edges.map((edge) => edge.node),
translation,
},
};
};
-export default Error404;
+export default Error404Page;
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 84c2469..5bc9f85 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -1,4 +1,4 @@
-import { AppPropsWithLayout } from '@ts/types/app';
+import { type AppPropsWithLayout } from '@ts/types/app';
import { settings } from '@utils/config';
import { AckeeProvider } from '@utils/providers/ackee';
import { PrismThemeProvider } from '@utils/providers/prism-theme';
@@ -7,11 +7,11 @@ import { useRouter } from 'next/router';
import { IntlProvider } from 'react-intl';
import '../styles/globals.scss';
-const MyApp = ({ Component, pageProps }: AppPropsWithLayout) => {
+const App = ({ Component, pageProps }: AppPropsWithLayout) => {
const { locale, defaultLocale } = useRouter();
const appLocale: string = locale || settings.locales.defaultLocale;
-
const getLayout = Component.getLayout ?? ((page) => page);
+
return (
<AckeeProvider domain={settings.ackee.url} siteId={settings.ackee.siteId}>
<IntlProvider
@@ -25,7 +25,7 @@ const MyApp = ({ Component, pageProps }: AppPropsWithLayout) => {
enableSystem={true}
>
<PrismThemeProvider>
- {getLayout(<Component {...pageProps} />)}
+ {getLayout(<Component {...pageProps} />, {})}
</PrismThemeProvider>
</ThemeProvider>
</IntlProvider>
@@ -33,4 +33,4 @@ const MyApp = ({ Component, pageProps }: AppPropsWithLayout) => {
);
};
-export default MyApp;
+export default App;
diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx
index 27a6f7b..ea679ab 100644
--- a/src/pages/article/[slug].tsx
+++ b/src/pages/article/[slug].tsx
@@ -1,286 +1,262 @@
-import CommentForm from '@components/CommentForm/CommentForm';
-import CommentsList from '@components/CommentsList/CommentsList';
-import { getLayout } from '@components/Layouts/Layout';
-import PostFooter from '@components/PostFooter/PostFooter';
-import PostHeader from '@components/PostHeader/PostHeader';
-import Sidebar from '@components/Sidebar/Sidebar';
-import Spinner from '@components/Spinner/Spinner';
-import { Sharing, ToC } from '@components/Widgets';
+import ButtonLink from '@components/atoms/buttons/button-link';
+import Link from '@components/atoms/links/link';
+import Spinner from '@components/atoms/loaders/spinner';
+import ResponsiveImage from '@components/molecules/images/responsive-image';
+import Sharing from '@components/organisms/widgets/sharing';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout, {
+ type PageLayoutProps,
+} from '@components/templates/page/page-layout';
import {
- getAllPostsSlug,
- getCommentsByPostId,
- getPostBySlug,
-} from '@services/graphql/queries';
-import styles from '@styles/pages/Page.module.scss';
-import { NextPageWithLayout } from '@ts/types/app';
-import { ArticleMeta, ArticleProps } from '@ts/types/articles';
-import { PrismDefaultPlugins, PrismPlugins } from '@ts/types/prism';
-import { settings } from '@utils/config';
-import { getFormattedPaths } from '@utils/helpers/format';
-import { loadTranslation } from '@utils/helpers/i18n';
-import { addPrismClasses } from '@utils/helpers/prism';
-import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
+ getAllArticlesSlugs,
+ getArticleBySlug,
+} from '@services/graphql/articles';
+import { getPostComments } from '@services/graphql/comments';
+import styles from '@styles/pages/article.module.scss';
+import {
+ type Article,
+ type Comment,
+ type NextPageWithLayout,
+} from '@ts/types/app';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import {
+ getBlogSchema,
+ getSchemaJson,
+ getSinglePageSchema,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import usePrism, { type OptionalPrismPlugin } from '@utils/hooks/use-prism';
+import useReadingTime from '@utils/hooks/use-reading-time';
+import useSettings from '@utils/hooks/use-settings';
+import { GetStaticPaths, GetStaticProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Script from 'next/script';
-import Prism from 'prismjs';
import { ParsedUrlQuery } from 'querystring';
-import { useCallback, useEffect, useMemo } from 'react';
+import { HTMLAttributes } from 'react';
import { useIntl } from 'react-intl';
-import { Blog, BlogPosting, Graph, WebPage } from 'schema-dts';
+import useSWR from 'swr';
+
+type ArticlePageProps = {
+ comments: Comment[];
+ post: Article;
+ slug: string;
+ translation: Messages;
+};
-const SingleArticle: NextPageWithLayout<ArticleProps> = ({
+/**
+ * Article page.
+ */
+const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({
comments,
post,
+ slug,
}) => {
+ const { isFallback } = useRouter();
const intl = useIntl();
- const router = useRouter();
+ const { data: article } = useSWR(() => slug, getArticleBySlug, {
+ fallbackData: post,
+ });
+ const { data: commentsData } = useSWR(() => id, getPostComments, {
+ fallbackData: comments,
+ });
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title: article?.title || '',
+ url: `/article/${slug}`,
+ });
+ const readingTime = useReadingTime(article?.meta.wordsCount || 0, true);
+ const { website } = useSettings();
+ const prismPlugins: OptionalPrismPlugin[] = ['command-line', 'line-numbers'];
+ const { attributes, className } = usePrism({ plugins: prismPlugins });
- const loadPrismPlugins = useCallback(
- async (prismPlugins: (PrismDefaultPlugins | PrismPlugins)[]) => {
- for (const plugin of prismPlugins) {
- try {
- if (plugin === 'color-scheme') {
- await import(`@utils/plugins/prism-${plugin}`);
- } else {
- await import(`prismjs/plugins/${plugin}/prism-${plugin}.min.js`);
+ if (isFallback) return <Spinner />;
- if (plugin === 'autoloader')
- Prism.plugins.autoloader.languages_path = '/prism/';
- }
- } catch (error) {
- console.error('Article: an error occurred with Prism.');
- console.error(error);
- }
- }
- },
- []
- );
+ const { content, id, intro, meta, title } = article!;
+ const { author, commentsCount, cover, dates, seo, thematics, topics } = meta;
- const plugins: (PrismDefaultPlugins | PrismPlugins)[] = useMemo(
- () => [
- 'autoloader',
- 'toolbar',
- 'show-language',
- 'copy-to-clipboard',
- 'color-scheme',
- 'command-line',
- 'line-numbers',
- 'match-braces',
- 'normalize-whitespace',
- ],
- []
- );
+ const headerMeta: PageLayoutProps['headerMeta'] = {
+ author: author?.name,
+ publication: { date: dates.publication },
+ update:
+ dates.update && dates.publication !== dates.update
+ ? { date: dates.update }
+ : undefined,
+ readingTime,
+ thematics:
+ thematics &&
+ thematics.map((thematic) => (
+ <Link key={thematic.id} href={thematic.url}>
+ {thematic.name}
+ </Link>
+ )),
+ };
- useEffect(() => {
- loadPrismPlugins(plugins).then(() => {
- addPrismClasses();
- Prism.highlightAll();
- });
- }, [plugins, loadPrismPlugins]);
+ const footerMetaLabel = intl.formatMessage({
+ defaultMessage: 'Read more articles about:',
+ description: 'ArticlePage: footer topics list label',
+ id: '50xc4o',
+ });
- if (router.isFallback) return <Spinner />;
+ const footerMeta: PageLayoutProps['footerMeta'] = {
+ custom: topics && {
+ label: footerMetaLabel,
+ value: topics.map((topic) => {
+ return (
+ <ButtonLink key={topic.id} target={topic.url} className={styles.btn}>
+ {topic.logo && <ResponsiveImage {...topic.logo} />} {topic.name}
+ </ButtonLink>
+ );
+ }),
+ },
+ };
- const {
- author,
- commentCount,
+ const webpageSchema = getWebPageSchema({
+ description: intro,
+ locale: website.locales.default,
+ slug,
+ title,
+ updateDate: dates.update,
+ });
+ const blogSchema = getBlogSchema({
+ isSinglePage: true,
+ locale: website.locales.default,
+ slug,
+ });
+ const blogPostSchema = getSinglePageSchema({
+ commentsCount,
content,
- databaseId,
+ cover: cover?.src,
dates,
- featuredImage,
- info,
- intro,
- seo,
- topics,
- thematics,
+ description: intro,
+ id: 'article',
+ kind: 'post',
+ locale: website.locales.default,
+ slug,
title,
- } = post;
-
- const meta: ArticleMeta = {
- author,
- commentCount: commentCount || undefined,
- dates,
- readingTime: info.readingTime,
- thematics,
- wordsCount: info.wordsCount,
- };
-
- const articleUrl = `${settings.url}${router.asPath}`;
+ });
+ const schemaJsonLd = getSchemaJson([
+ webpageSchema,
+ blogSchema,
+ blogPostSchema,
+ ]);
- const webpageSchema: WebPage = {
- '@id': `${articleUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- lastReviewed: dates.update,
- name: seo.title,
- description: seo.metaDesc,
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${articleUrl}`,
- isPartOf: {
- '@id': `${settings.url}`,
- },
- };
+ const lineNumbersClassName = className
+ .replace('command-line', '')
+ .replace(/\s\s+/g, ' ');
+ const commandLineClassName = className
+ .replace('line-numbers', '')
+ .replace(/\s\s+/g, ' ');
- const blogSchema: Blog = {
- '@id': `${settings.url}/#blog`,
- '@type': 'Blog',
- blogPost: { '@id': `${settings.url}/#article` },
- isPartOf: {
- '@id': `${articleUrl}`,
- },
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- };
+ /**
+ * Replace a string with Prism classnames and attributes.
+ *
+ * @param {string} str - The found string.
+ * @returns {string} The classes and attributes.
+ */
+ const prismClassNameReplacer = (str: string): string => {
+ const wpBlockClassName = 'wp-block-code';
+ const languageArray = str.match(/language-[^\s|"]+/);
+ const languageClassName = languageArray ? `${languageArray[0]}` : '';
- const publicationDate = new Date(dates.publication);
- const updateDate = new Date(dates.update);
+ if (
+ str.includes('command-line') ||
+ (!str.includes('command-line') && str.includes('language-bash'))
+ ) {
+ return `class="${wpBlockClassName} ${commandLineClassName}${languageClassName}" tabindex="0" data-filter-output="#output#`;
+ }
- const blogPostSchema: BlogPosting = {
- '@id': `${settings.url}/#article`,
- '@type': 'BlogPosting',
- name: title,
- description: intro,
- articleBody: content,
- author: { '@id': `${settings.url}/#branding` },
- commentCount: commentCount || undefined,
- copyrightYear: publicationDate.getFullYear(),
- creator: { '@id': `${settings.url}/#branding` },
- dateCreated: publicationDate.toISOString(),
- dateModified: updateDate.toISOString(),
- datePublished: publicationDate.toISOString(),
- discussionUrl: `${articleUrl}/#comments`,
- editor: { '@id': `${settings.url}/#branding` },
- headline: title,
- image: featuredImage?.sourceUrl,
- inLanguage: settings.locales.defaultLocale,
- isPartOf: {
- '@id': `${settings.url}/blog`,
- },
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: { '@id': `${articleUrl}` },
- thumbnailUrl: featuredImage?.sourceUrl,
+ return `class="${wpBlockClassName} ${lineNumbersClassName}${languageClassName}" tabindex="0`;
};
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, blogSchema, blogPostSchema],
- };
+ const contentWithPrismClasses = content.replaceAll(
+ /class="wp-block-code[^"]+/gm,
+ prismClassNameReplacer
+ );
- const copyText = intl.formatMessage({
- defaultMessage: 'Copy',
- description: 'Prism: copy button text (no clicked)',
- id: '/ly3AC',
- });
- const copiedText = intl.formatMessage({
- defaultMessage: 'Copied!',
- description: 'Prism: copy button text (clicked)',
- id: 'OV9r1K',
- });
- const errorText = intl.formatMessage({
- defaultMessage: 'Use Ctrl+c to copy',
- description: 'Prism: error text',
- id: 'z9qkcQ',
- });
- const darkTheme = intl.formatMessage({
- defaultMessage: 'Dark Theme 🌙',
- description: 'Prism: toggle dark theme button text',
- id: 'nFMdWI',
- });
- const lightTheme = intl.formatMessage({
- defaultMessage: 'Light Theme 🌞',
- description: 'Prism: toggle light theme button text',
- id: 'Ua2g2p',
- });
+ const pageUrl = `${website.url}${slug}`;
return (
<>
<Head>
<title>{seo.title}</title>
- <meta name="description" content={seo.metaDesc} />
- <meta property="og:url" content={`${articleUrl}`} />
+ <meta name="description" content={seo.description} />
+ <meta property="og:url" content={`${pageUrl}`} />
<meta property="og:type" content="article" />
<meta property="og:title" content={title} />
<meta property="og:description" content={intro} />
- <meta property="og:image" content={featuredImage?.sourceUrl} />
- <meta property="og:image:alt" content={featuredImage?.altText} />
</Head>
<Script
- id="schema-article"
+ id="schema-project"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
/>
- <article
- id="article"
- className={styles.article}
- data-prismjs-copy={copyText}
- data-prismjs-copy-success={copiedText}
- data-prismjs-copy-error={errorText}
- data-prismjs-color-scheme-dark={darkTheme}
- data-prismjs-color-scheme-light={lightTheme}
+ <PageLayout
+ allowComments={true}
+ bodyAttributes={{
+ ...(attributes as HTMLAttributes<HTMLDivElement>),
+ }}
+ bodyClassName={styles.body}
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ comments={commentsData}
+ footerMeta={footerMeta}
+ headerMeta={headerMeta}
+ id={id as number}
+ intro={intro}
+ title={title}
+ withToC={true}
+ widgets={[
+ <Sharing
+ key="sharing-widget"
+ className={styles.widget}
+ data={{ excerpt: intro, title, url: pageUrl }}
+ media={[
+ 'diaspora',
+ 'email',
+ 'facebook',
+ 'journal-du-hacker',
+ 'linkedin',
+ 'twitter',
+ ]}
+ />,
+ ]}
>
- <PostHeader intro={intro} meta={meta} title={title} />
- <Sidebar
- position="left"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Table of Contents',
- description: 'ArticlePage: ToC sidebar aria-label',
- id: '9nhYRA',
- })}
- >
- <ToC />
- </Sidebar>
- <div
- className={styles.body}
- dangerouslySetInnerHTML={{ __html: content }}
- ></div>
- <PostFooter topics={topics} />
- <Sidebar
- position="right"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Sidebar',
- description: 'ArticlePage: right sidebar aria-label',
- id: 'JeYOeA',
- })}
- >
- <Sharing title={title} excerpt={intro} />
- </Sidebar>
- <section id="comments" className={styles.comments}>
- <CommentsList articleId={databaseId} comments={comments} />
- <CommentForm articleId={databaseId} />
- </section>
- </article>
+ {contentWithPrismClasses}
+ </PageLayout>
</>
);
};
-SingleArticle.getLayout = getLayout;
+ArticlePage.getLayout = (page) => getLayout(page, { useGrid: true });
interface PostParams extends ParsedUrlQuery {
slug: string;
}
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const { locale } = context;
+export const getStaticProps: GetStaticProps<ArticlePageProps> = async ({
+ locale,
+ params,
+}) => {
+ const post = await getArticleBySlug(params!.slug as PostParams['slug']);
+ const comments = await getPostComments(post.id as number);
const translation = await loadTranslation(locale);
- const { slug } = context.params as PostParams;
- const post = await getPostBySlug(slug);
- const comments = await getCommentsByPostId(post.databaseId);
- const breadcrumbTitle = post.title;
return {
props: {
- breadcrumbTitle,
- comments,
- post,
+ comments: JSON.parse(JSON.stringify(comments)),
+ post: JSON.parse(JSON.stringify(post)),
+ slug: post.slug,
translation,
},
};
};
export const getStaticPaths: GetStaticPaths = async () => {
- const allSlugs = await getAllPostsSlug();
- const paths = getFormattedPaths(allSlugs);
+ const slugs = await getAllArticlesSlugs();
+ const paths = slugs.map((slug) => {
+ return { params: { slug } };
+ });
return {
paths,
@@ -288,4 +264,4 @@ export const getStaticPaths: GetStaticPaths = async () => {
};
};
-export default SingleArticle;
+export default ArticlePage;
diff --git a/src/pages/blog/index.tsx b/src/pages/blog/index.tsx
index b5ced07..3f7eefd 100644
--- a/src/pages/blog/index.tsx
+++ b/src/pages/blog/index.tsx
@@ -1,116 +1,79 @@
-import { Button } from '@components/Buttons';
-import { getLayout } from '@components/Layouts/Layout';
-import Pagination from '@components/Pagination/Pagination';
-import PaginationCursor from '@components/PaginationCursor/PaginationCursor';
-import PostHeader from '@components/PostHeader/PostHeader';
-import PostsList from '@components/PostsList/PostsList';
-import Sidebar from '@components/Sidebar/Sidebar';
-import Spinner from '@components/Spinner/Spinner';
-import { ThematicsList, TopicsList } from '@components/Widgets';
+import Notice from '@components/atoms/layout/notice';
+import PostsList from '@components/organisms/layout/posts-list';
+import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout from '@components/templates/page/page-layout';
+import { type EdgesResponse } from '@services/graphql/api';
+import { getArticles, getTotalArticles } from '@services/graphql/articles';
import {
- getAllThematics,
- getAllTopics,
- getPostsTotal,
- getPublishedPosts,
-} from '@services/graphql/queries';
-import styles from '@styles/pages/Page.module.scss';
-import { NextPageWithLayout } from '@ts/types/app';
-import { BlogPageProps, PostsList as PostsListData } from '@ts/types/blog';
+ getThematicsPreview,
+ getTotalThematics,
+} from '@services/graphql/thematics';
+import { getTopicsPreview, getTotalTopics } from '@services/graphql/topics';
+import { type NextPageWithLayout } from '@ts/types/app';
+import {
+ type RawArticle,
+ type RawThematicPreview,
+ type RawTopicPreview,
+} from '@ts/types/raw-data';
import { settings } from '@utils/config';
-import { getIntlInstance, loadTranslation } from '@utils/helpers/i18n';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import {
+ getLinksListItems,
+ getPageLinkFromRawData,
+ getPostsList,
+} from '@utils/helpers/pages';
+import {
+ getBlogSchema,
+ getSchemaJson,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import usePagination from '@utils/hooks/use-pagination';
+import useSettings from '@utils/hooks/use-settings';
+import { GetStaticProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Script from 'next/script';
-import { useEffect, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
-import { Blog as BlogSchema, Graph, WebPage } from 'schema-dts';
-import useSWRInfinite from 'swr/infinite';
-const Blog: NextPageWithLayout<BlogPageProps> = ({
- allThematics,
- allTopics,
- posts,
- totalPosts,
+type BlogPageProps = {
+ articles: EdgesResponse<RawArticle>;
+ thematicsList: RawThematicPreview[];
+ topicsList: RawTopicPreview[];
+ totalArticles: number;
+ translation: Messages;
+};
+
+/**
+ * Blog index page.
+ */
+const BlogPage: NextPageWithLayout<BlogPageProps> = ({
+ articles,
+ thematicsList,
+ topicsList,
+ totalArticles,
}) => {
const intl = useIntl();
- const lastPostRef = useRef<HTMLSpanElement>(null);
- const router = useRouter();
- const [isMounted, setIsMounted] = useState<boolean>(false);
-
- useEffect(() => {
- if (typeof window !== undefined) setIsMounted(true);
- }, []);
-
- const getKey = (pageIndex: number, previousData: PostsListData) => {
- if (previousData && !previousData.posts) return null;
-
- return pageIndex === 0
- ? { first: settings.postsPerPage }
- : {
- first: settings.postsPerPage,
- after: previousData.pageInfo.endCursor,
- };
- };
-
- const { data, error, size, setSize } = useSWRInfinite(
- getKey,
- getPublishedPosts,
- { fallbackData: [posts] }
- );
- const [totalPostsCount, setTotalPostsCount] = useState<number>(totalPosts);
-
- useEffect(() => {
- if (data) setTotalPostsCount(data[0].pageInfo.total);
- }, [data]);
-
- const [loadedPostsCount, setLoadedPostsCount] = useState<number>(
- settings.postsPerPage
- );
-
- useEffect(() => {
- if (data && data.length > 0) {
- const newCount =
- settings.postsPerPage +
- data[0].pageInfo.total -
- data[data.length - 1].pageInfo.total;
- setLoadedPostsCount(newCount);
- }
- }, [data]);
-
- const isLoadingInitialData = !data && !error;
- const isLoadingMore: boolean =
- isLoadingInitialData ||
- (size > 0 && data !== undefined && typeof data[size - 1] === 'undefined');
-
- const hasNextPage = data && data[data.length - 1].pageInfo.hasNextPage;
-
- const loadMorePosts = () => {
- if (lastPostRef.current) {
- lastPostRef.current.focus();
- }
- setSize(size + 1);
- };
-
- const getPostsList = () => {
- if (error)
- return intl.formatMessage({
- defaultMessage: 'Failed to load.',
- description: 'BlogPage: failed to load text',
- id: 'C/XGkH',
- });
- if (!data) return <Spinner />;
-
- return <PostsList ref={lastPostRef} data={data} showYears={true} />;
- };
+ const title = intl.formatMessage({
+ defaultMessage: 'Blog',
+ description: 'BlogPage: page title',
+ id: '7TbbIk',
+ });
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: '/blog',
+ });
+ const { blog, website } = useSettings();
+ const { asPath } = useRouter();
const pageTitle = intl.formatMessage(
{
defaultMessage: 'Blog: development, open source - {websiteName}',
description: 'BlogPage: SEO - Page title',
id: '+Y+tLK',
},
- { websiteName: settings.name }
+ { websiteName: website.name }
);
const pageDescription = intl.formatMessage(
{
@@ -119,44 +82,51 @@ const Blog: NextPageWithLayout<BlogPageProps> = ({
description: 'BlogPage: SEO - Meta description',
id: '18h/t0',
},
- { websiteName: settings.name }
+ { websiteName: website.name }
);
- const pageUrl = `${settings.url}${router.asPath}`;
-
- const webpageSchema: WebPage = {
- '@id': `${pageUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- name: pageTitle,
+ const webpageSchema = getWebPageSchema({
description: pageDescription,
- inLanguage: settings.locales.defaultLocale,
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${settings.url}`,
- isPartOf: {
- '@id': `${settings.url}`,
- },
- };
+ locale: website.locales.default,
+ slug: asPath,
+ title,
+ });
+ const blogSchema = getBlogSchema({
+ isSinglePage: false,
+ locale: website.locales.default,
+ slug: asPath,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema, blogSchema]);
+
+ const {
+ data,
+ error,
+ isLoadingInitialData,
+ isLoadingMore,
+ hasNextPage,
+ setSize,
+ } = usePagination<RawArticle>({
+ fallbackData: [articles],
+ fetcher: getArticles,
+ perPage: blog.postsPerPage,
+ });
- const blogSchema: BlogSchema = {
- '@id': `${settings.url}/#blog`,
- '@type': 'Blog',
- author: { '@id': `${settings.url}/#branding` },
- creator: { '@id': `${settings.url}/#branding` },
- editor: { '@id': `${settings.url}/#branding` },
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: { '@id': `${pageUrl}` },
+ /**
+ * Load more posts handler.
+ */
+ const loadMore = () => {
+ setSize((prevSize) => prevSize + 1);
};
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, blogSchema],
- };
+ const thematicsListTitle = intl.formatMessage({
+ defaultMessage: 'Thematics',
+ description: 'BlogPage: thematics list widget title',
+ id: 'HriY57',
+ });
- const title = intl.formatMessage({
- defaultMessage: 'Blog',
- description: 'BlogPage: page title',
- id: '7TbbIk',
+ const topicsListTitle = intl.formatMessage({
+ defaultMessage: 'Topics',
+ description: 'BlogPage: topics list widget title',
+ id: '2D9tB5',
});
return (
@@ -164,7 +134,7 @@ const Blog: NextPageWithLayout<BlogPageProps> = ({
<Head>
<title>{pageTitle}</title>
<meta name="description" content={pageDescription} />
- <meta property="og:url" content={`${pageUrl}`} />
+ <meta property="og:url" content={`${website.url}${asPath}`} />
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={pageDescription} />
@@ -174,96 +144,82 @@ const Blog: NextPageWithLayout<BlogPageProps> = ({
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
/>
- <article
- id="blog"
- className={`${styles.article} ${styles['article--no-comments']}`}
+ <PageLayout
+ title={title}
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ headerMeta={{ total: totalArticles }}
+ widgets={[
+ <LinksListWidget
+ key="thematics-list"
+ items={getLinksListItems(
+ thematicsList.map((thematic) =>
+ getPageLinkFromRawData(thematic, 'thematic')
+ )
+ )}
+ title={thematicsListTitle}
+ level={2}
+ />,
+ <LinksListWidget
+ key="topics-list"
+ items={getLinksListItems(
+ topicsList.map((topic) => getPageLinkFromRawData(topic, 'topic'))
+ )}
+ title={topicsListTitle}
+ level={2}
+ />,
+ ]}
>
- <PostHeader title={title} meta={{ results: totalPostsCount }} />
- <div className={styles.body}>
- {getPostsList()}
- {hasNextPage &&
- (isMounted ? (
- <>
- <PaginationCursor
- current={loadedPostsCount}
- total={totalPostsCount}
- />
- <Button
- isDisabled={isLoadingMore}
- clickHandler={loadMorePosts}
- position="center"
- spacing={true}
- >
- {intl.formatMessage({
- defaultMessage: 'Load more?',
- description: 'BlogPage: load more text',
- id: 'Kqq2cm',
- })}
- </Button>
- </>
- ) : (
- <Pagination baseUrl="/blog" total={totalPostsCount} />
- ))}
- </div>
- <Sidebar
- position="right"
- title={intl.formatMessage({
- defaultMessage: 'Filter by:',
- description: 'BlogPage: sidebar title',
- id: 'KERk7L',
- })}
- >
- <ThematicsList
- initialData={allThematics}
- title={intl.formatMessage({
- defaultMessage: 'Thematics',
- description: 'BlogPage: thematics list widget title',
- id: 'HriY57',
- })}
+ {data && (
+ <PostsList
+ baseUrl="/blog/page/"
+ byYear={true}
+ isLoading={isLoadingMore || isLoadingInitialData}
+ loadMore={loadMore}
+ posts={getPostsList(data)}
+ searchPage="/recherche/"
+ showLoadMoreBtn={hasNextPage}
+ total={totalArticles}
/>
- <TopicsList
- initialData={allTopics}
- title={intl.formatMessage({
- defaultMessage: 'Topics',
- description: 'BlogPage: topics list widget title',
- id: '2D9tB5',
+ )}
+ {error && (
+ <Notice
+ kind="error"
+ message={intl.formatMessage({
+ defaultMessage: 'Failed to load.',
+ description: 'BlogPage: failed to load text',
+ id: 'C/XGkH',
})}
/>
- </Sidebar>
- </article>
+ )}
+ </PageLayout>
</>
);
};
-Blog.getLayout = getLayout;
+BlogPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const intl = await getIntlInstance();
- const breadcrumbTitle = intl.formatMessage({
- defaultMessage: 'Blog',
- description: 'BlogPage: breadcrumb item',
- id: 'R0eDmw',
- });
- const firstPosts = await getPublishedPosts({ first: settings.postsPerPage });
- const totalPosts = await getPostsTotal();
- const allThematics = await getAllThematics();
- const allTopics = await getAllTopics();
- const { locale } = context;
+export const getStaticProps: GetStaticProps<BlogPageProps> = async ({
+ locale,
+}) => {
+ const articles = await getArticles({ first: settings.postsPerPage });
+ const totalArticles = await getTotalArticles();
+ const totalThematics = await getTotalThematics();
+ const thematics = await getThematicsPreview({ first: totalThematics });
+ const totalTopics = await getTotalTopics();
+ const topics = await getTopicsPreview({ first: totalTopics });
const translation = await loadTranslation(locale);
return {
props: {
- allThematics,
- allTopics,
- breadcrumbTitle,
- locale,
- posts: firstPosts,
- totalPosts,
+ articles: JSON.parse(JSON.stringify(articles)),
+ thematicsList: thematics.edges.map((edge) => edge.node),
+ topicsList: topics.edges.map((edge) => edge.node),
+ totalArticles,
translation,
},
};
};
-export default Blog;
+export default BlogPage;
diff --git a/src/pages/blog/page/[id].tsx b/src/pages/blog/page/[id].tsx
deleted file mode 100644
index 6c4d2f8..0000000
--- a/src/pages/blog/page/[id].tsx
+++ /dev/null
@@ -1,205 +0,0 @@
-import { getLayout } from '@components/Layouts/Layout';
-import Pagination from '@components/Pagination/Pagination';
-import PostHeader from '@components/PostHeader/PostHeader';
-import PostsList from '@components/PostsList/PostsList';
-import Sidebar from '@components/Sidebar/Sidebar';
-import { ThematicsList, TopicsList } from '@components/Widgets';
-import {
- getAllThematics,
- getAllTopics,
- getEndCursor,
- getPostsTotal,
- getPublishedPosts,
-} from '@services/graphql/queries';
-import { NextPageWithLayout } from '@ts/types/app';
-import { BlogPageProps } from '@ts/types/blog';
-import { settings } from '@utils/config';
-import { getIntlInstance, loadTranslation } from '@utils/helpers/i18n';
-import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
-import Head from 'next/head';
-import { useRouter } from 'next/router';
-import Script from 'next/script';
-import { useIntl } from 'react-intl';
-import { Blog, Graph, WebPage } from 'schema-dts';
-import styles from '@styles/pages/Page.module.scss';
-import { getFormattedPageNumbers } from '@utils/helpers/format';
-import { useEffect } from 'react';
-import Spinner from '@components/Spinner/Spinner';
-
-const BlogPage: NextPageWithLayout<BlogPageProps> = ({
- allThematics,
- allTopics,
- posts,
- totalPosts,
-}) => {
- const intl = useIntl();
- const router = useRouter();
- const pageNumber = Number(router.query.id);
-
- useEffect(() => {
- if (router.query.id === '1') router.push('/blog');
- }, [router]);
-
- if (router.isFallback) return <Spinner />;
-
- const pageTitle = intl.formatMessage(
- {
- defaultMessage: 'Blog - Page {number} - {websiteName}',
- description: 'BlogPage: SEO - Page title',
- id: '8w+jnD',
- },
- { number: pageNumber, websiteName: settings.name }
- );
- const pageDescription = intl.formatMessage(
- {
- defaultMessage:
- "Discover {websiteName}'s writings. He talks about web development, Linux and open source mostly.",
- description: 'BlogPage: SEO - Meta description',
- id: '18h/t0',
- },
- { websiteName: settings.name }
- );
- const pageUrl = `${settings.url}${router.asPath}`;
-
- const webpageSchema: WebPage = {
- '@id': `${pageUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- name: pageTitle,
- description: pageDescription,
- inLanguage: settings.locales.defaultLocale,
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${settings.url}`,
- isPartOf: {
- '@id': `${settings.url}`,
- },
- };
-
- const blogSchema: Blog = {
- '@id': `${settings.url}/#blog`,
- '@type': 'Blog',
- author: { '@id': `${settings.url}/#branding` },
- creator: { '@id': `${settings.url}/#branding` },
- editor: { '@id': `${settings.url}/#branding` },
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: { '@id': `${pageUrl}` },
- };
-
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, blogSchema],
- };
-
- const title = intl.formatMessage({
- defaultMessage: 'Blog',
- description: 'BlogPage: page title',
- id: '7TbbIk',
- });
-
- return (
- <>
- <Head>
- <title>{pageTitle}</title>
- <meta name="description" content={pageDescription} />
- <meta property="og:url" content={`${pageUrl}`} />
- <meta property="og:type" content="website" />
- <meta property="og:title" content={title} />
- <meta property="og:description" content={pageDescription} />
- </Head>
- <Script
- id="schema-blog"
- type="application/ld+json"
- dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
- />
- <article
- id="blog"
- className={`${styles.article} ${styles['article--no-comments']}`}
- >
- <PostHeader title={title} meta={{ results: totalPosts }} />
- <div className={styles.body}>
- <PostsList data={[posts]} showYears={true} />
- <Pagination baseUrl="/blog" total={totalPosts} />
- </div>
- <Sidebar
- position="right"
- title={intl.formatMessage({
- defaultMessage: 'Filter by:',
- description: 'BlogPage: sidebar title',
- id: 'KERk7L',
- })}
- >
- <ThematicsList
- initialData={allThematics}
- title={intl.formatMessage({
- defaultMessage: 'Thematics',
- description: 'BlogPage: thematics list widget title',
- id: 'HriY57',
- })}
- />
- <TopicsList
- initialData={allTopics}
- title={intl.formatMessage({
- defaultMessage: 'Topics',
- description: 'BlogPage: topics list widget title',
- id: '2D9tB5',
- })}
- />
- </Sidebar>
- </article>
- </>
- );
-};
-
-BlogPage.getLayout = getLayout;
-
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const intl = await getIntlInstance();
- const breadcrumbTitle = intl.formatMessage({
- defaultMessage: 'Blog',
- description: 'BlogPage: breadcrumb item',
- id: 'R0eDmw',
- });
- const { locale, params } = context;
- const queriedPageNumber = params ? Number(params.id) : 1;
- const queriedPostsNumber = settings.postsPerPage * queriedPageNumber;
- const endCursor =
- queriedPostsNumber === 1
- ? undefined
- : await getEndCursor({ first: queriedPostsNumber });
- const posts = await getPublishedPosts({
- first: settings.postsPerPage,
- after: endCursor,
- });
- const totalPosts = await getPostsTotal();
- const allThematics = await getAllThematics();
- const allTopics = await getAllTopics();
- const translation = await loadTranslation(locale);
-
- return {
- props: {
- allThematics,
- allTopics,
- breadcrumbTitle,
- locale,
- posts,
- totalPosts,
- translation,
- },
- };
-};
-
-export default BlogPage;
-
-export const getStaticPaths: GetStaticPaths = async () => {
- const totalPosts = await getPostsTotal();
- const totalPages = Math.floor(totalPosts / settings.postsPerPage);
- const paths = getFormattedPageNumbers(totalPages);
-
- return {
- paths,
- fallback: true,
- };
-};
diff --git a/src/pages/blog/page/[number].tsx b/src/pages/blog/page/[number].tsx
new file mode 100644
index 0000000..1e1240a
--- /dev/null
+++ b/src/pages/blog/page/[number].tsx
@@ -0,0 +1,237 @@
+import PostsList from '@components/organisms/layout/posts-list';
+import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout from '@components/templates/page/page-layout';
+import { type EdgesResponse } from '@services/graphql/api';
+import {
+ getArticles,
+ getArticlesEndCursor,
+ getTotalArticles,
+} from '@services/graphql/articles';
+import {
+ getThematicsPreview,
+ getTotalThematics,
+} from '@services/graphql/thematics';
+import { getTopicsPreview, getTotalTopics } from '@services/graphql/topics';
+import { type NextPageWithLayout } from '@ts/types/app';
+import {
+ type RawArticle,
+ type RawThematicPreview,
+ type RawTopicPreview,
+} from '@ts/types/raw-data';
+import { settings } from '@utils/config';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import {
+ getLinksListItems,
+ getPageLinkFromRawData,
+ getPostsList,
+} from '@utils/helpers/pages';
+import {
+ getBlogSchema,
+ getSchemaJson,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useRedirection from '@utils/hooks/use-redirection';
+import useSettings from '@utils/hooks/use-settings';
+import { GetStaticPaths, GetStaticProps } from 'next';
+import Head from 'next/head';
+import { useRouter } from 'next/router';
+import Script from 'next/script';
+import { ParsedUrlQuery } from 'querystring';
+import { useIntl } from 'react-intl';
+
+type BlogPageProps = {
+ articles: EdgesResponse<RawArticle>;
+ pageNumber: number;
+ thematicsList: RawThematicPreview[];
+ topicsList: RawTopicPreview[];
+ totalArticles: number;
+ translation: Messages;
+};
+
+/**
+ * Blog index page.
+ */
+const BlogPage: NextPageWithLayout<BlogPageProps> = ({
+ articles,
+ pageNumber,
+ thematicsList,
+ topicsList,
+ totalArticles,
+}) => {
+ useRedirection({
+ query: { param: 'number', value: '1' },
+ redirectTo: '/blog',
+ });
+
+ const intl = useIntl();
+ const title = intl.formatMessage({
+ defaultMessage: 'Blog',
+ description: 'BlogPage: page title',
+ id: '7TbbIk',
+ });
+ const pageNumberTitle = intl.formatMessage(
+ {
+ defaultMessage: 'Page {number}',
+ id: 'zbzlb1',
+ description: 'BlogPage: page number',
+ },
+ {
+ number: pageNumber,
+ }
+ );
+ const pageTitleWithPageNumber = `${title} - ${pageNumberTitle}`;
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title: pageNumberTitle,
+ url: `/blog/page/${pageNumber}`,
+ });
+
+ const { website } = useSettings();
+ const { asPath } = useRouter();
+ const pageTitle = `${pageTitleWithPageNumber} - ${website.name}`;
+ const pageDescription = intl.formatMessage(
+ {
+ defaultMessage:
+ "Discover {websiteName}'s writings. He talks about web development, Linux and open source mostly.",
+ description: 'BlogPage: SEO - Meta description',
+ id: '18h/t0',
+ },
+ { websiteName: website.name }
+ );
+ const webpageSchema = getWebPageSchema({
+ description: pageDescription,
+ locale: website.locales.default,
+ slug: asPath,
+ title,
+ });
+ const blogSchema = getBlogSchema({
+ isSinglePage: false,
+ locale: website.locales.default,
+ slug: asPath,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema, blogSchema]);
+
+ const thematicsListTitle = intl.formatMessage({
+ defaultMessage: 'Thematics',
+ description: 'BlogPage: thematics list widget title',
+ id: 'HriY57',
+ });
+
+ const topicsListTitle = intl.formatMessage({
+ defaultMessage: 'Topics',
+ description: 'BlogPage: topics list widget title',
+ id: '2D9tB5',
+ });
+
+ return (
+ <>
+ <Head>
+ <title>{pageTitle}</title>
+ <meta name="description" content={pageDescription} />
+ <meta property="og:url" content={`${website.url}${asPath}`} />
+ <meta property="og:type" content="website" />
+ <meta property="og:title" content={pageTitleWithPageNumber} />
+ <meta property="og:description" content={pageDescription} />
+ </Head>
+ <Script
+ id="schema-blog"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ />
+ <PageLayout
+ title={pageTitleWithPageNumber}
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ headerMeta={{ total: totalArticles }}
+ widgets={[
+ <LinksListWidget
+ key="thematics-list"
+ items={getLinksListItems(
+ thematicsList.map((thematic) =>
+ getPageLinkFromRawData(thematic, 'thematic')
+ )
+ )}
+ title={thematicsListTitle}
+ level={2}
+ />,
+ <LinksListWidget
+ key="topics-list"
+ items={getLinksListItems(
+ topicsList.map((topic) => getPageLinkFromRawData(topic, 'topic'))
+ )}
+ title={topicsListTitle}
+ level={2}
+ />,
+ ]}
+ >
+ <PostsList
+ baseUrl="/blog/page/"
+ byYear={true}
+ pageNumber={pageNumber}
+ posts={getPostsList([articles])}
+ searchPage="/recherche/"
+ total={totalArticles}
+ />
+ </PageLayout>
+ </>
+ );
+};
+
+BlogPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
+
+interface BlogPageParams extends ParsedUrlQuery {
+ number: string;
+}
+
+export const getStaticProps: GetStaticProps<BlogPageProps> = async ({
+ locale,
+ params,
+}) => {
+ const pageNumber = Number(params!.number as BlogPageParams['number']);
+ const queriedPostsNumber = settings.postsPerPage * pageNumber;
+ const lastCursor = await getArticlesEndCursor({
+ first: queriedPostsNumber,
+ });
+ const articles = await getArticles({
+ first: settings.postsPerPage,
+ after: lastCursor,
+ });
+ const totalArticles = await getTotalArticles();
+ const totalThematics = await getTotalThematics();
+ const thematics = await getThematicsPreview({ first: totalThematics });
+ const totalTopics = await getTotalTopics();
+ const topics = await getTopicsPreview({ first: totalTopics });
+ const translation = await loadTranslation(locale);
+
+ return {
+ props: {
+ articles: JSON.parse(JSON.stringify(articles)),
+ pageNumber,
+ thematicsList: thematics.edges.map((edge) => edge.node),
+ topicsList: topics.edges.map((edge) => edge.node),
+ totalArticles,
+ translation,
+ },
+ };
+};
+
+export const getStaticPaths: GetStaticPaths = async () => {
+ const totalArticles = await getTotalArticles();
+ const totalPages = Math.ceil(totalArticles / settings.postsPerPage);
+ const pagesArray = Array.from(
+ { length: totalPages },
+ (_, index) => index + 1
+ );
+ const paths = pagesArray.map((number) => {
+ return { params: { number: `${number}` } };
+ });
+
+ return {
+ paths,
+ fallback: false,
+ };
+};
+
+export default BlogPage;
diff --git a/src/pages/contact.tsx b/src/pages/contact.tsx
index 5934dd9..2392fe2 100644
--- a/src/pages/contact.tsx
+++ b/src/pages/contact.tsx
@@ -1,89 +1,124 @@
-import ContactForm from '@components/ContactForm/ContactForm';
-import { getLayout } from '@components/Layouts/Layout';
-import PostHeader from '@components/PostHeader/PostHeader';
-import Sidebar from '@components/Sidebar/Sidebar';
-import { SocialMedia } from '@components/Widgets';
-import styles from '@styles/pages/Page.module.scss';
-import { NextPageWithLayout } from '@ts/types/app';
-import { settings } from '@utils/config';
-import { getIntlInstance, loadTranslation } from '@utils/helpers/i18n';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
+import Notice, { type NoticeKind } from '@components/atoms/layout/notice';
+import ContactForm, {
+ type ContactFormProps,
+} from '@components/organisms/forms/contact-form';
+import SocialMedia from '@components/organisms/widgets/social-media';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout from '@components/templates/page/page-layout';
+import { meta } from '@content/pages/contact.mdx';
+import { sendMail } from '@services/graphql/contact';
+import styles from '@styles/pages/contact.module.scss';
+import { type NextPageWithLayout } from '@ts/types/app';
+import { loadTranslation } from '@utils/helpers/i18n';
+import {
+ getSchemaJson,
+ getSinglePageSchema,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useSettings from '@utils/hooks/use-settings';
+import { GetStaticProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Script from 'next/script';
+import { useState } from 'react';
import { useIntl } from 'react-intl';
-import { ContactPage as ContactPageSchema, Graph, WebPage } from 'schema-dts';
const ContactPage: NextPageWithLayout = () => {
+ const { dates, intro, seo, title } = meta;
const intl = useIntl();
- const router = useRouter();
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: `/contact`,
+ });
- const pageTitle = intl.formatMessage(
- {
- defaultMessage: 'Contact form - {websiteName}',
- description: 'ContactPage: SEO - Page title',
- id: 'Y3qRib',
- },
- { websiteName: settings.name }
- );
- const pageDescription = intl.formatMessage(
- {
- defaultMessage:
- "Contact {websiteName} through its website. All you need to do it's to fill the contact form.",
- description: 'ContactPage: SEO - Meta description',
- id: 'OIffB4',
- },
- { websiteName: settings.name }
- );
- const pageUrl = `${settings.url}${router.asPath}`;
- const title = intl.formatMessage({
- defaultMessage: 'Contact',
- description: 'ContactPage: page title',
- id: 'AN9iy7',
+ const socialMediaTitle = intl.formatMessage({
+ defaultMessage: 'Find me elsewhere',
+ description: 'ContactPage: social media widget title',
+ id: 'Qh2CwH',
+ });
+
+ const { website } = useSettings();
+ const { asPath } = useRouter();
+ const webpageSchema = getWebPageSchema({
+ description: seo.description,
+ locale: website.locales.default,
+ slug: asPath,
+ title: seo.title,
+ updateDate: dates.update,
});
- const intro = intl.formatMessage({
- defaultMessage: 'Please fill the form to contact me.',
- description: 'ContactPage: page introduction',
- id: '8Ls2mD',
+ const contactSchema = getSinglePageSchema({
+ dates,
+ description: intro,
+ id: 'contact',
+ kind: 'contact',
+ locale: website.locales.default,
+ slug: asPath,
+ title,
});
+ const schemaJsonLd = getSchemaJson([webpageSchema, contactSchema]);
- const webpageSchema: WebPage = {
- '@id': `${pageUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- name: pageTitle,
- description: pageDescription,
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${pageUrl}`,
- isPartOf: {
- '@id': `${settings.url}`,
- },
- };
+ const widgets = [
+ <SocialMedia
+ key="social-media"
+ title={socialMediaTitle}
+ level={2}
+ media={[
+ { name: 'Github', url: 'https://github.com/ArmandPhilippot' },
+ { name: 'Gitlab', url: 'https://gitlab.com/ArmandPhilippot' },
+ {
+ name: 'LinkedIn',
+ url: 'https://www.linkedin.com/in/armandphilippot',
+ },
+ ]}
+ />,
+ ];
- const contactSchema: ContactPageSchema = {
- '@id': `${settings.url}/#contact`,
- '@type': 'ContactPage',
- name: title,
- description: intro,
- author: { '@id': `${settings.url}/#branding` },
- creator: { '@id': `${settings.url}/#branding` },
- editor: { '@id': `${settings.url}/#branding` },
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: { '@id': `${pageUrl}` },
- };
+ const [status, setStatus] = useState<NoticeKind>('info');
+ const [statusMessage, setStatusMessage] = useState<string>('');
+
+ const submitMail: ContactFormProps['sendMail'] = async (data, reset) => {
+ const { email, message, name, subject } = data;
+ const messageHTML = message.replace(/\r?\n/g, '<br />');
+ const body = `Message received from ${name} <${email}> on ${website.url}.<br /><br />${messageHTML}`;
+ const replyTo = `${name} <${email}>`;
+ const mailData = {
+ body,
+ clientMutationId: 'contact',
+ replyTo,
+ subject,
+ };
+ const { message: mutationMessage, sent } = await sendMail(mailData);
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, contactSchema],
+ if (sent) {
+ setStatus('success');
+ setStatusMessage(
+ intl.formatMessage({
+ defaultMessage:
+ 'Thanks. Your message was successfully sent. I will answer it as soon as possible.',
+ description: 'Contact: success message',
+ id: '3Pipok',
+ })
+ );
+ reset();
+ } else {
+ const errorPrefix = intl.formatMessage({
+ defaultMessage: 'An error occurred:',
+ description: 'Contact: error message',
+ id: 'TpyFZ6',
+ });
+ const error = `${errorPrefix} ${mutationMessage}`;
+ setStatus('error');
+ setStatusMessage(error);
+ }
};
return (
<>
<Head>
- <title>{pageTitle}</title>
- <meta name="description" content={pageDescription} />
- <meta property="og:url" content={`${pageUrl}`} />
+ <title>{`${seo.title} - ${website.name}`}</title>
+ <meta name="description" content={seo.description} />
+ <meta property="og:url" content={`${website.url}${asPath}`} />
<meta property="og:type" content="article" />
<meta property="og:title" content={title} />
<meta property="og:description" content={intro} />
@@ -93,56 +128,36 @@ const ContactPage: NextPageWithLayout = () => {
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
/>
- <article
- id="contact"
- className={`${styles.article} ${styles['article--no-comments']}`}
+ <PageLayout
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ intro={intro}
+ title="Contact"
+ widgets={widgets}
>
- <PostHeader title={title} intro={intro} />
- <div className={styles.body}>
- <p>
- {intl.formatMessage({
- defaultMessage: 'All fields marked with * are required.',
- description: 'ContactPage: required fields text',
- id: 'txusHd',
- })}
- </p>
- <ContactForm />
- </div>
- <Sidebar position="right">
- <SocialMedia
- title={intl.formatMessage({
- defaultMessage: 'Find me elsewhere',
- description: 'ContactPage: social media widget title',
- id: 'Qh2CwH',
- })}
- github={true}
- gitlab={true}
- linkedin={true}
- />
- </Sidebar>
- </article>
+ <ContactForm
+ sendMail={submitMail}
+ Notice={
+ <Notice
+ kind={status}
+ message={statusMessage}
+ className={styles.notice}
+ />
+ }
+ />
+ </PageLayout>
</>
);
};
-ContactPage.getLayout = getLayout;
+ContactPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const intl = await getIntlInstance();
- const breadcrumbTitle = intl.formatMessage({
- defaultMessage: 'Contact',
- description: 'ContactPage: breadcrumb item',
- id: 'CzTbM4',
- });
- const { locale } = context;
+export const getStaticProps: GetStaticProps = async ({ locale }) => {
const translation = await loadTranslation(locale);
return {
props: {
- breadcrumbTitle,
- locale,
translation,
},
};
diff --git a/src/pages/cv.tsx b/src/pages/cv.tsx
index 71eb449..4686505 100644
--- a/src/pages/cv.tsx
+++ b/src/pages/cv.tsx
@@ -1,108 +1,164 @@
-import { getLayout } from '@components/Layouts/Layout';
-import PostHeader from '@components/PostHeader/PostHeader';
-import Sidebar from '@components/Sidebar/Sidebar';
-import { CVPreview, SocialMedia, ToC } from '@components/Widgets';
-import CVContent, { intro, meta, pdf, image } from '@content/pages/cv.mdx';
-import styles from '@styles/pages/Page.module.scss';
-import { NextPageWithLayout } from '@ts/types/app';
-import { ArticleMeta } from '@ts/types/articles';
-import { settings } from '@utils/config';
+import Heading from '@components/atoms/headings/heading';
+import Link from '@components/atoms/links/link';
+import List from '@components/atoms/lists/list';
+import ImageWidget from '@components/organisms/widgets/image-widget';
+import SocialMedia from '@components/organisms/widgets/social-media';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout, {
+ type PageLayoutProps,
+} from '@components/templates/page/page-layout';
+import CVContent, { data, meta } from '@content/pages/cv.mdx';
+import styles from '@styles/pages/cv.module.scss';
+import { type NextPageWithLayout } from '@ts/types/app';
import { loadTranslation } from '@utils/helpers/i18n';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
+import {
+ getSchemaJson,
+ getSinglePageSchema,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useSettings from '@utils/hooks/use-settings';
+import { NestedMDXComponents } from 'mdx/types';
+import { GetStaticProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Script from 'next/script';
+import React, { ReactNode } from 'react';
import { useIntl } from 'react-intl';
-import { AboutPage, Graph, WebPage } from 'schema-dts';
-const CV: NextPageWithLayout = () => {
+/**
+ * CV page.
+ */
+const CVPage: NextPageWithLayout = () => {
const intl = useIntl();
- const router = useRouter();
- const dates = {
- publication: meta.publishedOn,
- update: meta.updatedOn,
- };
+ const { file, image } = data;
+ const { dates, intro, seo, title } = meta;
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: `/cv`,
+ });
- const pageMeta: ArticleMeta = {
- dates,
+ const imageWidgetTitle = intl.formatMessage({
+ defaultMessage: 'Others formats',
+ description: 'CVPage: cv preview widget title',
+ id: 'B9OCyV',
+ });
+ const socialMediaTitle = intl.formatMessage({
+ defaultMessage: 'Open-source projects',
+ description: 'CVPage: social media widget title',
+ id: '+Dre5J',
+ });
+
+ const headerMeta: PageLayoutProps['headerMeta'] = {
+ publication: {
+ date: dates.publication,
+ },
+ update: dates.update
+ ? {
+ date: dates.update,
+ }
+ : undefined,
};
- const pageUrl = `${settings.url}${router.asPath}`;
- const pageTitle = intl.formatMessage(
+
+ const { website } = useSettings();
+ const cvAlt = intl.formatMessage(
{
- defaultMessage: 'CV Front-end developer - {websiteName}',
- description: 'CVPage: SEO - Page title',
- id: 'Y1ZdJ6',
+ defaultMessage: '{name} CV',
+ description: 'CVPage: CV image alternative text',
+ id: 'KUowUk',
},
- { websiteName: settings.name }
+ { name: website.name }
);
- const pageDescription = intl.formatMessage(
+ const cvCaption = intl.formatMessage(
{
- defaultMessage:
- 'Discover the curriculum of {websiteName}, front-end developer located in France: skills, experiences and training.',
- description: 'CVPage: SEO - Meta description',
- id: 'bBdMGm',
+ defaultMessage: '<link>Download the CV in PDF</link>',
+ id: 'fN04AJ',
+ description: 'CVPage: download CV in PDF text',
},
- { websiteName: settings.name }
+ {
+ link: (chunks: ReactNode) => (
+ <Link download={true} href={file}>
+ {chunks}
+ </Link>
+ ),
+ }
);
- const webpageSchema: WebPage = {
- '@id': `${pageUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- name: pageTitle,
- description: pageDescription,
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${pageUrl}`,
- isPartOf: {
- '@id': `${settings.url}`,
- },
- };
+ const widgets = [
+ <ImageWidget
+ key="image-widget"
+ expanded={true}
+ title={imageWidgetTitle}
+ level={2}
+ image={{ alt: cvAlt, ...image }}
+ description={cvCaption}
+ imageClassName={styles.image}
+ />,
+ <SocialMedia
+ key="social-media"
+ title={socialMediaTitle}
+ level={2}
+ media={[
+ { name: 'Github', url: 'https://github.com/ArmandPhilippot' },
+ { name: 'Gitlab', url: 'https://gitlab.com/ArmandPhilippot' },
+ {
+ name: 'LinkedIn',
+ url: 'https://www.linkedin.com/in/armandphilippot',
+ },
+ ]}
+ />,
+ ];
- const publicationDate = new Date(dates.publication);
- const updateDate = new Date(dates.update);
-
- const cvSchema: AboutPage = {
- '@id': `${settings.url}/#cv`,
- '@type': 'AboutPage',
- name: pageTitle,
+ const { asPath } = useRouter();
+ const webpageSchema = getWebPageSchema({
+ description: seo.description,
+ locale: website.locales.default,
+ slug: asPath,
+ title: seo.title,
+ updateDate: dates.update,
+ });
+ const cvSchema = getSinglePageSchema({
+ cover: image.src,
+ dates,
description: intro,
- author: { '@id': `${settings.url}/#branding` },
- creator: { '@id': `${settings.url}/#branding` },
- dateCreated: publicationDate.toISOString(),
- dateModified: updateDate.toISOString(),
- datePublished: publicationDate.toISOString(),
- editor: { '@id': `${settings.url}/#branding` },
- image,
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- thumbnailUrl: image,
- mainEntityOfPage: { '@id': `${pageUrl}` },
- };
+ id: 'cv',
+ kind: 'about',
+ locale: website.locales.default,
+ slug: asPath,
+ title: title,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema, cvSchema]);
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, cvSchema],
+ const components: NestedMDXComponents = {
+ a: (props) => <Link external={true} {...props} />,
+ h1: (props) => <Heading level={1} {...props} />,
+ h2: (props) => <Heading level={2} {...props} />,
+ h3: (props) => <Heading level={3} {...props} />,
+ h4: (props) => <Heading level={4} {...props} />,
+ h5: (props) => <Heading level={5} {...props} />,
+ h6: (props) => <Heading level={6} {...props} />,
+ Link: (props) => <Link {...props} />,
+ List: (props) => <List {...props} />,
};
- const title = intl.formatMessage(
- {
- defaultMessage: "{name}'s CV",
- description: 'CVPage: page title',
- id: 'Mj2BQf',
- },
- { name: settings.name }
- );
-
return (
- <>
+ <PageLayout
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ headerMeta={headerMeta}
+ intro={intro}
+ title={title}
+ widgets={widgets}
+ withToC={true}
+ >
<Head>
- <title>{pageTitle}</title>
- <meta name="description" content={pageDescription} />
- <meta property="og:url" content={`${pageUrl}`} />
+ <title>{`${seo.title} - ${website.name}`}</title>
+ <meta name="description" content={seo.description} />
+ <meta property="og:url" content={`${website.url}${asPath}`} />
<meta property="og:type" content="article" />
<meta property="og:title" content={title} />
<meta property="og:description" content={intro} />
- <meta property="og:image" content={image} />
+ <meta property="og:image" content={image.src} />
<meta property="og:image:alt" content={title} />
</Head>
<Script
@@ -110,72 +166,22 @@ const CV: NextPageWithLayout = () => {
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
/>
- <article
- id="cv"
- className={`${styles.article} ${styles['article--no-comments']}`}
- >
- <PostHeader intro={intro} meta={pageMeta} title={meta.title} />
- <Sidebar
- position="left"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Table of Contents',
- description: 'CVPage: ToC sidebar aria-label',
- id: 'g4DckL',
- })}
- >
- <ToC />
- </Sidebar>
- <div className={styles.body}>
- <CVContent />
- </div>
- <Sidebar
- position="right"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Sidebar',
- description: 'CVPage: right sidebar aria-label',
- id: 'QHOm5t',
- })}
- >
- <CVPreview
- title={intl.formatMessage({
- defaultMessage: 'Others formats',
- description: 'CVPage: cv preview widget title',
- id: 'B9OCyV',
- })}
- imgSrc={image}
- pdf={pdf}
- />
- <SocialMedia
- title={intl.formatMessage({
- defaultMessage: 'Open-source projects',
- description: 'CVPage: social media widget title',
- id: '+Dre5J',
- })}
- github={true}
- gitlab={true}
- />
- </Sidebar>
- </article>
- </>
+ <CVContent components={components} />
+ </PageLayout>
);
};
-CV.getLayout = getLayout;
+CVPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const breadcrumbTitle = meta.title;
- const { locale } = context;
+export const getStaticProps: GetStaticProps = async ({ locale }) => {
const translation = await loadTranslation(locale);
return {
props: {
- breadcrumbTitle,
- locale,
translation,
},
};
};
-export default CV;
+export default CVPage;
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index ca0a809..6e9c4c6 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,39 +1,59 @@
import FeedIcon from '@assets/images/icon-feed.svg';
-import { ButtonLink } from '@components/Buttons';
-import { ContactIcon } from '@components/Icons';
-import Layout from '@components/Layouts/Layout';
-import { ResponsiveImage } from '@components/MDX';
-import { RecentPosts } from '@components/Widgets';
+import ButtonLink from '@components/atoms/buttons/button-link';
+import Envelop from '@components/atoms/icons/envelop';
+import Column, { type ColumnProps } from '@components/atoms/layout/column';
+import Section, { type SectionProps } from '@components/atoms/layout/section';
+import List, { type ListItem } from '@components/atoms/lists/list';
+import ResponsiveImage, {
+ type ResponsiveImageProps,
+} from '@components/molecules/images/responsive-image';
+import Columns, {
+ type ColumnsProps,
+} from '@components/molecules/layout/columns';
+import CardsList, {
+ type CardsListItem,
+} from '@components/organisms/layout/cards-list';
+import { getLayout } from '@components/templates/layout/layout';
import HomePageContent from '@content/pages/homepage.mdx';
-import { getPublishedPosts } from '@services/graphql/queries';
-import styles from '@styles/pages/Home.module.scss';
-import { NextPageWithLayout, ResponsiveImageProps } from '@ts/types/app';
-import { PostsList } from '@ts/types/blog';
-import { settings } from '@utils/config';
-import { loadTranslation } from '@utils/helpers/i18n';
+import { getArticlesCard } from '@services/graphql/articles';
+import styles from '@styles/pages/home.module.scss';
+import { type ArticleCard, type NextPageWithLayout } from '@ts/types/app';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import { getSchemaJson, getWebPageSchema } from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useSettings from '@utils/hooks/use-settings';
import { NestedMDXComponents } from 'mdx/types';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
+import { GetStaticProps } from 'next';
import Head from 'next/head';
import Script from 'next/script';
-import type { ReactElement } from 'react';
+import { ReactElement } from 'react';
import { useIntl } from 'react-intl';
-import { Graph, WebPage } from 'schema-dts';
-type HomePageProps = {
- recentPosts: PostsList;
+type HomeProps = {
+ recentPosts: ArticleCard[];
+ translation?: Messages;
};
-const Home: NextPageWithLayout<HomePageProps> = ({
- recentPosts,
-}: {
- recentPosts: PostsList;
-}) => {
+/**
+ * Home page.
+ */
+const HomePage: NextPageWithLayout<HomeProps> = ({ recentPosts }) => {
const intl = useIntl();
+ const { schema: breadcrumbSchema } = useBreadcrumb({
+ title: '',
+ url: `/`,
+ });
- const CodingLinks = () => {
- return (
- <ul className={styles['links-list']}>
- <li>
+ /**
+ * Retrieve a list of coding links.
+ *
+ * @returns {JSX.Element} - A list of links.
+ */
+ const CodingLinks = (): JSX.Element => {
+ const links: ListItem[] = [
+ {
+ id: 'web-development',
+ value: (
<ButtonLink target="/thematique/developpement-web">
{intl.formatMessage({
defaultMessage: 'Web development',
@@ -41,8 +61,11 @@ const Home: NextPageWithLayout<HomePageProps> = ({
id: 'vkF/RP',
})}
</ButtonLink>
- </li>
- <li>
+ ),
+ },
+ {
+ id: 'projects',
+ value: (
<ButtonLink target="/projets">
{intl.formatMessage({
defaultMessage: 'Projects',
@@ -50,38 +73,65 @@ const Home: NextPageWithLayout<HomePageProps> = ({
id: 'N44SOc',
})}
</ButtonLink>
- </li>
- </ul>
- );
+ ),
+ },
+ ];
+
+ return <List kind="flex" items={links} className={styles.list} />;
};
- const ColdarkRepos = () => {
- return (
- <ul className={styles['links-list']}>
- <li>
+ /**
+ * Retrieve a list of Coldark repositories.
+ *
+ * @returns {JSX.Element} - A list of links.
+ */
+ const ColdarkRepos = (): JSX.Element => {
+ const links: ListItem[] = [
+ {
+ id: 'coldark-github',
+ value: (
<ButtonLink
target="https://github.com/ArmandPhilippot/coldark"
- isExternal={true}
+ external={true}
>
- Github
+ {intl.formatMessage({
+ defaultMessage: 'Github',
+ description: 'HomePage: Github link',
+ id: '3f3PzH',
+ })}
</ButtonLink>
- </li>
- <li>
+ ),
+ },
+ {
+ id: 'coldark-gitlab',
+ value: (
<ButtonLink
target="https://gitlab.com/ArmandPhilippot/coldark"
- isExternal={true}
+ external={true}
>
- Gitlab
+ {intl.formatMessage({
+ defaultMessage: 'Gitlab',
+ description: 'HomePage: Gitlab link',
+ id: '7AnwZ7',
+ })}
</ButtonLink>
- </li>
- </ul>
- );
+ ),
+ },
+ ];
+
+ return <List kind="flex" items={links} className={styles.list} />;
};
- const LibreLinks = () => {
- return (
- <ul className={styles['links-list']}>
- <li>
+ /**
+ * Retrieve a list of links related to Free thematic.
+ *
+ * @returns {JSX.Element} - A list of links.
+ */
+ const LibreLinks = (): JSX.Element => {
+ const links: ListItem[] = [
+ {
+ id: 'free',
+ value: (
<ButtonLink target="/thematique/libre">
{intl.formatMessage({
defaultMessage: 'Free',
@@ -89,8 +139,11 @@ const Home: NextPageWithLayout<HomePageProps> = ({
id: 'w8GrOf',
})}
</ButtonLink>
- </li>
- <li>
+ ),
+ },
+ {
+ id: 'linux',
+ value: (
<ButtonLink target="/thematique/linux">
{intl.formatMessage({
defaultMessage: 'Linux',
@@ -98,15 +151,23 @@ const Home: NextPageWithLayout<HomePageProps> = ({
id: 'jASD7k',
})}
</ButtonLink>
- </li>
- </ul>
- );
+ ),
+ },
+ ];
+
+ return <List kind="flex" items={links} className={styles.list} />;
};
- const ShaarliLink = () => {
- return (
- <ul className={styles['links-list']}>
- <li>
+ /**
+ * Retrieve the Shaarli link.
+ *
+ * @returns {JSX.Element} - A list of links
+ */
+ const ShaarliLink = (): JSX.Element => {
+ const links: ListItem[] = [
+ {
+ id: 'shaarli',
+ value: (
<ButtonLink target="https://shaarli.armandphilippot.com/">
{intl.formatMessage({
defaultMessage: 'Shaarli',
@@ -114,59 +175,127 @@ const Home: NextPageWithLayout<HomePageProps> = ({
id: 'i5L19t',
})}
</ButtonLink>
- </li>
- </ul>
- );
+ ),
+ },
+ ];
+
+ return <List kind="flex" items={links} className={styles.list} />;
};
- const MoreLinks = () => {
- return (
- <ul className={styles['links-list']}>
- <li>
+ /**
+ * Retrieve the additional links.
+ *
+ * @returns {JSX.Element} - A list of links.
+ */
+ const MoreLinks = (): JSX.Element => {
+ const links: ListItem[] = [
+ {
+ id: 'contact-me',
+ value: (
<ButtonLink target="/contact">
- <ContactIcon />
+ <Envelop className={styles.icon} />
{intl.formatMessage({
defaultMessage: 'Contact me',
description: 'HomePage: contact button text',
id: 'sO/Iwj',
})}
</ButtonLink>
- </li>
- <li>
+ ),
+ },
+ {
+ id: 'rss-feed',
+ value: (
<ButtonLink target="/feed">
- <FeedIcon className={styles['icon--feed']} />
+ <FeedIcon className={`${styles.icon} ${styles['icon--feed']}`} />
{intl.formatMessage({
defaultMessage: 'Subscribe',
description: 'HomePage: RSS feed subscription text',
id: 'T4YA64',
})}
</ButtonLink>
- </li>
- </ul>
+ ),
+ },
+ ];
+
+ return <List kind="flex" items={links} className={styles.list} />;
+ };
+
+ /**
+ * Get a cards list of recent posts.
+ *
+ * @returns {JSX.Element} - The cards list.
+ */
+ const getRecentPosts = (): JSX.Element => {
+ const posts: CardsListItem[] = recentPosts.map((post) => {
+ return {
+ cover: post.cover,
+ id: post.slug,
+ meta: { publication: { date: post.dates.publication } },
+ title: post.title,
+ url: `/article/${post.slug}`,
+ };
+ });
+
+ return (
+ <CardsList
+ items={posts}
+ titleLevel={3}
+ className={`${styles.list} ${styles['list--cards']}`}
+ />
);
};
- const getRecentPosts = () => {
- return <RecentPosts posts={recentPosts} />;
+ /**
+ * Create the page sections.
+ *
+ * @param {object} obj - An object containing the section body.
+ * @param {ReactElement[]} obj.children - The section body.
+ * @returns {JSX.Element} A section element.
+ */
+ const getSection = ({
+ children,
+ variant,
+ }: {
+ children: ReactElement[];
+ variant: SectionProps['variant'];
+ }): JSX.Element => {
+ const [headingEl, ...content] = children;
+ const title = headingEl.props.children;
+
+ return (
+ <Section
+ title={title}
+ content={content}
+ variant={variant}
+ className={styles.section}
+ />
+ );
};
const components: NestedMDXComponents = {
CodingLinks: CodingLinks,
ColdarkRepos: ColdarkRepos,
- Image: (props: ResponsiveImageProps) => ResponsiveImage({ ...props }),
+ Column: (props: ColumnProps) => <Column {...props} />,
+ Columns: (props: ColumnsProps) => (
+ <Columns className={styles.columns} {...props} />
+ ),
+ Image: (props: ResponsiveImageProps) => <ResponsiveImage {...props} />,
LibreLinks: LibreLinks,
MoreLinks: MoreLinks,
RecentPosts: getRecentPosts,
+ Section: getSection,
ShaarliLink: ShaarliLink,
};
+ const { website } = useSettings();
+
const pageTitle = intl.formatMessage(
{
defaultMessage: '{websiteName} | Front-end developer: WordPress/React',
description: 'HomePage: SEO - Page title',
id: 'PXp2hv',
},
- { websiteName: settings.name }
+ { websiteName: website.name }
);
const pageDescription = intl.formatMessage(
{
@@ -175,35 +304,22 @@ const Home: NextPageWithLayout<HomePageProps> = ({
description: 'HomePage: SEO - Meta description',
id: 'tMuNTy',
},
- { websiteName: settings.name }
+ { websiteName: website.name }
);
-
- const webpageSchema: WebPage = {
- '@id': `${settings.url}/#home`,
- '@type': 'WebPage',
- name: pageTitle,
+ const webpageSchema = getWebPageSchema({
description: pageDescription,
- author: { '@id': `${settings.url}/#branding` },
- creator: { '@id': `${settings.url}/#branding` },
- editor: { '@id': `${settings.url}/#branding` },
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${settings.url}`,
- };
-
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema],
- };
+ locale: website.locales.default,
+ slug: '',
+ title: pageTitle,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema]);
return (
<>
<Head>
<title>{pageTitle}</title>
<meta name="description" content={pageDescription} />
- <meta property="og:type" content="website" />
- <meta property="og:url" content={`${settings.url}`} />
+ <meta property="og:url" content={website.url} />
<meta property="og:title" content={pageTitle} />
<meta property="og:description" content={pageDescription} />
</Head>
@@ -212,23 +328,22 @@ const Home: NextPageWithLayout<HomePageProps> = ({
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
/>
- <div id="home">
- <HomePageContent components={components} />
- </div>
+ <Script
+ id="schema-breadcrumb"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
+ />
+ <HomePageContent components={components} />
</>
);
};
-Home.getLayout = function getLayout(page: ReactElement) {
- return <Layout isHome={true}>{page}</Layout>;
-};
+HomePage.getLayout = (page) =>
+ getLayout(page, { isHome: true, withExtraPadding: false });
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const { locale } = context;
+export const getStaticProps: GetStaticProps<HomeProps> = async ({ locale }) => {
const translation = await loadTranslation(locale);
- const recentPosts = await getPublishedPosts({ first: 3 });
+ const recentPosts = await getArticlesCard({ first: 3 });
return {
props: {
@@ -238,4 +353,4 @@ export const getStaticProps: GetStaticProps = async (
};
};
-export default Home;
+export default HomePage;
diff --git a/src/pages/mentions-legales.tsx b/src/pages/mentions-legales.tsx
index b103b5e..a58a850 100644
--- a/src/pages/mentions-legales.tsx
+++ b/src/pages/mentions-legales.tsx
@@ -1,111 +1,86 @@
-import { getLayout } from '@components/Layouts/Layout';
-import { Link } from '@components/MDX';
-import PostHeader from '@components/PostHeader/PostHeader';
-import Sidebar from '@components/Sidebar/Sidebar';
-import { ToC } from '@components/Widgets';
-import LegalNoticeContent, {
- intro,
- meta,
-} from '@content/pages/legal-notice.mdx';
-import styles from '@styles/pages/Page.module.scss';
-import { NextPageWithLayout } from '@ts/types/app';
-import { ArticleMeta } from '@ts/types/articles';
-import { settings } from '@utils/config';
+import Link from '@components/atoms/links/link';
+import ResponsiveImage from '@components/molecules/images/responsive-image';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout, {
+ type PageLayoutProps,
+} from '@components/templates/page/page-layout';
+import LegalNoticeContent, { meta } from '@content/pages/legal-notice.mdx';
+import { type NextPageWithLayout } from '@ts/types/app';
import { loadTranslation } from '@utils/helpers/i18n';
+import {
+ getSchemaJson,
+ getSinglePageSchema,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useSettings from '@utils/hooks/use-settings';
import { NestedMDXComponents } from 'mdx/types';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
+import { GetStaticProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Script from 'next/script';
-import { useIntl } from 'react-intl';
-import { Article, Graph, WebPage } from 'schema-dts';
-const LegalNotice: NextPageWithLayout = () => {
- const intl = useIntl();
- const router = useRouter();
- const dates = {
- publication: meta.publishedOn,
- update: meta.updatedOn,
- };
-
- const pageMeta: ArticleMeta = {
- dates,
- };
- const pageTitle = intl.formatMessage(
- {
- defaultMessage: 'Legal notice - {websiteName}',
- description: 'LegalNoticePage: SEO - Page title',
- id: '4zAUSu',
- },
- { websiteName: settings.name }
- );
- const pageDescription = intl.formatMessage(
- {
- defaultMessage: "Discover the legal notice of {websiteName}'s website.",
- description: 'LegalNoticePage: SEO - Meta description',
- id: 'uvB+32',
- },
- { websiteName: settings.name }
- );
- const pageUrl = `${settings.url}${router.asPath}`;
- const title = intl.formatMessage({
- defaultMessage: 'Legal notice',
- description: 'LegalNoticePage: page title',
- id: '/IirIt',
+/**
+ * Legal Notice page.
+ */
+const LegalNoticePage: NextPageWithLayout = () => {
+ const { dates, intro, seo, title } = meta;
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: `/mentions-legales`,
});
- const publicationDate = new Date(dates.publication);
- const updateDate = new Date(dates.update);
- const webpageSchema: WebPage = {
- '@id': `${pageUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- name: pageTitle,
- description: pageDescription,
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${pageUrl}`,
- isPartOf: {
- '@id': `${settings.url}`,
+ const headerMeta: PageLayoutProps['headerMeta'] = {
+ publication: {
+ date: dates.publication,
},
- };
-
- const articleSchema: Article = {
- '@id': `${settings.url}/#legal-notice`,
- '@type': 'Article',
- name: title,
- description: intro,
- author: { '@id': `${settings.url}/#branding` },
- copyrightYear: publicationDate.getFullYear(),
- creator: { '@id': `${settings.url}/#branding` },
- dateCreated: publicationDate.toISOString(),
- dateModified: updateDate.toISOString(),
- datePublished: publicationDate.toISOString(),
- editor: { '@id': `${settings.url}/#branding` },
- headline: title,
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: { '@id': `${pageUrl}` },
- };
-
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, articleSchema],
+ update: dates.update
+ ? {
+ date: dates.update,
+ }
+ : undefined,
};
const components: NestedMDXComponents = {
- Link: (props) => Link(props),
+ Image: (props) => <ResponsiveImage {...props} />,
+ Link: (props) => <Link {...props} />,
};
+ const { website } = useSettings();
+ const { asPath } = useRouter();
+ const webpageSchema = getWebPageSchema({
+ description: seo.description,
+ locale: website.locales.default,
+ slug: asPath,
+ title: seo.title,
+ updateDate: dates.update,
+ });
+ const articleSchema = getSinglePageSchema({
+ dates,
+ description: intro,
+ id: 'legal-notice',
+ kind: 'page',
+ locale: website.locales.default,
+ slug: asPath,
+ title,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema, articleSchema]);
+
return (
- <>
+ <PageLayout
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ headerMeta={headerMeta}
+ intro={intro}
+ title={title}
+ withToC={true}
+ >
<Head>
- <title>{pageTitle}</title>
- <meta name="description" content={pageDescription} />
- <meta property="og:url" content={`${pageUrl}`} />
+ <title>{`${seo.title} - ${website.name}`}</title>
+ <meta name="description" content={seo.description} />
+ <meta property="og:url" content={`${website.url}${asPath}`} />
<meta property="og:type" content="article" />
- <meta property="og:title" content={pageTitle} />
+ <meta property="og:title" content={`${seo.title} - ${website.name}`} />
<meta property="og:description" content={intro} />
</Head>
<Script
@@ -113,38 +88,22 @@ const LegalNotice: NextPageWithLayout = () => {
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
/>
- <article
- id="legal-notice"
- className={`${styles.article} ${styles['article--no-comments']}`}
- >
- <PostHeader intro={intro} meta={pageMeta} title={meta.title} />
- <Sidebar position="left">
- <ToC />
- </Sidebar>
- <div className={styles.body}>
- <LegalNoticeContent components={components} />
- </div>
- </article>
- </>
+ <LegalNoticeContent components={components} />
+ </PageLayout>
);
};
-LegalNotice.getLayout = getLayout;
+LegalNoticePage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const breadcrumbTitle = meta.title;
- const { locale } = context;
+export const getStaticProps: GetStaticProps = async ({ locale }) => {
const translation = await loadTranslation(locale);
return {
props: {
- breadcrumbTitle,
- locale,
translation,
},
};
};
-export default LegalNotice;
+export default LegalNoticePage;
diff --git a/src/pages/projet/[slug].tsx b/src/pages/projet/[slug].tsx
deleted file mode 100644
index 1f09fed..0000000
--- a/src/pages/projet/[slug].tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import { getLayout } from '@components/Layouts/Layout';
-import { CodeBlock, Gallery, Link, ResponsiveImage } from '@components/MDX';
-import PostHeader from '@components/PostHeader/PostHeader';
-import ProjectSummary from '@components/ProjectSummary/ProjectSummary';
-import Sidebar from '@components/Sidebar/Sidebar';
-import { Sharing, ToC } from '@components/Widgets';
-import styles from '@styles/pages/Page.module.scss';
-import {
- NextPageWithLayout,
- Project as ProjectData,
- ProjectProps,
-} from '@ts/types/app';
-import { settings } from '@utils/config';
-import { loadTranslation } from '@utils/helpers/i18n';
-import {
- getAllProjectsFilename,
- getProjectData,
-} from '@utils/helpers/projects';
-import { MDXComponents, NestedMDXComponents } from 'mdx/types';
-import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
-import Head from 'next/head';
-import { useRouter } from 'next/router';
-import Script from 'next/script';
-import { ParsedUrlQuery } from 'querystring';
-import { ComponentType } from 'react';
-import { useIntl } from 'react-intl';
-import { Article, Graph, WebPage } from 'schema-dts';
-
-const Project: NextPageWithLayout<ProjectProps> = ({
- project,
-}: {
- project: ProjectData;
-}) => {
- const intl = useIntl();
- const router = useRouter();
- const projectUrl = `${settings.url}${router.asPath}`;
- const { id, intro, meta, title, seo } = project;
- const dates = {
- publication: meta.publishedOn,
- update: meta.updatedOn,
- };
-
- const components: NestedMDXComponents = {
- CodeBlock: (props) => CodeBlock(props),
- Gallery: (props) => Gallery(props),
- Image: (props) => ResponsiveImage({ caption: props.caption, ...props }),
- Link: (props) => Link(props),
- pre: ({ children }) => CodeBlock(children.props),
- };
-
- const ProjectContent: ComponentType<MDXComponents> =
- require(`../../content/projects/${id}.mdx`).default;
-
- const webpageSchema: WebPage = {
- '@id': `${projectUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- name: seo.title,
- description: seo.description,
- inLanguage: settings.locales.defaultLocale,
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${settings.url}`,
- isPartOf: {
- '@id': `${settings.url}`,
- },
- };
-
- const publicationDate = new Date(dates.publication);
- const updateDate = new Date(dates.update);
-
- const articleSchema: Article = {
- '@id': `${settings.url}/project`,
- '@type': 'Article',
- name: title,
- description: intro,
- author: { '@id': `${settings.url}/#branding` },
- copyrightYear: publicationDate.getFullYear(),
- creator: { '@id': `${settings.url}/#branding` },
- dateCreated: publicationDate.toISOString(),
- dateModified: updateDate.toISOString(),
- datePublished: publicationDate.toISOString(),
- editor: { '@id': `${settings.url}/#branding` },
- headline: title,
- thumbnailUrl: meta.hasCover ? `/projects/${id}.jpg` : '',
- image: meta.hasCover ? `/projects/${id}.jpg` : '',
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: { '@id': `${projectUrl}` },
- };
-
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, articleSchema],
- };
-
- return (
- <>
- <Head>
- <title>{seo.title}</title>
- <meta name="description" content={seo.description} />
- <meta property="og:url" content={`${projectUrl}`} />
- <meta property="og:type" content="article" />
- <meta property="og:title" content={title} />
- <meta property="og:description" content={intro} />
- </Head>
- <Script
- id="schema-project"
- type="application/ld+json"
- dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
- />
- <article
- id="project"
- className={`${styles.article} ${styles['article--no-comments']}`}
- >
- <PostHeader title={title} intro={intro} meta={{ dates }} />
- <Sidebar
- position="left"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Table of Contents',
- description: 'ProjectPage: ToC sidebar aria-label',
- id: '6dXfvr',
- })}
- >
- <ToC />
- </Sidebar>
- <div className={styles.body}>
- <ProjectSummary id={id} title={title} meta={meta} />
- <ProjectContent components={components} />
- </div>
- <Sidebar
- position="right"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Sidebar',
- description: 'ProjectPage: right sidebar aria-label',
- id: 'hHrNd0',
- })}
- >
- <Sharing title={title} excerpt={intro} />
- </Sidebar>
- </article>
- </>
- );
-};
-
-Project.getLayout = getLayout;
-
-interface ProjectParams extends ParsedUrlQuery {
- slug: string;
-}
-
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const { locale } = context;
- const translation = await loadTranslation(locale);
- const { slug } = context.params as ProjectParams;
- const project = await getProjectData(slug);
- const breadcrumbTitle = project.title;
-
- return {
- props: {
- breadcrumbTitle,
- locale,
- project,
- translation,
- },
- };
-};
-
-export const getStaticPaths: GetStaticPaths = async () => {
- const filenames = getAllProjectsFilename();
- const paths = filenames.map((filename) => {
- return {
- params: {
- slug: filename,
- },
- };
- });
-
- return {
- paths,
- fallback: false,
- };
-};
-
-export default Project;
diff --git a/src/pages/projets.tsx b/src/pages/projets.tsx
deleted file mode 100644
index 8a81f39..0000000
--- a/src/pages/projets.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import { getLayout } from '@components/Layouts/Layout';
-import PostHeader from '@components/PostHeader/PostHeader';
-import ProjectsList from '@components/ProjectsList/ProjectsList';
-import PageContent, { meta } from '@content/pages/projects.mdx';
-import styles from '@styles/pages/Projects.module.scss';
-import { Project } from '@ts/types/app';
-import { settings } from '@utils/config';
-import { loadTranslation } from '@utils/helpers/i18n';
-import { getSortedProjects } from '@utils/helpers/projects';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
-import Head from 'next/head';
-import { useRouter } from 'next/router';
-import Script from 'next/script';
-import { useIntl } from 'react-intl';
-import { Article, Graph, WebPage } from 'schema-dts';
-
-const Projects = ({ projects }: { projects: Project[] }) => {
- const intl = useIntl();
- const dates = {
- publication: meta.publishedOn,
- update: meta.updatedOn,
- };
- const publicationDate = new Date(dates.publication);
- const updateDate = new Date(dates.update);
- const router = useRouter();
- const pageUrl = `${settings.url}${router.asPath}`;
- const pageTitle = intl.formatMessage(
- {
- defaultMessage: 'Projects: open-source makings - {websiteName}',
- description: 'ProjectsPage: SEO - Page title',
- id: 'SX1z3t',
- },
- { websiteName: settings.name }
- );
- const pageDescription = intl.formatMessage(
- {
- defaultMessage:
- 'Discover {websiteName} projects. Mostly related to web development and open source.',
- description: 'ProjectsPage: SEO - Meta description',
- id: 's6U1Xt',
- },
- { websiteName: settings.name }
- );
-
- const webpageSchema: WebPage = {
- '@id': `${pageUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- name: pageTitle,
- description: pageDescription,
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${pageUrl}`,
- isPartOf: {
- '@id': `${settings.url}`,
- },
- };
-
- const articleSchema: Article = {
- '@id': `${settings.url}/#projects`,
- '@type': 'Article',
- name: meta.title,
- description: pageDescription,
- author: { '@id': `${settings.url}/#branding` },
- copyrightYear: publicationDate.getFullYear(),
- creator: { '@id': `${settings.url}/#branding` },
- dateCreated: publicationDate.toISOString(),
- dateModified: updateDate.toISOString(),
- datePublished: publicationDate.toISOString(),
- editor: { '@id': `${settings.url}/#branding` },
- headline: meta.title,
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: { '@id': `${pageUrl}` },
- };
-
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, articleSchema],
- };
-
- return (
- <>
- <Head>
- <title>{pageTitle}</title>
- <meta name="description" content={pageDescription} />
- <meta property="og:url" content={`${pageUrl}`} />
- <meta property="og:type" content="article" />
- <meta property="og:title" content={meta.title} />
- <meta property="og:description" content={pageDescription} />
- </Head>
- <Script
- id="schema-projects"
- type="application/ld+json"
- dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
- />
- <article id="projects" className={styles.article}>
- <PostHeader title={meta.title} intro={<PageContent />} />
- <div className={styles.body}>
- {projects.length > 0 && <ProjectsList projects={projects} />}
- </div>
- </article>
- </>
- );
-};
-
-Projects.getLayout = getLayout;
-
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const breadcrumbTitle = meta.title;
- const { locale } = context;
- const projects: Project[] = await getSortedProjects();
- const translation = await loadTranslation(locale);
-
- return {
- props: {
- breadcrumbTitle,
- locale,
- projects,
- translation,
- },
- };
-};
-
-export default Projects;
diff --git a/src/pages/projets/[slug].tsx b/src/pages/projets/[slug].tsx
new file mode 100644
index 0000000..247f350
--- /dev/null
+++ b/src/pages/projets/[slug].tsx
@@ -0,0 +1,241 @@
+import Link from '@components/atoms/links/link';
+import SocialLink, {
+ type SocialWebsite,
+} from '@components/atoms/links/social-link';
+import Spinner from '@components/atoms/loaders/spinner';
+import ResponsiveImage from '@components/molecules/images/responsive-image';
+import Code from '@components/molecules/layout/code';
+import Gallery from '@components/organisms/images/gallery';
+import Overview, {
+ type OverviewMeta,
+} from '@components/organisms/layout/overview';
+import Sharing from '@components/organisms/widgets/sharing';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout, {
+ type PageLayoutProps,
+} from '@components/templates/page/page-layout';
+import styles from '@styles/pages/project.module.scss';
+import {
+ type NextPageWithLayout,
+ type ProjectPreview,
+ type Repos,
+} from '@ts/types/app';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import { getProjectData, getProjectFilenames } from '@utils/helpers/projects';
+import {
+ getSchemaJson,
+ getSinglePageSchema,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import { capitalize } from '@utils/helpers/strings';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useGithubApi, { type RepoData } from '@utils/hooks/use-github-api';
+import useSettings from '@utils/hooks/use-settings';
+import { MDXComponents, NestedMDXComponents } from 'mdx/types';
+import { GetStaticPaths, GetStaticProps } from 'next';
+import Head from 'next/head';
+import { useRouter } from 'next/router';
+import Script from 'next/script';
+import { ComponentType } from 'react';
+import { useIntl } from 'react-intl';
+
+type ProjectPageProps = {
+ project: ProjectPreview;
+ translation: Messages;
+};
+
+/**
+ * Project page.
+ */
+const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => {
+ const { id, intro, meta, title } = project;
+ const { cover, dates, license, repos, seo, technologies } = meta;
+ const intl = useIntl();
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: `/projets/${id}`,
+ });
+
+ const ProjectContent: ComponentType<MDXComponents> =
+ require(`../../content/projects/${id}.mdx`).default;
+
+ const components: NestedMDXComponents = {
+ Code: (props) => <Code {...props} />,
+ Gallery: (props) => <Gallery {...props} />,
+ Image: (props) => <ResponsiveImage withBorders={true} {...props} />,
+ Link: (props) => <Link {...props} />,
+ pre: ({ children }) => <Code {...children.props} />,
+ };
+
+ const { website } = useSettings();
+ const { asPath } = useRouter();
+ const pageUrl = `${website.url}${asPath}`;
+
+ const headerMeta: PageLayoutProps['headerMeta'] = {
+ publication: { date: dates.publication },
+ update:
+ dates.update && dates.update !== dates.publication
+ ? { date: dates.update }
+ : undefined,
+ };
+
+ /**
+ * Retrieve the repositories links.
+ *
+ * @param {Repos} repos - A repositories object.
+ * @returns {JSX.Element[]} - An array of SocialLink.
+ */
+ const getReposLinks = (repositories: Repos): JSX.Element[] => {
+ const links = [];
+
+ for (const [name, url] of Object.entries(repositories)) {
+ const socialWebsite = capitalize(name) as SocialWebsite;
+ const socialUrl = `https://${name}.com/${url}`;
+
+ links.push(<SocialLink name={socialWebsite} url={socialUrl} />);
+ }
+
+ return links;
+ };
+
+ const { isError, isLoading, data } = useGithubApi(meta.repos!.github!);
+
+ const getGithubData = (key: keyof RepoData) => {
+ if (isError) return 'Error';
+ if (isLoading || !data) return <Spinner />;
+
+ switch (key) {
+ case 'created_at':
+ return data.created_at;
+ case 'updated_at':
+ return data.updated_at;
+ case 'stargazers_count':
+ const stars = intl.formatMessage(
+ {
+ defaultMessage:
+ '{starsCount, plural, =0 {No stars on Github} one {# star on Github} other {# stars on Github}}',
+ id: 'Gnf1Si',
+ description: 'Projets: Github stars count',
+ },
+ { starsCount: data.stargazers_count }
+ );
+ return (
+ <>
+ ⭐&nbsp;
+ <Link href={`https://github.com/${repos!.github}/stargazers`}>
+ {stars}
+ </Link>
+ </>
+ );
+ }
+ };
+
+ const overviewData: OverviewMeta = {
+ creation: data && { date: getGithubData('created_at') as string },
+ update: data && { date: getGithubData('updated_at') as string },
+ license,
+ popularity: data && getGithubData('stargazers_count'),
+ repositories: repos ? getReposLinks(repos) : undefined,
+ technologies,
+ };
+
+ const webpageSchema = getWebPageSchema({
+ description: seo.description,
+ locale: website.locales.default,
+ slug: asPath,
+ title: seo.title,
+ updateDate: dates.update,
+ });
+ const articleSchema = getSinglePageSchema({
+ cover: `/projects/${id}.jpg`,
+ dates,
+ description: intro,
+ id: 'project',
+ kind: 'page',
+ locale: website.locales.default,
+ slug: asPath,
+ title,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema, articleSchema]);
+
+ return (
+ <>
+ <Head>
+ <title>{`${seo.title} - ${website.name}`}</title>
+ <meta name="description" content={seo.description} />
+ <meta property="og:url" content={`${pageUrl}`} />
+ <meta property="og:type" content="article" />
+ <meta property="og:title" content={title} />
+ <meta property="og:description" content={intro} />
+ </Head>
+ <Script
+ id="schema-project"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ />
+ <PageLayout
+ title={title}
+ intro={intro}
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ headerMeta={headerMeta}
+ withToC={true}
+ widgets={[
+ <Sharing
+ key="sharing-widget"
+ data={{ excerpt: intro, title, url: pageUrl }}
+ media={[
+ 'diaspora',
+ 'email',
+ 'facebook',
+ 'journal-du-hacker',
+ 'linkedin',
+ 'twitter',
+ ]}
+ className={styles.widget}
+ />,
+ ]}
+ >
+ <Overview cover={cover} meta={overviewData} />
+ <ProjectContent components={components} />
+ </PageLayout>
+ </>
+ );
+};
+
+ProjectPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
+
+export const getStaticProps: GetStaticProps<ProjectPageProps> = async ({
+ locale,
+ params,
+}) => {
+ const translation = await loadTranslation(locale);
+ const { slug } = params!;
+ const project = await getProjectData(slug as string);
+
+ return {
+ props: {
+ project,
+ translation,
+ },
+ };
+};
+
+export const getStaticPaths: GetStaticPaths = async () => {
+ const filenames = getProjectFilenames();
+ const paths = filenames.map((filename) => {
+ return {
+ params: {
+ slug: filename,
+ },
+ };
+ });
+
+ return {
+ paths,
+ fallback: false,
+ };
+};
+
+export default ProjectPage;
diff --git a/src/pages/projets/index.tsx b/src/pages/projets/index.tsx
new file mode 100644
index 0000000..dbca019
--- /dev/null
+++ b/src/pages/projets/index.tsx
@@ -0,0 +1,123 @@
+import Link from '@components/atoms/links/link';
+import CardsList, {
+ type CardsListItem,
+} from '@components/organisms/layout/cards-list';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout from '@components/templates/page/page-layout';
+import PageContent, { meta } from '@content/pages/projects.mdx';
+import styles from '@styles/pages/projects.module.scss';
+import { type NextPageWithLayout, type ProjectCard } from '@ts/types/app';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import { getProjectsCard } from '@utils/helpers/projects';
+import {
+ getSchemaJson,
+ getSinglePageSchema,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useSettings from '@utils/hooks/use-settings';
+import { NestedMDXComponents } from 'mdx/types';
+import { GetStaticProps } from 'next';
+import Head from 'next/head';
+import { useRouter } from 'next/router';
+import Script from 'next/script';
+
+type ProjectsPageProps = {
+ projects: ProjectCard[];
+ translation?: Messages;
+};
+
+/**
+ * Projects page.
+ */
+const ProjectsPage: NextPageWithLayout<ProjectsPageProps> = ({ projects }) => {
+ const { dates, seo, title } = meta;
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: `/projets`,
+ });
+
+ const items: CardsListItem[] = projects.map(
+ ({ id, meta: projectMeta, slug, title: projectTitle }) => {
+ const { cover, tagline, technologies } = projectMeta;
+
+ return {
+ cover,
+ id: id as string,
+ meta: { technologies: technologies },
+ tagline,
+ title: projectTitle,
+ url: `/projets/${slug}`,
+ };
+ }
+ );
+
+ const components: NestedMDXComponents = {
+ Links: (props) => <Link {...props} />,
+ };
+
+ const { website } = useSettings();
+ const { asPath } = useRouter();
+ const webpageSchema = getWebPageSchema({
+ description: seo.description,
+ locale: website.locales.default,
+ slug: asPath,
+ title: seo.title,
+ updateDate: dates.update,
+ });
+ const articleSchema = getSinglePageSchema({
+ dates,
+ description: seo.description,
+ id: 'projects',
+ kind: 'page',
+ locale: website.locales.default,
+ slug: asPath,
+ title,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema, articleSchema]);
+
+ return (
+ <>
+ <Head>
+ <title>{`${seo.title} - ${website.name}`}</title>
+ <meta name="description" content={seo.description} />
+ <meta property="og:url" content={`${website.url}${asPath}`} />
+ <meta property="og:type" content="article" />
+ <meta property="og:title" content={`${seo.title} - ${website.name}`} />
+ <meta property="og:description" content={seo.description} />
+ </Head>
+ <Script
+ id="schema-projects"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ />
+ <PageLayout
+ title={title}
+ intro={<PageContent components={components} />}
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ >
+ <CardsList items={items} titleLevel={2} className={styles.list} />
+ </PageLayout>
+ </>
+ );
+};
+
+ProjectsPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
+
+export const getStaticProps: GetStaticProps<ProjectsPageProps> = async ({
+ locale,
+}) => {
+ const projects = await getProjectsCard();
+ const translation = await loadTranslation(locale);
+
+ return {
+ props: {
+ projects: JSON.parse(JSON.stringify(projects)),
+ translation,
+ },
+ };
+};
+
+export default ProjectsPage;
diff --git a/src/pages/recherche/index.tsx b/src/pages/recherche/index.tsx
index b843f8d..dbbec55 100644
--- a/src/pages/recherche/index.tsx
+++ b/src/pages/recherche/index.tsx
@@ -1,213 +1,235 @@
-import { Button } from '@components/Buttons';
-import { getLayout } from '@components/Layouts/Layout';
-import PaginationCursor from '@components/PaginationCursor/PaginationCursor';
-import PostHeader from '@components/PostHeader/PostHeader';
-import PostsList from '@components/PostsList/PostsList';
-import Sidebar from '@components/Sidebar/Sidebar';
-import Spinner from '@components/Spinner/Spinner';
-import { ThematicsList, TopicsList } from '@components/Widgets';
-import { getPublishedPosts } from '@services/graphql/queries';
-import styles from '@styles/pages/Page.module.scss';
-import { NextPageWithLayout } from '@ts/types/app';
-import { PostsList as PostsListData } from '@ts/types/blog';
-import { settings } from '@utils/config';
-import { getIntlInstance, loadTranslation } from '@utils/helpers/i18n';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
+import Notice from '@components/atoms/layout/notice';
+import Spinner from '@components/atoms/loaders/spinner';
+import PostsList from '@components/organisms/layout/posts-list';
+import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout from '@components/templates/page/page-layout';
+import { getArticles, getTotalArticles } from '@services/graphql/articles';
+import {
+ getThematicsPreview,
+ getTotalThematics,
+} from '@services/graphql/thematics';
+import { getTopicsPreview, getTotalTopics } from '@services/graphql/topics';
+import { type NextPageWithLayout } from '@ts/types/app';
+import {
+ type RawArticle,
+ type RawThematicPreview,
+ type RawTopicPreview,
+} from '@ts/types/raw-data';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import {
+ getLinksListItems,
+ getPageLinkFromRawData,
+ getPostsList,
+} from '@utils/helpers/pages';
+import {
+ getBlogSchema,
+ getSchemaJson,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useDataFromAPI from '@utils/hooks/use-data-from-api';
+import usePagination from '@utils/hooks/use-pagination';
+import useSettings from '@utils/hooks/use-settings';
+import { GetStaticProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
-import { useEffect, useRef, useState } from 'react';
+import Script from 'next/script';
import { useIntl } from 'react-intl';
-import useSWRInfinite from 'swr/infinite';
-const Search: NextPageWithLayout = () => {
- const intl = useIntl();
- const [query, setQuery] = useState('');
- const router = useRouter();
- const lastPostRef = useRef<HTMLSpanElement>(null);
-
- useEffect(() => {
- if (!router.isReady) return;
-
- if (router.query?.s && typeof router.query.s === 'string') {
- setQuery(router.query.s);
- }
- }, [router.isReady, router.query.s]);
-
- const getKey = (pageIndex: number, previousData: PostsListData) => {
- if (previousData && !previousData.posts) return null;
-
- return pageIndex === 0
- ? { first: settings.postsPerPage, searchQuery: query }
- : {
- first: settings.postsPerPage,
- after: previousData.pageInfo.endCursor,
- searchQuery: query,
- };
- };
-
- const { data, error, size, setSize } = useSWRInfinite(
- getKey,
- getPublishedPosts
- );
- const [totalPostsCount, setTotalPostsCount] = useState<number>(0);
-
- useEffect(() => {
- if (data) setTotalPostsCount(data[0].pageInfo.total);
- }, [data]);
-
- const [loadedPostsCount, setLoadedPostsCount] = useState<number>(
- settings.postsPerPage
- );
-
- useEffect(() => {
- if (data && data.length > 0) {
- const newCount =
- settings.postsPerPage +
- data[0].pageInfo.total -
- data[data.length - 1].pageInfo.total;
- setLoadedPostsCount(newCount);
- }
- }, [data]);
-
- const isLoadingInitialData = !data && !error;
- const isLoadingMore: boolean =
- isLoadingInitialData ||
- (size > 0 && data !== undefined && typeof data[size - 1] === 'undefined');
-
- const hasNextPage = data && data[data.length - 1].pageInfo.hasNextPage;
+type SearchPageProps = {
+ thematicsList: RawThematicPreview[];
+ topicsList: RawTopicPreview[];
+ translation: Messages;
+};
- const title = query
+/**
+ * Search page.
+ */
+const SearchPage: NextPageWithLayout<SearchPageProps> = ({
+ thematicsList,
+ topicsList,
+}) => {
+ const intl = useIntl();
+ const { asPath, query } = useRouter();
+ const title = query.s
? intl.formatMessage(
{
defaultMessage: 'Search results for {query}',
- description: 'SearchPage: search results text',
- id: 'VSGuGE',
+ description: 'SearchPage: SEO - Page title',
+ id: 'ZNBhDP',
},
- { query }
+ { query: query.s as string }
)
: intl.formatMessage({
defaultMessage: 'Search',
- description: 'SearchPage: page title',
- id: 'U+35YD',
+ description: 'SearchPage: SEO - Page title',
+ id: 'WDwNDl',
});
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: `/recherche`,
+ });
- const description = query
+ const { blog, website } = useSettings();
+ const pageTitle = `${title} - ${website.name}`;
+ const pageDescription = query.s
? intl.formatMessage(
{
- defaultMessage: 'Discover search results for {query}',
- description: 'SearchPage: meta description with query',
- id: 'A4LTGq',
+ defaultMessage:
+ 'Discover search results for {query} on {websiteName}.',
+ description: 'SearchPage: SEO - Meta description',
+ id: 'pg26sn',
},
- { query }
+ { query: query.s as string, websiteName: website.name }
)
: intl.formatMessage(
{
- defaultMessage: 'Search for a post on {websiteName}',
- description: 'SearchPage: meta description without query',
- id: 'PrIz5o',
+ defaultMessage: 'Search for a post on {websiteName}.',
+ description: 'SearchPage: SEO - Meta description',
+ id: 'npisb3',
},
- { websiteName: settings.name }
+ { websiteName: website.name }
);
+ const webpageSchema = getWebPageSchema({
+ description: pageDescription,
+ locale: website.locales.default,
+ slug: asPath,
+ title: pageTitle,
+ });
+ const blogSchema = getBlogSchema({
+ isSinglePage: false,
+ locale: website.locales.default,
+ slug: asPath,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema, blogSchema]);
+
+ const {
+ data,
+ error,
+ isLoadingInitialData,
+ isLoadingMore,
+ hasNextPage,
+ setSize,
+ } = usePagination<RawArticle>({
+ fallbackData: [],
+ fetcher: getArticles,
+ perPage: blog.postsPerPage,
+ search: query.s as string,
+ });
- const head = {
- title: `${title} | ${settings.name}`,
- description,
- };
+ const totalArticles = useDataFromAPI<number>(() =>
+ getTotalArticles(query.s as string)
+ );
- const loadMorePosts = () => {
- if (lastPostRef.current) {
- lastPostRef.current.focus();
- }
- setSize(size + 1);
+ /**
+ * Load more posts handler.
+ */
+ const loadMore = () => {
+ setSize((prevSize) => prevSize + 1);
};
- const getPostsList = () => {
- if (error)
- return intl.formatMessage({
- defaultMessage: 'Failed to load.',
- description: 'SearchPage: failed to load text',
- id: 'fOe8rH',
- });
- if (!data) return <Spinner />;
+ const thematicsListTitle = intl.formatMessage({
+ defaultMessage: 'Thematics',
+ description: 'SearchPage: thematics list widget title',
+ id: 'Dq6+WH',
+ });
- return <PostsList ref={lastPostRef} data={data} showYears={false} />;
- };
+ const topicsListTitle = intl.formatMessage({
+ defaultMessage: 'Topics',
+ description: 'SearchPage: topics list widget title',
+ id: 'N804XO',
+ });
return (
<>
<Head>
- <title>{head.title}</title>
- <meta name="description" content={head.description} />
+ <title>{pageTitle}</title>
+ <meta name="description" content={pageDescription} />
+ <meta property="og:url" content={`${website.url}${asPath}`} />
+ <meta property="og:type" content="website" />
+ <meta property="og:title" content={title} />
+ <meta property="og:description" content={pageDescription} />
</Head>
- <article
- className={`${styles.article} ${styles['article--no-comments']}`}
+ <Script
+ id="schema-blog"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ />
+ <PageLayout
+ title={title}
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ headerMeta={{ total: totalArticles }}
+ widgets={[
+ <LinksListWidget
+ key="thematics-list"
+ items={getLinksListItems(
+ thematicsList.map((thematic) =>
+ getPageLinkFromRawData(thematic, 'thematic')
+ )
+ )}
+ title={thematicsListTitle}
+ level={2}
+ />,
+ <LinksListWidget
+ key="topics-list"
+ items={getLinksListItems(
+ topicsList.map((topic) => getPageLinkFromRawData(topic, 'topic'))
+ )}
+ title={topicsListTitle}
+ level={2}
+ />,
+ ]}
>
- <PostHeader title={title} meta={{ results: totalPostsCount }} />
- <div className={styles.body}>
- {getPostsList()}
- {hasNextPage && (
- <>
- <PaginationCursor
- current={loadedPostsCount}
- total={totalPostsCount}
- />
- <Button
- isDisabled={isLoadingMore}
- clickHandler={loadMorePosts}
- position="center"
- spacing={true}
- >
- {intl.formatMessage({
- defaultMessage: 'Load more?',
- description: 'SearchPage: load more text',
- id: 'pEtJik',
- })}
- </Button>
- </>
- )}
- </div>
- <Sidebar position="right">
- <ThematicsList
- title={intl.formatMessage({
- defaultMessage: 'Thematics',
- description: 'SearchPage: thematics list widget title',
- id: 'Dq6+WH',
- })}
+ {data && data.length > 0 ? (
+ <PostsList
+ baseUrl="/recherche/page/"
+ byYear={true}
+ isLoading={isLoadingMore || isLoadingInitialData}
+ loadMore={loadMore}
+ posts={getPostsList(data)}
+ searchPage="/recherche/"
+ showLoadMoreBtn={hasNextPage}
+ total={totalArticles || 0}
/>
- <TopicsList
- title={intl.formatMessage({
- defaultMessage: 'Topics',
- description: 'SearchPage: topics list widget title',
- id: 'N804XO',
+ ) : (
+ <Spinner />
+ )}
+ {error && (
+ <Notice
+ kind="error"
+ message={intl.formatMessage({
+ defaultMessage: 'Failed to load.',
+ description: 'SearchPage: failed to load text',
+ id: 'fOe8rH',
})}
/>
- </Sidebar>
- </article>
+ )}
+ </PageLayout>
</>
);
};
-Search.getLayout = getLayout;
+SearchPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const intl = await getIntlInstance();
- const breadcrumbTitle = intl.formatMessage({
- defaultMessage: 'Search',
- description: 'SearchPage: breadcrumb item',
- id: 'TfU6Qm',
- });
- const { locale } = context;
+export const getStaticProps: GetStaticProps<SearchPageProps> = async ({
+ locale,
+}) => {
+ const totalThematics = await getTotalThematics();
+ const thematics = await getThematicsPreview({ first: totalThematics });
+ const totalTopics = await getTotalTopics();
+ const topics = await getTopicsPreview({ first: totalTopics });
const translation = await loadTranslation(locale);
return {
props: {
- breadcrumbTitle,
- locale,
+ thematicsList: thematics.edges.map((edge) => edge.node),
+ topicsList: topics.edges.map((edge) => edge.node),
translation,
},
};
};
-export default Search;
+export default SearchPage;
diff --git a/src/pages/sujet/[slug].tsx b/src/pages/sujet/[slug].tsx
index 30dd36c..48924e5 100644
--- a/src/pages/sujet/[slug].tsx
+++ b/src/pages/sujet/[slug].tsx
@@ -1,224 +1,230 @@
-import { getLayout } from '@components/Layouts/Layout';
-import PostHeader from '@components/PostHeader/PostHeader';
-import PostPreview from '@components/PostPreview/PostPreview';
-import Sidebar from '@components/Sidebar/Sidebar';
-import Spinner from '@components/Spinner/Spinner';
-import { RelatedThematics, ToC, TopicsList } from '@components/Widgets';
+import Heading from '@components/atoms/headings/heading';
+import ResponsiveImage from '@components/molecules/images/responsive-image';
+import PostsList from '@components/organisms/layout/posts-list';
+import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout, {
+ type PageLayoutProps,
+} from '@components/templates/page/page-layout';
import {
- getAllTopics,
- getAllTopicsSlug,
+ getAllTopicsSlugs,
getTopicBySlug,
-} from '@services/graphql/queries';
-import styles from '@styles/pages/Page.module.scss';
-import { NextPageWithLayout } from '@ts/types/app';
-import { ArticleMeta } from '@ts/types/articles';
-import { TopicProps, ThematicPreview } from '@ts/types/taxonomies';
-import { settings } from '@utils/config';
-import { getFormattedPaths } from '@utils/helpers/format';
-import { loadTranslation } from '@utils/helpers/i18n';
-import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
+ getTopicsPreview,
+ getTotalTopics,
+} from '@services/graphql/topics';
+import styles from '@styles/pages/topic.module.scss';
+import {
+ type NextPageWithLayout,
+ type PageLink,
+ type Topic,
+} from '@ts/types/app';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import {
+ getLinksListItems,
+ getPageLinkFromRawData,
+ getPostsWithUrl,
+} from '@utils/helpers/pages';
+import {
+ getSchemaJson,
+ getSinglePageSchema,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useSettings from '@utils/hooks/use-settings';
+import { GetStaticPaths, GetStaticProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Script from 'next/script';
import { ParsedUrlQuery } from 'querystring';
-import { useRef } from 'react';
import { useIntl } from 'react-intl';
-import { Article as Article, Graph, WebPage } from 'schema-dts';
-
-const Topic: NextPageWithLayout<TopicProps> = ({ topic, allTopics }) => {
- const intl = useIntl();
- const relatedThematics = useRef<ThematicPreview[]>([]);
- const router = useRouter();
-
- if (router.isFallback) return <Spinner />;
-
- const updateRelatedThematics = (newThematics: ThematicPreview[]) => {
- newThematics.forEach((thematic) => {
- const thematicIndex = relatedThematics.current.findIndex(
- (relatedThematic) => relatedThematic.id === thematic.id
- );
- const hasThematic = thematicIndex === -1 ? false : true;
-
- if (!hasThematic) relatedThematics.current.push(thematic);
- });
- };
-
- const getPostsList = () => {
- return [...topic.posts].reverse().map((post) => {
- updateRelatedThematics(post.thematics);
-
- return (
- <li key={post.id} className={styles.item}>
- <PostPreview post={post} titleLevel={3} />
- </li>
- );
- });
- };
-
- const meta: ArticleMeta = {
- dates: topic.dates,
- results: topic.posts.length,
- website: topic.officialWebsite,
- };
- const topicUrl = `${settings.url}${router.asPath}`;
-
- const webpageSchema: WebPage = {
- '@id': `${topicUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- name: topic.seo.title,
- description: topic.seo.metaDesc,
- inLanguage: settings.locales.defaultLocale,
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${settings.url}`,
- isPartOf: {
- '@id': `${settings.url}`,
- },
- };
- const publicationDate = new Date(topic.dates.publication);
- const updateDate = new Date(topic.dates.update);
+export type TopicPageProps = {
+ currentTopic: Topic;
+ topics: PageLink[];
+ translation: Messages;
+};
- const articleSchema: Article = {
- '@id': `${settings.url}/#topic`,
- '@type': 'Article',
- name: topic.title,
- description: topic.intro,
- author: { '@id': `${settings.url}/#branding` },
- copyrightYear: publicationDate.getFullYear(),
- creator: { '@id': `${settings.url}/#branding` },
- dateCreated: publicationDate.toISOString(),
- dateModified: updateDate.toISOString(),
- datePublished: publicationDate.toISOString(),
- editor: { '@id': `${settings.url}/#branding` },
- headline: topic.title,
- thumbnailUrl: topic.featuredImage?.sourceUrl,
- image: topic.featuredImage?.sourceUrl,
- inLanguage: settings.locales.defaultLocale,
- isPartOf: { '@id': `${settings.url}/blog` },
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: { '@id': `${topicUrl}` },
- subjectOf: { '@id': `${settings.url}/blog` },
+const TopicPage: NextPageWithLayout<TopicPageProps> = ({
+ currentTopic,
+ topics,
+}) => {
+ const { content, intro, meta, slug, title } = currentTopic;
+ const {
+ articles,
+ cover,
+ dates,
+ seo,
+ thematics,
+ website: officialWebsite,
+ } = meta;
+ const intl = useIntl();
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: `/sujet/${slug}`,
+ });
+
+ const headerMeta: PageLayoutProps['headerMeta'] = {
+ publication: { date: dates.publication },
+ update: dates.update ? { date: dates.update } : undefined,
+ website: officialWebsite,
+ total: articles ? articles.length : undefined,
};
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, articleSchema],
+ const { website } = useSettings();
+ const { asPath } = useRouter();
+ const webpageSchema = getWebPageSchema({
+ description: seo.description,
+ locale: website.locales.default,
+ slug: asPath,
+ title: seo.title,
+ updateDate: dates.update,
+ });
+ const articleSchema = getSinglePageSchema({
+ cover: cover?.src,
+ dates,
+ description: intro,
+ id: 'topic',
+ kind: 'page',
+ locale: website.locales.default,
+ slug: asPath,
+ title,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema, articleSchema]);
+
+ const topicsListTitle = intl.formatMessage({
+ defaultMessage: 'Other topics',
+ description: 'TopicPage: other topics list widget title',
+ id: 'JpC3JH',
+ });
+
+ const thematicsListTitle = intl.formatMessage({
+ defaultMessage: 'Related thematics',
+ description: 'TopicPage: related thematics list widget title',
+ id: '/sRqPT',
+ });
+
+ const getPageHeading = () => {
+ return (
+ <>
+ {cover && <ResponsiveImage className={styles.logo} {...cover} />}
+ {title}
+ </>
+ );
};
return (
<>
<Head>
- <title>{topic.seo.title}</title>
- <meta name="description" content={topic.seo.metaDesc} />
- <meta property="og:url" content={`${topicUrl}`} />
+ <title>{seo.title}</title>
+ <meta name="description" content={seo.description} />
+ <meta property="og:url" content={`${website.url}${asPath}`} />
<meta property="og:type" content="article" />
- <meta property="og:title" content={topic.title} />
- <meta property="og:description" content={topic.intro} />
- <meta property="og:image" content={topic.featuredImage?.sourceUrl} />
- <meta property="og:image:alt" content={topic.featuredImage?.altText} />
+ <meta property="og:title" content={title} />
+ <meta property="og:description" content={intro} />
</Head>
<Script
- id="schema-subject"
+ id="schema-project"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
/>
- <article
- id="topic"
- className={`${styles.article} ${styles['article--no-comments']}`}
+ <PageLayout
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ title={getPageHeading()}
+ intro={intro}
+ headerMeta={headerMeta}
+ widgets={
+ thematics
+ ? [
+ <LinksListWidget
+ key="related-thematics"
+ items={getLinksListItems(thematics)}
+ title={thematicsListTitle}
+ level={2}
+ />,
+ <LinksListWidget
+ key="topics"
+ items={getLinksListItems(topics)}
+ title={topicsListTitle}
+ level={2}
+ />,
+ ]
+ : []
+ }
>
- <PostHeader
- cover={topic.featuredImage}
- intro={topic.intro}
- meta={meta}
- title={topic.title}
- />
- <Sidebar
- position="left"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Table of Contents',
- description: 'TopicPage: ToC sidebar aria-label',
- id: 'lsDB5G',
- })}
- >
- <ToC />
- </Sidebar>
- <div className={styles.body}>
- <div dangerouslySetInnerHTML={{ __html: topic.content }}></div>
- {topic.posts.length > 0 && (
- <section className={styles.section}>
- <h2>
- {intl.formatMessage(
- {
- defaultMessage: 'All posts in {name}',
- description: 'TopicPage: posts list title',
- id: 'FLkF2R',
- },
- { name: topic.title }
- )}
- </h2>
- <ol className={styles.list}>{getPostsList()}</ol>
- </section>
- )}
- </div>
- <Sidebar
- position="right"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Sidebar',
- description: 'TopicPage: right sidebar aria-label',
- id: 'eu3beS',
- })}
- >
- <RelatedThematics thematics={relatedThematics.current} />
- <TopicsList
- initialData={allTopics}
- title={intl.formatMessage({
- defaultMessage: 'Others topics',
- description: 'TopicPage: topics list widget title',
- id: '+4tiVb',
- })}
- />
- </Sidebar>
- </article>
+ {content && <div dangerouslySetInnerHTML={{ __html: content }} />}
+ {articles && (
+ <>
+ <Heading level={2}>
+ {intl.formatMessage(
+ {
+ defaultMessage: 'All posts in {topicName}',
+ description: 'TopicPage: posts list heading',
+ id: 'zEN3fd',
+ },
+ { topicName: title }
+ )}
+ </Heading>
+ <PostsList
+ baseUrl="/sujet/page/"
+ byYear={true}
+ posts={getPostsWithUrl(articles)}
+ searchPage="/recherche/"
+ titleLevel={3}
+ total={articles.length}
+ />
+ </>
+ )}
+ </PageLayout>
</>
);
};
-Topic.getLayout = getLayout;
+TopicPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
-interface PostParams extends ParsedUrlQuery {
+interface TopicParams extends ParsedUrlQuery {
slug: string;
}
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const { locale } = context;
+export const getStaticProps: GetStaticProps<TopicPageProps> = async ({
+ locale,
+ params,
+}) => {
+ const currentTopic = await getTopicBySlug(
+ params!.slug as TopicParams['slug']
+ );
+ const totalTopics = await getTotalTopics();
+ const allTopicsEdges = await getTopicsPreview({
+ first: totalTopics,
+ });
+ const allTopics = allTopicsEdges.edges.map((edge) =>
+ getPageLinkFromRawData(edge.node, 'topic')
+ );
+ const topicsLinks = allTopics.filter(
+ (topic) => topic.url !== `/sujet/${params!.slug as TopicParams['slug']}`
+ );
const translation = await loadTranslation(locale);
- const { slug } = context.params as PostParams;
- const topic = await getTopicBySlug(slug);
- const allTopics = await getAllTopics();
- const breadcrumbTitle = topic.title;
return {
props: {
- allTopics,
- breadcrumbTitle,
- locale,
- topic,
+ currentTopic: JSON.parse(JSON.stringify(currentTopic)),
+ topics: JSON.parse(JSON.stringify(topicsLinks)),
translation,
},
};
};
export const getStaticPaths: GetStaticPaths = async () => {
- const allTopics = await getAllTopicsSlug();
- const paths = getFormattedPaths(allTopics);
+ const slugs = await getAllTopicsSlugs();
+ const paths = slugs.map((slug) => {
+ return { params: { slug } };
+ });
return {
paths,
- fallback: true,
+ fallback: false,
};
};
-export default Topic;
+export default TopicPage;
diff --git a/src/pages/thematique/[slug].tsx b/src/pages/thematique/[slug].tsx
index db22214..7aa6c1c 100644
--- a/src/pages/thematique/[slug].tsx
+++ b/src/pages/thematique/[slug].tsx
@@ -1,214 +1,211 @@
-import { getLayout } from '@components/Layouts/Layout';
-import PostHeader from '@components/PostHeader/PostHeader';
-import PostPreview from '@components/PostPreview/PostPreview';
-import Sidebar from '@components/Sidebar/Sidebar';
-import Spinner from '@components/Spinner/Spinner';
-import { RelatedTopics, ThematicsList, ToC } from '@components/Widgets';
+import Heading from '@components/atoms/headings/heading';
+import PostsList from '@components/organisms/layout/posts-list';
+import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout, {
+ type PageLayoutProps,
+} from '@components/templates/page/page-layout';
import {
- getAllThematics,
- getAllThematicsSlug,
+ getAllThematicsSlugs,
getThematicBySlug,
-} from '@services/graphql/queries';
-import styles from '@styles/pages/Page.module.scss';
-import { NextPageWithLayout } from '@ts/types/app';
-import { ArticleMeta } from '@ts/types/articles';
-import { TopicPreview, ThematicProps } from '@ts/types/taxonomies';
-import { settings } from '@utils/config';
-import { getFormattedPaths } from '@utils/helpers/format';
-import { loadTranslation } from '@utils/helpers/i18n';
-import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
+ getThematicsPreview,
+ getTotalThematics,
+} from '@services/graphql/thematics';
+import {
+ type NextPageWithLayout,
+ type PageLink,
+ type Thematic,
+} from '@ts/types/app';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import {
+ getLinksListItems,
+ getPageLinkFromRawData,
+ getPostsWithUrl,
+} from '@utils/helpers/pages';
+import {
+ getSchemaJson,
+ getSinglePageSchema,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useSettings from '@utils/hooks/use-settings';
+import { GetStaticPaths, GetStaticProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Script from 'next/script';
import { ParsedUrlQuery } from 'querystring';
-import { useRef } from 'react';
import { useIntl } from 'react-intl';
-import { Article, Graph, WebPage } from 'schema-dts';
-const Thematic: NextPageWithLayout<ThematicProps> = ({
- thematic,
- allThematics,
+export type ThematicPageProps = {
+ currentThematic: Thematic;
+ thematics: PageLink[];
+ translation: Messages;
+};
+
+const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({
+ currentThematic,
+ thematics,
}) => {
+ const { content, intro, meta, slug, title } = currentThematic;
+ const { articles, dates, seo, topics } = meta;
const intl = useIntl();
- const relatedTopics = useRef<TopicPreview[]>([]);
- const router = useRouter();
-
- if (router.isFallback) return <Spinner />;
-
- const updateRelatedTopics = (newTopics: TopicPreview[]) => {
- newTopics.forEach((topic) => {
- const topicIndex = relatedTopics.current.findIndex(
- (relatedTopic) => relatedTopic.id === topic.id
- );
- const hasTopic = topicIndex === -1 ? false : true;
-
- if (!hasTopic) relatedTopics.current.push(topic);
- });
- };
-
- const getPostsList = () => {
- return [...thematic.posts].reverse().map((post) => {
- updateRelatedTopics(post.topics);
-
- return (
- <li key={post.id} className={styles.item}>
- <PostPreview post={post} titleLevel={3} />
- </li>
- );
- });
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: `/thematique/${slug}`,
+ });
+
+ const headerMeta: PageLayoutProps['headerMeta'] = {
+ publication: { date: dates.publication },
+ update: dates.update ? { date: dates.update } : undefined,
+ total: articles ? articles.length : undefined,
};
- const meta: ArticleMeta = {
- dates: thematic.dates,
- results: thematic.posts.length,
- };
- const thematicUrl = `${settings.url}${router.asPath}`;
-
- const webpageSchema: WebPage = {
- '@id': `${thematicUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- name: thematic.seo.title,
- description: thematic.seo.metaDesc,
- inLanguage: settings.locales.defaultLocale,
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${settings.url}`,
- };
-
- const publicationDate = new Date(thematic.dates.publication);
- const updateDate = new Date(thematic.dates.update);
-
- const articleSchema: Article = {
- '@id': `${settings.url}/#thematic`,
- '@type': 'Article',
- name: thematic.title,
- description: thematic.intro,
- author: { '@id': `${settings.url}/#branding` },
- copyrightYear: publicationDate.getFullYear(),
- creator: { '@id': `${settings.url}/#branding` },
- dateCreated: publicationDate.toISOString(),
- dateModified: updateDate.toISOString(),
- datePublished: publicationDate.toISOString(),
- editor: { '@id': `${settings.url}/#branding` },
- headline: thematic.title,
- inLanguage: settings.locales.defaultLocale,
- isPartOf: { '@id': `${settings.url}/blog` },
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: { '@id': `${thematicUrl}` },
- subjectOf: { '@id': `${settings.url}/blog` },
- };
-
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, articleSchema],
- };
+ const { website } = useSettings();
+ const { asPath } = useRouter();
+ const webpageSchema = getWebPageSchema({
+ description: seo.description,
+ locale: website.locales.default,
+ slug: asPath,
+ title: seo.title,
+ updateDate: dates.update,
+ });
+ const articleSchema = getSinglePageSchema({
+ dates,
+ description: intro,
+ id: 'thematic',
+ kind: 'page',
+ locale: website.locales.default,
+ slug: asPath,
+ title,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema, articleSchema]);
+
+ const thematicsListTitle = intl.formatMessage({
+ defaultMessage: 'Other thematics',
+ description: 'ThematicPage: other thematics list widget title',
+ id: 'KVSWGP',
+ });
+
+ const topicsListTitle = intl.formatMessage({
+ defaultMessage: 'Related topics',
+ description: 'ThematicPage: related topics list widget title',
+ id: '/42Z0z',
+ });
return (
<>
<Head>
- <title>{thematic.seo.title}</title>
- <meta name="description" content={thematic.seo.metaDesc} />
- <meta property="og:url" content={`${thematic}`} />
+ <title>{seo.title}</title>
+ <meta name="description" content={seo.description} />
+ <meta property="og:url" content={`${website.url}${asPath}`} />
<meta property="og:type" content="article" />
- <meta property="og:title" content={thematic.title} />
- <meta property="og:description" content={thematic.intro} />
+ <meta property="og:title" content={title} />
+ <meta property="og:description" content={intro} />
</Head>
<Script
- id="schema-thematic"
+ id="schema-project"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
/>
- <article
- id="thematic"
- className={`${styles.article} ${styles['article--no-comments']}`}
+ <PageLayout
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ title={title}
+ intro={intro}
+ headerMeta={headerMeta}
+ widgets={
+ topics
+ ? [
+ <LinksListWidget
+ key="thematics"
+ items={getLinksListItems(thematics)}
+ title={thematicsListTitle}
+ level={2}
+ />,
+ <LinksListWidget
+ key="related-topics"
+ items={getLinksListItems(topics)}
+ title={topicsListTitle}
+ level={2}
+ />,
+ ]
+ : []
+ }
>
- <PostHeader intro={thematic.intro} meta={meta} title={thematic.title} />
- <Sidebar
- position="left"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Table of Contents',
- description: 'ThematicPage: ToC sidebar aria-label',
- id: 'YwvYfw',
- })}
- >
- <ToC />
- </Sidebar>
- <div className={styles.body}>
- <div dangerouslySetInnerHTML={{ __html: thematic.content }}></div>
- {thematic.posts.length > 0 && (
- <section className={styles.section}>
- <h2>
- {intl.formatMessage(
- {
- defaultMessage: 'All posts in {name}',
- description: 'ThematicPage: posts list title',
- id: 'P7fxX2',
- },
- { name: thematic.title }
- )}
- </h2>
- <ol className={styles.list}>{getPostsList()}</ol>
- </section>
- )}
- </div>
- <Sidebar
- position="right"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Sidebar',
- description: 'ThematicPage: right sidebar aria-label',
- id: 'syLgY9',
- })}
- >
- <RelatedTopics topics={relatedTopics.current} />
- <ThematicsList
- initialData={allThematics}
- title={intl.formatMessage({
- defaultMessage: 'Others thematics',
- description: 'ThematicPage: thematics list widget title',
- id: 'norrGp',
- })}
- />
- </Sidebar>
- </article>
+ <div dangerouslySetInnerHTML={{ __html: content }} />
+ {articles && (
+ <>
+ <Heading level={2}>
+ {intl.formatMessage(
+ {
+ defaultMessage: 'All posts in {thematicName}',
+ description: 'ThematicPage: posts list heading',
+ id: 'LszkU6',
+ },
+ { thematicName: title }
+ )}
+ </Heading>
+ <PostsList
+ baseUrl="/thematique/page/"
+ byYear={true}
+ posts={getPostsWithUrl(articles)}
+ searchPage="/recherche/"
+ titleLevel={3}
+ total={articles.length}
+ />
+ </>
+ )}
+ </PageLayout>
</>
);
};
-Thematic.getLayout = getLayout;
+ThematicPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
-interface PostParams extends ParsedUrlQuery {
+interface ThematicParams extends ParsedUrlQuery {
slug: string;
}
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const { locale } = context;
+export const getStaticProps: GetStaticProps<ThematicPageProps> = async ({
+ locale,
+ params,
+}) => {
+ const currentThematic = await getThematicBySlug(
+ params!.slug as ThematicParams['slug']
+ );
+ const totalThematics = await getTotalThematics();
+ const allThematicsEdges = await getThematicsPreview({
+ first: totalThematics,
+ });
+ const allThematics = allThematicsEdges.edges.map((edge) =>
+ getPageLinkFromRawData(edge.node, 'thematic')
+ );
+ const allThematicsLinks = allThematics.filter(
+ (thematic) =>
+ thematic.url !== `/thematique/${params!.slug as ThematicParams['slug']}`
+ );
const translation = await loadTranslation(locale);
- const { slug } = context.params as PostParams;
- const thematic = await getThematicBySlug(slug);
- const allThematics = await getAllThematics();
- const breadcrumbTitle = thematic.title;
return {
props: {
- allThematics,
- breadcrumbTitle,
- locale,
- thematic,
+ currentThematic: JSON.parse(JSON.stringify(currentThematic)),
+ thematics: JSON.parse(JSON.stringify(allThematicsLinks)),
translation,
},
};
};
export const getStaticPaths: GetStaticPaths = async () => {
- const allSlugs = await getAllThematicsSlug();
- const paths = getFormattedPaths(allSlugs);
+ const slugs = await getAllThematicsSlugs();
+ const paths = slugs.map((slug) => {
+ return { params: { slug } };
+ });
return {
paths,
- fallback: true,
+ fallback: false,
};
};
-export default Thematic;
+export default ThematicPage;