Skip to content

Commit

Permalink
feat: wip blog with next-mdx-remote
Browse files Browse the repository at this point in the history
  • Loading branch information
ixahmedxi committed Jun 6, 2024
1 parent 0dc06a1 commit 0a5087c
Show file tree
Hide file tree
Showing 16 changed files with 296 additions and 15 deletions.
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ words:
- clsx
- commitlint
- compat
- frontmatter
- hookform
- ianvs
- ixahmedxi
Expand Down
4 changes: 4 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export default tseslint.config(
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: __dirname,
},
},
settings: {
Expand Down Expand Up @@ -109,6 +110,9 @@ export default tseslint.config(
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',

// security
'security/detect-non-literal-fs-filename': 'off',

// we're not building a library here
'jsdoc/require-jsdoc': 'off',
},
Expand Down
11 changes: 7 additions & 4 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import { fileURLToPath } from 'node:url';

import createJiti from 'jiti';
import mdxPlugin from '@next/mdx';

const jiti = createJiti(fileURLToPath(import.meta.url));

jiti('./src/env');

const withMDX = mdxPlugin();

const extensions = ['js', 'jsx', 'ts', 'tsx', 'mdx', 'md'];

/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
pageExtensions: extensions,
transpilePackages: ['next-mdx-remote'],

// We run ESLint and TypeScript separately in the CI pipeline
eslint: {
Expand All @@ -23,6 +21,11 @@ const nextConfig = {
typescript: {
ignoreBuildErrors: true,
},
logging: {
fetches: {
fullUrl: true,
},
},
experimental: {
mdxRs: true,
turbo: {
Expand All @@ -31,4 +34,4 @@ const nextConfig = {
},
};

export default withMDX(nextConfig);
export default nextConfig;
5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,7 @@
"@clerk/nextjs": "^5.1.4",
"@clerk/themes": "^2.1.9",
"@hookform/resolvers": "^3.6.0",
"@mdx-js/loader": "^3.0.1",
"@mdx-js/react": "^3.0.1",
"@neondatabase/serverless": "^0.9.3",
"@next/mdx": "^14.2.3",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-label": "^2.0.2",
Expand All @@ -73,7 +70,6 @@
"@trpc/client": "next",
"@trpc/react-query": "next",
"@trpc/server": "next",
"@types/mdx": "^2.0.13",
"@upstash/redis": "^1.31.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
Expand All @@ -83,6 +79,7 @@
"jiti": "^1.21.3",
"lucide-react": "^0.383.0",
"next": "14.2.3",
"next-mdx-remote": "^5.0.0",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
2 changes: 1 addition & 1 deletion src/app/(site)/(legal)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { PropsWithChildren } from 'react';

export default function LegalLayout({ children }: PropsWithChildren) {
return <main className="mx-auto max-w-prose py-12">{children}</main>;
return <main className="mx-auto max-w-prose py-8 md:py-12">{children}</main>;
}
22 changes: 22 additions & 0 deletions src/app/(site)/(legal)/privacy/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { getLegalDocs } from '@/lib/mdx';
import { notFound } from 'next/navigation';
import { CustomMDX, MDXComponents } from '../../_components/custom-mdx';

export default async function PrivacyPage() {
const docs = await getLegalDocs();
const post = docs.find((d) => d.slug === 'privacy');

if (!post) {
notFound();
}

return (
<>
<MDXComponents.h1>{post.metadata.title}</MDXComponents.h1>
<p className="my-3 text-sm md:my-4 md:text-base">
{post.metadata.effectiveDate}
</p>
<CustomMDX source={post.content} />
</>
);
}
22 changes: 22 additions & 0 deletions src/app/(site)/(legal)/tos/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { getLegalDocs } from '@/lib/mdx';
import { notFound } from 'next/navigation';
import { CustomMDX, MDXComponents } from '../../_components/custom-mdx';

export default async function TermsPage() {
const docs = await getLegalDocs();
const post = docs.find((d) => d.slug === 'tos');

if (!post) {
notFound();
}

return (
<>
<MDXComponents.h1>{post.metadata.title}</MDXComponents.h1>
<p className="my-3 text-sm md:my-4 md:text-base">
Effective date: {post.metadata.effectiveDate}
</p>
<CustomMDX source={post.content} />
</>
);
}
99 changes: 99 additions & 0 deletions src/app/(site)/_components/custom-mdx.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { cn, slugify } from '@/lib/utils';
import { buttonVariants } from '@/primitives/button';
import type { MDXRemoteProps } from 'next-mdx-remote/rsc';
import { MDXRemote } from 'next-mdx-remote/rsc';
import type { PropsWithChildren } from 'react';
import { createElement } from 'react';

function createHeading(level: 1 | 2 | 3 | 4 | 5 | 6, className: string) {
const Element = ({ children }: PropsWithChildren) => {
const slug = typeof children === 'string' ? slugify(children) : '';

return createElement(
`h${String(level)}`,
{ id: slug },
createElement(
'a',
{
href: `#${slug}`,
key: `link-${slug}`,
className: cn('font-medium', className),
},
children,
),
);
};

Element.displayName = `h${String(level)}`;

return Element;
}

export const MDXComponents = {
h1: createHeading(1, 'text-2xl md:text-3xl'),
h2: createHeading(
2,
'text-xl md:text-2xl mb-3 md:mb-4 mt-3 md:mt-4 inline-block',
),
ul: ({ children, className, ...props }) => (
<ul
{...props}
className={cn(
className,
'mb-3 list-disc pl-6 text-sm md:mb-4 md:text-base',
)}
>
{children}
</ul>
),
li: ({ children, className, ...props }) => (
<li
{...props}
className={cn(
className,
'mb-1.5 text-sm leading-relaxed text-foreground-muted md:mb-2 md:text-base',
)}
>
{children}
</li>
),
strong: ({ children, className, ...props }) => (
<strong {...props} className={cn(className, 'font-medium text-foreground')}>
{children}
</strong>
),
a: ({ children, className, ...props }) => (
<a
{...props}
className={cn(
buttonVariants({ variant: 'link' }),
className,
'p-0 pb-0.5 font-bold before:w-full',
)}
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
),
p: ({ children, className, ...props }) => (
<p
{...props}
className={cn(
className,
'mb-3 text-sm leading-relaxed text-foreground-muted md:mb-4 md:text-base',
)}
>
{children}
</p>
),
} satisfies MDXRemoteProps['components'];

export function CustomMDX(props: MDXRemoteProps) {
return (
<MDXRemote
{...props}
components={{ ...MDXComponents, ...(props.components ?? {}) }}
/>
);
}
28 changes: 28 additions & 0 deletions src/app/(site)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { getBlogPosts } from '@/lib/mdx';
import { notFound } from 'next/navigation';
import { CustomMDX, MDXComponents } from '../../_components/custom-mdx';

export const dynamic = 'force-static';

interface Props {
params: {
slug: string;
};
}

export default async function Home({ params }: Props) {
const posts = await getBlogPosts();
const post = posts.find((p) => p.slug === params.slug);

if (!post) {
notFound();
}

return (
<main className="mx-auto max-w-prose py-12">
<MDXComponents.h1>{post.metadata.title}</MDXComponents.h1>
<p>{post.metadata.summary}</p>
<CustomMDX source={post.content} />
</main>
);
}
7 changes: 7 additions & 0 deletions src/app/(site)/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function BlogPage() {
return (
<div>
<h1>Blog Page</h1>
</div>
);
}
11 changes: 11 additions & 0 deletions src/content/blog/hello-world.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: Hello World
publishedAt: Jun 7th, 2024
summary: A simple blog post to say hello to the world.
---

This is a blog post content.

```ts
console.log('Hello World');
```
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Privacy Policy
---
title: Privacy Policy
effectiveDate: Jun 6th, 2024
---

## 1. Introduction

Expand Down Expand Up @@ -36,5 +39,3 @@ We may update this Privacy Policy from time to time. We will notify you of any c
## 8. Contact Us

If you have any questions about this Privacy Policy, please contact us at [[email protected]](mailto:[email protected]).

Effective Date: Jun 6th, 2024
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Terms of Service
---
title: Terms of Service
effectiveDate: Jun 6th, 2024
---

## 1. Introduction

Expand Down Expand Up @@ -31,5 +34,3 @@ We reserve the right to modify these Terms at any time. We will notify you of an
## 8. Contact Us

If you have any questions about these Terms, please contact us at [[email protected]](mailto:[email protected]).

Effective Date: Jun 6th, 2024
74 changes: 74 additions & 0 deletions src/lib/mdx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import fs from 'node:fs/promises';
import path from 'path';

function parseFrontmatter<T extends Record<string, unknown>>(
fileContent: string,
) {
const frontmatterRegex = /---\n([\s\S]*?)\n---/;
const match = frontmatterRegex.exec(fileContent);
if (!match) {
throw new Error('No frontmatter found');
}
const frontMatterBlock = match[1];
const content = fileContent.replace(frontmatterRegex, '').trim();
const frontMatterLines = frontMatterBlock?.trim().split('\n');
const metadata: Partial<T> = {};

frontMatterLines?.forEach((line) => {
const [key, ...valueArr] = line.split(': ');
let value = valueArr.join(': ').trim();
value = value.replace(/^['"](.*)['"]$/, '$1');
if (!key) {
throw new Error('Invalid frontmatter');
}
(metadata as Record<string, unknown>)[key.trim()] = value;
});

return { metadata: metadata as T, content };
}

async function getMDXData<T extends Record<string, unknown>>(dir: string) {
const files = await fs.readdir(dir);
const mdxFiles = files.filter(
(file) => path.extname(file) === '.mdx' || path.extname(file) === '.md',
);

const mapped = await Promise.all(
mdxFiles.map(async (file) => {
const rawContent = await fs.readFile(path.join(dir, file), 'utf-8');
const { metadata, content } = parseFrontmatter<T>(rawContent);
const slug = path.basename(file, path.extname(file));
return {
metadata,
slug,
content,
};
}),
);

return mapped;
}

interface BlogMetadata {
[key: string]: unknown;
title: string;
publishedAt: string;
summary: string;
image?: string;
}

interface LegalMetadata {
[key: string]: unknown;
title: string;
effectiveDate: string;
}

export function getBlogPosts() {
return getMDXData<BlogMetadata>(path.join(process.cwd(), 'src/content/blog'));
}

export function getLegalDocs() {
return getMDXData<LegalMetadata>(
path.join(process.cwd(), 'src/content/legal'),
);
}
Loading

0 comments on commit 0a5087c

Please sign in to comment.