summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/ProjectPreview/ProjectPreview.tsx4
-rw-r--r--src/components/ProjectsList/ProjectsList.tsx2
-rw-r--r--src/pages/projet/[slug].tsx154
-rw-r--r--src/ts/types/app.ts13
-rw-r--r--src/utils/helpers/projects.ts86
5 files changed, 218 insertions, 41 deletions
diff --git a/src/components/ProjectPreview/ProjectPreview.tsx b/src/components/ProjectPreview/ProjectPreview.tsx
index 91969b0..f5d2e8f 100644
--- a/src/components/ProjectPreview/ProjectPreview.tsx
+++ b/src/components/ProjectPreview/ProjectPreview.tsx
@@ -18,11 +18,11 @@ const ProjectPreview = ({ project }: { project: Project }) => {
layout="fill"
objectFit="contain"
objectPosition="center"
- alt={`${project.meta.title} picture`}
+ alt={t`${project.title} picture`}
/>
</div>
)}
- <h2 className={styles.title}>{project.meta.title}</h2>
+ <h2 className={styles.title}>{project.title}</h2>
</header>
<div
className={styles.body}
diff --git a/src/components/ProjectsList/ProjectsList.tsx b/src/components/ProjectsList/ProjectsList.tsx
index 609d824..07e6a71 100644
--- a/src/components/ProjectsList/ProjectsList.tsx
+++ b/src/components/ProjectsList/ProjectsList.tsx
@@ -5,7 +5,7 @@ import styles from './ProjectsList.module.scss';
const ProjectsList = ({ projects }: { projects: Project[] }) => {
const getProjectItems = () => {
return projects.map((project) => {
- return project.meta.title ? (
+ return project.title ? (
<li className={styles.item} key={project.id}>
<ProjectPreview project={project} />
</li>
diff --git a/src/pages/projet/[slug].tsx b/src/pages/projet/[slug].tsx
new file mode 100644
index 0000000..03aa6ea
--- /dev/null
+++ b/src/pages/projet/[slug].tsx
@@ -0,0 +1,154 @@
+import { getLayout } from '@components/Layouts/Layout';
+import PostHeader from '@components/PostHeader/PostHeader';
+import Sidebar from '@components/Sidebar/Sidebar';
+import { ToC } from '@components/Widgets';
+import { config } from '@config/website';
+import styles from '@styles/pages/Page.module.scss';
+import {
+ NextPageWithLayout,
+ Project as ProjectData,
+ ProjectProps,
+} from '@ts/types/app';
+import { loadTranslation } from '@utils/helpers/i18n';
+import {
+ getAllProjectsFilename,
+ getProjectData,
+} from '@utils/helpers/projects';
+import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
+import dynamic from 'next/dynamic';
+import Head from 'next/head';
+import { useRouter } from 'next/router';
+import { ParsedUrlQuery } from 'querystring';
+import { Article, Graph, WebPage } from 'schema-dts';
+
+const Project: NextPageWithLayout<ProjectProps> = ({
+ project,
+}: {
+ project: ProjectData;
+}) => {
+ const router = useRouter();
+ const projectUrl = `${config.url}${router.asPath}`;
+ const { cover, id, intro, meta, title, seo } = project;
+ const dates = {
+ publication: meta.publishedOn,
+ update: meta.updatedOn,
+ };
+
+ const ProjectContent = dynamic(
+ () => import(`../../content/projects/${id}.mdx`)
+ );
+
+ const webpageSchema: WebPage = {
+ '@id': `${projectUrl}`,
+ '@type': 'WebPage',
+ breadcrumb: { '@id': `${config.url}/#breadcrumb` },
+ name: seo.title,
+ description: seo.description,
+ inLanguage: config.locales.defaultLocale,
+ reviewedBy: { '@id': `${config.url}/#branding` },
+ url: `${config.url}`,
+ isPartOf: {
+ '@id': `${config.url}`,
+ },
+ };
+
+ const publicationDate = new Date(dates.publication);
+ const updateDate = new Date(dates.update);
+
+ const articleSchema: Article = {
+ '@id': `${config.url}/subject`,
+ '@type': 'Article',
+ name: title,
+ description: intro,
+ author: { '@id': `${config.url}/#branding` },
+ copyrightYear: publicationDate.getFullYear(),
+ creator: { '@id': `${config.url}/#branding` },
+ dateCreated: publicationDate.toISOString(),
+ dateModified: updateDate.toISOString(),
+ datePublished: publicationDate.toISOString(),
+ editor: { '@id': `${config.url}/#branding` },
+ thumbnailUrl: cover,
+ image: cover,
+ inLanguage: config.locales.defaultLocale,
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ mainEntityOfPage: { '@id': `${projectUrl}` },
+ };
+
+ const schemaJsonLd: Graph = {
+ '@context': 'https://schema.org',
+ '@graph': [webpageSchema, articleSchema],
+ };
+
+ return (
+ <>
+ <Head>
+ <title>{seo.title}</title>
+ <meta name="description" content={seo.description} />
+ <meta property="og:url" content={`${projectUrl}`} />
+ <meta property="og:type" content="article" />
+ <meta property="og:title" content={title} />
+ <meta property="og:description" content={intro} />
+ <script
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ ></script>
+ </Head>
+ <article
+ id="project"
+ className={`${styles.article} ${styles['article--no-comments']}`}
+ >
+ <PostHeader title={title} intro={intro} meta={{ dates }} />
+ <Sidebar position="left">
+ <ToC />
+ </Sidebar>
+ <div className={styles.body}>
+ <ProjectContent />
+ </div>
+ </article>
+ </>
+ );
+};
+
+Project.getLayout = getLayout;
+
+interface ProjectParams extends ParsedUrlQuery {
+ slug: string;
+}
+
+export const getStaticProps: GetStaticProps = async (
+ context: GetStaticPropsContext
+) => {
+ const translation = await loadTranslation(
+ context.locale!,
+ process.env.NODE_ENV === 'production'
+ );
+ const breadcrumbTitle = '';
+ const { slug } = context.params as ProjectParams;
+ const project = await getProjectData(slug);
+
+ return {
+ props: {
+ breadcrumbTitle,
+ project,
+ translation,
+ },
+ };
+};
+
+export const getStaticPaths: GetStaticPaths = async () => {
+ const filenames = getAllProjectsFilename();
+ const paths = filenames.map((filename) => {
+ return {
+ params: {
+ slug: filename,
+ },
+ };
+ });
+
+ return {
+ paths,
+ fallback: false,
+ };
+};
+
+export default Project;
diff --git a/src/ts/types/app.ts b/src/ts/types/app.ts
index 6f9bbce..b2a5cd6 100644
--- a/src/ts/types/app.ts
+++ b/src/ts/types/app.ts
@@ -1,6 +1,6 @@
import { NextPage } from 'next';
import { AppProps } from 'next/app';
-import { ReactElement, ReactNode } from 'react';
+import { ComponentType, ReactElement, ReactNode } from 'react';
import { PostBy } from './articles';
import { AllPostsSlug, RawPostsList } from './blog';
import { CommentData, CreateComment } from './comments';
@@ -89,7 +89,7 @@ export type Meta = {
updatedOn: string;
};
-export type ProjectMeta = Meta & {
+export type ProjectMeta = Omit<Meta, 'title'> & {
license: string;
repos?: {
github?: string;
@@ -109,6 +109,15 @@ export type Project = {
intro: string;
meta: ProjectMeta;
slug: string;
+ title: string;
+ seo: {
+ title: string;
+ description: string;
+ };
+};
+
+export type ProjectProps = {
+ project: Project;
};
export type Slug = {
diff --git a/src/utils/helpers/projects.ts b/src/utils/helpers/projects.ts
index 3736242..12e5912 100644
--- a/src/utils/helpers/projects.ts
+++ b/src/utils/helpers/projects.ts
@@ -3,46 +3,50 @@ 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.
+ */
+export const getProjectData = async (id: string): Promise<Project> => {
+ try {
+ const {
+ image,
+ intro,
+ meta,
+ seo,
+ }: {
+ image: string;
+ intro: string;
+ meta: ProjectMeta & { title: string };
+ seo: { title: string; description: string };
+ } = await import(`../../content/projects/${id}.mdx`);
+
+ const { title, ...onlyMeta } = meta;
+
+ return {
+ id,
+ cover: image || '',
+ intro: intro || '',
+ meta: onlyMeta || {},
+ slug: id,
+ title,
+ seo: seo || {},
+ };
+ } catch (err) {
+ console.error(err);
+ throw err;
+ }
+};
+
+/**
* Retrieve the projects data from filenames.
* @param {string[]} filenames - An array of filenames.
- * @returns {Promise<Project[]>} An array of projects.
+ * @returns {Promise<Project[]>} An array of projects with meta.
*/
const getProjectsWithMeta = async (filenames: string[]): Promise<Project[]> => {
return Promise.all(
filenames.map(async (filename) => {
- const id = filename.replace(/\.mdx$/, '');
-
- try {
- const {
- image,
- intro,
- meta,
- }: { image: string; intro: string; meta: ProjectMeta } = await import(
- `../../content/projects/${filename}`
- );
-
- const projectMeta: ProjectMeta = meta
- ? meta
- : {
- title: '',
- publishedOn: '',
- updatedOn: '',
- license: '',
- };
- const projectIntro = intro ? intro : '';
- const projectCover = image ? image : '';
-
- return {
- id,
- cover: projectCover,
- intro: projectIntro,
- meta: projectMeta,
- slug: id,
- };
- } catch (err) {
- console.error(err);
- throw err;
- }
+ return getProjectData(filename);
})
);
};
@@ -60,12 +64,22 @@ const sortProjectByPublicationDate = (a: Project, b: Project) => {
};
/**
+ * 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.
*/
export const getSortedProjects = async (): Promise<Project[]> => {
- const projectsDirectory = path.join(process.cwd(), 'src/content/projects');
- const filenames = readdirSync(projectsDirectory);
+ const filenames = getAllProjectsFilename();
const projects = await getProjectsWithMeta(filenames);
return [...projects].sort(sortProjectByPublicationDate);