blog articles

This commit is contained in:
m5r 2021-08-04 05:04:17 +08:00
parent 1f80ce4114
commit 6f64d80170
7 changed files with 1763 additions and 174 deletions

View File

@ -0,0 +1,10 @@
import type { BlitzApiRequest, BlitzApiResponse } from "blitz";
export default async function preview(req: BlitzApiRequest, res: BlitzApiResponse) {
// Exit the current user from "Preview Mode". This function accepts no args.
res.clearPreviewData();
// Redirect the user back to the index page.
res.writeHead(307, { Location: "/" });
res.end();
}

View File

@ -0,0 +1,34 @@
import type { BlitzApiRequest, BlitzApiResponse } from "blitz";
import { getConfig } from "blitz";
import { getPreviewPostBySlug } from "../../../../integrations/datocms";
const { serverRuntimeConfig } = getConfig();
export default async function preview(req: BlitzApiRequest, res: BlitzApiResponse) {
// Check the secret and next parameters
// This secret should only be known to this API route and the CMS
if (
req.query.secret !== serverRuntimeConfig.datoCms.previewSecret ||
!req.query.slug ||
Array.isArray(req.query.slug)
) {
return res.status(401).json({ message: "Invalid token" });
}
// Fetch the headless CMS to check if the provided `slug` exists
const post = await getPreviewPostBySlug(req.query.slug);
// If the slug doesn't exist prevent preview mode from being enabled
if (!post) {
return res.status(401).json({ message: "Invalid slug" });
}
// Enable Preview Mode by setting the cookies
res.setPreviewData({});
// Redirect to the path from the fetched post
// We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities
res.writeHead(307, { Location: `/posts/${post.slug}` });
res.end();
}

View File

@ -0,0 +1,86 @@
import { BlitzPage, GetStaticPaths, GetStaticProps, Head, useRouter } from "blitz";
import ErrorPage from "next/error";
import type { Post } from "integrations/datocms";
import { getAllPostsWithSlug, getPostAndMorePosts, markdownToHtml } from "integrations/datocms";
type Props = {
post: Post;
morePosts: Post[];
preview: boolean;
};
const PostPage: BlitzPage<Props> = ({ post, morePosts, preview }) => {
const router = useRouter();
if (!router.isFallback && !post?.slug) {
return <ErrorPage statusCode={404} />;
}
console.log("post", post);
// TODO
/*return (
<Layout preview={preview}>
<Container>
<Header />
{router.isFallback ? (
<PostTitle>Loading</PostTitle>
) : (
<>
<article>
<Head>
<title>
{post.title} | Next.js Blog Example with {CMS_NAME}
</title>
<meta property="og:image" content={post.ogImage.url} />
</Head>
<PostHeader
title={post.title}
coverImage={post.coverImage}
date={post.date}
author={post.author}
/>
<PostBody content={post.content} />
</article>
<SectionSeparator />
{morePosts.length > 0 && <MoreStories posts={morePosts} />}
</>
)}
</Container>
</Layout>
);*/
return null;
};
export default PostPage;
export const getStaticProps: GetStaticProps = async ({ params, preview = false }) => {
if (!params || !params.slug || Array.isArray(params.slug)) {
return {
notFound: true,
};
}
const data = await getPostAndMorePosts(params.slug, preview);
const content = await markdownToHtml(data.post.content || "");
return {
props: {
preview,
post: {
...data.post,
content,
},
morePosts: data.morePosts,
},
};
};
export const getStaticPaths: GetStaticPaths = async () => {
const allPosts = await getAllPostsWithSlug();
return {
paths: allPosts.map((post) => `/articles/${post.slug}`),
fallback: true,
};
};

View File

