| 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 |