返回 marp
blog.tsx
根目录 / website / pages / blog.tsx
1 import fs from 'fs'
2 import { basename } from 'path'
3 import { ArrowRightIcon } from '@primer/octicons-react'
4 import { InferGetStaticPropsType } from 'next'
5 import Head from 'next/head'
6 import Link from 'next/link'
7 import { Layout } from 'components/Layout'
8 import { Title } from 'components/Title'
9 import { Typography } from 'components/Typography'
10 import { formatDate, formatDateShort } from 'utils/date'
11 import { parse, parseMatter, renderToReact } from 'utils/markdown'
12 import { absoluteUrl } from 'utils/url'
13
14 const toJSON = (obj: any) => JSON.parse(JSON.stringify(obj))
15
16 const escapeEntities = (raw: string) =>
17 raw.replace(/[&<>]/g, (matched) => `&#${matched.charCodeAt(0)};`)
18
19 export const getStaticProps = async () => {
20 const ctx = require.context('blog', false, /^.[\\/][^\\/]*\.md$/)
21 const mdMetas = await Promise.all(
22 ctx.keys().map((id) => {
23 const md = ctx(id)
24 const { data, excerpt } = parseMatter(md)
25
26 return (async () => ({
27 data,
28 excerpt: excerpt ? await parse(excerpt) : undefined,
29 slug: basename(id, '.md'),
30 }))()
31 })
32 )
33
34 const articles = mdMetas.filter(({ data }) => data.date)
35 articles.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
36
37 // Generate RSS
38 const rss = `
39 <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
40 <channel>
41 <title>Blog | Marp</title>
42 <link>${escapeEntities(absoluteUrl('/blog').toString())}</link>
43 <description>Marp, Markdown Presentation Ecosystem, provides the great experience to create beautiful slide deck. You only have to focus writing your story in Markdown document.</description>
44 <language>en</language>
45 <lastBuildDate>${articles[0].data.date.toUTCString()}</lastBuildDate>
46 <atom:link href="${absoluteUrl(
47 '/blog/feed.xml'
48 )}" rel="self" type="application/rss+xml"/>
49 ${articles
50 .map((article) =>
51 `
52 <item>
53 <guid>${escapeEntities(
54 absoluteUrl(`/blog/${article.slug}`).toString()
55 )}</guid>
56 <title>${escapeEntities(article.data.title)}</title>
57 <link>${escapeEntities(
58 absoluteUrl(`/blog/${article.slug}`).toString()
59 )}</link>
60 <description>${escapeEntities(article.data.description)}</description>
61 <pubDate>${article.data.date.toUTCString()}</pubDate>
62 </item>
63 `.trim()
64 )
65 .join('')}
66 </channel>
67 </rss>
68 `.trim()
69
70 await fs.promises.writeFile('./public/blog/feed.xml', rss)
71
72 return {
73 props: {
74 articles: articles.map((article) =>
75 toJSON({
76 data: article.data,
77 excerpt: article.excerpt?.mdast,
78 slug: article.slug,
79 })
80 ),
81 },
82 }
83 }
84
85 const Blog = ({ articles }: InferGetStaticPropsType<typeof getStaticProps>) => (
86 <Layout activeItem="blog" title={['Blog']}>
87 <Title>
88 <Link href="/blog">Blog</Link>
89 </Title>
90 <Head>
91 <link
92 rel="alternate"
93 type="application/rss+xml"
94 title="Blog | Marp"
95 href={absoluteUrl(`/blog/feed.xml`).toString()}
96 />
97 </Head>
98 <div className="container mx-auto max-w-screen-lg px-3 py-1">
99 {articles.map((article) => {
100 let summary: JSX.Element | string | undefined
101
102 if (article.excerpt) {
103 summary = (
104 <Typography>
105 {renderToReact(article.excerpt, { anchorLink: false })}
106 </Typography>
107 )
108 } else if (article.data.description) {
109 summary = article.data.description
110 }
111
112 const date = new Date(article.data.date)
113
114 return (
115 <div key={article.slug} className="article-container">
116 <Link href={`/blog/${article.slug}`} legacyBehavior>
117 <a className="article-container-link">
118 <h1 className="sr-only">{article.data.title}</h1>
119 </a>
120 </Link>
121 <div className="pointer-events-none relative">
122 <h1 className="text-gradient text-3xl font-bold" aria-hidden>
123 {article.data.title}
124 </h1>
125 <p className="mt-2 mb-4 border-b-2 pb-4 text-sm text-gray-600">
126 <time dateTime={formatDateShort(date)}>{formatDate(date)}</time>
127 {article.data.author && ` by ${article.data.author}`}
128 </p>
129 {summary && (
130 <>
131 <div
132 className="article-summary flex flex-col lg:flex-row"
133 inert=""
134 >
135 {article.data.image && (
136 <figure className="mx-auto mb-6 lg:order-1 lg:mb-0 lg:ml-6 lg:w-full lg:max-w-[20rem]">
137 <img
138 src={article.data.image}
139 alt={article.data.title}
140 className="w-full max-w-[20rem] bg-white shadow-md lg:w-[100vw]"
141 />
142 </figure>
143 )}
144 <article className="flex-grow">{summary}</article>
145 </div>
146 </>
147 )}
148 <p className="read-more">
149 <ArrowRightIcon className="read-more-icon" />
150 Read more...
151 </p>
152 </div>
153 </div>
154 )
155 })}
156 <style jsx>{`
157 .article-container {
158 @apply relative my-6 p-6;
159 }
160 .article-container-link {
161 @apply absolute inset-0 rounded-lg transition-all duration-300;
162 }
163
164 @media not all and (hover: none) {
165 .article-container-link:hover {
166 @apply bg-white shadow-lg;
167 }
168 .article-container-link:hover:active {
169 @apply duration-0 bg-white outline-none ring-1 ring-white ring-offset-2;
170 }
171 .article-container-link:hover + * .read-more {
172 @apply text-marp-brand;
173 }
174 }
175
176 .article-container-link:focus {
177 @apply duration-0 bg-white outline-none ring-1 ring-white ring-offset-2;
178 }
179
180 .article-summary :global(a) {
181 color: inherit;
182 }
183
184 .read-more {
185 @apply mt-6 flex items-center justify-end text-right font-bold uppercase transition-colors duration-300;
186 }
187
188 .read-more :global(.read-more-icon) {
189 @apply h-8 w-8;
190 }
191
192 @screen md {
193 .article-container {
194 @apply relative p-8;
195 }
196 }
197 `}</style>
198 </div>
199 </Layout>
200 )
201
202 export default Blog
203
203 lines Plain Text