summaryrefslogtreecommitdiffstats
path: root/src/utils/helpers
diff options
context:
space:
mode:
Diffstat (limited to 'src/utils/helpers')
-rw-r--r--src/utils/helpers/author.ts32
-rw-r--r--src/utils/helpers/dates.ts40
-rw-r--r--src/utils/helpers/format.ts310
-rw-r--r--src/utils/helpers/i18n.ts2
-rw-r--r--src/utils/helpers/images.ts18
-rw-r--r--src/utils/helpers/pages.ts85
-rw-r--r--src/utils/helpers/prism.ts34
-rw-r--r--src/utils/helpers/projects.ts109
-rw-r--r--src/utils/helpers/rss.ts51
-rw-r--r--src/utils/helpers/schema-org.ts224
-rw-r--r--src/utils/helpers/slugify.ts18
-rw-r--r--src/utils/helpers/sort.ts21
-rw-r--r--src/utils/helpers/strings.ts39
13 files changed, 534 insertions, 449 deletions
diff --git a/src/utils/helpers/author.ts b/src/utils/helpers/author.ts
new file mode 100644
index 0000000..40743ca
--- /dev/null
+++ b/src/utils/helpers/author.ts
@@ -0,0 +1,32 @@
+import { type Author, type ContentKind } from '@ts/types/app';
+import { type RawAuthor } from '@ts/types/raw-data';
+
+/**
+ * Convert author raw data to regular data.
+ *
+ * @param {RawAuthor<ContentKind>} data - The author raw data.
+ * @param {ContentKind} kind - The author kind. Either `page` or `comment`.
+ * @param {number} [avatarSize] - The author avatar size.
+ * @returns {Author<ContentKind>} The author data.
+ */
+export const getAuthorFromRawData = (
+ data: RawAuthor<typeof kind>,
+ kind: ContentKind,
+ avatarSize: number = 80
+): Author<typeof kind> => {
+ const { name, description, gravatarUrl, url } = data;
+
+ return {
+ name,
+ avatar: gravatarUrl
+ ? {
+ alt: `${name} avatar`,
+ height: avatarSize,
+ src: gravatarUrl,
+ width: avatarSize,
+ }
+ : undefined,
+ description,
+ website: url,
+ };
+};
diff --git a/src/utils/helpers/dates.ts b/src/utils/helpers/dates.ts
new file mode 100644
index 0000000..cb56ad2
--- /dev/null
+++ b/src/utils/helpers/dates.ts
@@ -0,0 +1,40 @@
+import { settings } from '@utils/config';
+
+/**
+ * Format a date based on a locale.
+ *
+ * @param {string} date - The date.
+ * @param {string} [locale] - A locale.
+ * @returns {string} The locale date string.
+ */
+export const getFormattedDate = (
+ date: string,
+ locale: string = settings.locales.defaultLocale
+): string => {
+ const dateOptions: Intl.DateTimeFormatOptions = {
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric',
+ };
+
+ return new Date(date).toLocaleDateString(locale, dateOptions);
+};
+
+/**
+ * Format a time based on a locale.
+ *
+ * @param {string} time - The time.
+ * @param {string} [locale] - A locale.
+ * @returns {string} The locale time string.
+ */
+export const getFormattedTime = (
+ time: string,
+ locale: string = settings.locales.defaultLocale
+): string => {
+ const formattedTime = new Date(time).toLocaleTimeString(locale, {
+ hour: 'numeric',
+ minute: 'numeric',
+ });
+
+ return locale === 'fr' ? formattedTime.replace(':', 'h') : formattedTime;
+};
diff --git a/src/utils/helpers/format.ts b/src/utils/helpers/format.ts
deleted file mode 100644
index dd35868..0000000
--- a/src/utils/helpers/format.ts
+++ /dev/null
@@ -1,310 +0,0 @@
-import { ParamsIds, ParamsSlug, Slug } from '@ts/types/app';
-import {
- Article,
- ArticlePreview,
- RawArticle,
- RawArticlePreview,
-} from '@ts/types/articles';
-import { Comment, RawComment } from '@ts/types/comments';
-import {
- RawTopic,
- RawTopicPreview,
- RawThematic,
- Topic,
- TopicPreview,
- Thematic,
-} from '@ts/types/taxonomies';
-
-/**
- * Format a post preview from RawArticlePreview to ArticlePreview type.
- * @param rawPost - A post preview coming from WP GraphQL.
- * @returns A formatted post preview.
- */
-export const getFormattedPostPreview = (rawPost: RawArticlePreview) => {
- const {
- acfPosts,
- commentCount,
- contentParts,
- date,
- featuredImage,
- id,
- info,
- modified,
- slug,
- title,
- } = rawPost;
-
- const dates = {
- publication: date,
- update: modified,
- };
-
- const topics = acfPosts.postsInTopic ? acfPosts.postsInTopic : [];
- const thematics = acfPosts.postsInThematic ? acfPosts.postsInThematic : [];
-
- const formattedPost: ArticlePreview = {
- commentCount,
- dates,
- featuredImage: featuredImage ? featuredImage.node : null,
- id,
- info,
- intro: contentParts.beforeMore,
- slug,
- topics,
- thematics,
- title,
- };
-
- return formattedPost;
-};
-
-/**
- * Format an array of posts list from RawArticlePreview to ArticlePreview type.
- * @param rawPosts - A posts list coming from WP GraphQL.
- * @returns A formatted posts list.
- */
-export const getFormattedPostsList = (
- rawPosts: RawArticlePreview[]
-): ArticlePreview[] => {
- return rawPosts
- .filter((post) => Object.getOwnPropertyNames(post).length > 0)
- .map((post) => {
- return getFormattedPostPreview(post);
- });
-};
-
-/**
- * Format a topic from RawTopic to Topic type.
- * @param rawTopic - A topic coming from WP GraphQL.
- * @returns A formatted topic.
- */
-export const getFormattedTopic = (rawTopic: RawTopic): Topic => {
- const {
- acfTopics,
- contentParts,
- databaseId,
- date,
- featuredImage,
- id,
- info,
- modified,
- seo,
- title,
- } = rawTopic;
-
- const dates = {
- publication: date,
- update: modified,
- };
-
- const posts = getFormattedPostsList(acfTopics.postsInTopic);
-
- const formattedTopic: Topic = {
- content: contentParts.afterMore,
- databaseId,
- dates,
- featuredImage: featuredImage ? featuredImage.node : null,
- id,
- info,
- intro: contentParts.beforeMore,
- officialWebsite: acfTopics.officialWebsite,
- posts,
- seo,
- title,
- };
-
- return formattedTopic;
-};
-
-/**
- * Format a thematic from RawThematic to Thematic type.
- * @param rawThematic - A thematic coming from wP GraphQL.
- * @returns A formatted thematic.
- */
-export const getFormattedThematic = (rawThematic: RawThematic): Thematic => {
- const {
- acfThematics,
- contentParts,
- databaseId,
- date,
- id,
- info,
- modified,
- seo,
- title,
- } = rawThematic;
-
- const dates = {
- publication: date,
- update: modified,
- };
-
- const posts = getFormattedPostsList(acfThematics.postsInThematic);
-
- const formattedThematic: Thematic = {
- content: contentParts.afterMore,
- databaseId,
- dates,
- id,
- info,
- intro: contentParts.beforeMore,
- posts,
- seo,
- title,
- };
-
- return formattedThematic;
-};
-
-/**
- * Format a comments list from RawComment to Comment type.
- * @param rawComments - A comments list coming from WP GraphQL.
- * @returns A formatted comments list.
- */
-export const getFormattedComments = (rawComments: RawComment[]): Comment[] => {
- const formattedComments: Comment[] = rawComments.map((comment) => {
- const formattedComment: Comment = {
- ...comment,
- author: comment.author.node,
- replies: [],
- };
-
- return formattedComment;
- });
-
- return formattedComments;
-};
-
-/**
- * Create a comments tree with replies.
- * @param comments - A flatten comments list.
- * @returns An array of comments with replies.
- */
-export const buildCommentsTree = (comments: Comment[]) => {
- type CommentsHashTable = {
- [key: string]: Comment;
- };
-
- const hashTable: CommentsHashTable = Object.create(null);
- const commentsTree: Comment[] = [];
-
- comments.forEach(
- (comment) => (hashTable[comment.databaseId] = { ...comment, replies: [] })
- );
-
- comments.forEach((comment) => {
- if (!comment.parentDatabaseId) {
- commentsTree.push(hashTable[comment.databaseId]);
- } else {
- hashTable[comment.parentDatabaseId].replies.push(
- hashTable[comment.databaseId]
- );
- }
- });
-
- return commentsTree;
-};
-
-export const getFormattedTopicsPreview = (
- topics: RawTopicPreview[]
-): TopicPreview[] => {
- const formattedTopics: TopicPreview[] = topics.map((topic) => {
- return {
- ...topic,
- featuredImage: topic.featuredImage ? topic.featuredImage.node : null,
- };
- });
-
- return formattedTopics;
-};
-
-/**
- * Format an article from RawArticle to Article type.
- * @param rawPost - An article coming from WP GraphQL.
- * @returns A formatted article.
- */
-export const getFormattedPost = (rawPost: RawArticle): Article => {
- const {
- acfPosts,
- author,
- commentCount,
- contentParts,
- databaseId,
- date,
- featuredImage,
- id,
- info,
- modified,
- seo,
- title,
- } = rawPost;
-
- const dates = {
- publication: date,
- update: modified,
- };
-
- const topics = acfPosts.postsInTopic
- ? getFormattedTopicsPreview(acfPosts.postsInTopic)
- : [];
-
- const formattedPost: Article = {
- author: author.node,
- commentCount,
- content: contentParts.afterMore,
- databaseId,
- dates,
- featuredImage: featuredImage ? featuredImage.node : null,
- id,
- info,
- intro: contentParts.beforeMore,
- seo,
- topics,
- thematics: acfPosts.postsInThematic ? acfPosts.postsInThematic : [],
- title,
- };
-
- return formattedPost;
-};
-
-/**
- * Converts a date to a string by using the specified locale.
- * @param {string} date The date.
- * @param {string} locale A locale.
- * @returns {string} The formatted date to locale date string.
- */
-export const getFormattedDate = (date: string, locale: string) => {
- const dateOptions: Intl.DateTimeFormatOptions = {
- day: 'numeric',
- month: 'long',
- year: 'numeric',
- };
-
- return new Date(date).toLocaleDateString(locale, dateOptions);
-};
-
-/**
- * Convert an array of slugs to an array of params with slug.
- * @param {Slug} array - An array of object with slug.
- * @returns {ParamsSlug} An array of params with slug.
- */
-export const getFormattedPaths = (array: Slug[]): ParamsSlug[] => {
- return array.map((object) => {
- return { params: { slug: object.slug } };
- });
-};
-
-/**
- * Convert a number of pages to an array of params with ids.
- * @param {number} totalPages - The total pages.
- * @returns {ParamsIds} An array of params with ids.
- */
-export const getFormattedPageNumbers = (totalPages: number): ParamsIds[] => {
- const paths = [];
-
- for (let i = 1; i <= totalPages; i++) {
- paths.push({ params: { id: `${i}` } });
- }
-
- return paths;
-};
diff --git a/src/utils/helpers/i18n.ts b/src/utils/helpers/i18n.ts
index c4734ad..5d19c8c 100644
--- a/src/utils/helpers/i18n.ts
+++ b/src/utils/helpers/i18n.ts
@@ -3,7 +3,7 @@ import { settings } from '@utils/config';
import { readFile } from 'fs/promises';
import path from 'path';
-type Messages = { [key: string]: string };
+export type Messages = { [key: string]: string };
export const defaultLocale = settings.locales.defaultLocale;
diff --git a/src/utils/helpers/images.ts b/src/utils/helpers/images.ts
new file mode 100644
index 0000000..30bb8be
--- /dev/null
+++ b/src/utils/helpers/images.ts
@@ -0,0 +1,18 @@
+import { Image } from '@ts/types/app';
+import { RawCover } from '@ts/types/raw-data';
+
+/**
+ * Retrieve an Image object from raw data.
+ *
+ * @param image - The cover raw data.
+ * @returns {Image} - An Image object.
+ */
+export const getImageFromRawData = (image: RawCover): Image => {
+ return {
+ alt: image.altText,
+ height: image.mediaDetails.height,
+ src: image.sourceUrl,
+ title: image.title,
+ width: image.mediaDetails.width,
+ };
+};
diff --git a/src/utils/helpers/pages.ts b/src/utils/helpers/pages.ts
new file mode 100644
index 0000000..773d454
--- /dev/null
+++ b/src/utils/helpers/pages.ts
@@ -0,0 +1,85 @@
+import { type Post } from '@components/organisms/layout/posts-list';
+import { type LinksListItems } from '@components/organisms/widgets/links-list-widget';
+import { type EdgesResponse } from '@services/graphql/api';
+import { getArticleFromRawData } from '@services/graphql/articles';
+import { type Article, type PageLink } from '@ts/types/app';
+import {
+ type RawArticle,
+ type RawThematicPreview,
+ type RawTopicPreview,
+} from '@ts/types/raw-data';
+import { getImageFromRawData } from './images';
+
+/**
+ * Convert raw data to a Link object.
+ *
+ * @param data - An object.
+ * @param {number} data.databaseId - The data id.
+ * @param {number} [data.logo] - The data logo.
+ * @param {string} data.slug - The data slug.
+ * @param {string} data.title - The data name.
+ * @returns {PageLink} The link data (id, slug and title).
+ */
+export const getPageLinkFromRawData = (
+ data: RawThematicPreview | RawTopicPreview,
+ kind: 'thematic' | 'topic'
+): PageLink => {
+ const { databaseId, featuredImage, slug, title } = data;
+ const baseUrl = kind === 'thematic' ? '/thematique/' : '/sujet/';
+
+ return {
+ id: databaseId,
+ logo: featuredImage ? getImageFromRawData(featuredImage?.node) : undefined,
+ name: title,
+ url: `${baseUrl}${slug}`,
+ };
+};
+
+/**
+ * Convert page link data to an array of links items.
+ *
+ * @param {PageLink[]} links - An array of page links.
+ * @returns {LinksListItem[]} An array of links items.
+ */
+export const getLinksListItems = (links: PageLink[]): LinksListItems[] => {
+ return links.map((link) => {
+ return {
+ name: link.name,
+ url: link.url,
+ };
+ });
+};
+
+/**
+ * Retrieve the posts list with the article URL.
+ *
+ * @param {Article[]} posts - An array of articles.
+ * @returns {Post[]} An array of posts with full article URL.
+ */
+export const getPostsWithUrl = (posts: Article[]): Post[] => {
+ return posts.map((post) => {
+ return {
+ ...post,
+ url: `/article/${post.slug}`,
+ };
+ });
+};
+
+/**
+ * Retrieve the posts list from raw data.
+ *
+ * @param {EdgesResponse<RawArticle>[]} rawData - The raw data.
+ * @returns {Post[]} An array of posts.
+ */
+export const getPostsList = (rawData: EdgesResponse<RawArticle>[]): Post[] => {
+ const articlesList: RawArticle[] = [];
+ rawData.forEach((articleData) =>
+ articleData.edges.forEach((edge) => {
+ articlesList.push(edge.node);
+ })
+ );
+
+ return getPostsWithUrl(
+ articlesList.map((article) => getArticleFromRawData(article))
+ );
+};
diff --git a/src/utils/helpers/prism.ts b/src/utils/helpers/prism.ts
deleted file mode 100644
index a5f5787..0000000
--- a/src/utils/helpers/prism.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Check if the current block has a defined language.
- * @param classList - A list of class.
- * @returns {boolean} - True if a class starts with "language-".
- */
-const isLanguageBlock = (classList: DOMTokenList) => {
- const classes = Array.from(classList);
- return classes.some((className) => /language-.*/.test(className));
-};
-
-/**
- * Add automatically some classes and attributes for PrismJs.
- *
- * These classes and attributes are needed by Prism or to customize comments.
- */
-export const addPrismClasses = () => {
- const preTags = document.getElementsByTagName('pre');
-
- Array.from(preTags).forEach((preTag) => {
- if (!isLanguageBlock(preTag.classList)) return;
-
- preTag.classList.add('match-braces');
-
- if (preTag.classList.contains('filter-output')) {
- preTag.setAttribute('data-filter-output', '#output#');
- }
-
- if (preTag.classList.contains('language-bash')) {
- preTag.classList.add('command-line');
- } else if (!preTag.classList.contains('language-diff')) {
- preTag.classList.add('line-numbers');
- }
- });
-};
diff --git a/src/utils/helpers/projects.ts b/src/utils/helpers/projects.ts
index 1612dae..a0f0c04 100644
--- a/src/utils/helpers/projects.ts
+++ b/src/utils/helpers/projects.ts
@@ -1,36 +1,55 @@
-import { Project, ProjectMeta } from '@ts/types/app';
+import { ProjectCard, ProjectPreview } from '@ts/types/app';
+import { MDXProjectMeta } from '@ts/types/mdx';
import { readdirSync } from 'fs';
import path from 'path';
/**
- * Retrieve project's data by id.
- * @param {string} id - The filename without extension.
- * @returns {Promise<Project>} - The project data.
+ * Retrieve all the projects filename.
+ *
+ * @returns {string[]} An array of filenames.
*/
-export const getProjectData = async (id: string): Promise<Project> => {
+export const getProjectFilenames = (): string[] => {
+ const projectsDirectory = path.join(process.cwd(), 'src/content/projects');
+ const filenames = readdirSync(projectsDirectory);
+
+ return filenames.map((filename) => filename.replace(/\.mdx$/, ''));
+};
+
+/**
+ * Retrieve the data of a project by filename.
+ *
+ * @param {string} filename - The project filename.
+ * @returns {Promise<ProjectPreview>}
+ */
+export const getProjectData = async (
+ filename: string
+): Promise<ProjectPreview> => {
try {
const {
- intro,
meta,
- seo,
- tagline,
}: {
- intro: string;
- meta: ProjectMeta & { title: string };
- seo: { title: string; description: string };
- tagline?: string;
- } = await import(`../../content/projects/${id}.mdx`);
+ meta: MDXProjectMeta;
+ } = await import(`../../content/projects/${filename}.mdx`);
- const { title, ...onlyMeta } = meta;
+ const { dates, intro, title, ...projectMeta } = meta;
+ const { publication, update } = dates;
+ const cover = await import(`../../../public/projects/${filename}.jpg`);
return {
- id,
- intro: intro || '',
- meta: onlyMeta || {},
- slug: id,
- title,
- seo: seo || {},
- tagline: tagline || '',
+ id: filename,
+ intro,
+ meta: {
+ ...projectMeta,
+ dates: { publication, update },
+ // Dynamic import source does not work so I use it only to get sizes
+ cover: {
+ ...cover.default,
+ alt: `${title} image`,
+ src: `/projects/${filename}.jpg`,
+ },
+ },
+ slug: filename,
+ title: title,
};
} catch (err) {
console.error(err);
@@ -39,48 +58,44 @@ export const getProjectData = async (id: string): Promise<Project> => {
};
/**
- * Retrieve the projects data from filenames.
- * @param {string[]} filenames - An array of filenames.
- * @returns {Promise<Project[]>} An array of projects with meta.
+ * Retrieve all the projects data using filenames.
+ *
+ * @param {string[]} filenames - The filenames without extension.
+ * @returns {Promise<ProjectCard[]>} - An array of projects data.
*/
-const getProjectsWithMeta = async (filenames: string[]): Promise<Project[]> => {
+export const getProjectsData = async (
+ filenames: string[]
+): Promise<ProjectCard[]> => {
return Promise.all(
filenames.map(async (filename) => {
- return getProjectData(filename);
+ const { id, meta, slug, title } = await getProjectData(filename);
+ const { cover, dates, tagline, technologies } = meta;
+ return { id, meta: { cover, dates, tagline, technologies }, slug, title };
})
);
};
/**
* Method to sort an array of projects by publication date.
- * @param {Project} a - A single project.
- * @param {Project} b - A single project.
+ *
+ * @param {ProjectCard} a - A single project.
+ * @param {ProjectCard} b - A single project.
* @returns The result used by Array.sort() method: 1 || -1 || 0.
*/
-const sortProjectByPublicationDate = (a: Project, b: Project) => {
- if (a.meta.publishedOn < b.meta.publishedOn) return 1;
- if (a.meta.publishedOn > b.meta.publishedOn) return -1;
+const sortProjectsByPublicationDate = (a: ProjectCard, b: ProjectCard) => {
+ if (a.meta.dates.publication < b.meta.dates.publication) return 1;
+ if (a.meta.dates.publication > b.meta.dates.publication) return -1;
return 0;
};
/**
- * Retrieve all the projects filename.
- * @returns {string[]} An array of filenames.
- */
-export const getAllProjectsFilename = (): string[] => {
- const projectsDirectory = path.join(process.cwd(), 'src/content/projects');
- const filenames = readdirSync(projectsDirectory);
-
- return filenames.map((filename) => filename.replace(/\.mdx$/, ''));
-};
-
-/**
* Retrieve all projects in content folder sorted by publication date.
- * @returns {Promise<Project[]>} An array of projects.
+ *
+ * @returns {Promise<ProjectCard[]>} An array of projects.
*/
-export const getSortedProjects = async (): Promise<Project[]> => {
- const filenames = getAllProjectsFilename();
- const projects = await getProjectsWithMeta(filenames);
+export const getProjectsCard = async (): Promise<ProjectCard[]> => {
+ const filenames = getProjectFilenames();
+ const projects = await getProjectsData(filenames);
- return [...projects].sort(sortProjectByPublicationDate);
+ return [...projects].sort(sortProjectsByPublicationDate);
};
diff --git a/src/utils/helpers/rss.ts b/src/utils/helpers/rss.ts
index 10a8e77..8ee774c 100644
--- a/src/utils/helpers/rss.ts
+++ b/src/utils/helpers/rss.ts
@@ -1,20 +1,35 @@
-import { getPostsTotal, getPublishedPosts } from '@services/graphql/queries';
-import { ArticlePreview } from '@ts/types/articles';
-import { PostsList } from '@ts/types/blog';
+import {
+ getArticleFromRawData,
+ getArticles,
+ getTotalArticles,
+} from '@services/graphql/articles';
+import { Article } from '@ts/types/app';
import { settings } from '@utils/config';
import { Feed } from 'feed';
-const getAllPosts = async (): Promise<ArticlePreview[]> => {
- const totalPosts = await getPostsTotal();
- const posts: ArticlePreview[] = [];
+/**
+ * Retrieve the data for all the articles.
+ *
+ * @returns {Promise<Article[]>} - All the articles.
+ */
+const getAllArticles = async (): Promise<Article[]> => {
+ const totalArticles = await getTotalArticles();
+ const rawArticles = await getArticles({ first: totalArticles });
+ const articles: Article[] = [];
- const postsList: PostsList = await getPublishedPosts({ first: totalPosts });
- posts.push(...postsList.posts);
+ rawArticles.edges.forEach((edge) =>
+ articles.push(getArticleFromRawData(edge.node))
+ );
- return posts;
+ return articles;
};
-export const generateFeed = async () => {
+/**
+ * Generate a new feed.
+ *
+ * @returns {Promise<Feed>} - The feed.
+ */
+export const generateFeed = async (): Promise<Feed> => {
const author = {
name: settings.name,
email: process.env.APP_AUTHOR_EMAIL,
@@ -38,16 +53,16 @@ export const generateFeed = async () => {
title,
});
- const posts = await getAllPosts();
+ const articles = await getAllArticles();
- posts.forEach((post) => {
+ articles.forEach((article) => {
feed.addItem({
- content: post.intro,
- date: new Date(post.dates.publication),
- description: post.intro,
- id: post.id,
- link: `${settings.url}/article/${post.slug}`,
- title: post.title,
+ content: article.intro,
+ date: new Date(article.meta!.dates.publication),
+ description: article.intro,
+ id: `${article.id}`,
+ link: `${settings.url}/article/${article.slug}`,
+ title: article.title,
});
});
diff --git a/src/utils/helpers/schema-org.ts b/src/utils/helpers/schema-org.ts
new file mode 100644
index 0000000..cdace00
--- /dev/null
+++ b/src/utils/helpers/schema-org.ts
@@ -0,0 +1,224 @@
+import { Dates } from '@ts/types/app';
+import { settings } from '@utils/config';
+import {
+ AboutPage,
+ Article,
+ Blog,
+ BlogPosting,
+ ContactPage,
+ Graph,
+ WebPage,
+} from 'schema-dts';
+
+export type GetBlogSchemaProps = {
+ /**
+ * True if the page is part of the blog.
+ */
+ isSinglePage: boolean;
+ /**
+ * The page locale.
+ */
+ locale: string;
+ /**
+ * The page slug with a leading slash.
+ */
+ slug: string;
+};
+
+/**
+ * Retrieve the JSON for Blog schema.
+ *
+ * @param props - The page data.
+ * @returns {Blog} The JSON for Blog schema.
+ */
+export const getBlogSchema = ({
+ isSinglePage,
+ locale,
+ slug,
+}: GetBlogSchemaProps): Blog => {
+ return {
+ '@id': `${settings.url}/#blog`,
+ '@type': 'Blog',
+ author: { '@id': `${settings.url}/#branding` },
+ creator: { '@id': `${settings.url}/#branding` },
+ editor: { '@id': `${settings.url}/#branding` },
+ blogPost: isSinglePage ? { '@id': `${settings.url}/#article` } : undefined,
+ inLanguage: locale,
+ isPartOf: isSinglePage
+ ? {
+ '@id': `${settings.url}${slug}`,
+ }
+ : undefined,
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ mainEntityOfPage: isSinglePage
+ ? undefined
+ : { '@id': `${settings.url}${slug}` },
+ };
+};
+
+export type SinglePageSchemaReturn = {
+ about: AboutPage;
+ contact: ContactPage;
+ page: Article;
+ post: BlogPosting;
+};
+
+export type SinglePageSchemaKind = keyof SinglePageSchemaReturn;
+
+export type GetSinglePageSchemaProps<T extends SinglePageSchemaKind> = {
+ /**
+ * The number of comments.
+ */
+ commentsCount?: number;
+ /**
+ * The page content.
+ */
+ content?: string;
+ /**
+ * The url of the cover.
+ */
+ cover?: string;
+ /**
+ * The page dates.
+ */
+ dates: Dates;
+ /**
+ * The page description.
+ */
+ description: string;
+ /**
+ * The page id.
+ */
+ id: string;
+ /**
+ * The page kind.
+ */
+ kind: T;
+ /**
+ * The page locale.
+ */
+ locale: string;
+ /**
+ * The page slug with a leading slash.
+ */
+ slug: string;
+ /**
+ * The page title.
+ */
+ title: string;
+};
+
+/**
+ * Retrieve the JSON schema depending on the page kind.
+ *
+ * @param props - The page data.
+ * @returns {SinglePageSchemaReturn[T]} - Either AboutPage, ContactPage, Article or BlogPosting schema.
+ */
+export const getSinglePageSchema = <T extends SinglePageSchemaKind>({
+ commentsCount,
+ content,
+ cover,
+ dates,
+ description,
+ id,
+ kind,
+ locale,
+ title,
+ slug,
+}: GetSinglePageSchemaProps<T>): SinglePageSchemaReturn[T] => {
+ const publicationDate = new Date(dates.publication);
+ const updateDate = dates.update ? new Date(dates.update) : undefined;
+ const singlePageSchemaType = {
+ about: 'AboutPage',
+ contact: 'ContactPage',
+ page: 'Article',
+ post: 'BlogPosting',
+ };
+
+ return {
+ '@id': `${settings.url}/#${id}`,
+ '@type': singlePageSchemaType[kind],
+ name: title,
+ description,
+ articleBody: content,
+ author: { '@id': `${settings.url}/#branding` },
+ commentCount: commentsCount,
+ copyrightYear: publicationDate.getFullYear(),
+ creator: { '@id': `${settings.url}/#branding` },
+ dateCreated: publicationDate.toISOString(),
+ dateModified: updateDate && updateDate.toISOString(),
+ datePublished: publicationDate.toISOString(),
+ editor: { '@id': `${settings.url}/#branding` },
+ headline: title,
+ image: cover,
+ inLanguage: locale,
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ thumbnailUrl: cover,
+ isPartOf:
+ kind === 'post'
+ ? {
+ '@id': `${settings.url}/blog`,
+ }
+ : undefined,
+ mainEntityOfPage: { '@id': `${settings.url}${slug}` },
+ } as SinglePageSchemaReturn[T];
+};
+
+export type GetWebPageSchemaProps = {
+ /**
+ * The page description.
+ */
+ description: string;
+ /**
+ * The page locale.
+ */
+ locale: string;
+ /**
+ * The page slug.
+ */
+ slug: string;
+ /**
+ * The page title.
+ */
+ title: string;
+ /**
+ * The page last update.
+ */
+ updateDate?: string;
+};
+
+/**
+ * Retrieve the JSON for WebPage schema.
+ *
+ * @param props - The page data.
+ * @returns {WebPage} The JSON for WebPage schema.
+ */
+export const getWebPageSchema = ({
+ description,
+ locale,
+ slug,
+ title,
+ updateDate,
+}: GetWebPageSchemaProps): WebPage => {
+ return {
+ '@id': `${settings.url}${slug}`,
+ '@type': 'WebPage',
+ breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
+ lastReviewed: updateDate,
+ name: title,
+ description: description,
+ inLanguage: locale,
+ reviewedBy: { '@id': `${settings.url}/#branding` },
+ url: `${settings.url}${slug}`,
+ isPartOf: {
+ '@id': `${settings.url}`,
+ },
+ };
+};
+
+export const getSchemaJson = (graphs: Graph['@graph']): Graph => {
+ return {
+ '@context': 'https://schema.org',
+ '@graph': graphs,
+ };
+};
diff --git a/src/utils/helpers/slugify.ts b/src/utils/helpers/slugify.ts
deleted file mode 100644
index 55ff583..0000000
--- a/src/utils/helpers/slugify.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Convert a text into a slug or id.
- * https://gist.github.com/codeguy/6684588#gistcomment-3332719
- *
- * @param {string} text Text to slugify.
- */
-export const slugify = (text: string) => {
- return text
- .toString()
- .normalize('NFD')
- .replace(/[\u0300-\u036f]/g, '')
- .toLowerCase()
- .trim()
- .replace(/\s+/g, '-')
- .replace(/[^\w\-]+/g, '-')
- .replace(/\-\-+/g, '-')
- .replace(/(^-)|(-$)/g, '');
-};
diff --git a/src/utils/helpers/sort.ts b/src/utils/helpers/sort.ts
deleted file mode 100644
index c1ee35d..0000000
--- a/src/utils/helpers/sort.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { ArticlePreview } from '@ts/types/articles';
-import { PostsList } from '@ts/types/blog';
-
-type YearCollection = {
- [key: string]: ArticlePreview[];
-};
-
-export const sortPostsByYear = (data: PostsList[]) => {
- const yearCollection: YearCollection = {};
-
- data.forEach((page) => {
- page.posts.forEach((post) => {
- const postYear = new Date(post.dates.publication)
- .getFullYear()
- .toString();
- yearCollection[postYear] = [...(yearCollection[postYear] || []), post];
- });
- });
-
- return yearCollection;
-};
diff --git a/src/utils/helpers/strings.ts b/src/utils/helpers/strings.ts
new file mode 100644
index 0000000..1af0ca2
--- /dev/null
+++ b/src/utils/helpers/strings.ts
@@ -0,0 +1,39 @@
+/**
+ * Convert a text into a slug or id.
+ * https://gist.github.com/codeguy/6684588#gistcomment-3332719
+ *
+ * @param {string} text - A text to slugify.
+ * @returns {string} The slug.
+ */
+export const slugify = (text: string): string => {
+ return text
+ .toString()
+ .normalize('NFD')
+ .replace(/[\u0300-\u036f]/g, '')
+ .toLowerCase()
+ .trim()
+ .replace(/\s+/g, '-')
+ .replace(/[^\w\-]+/g, '-')
+ .replace(/\-\-+/g, '-')
+ .replace(/(^-)|(-$)/g, '');
+};
+
+/**
+ * Capitalize the first letter of a string.
+ *
+ * @param {string} text - A text to capitalize.
+ * @returns {string} The capitalized text.
+ */
+export const capitalize = (text: string): string => {
+ return text.replace(/^\w/, (firstLetter) => firstLetter.toUpperCase());
+};
+
+/**
+ * Convert a text from kebab case (foo-bar) to camel case (fooBar).
+ *
+ * @param {string} text - A text to transform.
+ * @returns {string} The text in camel case.
+ */
+export const fromKebabCaseToCamelCase = (text: string): string => {
+ return text.replace(/-./g, (x) => x[1].toUpperCase());
+};