diff --git a/README.md b/README.md index ff27f1e7f..75e0a54c0 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,7 @@ excerpt: Description of the article (Visible on the list pages) cover: alt: Alt image path: /imgs/articles/YYYY-MM-DD-slug/cover.jpg + position: top | right top | right | right bottom | bottom | left bottom | left | left top | center | north | northeast | east | southeast | south | southwest | west | northwest // Default value is center categories: - javascript | php | agile | architecture keywords: diff --git a/_articles/fr/2024-02-19-quelques-conseils-pour-optimiser-votre-environnement-de-travail-sous-linux.md b/_articles/fr/2024-02-19-quelques-conseils-pour-optimiser-votre-environnement-de-travail-sous-linux.md index 401e2ee90..94b692ea0 100644 --- a/_articles/fr/2024-02-19-quelques-conseils-pour-optimiser-votre-environnement-de-travail-sous-linux.md +++ b/_articles/fr/2024-02-19-quelques-conseils-pour-optimiser-votre-environnement-de-travail-sous-linux.md @@ -9,6 +9,7 @@ excerpt: >- categories: [] cover: path: /imgs/articles/2024-02-19-quelques-conseils-pour-optimiser-votre-environnement-de-travail-sous-linux/cover.jpg + position: top authors: - nicolas keywords: diff --git a/_tutorials/fr/2024-03-27-chromatic/index.md b/_tutorials/fr/2024-03-27-chromatic/index.md index e801686b9..b3c68d7fd 100644 --- a/_tutorials/fr/2024-03-27-chromatic/index.md +++ b/_tutorials/fr/2024-03-27-chromatic/index.md @@ -8,6 +8,7 @@ excerpt: >- Nous allons découvrir comment utiliser Chromatic en CI pour faire des tests de non régression visuelle et des tests d'interaction sur un Storybook pour être confiant à chaque nouvelle feature implémentée. cover: path: /imgs/tutorials/2024-03-27-chromatic/cover.jpg + position: top categories: - javascript keywords: diff --git a/src/config/i18n/i18n.config.ts b/src/config/i18n/i18n.config.ts index b418ceca3..848de63bb 100644 --- a/src/config/i18n/i18n.config.ts +++ b/src/config/i18n/i18n.config.ts @@ -1,12 +1,12 @@ import { InitOptions } from 'i18next'; -import { BASE_URL, DEFAULT_LANGUAGE, IS_DEBUG, LanguageEnum, LANGUAGES_AVAILABLE } from '@/constants'; +import { BASE_URL, DEFAULT_LANGUAGE, IS_DEBUG, LANGUAGES, LANGUAGES_AVAILABLE } from '@/constants'; export const i18nConfig = { load: 'languageOnly', preload: LANGUAGES_AVAILABLE, whitelist: LANGUAGES_AVAILABLE, - fallbackLng: IS_DEBUG ? LanguageEnum.DT : DEFAULT_LANGUAGE, + fallbackLng: IS_DEBUG ? LANGUAGES.DT : DEFAULT_LANGUAGE, returnEmptyString: false, defaultNS: 'messages', ns: 'messages', diff --git a/src/config/router/routes.tsx b/src/config/router/routes.tsx index b225e2587..805ea5948 100644 --- a/src/config/router/routes.tsx +++ b/src/config/router/routes.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Params, RouteObject } from 'react-router'; import { Outlet } from 'react-router-dom'; -import { LanguageEnum, LANGUAGES_AVAILABLE_WITH_DT, PATHS } from '@/constants'; +import { LANGUAGES_AVAILABLE_WITH_DT, PATHS } from '@/constants'; import { AuthorPageContainer } from '@/containers/AuthorPageContainer'; import { CategoryPageContainer } from '@/containers/CategoryPageContainer'; import { HomePageContainer } from '@/containers/HomePageContainer'; @@ -16,7 +16,7 @@ import { loadPostListPageData, loadPostPageData, } from '@/helpers/loaderDataHelper'; -import { LayoutTemplateData } from '@/types'; +import { LanguageType, LayoutTemplateData } from '@/types'; export const routes: RouteObject[] = [ { @@ -50,8 +50,8 @@ export const routes: RouteObject[] = [ { path: '/:lang/', loader: ({ params }): Record => { - const languages = LANGUAGES_AVAILABLE_WITH_DT as LanguageEnum[]; - if (!languages.includes(params.lang as LanguageEnum)) { + const languages = LANGUAGES_AVAILABLE_WITH_DT as LanguageType[]; + if (!languages.includes(params.lang as LanguageType)) { throw new Error(`The \`${params.lang}\` language doesn't exist`); } return {}; diff --git a/src/config/schemaValidation/AuthorDataValidationSchema.ts b/src/config/schemaValidation/AuthorDataValidationSchema.ts index b55a84c31..0ff03345d 100644 --- a/src/config/schemaValidation/AuthorDataValidationSchema.ts +++ b/src/config/schemaValidation/AuthorDataValidationSchema.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -import { ContentTypeEnum } from '@/constants'; +import { MARKDOWN_CONTENT_TYPES } from '@/constants'; export const AuthorDataValidationSchema = z.object({ - contentType: z.literal(ContentTypeEnum.AUTHOR), + contentType: z.literal(MARKDOWN_CONTENT_TYPES.AUTHOR), username: z.string().regex(/^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/, 'Kebab case format not respected'), name: z.string(), twitter: z diff --git a/src/config/schemaValidation/PostDataSchemaValidation.ts b/src/config/schemaValidation/PostDataSchemaValidation.ts index 1ce932426..e7440c9e7 100644 --- a/src/config/schemaValidation/PostDataSchemaValidation.ts +++ b/src/config/schemaValidation/PostDataSchemaValidation.ts @@ -1,11 +1,14 @@ import { z } from 'zod'; -import { CATEGORIES, ContentTypeEnum, LanguageEnum } from '@/constants'; +import { CATEGORIES, IMAGE_POSITIONS, LANGUAGES, MARKDOWN_CONTENT_TYPES } from '@/constants'; import { intersection } from '@/helpers/objectHelper'; export const PostDataSchemaValidation = z.object({ - contentType: z.enum([ContentTypeEnum.ARTICLE, ContentTypeEnum.TUTORIAL]), - lang: z.nativeEnum(LanguageEnum), + contentType: z.nativeEnum({ + ARTICLE: MARKDOWN_CONTENT_TYPES.ARTICLE, + TUTORIAL: MARKDOWN_CONTENT_TYPES.TUTORIAL, + } as const), + lang: z.nativeEnum(LANGUAGES), date: z.coerce.date().transform((date) => date.toISOString().slice(0, 10)), slug: z.string().regex(/^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/, 'Kebab case format not respected'), title: z.string(), @@ -14,6 +17,7 @@ export const PostDataSchemaValidation = z.object({ cover: z .object({ path: z.string(), + position: z.nativeEnum(IMAGE_POSITIONS).optional(), alt: z.string(), }) .optional(), @@ -48,13 +52,13 @@ export const PostDataSchemaValidation = z.object({ export const ArticleDataSchemaValidation = PostDataSchemaValidation.merge( z.object({ - contentType: z.literal(ContentTypeEnum.ARTICLE), + contentType: z.literal(MARKDOWN_CONTENT_TYPES.ARTICLE), }) ); export const TutorialDataSchemaValidation = PostDataSchemaValidation.merge( z.object({ - contentType: z.literal(ContentTypeEnum.TUTORIAL), + contentType: z.literal(MARKDOWN_CONTENT_TYPES.TUTORIAL), steps: z.array(z.string()), }) ); diff --git a/src/config/schemaValidation/TutorialStepDataValidationSchema.ts b/src/config/schemaValidation/TutorialStepDataValidationSchema.ts index f0bef8eef..47d49fbb9 100644 --- a/src/config/schemaValidation/TutorialStepDataValidationSchema.ts +++ b/src/config/schemaValidation/TutorialStepDataValidationSchema.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -import { ContentTypeEnum } from '@/constants'; +import { MARKDOWN_CONTENT_TYPES } from '@/constants'; export const TutorialStepDataValidationSchema = z.object({ - contentType: z.literal(ContentTypeEnum.TUTORIAL_STEP), + contentType: z.literal(MARKDOWN_CONTENT_TYPES.TUTORIAL_STEP), tutorial: z.string(), slug: z.string(), title: z.string(), diff --git a/src/constants.ts b/src/constants.ts index a439a9b4c..71f312ad9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,5 @@ import { getEnv } from '@/helpers/getEnvHelper'; +import { DeviceType, ImageExtensionType, ImageFormatType } from '@/types'; export const IS_SSR = import.meta.env?.SSR ?? false; export const IS_PRERENDER = import.meta.env?.MODE === 'prerender'; @@ -7,26 +8,25 @@ export const BASE_URL = import.meta.env?.BASE_URL || '/'; export const IS_DEBUG = getEnv('VITE_IS_DEBUG') === 'true'; -export enum LanguageEnum { - FR = 'fr', - EN = 'en', - DT = 'dt', -} +export const LANGUAGES = { + FR: 'fr', + EN: 'en', + DT: 'dt', +} as const; -export const LANGUAGES_AVAILABLE = [LanguageEnum.FR, LanguageEnum.EN] as const; -export const LANGUAGES_AVAILABLE_WITH_DT = IS_DEBUG ? [...LANGUAGES_AVAILABLE, LanguageEnum.DT] : LANGUAGES_AVAILABLE; +export const LANGUAGES_AVAILABLE = [LANGUAGES.FR, LANGUAGES.EN] as const; +export const LANGUAGES_AVAILABLE_WITH_DT = IS_DEBUG ? [...LANGUAGES_AVAILABLE, LANGUAGES.DT] : LANGUAGES_AVAILABLE; -export enum ContentTypeEnum { - ARTICLE = 'article', - TUTORIAL = 'tutorial', - TUTORIAL_STEP = 'tutorial-step', - AUTHOR = 'author', -} +export const MARKDOWN_CONTENT_TYPES = { + ARTICLE: 'article', + TUTORIAL: 'tutorial', + TUTORIAL_STEP: 'tutorial-step', + AUTHOR: 'author', +} as const; export const CATEGORIES = ['javascript', 'php', 'agile', 'architecture'] as const; -export type CategoryEnum = (typeof CATEGORIES)[number]; -export const DEFAULT_LANGUAGE = LanguageEnum.FR; +export const DEFAULT_LANGUAGE = LANGUAGES.FR; export const NUMBER_OF_ITEMS_FOR_SEARCH = 6; export const NUMBER_OF_ITEMS_PER_PAGE = 12; @@ -51,53 +51,83 @@ export const GTM_ID = getEnv('VITE_GTM_ID'); export const GOOGLE_SITE_VERIFICATION = getEnv('VITE_GOOGLE_SITE_VERIFICATION'); -export enum ImageFormatEnum { - HIGHLIGHTED_ARTICLE_POST_CARD_COVER = 'highlighted-article-post-card-cover', - HIGHLIGHTED_TUTORIAL_POST_CARD_COVER = 'highlighted-tutorial-post-card-cover', - POST_CARD_COVER = 'post-card-cover', - POST_COVER = 'post-cover', -} - -export enum DeviceEnum { - MOBILE = 'mobile', - DESKTOP = 'desktop', -} - -export const IMAGE_FORMATS: Record> = { - mobile: { - [ImageFormatEnum.HIGHLIGHTED_ARTICLE_POST_CARD_COVER]: { - width: 67, - height: 67, - }, - [ImageFormatEnum.HIGHLIGHTED_TUTORIAL_POST_CARD_COVER]: { - width: 328, - height: 130, - }, - [ImageFormatEnum.POST_CARD_COVER]: { - width: 67, - height: 67, - }, - [ImageFormatEnum.POST_COVER]: { - width: 330, - height: 160, - }, - }, - desktop: { - [ImageFormatEnum.HIGHLIGHTED_ARTICLE_POST_CARD_COVER]: { +export const IMAGE_FORMATS = { + HIGHLIGHTED_ARTICLE_POST_CARD_COVER: 'highlighted-article-post-card-cover', + HIGHLIGHTED_TUTORIAL_POST_CARD_COVER: 'highlighted-tutorial-post-card-cover', + POST_CARD_COVER: 'post-card-cover', + POST_COVER: 'post-cover', +} as const; + +export const DEVICES = { + DESKTOP: 'desktop', + MOBILE: 'mobile', +} as const; + +export const IMAGE_CONTENT_TYPES = { + jpeg: 'image/jpeg', + gif: 'image/gif', + webp: 'image/webp', + png: 'image/png', + avif: 'image/avif', +} as const; + +export const IMAGE_POSITIONS = { + TOP: 'top', + RIGHT_AND_TOP: 'right top', + RIGHT: 'right', + RIGHT_BOTTOM: 'right bottom', + BOTTOM: 'bottom', + LEFT_AND_BOTTOM: 'left bottom', + LEFT: 'left', + LEFT_TOP: 'left top', + CENTER: 'center', + NORTH: 'north', + NORTHEAST: 'northeast', + EAST: 'east', + SOUTHEAST: 'southeast', + SOUTH: 'south', + SOUTHWEST: 'southwest', + WEST: 'west', + NORTHWEST: 'northwest', +} as const; + +export const DEFAULT_EXTENSION_FOR_IMAGES: ImageExtensionType = 'avif'; + +export const SIZES_BY_IMAGE_FORMAT: Record> = { + [DEVICES.DESKTOP]: { + [IMAGE_FORMATS.HIGHLIGHTED_ARTICLE_POST_CARD_COVER]: { width: 385, height: 175, }, - [ImageFormatEnum.HIGHLIGHTED_TUTORIAL_POST_CARD_COVER]: { + [IMAGE_FORMATS.HIGHLIGHTED_TUTORIAL_POST_CARD_COVER]: { width: 400, height: 245, }, - [ImageFormatEnum.POST_CARD_COVER]: { + [IMAGE_FORMATS.POST_CARD_COVER]: { width: 190, height: 190, }, - [ImageFormatEnum.POST_COVER]: { + [IMAGE_FORMATS.POST_COVER]: { width: 1200, height: 330, }, }, + [DEVICES.MOBILE]: { + [IMAGE_FORMATS.HIGHLIGHTED_ARTICLE_POST_CARD_COVER]: { + width: 67, + height: 67, + }, + [IMAGE_FORMATS.HIGHLIGHTED_TUTORIAL_POST_CARD_COVER]: { + width: 328, + height: 130, + }, + [IMAGE_FORMATS.POST_CARD_COVER]: { + width: 67, + height: 67, + }, + [IMAGE_FORMATS.POST_COVER]: { + width: 330, + height: 160, + }, + }, } as const; diff --git a/src/containers/ArticlePageContainer/useArticlePageContainer.tsx b/src/containers/ArticlePageContainer/useArticlePageContainer.tsx index 0a6ac6a4f..5603822a7 100644 --- a/src/containers/ArticlePageContainer/useArticlePageContainer.tsx +++ b/src/containers/ArticlePageContainer/useArticlePageContainer.tsx @@ -2,7 +2,7 @@ import { Box, PostPageProps } from '@eleven-labs/design-system'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { ContentTypeEnum } from '@/constants'; +import { MARKDOWN_CONTENT_TYPES } from '@/constants'; import { slugify } from '@/helpers/stringHelper'; import { usePostPage } from '@/hooks/usePostPage'; import { ArticlePageData } from '@/types'; @@ -12,7 +12,7 @@ export const useArticlePageContainer = (article: ArticlePageData): PostPageProps const postPage = usePostPage(article); return { - variant: ContentTypeEnum.ARTICLE, + variant: MARKDOWN_CONTENT_TYPES.ARTICLE, ...postPage, summary: { title: t('pages.article.summary_card.title'), diff --git a/src/containers/CategoryPageContainer/useCategoryPageContainer.tsx b/src/containers/CategoryPageContainer/useCategoryPageContainer.tsx index 459875e86..c48116c51 100644 --- a/src/containers/CategoryPageContainer/useCategoryPageContainer.tsx +++ b/src/containers/CategoryPageContainer/useCategoryPageContainer.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import { useLoaderData, useParams } from 'react-router-dom'; import { blogUrl } from '@/config/website'; -import { ContentTypeEnum, DEFAULT_LANGUAGE, PATHS } from '@/constants'; +import { DEFAULT_LANGUAGE, MARKDOWN_CONTENT_TYPES, PATHS } from '@/constants'; import { PostCardListContainer, PostCardListContainerProps } from '@/containers/PostCardListContainer'; import { TransWithHtml } from '@/containers/TransWithHtml'; import { generatePath } from '@/helpers/routerHelper'; @@ -40,7 +40,7 @@ export const useCategoryPageContainer = (): CategoryPageProps => { title: , description: , }, - categoryEndingBlock: !['all', ContentTypeEnum.TUTORIAL].includes(categoryName as string) + categoryEndingBlock: !['all', MARKDOWN_CONTENT_TYPES.TUTORIAL].includes(categoryName as string) ? { title: , description: , diff --git a/src/containers/HomePageContainer/useHomePageContainer.tsx b/src/containers/HomePageContainer/useHomePageContainer.tsx index 32f4d9f5d..11b760d69 100644 --- a/src/containers/HomePageContainer/useHomePageContainer.tsx +++ b/src/containers/HomePageContainer/useHomePageContainer.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import { useLoaderData } from 'react-router-dom'; import { blogUrl, websiteUrl } from '@/config/website'; -import { ContentTypeEnum, DEFAULT_LANGUAGE, ImageFormatEnum, LanguageEnum, PATHS } from '@/constants'; +import { DEFAULT_LANGUAGE, IMAGE_FORMATS, LANGUAGES, MARKDOWN_CONTENT_TYPES, PATHS } from '@/constants'; import { TransWithHtml } from '@/containers/TransWithHtml'; import { generatePath } from '@/helpers/routerHelper'; import { useNewsletterCard } from '@/hooks/useNewsletterCard'; @@ -22,21 +22,21 @@ export const useHomePageContainer = (): HomePageProps => { posts: postListPageData.posts .filter( (post) => - post.contentType === ContentTypeEnum.ARTICLE && - (i18n.language === LanguageEnum.DT || post.lang === i18n.language) + post.contentType === MARKDOWN_CONTENT_TYPES.ARTICLE && + (i18n.language === LANGUAGES.DT || post.lang === i18n.language) ) .slice(0, 3), - imageFormatEnum: ImageFormatEnum.HIGHLIGHTED_ARTICLE_POST_CARD_COVER, + imageFormat: IMAGE_FORMATS.HIGHLIGHTED_ARTICLE_POST_CARD_COVER, }); const lastTutorialsForCardList = usePostsForCardList({ posts: postListPageData.posts .filter( (post) => - post.contentType === ContentTypeEnum.TUTORIAL && - (i18n.language === LanguageEnum.DT || post.lang === i18n.language) + post.contentType === MARKDOWN_CONTENT_TYPES.TUTORIAL && + (i18n.language === LANGUAGES.DT || post.lang === i18n.language) ) .slice(0, 2), - imageFormatEnum: ImageFormatEnum.HIGHLIGHTED_TUTORIAL_POST_CARD_COVER, + imageFormat: IMAGE_FORMATS.HIGHLIGHTED_TUTORIAL_POST_CARD_COVER, }); useTitle(t('pages.home.seo.title')); @@ -73,7 +73,7 @@ export const useHomePageContainer = (): HomePageProps => { posts: lastTutorialsForCardList, linkSeeMore: { label: t('pages.home.last-tutorials-block.link-see-more'), - href: generatePath(PATHS.CATEGORY, { categoryName: ContentTypeEnum.TUTORIAL, lang: i18n.language }), + href: generatePath(PATHS.CATEGORY, { categoryName: MARKDOWN_CONTENT_TYPES.TUTORIAL, lang: i18n.language }), }, } : undefined, diff --git a/src/containers/PostCardListContainer/usePostCardListContainer.ts b/src/containers/PostCardListContainer/usePostCardListContainer.ts index 59b91d38a..7e3ca9a0b 100644 --- a/src/containers/PostCardListContainer/usePostCardListContainer.ts +++ b/src/containers/PostCardListContainer/usePostCardListContainer.ts @@ -1,6 +1,6 @@ import { PostCardListProps } from '@eleven-labs/design-system'; -import { ImageFormatEnum, NUMBER_OF_ITEMS_PER_PAGE } from '@/constants'; +import { IMAGE_FORMATS, NUMBER_OF_ITEMS_PER_PAGE } from '@/constants'; import { usePostsForCardList } from '@/hooks/usePostsForCardList'; import { PostCardListContainerProps } from './PostCardListContainer'; @@ -20,7 +20,7 @@ export const usePostCardListContainer = ({ isLoading, numberOfItems: NUMBER_OF_ITEMS_PER_PAGE, posts, - imageFormatEnum: ImageFormatEnum.POST_CARD_COVER, + imageFormat: IMAGE_FORMATS.POST_CARD_COVER, }); return { diff --git a/src/containers/PostPageContainer/PostPageContainer.tsx b/src/containers/PostPageContainer/PostPageContainer.tsx index 1cf6e6fee..609b9ead4 100644 --- a/src/containers/PostPageContainer/PostPageContainer.tsx +++ b/src/containers/PostPageContainer/PostPageContainer.tsx @@ -2,7 +2,7 @@ import { useScript } from 'hoofd'; import React from 'react'; import { useLoaderData } from 'react-router-dom'; -import { ContentTypeEnum } from '@/constants'; +import { MARKDOWN_CONTENT_TYPES } from '@/constants'; import { ArticlePageContainer } from '@/containers/ArticlePageContainer'; import { NotFoundPageContainer } from '@/containers/NotFoundPageContainer'; import { TutorialPageContainer } from '@/containers/TutorialPageContainer'; @@ -22,7 +22,7 @@ export const PostPageContainer: React.FC = () => { return ; } - if (postPageData.contentType === ContentTypeEnum.TUTORIAL) { + if (postPageData.contentType === MARKDOWN_CONTENT_TYPES.TUTORIAL) { return ; } diff --git a/src/containers/SearchPageContainer/useSearchPageContentContainer.tsx b/src/containers/SearchPageContainer/useSearchPageContentContainer.tsx index e2fc53459..6840b727c 100644 --- a/src/containers/SearchPageContainer/useSearchPageContentContainer.tsx +++ b/src/containers/SearchPageContainer/useSearchPageContentContainer.tsx @@ -4,13 +4,13 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { blogUrl } from '@/config/website'; -import { DEFAULT_LANGUAGE, IS_SSR, LanguageEnum, PATHS } from '@/constants'; +import { DEFAULT_LANGUAGE, IS_SSR, PATHS } from '@/constants'; import { PostCardListContainer, PostCardListContainerProps } from '@/containers/PostCardListContainer'; import { TransWithHtml } from '@/containers/TransWithHtml'; import { generatePath } from '@/helpers/routerHelper'; import { useAlgoliaSearchIndex } from '@/hooks/useAlgoliaSearchIndex'; import { useTitle } from '@/hooks/useTitle'; -import { AlgoliaPostData } from '@/types'; +import { AlgoliaPostData, LanguageType } from '@/types'; export const useSearchPageContentContainer = (): SearchPageContentProps => { const { t, i18n } = useTranslation(); @@ -35,7 +35,7 @@ export const useSearchPageContentContainer = (): SearchPageContentProps => { const currentPostBySearch = response.hits.map((hit) => ({ contentType: hit.contentType, - lang: hit.lang as LanguageEnum, + lang: hit.lang as LanguageType, slug: hit.slug, date: hit.date, readingTime: hit.readingTime, diff --git a/src/containers/TutorialPageContainer/useTutorialPageContainer.tsx b/src/containers/TutorialPageContainer/useTutorialPageContainer.tsx index 1923f83df..b79774721 100644 --- a/src/containers/TutorialPageContainer/useTutorialPageContainer.tsx +++ b/src/containers/TutorialPageContainer/useTutorialPageContainer.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; -import { ContentTypeEnum, PATHS } from '@/constants'; +import { MARKDOWN_CONTENT_TYPES, PATHS } from '@/constants'; import { generatePath } from '@/helpers/routerHelper'; import { usePostPage } from '@/hooks/usePostPage'; import { TutorialPageData } from '@/types'; @@ -20,7 +20,7 @@ export const useTutorialPageContainer = (tutorial: TutorialPageData): PostPagePr const nextStep = tutorial.steps[Number(currentTutorialStepIndex) + 1]; return { - variant: ContentTypeEnum.TUTORIAL, + variant: MARKDOWN_CONTENT_TYPES.TUTORIAL, ...postPageProps, summary: { title: t('pages.tutorial.summary_card.title'), diff --git a/src/entry-server.tsx b/src/entry-server.tsx index acea12933..976b5a2e2 100644 --- a/src/entry-server.tsx +++ b/src/entry-server.tsx @@ -7,7 +7,7 @@ import ReactDOMServer from 'react-dom/server'; import { createStaticRouter, StaticRouterProvider } from 'react-router-dom/server'; import { routes } from '@/config/router'; -import { BASE_URL, IS_DEBUG, LanguageEnum } from '@/constants'; +import { BASE_URL, IS_DEBUG, LANGUAGES } from '@/constants'; import { RootContainer } from '@/containers/RootContainer'; import { HtmlTemplate, HtmlTemplateProps } from '@/templates/HtmlTemplate'; @@ -24,7 +24,7 @@ export const render = async (options: RenderOptions): Promise => { if (IS_DEBUG) { const isHomePage = new URL(options.request.url).pathname.replace(BASE_URL, '') === ''; if (isHomePage) { - await options.i18n.changeLanguage(LanguageEnum.FR); + await options.i18n.changeLanguage(LANGUAGES.FR); } } diff --git a/src/helpers/assetHelper.ts b/src/helpers/assetHelper.ts index 46e2caa7f..da07e43e7 100644 --- a/src/helpers/assetHelper.ts +++ b/src/helpers/assetHelper.ts @@ -1,7 +1,20 @@ import type { PictureProps } from '@eleven-labs/design-system'; -import { BASE_URL, DeviceEnum, IMAGE_FORMATS, ImageFormatEnum } from '@/constants'; -import { TransformedPostDataWithTransformedAuthors } from '@/types'; +import { + BASE_URL, + DEFAULT_EXTENSION_FOR_IMAGES, + DEVICES, + IMAGE_CONTENT_TYPES, + IMAGE_POSITIONS, + SIZES_BY_IMAGE_FORMAT, +} from '@/constants'; +import { + DeviceType, + ImageExtensionType, + ImageFormatType, + ImagePositionType, + TransformedPostDataWithTransformedAuthors, +} from '@/types'; const basename = (path: string, extension: string = ''): string => { const filename = path.split('/').pop() || ''; @@ -17,59 +30,69 @@ export const getCoverPath = ({ path = '/imgs/default-cover.jpg', format, device, - pixelRatio = 1, + pixelRatio, + extension = DEFAULT_EXTENSION_FOR_IMAGES, + position = IMAGE_POSITIONS.CENTER, }: { path?: string; - format: ImageFormatEnum; - device: DeviceEnum; - pixelRatio?: number; + format: ImageFormatType; + device: DeviceType; + pixelRatio: number; + extension?: ImageExtensionType; + position?: ImagePositionType; }): string => { const isProd: boolean = process.env.NODE_ENV === 'production'; const directoryPath = dirname(path); const filename = basename(path, extname(path)); - const imageFormat = IMAGE_FORMATS[device][format]; + const imageFormat = SIZES_BY_IMAGE_FORMAT[device][format]; const pathFile = isProd - ? `${directoryPath}/${filename}-w${imageFormat.width * pixelRatio}-h${imageFormat.height * pixelRatio}.avif` - : `${path}?width=${imageFormat.width * pixelRatio}&height=${imageFormat.height * pixelRatio}&format=avif`; + ? `${directoryPath}/${filename}-w${imageFormat.width}-h${imageFormat.height}-x${pixelRatio}.${extension}` + : `${path}?width=${imageFormat.width}&height=${imageFormat.height}&pixelRatio=${pixelRatio}&position=${position}&format=${extension}`; return getPathFile(pathFile); }; -export const getCover = (post: TransformedPostDataWithTransformedAuthors, format: ImageFormatEnum): PictureProps => ({ - sources: [ - { - media: '(max-width: 571px)', - srcSet: `${getCoverPath({ - path: post.cover?.path, - format, - pixelRatio: 2, - device: DeviceEnum.MOBILE, - })} 2x`, - type: 'image/jpeg', - }, - { - media: '(min-width: 572px)', - srcSet: `${getCoverPath({ - path: post.cover?.path, - format, - pixelRatio: 2, - device: DeviceEnum.DESKTOP, - })} 2x`, - type: 'image/jpeg', - }, - ], +export const getSrcSet = ( + options: Omit[0], 'pixelRatio'> & { pixelRatios: number[] } +): string => + options.pixelRatios.map((pixelRatio) => `${getCoverPath({ ...options, pixelRatio })} ${pixelRatio}x`).join(', '); + +export const getMediaByDevice = (device: DeviceType): string => + device === DEVICES.DESKTOP ? '(min-width: 572px)' : '(max-width: 571px)'; + +export const getSources = (options: { + path?: string; + format: ImageFormatType; + position?: ImagePositionType; +}): PictureProps['sources'] => + Object.values(DEVICES).map((device) => ({ + media: getMediaByDevice(device), + srcSet: getSrcSet({ + path: options.path, + format: options.format, + device, + pixelRatios: [2, 1], + position: options.position, + }), + type: IMAGE_CONTENT_TYPES[DEFAULT_EXTENSION_FOR_IMAGES], + })); + +export const getCover = (post: TransformedPostDataWithTransformedAuthors, format: ImageFormatType): PictureProps => ({ + sources: getSources({ path: post.cover?.path, format, position: post?.cover?.position as ImagePositionType }), img: { src: getCoverPath({ path: post.cover?.path, format, - pixelRatio: 2, - device: DeviceEnum.DESKTOP, + pixelRatio: 1, + device: DEVICES.DESKTOP, + position: post?.cover?.position as ImagePositionType, }), alt: post.cover?.alt ?? post.title, - width: IMAGE_FORMATS[DeviceEnum.DESKTOP][format].width, - height: IMAGE_FORMATS[DeviceEnum.DESKTOP][format].height, + width: SIZES_BY_IMAGE_FORMAT[DEVICES.DESKTOP][format].width, + height: SIZES_BY_IMAGE_FORMAT[DEVICES.DESKTOP][format].height, loading: 'eager', decoding: 'sync', + fetchPriority: 'high', }, }); diff --git a/src/helpers/contentHelper.ts b/src/helpers/contentHelper.ts index 5c3fda520..add649493 100644 --- a/src/helpers/contentHelper.ts +++ b/src/helpers/contentHelper.ts @@ -2,7 +2,7 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { DATA_DIR } from '@/app-paths'; -import { CATEGORIES, ContentTypeEnum, LanguageEnum, LANGUAGES_AVAILABLE_WITH_DT } from '@/constants'; +import { CATEGORIES, LANGUAGES, LANGUAGES_AVAILABLE_WITH_DT, MARKDOWN_CONTENT_TYPES } from '@/constants'; import { getArticles, getAuthors, getTutorials } from '@/helpers/markdownContentManagerHelper'; import { intersection } from '@/helpers/objectHelper'; import { @@ -91,7 +91,7 @@ export const getLayoutTemplateData = (options: { ), ]; const hasTutorial = options.posts.some( - (post) => post.lang === options.lang && post?.contentType === ContentTypeEnum.TUTORIAL + (post) => post.lang === options.lang && post?.contentType === MARKDOWN_CONTENT_TYPES.TUTORIAL ); return { @@ -136,11 +136,11 @@ export const writeJsonDataFiles = (): void => { const authors = getAuthors(); for (const lang of LANGUAGES_AVAILABLE_WITH_DT) { - const articlesByLang = articles.filter((article) => lang === LanguageEnum.DT || article.lang === lang); - const tutorialsByLang = tutorials.filter((tutorial) => lang === LanguageEnum.DT || tutorial.lang === lang); + const articlesByLang = articles.filter((article) => lang === LANGUAGES.DT || article.lang === lang); + const tutorialsByLang = tutorials.filter((tutorial) => lang === LANGUAGES.DT || tutorial.lang === lang); const postsByLang = [...articlesByLang, ...tutorialsByLang] as TransformedPostData[]; const postsByLangWithoutContentOrSteps: TransformedPostDataWithoutContent[] = postsByLang.map((post) => { - if (post.contentType === ContentTypeEnum.TUTORIAL) { + if (post.contentType === MARKDOWN_CONTENT_TYPES.TUTORIAL) { const { steps, ...tutorial } = post; return tutorial; } diff --git a/src/helpers/downloadTranslationsHelper.ts b/src/helpers/downloadTranslationsHelper.ts index 63dfa0cff..e4eed17e1 100644 --- a/src/helpers/downloadTranslationsHelper.ts +++ b/src/helpers/downloadTranslationsHelper.ts @@ -2,12 +2,12 @@ import fetch from 'cross-fetch'; import { writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; -import { LanguageEnum, LANGUAGES_AVAILABLE } from '@/constants'; +import { LANGUAGES, LANGUAGES_AVAILABLE } from '@/constants'; const getTranslations = (lang: (typeof LANGUAGES_AVAILABLE)[number]): Promise => { - const locales: Record = { - [LanguageEnum.FR]: 'fr-FR', - [LanguageEnum.EN]: 'en-GB', + const locales: Record<'fr' | 'en', string> = { + [LANGUAGES.FR]: 'fr-FR', + [LANGUAGES.EN]: 'en-GB', }; return fetch(`https://localise.biz/api/export/locale/${locales[lang]}.json?key=${process.env.LOCO_API_KEY}`) diff --git a/src/helpers/generateImageFormats.ts b/src/helpers/generateImageFormats.ts index e70cea3f8..29f65181e 100644 --- a/src/helpers/generateImageFormats.ts +++ b/src/helpers/generateImageFormats.ts @@ -1,61 +1,75 @@ -import { readFileSync, writeFileSync } from 'node:fs'; import { basename, dirname, extname, resolve } from 'node:path'; -import Sharp, { FormatEnum } from 'sharp'; +import Sharp, { FormatEnum, OutputInfo } from 'sharp'; -import { DeviceEnum, IMAGE_FORMATS } from '@/constants'; +import { DEFAULT_EXTENSION_FOR_IMAGES, DEVICES, SIZES_BY_IMAGE_FORMAT } from '@/constants'; import { getPosts } from '@/helpers/markdownContentManagerHelper'; +import { ImagePositionType } from '@/types'; const resizeImage = async (options: { - imagePathOrBuffer: string | Buffer; + imagePath: string; directoryPath: string; filename: string; width: number; height: number; - extension: keyof FormatEnum; -}): Promise => { - const transformedImage = Sharp(options.imagePathOrBuffer, { failOn: 'none', animated: true }) - .resize({ width: options.width, height: options.height }) - .toFormat(options.extension); - - const transformedBuffer = await transformedImage.toBuffer(); - + pixelRatio: number; + extension?: keyof FormatEnum; + position?: ImagePositionType; +}): Promise => { + const extension = options?.extension ?? DEFAULT_EXTENSION_FOR_IMAGES; + const width = options.width * options.pixelRatio; + const height = options.height * options.pixelRatio; + const position = options?.position ?? 'center'; const imageDestPath = resolve( options.directoryPath, - `${options.filename}-w${options.width}-h${options.height}.${options.extension}` + `${options.filename}-w${options.width}-h${options.height}-x${options.pixelRatio}.${extension}` ); - writeFileSync(imageDestPath, transformedBuffer); + + const sharpImage = Sharp(options.imagePath, { failOn: 'none', animated: true }); + const metadata = await sharpImage.metadata(); + + if (metadata?.width && metadata?.height && metadata.width < width && metadata.height < height) { + console.info( + `The image "${options.imagePath}" is too small, you need to enlarge it to be able to resize it (${options.width}x${options.height})` + ); + } + + const transformedImage = sharpImage.resize({ width, height, position, withoutEnlargement: true }).toFormat(extension); + return transformedImage.toFile(imageDestPath); }; export const generateImageFormats = async (): Promise => { const posts = getPosts(); - const transformedBufferPromises: Promise[] = []; + performance.mark('generate-image-formats-start'); - const covers = posts.filter((post) => post.cover?.path).map((post) => post.cover?.path); - covers.push('/imgs/default-cover.jpg'); + const transformedBufferPromises: Promise[] = []; + const covers: { path: string; position?: ImagePositionType }[] = posts + .filter((post) => post.cover?.path) + .map((post) => ({ + path: post.cover!.path, + position: post.cover?.position, + })); + covers.push({ + path: '/imgs/default-cover.jpg', + }); - performance.mark('generate-image-formats-start'); - for (const cover of covers) { - const imagePath = resolve(process.cwd(), 'public', cover!.slice(1) as string); + for (const { path, position } of covers) { + const imagePath = resolve(process.cwd(), 'public', path!.slice(1) as string); const directoryPath = dirname(imagePath); const filename = basename(imagePath, extname(imagePath)); - const extension = 'avif'; - - const originalImageBuffer = readFileSync(imagePath); - for (const device of Object.values(DeviceEnum)) { - for (const pixelRatio of [1, 2]) { - for (const format of Object.values(IMAGE_FORMATS[device])) { - const width = format.width * pixelRatio; - const height = format.height * pixelRatio; + for (const device of Object.values(DEVICES)) { + for (const format of Object.values(SIZES_BY_IMAGE_FORMAT[device])) { + for (const pixelRatio of [2, 1]) { transformedBufferPromises.push( resizeImage({ - imagePathOrBuffer: originalImageBuffer, + imagePath, filename, directoryPath, - width, - height, - extension, + width: format.width, + height: format.height, + pixelRatio, + position, }) ); } diff --git a/src/helpers/loaderDataHelper.ts b/src/helpers/loaderDataHelper.ts index aa9a7b5c6..ba8301b7f 100644 --- a/src/helpers/loaderDataHelper.ts +++ b/src/helpers/loaderDataHelper.ts @@ -1,8 +1,15 @@ import { LoaderFunctionArgs } from '@remix-run/router/utils'; import fetch from 'cross-fetch'; -import { BASE_URL, CategoryEnum, ContentTypeEnum, IS_PRERENDER, IS_SSR } from '@/constants'; -import { ArticlePageData, AuthorPageData, LayoutTemplateData, PostListPageData, TutorialPageData } from '@/types'; +import { BASE_URL, IS_PRERENDER, IS_SSR, MARKDOWN_CONTENT_TYPES } from '@/constants'; +import { + ArticlePageData, + AuthorPageData, + CategoryType, + LayoutTemplateData, + PostListPageData, + TutorialPageData, +} from '@/types'; const cache = new Map(); @@ -46,11 +53,11 @@ export const loadPostListPageData = async (options: LoaderFunctionArgs): Promise if (options.params.categoryName) { const postsByCategoryName = dataFromPostListPage.posts.filter((post) => - options.params.categoryName === ContentTypeEnum.TUTORIAL - ? post.contentType === ContentTypeEnum.TUTORIAL + options.params.categoryName === MARKDOWN_CONTENT_TYPES.TUTORIAL + ? post.contentType === MARKDOWN_CONTENT_TYPES.TUTORIAL : options.params.categoryName === 'all' ? true - : post?.categories?.includes(options.params.categoryName as CategoryEnum) + : post?.categories?.includes(options.params.categoryName as CategoryType) ); if (postsByCategoryName.length === 0) { throw new Error( diff --git a/src/helpers/markdownContentManagerHelper.ts b/src/helpers/markdownContentManagerHelper.ts index 0e6122325..4b4eb7a82 100644 --- a/src/helpers/markdownContentManagerHelper.ts +++ b/src/helpers/markdownContentManagerHelper.ts @@ -9,7 +9,7 @@ import { unified } from 'unified'; import { visit } from 'unist-util-visit'; import { ASSETS_DIR, MARKDOWN_FILE_PATHS } from '@/app-paths'; -import { ContentTypeEnum } from '@/constants'; +import { MARKDOWN_CONTENT_TYPES } from '@/constants'; import { getPathFile } from '@/helpers/assetHelper'; import { markdownToHtml as defaultMarkdownToHtml } from '@/helpers/markdownToHtmlHelper'; import { @@ -94,7 +94,7 @@ const getReadingTime = (content: string): number => { }; export const getAuthors = (): TransformedAuthorData[] => - getCollection(ContentTypeEnum.AUTHOR).reduce( + getCollection(MARKDOWN_CONTENT_TYPES.AUTHOR).reduce( (currentAuthors, { data, html }) => { const avatarImageFileNames = globSync(`${data.username}.*`, { cwd: resolve(ASSETS_DIR, 'authors') }); currentAuthors.push({ @@ -118,7 +118,7 @@ export const getAuthors = (): TransformedAuthorData[] => ); export const getArticles = (): TransformedArticleData[] => - getCollection(ContentTypeEnum.ARTICLE).reduce( + getCollection(MARKDOWN_CONTENT_TYPES.ARTICLE).reduce( (currentArticles, { data, content, html }) => { currentArticles.push({ contentType: data.contentType, @@ -140,8 +140,8 @@ export const getArticles = (): TransformedArticleData[] => ); export const getTutorials = (): TransformedTutorialData[] => { - const tutorialSteps = getCollection(ContentTypeEnum.TUTORIAL_STEP); - return getCollection(ContentTypeEnum.TUTORIAL).reduce( + const tutorialSteps = getCollection(MARKDOWN_CONTENT_TYPES.TUTORIAL_STEP); + return getCollection(MARKDOWN_CONTENT_TYPES.TUTORIAL).reduce( (currentTutorials, { data }) => { const steps = data.steps.reduce((currentSteps, step) => { const currentStep = tutorialSteps.find( diff --git a/src/helpers/prerenderHelper/generateFeedFile.ts b/src/helpers/prerenderHelper/generateFeedFile.ts index 61a29a737..7cfbb7bf1 100644 --- a/src/helpers/prerenderHelper/generateFeedFile.ts +++ b/src/helpers/prerenderHelper/generateFeedFile.ts @@ -4,7 +4,7 @@ import { resolve } from 'node:path'; import sanitizeHtml from 'sanitize-html'; import { blogUrl } from '@/config/website'; -import { ContentTypeEnum, PATHS } from '@/constants'; +import { MARKDOWN_CONTENT_TYPES, PATHS } from '@/constants'; import { getPosts } from '@/helpers/markdownContentManagerHelper'; import { generatePath } from '@/helpers/routerHelper'; @@ -34,7 +34,7 @@ export const generateFeedFile = (options: { rootDir: string }): void => { link: url, date: new Date(post.date), description: post.excerpt, - content: post.contentType === ContentTypeEnum.ARTICLE ? sanitizeHtml(post.content) : undefined, + content: post.contentType === MARKDOWN_CONTENT_TYPES.ARTICLE ? sanitizeHtml(post.content) : undefined, }); } diff --git a/src/helpers/prerenderHelper/getSitemapEntries.test.ts b/src/helpers/prerenderHelper/getSitemapEntries.test.ts index e37437515..e303007e8 100644 --- a/src/helpers/prerenderHelper/getSitemapEntries.test.ts +++ b/src/helpers/prerenderHelper/getSitemapEntries.test.ts @@ -8,7 +8,6 @@ describe('getSitemapEntries', () => { ...mod, IS_DEBUG: false, LANGUAGES_AVAILABLE_WITH_DT: mod.LANGUAGES_AVAILABLE, - CategoryEnum: ['category-1'], }; }); vi.mock('@/helpers/markdownContentManagerHelper', () => ({ diff --git a/src/helpers/prerenderHelper/getUrls.test.ts b/src/helpers/prerenderHelper/getUrls.test.ts index 087226144..1ec8a500b 100644 --- a/src/helpers/prerenderHelper/getUrls.test.ts +++ b/src/helpers/prerenderHelper/getUrls.test.ts @@ -1,4 +1,4 @@ -import { ContentTypeEnum, LanguageEnum } from '@/constants'; +import { LANGUAGES, MARKDOWN_CONTENT_TYPES } from '@/constants'; import { getAuthorPageUrls, @@ -33,11 +33,11 @@ describe('getSitemapEntries', () => { }>([ { mockPosts: [ - { lang: LanguageEnum.FR, categories: ['architecture'] }, - { lang: LanguageEnum.FR, categories: ['php'] }, - { lang: LanguageEnum.EN, categories: ['architecture'] }, - { lang: LanguageEnum.FR, contentType: ContentTypeEnum.TUTORIAL, categories: [] }, - { lang: LanguageEnum.EN, contentType: ContentTypeEnum.TUTORIAL, categories: [] }, + { lang: LANGUAGES.FR, categories: ['architecture'] }, + { lang: LANGUAGES.FR, categories: ['php'] }, + { lang: LANGUAGES.EN, categories: ['architecture'] }, + { lang: LANGUAGES.FR, contentType: MARKDOWN_CONTENT_TYPES.TUTORIAL, categories: [] }, + { lang: LANGUAGES.EN, contentType: MARKDOWN_CONTENT_TYPES.TUTORIAL, categories: [] }, ] as Parameters[0], expectedUrls: [ [ @@ -57,15 +57,15 @@ describe('getSitemapEntries', () => { }, { mockPosts: [ - ...Array.from({ length: 15 }).map(() => ({ lang: LanguageEnum.FR, categories: ['architecture'] })), - ...Array.from({ length: 15 }).map(() => ({ lang: LanguageEnum.FR, categories: ['php'] })), - ...Array.from({ length: 15 }).map(() => ({ lang: LanguageEnum.EN, categories: ['architecture'] })), + ...Array.from({ length: 15 }).map(() => ({ lang: LANGUAGES.FR, categories: ['architecture'] })), + ...Array.from({ length: 15 }).map(() => ({ lang: LANGUAGES.FR, categories: ['php'] })), + ...Array.from({ length: 15 }).map(() => ({ lang: LANGUAGES.EN, categories: ['architecture'] })), ...Array.from({ length: 15 }).map(() => ({ - lang: LanguageEnum.FR, - contentType: ContentTypeEnum.TUTORIAL, + lang: LANGUAGES.FR, + contentType: MARKDOWN_CONTENT_TYPES.TUTORIAL, categories: [], })), - { lang: LanguageEnum.EN, contentType: ContentTypeEnum.TUTORIAL, categories: [] }, + { lang: LANGUAGES.EN, contentType: MARKDOWN_CONTENT_TYPES.TUTORIAL, categories: [] }, ] as Parameters[0], expectedUrls: [ [ @@ -111,8 +111,8 @@ describe('getSitemapEntries', () => { it('should return URLs of author page grouped by language', () => { const mockPosts = [ - { lang: LanguageEnum.FR, authors: ['john'] }, - { lang: LanguageEnum.EN, authors: ['john'] }, + { lang: LANGUAGES.FR, authors: ['john'] }, + { lang: LANGUAGES.EN, authors: ['john'] }, ]; const expectedUrls: ReturnType = [ [ @@ -127,8 +127,8 @@ describe('getSitemapEntries', () => { it('should return URLs of post page grouped by language', () => { const mockPosts = [ - { lang: LanguageEnum.FR, slug: 'post-1' }, - { lang: LanguageEnum.EN, slug: 'post-2' }, + { lang: LANGUAGES.FR, slug: 'post-1' }, + { lang: LANGUAGES.EN, slug: 'post-2' }, ]; const expectedUrls: ReturnType = [ [{ lang: 'fr', url: '/fr/post-1/' }], @@ -140,10 +140,10 @@ describe('getSitemapEntries', () => { it('should return URLs of tutorial step page grouped by language', () => { const mockPosts = [ - { contentType: ContentTypeEnum.ARTICLE }, + { contentType: MARKDOWN_CONTENT_TYPES.ARTICLE }, { - lang: LanguageEnum.FR, - contentType: ContentTypeEnum.TUTORIAL, + lang: LANGUAGES.FR, + contentType: MARKDOWN_CONTENT_TYPES.TUTORIAL, slug: 'tutorial-1', steps: [ { @@ -158,8 +158,8 @@ describe('getSitemapEntries', () => { ], }, { - lang: LanguageEnum.EN, - contentType: ContentTypeEnum.TUTORIAL, + lang: LANGUAGES.EN, + contentType: MARKDOWN_CONTENT_TYPES.TUTORIAL, slug: 'tutorial-2', steps: [ { diff --git a/src/helpers/prerenderHelper/getUrls.ts b/src/helpers/prerenderHelper/getUrls.ts index d59b77a49..7eeba3411 100644 --- a/src/helpers/prerenderHelper/getUrls.ts +++ b/src/helpers/prerenderHelper/getUrls.ts @@ -1,16 +1,21 @@ import { CATEGORIES, - CategoryEnum, - ContentTypeEnum, DEFAULT_LANGUAGE, IS_DEBUG, - LanguageEnum, + LANGUAGES, LANGUAGES_AVAILABLE_WITH_DT, + MARKDOWN_CONTENT_TYPES, NUMBER_OF_ITEMS_PER_PAGE, PATHS, } from '@/constants'; import { generatePath } from '@/helpers/routerHelper'; -import { TransformedArticleData, TransformedAuthorData, TransformedPostData, TransformedTutorialData } from '@/types'; +import { + CategoryType, + TransformedArticleData, + TransformedAuthorData, + TransformedPostData, + TransformedTutorialData, +} from '@/types'; export type Urls = { lang: string; @@ -37,8 +42,8 @@ export const getCategoryPageUrls = ( for (const lang of LANGUAGES_AVAILABLE_WITH_DT) { const numberOfPosts = postsData.filter( (post) => - (lang === LanguageEnum.DT || post.lang === lang) && - (categoryName === 'all' ? true : post?.categories?.includes(categoryName as CategoryEnum)) + (lang === LANGUAGES.DT || post.lang === lang) && + (categoryName === 'all' ? true : post?.categories?.includes(categoryName as CategoryType)) ).length; if (numberOfPosts) { @@ -66,7 +71,7 @@ export const getCategoryPageUrls = ( for (const lang of LANGUAGES_AVAILABLE_WITH_DT) { const numberOfPosts = postsData.filter( - (post) => (lang === LanguageEnum.DT || post.lang === lang) && post.contentType === ContentTypeEnum.TUTORIAL + (post) => (lang === LANGUAGES.DT || post.lang === lang) && post.contentType === MARKDOWN_CONTENT_TYPES.TUTORIAL ).length; if (numberOfPosts) { if (!urls['tutorial']) { @@ -74,7 +79,7 @@ export const getCategoryPageUrls = ( } urls['tutorial'].push({ lang, - url: generatePath(PATHS.CATEGORY, { lang, categoryName: ContentTypeEnum.TUTORIAL }), + url: generatePath(PATHS.CATEGORY, { lang, categoryName: MARKDOWN_CONTENT_TYPES.TUTORIAL }), }); const numberOfPages = Math.ceil(numberOfPosts / NUMBER_OF_ITEMS_PER_PAGE); @@ -88,7 +93,7 @@ export const getCategoryPageUrls = ( lang, url: generatePath(PATHS.CATEGORY_PAGINATED, { lang, - categoryName: ContentTypeEnum.TUTORIAL, + categoryName: MARKDOWN_CONTENT_TYPES.TUTORIAL, page: index + 1, }), }); @@ -109,7 +114,7 @@ export const getAuthorPageUrls = ( for (const author of authorData) { for (const lang of LANGUAGES_AVAILABLE_WITH_DT) { const numberOfPosts = postsData.filter( - (post) => (lang === LanguageEnum.DT || post.lang === lang) && post.authors.includes(author.username) + (post) => (lang === LANGUAGES.DT || post.lang === lang) && post.authors.includes(author.username) ).length; if (numberOfPosts) { @@ -150,8 +155,8 @@ export const getPostPageUrls = (postsData: Pick )[] ): Urls => { - const tutorials = postsData.filter((post) => post.contentType === ContentTypeEnum.TUTORIAL && post.steps) as Pick< - TransformedTutorialData, - 'lang' | 'contentType' | 'steps' | 'slug' - >[]; + const tutorials = postsData.filter( + (post) => post.contentType === MARKDOWN_CONTENT_TYPES.TUTORIAL && post.steps + ) as Pick[]; return tutorials.reduce((urls, tutorial) => { const steps = tutorial.steps.slice(1); @@ -179,8 +183,8 @@ export const getTutorialStepPageUrls = ( ...(IS_DEBUG ? [ { - lang: LanguageEnum.DT, - url: generatePath(PATHS.POST, { lang: LanguageEnum.DT, slug: tutorial.slug, step: step.slug }), + lang: LANGUAGES.DT, + url: generatePath(PATHS.POST, { lang: LANGUAGES.DT, slug: tutorial.slug, step: step.slug }), }, ] : []), diff --git a/src/hooks/usePostPage.tsx b/src/hooks/usePostPage.tsx index 083fe7bfd..b9689ad59 100644 --- a/src/hooks/usePostPage.tsx +++ b/src/hooks/usePostPage.tsx @@ -4,7 +4,7 @@ import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; -import { ImageFormatEnum, PATHS } from '@/constants'; +import { IMAGE_FORMATS, PATHS } from '@/constants'; import { getCover } from '@/helpers/assetHelper'; import { getUrl } from '@/helpers/getUrlHelper'; import { generatePath } from '@/helpers/routerHelper'; @@ -47,7 +47,7 @@ export const usePostPage = (post: PostPageData): Omit { const { getDateToString } = useDateToString(); const { t, i18n } = useTranslation(); @@ -21,7 +21,7 @@ export const usePostsForCardList = (options: { : (options.posts ?? []).map((post) => ({ contentType: post.contentType, slug: post.slug, - cover: getCover(post, options.imageFormatEnum), + cover: getCover(post, options.imageFormat), title: post.title, excerpt: post.excerpt, date: getDateToString({ date: post.date }), diff --git a/src/middlewares/imageMiddleware.ts b/src/middlewares/imageMiddleware.ts index 8f657aa7b..b512cfdee 100644 --- a/src/middlewares/imageMiddleware.ts +++ b/src/middlewares/imageMiddleware.ts @@ -4,15 +4,8 @@ import { existsSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import Sharp from 'sharp'; -const contentTypesByFormat = { - jpeg: 'image/jpeg', - gif: 'image/gif', - webp: 'image/webp', - png: 'image/png', - avif: 'image/avif', -} as const; - -type FormatEnum = keyof typeof contentTypesByFormat; +import { DEFAULT_EXTENSION_FOR_IMAGES, IMAGE_CONTENT_TYPES } from '@/constants'; +import { ImageExtensionType, ImagePositionType } from '@/types'; export const imageMiddleware = async (req: Request, res: Response): Promise => { try { @@ -28,37 +21,43 @@ export const imageMiddleware = async (req: Request, res: Response): PromiseThe Agile methodology is an IT project management approach, or rather a “product management” approach, which allows the team to manage a project by breaking it down into short development cycles, called sprints. Today, the word Agile can refer to these values as well as the implementation frameworks, including: Scrum, Kanban, Safe, Lean… In this category, find all the articles, feedbacks and tutorials from our Agile team about Scrum, the job of Scrum Master and Product Manager and their advices on how to manage a sprint, which agile method to choose depending on the context or even on product prioritization! Good reading!

