Add MDX support + clean TS

This commit is contained in:
Michael
2022-01-18 21:57:37 +01:00
parent 8aede6e33e
commit 6e319c0527
22 changed files with 928 additions and 228 deletions

View File

@@ -1,15 +1,18 @@
---
title: Another example
description: A very small example of a blog post with a title and a description.
excerpt: Want to easily integrate a blog into your SaaS? Nothing could be simpler, with SupaNexTail, everything is already configured, you just have to write!
date: '2022-01-17'
coverImage: '/blog/coverdefault.png'
coverImage: '/blog/covers/writeblog.png'
author:
name: 'Michael B'
picture: '/blog/author/Avatar1.svg'
ogImage:
url: '/blog/Blog.png'
ogImage: '/blog/covers/writeblog.png'
url: '/blog/Blog.png'
---
import Button from '../components/ButtonMdx';
## A header
This is a small example of a blog post.
@@ -24,4 +27,8 @@ Suspendisse fringilla sit amet lorem nec semper. Curabitur aliquet ultrices rhon
Integer egestas nulla non ultricies ultricies. Nulla et ultrices odio. Etiam eget tempor ipsum. Pellentesque sit amet commodo est. Praesent tincidunt faucibus orci. Proin at aliquet orci. Sed eget ultrices ligula. Maecenas bibendum, leo quis suscipit posuere, tellus libero maximus nibh, vel bibendum eros leo non nulla. Proin quis gravida leo. Cras efficitur mi congue lacus viverra placerat quis in leo. Nam faucibus est sollicitudin, suscipit erat ac, ultrices metus. Fusce sagittis vulputate turpis eget laoreet.
### A smaller header
### And this is a MDX example
This button is a React component inside a MDX file. Crazy, right?
<Button>Click me</Button>

View File

@@ -5,7 +5,7 @@ date: '2022-01-17'
author:
name: Michael B
picture: '/blog/author/Avatar1.svg'
coverImage: '/blog/coverdefault.png'
coverImage: '/blog/covers/smallexample.png'
ogImage:
url: '/blog/Blog.png'
---

View File

@@ -1,44 +0,0 @@
/* eslint-disable unicorn/prevent-abbreviations */
import { format, parseISO } from 'date-fns';
import Link from 'next/link';
import PostType from 'types/post';
import React from 'react';
type Properties = {
posts: PostType[];
};
const Blog = (props: Properties): JSX.Element => {
const { posts } = props;
return (
<div className="max-w-xl px-5 py-10 m-auto">
<h1>Welcome on the SupaNexTail blog</h1>
{posts.map((post) => (
<article key={post.slug} className="mt-12">
<p className="mb-1 text-sm text-gray-500 dark:text-gray-400">
{post.date && format(parseISO(post.date), 'MMMM dd, yyyy')}
</p>
<h1 className="mb-2 text-xl">
<Link as={`/posts/${post.slug}`} href={`/posts/[slug]`}>
<a className="text-gray-900 dark:text-white dark:hover:text-blue-400">
{post.title}
</a>
</Link>
</h1>
<p className="mb-3">{post.description}</p>
<p>
<Link as={`/posts/${post.slug}`} href={`/posts/[slug]`}>
<a>Read More</a>
</Link>
</p>
</article>
))}
</div>
);
};
export default Blog;

3
components/ButtonMdx.tsx Normal file
View File

@@ -0,0 +1,3 @@
export default function Button({ children }: any): JSX.Element {
return <button className="btn-primary btn">{children}</button>;
}

View File

@@ -24,6 +24,12 @@ const Nav = ({ user, signOut }: NavProperties): JSX.Element => {
</Link>
)}
<Link href="/blog">
<a id="pricing" className="nav-btn">
Blog
</a>
</Link>
<Link href="/pricing">
<a id="pricing" className="nav-btn">
Pricing

View File

@@ -10,12 +10,12 @@ const Avatar = ({ name, picture }: Properties): JSX.Element => {
<div className="flex items-center">
<Image
src={picture}
className="w-12 h-12 rounded-full mr-4"
className="w-12 h-12 rounded-full"
alt={name}
width={48}
height={48}
/>
<div className="text-xl font-bold">{name}</div>
<div className="ml-3 font-medium">{name}</div>
</div>
);
};

View File

@@ -5,7 +5,7 @@ type Properties = {
};
const Container: FunctionComponent = ({ children }: Properties) => {
return <div className="container mx-auto px-5">{children}</div>;
return <div className="container mx-auto p-10">{children}</div>;
};
export default Container;

View File

