aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2021-12-15 18:18:49 +0100
committerArmand Philippot <git@armandphilippot.com>2021-12-15 18:18:49 +0100
commit102121498b45ef221191401f6216260f072f78a9 (patch)
treefb9ef1e648929b24bdbeefc719b5831458ef1a4b
parent0bc323a777a607090af87636026f668104cf8a0c (diff)
chore: create single post view
-rw-r--r--src/pages/article/[slug].tsx58
-rw-r--r--src/services/graphql/blog.ts33
-rw-r--r--src/services/graphql/post.ts136
-rw-r--r--src/ts/types/articles.ts36
-rw-r--r--src/ts/types/blog.ts18
-rw-r--r--src/ts/types/comments.ts18
-rw-r--r--src/ts/types/seo.ts21
7 files changed, 311 insertions, 9 deletions
diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx
new file mode 100644
index 0000000..7ad3692
--- /dev/null
+++ b/src/pages/article/[slug].tsx
@@ -0,0 +1,58 @@
+import Layout from '@components/Layouts/Layout';
+import { fetchAllPostsSlug } from '@services/graphql/blog';
+import { getPostBySlug } from '@services/graphql/post';
+import { NextPageWithLayout } from '@ts/types/app';
+import { ArticleProps } from '@ts/types/articles';
+import { loadTranslation } from '@utils/helpers/i18n';
+import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
+import { ParsedUrlQuery } from 'querystring';
+import { ReactElement } from 'react';
+
+const SingleArticle: NextPageWithLayout<ArticleProps> = ({ post }) => {
+ return (
+ <article>
+ <header>
+ <h1>{post.title}</h1>
+ <div dangerouslySetInnerHTML={{ __html: post.intro }}></div>
+ </header>
+ <div dangerouslySetInnerHTML={{ __html: post.content }}></div>
+ </article>
+ );
+};
+
+SingleArticle.getLayout = function getLayout(page: ReactElement) {
+ return <Layout>{page}</Layout>;
+};
+
+interface PostParams extends ParsedUrlQuery {
+ slug: string;
+}
+
+export const getStaticProps: GetStaticProps = async (
+ context: GetStaticPropsContext
+) => {
+ const translation = await loadTranslation(
+ context.locale!,
+ process.env.NODE_ENV === 'production'
+ );
+ const { slug } = context.params as PostParams;
+ const post = await getPostBySlug(slug);
+
+ return {
+ props: {
+ post,
+ translation,
+ },
+ };
+};
+
+export const getStaticPaths: GetStaticPaths = async () => {
+ const allSlugs = await fetchAllPostsSlug();
+
+ return {
+ paths: allSlugs.map((post) => `/article/${post.slug}`),
+ fallback: true,
+ };
+};
+
+export default SingleArticle;
diff --git a/src/services/graphql/blog.ts b/src/services/graphql/blog.ts
index 1cfdd44..127eb1e 100644
--- a/src/services/graphql/blog.ts
+++ b/src/services/graphql/blog.ts
@@ -1,13 +1,15 @@
import { ArticlePreview } from '@ts/types/articles';
import {
- fetchPostsListReturn,
- getPostsListReturn,
+ AllPostsSlugReponse,
+ FetchAllPostsSlugReturn,
+ FetchPostsListReturn,
+ GetPostsListReturn,
PostsListResponse,
} from '@ts/types/blog';
import { gql } from 'graphql-request';
import { getGraphQLClient } from './client';
-export const fetchPublishedPosts: fetchPostsListReturn = async (
+export const fetchPublishedPosts: FetchPostsListReturn = async (
first = 10,
after = ''
) => {
@@ -85,7 +87,7 @@ export const fetchPublishedPosts: fetchPostsListReturn = async (
}
};
-export const getPublishedPosts: getPostsListReturn = async ({
+export const getPublishedPosts: GetPostsListReturn = async ({
first = 10,
after = '',
}) => {
@@ -128,3 +130,26 @@ export const getPublishedPosts: getPostsListReturn = async ({
return { posts: postsList, pageInfo: rawPostsList.posts.pageInfo };
};
+
+export const fetchAllPostsSlug: FetchAllPostsSlugReturn = async () => {
+ const client = getGraphQLClient();
+
+ // 10 000 is an arbitrary number for small websites.
+ const query = gql`
+ query AllPostsSlug {
+ posts(first: 10000) {
+ nodes {
+ slug
+ }
+ }
+ }
+ `;
+
+ try {
+ const response: AllPostsSlugReponse = await client.request(query);
+ return response.posts.nodes;
+ } catch (error) {
+ console.error(JSON.stringify(error, undefined, 2));
+ process.exit(1);
+ }
+};
diff --git a/src/services/graphql/post.ts b/src/services/graphql/post.ts
new file mode 100644
index 0000000..c7144fc
--- /dev/null
+++ b/src/services/graphql/post.ts
@@ -0,0 +1,136 @@
+import {
+ Article,
+ FetchPostByReturn,
+ GetPostByReturn,
+ PostByResponse,
+} from '@ts/types/articles';
+import { gql } from 'graphql-request';
+import { getGraphQLClient } from './client';
+
+const fetchPostBySlug: FetchPostByReturn = async (slug: string) => {
+ const client = getGraphQLClient();
+ const query = gql`
+ query PostBySlug($slug: String!) {
+ postBy(slug: $slug) {
+ acfPosts {
+ postsInSubject {
+ ... on Subject {
+ id
+ featuredImage {
+ node {
+ altText
+ sourceUrl
+ title
+ }
+ }
+ slug
+ title
+ }
+ }
+ postsInThematic {
+ ... on Thematic {
+ id
+ slug
+ title
+ }
+ }
+ }
+ commentCount
+ comments {
+ nodes {
+ approved
+ author {
+ node {
+ gravatarUrl
+ name
+ url
+ }
+ }
+ commentId
+ content
+ date
+ id
+ parentDatabaseId
+ parentId
+ }
+ }
+ contentParts {
+ afterMore
+ beforeMore
+ }
+ date
+ featuredImage {
+ node {
+ altText
+ sourceUrl
+ title
+ }
+ }
+ modified
+ seo {
+ metaDesc
+ opengraphAuthor
+ opengraphDescription
+ opengraphImage {
+ altText
+ sourceUrl
+ srcSet
+ }
+ opengraphModifiedTime
+ opengraphPublishedTime
+ opengraphPublisher
+ opengraphSiteName
+ opengraphTitle
+ opengraphType
+ opengraphUrl
+ readingTime
+ }
+ title
+ }
+ }
+ `;
+
+ const variables = { slug };
+
+ try {
+ const response: PostByResponse = await client.request(query, variables);
+ return response;
+ } catch (error) {
+ console.error(JSON.stringify(error, undefined, 2));
+ process.exit(1);
+ }
+};
+
+export const getPostBySlug: GetPostByReturn = async (slug: string) => {
+ const rawPost = await fetchPostBySlug(slug);
+
+ const comments = rawPost.postBy.comments.nodes;
+ const content = rawPost.postBy.contentParts.afterMore;
+ const featuredImage = rawPost.postBy.featuredImage
+ ? rawPost.postBy.featuredImage.node
+ : {};
+ const date = {
+ publication: rawPost.postBy.date,
+ update: rawPost.postBy.modified,
+ };
+ const intro = rawPost.postBy.contentParts.beforeMore;
+ const subjects = rawPost.postBy.acfPosts.postsInSubject
+ ? rawPost.postBy.acfPosts.postsInSubject
+ : [];
+ const thematics = rawPost.postBy.acfPosts.postsInThematics
+ ? rawPost.postBy.acfPosts.postsInThematics
+ : [];
+
+ const formattedPost: Article = {
+ ...rawPost.postBy,
+ comments,
+ content,
+ featuredImage,
+ date,
+ intro,
+ subjects,
+ thematics,
+ };
+
+ return formattedPost;
+};
diff --git a/src/ts/types/articles.ts b/src/ts/types/articles.ts
index 5d5fbc5..664e237 100644
--- a/src/ts/types/articles.ts
+++ b/src/ts/types/articles.ts
@@ -1,4 +1,6 @@
+import { Comment, CommentsResponse } from './comments';
import { Cover, CoverResponse } from './cover';
+import { SEO } from './seo';
import { SubjectPreview, ThematicPreview } from './taxonomies';
export type ArticleDates = {
@@ -11,7 +13,7 @@ export type ArticlePreviewResponse = {
postsInSubject: SubjectPreview[] | null;
postsInThematics: ThematicPreview[] | null;
};
- commentCount: number;
+ commentCount: number | null;
contentParts: {
beforeMore: string;
};
@@ -25,7 +27,7 @@ export type ArticlePreviewResponse = {
};
export type ArticlePreview = {
- commentCount: number;
+ commentCount: number | null;
content: string;
databaseId: number;
date: ArticleDates;
@@ -36,3 +38,33 @@ export type ArticlePreview = {
thematics: ThematicPreview[] | [];
title: string;
};
+
+export type ArticleResponse = ArticlePreviewResponse & {
+ comments: CommentsResponse;
+ contentParts: {
+ afterMore: string;
+ };
+ seo: SEO;
+};
+
+export type Article = ArticlePreview & {
+ comments: Comment[];
+ intro: string;
+ seo: SEO;
+};
+
+export type PostByResponse = {
+ postBy: ArticleResponse;
+};
+
+export type FetchPostByReturn = (slug: string) => Promise<PostByResponse>;
+
+export type GetPostByReturn = (slug: string) => Promise<Article>;
+
+export type ArticleProps = {
+ post: Article;
+};
+
+export type ArticleSlug = {
+ slug: string;
+};
diff --git a/src/ts/types/blog.ts b/src/ts/types/blog.ts
index 366231e..76eaedb 100644
--- a/src/ts/types/blog.ts
+++ b/src/ts/types/blog.ts
@@ -1,4 +1,8 @@
-import { ArticlePreview, ArticlePreviewResponse } from './articles';
+import {
+ ArticlePreview,
+ ArticlePreviewResponse,
+ ArticleSlug,
+} from './articles';
import { PageInfo } from './pagination';
export type PostsListEdge = {
@@ -18,7 +22,7 @@ export type PostsList = {
pageInfo: PageInfo;
};
-export type fetchPostsListReturn = (
+export type FetchPostsListReturn = (
first?: number,
after?: string
) => Promise<PostsListResponse>;
@@ -28,8 +32,16 @@ type PostsListProps = {
after?: string;
};
-export type getPostsListReturn = (props: PostsListProps) => Promise<PostsList>;
+export type GetPostsListReturn = (props: PostsListProps) => Promise<PostsList>;
export type BlogPageProps = {
fallback: PostsList;
};
+
+export type AllPostsSlugReponse = {
+ posts: {
+ nodes: ArticleSlug[];
+ };
+};
+
+export type FetchAllPostsSlugReturn = () => Promise<ArticleSlug[]>;
diff --git a/src/ts/types/comments.ts b/src/ts/types/comments.ts
new file mode 100644
index 0000000..b196142
--- /dev/null
+++ b/src/ts/types/comments.ts
@@ -0,0 +1,18 @@
+export type CommentAuthor = {
+ gravatarUrl: string;
+ name: string;
+ url: string;
+};
+
+export type Comment = {
+ approved: '';
+ author: CommentAuthor;
+ commentId: number;
+ content: string;
+ date: string;
+ id: string;
+};
+
+export type CommentsResponse = {
+ nodes: Comment[];
+};
diff --git a/src/ts/types/seo.ts b/src/ts/types/seo.ts
new file mode 100644
index 0000000..fa30fe4
--- /dev/null
+++ b/src/ts/types/seo.ts
@@ -0,0 +1,21 @@
+export type SEO = {
+ title: string;
+ metaDesc: string;
+ readingTime: number;
+ opengraphAuthor: string;
+ opengraphDescription: string;
+ opengraphModifiedTime: string;
+ opengraphPublishedTime: string;
+ opengraphPublisher: string;
+ opengraphSiteName: string;
+ opengraphTitle: string;
+ opengraphType: string;
+ opengraphUrl: string;
+ opengraphImage: {
+ altText: string;
+ sourceUrl: string;
+ srcSet: string;
+ };
+ metaRobotsNofollow: string;
+ metaRobotsNoindex: string;
+};