diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-11-29 18:07:20 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-29 18:07:20 +0100 |
| commit | d363306235f2a48f16e488f20f73e2233ddcf281 (patch) | |
| tree | 5e86a7b5f38416d7ee56a9aff5ef972aa73d82b1 | |
| parent | dfa894b76ee3584bf169710c78c57330c5d6ee67 (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
21 files changed, 241 insertions, 434 deletions
@@ -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 + ); + }); }); |