@@ -15,12 +15,13 @@ const CoverImage = ({ title, src, slug }: Properties): JSX.Element => {
layout="fill"
objectFit="contain"
objectPosition={'center top'}
quality={100}
/>
);
return (
<div className="sm:mx-0 flex justify-center relative max-w-2xl h-48">
<div className="sm:mx-0 flex justify-center relative max-w-full h-48">
{slug ? (
<Link as={`/posts/${slug}`} href="/posts/[slug]">
<Link as={`/blog/${slug}`} href="/blog/[slug]">
<a aria-label={title}>{image}</a>
</Link>
) : (

View File

@@ -28,7 +28,7 @@ const HeroPost = ({
</div>
<div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28">
<div>
<h3 className="mb-4 text-4xl lg:text-5xl leading-tight">
<h3 className="mb-4 text-4xl lg:text-5xl leading-tight font-semibold">
<Link as={`/blog/${slug}`} href="/blog/[slug]">
<a className="hover:underline">{title}</a>
</Link>

View File

@@ -1,11 +1,16 @@
import React from 'react';
import { getMDXComponent } from 'mdx-bundler/client';
import { useMemo } from 'react';
type Properties = {
content: string;
code: string;
};
const PostBody = ({ content }: Properties): JSX.Element => {
const PostBody = ({ code }: Properties): JSX.Element => {
const BlogPost = useMemo(() => getMDXComponent(code), [code]);
return (
<div className="max-w-2xl mx-auto">
<div dangerouslySetInnerHTML={{ __html: content }} />
<BlogPost />
</div>
);
};

View File

@@ -26,7 +26,7 @@ const PostPreview = ({
<div className="mb-5">
<CoverImage slug={slug} title={title} src={coverImage} />
</div>
<h3 className="text-3xl mb-3 leading-snug">
<h3 className="text-3xl mb-3 leading-snug font-medium">
<Link as={`/blog/${slug}`} href="/blog/[slug]">
<a className="hover:underline">{title}</a>
</Link>

View File

@@ -6,7 +6,7 @@ type Properties = {
const PostTitle = ({ children }: Properties): JSX.Element => {
return (
<h1 className="text-5xl md:text-7xl lg:text-8xl font-bold tracking-tighter leading-tight md:leading-none mb-12 text-center md:text-left">
<h1 className="text-4xl md:text-6xl lg:text-7xl font-semibold tracking-tighter leading-tight md:leading-none mb-5 text-center md:text-left">
{children}
</h1>
);

View File

@@ -1,5 +1,5 @@
const SectionSeparator = (): JSX.Element => {
return <hr className="border-neutral-200 mt-28 mb-24" />;
return <hr className="border-neutral-200 mt-16 mb-12" />;
};
export default SectionSeparator;

View File

@@ -1,47 +1,73 @@
import fs from 'fs'
import { join } from 'path'
import matter from 'gray-matter'
import { bundleMDX } from 'mdx-bundler';
import fs from 'node:fs';
import { join } from 'node:path';
import matter from 'gray-matter';
const postsDirectory = join(process.cwd(), '_posts')
const postsDirectory = join(process.cwd(), '_posts');
export function getPostSlugs() {
return fs.readdirSync(postsDirectory)
export function getPostSlugs(): string[] {
return fs.readdirSync(postsDirectory);
}
type Items = {
[key: string]: string;
};
export function getPostBySlug(slug: string, fields: string[] = []) {
const realSlug = slug.replace(/\.md$/, '')
const fullPath = join(postsDirectory, `${realSlug}.md`)
const fileContents = fs.readFileSync(fullPath, 'utf8')
const { data, content } = matter(fileContents)
export function getPostBySlug(slug: string, fields: string[] = []): Items {
const realSlug = slug.replace(/\.mdx$/, '');
const fullPath = join(postsDirectory, `${realSlug}.mdx`);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const {
data,
content,
}: {
data: Items;
content: string;
} = matter(fileContents);
type Items = {
[key: string]: string
}
const items: Items = {}
const items: Items = {};
// Ensure only the minimal needed data is exposed
fields.forEach((field) => {
for (const field of fields) {
if (field === 'slug') {
items[field] = realSlug
items[field] = realSlug;
}
if (field === 'content') {
items[field] = content
items[field] = content;
}
if (typeof data[field] !== 'undefined') {
items[field] = data[field]
items[field] = data[field];
}
}
})
return items
return items;
}
export function getAllPosts(fields: string[] = []) {
const slugs = getPostSlugs()
const posts = slugs
export function getAllPosts(fields: string[] = []): Items[] {
const slugs = getPostSlugs();
return (
slugs
.map((slug) => getPostBySlug(slug, fields))
// sort posts by date in descending order
.sort((post1, post2) => (post1.date > post2.date ? -1 : 1))
return posts
);
}
type Parameters_ = {
slug: string;
frontmatter: { [key: string]: any };
code: string;
};
export async function getPostData(slug: string): Promise<Parameters_> {
const realSlug = slug.replace(/\.mdx$/, '');
const fullPath = join(postsDirectory, `${realSlug}.mdx`);
const source = fs.readFileSync(fullPath, 'utf8');
const { code, frontmatter } = await bundleMDX({ source: source });
return {
slug,
frontmatter,
code,
};
}

View File

@@ -1,7 +0,0 @@
import html from 'remark-html';
import { remark } from 'remark';
export default async function markdownToHtml(markdown: string) {
const result = await remark().use(html).process(markdown);
return result.toString();
}

View File

@@ -22,8 +22,10 @@
"cors": "^2.8.5",
"daisyui": "^1.20.0",
"date-fns": "^2.28.0",
"esbuild": "^0.14.11",
"express-rate-limit": "^6.0.1",
"gray-matter": "^4.0.3",
"mdx-bundler": "^8.0.1",
"micro": "^9.3.4",
"next": ">=12.0.7",
"next-seo": "^4.28.1",
@@ -32,8 +34,6 @@
"react-feather": "^2.0.9",
"react-icons": "^4.3.1",
"react-toastify": "^8.1.0",
"remark": "^14.0.2",
"remark-html": "^15.0.1",
"stripe": "^8.195.0"
},
"devDependencies": {

View File

@@ -1,17 +1,21 @@
import Container from 'components/blog/container';
import { GetStaticProps } from 'next/types';
import { GetStaticPropsResult } from 'next/types';
import Head from 'next/head';
import HeroPost from 'components/blog/hero-post';
import Layout from 'components/Layout';
import MoreStories from 'components/blog/more-stories';
import Post from '../types/post';
import SectionSeparator from 'components/blog/section-separator';
import { getAllPosts } from '../lib/api';
type Properties = {
allPosts: Post[];
};
type Items = {
[key: string]: string;
};
const Index = ({ allPosts }: Properties) => {
const Blog = ({ allPosts }: Properties): JSX.Element => {
const heroPost = allPosts[0];
const morePosts = allPosts.slice(1);
return (
@@ -33,6 +37,7 @@ const Index = ({ allPosts }: Properties) => {
excerpt={heroPost.excerpt}
/>
)}
<SectionSeparator />
{morePosts.length > 0 && <MoreStories posts={morePosts} />}
</Container>
</>
@@ -40,10 +45,14 @@ const Index = ({ allPosts }: Properties) => {
);
};
export default Index;
export default Blog;
type Parameters_ = {
allPosts: Items[];
};
// eslint-disable-next-line unicorn/prevent-abbreviations
export const getStaticProps = (): GetStaticProps => {
export const getStaticProps = (): GetStaticPropsResult<Parameters_> => {
const allPosts = getAllPosts([
'title',
'date',

View File

@@ -1,54 +1,42 @@
import { getAllPosts, getPostBySlug } from 'lib/api';
import { GetStaticPathsResult, GetStaticPropsResult } from 'next';
import { getAllPosts, getPostData } from 'lib/api';
import Container from 'components/blog/container';
import ErrorPage from 'next/error';
import Head from 'next/head';
import Header from 'components/blog/header';
import Layout from 'components/Layout';
import PostBody from 'components/blog/post-body';
import PostHeader from 'components/blog/post-header';
import PostTitle from 'components/blog/post-title';
import PostType from 'types/post';
import markdownToHtml from 'lib/markdownToHtml';
import { useRouter } from 'next/router';
type Props = {
post: PostType;
morePosts: PostType[];
type Properties = {
preview?: boolean;
code: string;
frontmatter: PostType;
};
const Post = ({ post, morePosts, preview }: Props) => {
const router = useRouter();
if (!router.isFallback && !post?.slug) {
return <ErrorPage statusCode={404} />;
}
const Post = ({ code, frontmatter }: Properties): JSX.Element => {
return (
<Layout preview={preview}>
<Layout>
<Container>
{router.isFallback ? (
<PostTitle>Loading</PostTitle>
) : (
<>
<article className="mb-32">
<Head>
<title>
{post.title} | Next.js Blog Example with{' '}
{process.env.NEXT_PUBLIC_TITLE}
{frontmatter.title} | {process.env.NEXT_PUBLIC_TITLE}
</title>
<meta property="og:image" content={post.ogImage.url} />
<meta property="og:image" content={frontmatter.ogImage.url} />
</Head>
<Header />
<PostHeader
title={post.title}
coverImage={post.coverImage}
date={post.date}
author={post.author}
title={frontmatter.title}
coverImage={frontmatter.coverImage}
date={frontmatter.date}
author={frontmatter.author}
/>
<PostBody content={post.content} />
<PostBody code={code} />
</article>
</>
)}
</Container>
</Layout>
);
@@ -56,35 +44,34 @@ const Post = ({ post, morePosts, preview }: Props) => {
export default Post;
type Params = {
type Parameters_ = {
params: {
slug: string;
};
};
export async function getStaticProps({ params }: Params) {
const post = getPostBySlug(params.slug, [
'title',
'date',
'slug',
'author',
'content',
'ogImage',
'coverImage',
]);
const content = await markdownToHtml(post.content || '');
type StaticResult = {
slug: string;
frontmatter: {
[key: string]: any;
};
code: string;
};
// eslint-disable-next-line unicorn/prevent-abbreviations
export async function getStaticProps({
params,
}: Parameters_): Promise<GetStaticPropsResult<StaticResult>> {
//const content = await markdownToHtml(post.content || '');
const postData = await getPostData(params.slug);
return {
props: {
post: {
...post,
content,
},
...postData,
},
};
}
export async function getStaticPaths() {
export function getStaticPaths(): GetStaticPathsResult {
const posts = getAllPosts(['slug']);
return {

849
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB