Skip to content

Commit 0a14327

Browse files
committed
remix blog
1 parent 8842af4 commit 0a14327

File tree

8 files changed

+1641
-921
lines changed

8 files changed

+1641
-921
lines changed

package-lock.json

Lines changed: 1375 additions & 921 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
"@reduxjs/toolkit": "^1.9.3",
1313
"classnames": "^2.3.1",
1414
"dayjs": "^1.10.7",
15+
"esbuild": "^0.17.18",
1516
"eslint": "^8.24.0",
1617
"firebase": "^9.6.1",
1718
"formik": "^2.2.9",
1819
"json-server": "^0.17.1",
1920
"lodash.debounce": "^4.0.8",
2021
"markdown-it": "^12.2.0",
22+
"mdx-bundler": "^9.2.1",
2123
"mobx": "^6.3.13",
2224
"mobx-react": "^7.2.1",
2325
"mobx-react-lite": "^3.2.3",

remix/app/blog-posts/welcome.mdx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
title: Welcome
3+
date: 2023-05-13T00:00:00.000-0800
4+
---
5+
6+
import { Icon } from '../components/Icon'
7+
8+
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Temporibus a eius neque molestiae natus, nostrum pariatur debitis sint, aperiam voluptate amet eveniet, fugit quaerat non sed. Harum dolores omnis ea.
9+
10+
<hr />
11+
12+
[External Anchor](https://google.com)
13+
14+
[SPA Anchor](/products)
15+
16+
<button className="button">
17+
<Icon name="cart" /> Components In Markdown
18+
</button>

remix/app/components/MDXContent.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { getMDXComponent } from 'mdx-bundler/client'
2+
import { useMemo } from 'react'
3+
import { Link } from 'react-router-dom'
4+
5+
export function MDXContent({ code }: { code: string }) {
6+
const Component = useMemo(() => getMDXComponent(code), [code])
7+
return (
8+
<Component
9+
components={{
10+
a: Anchor,
11+
}}
12+
/>
13+
)
14+
}
15+
16+
/****************************************
17+
Substitutions
18+
*****************************************/
19+
20+
type AnchorProps = {
21+
children: React.ReactNode
22+
href: string
23+
} & React.HTMLAttributes<HTMLAnchorElement>
24+
25+
function Anchor({ href, children, ...props }: AnchorProps) {
26+
if (href.toLowerCase().startsWith('http')) {
27+
return (
28+
<a href={href} {...props}>
29+
{children}
30+
</a>
31+
)
32+
} else {
33+
return (
34+
<Link to={href} {...props}>
35+
{children}
36+
</Link>
37+
)
38+
}
39+
}

remix/app/routes/blog.$slug.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { json } from '@remix-run/node'
2+
import { useLoaderData } from '@remix-run/react'
3+
import { MDXContent } from '~/components/MDXContent'
4+
import { getPost } from '~/utils/blog.server'
5+
import type { LoaderArgs } from '@remix-run/node'
6+
import { Heading } from '~/components/Heading'
7+
8+
export const loader = async ({ params }: LoaderArgs) => {
9+
const slug = params.slug
10+
if (!slug) throw new Response('Not found', { status: 404 })
11+
12+
try {
13+
const { frontmatter, code } = await getPost(slug)
14+
return json({ frontmatter, code })
15+
} catch (err) {
16+
throw new Response('Not found', { status: 404 })
17+
}
18+
}
19+
20+
export default function () {
21+
const { frontmatter, code } = useLoaderData<typeof loader>()
22+
23+
return (
24+
<article className="space-y-3">
25+
<Heading>{frontmatter.title}</Heading>
26+
<MDXContent code={code} />
27+
</article>
28+
)
29+
}

remix/app/routes/blog._index.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { json } from '@remix-run/node'
2+
import { Link, useLoaderData } from '@remix-run/react'
3+
import { getAllPosts } from '~/utils/blog.server'
4+
import { Heading } from '~/components/Heading'
5+
6+
// export const loader = async ({ params }: LoaderArgs) => {
7+
// const slug = params.slug
8+
// if (!slug) throw new Response('Not found', { status: 404 })
9+
10+
// try {
11+
// const { frontmatter, code } = await getPost(slug)
12+
// return json({ frontmatter, code })
13+
// } catch (err) {
14+
// throw new Response('Not found', { status: 404 })
15+
// }
16+
// }
17+
18+
export async function loader() {
19+
try {
20+
const posts = await getAllPosts()
21+
return json({ posts })
22+
} catch (error) {
23+
console.error(error)
24+
throw new Response('Not found', { status: 404 })
25+
}
26+
}
27+
28+
export default function () {
29+
const { posts } = useLoaderData<typeof loader>()
30+
31+
return (
32+
<article className="space-y-3">
33+
<Heading>Blog</Heading>
34+
35+
{posts.map((post) => {
36+
const path = `/blog/${post.slug}`
37+
return (
38+
<div key={path} className="bg-white rounded p-3 space-y-2">
39+
<Heading as="h2" size={3}>
40+
{post.title}
41+
</Heading>
42+
<Link to={path}>{path}</Link>
43+
</div>
44+
)
45+
})}
46+
</article>
47+
)
48+
}

remix/app/styles/app.css

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,34 @@ body {
5353
}
5454
}
5555

