Skip to content

Commit 5955d77

Browse files
committed
Add caching, rss generation, and update screenshot
1 parent ac8b77e commit 5955d77

File tree

10 files changed

+205
-42
lines changed

10 files changed

+205
-42
lines changed

assets/table-view.png

380 KB
Loading

next.config.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
1+
const fs = require('fs')
2+
const path = require('path')
13
const { NODE_ENV, NOTION_TOKEN, BLOG_INDEX_ID } = process.env
24

5+
try {
6+
fs.unlinkSync(path.resolve('.blog_index_data'))
7+
} catch (_) {
8+
/* non fatal */
9+
}
10+
try {
11+
fs.unlinkSync(path.resolve('.blog_index_data_previews'))
12+
} catch (_) {
13+
/* non fatal */
14+
}
15+
316
const warnOrError =
417
NODE_ENV !== 'production'
518
? console.warn
@@ -26,7 +39,22 @@ if (!BLOG_INDEX_ID) {
2639
}
2740

2841
module.exports = {
42+
target: 'experimental-serverless-trace',
43+
2944
experimental: {
3045
css: true,
3146
},
47+
48+
webpack(cfg, { dev, isServer }) {
49+
// only compile build-rss in production server build
50+
if (dev || !isServer) return cfg
51+
52+
const originalEntry = cfg.entry
53+
cfg.entry = async () => {
54+
const entries = { ...(await originalEntry()) }
55+
entries['./scripts/build-rss.js'] = './src/lib/build-rss.ts'
56+
return entries
57+
}
58+
return cfg
59+
},
3260
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"scripts": {
55
"dev": "next dev",
66
"start": "next start",
7-
"build": "next build",
7+
"build": "next build && node .next/serverless/scripts/build-rss.js",
88
"format": "prettier --write \"**/*.{js,jsx,json,ts,tsx,md,mdx,css,html,yml,yaml,scss,sass}\" --ignore-path .gitignore"
99
},
1010
"husky": {

src/lib/blog-helpers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ export const getDateStr = date => {
99
year: 'numeric',
1010
})
1111
}
12+
13+
export const postIsReady = (post: any) => {
14+
return process.env.NODE_ENV !== 'production' || post.Published === 'Yes'
15+
}

