diff options
| -rw-r--r-- | src/components/ProjectPreview/ProjectPreview.tsx | 4 | ||||
| -rw-r--r-- | src/components/ProjectsList/ProjectsList.tsx | 2 | ||||
| -rw-r--r-- | src/pages/projet/[slug].tsx | 154 | ||||
| -rw-r--r-- | src/ts/types/app.ts | 13 | ||||
| -rw-r--r-- | src/utils/helpers/projects.ts | 86 |
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); |