56+
/****************************************
57+
Lists
58+
*****************************************/
59+
60+
@layer base {
61+
/* Go back to User Agent Defaults (not the tailwind reset) */
62+
ul {
63+
display: block;
64+
list-style-type: disc;
65+
margin-block-start: 1em;
66+
margin-block-end: 1em;
67+
margin-inline-start: 0px;
68+
margin-inline-end: 0px;
69+
padding-inline-start: 40px;
70+
}
71+
72+
/* Go back to User Agent Defaults (not the tailwind reset) */
73+
ol {
74+
display: block;
75+
list-style-type: disc;
76+
margin-block-start: 1em;
77+
margin-block-end: 1em;
78+
margin-inline-start: 0px;
79+
margin-inline-end: 0px;
80+
padding-inline-start: 40px;
81+
}
82+
}
83+
5684
/****************************************
5785
Buttons
5886
*****************************************/

remix/app/utils/blog.server.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { readdir } from 'fs/promises'
2+
import path from 'path'
3+
import { bundleMDX } from 'mdx-bundler'
4+
5+
const blogPostsPath = path.join(`${__dirname}/../app/blog-posts`)
6+
7+
export type FrontmatterType = {
8+
title: string
9+
date: Date
10+
draft?: true // either `true` not not supplied
11+
}
12+
13+
export type Post = FrontmatterType & {
14+
slug: string
15+
markdown: string
16+
}
17+
18+
function fixFrontmatter(f: any) {
19+
return {
20+
...f,
21+
date: new Date(f.date as string),
22+
} as FrontmatterType
23+
}
24+
25+
export async function getPost(slug: string) {
26+
const parsed = await parseMDX(path.join(blogPostsPath, slug + '.mdx'))
27+
const frontmatter = fixFrontmatter(parsed.frontmatter)
28+
const code = parsed.code
29+
return { frontmatter, code }
30+
}
31+
32+
export async function getAllPosts(): Promise<Post[]> {
33+
const postsPath = await readdir(blogPostsPath, {
34+
withFileTypes: true,
35+
})
36+
37+
let posts = await Promise.all(
38+
postsPath.map(async (pathInfo) => {
39+
// Only get .mdx files
40+
if (!pathInfo.name.includes('.mdx')) return
41+
42+
const parsed = await parseMDX(path.join(blogPostsPath, pathInfo.name))
43+
const frontmatter = fixFrontmatter(parsed.frontmatter)
44+
45+
// Don't show draft posts in production
46+
if (process.env.NODE_ENV === 'production' && frontmatter.draft) return
47+
48+
return {
49+
slug: pathInfo.name.replace(/\.mdx/, ''),
50+
...frontmatter,
51+
}
52+
})
53+
)
54+
55+
posts = posts.filter(Boolean).sort((a, b) => b.date.getTime() - a.date.getTime())
56+
return posts as Post[]
57+
}
58+
59+
/****************************************
60+
Parse MDX
61+
*****************************************/
62+
63+
// https://github.com/kentcdodds/mdx-bundler/blob/main/README.md#nextjs-esbuild-enoent
64+
65+
if (process.platform === 'win32') {
66+
process.env.ESBUILD_BINARY_PATH = path.join(
67+
process.cwd(),
68+
'node_modules',
69+
'esbuild',
70+
'esbuild.exe'
71+
)
72+
} else {
73+
process.env.ESBUILD_BINARY_PATH = path.join(
74+
process.cwd(),
75+
'node_modules',
76+
'esbuild',
77+
'bin',
78+
'esbuild'
79+
)
80+
}
81+
82+
export async function parseMDX(file: string) {
83+
const { frontmatter, code } = await bundleMDX({
84+
file,
85+
// mdxOptions(options, frontmatter) {
86+
// // this is the recommended way to add custom remark/rehype plugins:
87+
// // The syntax might look weird, but it protects you in case we add/remove
88+
// // plugins in the future.
89+
// options.remarkPlugins = [...(options.remarkPlugins ?? []), myRemarkPlugin]
90+
// options.rehypePlugins = [...(options.rehypePlugins ?? []), myRehypePlugin]
91+
92+
// return options
93+
// },
94+
// esbuildOptions(options, frontmatter) {
95+
// options.minify = false
96+
// options.target = ['es2020', 'chrome58', 'firefox57', 'safari11', 'edge16', 'node12']
97+
98+
// return options
99+
// },
100+
})
101+
return { frontmatter, code }
102+
}

0 commit comments

Comments
 (0)