feat: update deps; fix eslint issues; fix bug with collections

This commit is contained in:
Travis Fischer
2025-06-06 22:19:41 +07:00
parent abc6fd9c4d
commit 92f8f69b9b
36 changed files with 2185 additions and 2040 deletions

View File

@@ -1,40 +1,29 @@
name: CI
on: [push, pull_request]
on: [push]
jobs:
test:
name: Test Node.js ${{ matrix.node-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
node-version:
- 18
- 20
- 22
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v3
id: pnpm-install
with:
version: 9.12.2
run_install: false
- name: Install Node.js
uses: actions/setup-node@v4
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile --strict-peer-dependencies
- name: Run test
run: pnpm test
- run: pnpm install --frozen-lockfile --strict-peer-dependencies
- run: pnpm test
# TODO Enable those lines below if you use a Redis cache, you'll also need to configure GitHub Repository Secrets
# env:

View File

@@ -4,6 +4,3 @@ dist/
node_modules/
.next/
.vercel/
.demo/
.renderer/

View File

@@ -1,11 +0,0 @@
{
"singleQuote": true,
"jsxSingleQuote": true,
"semi": false,
"useTabs": false,
"tabWidth": 2,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"trailingComma": "none"
}

View File

@@ -22,7 +22,7 @@ export function FooterImpl() {
const currentYear = new Date().getFullYear()
const onToggleDarkMode = React.useCallback(
(e) => {
(e: any) => {
e.preventDefault()
toggleDarkMode()
},

View File

@@ -39,36 +39,67 @@ const Code = dynamic(() =>
import('react-notion-x/build/third-party/code').then(async (m) => {
// add / remove any prism syntaxes here
await Promise.allSettled([
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-markup-templating.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-markup.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-bash.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-c.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-cpp.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-csharp.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-docker.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-java.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-js-templates.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-coffeescript.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-diff.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-git.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-go.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-graphql.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-handlebars.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-less.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-makefile.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-markdown.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-objectivec.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-ocaml.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-python.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-reason.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-rust.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-sass.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-scss.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-solidity.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-sql.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-stylus.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-swift.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-wasm.js'),
// @ts-expect-error Ignore prisma types
import('prismjs/components/prism-yaml.js')
])
return m.Code
@@ -112,7 +143,7 @@ function Tweet({ id }: { id: string }) {
}
const propertyLastEditedTimeValue = (
{ block, pageHeader },
{ block, pageHeader }: any,
defaultFn: () => React.ReactNode
) => {
if (pageHeader && block?.last_edited_time) {
@@ -125,7 +156,7 @@ const propertyLastEditedTimeValue = (
}
const propertyDateValue = (
{ data, schema, pageHeader },
{ data, schema, pageHeader }: any,
defaultFn: () => React.ReactNode
) => {
if (pageHeader && schema?.name?.toLowerCase() === 'published') {
@@ -142,7 +173,7 @@ const propertyDateValue = (
}
const propertyTextValue = (
{ schema, pageHeader },
{ schema, pageHeader }: any,
defaultFn: () => React.ReactNode
) => {
if (pageHeader && schema?.name?.toLowerCase() === 'author') {
@@ -189,11 +220,11 @@ export function NotionPage({
if (lite) params.lite = lite
const searchParams = new URLSearchParams(params)
return mapPageUrl(site, recordMap, searchParams)
return site ? mapPageUrl(site, recordMap!, searchParams) : undefined
}, [site, recordMap, lite])
const keys = Object.keys(recordMap?.block || {})
const block = recordMap?.block?.[keys[0]]?.value
const block = recordMap?.block?.[keys[0]!]?.value
// const isRootPage =
// parsePageId(block?.id) === parsePageId(site?.rootNotionPageId)
@@ -205,7 +236,11 @@ export function NotionPage({
const pageAside = React.useMemo(
() => (
<PageAside block={block} recordMap={recordMap} isBlogPost={isBlogPost} />
<PageAside
block={block!}
recordMap={recordMap!}
isBlogPost={isBlogPost}
/>
),
[block, recordMap, isBlogPost]
)
@@ -238,8 +273,9 @@ export function NotionPage({
g.block = block
}
const canonicalPageUrl =
!config.isDev && getCanonicalPageUrl(site, recordMap)(pageId)
const canonicalPageUrl = config.isDev
? undefined
: getCanonicalPageUrl(site, recordMap)(pageId)
const socialImage = mapImageUrl(
getPageProperty<string>('Social Image', block, recordMap) ||
@@ -286,7 +322,7 @@ export function NotionPage({
defaultPageCoverPosition={config.defaultPageCoverPosition}
mapPageUrl={siteMapPageUrl}
mapImageUrl={mapImageUrl}
searchNotion={config.isSearchEnabled ? searchNotion : null}
searchNotion={config.isSearchEnabled ? searchNotion : undefined}
pageAside={pageAside}
footer={footer}
/>

View File

@@ -51,7 +51,7 @@ export function NotionPageHeader({
<div className='notion-nav-header-rhs breadcrumbs'>
{navigationLinks
?.map((link, index) => {
if (!link.pageId && !link.url) {
if (!link?.pageId && !link?.url) {
return null
}

View File

@@ -19,7 +19,8 @@
line-height: 1.5;
color: rgb(55, 53, 47);
caret-color: rgb(55, 53, 47);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica,
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica,
'Apple Color Emoji', Arial, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol';
background-color: var(--bg-color);
}

22
eslint.config.js Normal file
View File

@@ -0,0 +1,22 @@
import { config } from '@fisch0920/config/eslint'
export default [
...config,
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'react/prop-types': 'off',
'unicorn/no-array-reduce': 'off',
'unicorn/filename-case': 'off',
'unicorn/prefer-global-this': 'off',
'no-process-env': 'off',
'array-callback-return': 'off',
'jsx-a11y/click-events-have-key-events': 'off',
'jsx-a11y/no-static-element-interactions': 'off',
'jsx-a11y/media-has-caption': 'off',
'jsx-a11y/interactive-supports-focus': 'off',
'jsx-a11y/anchor-is-valid': 'off',
'@typescript-eslint/naming-convention': 'off'
}
}
]

View File

@@ -4,7 +4,7 @@ export async function pageAcl({
site,
recordMap,
pageId
}: PageProps): Promise<PageProps> {
}: PageProps): Promise<PageProps | undefined> {
if (!site) {
return {
error: {

View File

@@ -7,7 +7,11 @@
import { parsePageId } from 'notion-utils'
import { type PostHogConfig } from 'posthog-js'
import { getEnv, getSiteConfig } from './get-config-value'
import {
getEnv,
getRequiredSiteConfig,
getSiteConfig
} from './get-config-value'
import { type NavigationLink } from './site-config'
import {
type NavigationStyle,
@@ -19,17 +23,15 @@ import {
export const rootNotionPageId: string = parsePageId(
getSiteConfig('rootNotionPageId'),
{ uuid: false }
)
)!
if (!rootNotionPageId) {
throw new Error('Config error invalid "rootNotionPageId"')
}
// if you want to restrict pages to a single notion workspace (optional)
export const rootNotionSpaceId: string | null = parsePageId(
getSiteConfig('rootNotionSpaceId', null),
{ uuid: true }
)
export const rootNotionSpaceId: string | null =
parsePageId(getSiteConfig('rootNotionSpaceId'), { uuid: true }) ?? null
export const pageUrlOverrides = cleanPageUrlMap(
getSiteConfig('pageUrlOverrides', {}) || {},
@@ -47,24 +49,24 @@ export const environment = process.env.NODE_ENV || 'development'
export const isDev = environment === 'development'
// general site config
export const name: string = getSiteConfig('name')
export const author: string = getSiteConfig('author')
export const domain: string = getSiteConfig('domain')
export const name: string = getRequiredSiteConfig('name')
export const author: string = getRequiredSiteConfig('author')
export const domain: string = getRequiredSiteConfig('domain')
export const description: string = getSiteConfig('description', 'Notion Blog')
export const language: string = getSiteConfig('language', 'en')
// social accounts
export const twitter: string | null = getSiteConfig('twitter', null)
export const mastodon: string | null = getSiteConfig('mastodon', null)
export const github: string | null = getSiteConfig('github', null)
export const youtube: string | null = getSiteConfig('youtube', null)
export const linkedin: string | null = getSiteConfig('linkedin', null)
export const newsletter: string | null = getSiteConfig('newsletter', null)
export const zhihu: string | null = getSiteConfig('zhihu', null)
export const twitter: string | undefined = getSiteConfig('twitter')
export const mastodon: string | undefined = getSiteConfig('mastodon')
export const github: string | undefined = getSiteConfig('github')
export const youtube: string | undefined = getSiteConfig('youtube')
export const linkedin: string | undefined = getSiteConfig('linkedin')
export const newsletter: string | undefined = getSiteConfig('newsletter')
export const zhihu: string | undefined = getSiteConfig('zhihu')
export const getMastodonHandle = (): string | null => {
export const getMastodonHandle = (): string | undefined => {
if (!mastodon) {
return null
return
}
// Since Mastodon is decentralized, handles include the instance domain name.
@@ -74,14 +76,10 @@ export const getMastodonHandle = (): string | null => {
}
// default notion values for site-wide consistency (optional; may be overridden on a per-page basis)
export const defaultPageIcon: string | null = getSiteConfig(
'defaultPageIcon',
null
)
export const defaultPageCover: string | null = getSiteConfig(
'defaultPageCover',
null
)
export const defaultPageIcon: string | undefined =
getSiteConfig('defaultPageIcon')
export const defaultPageCover: string | undefined =
getSiteConfig('defaultPageCover')
export const defaultPageCoverPosition: number = getSiteConfig(
'defaultPageCoverPosition',
0.5
@@ -104,7 +102,7 @@ export const navigationStyle: NavigationStyle = getSiteConfig(
'default'
)
export const navigationLinks: Array<NavigationLink | null> = getSiteConfig(
export const navigationLinks: Array<NavigationLink | undefined> = getSiteConfig(
'navigationLinks',
null
)
@@ -115,19 +113,18 @@ export const isSearchEnabled: boolean = getSiteConfig('isSearchEnabled', true)
// ----------------------------------------------------------------------------
// Optional redis instance for persisting preview images
export const isRedisEnabled: boolean =
getSiteConfig('isRedisEnabled', false) || !!getEnv('REDIS_ENABLED', null)
export const isRedisEnabled: boolean = getSiteConfig('isRedisEnabled', false)
// (if you want to enable redis, only REDIS_HOST and REDIS_PASSWORD are required)
// we recommend that you store these in a local `.env` file
export const redisHost: string | null = getEnv('REDIS_HOST', null)
export const redisPassword: string | null = getEnv('REDIS_PASSWORD', null)
export const redisUser: string = getEnv('REDIS_USER', 'default')
export const redisHost: string | undefined = getEnv('REDIS_HOST')
export const redisPassword: string | undefined = getEnv('REDIS_PASSWORD')
export const redisUser: string | undefined = getEnv('REDIS_USER', 'default')
export const redisUrl = getEnv(
'REDIS_URL',
`redis://${redisUser}:${redisPassword}@${redisHost}`
)
export const redisNamespace: string | null = getEnv(
export const redisNamespace: string | undefined = getEnv(
'REDIS_NAMESPACE',
'preview-images'
)
@@ -160,7 +157,7 @@ export const site: Site = {
description
}
export const fathomId = isDev ? null : process.env.NEXT_PUBLIC_FATHOM_ID
export const fathomId = isDev ? undefined : process.env.NEXT_PUBLIC_FATHOM_ID
export const fathomConfig = fathomId
? {
excludedDomains: ['localhost', 'localhost:3000']
@@ -211,7 +208,7 @@ function invertPageUrlOverrides(
pageUrlOverrides: PageUrlOverridesMap
): PageUrlOverridesInverseMap {
return Object.keys(pageUrlOverrides).reduce((acc, uri) => {
const pageId = pageUrlOverrides[uri]
const pageId = pageUrlOverrides[uri]!
return {
...acc,

View File

@@ -5,7 +5,7 @@ import { isRedisEnabled, redisNamespace, redisUrl } from './config'
let db: Keyv
if (isRedisEnabled) {
const keyvRedis = new KeyvRedis(redisUrl)
const keyvRedis = new KeyvRedis(redisUrl!)
db = new Keyv({ store: keyvRedis, namespace: redisNamespace || undefined })
} else {
db = new Keyv()

View File

@@ -10,18 +10,20 @@ export function getCanonicalPageId(
pageId: string,
recordMap: ExtendedRecordMap,
{ uuid = true }: { uuid?: boolean } = {}
): string | null {
): string | undefined {
const cleanPageId = parsePageId(pageId, { uuid: false })
if (!cleanPageId) {
return null
return
}
const override = inversePageUrlOverrides[cleanPageId]
if (override) {
return override
} else {
return getCanonicalPageIdImpl(pageId, recordMap, {
uuid
})
return (
getCanonicalPageIdImpl(pageId, recordMap, {
uuid
}) ?? undefined
)
}
}

View File

@@ -6,11 +6,13 @@ if (!rawSiteConfig) {
}
// allow environment variables to override site.config.ts
let siteConfigOverrides: SiteConfig
let siteConfigOverrides: SiteConfig | undefined
try {
if (process.env.NEXT_PUBLIC_SITE_CONFIG) {
siteConfigOverrides = JSON.parse(process.env.NEXT_PUBLIC_SITE_CONFIG)
siteConfigOverrides = JSON.parse(
process.env.NEXT_PUBLIC_SITE_CONFIG
) as SiteConfig
}
} catch (err) {
console.error('Invalid config "NEXT_PUBLIC_SITE_CONFIG" failed to parse')
@@ -22,25 +24,36 @@ const siteConfig: SiteConfig = {
...siteConfigOverrides
}
export function getSiteConfig<T>(key: string, defaultValue?: T): T {
const value = siteConfig[key]
export function getSiteConfig<T, TDefault>(
key: string,
defaultValue?: TDefault
): TDefault extends undefined ? T | undefined : T {
const value = siteConfig[key as keyof SiteConfig]
if (value !== undefined) {
return value
return value as T
}
if (defaultValue !== undefined) {
return defaultValue
return defaultValue as TDefault extends undefined ? T | undefined : T
}
export function getRequiredSiteConfig<T>(key: string): T {
const value = siteConfig[key as keyof SiteConfig]
if (value !== undefined) {
return value as T
}
throw new Error(`Config error: missing required site config value "${key}"`)
}
export const isServer = typeof window === 'undefined'
export function getEnv(
key: string,
defaultValue?: string,
env = process.env
): string {
): string | undefined {
const value = env[key]
if (value !== undefined) {
@@ -51,5 +64,7 @@ export function getEnv(
return defaultValue
}
throw new Error(`Config error: missing required env variable "${key}"`)
if (isServer) {
throw new Error(`Config error: missing required env variable "${key}"`)
}
}

View File

@@ -12,7 +12,7 @@ const uuid = !!includeNotionIdInUrls
export async function getSiteMap(): Promise<types.SiteMap> {
const partialSiteMap = await getAllPages(
config.rootNotionPageId,
config.rootNotionSpaceId
config.rootNotionSpaceId ?? undefined
)
return {
@@ -25,14 +25,19 @@ const getAllPages = pMemoize(getAllPagesImpl, {
cacheKey: (...args) => JSON.stringify(args)
})
const getPage = async (pageId: string, ...args) => {
const getPage = async (pageId: string, opts?: any) => {
console.log('\nnotion getPage', uuidToId(pageId))
return notion.getPage(pageId, ...args)
return notion.getPage(pageId, {
kyOptions: {
timeout: 30_000
},
...opts
})
}
async function getAllPagesImpl(
rootNotionPageId: string,
rootNotionSpaceId: string
rootNotionSpaceId?: string
): Promise<Partial<types.SiteMap>> {
const pageMap = await getAllPagesInSpace(
rootNotionPageId,
@@ -41,7 +46,7 @@ async function getAllPagesImpl(
)
const canonicalPageMap = Object.keys(pageMap).reduce(
(map, pageId: string) => {
(map: Record<string, string>, pageId: string) => {
const recordMap = pageMap[pageId]
if (!recordMap) {
throw new Error(`Error loading page "${pageId}"`)
@@ -49,14 +54,14 @@ async function getAllPagesImpl(
const block = recordMap.block[pageId]?.value
if (
!(getPageProperty<boolean | null>('Public', block, recordMap) ?? true)
!(getPageProperty<boolean | null>('Public', block!, recordMap) ?? true)
) {
return map
}
const canonicalPageId = getCanonicalPageId(pageId, recordMap, {
uuid
})
})!
if (map[canonicalPageId]) {
// you can have multiple pages in different collections that have the same id

View File

@@ -1,6 +1,6 @@
import { api, host } from './config'
export function getSocialImageUrl(pageId: string) {
export function getSocialImageUrl(pageId: string | undefined) {
try {
const url = new URL(api.getSocialImage, host)
@@ -8,7 +8,7 @@ export function getSocialImageUrl(pageId: string) {
url.searchParams.set('id', pageId)
return url.toString()
}
} catch (err) {
} catch (err: any) {
console.warn('error invalid social image url', pageId, err.message)
}

View File

@@ -38,7 +38,7 @@ async function getTweetImpl(tweetId: string): Promise<any> {
if (cachedTweet || cachedTweet === null) {
return cachedTweet
}
} catch (err) {
} catch (err: any) {
// ignore redis errors
console.warn(`redis error get "${cacheKey}"`, err.message)
}
@@ -47,7 +47,7 @@ async function getTweetImpl(tweetId: string): Promise<any> {
try {
await db.set(cacheKey, tweetData)
} catch (err) {
} catch (err: any) {
// ignore redis errors
console.warn(`redis error set "${cacheKey}"`, err.message)
}

View File

@@ -12,7 +12,7 @@ const uuid = !!includeNotionIdInUrls
export const mapPageUrl =
(site: Site, recordMap: ExtendedRecordMap, searchParams: URLSearchParams) =>
(pageId = '') => {
const pageUuid = parsePageId(pageId, { uuid: true })
const pageUuid = parsePageId(pageId, { uuid: true })!
if (uuidToId(pageUuid) === site.rootNotionPageId) {
return createUrl('/', searchParams)
@@ -27,7 +27,7 @@ export const mapPageUrl =
export const getCanonicalPageUrl =
(site: Site, recordMap: ExtendedRecordMap) =>
(pageId = '') => {
const pageUuid = parsePageId(pageId, { uuid: true })
const pageUuid = parsePageId(pageId, { uuid: true })!
if (uuidToId(pageId) === site.rootNotionPageId) {
return `https://${site.domain}`

View File

@@ -19,7 +19,7 @@ import { getPreviewImageMap } from './preview-images'
const getNavigationLinkPages = pMemoize(
async (): Promise<ExtendedRecordMap[]> => {
const navigationLinkPageIds = (navigationLinks || [])
.map((link) => link.pageId)
.map((link) => link?.pageId)
.filter(Boolean)
if (navigationStyle !== 'default' && navigationLinkPageIds.length) {

View File

@@ -15,7 +15,7 @@ export const oembed = async ({
dark?: boolean
}) => {
// TODO: handle pages with no pageId via domain
const pageId = parsePageId(url)
const pageId = parsePageId(url)!
let title = config.name
let authorName = config.author
@@ -26,7 +26,7 @@ export const oembed = async ({
const pageTitle = getPageTitle(page)
if (pageTitle) title = pageTitle
const user = page.notion_user[Object.keys(page.notion_user)[0]]?.value
const user = page.notion_user[Object.keys(page.notion_user)[0]!]!.value
const name = [user.given_name, user.family_name]
.filter(Boolean)
.join(' ')

View File

@@ -19,7 +19,7 @@ export async function getPreviewImageMap(
const urls: string[] = getPageImageUrls(recordMap, {
mapImageUrl
})
.concat([defaultPageIcon, defaultPageCover])
.concat([defaultPageIcon, defaultPageCover].filter(Boolean))
.filter(Boolean)
const previewImagesMap = Object.fromEntries(
@@ -48,7 +48,7 @@ async function createPreviewImage(
if (cachedPreviewImage) {
return cachedPreviewImage
}
} catch (err) {
} catch (err: any) {
// ignore redis errors
console.warn(`redis error get "${cacheKey}"`, err.message)
}
@@ -65,13 +65,13 @@ async function createPreviewImage(
try {
await db.set(cacheKey, previewImage)
} catch (err) {
} catch (err: any) {
// ignore redis errors
console.warn(`redis error set "${cacheKey}"`, err.message)
}
return previewImage
} catch (err) {
} catch (err: any) {
console.warn('failed to create preview image', url, err.message)
return null
}

1
lib/reset.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
import '@fisch0920/config/ts-reset'

View File

@@ -1,18 +1,22 @@
import { type ExtendedRecordMap } from 'notion-types'
import { parsePageId } from 'notion-utils'
import type { PageProps } from './types'
import * as acl from './acl'
import { environment, pageUrlAdditions, pageUrlOverrides, site } from './config'
import { db } from './db'
import { getSiteMap } from './get-site-map'
import { getPage } from './notion'
export async function resolveNotionPage(domain: string, rawPageId?: string) {
let pageId: string
export async function resolveNotionPage(
domain: string,
rawPageId?: string
): Promise<PageProps> {
let pageId: string | undefined
let recordMap: ExtendedRecordMap
if (rawPageId && rawPageId !== 'index') {
pageId = parsePageId(rawPageId)
pageId = parsePageId(rawPageId)!
if (!pageId) {
// check if the site configuration provides an override or a fallback for
@@ -21,7 +25,7 @@ export async function resolveNotionPage(domain: string, rawPageId?: string) {
pageUrlOverrides[rawPageId] || pageUrlAdditions[rawPageId]
if (override) {
pageId = parsePageId(override)
pageId = parsePageId(override)!
}
}
@@ -37,7 +41,7 @@ export async function resolveNotionPage(domain: string, rawPageId?: string) {
pageId = await db.get(cacheKey)
// console.log(`redis get "${cacheKey}"`, pageId)
} catch (err) {
} catch (err: any) {
// ignore redis errors
console.warn(`redis error get "${cacheKey}"`, err.message)
}
@@ -64,7 +68,7 @@ export async function resolveNotionPage(domain: string, rawPageId?: string) {
await db.set(cacheKey, pageId, cacheTTL)
// console.log(`redis set "${cacheKey}"`, pageId, { cacheTTL })
} catch (err) {
} catch (err: any) {
// ignore redis errors
console.warn(`redis error set "${cacheKey}"`, err.message)
}
@@ -86,6 +90,6 @@ export async function resolveNotionPage(domain: string, rawPageId?: string) {
recordMap = await getPage(pageId)
}
const props = { site, recordMap, pageId }
const props: PageProps = { site, recordMap, pageId }
return { ...props, ...(await acl.pageAcl(props)) }
}

View File

@@ -29,7 +29,7 @@ async function searchNotionImpl(
error.response = res
throw error
})
.then((res) => res.json())
.then((res) => res.json() as Promise<types.SearchResults>)
// return ky
// .post(api.searchNotion, {

View File

@@ -2,7 +2,7 @@ import type * as types from './types'
export interface SiteConfig {
rootNotionPageId: string
rootNotionSpaceId?: string
rootNotionSpaceId?: string | null
name: string
domain: string
@@ -28,8 +28,8 @@ export interface SiteConfig {
isSearchEnabled?: boolean
includeNotionIdInUrls?: boolean
pageUrlOverrides?: types.PageUrlOverridesMap
pageUrlAdditions?: types.PageUrlOverridesMap
pageUrlOverrides?: types.PageUrlOverridesMap | null
pageUrlAdditions?: types.PageUrlOverridesMap | null
navigationStyle?: types.NavigationStyle
navigationLinks?: Array<NavigationLink>

View File

@@ -31,7 +31,7 @@ export interface Site {
domain: string
rootNotionPageId: string
rootNotionSpaceId: string
rootNotionSpaceId: string | null
// settings
html?: string
@@ -69,9 +69,9 @@ export interface PageUrlOverridesInverseMap {
export interface NotionPageInfo {
pageId: string
title: string
image: string
imageObjectPosition: string
author: string
authorImage: string
detail: string
image?: string
imageObjectPosition?: string
author?: string
authorImage?: string
detail?: string
}

2
next-env.d.ts vendored
View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.

View File

@@ -1,8 +1,10 @@
import bundleAnalyzer from '@next/bundle-analyzer'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import bundleAnalyzer from '@next/bundle-analyzer'
const withBundleAnalyzer = bundleAnalyzer({
// eslint-disable-next-line no-process-env
enabled: process.env.ANALYZE === 'true'
})
@@ -22,7 +24,7 @@ export default withBundleAnalyzer({
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;"
},
webpack: (config, _context) => {
webpack: (config) => {
// Workaround for ensuring that `react` and `react-dom` resolve correctly
// when using a locally-linked version of `react-notion-x`.
// @see https://github.com/vercel/next.js/issues/50391

View File

@@ -6,6 +6,7 @@
"author": "Travis Fischer <travis@transitivebullsh.it>",
"repository": "transitive-bullshit/nextjs-notion-starter-kit",
"license": "MIT",
"packageManager": "pnpm@10.11.1",
"engines": {
"node": ">=18"
},
@@ -37,38 +38,51 @@
"classnames": "^2.5.1",
"expiry-map": "^2.0.0",
"fathom-client": "^3.4.1",
"ky": "^1.7.2",
"ky": "^1.8.1",
"lqip-modern": "^2.2.1",
"next": "^15.0.3",
"notion-client": "^7.1.3",
"notion-types": "^7.1.3",
"notion-utils": "^7.1.3",
"p-map": "^7.0.2",
"next": "^15.3.3",
"notion-client": "^7.4.0",
"notion-types": "^7.4.0",
"notion-utils": "^7.4.0",
"p-map": "^7.0.3",
"p-memoize": "^7.1.1",
"posthog-js": "^1.181.0",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"posthog-js": "^1.249.4",
"prismjs": "^1.30.0",
"react": "^19.1.0",
"react-body-classname": "^1.3.1",
"react-dom": "^18.2.0",
"react-notion-x": "^7.2.3",
"react-tweet": "^3.2.1",
"react-use": "^17.4.2",
"react-dom": "^19.1.0",
"react-notion-x": "^7.4.0",
"react-tweet": "^3.2.2",
"react-use": "^17.6.0",
"rss": "^1.2.2"
},
"devDependencies": {
"@fisch0920/eslint-config": "^1.4.0",
"@next/bundle-analyzer": "^15.0.2",
"@types/node": "^22.8.6",
"@types/react": "^18.0.21",
"@fisch0920/config": "^1.1.2",
"@next/bundle-analyzer": "^15.3.3",
"@types/node": "^22.15.30",
"@types/prismjs": "^1.26.5",
"@types/react": "^19.1.6",
"@types/react-body-classname": "^1.1.10",
"@types/rss": "^0.0.32",
"cross-env": "^7.0.2",
"eslint": "^8.57.1",
"npm-run-all2": "^7.0.1",
"prettier": "^3.3.3",
"typescript": "^5.6.3"
"eslint": "^9.28.0",
"npm-run-all2": "^8.0.4",
"prettier": "^3.5.3",
"typescript": "^5.8.3"
},
"overrides": {
"cacheable-request": {
"keyv": "npm:@keyvhq/core@~1.6.6"
}
},
"prettier": "@fisch0920/config/prettier",
"simple-git-hooks": {
"pre-commit": "npx lint-staged"
},
"lint-staged": {
"*.{ts,tsx}": [
"prettier --ignore-unknown --write",
"eslint --fix"
]
}
}

View File

@@ -9,7 +9,7 @@ import { type PageProps, type Params } from '@/lib/types'
export const getStaticProps: GetStaticProps<PageProps, Params> = async (
context
) => {
const rawPageId = context.params.pageId as string
const rawPageId = context.params?.pageId as string
try {
const props = await resolveNotionPage(domain, rawPageId)
@@ -48,6 +48,6 @@ export async function getStaticPaths() {
return staticPaths
}
export default function NotionDomainDynamicPage(props) {
export default function NotionDomainDynamicPage(props: PageProps) {
return <NotionPage {...props} />
}

View File

@@ -16,7 +16,7 @@ import 'styles/prism-theme.css'
import type { AppProps } from 'next/app'
import * as Fathom from 'fathom-client'
import { useRouter } from 'next/router'
import posthog from 'posthog-js'
import { posthog } from 'posthog-js'
import * as React from 'react'
import { bootstrap } from '@/lib/bootstrap-client'

View File

@@ -2,7 +2,7 @@ import { IconContext } from '@react-icons/all-files'
import Document, { Head, Html, Main, NextScript } from 'next/document'
export default class MyDocument extends Document {
render() {
override render() {
return (
<IconContext.Provider value={{ style: { verticalAlign: 'middle' } }}>
<Html lang='en'>

View File

@@ -22,7 +22,7 @@ export default async function OGImage(
req: NextApiRequest,
res: NextApiResponse
) {
const { searchParams } = new URL(req.url)
const { searchParams } = new URL(req.url!)
const pageId = parsePageId(
searchParams.get('id') || libConfig.rootNotionPageId
)
@@ -178,7 +178,7 @@ export async function getNotionPageInfo({
const recordMap = await notion.getPage(pageId)
const keys = Object.keys(recordMap?.block || {})
const block = recordMap?.block?.[keys[0]]?.value
const block = recordMap?.block?.[keys[0]!]?.value
if (!block) {
throw new Error('Invalid recordMap for page')
@@ -209,7 +209,7 @@ export async function getNotionPageInfo({
libConfig.defaultPageCoverPosition
const imageObjectPosition = imageCoverPosition
? `center ${(1 - imageCoverPosition) * 100}%`
: null
: undefined
const imageBlockUrl = mapImageUrl(
getPageProperty<string>('Social Image', block, recordMap) ||
@@ -220,7 +220,7 @@ export async function getNotionPageInfo({
const blockIcon = getBlockIcon(block, recordMap)
const authorImageBlockUrl = mapImageUrl(
blockIcon && isUrl(blockIcon) ? blockIcon : null,
blockIcon && isUrl(blockIcon) ? blockIcon : undefined,
block
)
const authorImageFallbackUrl = mapImageUrl(libConfig.defaultPageIcon, block)
@@ -272,7 +272,9 @@ export async function getNotionPageInfo({
}
}
async function isUrlReachable(url: string | null): Promise<boolean> {
async function isUrlReachable(
url: string | undefined | null
): Promise<boolean> {
if (!url) {
return false
}
@@ -286,9 +288,9 @@ async function isUrlReachable(url: string | null): Promise<boolean> {
}
async function getCompatibleImageUrl(
url: string | null,
fallbackUrl: string | null
): Promise<string | null> {
url: string | undefined | null,
fallbackUrl: string | undefined | null
): Promise<string | undefined> {
const image = (await isUrlReachable(url)) ? url : fallbackUrl
if (image) {
@@ -303,5 +305,5 @@ async function getCompatibleImageUrl(
}
}
return image
return image ?? undefined
}

View File

@@ -35,12 +35,12 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
})
for (const pagePath of Object.keys(siteMap.canonicalPageMap)) {
const pageId = siteMap.canonicalPageMap[pagePath]
const pageId = siteMap.canonicalPageMap[pagePath]!
const recordMap = siteMap.pageMap[pageId] as ExtendedRecordMap
if (!recordMap) continue
const keys = Object.keys(recordMap?.block || {})
const block = recordMap?.block?.[keys[0]]?.value
const block = recordMap?.block?.[keys[0]!]?.value
if (!block) continue
const parentPage = getBlockParentPage(block, recordMap)
@@ -67,7 +67,7 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
? new Date(lastUpdatedTime)
: publishedTime
? new Date(publishedTime)
: undefined
: new Date()
const socialImageUrl = getSocialImageUrl(pageId)
feed.item({

View File

@@ -1,3 +1,4 @@
import type { PageProps } from '@/lib/types'
import { NotionPage } from '@/components/NotionPage'
import { domain } from '@/lib/config'
import { resolveNotionPage } from '@/lib/resolve-notion-page'
@@ -16,6 +17,6 @@ export const getStaticProps = async () => {
}
}
export default function NotionDomainPage(props) {
export default function NotionDomainPage(props: PageProps) {
return <NotionPage {...props} />
}

3779
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,13 @@
{
"extends": "@fisch0920/config/tsconfig-react",
"compilerOptions": {
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"moduleDetection": "force",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": ".",
"typeRoots": ["./node_modules/@types"],
"incremental": true,
"paths": {
"@/components/*": ["components/*"],
"@/lib/*": ["lib/*"],
"@/styles/*": ["styles/*"]
}
},
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "site.config.ts"]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "*.config.ts"],
"exclude": ["node_modules"]
}