aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/components/templates/page/page.module.scss9
-rw-r--r--src/i18n/en.json30
-rw-r--r--src/i18n/fr.json30
-rw-r--r--src/pages/404.tsx194
-rw-r--r--src/styles/pages/blog.module.scss2
-rw-r--r--tests/cypress/e2e/pages/404.cy.ts31
6 files changed, 193 insertions, 103 deletions
diff --git a/src/components/templates/page/page.module.scss b/src/components/templates/page/page.module.scss
index e7d3587..91a1b58 100644
--- a/src/components/templates/page/page.module.scss
+++ b/src/components/templates/page/page.module.scss
@@ -78,9 +78,8 @@
padding-bottom: var(--spacing-md);
}
-.body > * + * {
- margin-top: var(--spacing-sm);
- margin-bottom: var(--spacing-sm);
+.body > * {
+ margin-block: var(--spacing-sm);
}
.footer {
@@ -143,6 +142,10 @@
}
}
+.body > *:first-child {
+ margin-block-start: var(--spacing-md);
+}
+
:where(.footer) {
.btn {
margin-inline-end: var(--spacing-2xs);
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 935dcdc..820902b 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -99,10 +99,6 @@
"defaultMessage": "Written by:",
"description": "PostPreviewMeta: author label"
},
- "310o3F": {
- "defaultMessage": "Error 404: Page not found - {websiteName}",
- "description": "404Page: SEO - Page title"
- },
"3Pipok": {
"defaultMessage": "Thanks. Your message was successfully sent. I will answer it as soon as possible.",
"description": "Contact: success message"
@@ -111,9 +107,9 @@
"defaultMessage": "Repositories:",
"description": "ProjectOverview: repositories label"
},
- "48Ww//": {
- "defaultMessage": "Page not found.",
- "description": "404Page: SEO - Meta description"
+ "3u29G5": {
+ "defaultMessage": "Query must be longer than one character.",
+ "description": "Error404Page: invalid query message"
},
"4M71hp": {
"defaultMessage": "{starsCount, plural, =0 {No stars} one {# star} other {# stars}}",
@@ -135,6 +131,10 @@
"defaultMessage": "Copy",
"description": "usePrism: copy button text (not clicked)"
},
+ "6IAJYx": {
+ "defaultMessage": "Thematics are loading...",
+ "description": "Error404Page: loading thematics message"
+ },
"701ggm": {
"defaultMessage": "Illustration of {projectName}",
"description": "ProjectOverview: cover accessible name"
@@ -223,10 +223,6 @@
"defaultMessage": "Failed to load.",
"description": "BlogPage: failed to load text"
},
- "C6oK7h": {
- "defaultMessage": "Query must be longer than one character.",
- "description": "404Page: invalid query message"
- },
"Dq6+WH": {
"defaultMessage": "Thematics",
"description": "SearchPage: thematics list widget title"
@@ -263,6 +259,10 @@
"defaultMessage": "Share on Journal du Hacker",
"description": "SharingWidget: Journal du Hacker sharing link"
},
+ "HnMf0i": {
+ "defaultMessage": "Topics are loading...",
+ "description": "Error404Page: loading topics message"
+ },
"HohQPh": {
"defaultMessage": "Thematics",
"description": "Error404Page: thematics list widget title"
@@ -647,6 +647,10 @@
"defaultMessage": "Go to next page, page {number}",
"description": "BlogPage: next page label"
},
+ "pNIIU1": {
+ "defaultMessage": "Error 404: Page not found - {websiteName}",
+ "description": "Error404Page: SEO - Page title"
+ },
"pT5nHk": {
"defaultMessage": "Published on:",
"description": "HomePage: publication date label"
@@ -739,6 +743,10 @@
"defaultMessage": "Thematics are loading...",
"description": "BlogPage: loading thematics message"
},
+ "yKoGqg": {
+ "defaultMessage": "Page not found.",
+ "description": "Error404Page: SEO - Meta description"
+ },
"yN5P+m": {
"defaultMessage": "Message:",
"description": "ContactForm: message label"
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index 6e65be0..3628763 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -99,10 +99,6 @@
"defaultMessage": "Écrit par :",
"description": "PostPreviewMeta: author label"
},
- "310o3F": {
- "defaultMessage": "Erreur 404 : Page non trouvée - {websiteName}",
- "description": "404Page: SEO - Page title"
- },
"3Pipok": {
"defaultMessage": "Merci. Votre message a été envoyé avec succès. J’y répondrai dès que possible.",
"description": "Contact: success message"
@@ -111,9 +107,9 @@
"defaultMessage": "Dépôts :",
"description": "ProjectOverview: repositories label"
},
- "48Ww//": {
- "defaultMessage": "Page non trouvée.",
- "description": "404Page: SEO - Meta description"
+ "3u29G5": {
+ "defaultMessage": "Query must be longer than one character.",
+ "description": "Error404Page: invalid query message"
},
"4M71hp": {
"defaultMessage": "{starsCount, plural, =0 {0 étoile} one {# étoile} other {# étoiles}}",
@@ -135,6 +131,10 @@
"defaultMessage": "Copier",
"description": "usePrism: copy button text (not clicked)"
},
+ "6IAJYx": {
+ "defaultMessage": "Les thématiques sont en cours de chargement…",
+ "description": "Error404Page: loading thematics message"
+ },
"701ggm": {
"defaultMessage": "Illustration de {projectName}",
"description": "ProjectOverview: cover accessible name"
@@ -223,10 +223,6 @@
"defaultMessage": "Échec du chargement.",
"description": "BlogPage: failed to load text"
},
- "C6oK7h": {
- "defaultMessage": "Les mots-clés doivent être plus longs qu'un caractère.",
- "description": "404Page: invalid query message"
- },
"Dq6+WH": {
"defaultMessage": "Thématiques",
"description": "SearchPage: thematics list widget title"
@@ -263,6 +259,10 @@
"defaultMessage": "Partager sur le Journal du Hacker",
"description": "SharingWidget: Journal du Hacker sharing link"
},
+ "HnMf0i": {
+ "defaultMessage": "Les sujets sont en cours de chargement…",
+ "description": "Error404Page: loading topics message"
+ },
"HohQPh": {
"defaultMessage": "Thématiques",
"description": "Error404Page: thematics list widget title"
@@ -647,6 +647,10 @@
"defaultMessage": "Aller à la page suivante, page {number}",
"description": "BlogPage: next page label"
},
+ "pNIIU1": {
+ "defaultMessage": "Erreur 404 : Page non trouvée - {websiteName}",
+ "description": "Error404Page: SEO - Page title"
+ },
"pT5nHk": {
"defaultMessage": "Publié le :",
"description": "HomePage: publication date label"
@@ -739,6 +743,10 @@
"defaultMessage": "Les thématiques sont en cours de chargement…",
"description": "BlogPage: loading thematics message"
},
+ "yKoGqg": {
+ "defaultMessage": "Page non trouvée.",
+ "description": "Error404Page: SEO - Meta description"
+ },
"yN5P+m": {
"defaultMessage": "Message :",
"description": "ContactForm: message label"
diff --git a/src/pages/404.tsx b/src/pages/404.tsx
index a98931f..6ef0c55 100644
--- a/src/pages/404.tsx
+++ b/src/pages/404.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable max-statements */
import type { GetStaticProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
@@ -15,6 +14,7 @@ import {
PageHeader,
PageSidebar,
SearchForm,
+ Spinner,
type SearchFormSubmit,
} from '../components';
import {
@@ -25,7 +25,9 @@ import {
fetchTopicsCount,
fetchTopicsList,
} from '../services/graphql';
+import styles from '../styles/pages/blog.module.scss';
import type {
+ GraphQLConnection,
NextPageWithLayout,
WPThematicPreview,
WPTopicPreview,
@@ -34,82 +36,100 @@ import { CONFIG } from '../utils/config';
import { ROUTES } from '../utils/constants';
import { getLinksItemData } from '../utils/helpers';
import { loadTranslation, type Messages } from '../utils/helpers/server';
-import { useBreadcrumb } from '../utils/hooks';
+import { useBreadcrumb, useThematicsList, useTopicsList } from '../utils/hooks';
+
+const link = (chunks: ReactNode) => <Link href={ROUTES.CONTACT}>{chunks}</Link>;
type Error404PageProps = {
- thematicsList: WPThematicPreview[];
- topicsList: WPTopicPreview[];
+ data: {
+ thematics: GraphQLConnection<WPThematicPreview>;
+ topics: GraphQLConnection<WPTopicPreview>;
+ };
translation: Messages;
};
/**
* Error 404 page.
*/
-const Error404Page: NextPageWithLayout<Error404PageProps> = ({
- thematicsList,
- topicsList,
-}) => {
+const Error404Page: NextPageWithLayout<Error404PageProps> = ({ data }) => {
const router = useRouter();
const intl = useIntl();
- const title = intl.formatMessage({
- defaultMessage: 'Page not found',
- description: 'Error404Page: page title',
- id: 'KnWeKh',
+ const { isLoading: areThematicsLoading, thematics } = useThematicsList({
+ fallback: data.thematics,
+ input: { first: data.thematics.pageInfo.total },
});
- 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',
+ const { isLoading: areTopicsLoading, topics } = useTopicsList({
+ fallback: data.topics,
+ input: { first: data.topics.pageInfo.total },
+ });
+ const messages = {
+ loading: {
+ thematicsList: intl.formatMessage({
+ defaultMessage: 'Thematics are loading...',
+ description: 'Error404Page: loading thematics message',
+ id: '6IAJYx',
+ }),
+ topicsList: intl.formatMessage({
+ defaultMessage: 'Topics are loading...',
+ description: 'Error404Page: loading topics message',
+ id: 'HnMf0i',
+ }),
},
- {
- link: (chunks: ReactNode) => <Link href={ROUTES.CONTACT}>{chunks}</Link>,
- }
- );
+ page: {
+ title: intl.formatMessage({
+ defaultMessage: 'Page not found',
+ description: 'Error404Page: page title',
+ id: 'KnWeKh',
+ }),
+ },
+ seo: {
+ title: intl.formatMessage(
+ {
+ defaultMessage: 'Error 404: Page not found - {websiteName}',
+ description: 'Error404Page: SEO - Page title',
+ id: 'pNIIU1',
+ },
+ { websiteName: CONFIG.name }
+ ),
+ metaDesc: intl.formatMessage({
+ defaultMessage: 'Page not found.',
+ description: 'Error404Page: SEO - Meta description',
+ id: 'yKoGqg',
+ }),
+ },
+ widgets: {
+ thematicsListTitle: intl.formatMessage({
+ defaultMessage: 'Thematics',
+ description: 'Error404Page: thematics list widget title',
+ id: 'HohQPh',
+ }),
+ topicsListTitle: intl.formatMessage({
+ defaultMessage: 'Topics',
+ description: 'Error404Page: topics list widget title',
+ id: 'GVpTIl',
+ }),
+ },
+ };
const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
- title,
+ title: messages.page.title,
url: ROUTES.NOT_FOUND,
});
- const pageTitle = intl.formatMessage(
- {
- defaultMessage: 'Error 404: Page not found - {websiteName}',
- description: '404Page: SEO - Page title',
- id: '310o3F',
- },
- { websiteName: CONFIG.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',
- });
const searchSubmitHandler: SearchFormSubmit = useCallback(
- ({ query }) => {
+ async ({ query }) => {
if (!query)
return {
messages: {
error: intl.formatMessage({
defaultMessage: 'Query must be longer than one character.',
- description: '404Page: invalid query message',
- id: 'C6oK7h',
+ description: 'Error404Page: invalid query message',
+ id: '3u29G5',
}),
},
validator: (value) => value.query.length > 1,
};
- router.push({ pathname: ROUTES.SEARCH, query: { s: query } });
+ await router.push({ pathname: ROUTES.SEARCH, query: { s: query } });
return undefined;
},
@@ -119,9 +139,9 @@ const Error404Page: NextPageWithLayout<Error404PageProps> = ({
return (
<Page breadcrumbs={breadcrumbItems}>
<Head>
- <title>{pageTitle}</title>
+ <title>{messages.seo.title}</title>
{/*eslint-disable-next-line react/jsx-no-literals -- Name allowed */}
- <meta name="description" content={pageDescription} />
+ <meta name="description" content={messages.seo.metaDesc} />
</Head>
<Script
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
@@ -129,9 +149,19 @@ const Error404Page: NextPageWithLayout<Error404PageProps> = ({
id="schema-breadcrumb"
type="application/ld+json"
/>
- <PageHeader heading={title} />
- <PageBody>
- {body}
+ <PageHeader heading={messages.page.title} />
+ <PageBody className={styles['no-results']}>
+ <p>
+ {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 }
+ )}
+ </p>
<p>
{intl.formatMessage({
defaultMessage: 'You can also try a search:',
@@ -142,26 +172,34 @@ const Error404Page: NextPageWithLayout<Error404PageProps> = ({
<SearchForm isLabelHidden onSubmit={searchSubmitHandler} />
</PageBody>
<PageSidebar>
- <LinksWidget
- heading={
- <Heading isFake level={3}>
- {thematicsListTitle}
- </Heading>
- }
- items={getLinksItemData(
- thematicsList.map(convertWPThematicPreviewToPageLink)
- )}
- />
- <LinksWidget
- heading={
- <Heading isFake level={3}>
- {topicsListTitle}
- </Heading>
- }
- items={getLinksItemData(
- topicsList.map(convertWPTopicPreviewToPageLink)
- )}
- />
+ {areThematicsLoading ? (
+ <Spinner>{messages.loading.thematicsList}</Spinner>
+ ) : (
+ <LinksWidget
+ heading={
+ <Heading level={2}>{messages.widgets.thematicsListTitle}</Heading>
+ }
+ items={getLinksItemData(
+ thematics.edges.map((edge) =>
+ convertWPThematicPreviewToPageLink(edge.node)
+ )
+ )}
+ />
+ )}
+ {areTopicsLoading ? (
+ <Spinner>{messages.loading.topicsList}</Spinner>
+ ) : (
+ <LinksWidget
+ heading={
+ <Heading level={2}>{messages.widgets.topicsListTitle}</Heading>
+ }
+ items={getLinksItemData(
+ topics.edges.map((edge) =>
+ convertWPTopicPreviewToPageLink(edge.node)
+ )
+ )}
+ />
+ )}
</PageSidebar>
</Page>
);
@@ -180,8 +218,10 @@ export const getStaticProps: GetStaticProps<Error404PageProps> = async ({
return {
props: {
- thematicsList: thematics.edges.map((edge) => edge.node),
- topicsList: topics.edges.map((edge) => edge.node),
+ data: {
+ thematics,
+ topics,
+ },
translation,
},
};
diff --git a/src/styles/pages/blog.module.scss b/src/styles/pages/blog.module.scss
index 6b68849..62bc6a5 100644
--- a/src/styles/pages/blog.module.scss
+++ b/src/styles/pages/blog.module.scss
@@ -20,7 +20,7 @@
}
.no-results {
- margin-block-start: var(--spacing-md);
+ margin-block-start: var(--spacing-sm);
}
.pagination {
diff --git a/tests/cypress/e2e/pages/404.cy.ts b/tests/cypress/e2e/pages/404.cy.ts
new file mode 100644
index 0000000..7899275
--- /dev/null
+++ b/tests/cypress/e2e/pages/404.cy.ts
@@ -0,0 +1,31 @@
+import { ROUTES } from '../../../../src/utils/constants';
+
+describe('404 Page', () => {
+ beforeEach(() => {
+ cy.visit(ROUTES.NOT_FOUND, { failOnStatusCode: false });
+ });
+
+ it('successfully loads', () => {
+ cy.findByRole('heading', { level: 1 }).should(
+ 'contain.text',
+ 'Page non trouvée'
+ );
+ });
+
+ it('contains a breadcrumbs', () => {
+ cy.findByRole('navigation', { name: 'Fil d’Ariane' }).should('exist');
+ });
+
+ it('contains a thematics list widget and a topics list widget', () => {
+ cy.findByRole('heading', { level: 2, name: 'Thématiques' }).should('exist');
+ cy.findByRole('heading', { level: 2, name: 'Sujets' }).should('exist');
+ });
+
+ it('provides a form to search for keywords', () => {
+ const keywords = 'coldark';
+
+ cy.findByRole('searchbox').type(keywords);
+ cy.findByRole('button', { name: /Rechercher/ }).click();
+ cy.findByRole('heading', { level: 1 }).should('contain.text', keywords);
+ });
+});