mirror of
https://github.com/d0zingcat/nextjs-notion-starter-kit.git
synced 2026-05-13 15:09:47 +00:00
Merge pull request #383 from transitive-bullshit/feature/update-deps
This commit is contained in:
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
10
lib/types.ts
10
lib/types.ts
@@ -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
|
||||
}
|
||||
|
||||
48
package.json
48
package.json
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
113
pages/api/notion-page-info.tsx
Normal file
113
pages/api/notion-page-info.tsx
Normal 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)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
`
|
||||
|
||||
BIN
public/fonts/Inter-Regular.ttf
Normal file
BIN
public/fonts/Inter-Regular.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Inter-SemiBold.ttf
Normal file
BIN
public/fonts/Inter-SemiBold.ttf
Normal file
Binary file not shown.
@@ -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).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user