aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-11-29 18:07:20 +0100
committerArmand Philippot <git@armandphilippot.com>2023-11-29 18:07:20 +0100
commitd363306235f2a48f16e488f20f73e2233ddcf281 (patch)
tree5e86a7b5f38416d7ee56a9aff5ef972aa73d82b1
parentdfa894b76ee3584bf169710c78c57330c5d6ee67 (diff)
refactor(pages): improve Homepage
* move custom homepage components that does not require props to the MDX file (links should not need to be translated here but where they are defined) * move SEO title and meta desc to MDX file * make Page component the wrapper instead of using a React fragment * fix MDX module types
-rw-r--r--mdx.d.ts6
-rw-r--r--src/components/atoms/buttons/button-link/button-link.module.scss2
-rw-r--r--src/components/mdx.tsx17
-rw-r--r--src/components/molecules/grid/grid.module.scss12
-rw-r--r--src/components/molecules/grid/grid.test.tsx40
-rw-r--r--src/components/molecules/grid/grid.tsx8
m---------src/content0
-rw-r--r--src/i18n/en.json52
-rw-r--r--src/i18n/fr.json52
-rw-r--r--src/pages/index.tsx347
-rw-r--r--src/pages/thematique/[slug].tsx5
-rw-r--r--src/services/graphql/helpers/convert-taxonomy-to-page-link.test.ts2
-rw-r--r--src/services/graphql/helpers/convert-taxonomy-to-page-link.ts2
-rw-r--r--src/services/graphql/helpers/convert-wp-thematic-to-thematic.test.ts2
-rw-r--r--src/services/graphql/helpers/convert-wp-thematic-to-thematic.ts2
-rw-r--r--src/styles/pages/home.module.scss34
-rw-r--r--src/utils/constants.ts8
-rw-r--r--src/utils/helpers/schema-org.ts41
-rw-r--r--src/utils/helpers/strings.ts13
-rw-r--r--src/utils/hooks/use-breadcrumb.ts3
-rw-r--r--tests/cypress/e2e/pages/homepage.cy.ts27
21 files changed, 241 insertions, 434 deletions
diff --git a/mdx.d.ts b/mdx.d.ts
index 4fd2076..91cf7ea 100644
--- a/mdx.d.ts
+++ b/mdx.d.ts
@@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/consistent-type-imports */
declare module '*.mdx' {
type MDXProps = import('mdx/types').MDXProps;
- type MDXData = import('./src/types/mdx').MDXData;
- type MDXPageMeta = import('./src/types/mdx').MDXPageMeta;
- type MDXProjectMeta = import('./src/types/mdx').MDXProjectMeta;
+ type MDXData = import('./src/types/data').MDXData;
+ type MDXPageMeta = import('./src/types/data').MDXPageMeta;
+ type MDXProjectMeta = import('./src/types/data').MDXProjectMeta;
const MDXComponent: (props: MDXProps) => JSX.Element;
export default MDXComponent;
diff --git a/src/components/atoms/buttons/button-link/button-link.module.scss b/src/components/atoms/buttons/button-link/button-link.module.scss
index 0f35a24..3ddeffe 100644
--- a/src/components/atoms/buttons/button-link/button-link.module.scss
+++ b/src/components/atoms/buttons/button-link/button-link.module.scss
@@ -3,6 +3,8 @@
.btn {
@extend %button;
+ width: fit-content;
+
&--circle {
@extend %circle-button;
}
diff --git a/src/components/mdx.tsx b/src/components/mdx.tsx
index 9f0a4a5..eea80a9 100644
--- a/src/components/mdx.tsx
+++ b/src/components/mdx.tsx
@@ -1,8 +1,17 @@
import type { MDXComponents } from 'mdx/types';
import NextImage from 'next/image';
import type { AnchorHTMLAttributes, ImgHTMLAttributes, ReactNode } from 'react';
-import { Figure, Heading, Link, List, ListItem } from './atoms';
+import {
+ ButtonLink,
+ Figure,
+ Heading,
+ Icon,
+ Link,
+ List,
+ ListItem,
+} from './atoms';
import { Code, Grid, GridItem } from './molecules';
+import { PageSection } from './templates';
const Anchor = ({
children = '',
@@ -58,6 +67,7 @@ const Gallery = ({ children }: { children: ReactNode }) => (
export const mdxComponents: MDXComponents = {
a: Anchor,
+ ButtonLink,
Code,
figure: ({ ref, ...props }) => <Figure {...props} />,
Figure,
@@ -70,9 +80,14 @@ export const mdxComponents: MDXComponents = {
h4: ({ ref, ...props }) => <Heading {...props} level={4} />,
h5: ({ ref, ...props }) => <Heading {...props} level={5} />,
h6: ({ ref, ...props }) => <Heading {...props} level={6} />,
+ Icon,
img: Img,
+ Img,
li: ({ ref, ...props }) => <ListItem {...props} />,
Link,
+ List,
+ ListItem,
+ PageSection,
ol: ({ ref, ...props }) => (
<List
// eslint-disable-next-line react/jsx-no-literals
diff --git a/src/components/molecules/grid/grid.module.scss b/src/components/molecules/grid/grid.module.scss
index f13af30..d5260cf 100644
--- a/src/components/molecules/grid/grid.module.scss
+++ b/src/components/molecules/grid/grid.module.scss
@@ -2,6 +2,18 @@
display: grid;
gap: var(--gap);
+ &--align-items-center {
+ align-items: center;
+ }
+
+ &--align-items-start {
+ align-items: start;
+ }
+
+ &--align-items-end {
+ align-items: end;
+ }
+
&--is-centered {
place-content: center;
}
diff --git a/src/components/molecules/grid/grid.test.tsx b/src/components/molecules/grid/grid.test.tsx
index e69610d..b4b9f77 100644
--- a/src/components/molecules/grid/grid.test.tsx
+++ b/src/components/molecules/grid/grid.test.tsx
@@ -109,4 +109,44 @@ describe('Grid', () => {
expect(rtlScreen.getByRole('list')).toHaveClass('wrapper--is-centered');
});
+
+ it('can render a list of centered items', () => {
+ render(
+ <Grid alignItems="center">
+ {items.map((item) => (
+ <GridItem key={item.id}>{item.contents}</GridItem>
+ ))}
+ </Grid>
+ );
+
+ expect(rtlScreen.getByRole('list')).toHaveClass(
+ 'wrapper--align-items-center'
+ );
+ });
+
+ it('can render a list of items with end alignment', () => {
+ render(
+ <Grid alignItems="end">
+ {items.map((item) => (
+ <GridItem key={item.id}>{item.contents}</GridItem>
+ ))}
+ </Grid>
+ );
+
+ expect(rtlScreen.getByRole('list')).toHaveClass('wrapper--align-items-end');
+ });
+
+ it('can render a list of items with start alignment', () => {
+ render(
+ <Grid alignItems="start">
+ {items.map((item) => (
+ <GridItem key={item.id}>{item.contents}</GridItem>
+ ))}
+ </Grid>
+ );
+
+ expect(rtlScreen.getByRole('list')).toHaveClass(
+ 'wrapper--align-items-start'
+ );
+ });
});
diff --git a/src/components/molecules/grid/grid.tsx b/src/components/molecules/grid/grid.tsx
index 3d0ecf1..38f6e55 100644
--- a/src/components/molecules/grid/grid.tsx
+++ b/src/components/molecules/grid/grid.tsx
@@ -13,6 +13,12 @@ export type GridProps<T extends boolean> = Omit<
'children' | 'hideMarker' | 'isHierarchical' | 'isInline' | 'spacing'
> & {
/**
+ * How the items should be aligned?
+ *
+ * @default undefined // The default behavior is `stretch`.
+ */
+ alignItems?: 'center' | 'end' | 'start';
+ /**
* The grid items.
*/
children: ReactNode;
@@ -62,6 +68,7 @@ export type GridProps<T extends boolean> = Omit<
const GridWithRef = <T extends boolean>(
{
+ alignItems,
children,
className = '',
col = 'auto-fit',
@@ -77,6 +84,7 @@ const GridWithRef = <T extends boolean>(
) => {
const gridClass = [
styles.wrapper,
+ styles[alignItems ? `wrapper--align-items-${alignItems}` : ''],
styles[isCentered ? 'wrapper--is-centered' : ''],
styles[size ? 'wrapper--has-fixed-size' : ''],
styles[sizeMin ? 'wrapper--has-min-size' : ''],
diff --git a/src/content b/src/content
-Subproject 6c0f2250ea956f9511fed8d2620466cb43d18d3
+Subproject a9aa65d7f81b6ad72ef707f40f14e955cf4fcd6
diff --git a/src/i18n/en.json b/src/i18n/en.json
index aac327d..be67b38 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -99,10 +99,6 @@
"defaultMessage": "Repositories:",
"description": "ProjectOverview: repositories label"
},
- "3f3PzH": {
- "defaultMessage": "Github",
- "description": "HomePage: Github link"
- },
"48Ww//": {
"defaultMessage": "Page not found.",
"description": "404Page: SEO - Meta description"
@@ -135,10 +131,6 @@
"defaultMessage": "Github profile",
"description": "ContactPage: Github profile link"
},
- "7AnwZ7": {
- "defaultMessage": "Gitlab",
- "description": "HomePage: Gitlab link"
- },
"7TbbIk": {
"defaultMessage": "Blog",
"description": "BlogPage: page title"
@@ -315,10 +307,6 @@
"defaultMessage": "CV",
"description": "SiteNavbar: main nav - cv link"
},
- "N44SOc": {
- "defaultMessage": "Projects",
- "description": "HomePage: link to projects"
- },
"N804XO": {
"defaultMessage": "Topics",
"description": "SearchPage: topics list widget title"
@@ -363,10 +351,6 @@
"defaultMessage": "{starsCount, plural, =0 {No stars} one {# star} other {# stars}}",
"description": "ProjectOverview: stars count"
},
- "PXp2hv": {
- "defaultMessage": "{websiteName} | Front-end developer: WordPress/React",
- "description": "HomePage: SEO - Page title"
- },
"PnrHgZ": {
"defaultMessage": "Home",
"description": "SiteNavbar: main nav - home link"
@@ -407,10 +391,6 @@
"defaultMessage": "LinkedIn profile",
"description": "CVPage: LinkedIn profile link"
},
- "T4YA64": {
- "defaultMessage": "Subscribe",
- "description": "HomePage: RSS feed subscription text"
- },
"TpyFZ6": {
"defaultMessage": "An error occurred:",
"description": "Contact: error message"
@@ -551,10 +531,6 @@
"defaultMessage": "Light Theme 🌞",
"description": "usePrism: toggle light theme button text"
},
- "i5L19t": {
- "defaultMessage": "Shaarli",
- "description": "HomePage: link to Shaarli"
- },
"iG5SHf": {
"defaultMessage": "{postTitle} cover",
"description": "PostPreview: an accessible name for the figure wrapping the cover"
@@ -567,14 +543,14 @@
"defaultMessage": "Home",
"description": "Breadcrumb: home label"
},
- "jASD7k": {
- "defaultMessage": "Linux",
- "description": "HomePage: link to Linux thematic"
- },
"jJm8wd": {
"defaultMessage": "Reading time:",
"description": "PageHeader: reading time label"
},
+ "kq+fzI": {
+ "defaultMessage": "Cover of {pageTitle}",
+ "description": "RecentPosts: card cover accessible name"
+ },
"l50cYa": {
"defaultMessage": "Open settings",
"description": "SiteNavbar: settings button label in navbar"
@@ -587,6 +563,10 @@
"defaultMessage": "Legal notice",
"description": "SiteFooter: Legal notice link label"
},
+ "mWZU4R": {
+ "defaultMessage": "View {pageTitle}",
+ "description": "RecentPosts: card accessible name"
+ },
"nGss/j": {
"defaultMessage": "Ackee tracking (analytics)",
"description": "AckeeToggle: tooltip title"
@@ -647,10 +627,6 @@
"defaultMessage": "Gitlab profile",
"description": "ProjectsPage: Gitlab profile link"
},
- "sO/Iwj": {
- "defaultMessage": "Contact me",
- "description": "HomePage: contact button text"
- },
"sR5hah": {
"defaultMessage": "Updated on:",
"description": "PageHeader: update date label"
@@ -663,10 +639,6 @@
"defaultMessage": "Partial",
"description": "AckeeToggle: partial option name"
},
- "tMuNTy": {
- "defaultMessage": "{websiteName} is a front-end developer located in France. He codes and he writes mostly about web development and open-source.",
- "description": "HomePage: SEO - Meta description"
- },
"tsWh8x": {
"defaultMessage": "Light theme",
"description": "PrismThemeToggle: light theme label"
@@ -687,10 +659,6 @@
"defaultMessage": "On",
"description": "MotionToggle: activate reduce motion label"
},
- "vkF/RP": {
- "defaultMessage": "Web development",
- "description": "HomePage: link to web development thematic"
- },
"vtDLzG": {
"defaultMessage": "Would you like to try a new search?",
"description": "SearchPage: try a new search message"
@@ -703,10 +671,6 @@
"defaultMessage": "Email:",
"description": "ContactForm: email label"
},
- "w8GrOf": {
- "defaultMessage": "Free",
- "description": "HomePage: link to free thematic"
- },
"xaqaYQ": {
"defaultMessage": "Sending mail...",
"description": "ContactForm: spinner message on submit"
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index 17514a3..0226f1e 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -99,10 +99,6 @@
"defaultMessage": "Dépôts :",
"description": "ProjectOverview: repositories label"
},
- "3f3PzH": {
- "defaultMessage": "Github",
- "description": "HomePage: Github link"
- },
"48Ww//": {
"defaultMessage": "Page non trouvée.",
"description": "404Page: SEO - Meta description"
@@ -135,10 +131,6 @@
"defaultMessage": "Profil Github",
"description": "ContactPage: Github profile link"
},
- "7AnwZ7": {
- "defaultMessage": "Gitlab",
- "description": "HomePage: Gitlab link"
- },
"7TbbIk": {
"defaultMessage": "Blog",
"description": "BlogPage: page title"
@@ -315,10 +307,6 @@
"defaultMessage": "CV",
"description": "SiteNavbar: main nav - cv link"
},
- "N44SOc": {
- "defaultMessage": "Projets",
- "description": "HomePage: link to projects"
- },
"N804XO": {
"defaultMessage": "Sujets",
"description": "SearchPage: topics list widget title"
@@ -363,10 +351,6 @@
"defaultMessage": "{starsCount, plural, =0 {0 étoile} one {# étoile} other {# étoiles}}",
"description": "ProjectOverview: stars count"
},
- "PXp2hv": {
- "defaultMessage": "{websiteName} | Intégrateur web - Développeur WordPress / React",
- "description": "HomePage: SEO - Page title"
- },
"PnrHgZ": {
"defaultMessage": "Accueil",
"description": "SiteNavbar: main nav - home link"
@@ -407,10 +391,6 @@
"defaultMessage": "Profil LinkedIn",
"description": "CVPage: LinkedIn profile link"
},
- "T4YA64": {
- "defaultMessage": "Vous abonner",
- "description": "HomePage: RSS feed subscription text"
- },
"TpyFZ6": {
"defaultMessage": "Une erreur est survenue :",
"description": "Contact: error message"
@@ -551,10 +531,6 @@
"defaultMessage": "Thème clair 🌞",
"description": "usePrism: toggle light theme button text"
},
- "i5L19t": {
- "defaultMessage": "Shaarli",
- "description": "HomePage: link to Shaarli"
- },
"iG5SHf": {
"defaultMessage": "Illustration de {postTitle}",
"description": "PostPreview: an accessible name for the figure wrapping the cover"
@@ -567,14 +543,14 @@
"defaultMessage": "Accueil",
"description": "Breadcrumb: home label"
},
- "jASD7k": {
- "defaultMessage": "Linux",
- "description": "HomePage: link to Linux thematic"
- },
"jJm8wd": {
"defaultMessage": "Temps de lecture :",
"description": "PageHeader: reading time label"
},
+ "kq+fzI": {
+ "defaultMessage": "Illustration de {pageTitle}",
+ "description": "RecentPosts: card cover accessible name"
+ },
"l50cYa": {
"defaultMessage": "Ouvrir les réglages",
"description": "SiteNavbar: settings button label in navbar"
@@ -587,6 +563,10 @@
"defaultMessage": "Mentions légales",
"description": "SiteFooter: Legal notice link label"
},
+ "mWZU4R": {
+ "defaultMessage": "Consulter {pageTitle}",
+ "description": "RecentPosts: card accessible name"
+ },
"nGss/j": {
"defaultMessage": "Suivi Ackee (analytique)",
"description": "AckeeToggle: tooltip title"
@@ -647,10 +627,6 @@
"defaultMessage": "Profil Gitlab",
"description": "ProjectsPage: Gitlab profile link"
},
- "sO/Iwj": {
- "defaultMessage": "Me contacter",
- "description": "HomePage: contact button text"
- },
"sR5hah": {
"defaultMessage": "Mis à jour le :",
"description": "PageHeader: update date label"
@@ -663,10 +639,6 @@
"defaultMessage": "Partiel",
"description": "AckeeToggle: partial option name"
},
- "tMuNTy": {
- "defaultMessage": "{websiteName} est intégrateur web / développeur front-end en France. Il code et il écrit essentiellement à propos de développement web et du libre.",
- "description": "HomePage: SEO - Meta description"
- },
"tsWh8x": {
"defaultMessage": "Thème clair",
"description": "PrismThemeToggle: light theme label"
@@ -687,10 +659,6 @@
"defaultMessage": "Marche",
"description": "MotionToggle: activate reduce motion label"
},
- "vkF/RP": {
- "defaultMessage": "Développement web",
- "description": "HomePage: link to web development thematic"
- },
"vtDLzG": {
"defaultMessage": "Souhaitez-vous essayer une nouvelle recherche ?",
"description": "SearchPage: try a new search message"
@@ -703,10 +671,6 @@
"defaultMessage": "E-mail :",
"description": "ContactForm: email label"
},
- "w8GrOf": {
- "defaultMessage": "Libre",
- "description": "HomePage: link to free thematic"
- },
"xaqaYQ": {
"defaultMessage": "Mail en cours d’envoi…",
"description": "ContactForm: spinner message on submit"
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 7bd8aec..f4d36c1 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -3,10 +3,9 @@ import type { GetStaticProps } from 'next';
import Head from 'next/head';
import NextImage from 'next/image';
import Script from 'next/script';
-import type { FC, HTMLAttributes, ReactNode } from 'react';
+import type { FC } from 'react';
import { useIntl } from 'react-intl';
import {
- ButtonLink,
Card,
CardCover,
CardFooter,
@@ -15,261 +14,77 @@ import {
CardTitle,
getLayout,
Grid,
- Icon,
- List,
- ListItem,
Time,
MetaItem,
- type PageSectionProps,
- PageSection,
Page,
} from '../components';
import { mdxComponents } from '../components/mdx';
-import HomePageContent from '../content/pages/homepage.mdx';
+import HomePageContent, { meta } from '../content/pages/homepage.mdx';
import {
convertRecentPostToRecentArticle,
fetchRecentPosts,
} from '../services/graphql';
-import styles from '../styles/pages/home.module.scss';
import type { NextPageWithLayout, RecentArticle } from '../types';
import { CONFIG } from '../utils/config';
-import { PERSONAL_LINKS, ROUTES } from '../utils/constants';
+import { ROUTES } from '../utils/constants';
import { getSchemaJson, getWebPageSchema } from '../utils/helpers';
import { loadTranslation, type Messages } from '../utils/helpers/server';
import { useBreadcrumb } from '../utils/hooks';
-/**
- * Column component.
- *
- * Render the body as a column.
- */
-const Column = ({ children, ...props }: HTMLAttributes<HTMLDivElement>) => (
- <div {...props}>{children}</div>
-);
-
-/**
- * Retrieve a list of coding links.
- *
- * @returns {JSX.Element} - A list of links.
- */
-const CodingLinks: FC = () => {
- const intl = useIntl();
-
- return (
- <List className={styles.list} hideMarker isInline spacing="sm">
- <ListItem>
- <ButtonLink to={ROUTES.THEMATICS.WEB_DEV}>
- {intl.formatMessage({
- defaultMessage: 'Web development',
- description: 'HomePage: link to web development thematic',
- id: 'vkF/RP',
- })}
- </ButtonLink>
- </ListItem>
- <ListItem>
- <ButtonLink to={ROUTES.PROJECTS}>
- {intl.formatMessage({
- defaultMessage: 'Projects',
- description: 'HomePage: link to projects',
- id: 'N44SOc',
- })}
- </ButtonLink>
- </ListItem>
- </List>
- );
-};
-
-/**
- * Retrieve a list of Coldark repositories.
- *
- * @returns {JSX.Element} - A list of links.
- */
-const ColdarkRepos: FC = () => {
- const intl = useIntl();
- const repo = {
- github: 'https://github.com/ArmandPhilippot/coldark',
- gitlab: 'https://gitlab.com/ArmandPhilippot/coldark',
- };
-
- return (
- <List className={styles.list} hideMarker isInline spacing="sm">
- <ListItem>
- <ButtonLink isExternal to={repo.github}>
- {intl.formatMessage({
- defaultMessage: 'Github',
- description: 'HomePage: Github link',
- id: '3f3PzH',
- })}
- </ButtonLink>
- </ListItem>
- <ListItem>
- <ButtonLink isExternal to={repo.gitlab}>
- {intl.formatMessage({
- defaultMessage: 'Gitlab',
- description: 'HomePage: Gitlab link',
- id: '7AnwZ7',
- })}
- </ButtonLink>
- </ListItem>
- </List>
- );
-};
-
-/**
- * Retrieve a list of links related to Free thematic.
- *
- * @returns {JSX.Element} - A list of links.
- */
-const LibreLinks: FC = () => {
- const intl = useIntl();
-
- return (
- <List className={styles.list} hideMarker isInline spacing="sm">
- <ListItem>
- <ButtonLink to={ROUTES.THEMATICS.FREE}>
- {intl.formatMessage({
- defaultMessage: 'Free',
- description: 'HomePage: link to free thematic',
- id: 'w8GrOf',
- })}
- </ButtonLink>
- </ListItem>
- <ListItem>
- <ButtonLink to={ROUTES.THEMATICS.LINUX}>
- {intl.formatMessage({
- defaultMessage: 'Linux',
- description: 'HomePage: link to Linux thematic',
- id: 'jASD7k',
- })}
- </ButtonLink>
- </ListItem>
- </List>
- );
-};
-
-/**
- * Retrieve the Shaarli link.
- *
- * @returns {JSX.Element} - A list of links
- */
-const ShaarliLink: FC = () => {
- const intl = useIntl();
-
- return (
- <List className={styles.list} hideMarker isInline spacing="sm">
- <ListItem>
- <ButtonLink isExternal to={PERSONAL_LINKS.SHAARLI}>
- {intl.formatMessage({
- defaultMessage: 'Shaarli',
- description: 'HomePage: link to Shaarli',
- id: 'i5L19t',
- })}
- </ButtonLink>
- </ListItem>
- </List>
- );
-};
-
-/**
- * Retrieve the additional links.
- *
- * @returns {JSX.Element} - A list of links.
- */
-const MoreLinks: FC = () => {
- const intl = useIntl();
-
- return (
- <List className={styles.list} hideMarker isInline spacing="sm">
- <ListItem>
- <ButtonLink to={ROUTES.CONTACT}>
- <Icon aria-hidden={true} shape="envelop" />
- {intl.formatMessage({
- defaultMessage: 'Contact me',
- description: 'HomePage: contact button text',
- id: 'sO/Iwj',
- })}
- </ButtonLink>
- </ListItem>
- <ListItem>
- <ButtonLink to={ROUTES.RSS}>
- <Icon aria-hidden={true} shape="feed" />
- {intl.formatMessage({
- defaultMessage: 'Subscribe',
- description: 'HomePage: RSS feed subscription text',
- id: 'T4YA64',
- })}
- </ButtonLink>
- </ListItem>
- </List>
- );
+type RecentPostsProps = {
+ posts: RecentArticle[];
};
-const StyledGrid = ({ children }: { children: ReactNode }) => (
- <Grid className={styles.columns} gap="sm" sizeMin="250px">
- {children}
- </Grid>
-);
-
/**
- * Create the page sections.
+ * Get a cards list of recent posts.
*
- * @param {object} obj - An object containing the section body.
- * @param {ReactNode[]} obj.children - The section body.
- * @returns {JSX.Element} A section element.
- */
-const HomePageSection: FC<PageSectionProps> = ({
- children,
- hasBorder = true,
- variant,
-}) => (
- <PageSection
- className={styles.section}
- hasBorder={hasBorder}
- variant={variant}
- >
- {children}
- </PageSection>
-);
-
-type HomeProps = {
- recentPosts: RecentArticle[];
- translation?: Messages;
-};
-
-/**
- * Home page.
+ * @returns {JSX.Element} - The cards list.
*/
-const HomePage: NextPageWithLayout<HomeProps> = ({ recentPosts }) => {
+const RecentPosts: FC<RecentPostsProps> = ({ posts }): JSX.Element => {
const intl = useIntl();
const publicationDate = intl.formatMessage({
defaultMessage: 'Published on:',
description: 'HomePage: publication date label',
id: 'pT5nHk',
});
- const { schema: breadcrumbSchema } = useBreadcrumb({
- title: '',
- url: `/`,
- });
-
- /**
- * Get a cards list of recent posts.
- *
- * @returns {JSX.Element} - The cards list.
- */
- const getRecentPosts = (): JSX.Element => {
- const listClass = `${styles.list} ${styles['list--cards']}`;
- return (
- <Grid className={listClass} gap="sm" isCentered sizeMax="25ch">
- {recentPosts.map((post) => (
+ return (
+ <Grid
+ // eslint-disable-next-line react/jsx-no-literals
+ gap="sm"
+ // eslint-disable-next-line react/jsx-no-literals
+ sizeMax="25ch"
+ >
+ {posts.map((post) => {
+ const postUrl = `${ROUTES.ARTICLE}/${post.slug}`;
+ const cardLabel = intl.formatMessage(
+ {
+ defaultMessage: 'View {pageTitle}',
+ description: 'RecentPosts: card accessible name',
+ id: 'mWZU4R',
+ },
+ {
+ pageTitle: post.title,
+ }
+ );
+ const coverLabel = intl.formatMessage(
+ {
+ defaultMessage: 'Cover of {pageTitle}',
+ description: 'RecentPosts: card cover accessible name',
+ id: 'kq+fzI',
+ },
+ {
+ pageTitle: post.title,
+ }
+ );
+
+ return (
<Card
+ aria-label={cardLabel}
cover={
post.cover ? (
- <CardCover hasBorders>
- <NextImage
- {...post.cover}
- style={{ objectFit: 'scale-down' }}
- />
+ <CardCover aria-label={coverLabel} hasBorders>
+ <NextImage {...post.cover} />
</CardCover>
) : undefined
}
@@ -285,65 +100,57 @@ const HomePage: NextPageWithLayout<HomeProps> = ({ recentPosts }) => {
</CardMeta>
}
isCentered
- linkTo={`${ROUTES.ARTICLE}/${post.slug}`}
+ linkTo={postUrl}
>
<CardHeader>
<CardTitle level={3}>{post.title}</CardTitle>
</CardHeader>
<CardFooter />
</Card>
- ))}
- </Grid>
- );
- };
+ );
+ })}
+ </Grid>
+ );
+};
- const components: MDXComponents = {
+const getComponents = (recentPosts: RecentArticle[]): MDXComponents => {
+ return {
...mdxComponents,
- CodingLinks,
- ColdarkRepos,
- Column,
- Grid: StyledGrid,
- LibreLinks,
- MoreLinks,
- RecentPosts: getRecentPosts,
- Section: HomePageSection,
- ShaarliLink,
+ RecentPosts: () => <RecentPosts posts={recentPosts} />,
};
+};
+
+type HomeProps = {
+ recentPosts: RecentArticle[];
+ translation?: Messages;
+};
+
+/**
+ * Home page.
+ */
+const HomePage: NextPageWithLayout<HomeProps> = ({ recentPosts }) => {
+ const { schema: breadcrumbSchema } = useBreadcrumb({
+ title: '',
+ url: ROUTES.HOME,
+ });
- const pageTitle = intl.formatMessage(
- {
- defaultMessage: '{websiteName} | Front-end developer: WordPress/React',
- description: 'HomePage: SEO - Page title',
- id: 'PXp2hv',
- },
- { websiteName: CONFIG.name }
- );
- const pageDescription = intl.formatMessage(
- {
- defaultMessage:
- '{websiteName} is a front-end developer located in France. He codes and he writes mostly about web development and open-source.',
- description: 'HomePage: SEO - Meta description',
- id: 'tMuNTy',
- },
- { websiteName: CONFIG.name }
- );
const webpageSchema = getWebPageSchema({
- description: pageDescription,
+ description: meta.seo.description,
locale: CONFIG.locales.defaultLocale,
- slug: '',
- title: pageTitle,
+ slug: ROUTES.HOME,
+ title: meta.seo.title,
});
const schemaJsonLd = getSchemaJson([webpageSchema]);
return (
- <>
+ <Page hasSections>
<Head>
- <title>{pageTitle}</title>
+ <title>{meta.seo.title}</title>
{/*eslint-disable-next-line react/jsx-no-literals -- Name allowed */}
- <meta name="description" content={pageDescription} />
+ <meta name="description" content={meta.seo.description} />
<meta property="og:url" content={CONFIG.url} />
- <meta property="og:title" content={pageTitle} />
- <meta property="og:description" content={pageDescription} />
+ <meta property="og:title" content={meta.seo.title} />
+ <meta property="og:description" content={meta.seo.description} />
</Head>
<Script
// eslint-disable-next-line react/jsx-no-literals -- Id allowed
@@ -357,10 +164,8 @@ const HomePage: NextPageWithLayout<HomeProps> = ({ recentPosts }) => {
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
/>
- <Page hasSections>
- <HomePageContent components={components} />
- </Page>
- </>
+ <HomePageContent components={getComponents(recentPosts)} />
+ </Page>
);
};
diff --git a/src/pages/thematique/[slug].tsx b/src/pages/thematique/[slug].tsx
index 9ea52e1..3d1e966 100644
--- a/src/pages/thematique/[slug].tsx
+++ b/src/pages/thematique/[slug].tsx
@@ -52,7 +52,7 @@ const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({
const intl = useIntl();
const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
title,
- url: `${ROUTES.THEMATICS.INDEX}/${slug}`,
+ url: `${ROUTES.THEMATICS}/${slug}`,
});
const { asPath } = useRouter();
@@ -189,8 +189,7 @@ export const getStaticProps: GetStaticProps<ThematicPageProps> = async ({
);
const allThematicsLinks = allThematics.filter(
(thematic) =>
- thematic.url !==
- `${ROUTES.THEMATICS.INDEX}/${(params as ThematicParams).slug}`
+ thematic.url !== `${ROUTES.THEMATICS}/${(params as ThematicParams).slug}`
);
const translation = await loadTranslation(locale);
diff --git a/src/services/graphql/helpers/convert-taxonomy-to-page-link.test.ts b/src/services/graphql/helpers/convert-taxonomy-to-page-link.test.ts
index 54a62ad..f923850 100644
--- a/src/services/graphql/helpers/convert-taxonomy-to-page-link.test.ts
+++ b/src/services/graphql/helpers/convert-taxonomy-to-page-link.test.ts
@@ -18,7 +18,7 @@ describe('convert-taxonomy-to-page-link', () => {
expect(result.id).toBe(thematic.databaseId);
expect(result.logo).toBeUndefined();
expect(result.name).toBe(thematic.title);
- expect(result.url).toBe(`${ROUTES.THEMATICS.INDEX}/${thematic.slug}`);
+ expect(result.url).toBe(`${ROUTES.THEMATICS}/${thematic.slug}`);
});
it('can convert a WPTopicPreview object to a Topic object', () => {
diff --git a/src/services/graphql/helpers/convert-taxonomy-to-page-link.ts b/src/services/graphql/helpers/convert-taxonomy-to-page-link.ts
index 9b42eea..ca86a1e 100644
--- a/src/services/graphql/helpers/convert-taxonomy-to-page-link.ts
+++ b/src/services/graphql/helpers/convert-taxonomy-to-page-link.ts
@@ -28,7 +28,7 @@ export const convertWPThematicPreviewToPageLink = (
): PageLink =>
convertTaxonomyToPageLink({
...thematic,
- slug: `${ROUTES.THEMATICS.INDEX}/${thematic.slug}`,
+ slug: `${ROUTES.THEMATICS}/${thematic.slug}`,
});
export const convertWPTopicPreviewToPageLink = (
diff --git a/src/services/graphql/helpers/convert-wp-thematic-to-thematic.test.ts b/src/services/graphql/helpers/convert-wp-thematic-to-thematic.test.ts
index e535a21..435489d 100644
--- a/src/services/graphql/helpers/convert-wp-thematic-to-thematic.test.ts
+++ b/src/services/graphql/helpers/convert-wp-thematic-to-thematic.test.ts
@@ -42,7 +42,7 @@ describe('convert-wp-thematic-to-thematic', () => {
expect(result.meta.seo.description).toBe(thematic.seo.metaDesc);
expect(result.meta.seo.title).toBe(thematic.seo.title);
expect(result.meta.relatedTopics).toBeUndefined();
- expect(result.slug).toBe(`${ROUTES.THEMATICS.INDEX}/${thematic.slug}`);
+ expect(result.slug).toBe(`${ROUTES.THEMATICS}/${thematic.slug}`);
expect(result.title).toBe(thematic.title);
});
/* eslint-enable max-statements */
diff --git a/src/services/graphql/helpers/convert-wp-thematic-to-thematic.ts b/src/services/graphql/helpers/convert-wp-thematic-to-thematic.ts
index cabfa18..9aa1896 100644
--- a/src/services/graphql/helpers/convert-wp-thematic-to-thematic.ts
+++ b/src/services/graphql/helpers/convert-wp-thematic-to-thematic.ts
@@ -54,7 +54,7 @@ export const convertWPThematicToThematic = (thematic: WPThematic): Thematic => {
? getRelatedTopicsFrom(thematic.acfThematics.postsInThematic)
: undefined,
},
- slug: `${ROUTES.THEMATICS.INDEX}/${thematic.slug}`,
+ slug: `${ROUTES.THEMATICS}/${thematic.slug}`,
title: thematic.title,
};
};
diff --git a/src/styles/pages/home.module.scss b/src/styles/pages/home.module.scss
deleted file mode 100644
index a926ec3..0000000
--- a/src/styles/pages/home.module.scss
+++ /dev/null
@@ -1,34 +0,0 @@
-@use "../abstracts/functions" as fun;
-@use "../abstracts/mixins" as mix;
-
-.section {
- --card-width: 25ch;
-
- &:last-of-type {
- border-bottom: none;
- }
-}
-
-.columns {
- margin: 0 0 var(--spacing-sm);
-}
-
-.list {
- margin: 0 0 var(--spacing-sm);
-
- &--cards {
- @include mix.media("screen") {
- @include mix.dimensions("md") {
- margin: 0 calc(var(--spacing-sm) * -1) var(--spacing-sm);
- }
- }
- }
-}
-
-.icon {
- margin-right: var(--spacing-2xs);
-
- &--feed {
- width: var(--icon-size);
- }
-}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 26cbeaa..043a530 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -2,7 +2,6 @@ export const PERSONAL_LINKS = {
GITHUB: 'https://github.com/ArmandPhilippot',
GITLAB: 'https://gitlab.com/ArmandPhilippot',
LINKEDIN: 'https://www.linkedin.com/in/armandphilippot',
- SHAARLI: 'https://shaarli.armandphilippot.com/',
} as const;
/**
@@ -21,12 +20,7 @@ export const ROUTES = {
PROJECTS: '/projets',
RSS: '/feed',
SEARCH: '/recherche',
- THEMATICS: {
- INDEX: '/thematique',
- FREE: '/thematique/libre',
- LINUX: '/thematique/linux',
- WEB_DEV: '/thematique/developpement-web',
- },
+ THEMATICS: '/thematique',
TOPICS: '/sujet',
} as const;
diff --git a/src/utils/helpers/schema-org.ts b/src/utils/helpers/schema-org.ts
index 2edc11b..f028f5a 100644
--- a/src/utils/helpers/schema-org.ts
+++ b/src/utils/helpers/schema-org.ts
@@ -10,6 +10,9 @@ import type {
import type { Dates } from '../../types';
import { CONFIG } from '../config';
import { ROUTES } from '../constants';
+import { trimTrailingChars } from './strings';
+
+const host = trimTrailingChars(CONFIG.url, '/');
export type GetBlogSchemaProps = {
/**
@@ -38,22 +41,20 @@ export const getBlogSchema = ({
slug,
}: GetBlogSchemaProps): Blog => {
return {
- '@id': `${CONFIG.url}/#blog`,
+ '@id': `${host}/#blog`,
'@type': 'Blog',
- author: { '@id': `${CONFIG.url}/#branding` },
- creator: { '@id': `${CONFIG.url}/#branding` },
- editor: { '@id': `${CONFIG.url}/#branding` },
- blogPost: isSinglePage ? { '@id': `${CONFIG.url}/#article` } : undefined,
+ author: { '@id': `${host}/#branding` },
+ creator: { '@id': `${host}/#branding` },
+ editor: { '@id': `${host}/#branding` },
+ blogPost: isSinglePage ? { '@id': `${host}/#article` } : undefined,
inLanguage: locale,
isPartOf: isSinglePage
? {
- '@id': `${CONFIG.url}${slug}`,
+ '@id': `${host}${slug}`,
}
: undefined,
license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: isSinglePage
- ? undefined
- : { '@id': `${CONFIG.url}${slug}` },
+ mainEntityOfPage: isSinglePage ? undefined : { '@id': `${host}${slug}` },
};
};
@@ -137,19 +138,19 @@ export const getSinglePageSchema = <T extends SinglePageSchemaKind>({
};
return {
- '@id': `${CONFIG.url}/#${id}`,
+ '@id': `${host}/#${id}`,
'@type': singlePageSchemaType[kind],
name: title,
description,
articleBody: content,
- author: { '@id': `${CONFIG.url}/#branding` },
+ author: { '@id': `${host}/#branding` },
commentCount: commentsCount,
copyrightYear: publicationDate.getFullYear(),
- creator: { '@id': `${CONFIG.url}/#branding` },
+ creator: { '@id': `${host}/#branding` },
dateCreated: publicationDate.toISOString(),
dateModified: updateDate?.toISOString(),
datePublished: publicationDate.toISOString(),
- editor: { '@id': `${CONFIG.url}/#branding` },
+ editor: { '@id': `${host}/#branding` },
headline: title,
image: cover,
inLanguage: locale,
@@ -158,10 +159,10 @@ export const getSinglePageSchema = <T extends SinglePageSchemaKind>({
isPartOf:
kind === 'post'
? {
- '@id': `${CONFIG.url}${ROUTES.BLOG}`,
+ '@id': `${host}${ROUTES.BLOG}`,
}
: undefined,
- mainEntityOfPage: { '@id': `${CONFIG.url}${slug}` },
+ mainEntityOfPage: { '@id': `${host}${slug}` },
} as SinglePageSchemaReturn[T];
};
@@ -202,17 +203,17 @@ export const getWebPageSchema = ({
updateDate,
}: GetWebPageSchemaProps): WebPage => {
return {
- '@id': `${CONFIG.url}${slug}`,
+ '@id': `${host}${slug}`,
'@type': 'WebPage',
- breadcrumb: { '@id': `${CONFIG.url}/#breadcrumb` },
+ breadcrumb: { '@id': `${host}/#breadcrumb` },
lastReviewed: updateDate,
name: title,
description,
inLanguage: locale,
- reviewedBy: { '@id': `${CONFIG.url}/#branding` },
- url: `${CONFIG.url}${slug}`,
+ reviewedBy: { '@id': `${host}/#branding` },
+ url: `${host}${slug}`,
isPartOf: {
- '@id': `${CONFIG.url}`,
+ '@id': `${host}`,
},
};
};
diff --git a/src/utils/helpers/strings.ts b/src/utils/helpers/strings.ts
index 8b0f923..b8af61d 100644
--- a/src/utils/helpers/strings.ts
+++ b/src/utils/helpers/strings.ts
@@ -45,3 +45,16 @@ export const getDataAttributeFrom = (str: string) => {
if (str.startsWith('data-')) return str;
return `data-${str}`;
};
+
+/**
+ * Remove the given character if present at the end of the given string.
+ *
+ * @param {string} str - A string to trim.
+ * @param {string} char - The character to remove.
+ * @returns {string} The trimmed string.
+ */
+export const trimTrailingChars = (str: string, char: string): string => {
+ const regExp = new RegExp(`${char}+$`);
+
+ return str.replace(regExp, '');
+};
diff --git a/src/utils/hooks/use-breadcrumb.ts b/src/utils/hooks/use-breadcrumb.ts
index 1cd18d9..8b23ff2 100644
--- a/src/utils/hooks/use-breadcrumb.ts
+++ b/src/utils/hooks/use-breadcrumb.ts
@@ -16,8 +16,7 @@ const isProject = (url: string) => url.startsWith(`${ROUTES.PROJECTS}/`);
const isSearch = (url: string) => url.startsWith(ROUTES.SEARCH);
-const isThematic = (url: string) =>
- url.startsWith(`${ROUTES.THEMATICS.INDEX}/`);
+const isThematic = (url: string) => url.startsWith(`${ROUTES.THEMATICS}/`);
const isTopic = (url: string) => url.startsWith(`${ROUTES.TOPICS}/`);
diff --git a/tests/cypress/e2e/pages/homepage.cy.ts b/tests/cypress/e2e/pages/homepage.cy.ts
index 2d95767..29318be 100644
--- a/tests/cypress/e2e/pages/homepage.cy.ts
+++ b/tests/cypress/e2e/pages/homepage.cy.ts
@@ -1,9 +1,34 @@
import { CONFIG } from '../../../../src/utils/config';
+import { ROUTES } from '../../../../src/utils/constants';
describe('HomePage', () => {
+ beforeEach(() => {
+ cy.visit(ROUTES.HOME);
+ });
+
it('successfully loads', () => {
- cy.visit('/');
cy.findByRole('heading', { level: 1 }).contains(CONFIG.name);
cy.findByText(CONFIG.baseline).should('exist');
});
+
+ it('contains the three most recent articles', () => {
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
+ cy.findAllByRole('link', { name: /^Consulter/i }).should('have.length', 3);
+ });
+
+ it('contains a link to contact me', () => {
+ cy.findByRole('link', { name: 'Me contacter' }).should(
+ 'have.attr',
+ 'href',
+ ROUTES.CONTACT
+ );
+ });
+
+ it('contains a link to RSS feed', () => {
+ cy.findByRole('link', { name: 'S’abonner' }).should(
+ 'have.attr',
+ 'href',
+ ROUTES.RSS
+ );
+ });
});