mirror of
https://github.com/fergalmoran/supanextail.git
synced 2025-12-22 09:17:54 +00:00
Add MDX support + clean TS
This commit is contained in:
@@ -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>
|
||||
@@ -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'
|
||||
---
|
||||
@@ -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
3
components/ButtonMdx.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Button({ children }: any): JSX.Element {
|
||||
return <button className="btn-primary btn">{children}</button>;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
84
lib/api.ts
84
lib/api.ts
@@ -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
|
||||
.map((slug) => getPostBySlug(slug, fields))
|
||||
// sort posts by date in descending order
|
||||
.sort((post1, post2) => (post1.date > post2.date ? -1 : 1))
|
||||
return posts
|
||||
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))
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
</title>
|
||||
<meta property="og:image" content={post.ogImage.url} />
|
||||
</Head>
|
||||
<Header />
|
||||
<PostHeader
|
||||
title={post.title}
|
||||
coverImage={post.coverImage}
|
||||
date={post.date}
|
||||
author={post.author}
|
||||
/>
|
||||
<PostBody content={post.content} />
|
||||
</article>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<article className="mb-32">
|
||||
<Head>
|
||||
<title>
|
||||
{frontmatter.title} | {process.env.NEXT_PUBLIC_TITLE}
|
||||
</title>
|
||||
<meta property="og:image" content={frontmatter.ogImage.url} />
|
||||
</Head>
|
||||
<Header />
|
||||
<PostHeader
|
||||
title={frontmatter.title}
|
||||
coverImage={frontmatter.coverImage}
|
||||
date={frontmatter.date}
|
||||
author={frontmatter.author}
|
||||
/>
|
||||
<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
849
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
BIN
public/blog/covers/smallexample.png
Normal file
BIN
public/blog/covers/smallexample.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
public/blog/covers/writeblog.png
Normal file
BIN
public/blog/covers/writeblog.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Reference in New Issue
Block a user