Skip to content

Commit

Permalink
sample(next): next.js mdx with static imports
Browse files Browse the repository at this point in the history
  • Loading branch information
sdorra committed Dec 8, 2024
1 parent 338fea6 commit 12bd2be
Show file tree
Hide file tree
Showing 17 changed files with 883 additions and 3 deletions.
361 changes: 358 additions & 3 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions samples/next-mdx-static-import/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
36 changes: 36 additions & 0 deletions samples/next-mdx-static-import/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
104 changes: 104 additions & 0 deletions samples/next-mdx-static-import/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
title: Next.js MDX Static Import
description: Let Next.js compile MDX, and simply import the files
tags:
- next.js
- react
- rsc
- mdx
adapter: next
---

<Callout type="warn">
The method used in this sample does not currently work with turborepo.
</Callout>

## Installation

- Follow the instructions to [configure MDX routes in your Next.js project](https://nextjs.org/docs/app/building-your-application/configuring/mdx)
- Install Content Collections: [Next.js Quick Start](https://www.content-collections.dev/docs/quickstart/next)

## Configuration

Configure Content Collections that it discover the MDX files, but we exclude the content from the generated files e.g.:

```ts
import { defineCollection, defineConfig } from "@content-collections/core";

const posts = defineCollection({
name: "posts",
directory: "./content/posts",
include: "*.mdx",
schema: (z) => ({
title: z.string(),
}),
transform: ({ content: _, ...post }) => {
return post;
},
});

export default defineConfig({
collections: [posts],
});
```

We have to tell Next.js to remove the frontmatter from the MDX files during the compilation because the frontmatter is handled by Content Collections. For this, we have to configure the remark plugins remarkFrontmatter and remarkMdxFrontmatter in the Next.js configuration, e.g.:

```js
import { withContentCollections } from "@content-collections/next";
import createMDX from "@next/mdx";
import remarkFrontmatter from 'remark-frontmatter'
import remarkMdxFrontmatter from 'remark-mdx-frontmatter'

/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
};

const withMDX = createMDX({
options: {
remarkPlugins: [remarkFrontmatter,remarkMdxFrontmatter],
rehypePlugins: [],
},
});

export default withContentCollections(withMDX(nextConfig));
```

## Usage

Now we can use the generated `allPosts` collections as usual:

```tsx
<ul>
{allPosts.map((post) => (
<li key={post.slug}>
<Link href={`/posts/${post.slug}`}>
{post.title}
</Link>
</li>
))}
</ul>
```

But when we want to render the content of a post, we can use a dynamic import to let Next.js compile the MDX file for us:

```tsx
export default async function Post({ params: { slug } }: Props) {
const post = allPosts.find((post) => post.slug === slug);
if (!post) {
return notFound();
}

const { default: Content } = await import(`../../content/posts/${post.slug}.mdx`);

return (
<article className="post">
<h2>{post.title}</h2>
<div className="content">
<Content />
</div>
</article>
);
}
```
36 changes: 36 additions & 0 deletions samples/next-mdx-static-import/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Metadata } from "next";
import "@content-collections/sample-theme/sample.css";
import Link from "next/link";

export const metadata: Metadata = {
title: "Content Collections",
description: "Using Content Collections with Next.js",
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header id="top">
<h1>ContentCrafter Inc.</h1>
<p>
From Worldly Wonders to Polished Perfection – Crafting Content That
Captivates and Converts.
</p>
<nav>
<Link href="/">Posts</Link>
</nav>
</header>
<main>{children}</main>
<footer>
<a href="#top">↑ Back to top</a>
<p>Copyright © {new Date().getFullYear()} ContentCrafter Inc.</p>
</footer>
</body>
</html>
);
}
21 changes: 21 additions & 0 deletions samples/next-mdx-static-import/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { allPosts } from "content-collections";
import Link from "next/link";

export default function Page() {
return (
<main>
<h2>Posts</h2>
<div className="posts">
{allPosts.map((post) => (
<Link key={post.slug} href={`/posts/${post.slug}`}>
<header>
<h3>{post.title}</h3>
<time>{post.date}</time>
</header>
<p>{post.summary}</p>
</Link>
))}
</div>
</main>
);
}
32 changes: 32 additions & 0 deletions samples/next-mdx-static-import/app/posts/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { allPosts } from "content-collections";
import { notFound } from "next/navigation";

type Props = {
params: {
slug: string;
};
};

