Merge pull request #383 from transitive-bullshit/feature/update-deps

This commit is contained in:
Travis Fischer
2022-10-14 18:53:10 -04:00
committed by GitHub
12 changed files with 3325 additions and 3844 deletions

View File

@@ -6,17 +6,17 @@ jobs:
Build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v2
- uses: actions/setup-node@v3
with:
node-version: 16
cache: yarn
- run: yarn install --frozen-lockfile
- name: build
# TODO Enable those lines below if you use a Redis cache, you'll also need to configure GitHub Repository Secrets
# env:
# REDIS_HOST: ${{ secrets.REDIS_HOST }}
# REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}
# TODO Enable those lines below if you use a Redis cache, you'll also need to configure GitHub Repository Secrets
# env:
# REDIS_HOST: ${{ secrets.REDIS_HOST }}
# REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}
run: yarn build

View File

@@ -6,7 +6,7 @@
*/
import { parsePageId } from 'notion-utils'
import posthog from 'posthog-js'
import type posthog from 'posthog-js'
import { getEnv, getSiteConfig } from './get-config-value'
import { NavigationLink } from './site-config'
import {
@@ -125,12 +125,15 @@ export const redisNamespace: string | null = getEnv(
export const isServer = typeof window === 'undefined'
export const port = getEnv('PORT', '3000')
export const host = isDev ? `http://localhost:${port}` : `https://${domain}`
export const host = isDev
? `http://localhost:${port}`
: `https://${process.env.VERCEL_URL || domain}`
export const apiBaseUrl = `/api`
export const api = {
searchNotion: `${apiBaseUrl}/search-notion`,
getNotionPageInfo: `${apiBaseUrl}/notion-page-info`,
getSocialImage: `${apiBaseUrl}/social-image`
}

File diff suppressed because one or more lines are too long

View File

@@ -60,3 +60,13 @@ export interface PageUrlOverridesInverseMap {
// (this overrides the built-in URL path generation for these pages)
[pageId: string]: string
}
export interface NotionPageInfo {
pageId: string
title: string
image: string
imageObjectPosition: string
author: string
authorImage: string
detail: string
}

View File

@@ -7,7 +7,7 @@
"repository": "transitive-bullshit/nextjs-notion-starter-kit",
"license": "MIT",
"engines": {
"node": ">=14.17"
"node": ">=16"
},
"scripts": {
"dev": "next dev",
@@ -29,6 +29,7 @@
"@keyvhq/core": "^1.6.9",
"@keyvhq/redis": "^1.6.10",
"@react-icons/all-files": "^4.1.0",
"@vercel/og": "^0.0.18",
"classnames": "^2.3.1",
"date-fns": "^2.28.0",
"expiry-map": "^2.0.0",
@@ -36,48 +37,37 @@
"got": "^12.0.3",
"isomorphic-unfetch": "^3.1.0",
"lqip-modern": "^1.2.0",
"next": "^12.2.3",
"next-api-og-image": "^2.2.1",
"node-fetch": "^2.6.1",
"notion-client": "^6.12.6",
"notion-types": "^6.12.6",
"notion-utils": "^6.12.6",
"next": "^12.3.1",
"notion-client": "^6.13.11",
"notion-types": "^6.13.4",
"notion-utils": "^6.13.4",
"p-map": "^5.3.0",
"p-memoize": "^6.0.1",
"posthog-js": "^1.20.2",
"react": "^17.0.2",
"react": "^18.2.0",
"react-body-classname": "^1.3.1",
"react-dom": "^17.0.2",
"react-notion-x": "^6.12.7",
"react-dom": "^18.2.0",
"react-notion-x": "^6.15.3",
"react-tweet-embed": "^2.0.0",
"react-use": "^17.3.2",
"rss": "^1.2.2"
},
"devDependencies": {
"@next/bundle-analyzer": "^12.1.0",
"@types/node": "^17.0.23",
"@types/node-fetch": "^3.0.3",
"@types/react": "^17.0.31",
"@typescript-eslint/eslint-plugin": "^5.15.0",
"@typescript-eslint/parser": "^5.15.0",
"@next/bundle-analyzer": "^12.3.1",
"@types/node": "^18.8.5",
"@types/react": "^18.0.21",
"@typescript-eslint/eslint-plugin": "^5.40.0",
"@typescript-eslint/parser": "^5.40.0",
"cross-env": "^7.0.2",
"eslint": "^8.11.0",
"eslint": "^8.25.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-react": "^7.31.10",
"eslint-plugin-react-hooks": "^4.6.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.4.1",
"typescript": "^4.4.4"
},
"resolutions": {
"next-api-og-image/chrome-aws-lambda": "6.0.0",
"next-api-og-image/puppeteer-core": "6.0.0"
"prettier": "^2.7.1",
"typescript": "^4.8.4"
},
"overrides": {
"next-api-og-image": {
"chrome-aws-lambda": "6.0.0",
"puppeteer-core": "6.0.0"
},
"cacheable-request": {
"keyv": "npm:@keyvhq/core@~1.6.6"
}

View File

@@ -6,7 +6,9 @@ import { resolveNotionPage } from 'lib/resolve-notion-page'
import { PageProps, Params } from 'lib/types'
import { NotionPage } from 'components'
export const getStaticProps: GetStaticProps<PageProps, Params> = async (context) => {
export const getStaticProps: GetStaticProps<PageProps, Params> = async (
context
) => {
const rawPageId = context.params.pageId as string
try {

View File

@@ -0,0 +1,113 @@
import { NextApiRequest, NextApiResponse } from 'next'
import {
getBlockTitle,
getBlockIcon,
getPageProperty,
isUrl,
parsePageId
} from 'notion-utils'
import { PageBlock } from 'notion-types'
import { notion } from 'lib/notion-api'
import { mapImageUrl } from 'lib/map-image-url'
import { NotionPageInfo } from 'lib/types'
import * as libConfig from 'lib/config'
export default async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== 'POST') {
return res.status(405).send({ error: 'method not allowed' })
}
const pageId: string = parsePageId(req.body.pageId)
if (!pageId) {
throw new Error('Invalid notion page id')
}
const recordMap = await notion.getPage(pageId)
const keys = Object.keys(recordMap?.block || {})
const block = recordMap?.block?.[keys[0]]?.value
if (!block) {
throw new Error('Invalid recordMap for page')
}
const blockSpaceId = block.space_id
if (
blockSpaceId &&
libConfig.rootNotionSpaceId &&
blockSpaceId !== libConfig.rootNotionSpaceId
) {
return res.status(400).send({
error: `Notion page "${pageId}" belongs to a different workspace.`
})
}
const isBlogPost =
block.type === 'page' && block.parent_table === 'collection'
const title = getBlockTitle(block, recordMap) || libConfig.name
const image = mapImageUrl(
getPageProperty<string>('Social Image', block, recordMap) ||
(block as PageBlock).format?.page_cover ||
libConfig.defaultPageCover,
block
)
const imageCoverPosition =
(block as PageBlock).format?.page_cover_position ??
libConfig.defaultPageCoverPosition
const imageObjectPosition = imageCoverPosition
? `center ${(1 - imageCoverPosition) * 100}%`
: null
const blockIcon = getBlockIcon(block, recordMap)
const authorImage = mapImageUrl(
blockIcon && isUrl(blockIcon) ? blockIcon : libConfig.defaultPageIcon,
block
)
const author =
getPageProperty<string>('Author', block, recordMap) || libConfig.author
// const socialDescription =
// getPageProperty<string>('Description', block, recordMap) ||
// libConfig.description
// const lastUpdatedTime = getPageProperty<number>(
// 'Last Updated',
// block,
// recordMap
// )
const publishedTime = getPageProperty<number>('Published', block, recordMap)
const datePublished = publishedTime ? new Date(publishedTime) : undefined
// const dateUpdated = lastUpdatedTime
// ? new Date(lastUpdatedTime)
// : publishedTime
// ? new Date(publishedTime)
// : undefined
const date =
isBlogPost && datePublished
? `${datePublished.toLocaleString('en-US', {
month: 'long'
})} ${datePublished.getFullYear()}`
: undefined
const detail = date || author || libConfig.domain
const pageInfo: NotionPageInfo = {
pageId,
title,
image,
imageObjectPosition,
author,
authorImage,
detail
}
res.setHeader(
'Cache-Control',
'public, s-maxage=30, max-age=30, stale-while-revalidate=30'
)
res.status(200).json(pageInfo)
}

View File

@@ -1,251 +1,172 @@
import * as React from 'react'
import { withOGImage } from 'next-api-og-image'
import { ImageResponse } from '@vercel/og'
import { NextRequest } from 'next/server'
import {
getBlockTitle,
getBlockIcon,
getPageProperty,
isUrl,
parsePageId
} from 'notion-utils'
import { PageBlock } from 'notion-types'
import { NotionPageInfo } from 'lib/types'
import { host, api } from 'lib/config'
import { notion } from 'lib/notion-api'
import { mapImageUrl } from 'lib/map-image-url'
import { interRegular } from 'lib/fonts'
import * as config from 'lib/config'
const interRegularFontP = fetch(
new URL('../../public/fonts/Inter-Regular.ttf', import.meta.url)
).then((res) => res.arrayBuffer())
/**
* Social image generation via headless chrome.
*
* Note: To debug social images, set `debugInspectHtml` to true and load a social
* image URL. Instead of returning the rendered image, it will return the raw HTML
* that would've been passed to puppeteer. This makes it much easier to develop
* and debug issues locally.
*/
const debugInspectHtml = false
const interBoldFontP = fetch(
new URL('../../public/fonts/Inter-SemiBold.ttf', import.meta.url)
).then((res) => res.arrayBuffer())
export default withOGImage<'query', 'id'>({
template: {
react: async ({ id }) => {
const pageId = parsePageId(id)
export const config = {
runtime: 'experimental-edge'
}
if (!pageId) {
throw new Error('Invalid notion page id')
}
export default async function OGImage(req: NextRequest) {
const [interRegularFont, interBoldFont] = await Promise.all([
interRegularFontP,
interBoldFontP
])
const recordMap = await notion.getPage(pageId)
const keys = Object.keys(recordMap?.block || {})
const block = recordMap?.block?.[keys[0]]?.value
if (!block) {
throw new Error('Invalid recordMap for page')
}
const isBlogPost =
block.type === 'page' && block.parent_table === 'collection'
const title = getBlockTitle(block, recordMap) || config.name
const image = mapImageUrl(
getPageProperty<string>('Social Image', block, recordMap) ||
(block as PageBlock).format?.page_cover ||
config.defaultPageCover,
block
)
const imageCoverPosition =
(block as PageBlock).format?.page_cover_position ??
config.defaultPageCoverPosition
const imageObjectPosition = imageCoverPosition
? `center ${(1 - imageCoverPosition) * 100}%`
: null
const blockIcon = getBlockIcon(block, recordMap)
const authorImage = mapImageUrl(
blockIcon && isUrl(blockIcon) ? blockIcon : config.defaultPageIcon,
block
)
const author =
getPageProperty<string>('Author', block, recordMap) || config.author
// const socialDescription =
// getPageProperty<string>('Description', block, recordMap) ||
// config.description
const lastUpdatedTime = getPageProperty<number>(
'Last Updated',
block,
recordMap
)
const publishedTime = getPageProperty<number>(
'Published',
block,
recordMap
)
const dateUpdated = lastUpdatedTime
? new Date(lastUpdatedTime)
: publishedTime
? new Date(publishedTime)
: undefined
const date =
isBlogPost && dateUpdated
? `${dateUpdated.toLocaleString('en-US', {
month: 'long'
})} ${dateUpdated.getFullYear()}`
: undefined
const detail = date || config.domain
return (
<html>
<head>
<style dangerouslySetInnerHTML={{ __html: style }} />
</head>
<body>
<div className='container'>
<div className='horiz'>
<div className='lhs'>
<div className='main'>
<h1 className='title'>{title}</h1>
</div>
<div className='metadata'>
{authorImage && (
<div
className='author-image'
style={{ backgroundImage: `url(${authorImage})` }}
/>
)}
{(author || detail) && (
<div className='metadata-rhs'>
{author && <div className='author'>{author}</div>}
{detail && <div className='detail'>{detail}</div>}
</div>
)}
</div>
</div>
{image && (
<img
src={image}
className='rhs'
style={{
objectPosition: imageObjectPosition || undefined
}}
/>
)}
</div>
</div>
</body>
</html>
)
}
},
cacheControl: 'max-age=0, s-maxage=86400, stale-while-revalidate=3600',
type: 'jpeg',
quality: 75,
dev: {
inspectHtml: debugInspectHtml
const { searchParams } = new URL(req.url)
const pageId = searchParams.get('id')
if (!pageId) {
return new Response('Invalid notion page id', { status: 400 })
}
})
const style = `
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: normal;
src: url(data:font/woff2;charset=utf-8;base64,${interRegular}) format('woff2');
}
const pageInfoRes = await fetch(`${host}${api.getNotionPageInfo}`, {
method: 'POST',
body: JSON.stringify({ pageId }),
headers: { 'Content-Type': 'application/json' }
})
const pageInfo: NotionPageInfo = await pageInfoRes.json()
:root {
--padding: 8vmin;
}
return new ImageResponse(
(
<div
style={{
position: 'relative',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
backgroundColor: '#1F2027',
alignItems: 'center',
justifyContent: 'center',
fontFamily: '"Inter", sans-serif',
color: 'black'
}}
>
{pageInfo.image && (
<img
src={pageInfo.image}
style={{
position: 'absolute',
width: '100%',
height: '100%',
objectFit: 'cover'
// TODO: satori doesn't support background-size: cover and seems to
// have inconsistent support for filter + transform to get rid of the
// blurred edges. For now, we'll go without a blur filter on the
// background, but Satori is still very new, so hopefully we can re-add
// the blur soon.
* {
box-sizing: border-box;
}
// backgroundImage: pageInfo.image
// ? `url(${pageInfo.image})`
// : undefined,
// backgroundSize: '100% 100%'
// TODO: pageInfo.imageObjectPosition
// filter: 'blur(8px)'
// transform: 'scale(1.05)'
}}
/>
)}
body {
font-family: 'Inter', sans-serif;
padding: 0;
margin: 0;
}
<div
style={{
position: 'relative',
width: 900,
height: 450,
display: 'flex',
flexDirection: 'column',
border: '16px solid rgba(0,0,0,0.3)',
borderRadius: 8,
zIndex: '1'
}}
>
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-around',
backgroundColor: '#fff',
padding: 24,
alignItems: 'center',
textAlign: 'center'
}}
>
{pageInfo.detail && (
<div style={{ fontSize: 32, opacity: 0 }}>{pageInfo.detail}</div>
)}
.container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: var(--padding);
background: #1F2027;
color: #fff;
}
<div
style={{
fontSize: 70,
fontWeight: 700,
fontFamily: 'Inter'
}}
>
{pageInfo.title}
</div>
.horiz {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: var(--padding);
width: 100%;
height: 100%;
}
{pageInfo.detail && (
<div style={{ fontSize: 32, opacity: 0.6 }}>
{pageInfo.detail}
</div>
)}
</div>
</div>
.lhs {
display: flex;
flex-direction: column;
justify-content: space-between;
{pageInfo.authorImage && (
<div
style={{
position: 'absolute',
top: 32,
left: 104,
height: 128,
width: 128,
display: 'flex',
borderRadius: '50%',
border: '4px solid #fff',
zIndex: '5'
}}
>
<img
src={pageInfo.authorImage}
style={{
width: '100%',
height: '100%'
// transform: 'scale(1.04)'
}}
/>
</div>
)}
</div>
),
{
width: 1200,
height: 600,
fonts: [
{
name: 'Inter',
data: interRegularFont,
style: 'normal',
weight: 400
},
{
name: 'Inter',
data: interBoldFont,
style: 'normal',
weight: 700
}
]
}
)
}
.rhs {
width: 35%;
height: 100%;
border-radius: 4px;
object-fit: cover;
}
.title {
font-size: 3.2em;
line-height: 1.3;
}
.metadata {
color: #A9ACC0;
display: flex;
flex-direction: row;
align-items: center;
gap: calc(var(--padding) * 0.7);
font-size: 1.5em;
}
.author {
font-size: 1.75em;
}
.author-image {
background-size: cover;
width: 20vmin;
min-width: 20vmin;
max-width: 20vmin;
height: 20vmin;
min-height: 20vmin;
max-height: 20vmin;
border-radius: 50%;
border: 1.5vmin solid #fff;
}
.metadata-rhs {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.5em;
}
.detail {
overflow-wrap: break-word;
}
`

Binary file not shown.

Binary file not shown.

View File

@@ -40,7 +40,7 @@ It uses Notion as a CMS, [react-notion-x](https://github.com/NotionX/react-notio
**All config is defined in [site.config.ts](./site.config.ts).**
This project requires a recent version of Node.js (>= 14.17).
This project requires a recent version of Node.js (we recommend >= 16).
1. Fork / clone this repo
2. Change a few values in [site.config.ts](./site.config.ts)
@@ -129,7 +129,7 @@ Dark mode is fully supported and can be toggled via the sun / moon icon in the f
All Open Graph and social meta tags are generated from your Notion content, which makes social sharing look professional by default.
Social images are generated automatically using headless chrome. You can tweak the default React template for social images by editing [api/social-images.tsx](./pages/api/social-image.tsx).
Social images are generated automatically using [Vercel OG Image Generation](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation). You can tweak the default React template for social images by editing [api/social-images.tsx](./pages/api/social-image.tsx).
You can view an example social image live in production [here](https://transitivebullsh.it/api/social-image?id=dfc7f709-ae3e-42c6-9292-f6543d5586f0).

6579
yarn.lock

File diff suppressed because it is too large Load Diff