", + "description": "

The Agile methodology is an IT project management approach, or rather a “product management” approach, which allows the team to manage a project by breaking it down into short development cycles, called sprints. Today, the word Agile can refer to these values as well as the implementation frameworks, including: Scrum, Kanban, Safe, Lean… In this category, find all the articles, feedbacks and tutorials from our Agile team about Scrum, the job of Scrum Master and Product Manager and their advices on how to manage a sprint, which agile method to choose depending on the context or even on product prioritization! Good reading!

", "expertise": { "title": "What is the most popular agile methodology?", - "description": "Scrum is the most popular agile framework in 2023. It is used by companies both as sole agile method and as hybrid agile method. Its principle of dividing work into short periods of generally 2 weeks called \"sprints\" makes it possible to quickly come with a first version of a viable product. Each sprint is validated one after the other by the client, which allows work to be pursued on solid foundations, the following sprints aiming to further improve the product. The process is therefore significantly more productive. At Eleven Labs, we mainly work with the agile Scrum methodology and support our clients in managing their digital products, in training teams in the agile Scrum methodology or in agile coaching." + "description": "Scrum is the most popular agile framework in 2023. It is used by companies both as sole agile method and as hybrid agile method. Its principle of dividing work into short periods of generally 2 weeks called \"sprints\" makes it possible to quickly come with a first version of a viable product. Each sprint is validated one after the other by the client, which allows work to be pursued on solid foundations, the following sprints aiming to further improve the product. The process is therefore significantly more productive. At Eleven Labs, we mainly work with the agile Scrum methodology and support our clients in managing their digital products, in training teams in the agile Scrum methodology or in agile coaching." }, "post_list_title": "All our articles on agile methodologies" }, @@ -197,4 +197,4 @@ "description": "We're sorry for the inconvenience, but the page you requested was not found." } } -} +} \ No newline at end of file diff --git a/src/translations/fr.translations.json b/src/translations/fr.translations.json index bd565d1a8..67332396e 100644 --- a/src/translations/fr.translations.json +++ b/src/translations/fr.translations.json @@ -88,7 +88,7 @@ "description": "Tous nos articles de blog, REX et tutoriels autour du développement Javascript : React.js, Node.js, Nest.js, Next.js, Vue.js, Svelte.js" }, "title": "Nos articles et retours d’expérience en développement Javascript", - "description": "Javascript est un langage de programmation dynamique complet et doté d’une incroyable flexibilité ! Ce n’est pas pour rien que ce langage est aujourd'hui le plus utilisé par les développeurs à travers le monde. Dans cette catégorie, retrouvez tous les articles, retours d’expérience et tutoriels de nos astronautes autour de React.js, Node.js, Nest.js, Next.js, Vue.js, Svelte.js. Vous y retrouverez également des outils à utiliser pour faciliter votre delivery, un guide pour implémenter votre propre Design System et bien plus encore ! Bonne lecture.", + "description": "Javascript est un langage de programmation dynamique complet et doté d’une incroyable flexibilité ! Ce n’est pas pour rien que ce langage est aujourd'hui le plus utilisé par les développeurs à travers le monde. Dans cette catégorie, retrouvez tous les articles, retours d’expérience et tutoriels de nos astronautes autour de React.js, Node.js, Nest.js, Next.js, Vue.js, Svelte.js. Vous y retrouverez également des outils à utiliser pour faciliter votre delivery, un guide pour implémenter votre propre Design System et bien plus encore ! Bonne lecture.", "expertise": { "title": "Quels types d’applications peuvent être développées en Javascript ?", "description": "Aujourd’hui, il est quasiment possible de tout faire avec le langage Javascript : applications web et mobile, progressive web apps, logiciels, applications métier, sites web fullstack Javascript, APIs backend, jeux vidéos, applications TV et bien d’autres. En bref, Javascript est devenu en quelques années le langage central du web et celui le plus utilisé sur GitHub. Choisir Javascript pour son projet de développement web est donc, dans la majorité des cas, une bonne idée !", @@ -197,4 +197,4 @@ "description": "Nous nous excusons pour le désagrément, mais la page que vous avez demandée n'a pas été trouvée." } } -} +} \ No newline at end of file diff --git a/src/types/CategoryType.ts b/src/types/CategoryType.ts new file mode 100644 index 000000000..5f6620fe2 --- /dev/null +++ b/src/types/CategoryType.ts @@ -0,0 +1,3 @@ +import { CATEGORIES } from '@/constants'; + +export type CategoryType = (typeof CATEGORIES)[number]; diff --git a/src/types/ContentTypeData.ts b/src/types/ContentTypeData.ts index 9d3745900..03eb35aa6 100644 --- a/src/types/ContentTypeData.ts +++ b/src/types/ContentTypeData.ts @@ -6,28 +6,28 @@ import { TutorialDataSchemaValidation, TutorialStepDataValidationSchema, } from '@/config/schemaValidation'; -import { ContentTypeEnum } from '@/constants'; +import { MARKDOWN_CONTENT_TYPES } from '@/constants'; export interface ContentTypeData { - contentType: ContentTypeEnum; + contentType: (typeof MARKDOWN_CONTENT_TYPES)[keyof typeof MARKDOWN_CONTENT_TYPES]; } export interface CommonPostData extends z.infer {} export interface ArticleData extends CommonPostData { - contentType: ContentTypeEnum.ARTICLE; + contentType: 'article'; } export interface TutorialData extends CommonPostData, z.infer { - contentType: ContentTypeEnum.TUTORIAL; + contentType: 'tutorial'; } export interface TutorialStepData extends ContentTypeData, z.infer { - contentType: ContentTypeEnum.TUTORIAL_STEP; + contentType: 'tutorial-step'; } export interface AuthorData extends ContentTypeData, z.infer { - contentType: ContentTypeEnum.AUTHOR; + contentType: 'author'; } export type PostData = ArticleData | TutorialData; diff --git a/src/types/DeviceType.ts b/src/types/DeviceType.ts new file mode 100644 index 000000000..7214df364 --- /dev/null +++ b/src/types/DeviceType.ts @@ -0,0 +1,3 @@ +import { DEVICES } from '@/constants'; + +export type DeviceType = (typeof DEVICES)[keyof typeof DEVICES]; diff --git a/src/types/ImageExtensionType.ts b/src/types/ImageExtensionType.ts new file mode 100644 index 000000000..023a750f0 --- /dev/null +++ b/src/types/ImageExtensionType.ts @@ -0,0 +1,3 @@ +import { IMAGE_CONTENT_TYPES } from '@/constants'; + +export type ImageExtensionType = keyof typeof IMAGE_CONTENT_TYPES; diff --git a/src/types/ImageFormatType.ts b/src/types/ImageFormatType.ts new file mode 100644 index 000000000..e13f470ff --- /dev/null +++ b/src/types/ImageFormatType.ts @@ -0,0 +1,3 @@ +import { IMAGE_FORMATS } from '@/constants'; + +export type ImageFormatType = (typeof IMAGE_FORMATS)[keyof typeof IMAGE_FORMATS]; diff --git a/src/types/ImagePositionType.ts b/src/types/ImagePositionType.ts new file mode 100644 index 000000000..58ceeb3a8 --- /dev/null +++ b/src/types/ImagePositionType.ts @@ -0,0 +1,3 @@ +import { IMAGE_POSITIONS } from '@/constants'; + +export type ImagePositionType = (typeof IMAGE_POSITIONS)[keyof typeof IMAGE_POSITIONS]; diff --git a/src/types/LanguageType.ts b/src/types/LanguageType.ts new file mode 100644 index 000000000..a16365705 --- /dev/null +++ b/src/types/LanguageType.ts @@ -0,0 +1,3 @@ +import { LANGUAGES } from '@/constants'; + +export type LanguageType = (typeof LANGUAGES)[keyof typeof LANGUAGES]; diff --git a/src/types/PageData.ts b/src/types/PageData.ts index 2545e52a5..c49932f62 100644 --- a/src/types/PageData.ts +++ b/src/types/PageData.ts @@ -1,4 +1,5 @@ -import { CategoryEnum } from '@/constants'; +import { CategoryType } from '@/types/CategoryType'; +import { ImagePositionType } from '@/types/ImagePositionType'; import { TransformedArticleData, TransformedAuthorData, @@ -7,7 +8,7 @@ import { } from '@/types/TransformedContentTypeData'; export interface LayoutTemplateData { - categories: ('all' | CategoryEnum)[]; + categories: ('all' | CategoryType)[]; hasTutorial: boolean; } @@ -23,6 +24,7 @@ interface CommonPostPageData { cover?: { path: string; alt: string; + position?: ImagePositionType; }; authors: TransformedAuthorData[]; relatedPosts: TransformedPostDataWithTransformedAuthors[]; diff --git a/src/types/TransformedContentTypeData.ts b/src/types/TransformedContentTypeData.ts index 6699bb200..8c1c99b64 100644 --- a/src/types/TransformedContentTypeData.ts +++ b/src/types/TransformedContentTypeData.ts @@ -1,10 +1,8 @@ -import { ContentTypeEnum } from '@/constants'; - import { ArticleData, AuthorData, TutorialData, TutorialStepData } from './ContentTypeData'; export interface TransformedArticleData extends Pick { - contentType: ContentTypeEnum.ARTICLE; + contentType: 'article'; summary: { id: string; level: number; text: string }[]; date: string; readingTime: number; @@ -13,7 +11,7 @@ export interface TransformedArticleData export interface TransformedTutorialData extends Pick { - contentType: ContentTypeEnum.TUTORIAL; + contentType: 'tutorial'; date: string; readingTime: number; steps: (Pick & { content: string; readingTime: number })[]; diff --git a/src/types/index.ts b/src/types/index.ts index 3ae14c238..4b4aabc01 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,3 +2,9 @@ export * from './ContentTypeData'; export * from './TransformedContentTypeData'; export * from './PageData'; export * from './AlgoliaData'; +export * from './ImageFormatType'; +export * from './DeviceType'; +export * from './ImageExtensionType'; +export * from './ImagePositionType'; +export * from './CategoryType'; +export * from './LanguageType';