export default async function Post({ params: { slug } }: Props) {
const post = allPosts.find((post) => post.slug === slug);
if (!post) {
return notFound();
}

const MdxContent = post.mdxContent;

return (
<article className="post">
<header>
<h2>{post.title}</h2>
</header>
<div className="content">
<MdxContent />
</div>
<footer>
<p>By {post.author}</p>
<time>{post.date}</time>
</footer>
</article>
);
}
16 changes: 16 additions & 0 deletions samples/next-mdx-static-import/components/Internship.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
type Props = {
children: React.ReactNode;
};

export function Internship({ children }: Props) {
return (
<div style={{ border: "2px dashed red", padding: "1rem", marginTop: "2rem", borderRadius: "1rem" }}>
<h2 style={{marginTop: "0"}}>Internship at ContentCrafter Inc.</h2>
<p>
Are you creative and passionate about writing? ContentCrafter Inc. is
looking for motivated interns to support our team!
</p>
<p>Interested? Send your application to [email protected].</p>
</div>
);
}
31 changes: 31 additions & 0 deletions samples/next-mdx-static-import/content-collections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
createDefaultImport,
defineCollection,
defineConfig,
} from "@content-collections/core";
import { MDXContent } from "mdx/types";

const posts = defineCollection({
name: "posts",
directory: "./content/posts",
include: "*.mdx",
schema: (z) => ({
title: z.string(),
summary: z.string(),
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
author: z.string(),
}),
transform: ({ content: _, _meta, ...post }) => {
const mdxContent = createDefaultImport<MDXContent>(`@/content/posts/${_meta.filePath}`);
const slug = _meta.path;
return {
...post,
mdxContent,
slug,
};
},
});

