blog articles
This commit is contained in:
parent
1f80ce4114
commit
6f64d80170
10
app/blog/api/articles/exit-preview.ts
Normal file
10
app/blog/api/articles/exit-preview.ts
Normal 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();
|
||||||
|
}
|
34
app/blog/api/articles/preview.ts
Normal file
34
app/blog/api/articles/preview.ts
Normal 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();
|
||||||
|
}
|
86
app/blog/pages/articles/[slug].tsx
Normal file
86
app/blog/pages/articles/[slug].tsx
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
@ -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
196
integrations/datocms.ts
Normal 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
1605
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user