-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathblog.tsx
More file actions
136 lines (120 loc) · 4.46 KB
/
blog.tsx
File metadata and controls
136 lines (120 loc) · 4.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import { ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3'
import remarkWikiLink from '@portaljs/remark-wiki-link'
import { compileMDX } from 'next-mdx-remote/rsc'
import { cache } from 'react'
import Syntax from 'react-syntax-highlighter/dist/esm/prism'
import theme from 'react-syntax-highlighter/dist/esm/styles/prism/dracula'
import remarkGfm from 'remark-gfm'
import { env } from '/env'
import { getS3Url } from '/utils/getS3Url'
const s3 = new S3Client({
region: env.NEXT_PUBLIC_AWS_REGION,
credentials: { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY },
})
/** Take a path like `Portfolio/Blog/test-one.md` and strip the path and file extension to return `test-one` */
const getPageSlug = (filename: string) => {
const nameParts = filename.split('/')
return nameParts[nameParts.length - 1].slice(0, -3)
}
const wikiLinkResolver = (filename: string, objects: string[]) => {
const match = objects.find((path) => path.endsWith(filename))
if (!match) return '/404'
if (match.endsWith('.md')) return `/blog/${getPageSlug(match)}`
return getS3Url(match)
}
const parseBannerUrl = (banner: string | undefined, objects: string[]) => {
if (!banner) return
if (banner.startsWith('[[') && banner.endsWith(']]')) {
return wikiLinkResolver(banner.slice(2, -2), objects)
}
return banner
}
const parseMDX = async (filename: string, source: string, objects: string[]) => {
const { content, frontmatter } = await compileMDX<{
title?: string
tags?: string[]
published?: string
lastEdited?: string
banner?: string
}>({
source,
options: {
parseFrontmatter: true,
mdxOptions: {
remarkPlugins: [
remarkGfm,
[remarkWikiLink, { wikiLinkResolver: (name: string) => [wikiLinkResolver(name, objects)] }],
],
},
},
components: {
pre: (el) => {
if (el.children.type !== 'code') return el
return (
<Syntax
style={theme}
language={el.children.props.className?.replace('language-', '').toLocaleLowerCase()}
customStyle={{
background: 'none',
textShadow: 'none',
margin: 'initial',
padding: 0,
fontFamily: 'initial',
borderRadius: 0,
}}
codeTagProps={{ style: {} }}
showLineNumbers
lineNumberStyle={{
minWidth: `${el.children.props.children.trim().split('\n').length.toString().length}ch`,
position: 'sticky',
left: 0,
background: 'inherit',
paddingInlineStart: '1.5em',
boxSizing: 'content-box',
}}
>
{el.children.props.children.trim()}
</Syntax>
)
},
// TODO: Put images inside figure elements and parse captions
},
})
if (!frontmatter.title || !frontmatter.published) throw new Error('Post missing title or published date')
return {
slug: getPageSlug(filename),
title: frontmatter.title,
tags: frontmatter.tags ?? [],
published: new Date(frontmatter.published),
lastEdited: frontmatter.lastEdited ? new Date(frontmatter.lastEdited) : null,
banner: parseBannerUrl(frontmatter.banner, objects),
description: source.split('---\n')[2].slice(0, 300).trim(),
content,
}
}
export const fetchBlogPosts = cache(async () => {
// Fetch list of blog objects from S3
const res = await s3.send(new ListObjectsV2Command({ Bucket: env.NEXT_PUBLIC_AWS_BUCKET, Prefix: 'Portfolio/Blog' }))
const objects = res.Contents?.flatMap((item) => (item.Key ? [item.Key] : []))
if (!objects) return []
const markdownFiles = objects.filter((filename) => filename.endsWith('.md'))
const posts = await Promise.allSettled(
markdownFiles.map(async (filename) =>
fetch(getS3Url(filename))
.then((res) => res.text())
.then((source) => parseMDX(filename, source, objects)),
),
).then((results) =>
results.flatMap((res) => {
if (res.status === 'fulfilled') return [res.value]
// console.warn(res.reason)
return []
}),
)
posts.sort((a, b) => b.published.valueOf() - a.published.valueOf())
return posts.map((post, i) => ({ ...post, number: (posts.length - 1 - i).toString().padStart(2, '0') }))
})
export const getPost = cache(async (slug: string) => {
const posts = await fetchBlogPosts()
return posts.find((post) => post.slug.toLocaleLowerCase() === slug.toLocaleLowerCase())
})