export default defineConfig({
collections: [posts],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: "How a Sloth Revolutionized Our Deadline Management"
summary: "A chance encounter with a three-toed sloth named Fernando in Costa Rica leads ContentCrafter Inc. to revolutionize their approach to content creation. Through observing Fernando's methodical movements, the team learns valuable lessons about purposeful work, leading to improved quality and efficiency across all departments, proving that sometimes slowing down is the key to better results."
date: 2024-03-15
author: "Alexandra Winters"
---

import { Internship } from "@/components/Internship";

<Internship />

Our content creation journey at ContentCrafter Inc. has always been filled with unexpected twists and turns, but nothing quite prepared us for the day our entire workflow philosophy would be transformed by an unlikely productivity guru – a three-toed sloth named Fernando.

## When Slow and Steady Wins the Race

It all started when our collector, Sarah, ventured deep into the Costa Rican rainforest in search of inspiration for a client's eco-tourism campaign. Armed with her trusty camera and an overly ambitious shot list, she was determined to capture the perfect moments of wildlife in action. That's when she encountered Fernando, peacefully hanging from a branch, moving with such deliberate grace that it made our usual rushing around seem almost comical.

"I was frantically checking my watch, worried about missing my next appointment," Sarah recalls, chuckling. "Meanwhile, Fernando just gave me this knowing look, took three minutes to blink, and somehow made me question everything I knew about time management."

## The Great Validation Revelation

When Sarah's footage reached our validation team, something extraordinary happened. Our usually fast-paced validators, known for their lightning-quick fact-checking abilities, found themselves oddly mesmerized by Fernando's methodical movements.

"There was something almost hypnotic about watching Fernando navigate his branch," explains Marcus, our lead validator. "He took fifteen minutes to reach for a leaf, but every movement was purposeful, efficient, and – most importantly – error-free. It was like watching a masterclass in quality control."

The team started implementing what they jokingly called "The Fernando Method" – a deliberate, mindful approach to content validation that, surprisingly, resulted in fewer revision requests and higher quality outputs.

## The Transformation Station

Our transformers initially scoffed at the idea of slowing down their creative process. After all, creativity strikes like lightning, right? Wrong, as Fernando would have very slowly told us.

"We used to pride ourselves on our rapid-fire brainstorming sessions," admits Jessica, our senior transformer. "But after studying Fernando's footage, we realized that some of our best ideas needed time to marinate. We started taking 'sloth breaks' – five-minute periods of slow, deliberate thinking between ideation sessions."

The results were astonishing. Our content became richer, more nuanced, and ironically, we started meeting deadlines more consistently. Turns out, panic-induced creativity wasn't all it was cracked up to be.

## The Sloth Revolution

What began as a simple wildlife filming assignment evolved into a company-wide philosophy. We installed a large photo of Fernando in our main conference room, complete with a caption that reads "What Would Fernando Do?" It serves as a reminder that sometimes the path to efficiency isn't about moving faster, but about moving more purposefully.

Our clients noticed the difference too. One even commented, "There's something different about your content lately – it feels more... thoughtful." Little did they know it was all thanks to a sloth who never rushed a single moment of his life.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
title: My GPS Was Possessed By A Mischievous Mountain Goat
summary: A hilarious tale of ContentCrafter's collection team's misadventures in the Swiss Alps, where a tech-savvy mountain goat repeatedly altered their GPS coordinates, leading to an unexpected discovery of an ancient cheese-making tradition. The story showcases how sometimes the best content comes from embracing the unexpected, even if it means following a goat's digital breadcrumbs.
date: 2023-09-15
author: Alexandra Winters
---

The Swiss Alps are magnificent, they said. The GPS will guide you perfectly, they said. Nobody mentioned anything about tech-savvy mountain goats with a peculiar sense of humor. But here at ContentCrafter Inc., we've learned that the best stories often come from the most unexpected sources – even if that source happens to be a four-legged prankster with a mysterious ability to hack navigation systems.

## When Technology Meets Nature's Comedians

It all started on a crisp morning in the Bernese Oberland. Our collection team, armed with the latest GPS technology and enough equipment to document a small expedition, set out to gather content about traditional Swiss cheese-making techniques. Little did we know that our carefully planned route would soon be hijacked by what we now affectionately call "The Algorithm Goat."

Every time we checked our GPS coordinates, they somehow pointed us to seemingly random locations. At first, we blamed it on satellite interference or equipment malfunction. But then we noticed a pattern – and a suspicious-looking goat that appeared at every wrong turn, sporting what could only be described as a smirk.

## The Great Validation Debate

Back at headquarters, our validation team had their hands full with this one. "How do we verify that a goat manipulated our GPS?" demanded Sarah from Quality Control, peering through her reading glasses at hours of footage. The evidence was circumstantial but compelling: every time the goat appeared in frame, our coordinates shifted to point toward ancient cheese-making locations we hadn't even known existed.

"Maybe it's trying to help?" suggested Mike, our newest validator, earning himself eye-rolls from the veteran team members. But as they dug deeper into the data, they couldn't deny that our furry friend had led us to some extraordinary discoveries.

## Transforming Chaos into Content Gold

Our transformation team couldn't believe their luck. What started as a straightforward piece about traditional cheese-making evolved into something far more engaging. "We've got a viral story on our hands," declared Rachel, our lead content transformer, her eyes gleaming with excitement. "A mountain goat with a PhD in GPS manipulation? That's pure content gold!"

The team worked their magic, weaving together the technical mishaps, the goat's mysterious appearances, and the incredible traditional cheese-making locations we discovered along the way. They created an interactive map showing our intended route versus the "goat-guided tour," complete with cheese-themed markers and tiny digital goat footprints.

## The Unexpected Success

The finished piece became one of our most successful content campaigns ever. It turns out that people love stories about traditional craftsmanship, especially when they're delivered through the lens of a possibly tech-savvy mountain goat. Our client, a major Swiss tourism board, reported a significant increase in interest in their artisanal cheese tours.

As for our four-legged friend? Legend has it that on quiet mountain mornings, hikers still report mysterious GPS glitches, always accompanied by the distant sound of bleating and what sounds suspiciously like digital laughter.

Here at ContentCrafter Inc., we've learned to embrace the unexpected. Sometimes the best stories find us, even if they have to hack our GPS to do it. And if you ever find yourself in the Swiss Alps with a malfunctioning navigation system, look around – you might just be in the presence of our favorite content contributor.

Just remember to pack extra batteries. And maybe some cheese. You never know when you might need to bribe a tech-savvy goat.
7 changes: 7 additions & 0 deletions samples/next-mdx-static-import/mdx-components.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { MDXComponents } from "mdx/types";

export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...components,
};
}
16 changes: 16 additions & 0 deletions samples/next-mdx-static-import/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { withContentCollections } from "@content-collections/next";
import createMDX from "@next/mdx";
import remarkFrontmatter from "remark-frontmatter";
import remarkMdxFrontmatter from "remark-mdx-frontmatter";

/** @type {import('next').NextConfig} */
const nextConfig = {};

const withMDX = createMDX({
options: {
remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter],
rehypePlugins: [],
},
});

export default withContentCollections(withMDX(nextConfig));
Loading

0 comments on commit 12bd2be

Please sign in to comment.