src/lib/build-rss.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { resolve } from 'path'
2+
import { writeFile } from './fs-helpers'
3+
import { renderToStaticMarkup } from 'react-dom/server'
4+
5+
import { textBlock } from './notion/renderers'
6+
import getBlogIndex from './notion/getBlogIndex'
7+
import getNotionUsers from './notion/getNotionUsers'
8+
import { postIsReady, getBlogLink } from './blog-helpers'
9+
10+
// must use weird syntax to bypass auto replacing of NODE_ENV
11+
process.env['NODE' + '_ENV'] = 'production'
12+
13+
// constants
14+
const NOW = new Date().toJSON()
15+
16+
function mapToAuthor(author) {
17+
return `<author><name>${author.full_name}</name></author>`
18+
}
19+
20+
function decode(string) {
21+
return string
22+
.replace(/&/g, '&amp;')
23+
.replace(/</g, '&lt;')
24+
.replace(/>/g, '&gt;')
25+
.replace(/"/g, '&quot;')
26+
.replace(/'/g, '&apos;')
27+
}
28+
29+
function mapToEntry(post) {
30+
return `
31+
<entry>
32+
<id>${post.link}</id>
33+
<title>${decode(post.title)}</title>
34+
<link href="${post.link}"/>
35+
<updated>${new Date(post.date).toJSON()}</updated>
36+
<content type="xhtml">
37+
<div xmlns="http://www.w3.org/1999/xhtml">
38+
${renderToStaticMarkup(
39+
post.preview
40+
? (post.preview || []).map((block, idx) =>
41+
textBlock(block, false, post.title + idx)
42+
)
43+
: post.content
44+
)}
45+
<p class="more">
46+
<a href="${post.link}">Read more</a>
47+
</p>
48+
</div>
49+
</content>
50+
${(post.authors || []).map(mapToAuthor).join('\n ')}
51+
</entry>`
52+
}
53+
54+
function concat(total, item) {
55+
return total + item
56+
}
57+
58+
function createRSS(blogPosts = []) {
59+
const postsString = blogPosts.map(mapToEntry).reduce(concat, '')
60+
61+
return `<?xml version="1.0" encoding="utf-8"?>
62+
<feed xmlns="http://www.w3.org/2005/Atom">
63+
<title>My Blog</title>
64+
<subtitle>Blog</subtitle>
65+
<link href="/atom" rel="self" type="application/rss+xml"/>
66+
<link href="/" />
67+
<updated>${NOW}</updated>
68+
<id>My Notion Blog</id>${postsString}
69+
</feed>`
70+
}
71+
72+
async function main() {
73+
const postsTable = await getBlogIndex(true)
74+
const neededAuthors = new Set<string>()
75+
76+
const blogPosts = Object.keys(postsTable)
77+
.map(slug => {
78+
const post = postsTable[slug]
79+
if (!postIsReady(post)) return
80+
81+
post.authors = post.Authors || []
82+
83+
for (const author of post.authors) {
84+
neededAuthors.add(author)
85+
}
86+
return post
87+
})
88+
.filter(Boolean)
89+
90+
const { users } = await getNotionUsers([...neededAuthors])
91+
92+
blogPosts.forEach(post => {
93+
post.authors = post.authors.map(id => users[id])
94+
post.link = getBlogLink(post.Slug)
95+
post.title = post.Page
96+
post.date = post.Date
97+
})
98+
99+
const outputPath = './public/atom'
100+
await writeFile(resolve(outputPath), createRSS(blogPosts))
101+
console.log(`Atom feed file generated at \`${outputPath}\``)
102+
}
103+
104+
main().catch(error => console.error(error))

src/lib/fs-helpers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import fs from 'fs'
2+
import { promisify } from 'util'
3+
4+
export const readFile = promisify(fs.readFile)
5+
export const writeFile = promisify(fs.writeFile)

src/lib/notion/getBlogIndex.ts

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,66 @@ import { Sema } from 'async-sema'
22
import rpc, { values } from './rpc'
33
import getTableData from './getTableData'
44
import { getPostPreview } from './getPostPreview'
5-
import { BLOG_INDEX_ID } from './server-constants'
5+
import { readFile, writeFile } from '../fs-helpers'
6+
import { BLOG_INDEX_ID, BLOG_INDEX_CACHE } from './server-constants'
67

78
export default async function getBlogIndex(previews = true) {
8-
const data = await rpc('loadPageChunk', {
9-
pageId: BLOG_INDEX_ID,
10-
limit: 999, // TODO: figure out Notion's way of handling pagination
11-
cursor: { stack: [] },
12-
chunkNumber: 0,
13-
verticalColumns: false,
14-
})
15-
16-
// Parse table with posts
17-
const tableBlock = values(data.recordMap.block).find(
18-
(block: any) => block.value.type === 'collection_view'
19-
)
20-
21-
const postsTable = await getTableData(tableBlock, true)
22-
const postsKeys = Object.keys(postsTable)
23-
const sema = new Sema(3, { capacity: postsKeys.length })
24-
25-
if (previews) {
26-
await Promise.all(
27-
postsKeys
28-
.sort((a, b) => {
29-
const postA = postsTable[a]
30-
const postB = postsTable[b]
31-
const timeA = postA.Date
32-
const timeB = postB.Date
33-
return Math.sign(timeB - timeA)
34-
})
35-
.map(async postKey => {
36-
await sema.acquire()
37-
const post = postsTable[postKey]
38-
post.preview = post.id
39-
? await getPostPreview(postsTable[postKey].id)
40-
: []
41-
sema.release()
42-
})
9+
let postsTable: any = null
10+
const isProd = process.env.NODE_ENV === 'production'
11+
const cacheFile = `${BLOG_INDEX_CACHE}${previews ? '_previews' : ''}`
12+
13+
if (isProd) {
14+
try {
15+
postsTable = JSON.parse(await readFile(cacheFile, 'utf8'))
16+
} catch (_) {
17+
/* not fatal */
18+
}
19+
}
20+
21+
if (!postsTable) {
22+
const data = await rpc('loadPageChunk', {
23+
pageId: BLOG_INDEX_ID,
24+
limit: 999, // TODO: figure out Notion's way of handling pagination
25+
cursor: { stack: [] },
26+
chunkNumber: 0,
27+
verticalColumns: false,
28+
})
29+
30+
// Parse table with posts
31+
const tableBlock = values(data.recordMap.block).find(
32+
(block: any) => block.value.type === 'collection_view'
4333
)
34+
35+
postsTable = await getTableData(tableBlock, true)
36+
// only get 10 most recent post's previews
37+
const postsKeys = Object.keys(postsTable).splice(0, 10)
38+
39+
const sema = new Sema(3, { capacity: postsKeys.length })
40+
41+
if (previews) {
42+
await Promise.all(
43+
postsKeys
44+
.sort((a, b) => {
45+
const postA = postsTable[a]
46+
const postB = postsTable[b]
47+
const timeA = postA.Date
48+
const timeB = postB.Date
49+
return Math.sign(timeB - timeA)
50+
})
51+
.map(async postKey => {
52+
await sema.acquire()
53+
const post = postsTable[postKey]
54+
post.preview = post.id
55+
? await getPostPreview(postsTable[postKey].id)
56+
: []
57+
sema.release()
58+
})
59+
)
60+
}
61+
62+
if (isProd) {
63+
writeFile(cacheFile, JSON.stringify(postsTable), 'utf8').catch(() => {})
64+
}
4465
}
4566

4667
return postsTable

src/lib/notion/queryCollection.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ export default function queryCollection({
1111
loadContentCover = true,
1212
type = 'table',
1313
userLocale = 'en',
14-
// we use America/Phoenix since it doesn't do daylight savings and
15-
// we can't use UTC here
1614
userTimeZone = 'America/Phoenix',
1715
} = loader
1816

src/lib/notion/server-constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import path from 'path'
2+
13
export const NOTION_TOKEN = process.env.NOTION_TOKEN
24
export const BLOG_INDEX_ID = process.env.BLOG_INDEX_ID
35
export const API_ENDPOINT = 'https://www.notion.so/api/v3'
6+
export const BLOG_INDEX_CACHE = path.resolve('.blog_index_data')

src/pages/blog/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Header from '../../components/header'
44
import blogStyles from '../../styles/blog.module.css'
55
import sharedStyles from '../../styles/shared.module.css'
66

7-
import { getBlogLink, getDateStr } from '../../lib/blog-helpers'
7+
import { getBlogLink, getDateStr, postIsReady } from '../../lib/blog-helpers'
88
import { textBlock } from '../../lib/notion/renderers'
99
import getNotionUsers from '../../lib/notion/getNotionUsers'
1010
import getBlogIndex from '../../lib/notion/getBlogIndex'
@@ -17,7 +17,7 @@ export async function unstable_getStaticProps() {
1717
.map(slug => {
1818
const post = postsTable[slug]
1919
// remove draft posts in production
20-
if (process.env.NODE_ENV === 'production' && post.Published !== 'Yes') {
20+
if (!postIsReady(post)) {
2121
return null
2222
}
2323
post.Authors = post.Authors || []
@@ -61,7 +61,7 @@ export default ({ posts = [] }) => {
6161
<div className="authors">By: {post.Authors.join(' ')}</div>
6262
<div className="posted">Posted: {getDateStr(post.Date)}</div>
6363
<p>
64-
{post.preview.map((block, idx) =>
64+
{(post.preview || []).map((block, idx) =>
6565
textBlock(block, true, `${post.Slug}${idx}`)
6666
)}
6767
</p>

0 commit comments

Comments
 (0)