@ -31,6 +31,10 @@ const config: BlitzConfig = {
webPush: { webPush: {
privateKey: process.env.WEB_PUSH_VAPID_PRIVATE_KEY, privateKey: process.env.WEB_PUSH_VAPID_PRIVATE_KEY,
}, },
datoCms: {
apiToken: process.env.DATOCMS_API_TOKEN,
previewSecret: process.env.DATOCMS_PREVIEW_SECRET,
},
}, },
publicRuntimeConfig: { publicRuntimeConfig: {
webPush: { webPush: {

196
integrations/datocms.ts Normal file
View File

@ -0,0 +1,196 @@
import { getConfig } from "blitz";
import { remark } from "remark";
import html from "remark-html";
export async function markdownToHtml(markdown: string) {
const result = await remark().use(html).process(markdown);
return result.toString();
}
const { serverRuntimeConfig } = getConfig();
// See: https://www.datocms.com/blog/offer-responsive-progressive-lqip-images-in-2020
const responsiveImageFragment = `
fragment responsiveImageFragment on ResponsiveImage {
srcSet
webpSrcSet
sizes
src
width
height
aspectRatio
alt
title
bgColor
base64
}
`;
type Params = {
variables?: Record<string, string>;
preview?: boolean;
};
async function fetchAPI<Response = unknown>(query: string, { variables, preview }: Params = {}) {
const res = await fetch("https://graphql.datocms.com" + (preview ? "/preview" : ""), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${serverRuntimeConfig.datoCms.apiToken}`,
},
body: JSON.stringify({
query,
variables,
}),
});
const json = await res.json();
if (json.errors) {
console.error(json.errors);
throw new Error("Failed to fetch API");
}
return json.data as Response;
}
export type Post = {
slug: string;
title: string;
excerpt: string;
date: string; // "YYYY-MM-DD"
content: string;
ogImage: {
url: string;
};
coverImage: {
responsiveImage: {
srcSet: string;
webpSrcSet: string;
sizes: string;
src: string;
width: 2000;
height: 1000;
aspectRatio: 2;
alt: string | null;
title: string | null;
bgColor: string | null;
base64: string;
};
};
author: {
name: string;
picture: {
url: string;
};
};
};
export async function getPreviewPostBySlug(slug: string) {
const data = await fetchAPI<{ post: Pick<Post, "slug"> } | null>(
`
query PostBySlug($slug: String) {
post(filter: {slug: {eq: $slug}}) {
slug
}
}`,
{
preview: true,
variables: {
slug,
},
},
);
return data?.post;
}
export async function getAllPostsWithSlug() {
const { allPosts } = await fetchAPI<{ allPosts: Pick<Post, "slug">[] }>(`
{
allPosts {
slug
}
}
`);
return allPosts;
}
export async function getAllPostsForHome(preview: boolean) {
const data = await fetchAPI<{ allPosts: Omit<Post, "content" | "ogImage">[] }>(
`
{
allPosts(orderBy: date_DESC, first: 20) {
title
slug
excerpt
date
coverImage {
responsiveImage(imgixParams: {fm: jpg, fit: crop, w: 2000, h: 1000 }) {
...responsiveImageFragment
}
}
author {
name
picture {
url(imgixParams: {fm: jpg, fit: crop, w: 100, h: 100, sat: -100})
}
}
}
}
${responsiveImageFragment}
`,
{ preview },
);
return data?.allPosts;
}
export async function getPostAndMorePosts(slug: string, preview: boolean) {
return fetchAPI<{ post: Omit<Post, "excerpt">; morePosts: Omit<Post, "content" | "ogImage">[] }>(
`
query PostBySlug($slug: String) {
post(filter: {slug: {eq: $slug}}) {
title
slug
content
date
ogImage: coverImage{
url(imgixParams: {fm: jpg, fit: crop, w: 2000, h: 1000 })
}
coverImage {
responsiveImage(imgixParams: {fm: jpg, fit: crop, w: 2000, h: 1000 }) {
...responsiveImageFragment
}
}
author {
name
picture {
url(imgixParams: {fm: jpg, fit: crop, w: 100, h: 100, sat: -100})
}
}
}
morePosts: allPosts(orderBy: date_DESC, first: 2, filter: {slug: {neq: $slug}}) {
title
slug
excerpt
date
coverImage {
responsiveImage(imgixParams: {fm: jpg, fit: crop, w: 2000, h: 1000 }) {
...responsiveImageFragment
}
}
author {
name
picture {
url(imgixParams: {fm: jpg, fit: crop, w: 100, h: 100, sat: -100})
}
}
}
}
${responsiveImageFragment}
`,
{
preview,
variables: {
slug,
},
},
);
}

1605
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -67,6 +67,8 @@
"react-spring": "9.2.4", "react-spring": "9.2.4",
"react-spring-bottom-sheet": "3.4.0", "react-spring-bottom-sheet": "3.4.0",
"react-use-gesture": "9.1.3", "react-use-gesture": "9.1.3",
"remark": "14.0.1",
"remark-html": "13.0.1",
"tailwindcss": "2.2.7", "tailwindcss": "2.2.7",
"twilio": "3.66.1", "twilio": "3.66.1",
"web-push": "3.4.5", "web-push": "3.4.5",