From fba1852e7f7ffdcef26a1db4d8c025e6f28786c4 Mon Sep 17 00:00:00 2001 From: TheCatLady <52870424+TheCatLady@users.noreply.github.com> Date: Thu, 12 Oct 2023 15:07:30 -0700 Subject: [PATCH] feat: workaround for next-sitemap bug, blog posts SSG --- next-sitemap.config.js | 18 +++ src/api/hashnode.ts | 134 ++++++++++++++++++ src/app/api/blog/post/route.ts | 50 +------ src/app/api/blog/posts/route.ts | 59 ++------ src/app/blog/[slug]/page.tsx | 56 ++++---- src/app/blog/page.tsx | 4 +- src/app/code-of-conduct/page.tsx | 2 +- src/app/cookie-policy/page.tsx | 2 +- src/app/layout.tsx | 16 +-- src/app/not-found.tsx | 2 +- src/app/page.tsx | 2 +- src/app/server-sitemap-index.xml/route.ts | 12 ++ src/components/blog/BlogPost/index.tsx | 43 ++---- src/components/blog/MarkdownContent/index.tsx | 2 + src/components/common/NextImage.tsx | 2 + yarn.lock | 18 +-- 16 files changed, 250 insertions(+), 172 deletions(-) create mode 100644 src/api/hashnode.ts create mode 100644 src/app/server-sitemap-index.xml/route.ts diff --git a/next-sitemap.config.js b/next-sitemap.config.js index e7c04c8..1499e59 100644 --- a/next-sitemap.config.js +++ b/next-sitemap.config.js @@ -5,11 +5,29 @@ module.exports = { siteUrl: 'https://fix.tt', generateRobotsTxt: true, + exclude: ['/server-sitemap-index.xml'], + additionalPaths: async (config) => + await Promise.all( + ['/', '/blog', '/code-of-conduct', '/cookie-policy'].map( + async (path) => await config.transform(config, path), + ), + ), robotsTxtOptions: { + additionalSitemaps: ['https://fix.tt/server-sitemap-index.xml'], policies: [ process.env.VERCEL_ENV === 'production' ? { userAgent: '*', allow: '/' } : { userAgent: '*', disallow: '/' }, + { + userAgent: '*', + disallow: [ + '/*.json$', + '/*_buildManifest.js$', + '/*_middlewareManifest.js$', + '/*_ssgManifest.js$', + '/*.js$', + ], + }, ], }, }; diff --git a/src/api/hashnode.ts b/src/api/hashnode.ts new file mode 100644 index 0000000..ee1767d --- /dev/null +++ b/src/api/hashnode.ts @@ -0,0 +1,134 @@ +import { gql, request } from 'graphql-request'; + +import { HASHNODE_ENDPOINT, HASHNODE_HOST } from '@/constants/hashnode'; +import { + HashnodePostResponse, + HashnodePostsResponse, +} from '@/interfaces/hashnode'; + +export async function getHashnodePostSlugs() { + const variables = { + host: HASHNODE_HOST, + first: 20, + }; + + const query = gql` + query Publication($host: String!, $first: Int!) { + publication(host: $host) { + posts(first: $first) { + edges { + node { + slug + } + } + } + } + } + `; + + const data = await request( + HASHNODE_ENDPOINT, + query, + variables, + ); + + return data.publication.posts.edges.map((edge) => edge.node.slug); +} + +export async function getHashnodePosts({ + first, + after, +}: { + first?: number; + after?: string; +}) { + const variables = { + host: HASHNODE_HOST, + first: first && first > 0 ? (first > 20 ? 20 : first) : 5, + after, + }; + + const query = gql` + query Publication($host: String!, $first: Int!, $after: String) { + publication(host: $host) { + posts(first: $first, after: $after) { + edges { + node { + title + brief + slug + coverImage { + url + } + author { + name + tagline + profilePicture + socialMediaLinks { + website + linkedin + } + } + readTimeInMinutes + publishedAt + } + cursor + } + } + } + } + `; + + const data = await request( + HASHNODE_ENDPOINT, + query, + variables, + ); + + return data.publication.posts.edges; +} + +export async function getHashnodePost({ slug }: { slug: string }) { + const variables = { + host: HASHNODE_HOST, + slug, + }; + + const query = ` + query Publication($host: String!, $slug: String!) { + publication(host: $host) { + post(slug: $slug) { + title + subtitle + brief + slug + coverImage { + url + } + author { + name + tagline + profilePicture + socialMediaLinks { + website + linkedin + } + } + content { + markdown + } + readTimeInMinutes + publishedAt + } + } + } + `; + + const data = await request( + HASHNODE_ENDPOINT, + query, + variables, + ); + + return data.publication.post; +} diff --git a/src/app/api/blog/post/route.ts b/src/app/api/blog/post/route.ts index eaa0ef4..f065ecb 100644 --- a/src/app/api/blog/post/route.ts +++ b/src/app/api/blog/post/route.ts @@ -1,52 +1,10 @@ -import { request } from 'graphql-request'; import { type NextRequest, NextResponse } from 'next/server'; -import { HASHNODE_ENDPOINT, HASHNODE_HOST } from '@/constants/hashnode'; -import { HashnodePostResponse } from '@/interfaces/hashnode'; - -export const revalidate = 3600; // revalidate at most every hour +import { getHashnodePost } from '@/api/hashnode'; export async function GET(req: NextRequest) { - const variables = { - host: HASHNODE_HOST, - slug: req.nextUrl.searchParams.get('slug'), - }; - - const query = ` - query Publication($host: String!, $slug: String!) { - publication(host: $host) { - post(slug: $slug) { - title - subtitle - brief - slug - coverImage { - url - } - author { - name - tagline - profilePicture - socialMediaLinks { - website - linkedin - } - } - content { - markdown - } - readTimeInMinutes - publishedAt - } - } - } - `; - - const data = await request( - HASHNODE_ENDPOINT, - query, - variables, + return NextResponse.json( + await getHashnodePost({ slug: req.nextUrl.searchParams.get('slug') ?? '' }), + { status: 200 }, ); - - return NextResponse.json(data.publication.post, { status: 200 }); } diff --git a/src/app/api/blog/posts/route.ts b/src/app/api/blog/posts/route.ts index 7ce8a23..ec2dcce 100644 --- a/src/app/api/blog/posts/route.ts +++ b/src/app/api/blog/posts/route.ts @@ -1,56 +1,15 @@ -import { gql, request } from 'graphql-request'; import { type NextRequest, NextResponse } from 'next/server'; -import { HASHNODE_ENDPOINT, HASHNODE_HOST } from '@/constants/hashnode'; -import { HashnodePostsResponse } from '@/interfaces/hashnode'; - -export const revalidate = 600; // revalidate at most every 10 minutes +import { getHashnodePosts } from '@/api/hashnode'; export async function GET(req: NextRequest) { - const variables = { - host: HASHNODE_HOST, - first: parseInt(req.nextUrl.searchParams.get('first') ?? '5'), - after: req.nextUrl.searchParams.get('after'), - }; - - const query = gql` - query Publication($host: String!, $first: Int!, $after: String) { - publication(host: $host) { - posts(first: $first, after: $after) { - edges { - node { - title - brief - slug - coverImage { - url - } - author { - name - tagline - profilePicture - socialMediaLinks { - website - linkedin - } - } - readTimeInMinutes - publishedAt - } - cursor - } - } - } - } - `; - - const data = await request( - HASHNODE_ENDPOINT, - query, - variables, + return NextResponse.json( + await getHashnodePosts({ + first: parseInt(req.nextUrl.searchParams.get('first') ?? '5'), + after: req.nextUrl.searchParams.get('after') ?? undefined, + }), + { + status: 200, + }, ); - - return NextResponse.json(data.publication.posts.edges, { - status: 200, - }); } diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx index 8f9cde5..7df842c 100644 --- a/src/app/blog/[slug]/page.tsx +++ b/src/app/blog/[slug]/page.tsx @@ -1,60 +1,68 @@ import type { Metadata } from 'next'; -import { headers } from 'next/headers'; import BlogPost from '@/components/blog/BlogPost'; +import { getHashnodePost, getHashnodePostSlugs } from '@/api/hashnode'; import { siteConfig } from '@/constants/config'; -import { isVercel } from '@/constants/env'; -import { HashnodePost } from '@/interfaces/hashnode'; import { openGraph } from '@/utils/og'; -export const revalidate = 600; // revalidate at most every 10 minutes +export async function generateStaticParams() { + const slugs = await getHashnodePostSlugs(); + + return slugs.map((slug) => ({ + slug, + })); +} + +async function getPost(slug: string) { + return await getHashnodePost({ slug }); +} export async function generateMetadata({ params, }: { params: { slug: string }; }): Promise { - const host = headers().get('host'); - - const blogPost = (await fetch( - `${isVercel ? 'https' : 'http'}://${host}/api/blog/post?slug=${ - params.slug - }`, - ).then((res) => res.json())) as HashnodePost; + const post = await getPost(params.slug); return { - title: blogPost.title, - description: blogPost.brief, + title: post.title, + description: post.brief, openGraph: { - url: `${siteConfig.url}/blog/${params.slug}`, - title: blogPost.title, - description: blogPost.brief, + url: `${siteConfig.url}/blog/${post.slug}`, + title: post.title, + description: post.brief, images: [ openGraph({ - title: blogPost.title, - metadata: blogPost.subtitle, + title: post.title, + metadata: post.subtitle, }), ], type: 'article', }, twitter: { - title: `${blogPost.title} | ${siteConfig.title}`, - description: blogPost.brief, + title: `${post.title} | ${siteConfig.title}`, + description: post.brief, images: [ openGraph({ - title: blogPost.title, - metadata: blogPost.subtitle, + title: post.title, + metadata: post.subtitle, }), ], }, }; } -export default function BlogPostPage({ params }: { params: { slug: string } }) { +export default async function BlogPostPage({ + params, +}: { + params: { slug: string }; +}) { + const post = await getPost(params.slug); + return (
- +
); } diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx index 571b655..423ae57 100644 --- a/src/app/blog/page.tsx +++ b/src/app/blog/page.tsx @@ -5,8 +5,6 @@ import BlogPostList from '@/components/blog/BlogPostList'; import { siteConfig } from '@/constants/config'; import { openGraph } from '@/utils/og'; -export const revalidate = 600; // revalidate at most every 10 minutes - export const metadata: Metadata = { title: 'Blog', openGraph: { @@ -30,7 +28,7 @@ export const metadata: Metadata = { }, }; -export default function BlogPage() { +export default function Blog() { return (
diff --git a/src/app/code-of-conduct/page.tsx b/src/app/code-of-conduct/page.tsx index 68a775e..6c78a70 100644 --- a/src/app/code-of-conduct/page.tsx +++ b/src/app/code-of-conduct/page.tsx @@ -6,7 +6,7 @@ export const metadata: Metadata = { title: 'Code of conduct', }; -export default function CodeOfConductPage() { +export default function CodeOfConduct() { return (
diff --git a/src/app/cookie-policy/page.tsx b/src/app/cookie-policy/page.tsx index 3cbb660..7333ec6 100644 --- a/src/app/cookie-policy/page.tsx +++ b/src/app/cookie-policy/page.tsx @@ -6,7 +6,7 @@ export const metadata: Metadata = { title: 'Cookie policy', }; -export default function CookiePolicyPage() { +export default function CookiePolicy() { return (
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index fd9888d..e435cd7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -95,10 +95,10 @@ export default function RootLayout({ const consent = cookieStore.get('cookie_consent')?.value === 'true'; return ( - - - - + + + +
{children}