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

View File

@@ -4,6 +4,3 @@ dist/
node_modules/ node_modules/
.next/ .next/
.vercel/ .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 currentYear = new Date().getFullYear()
const onToggleDarkMode = React.useCallback( const onToggleDarkMode = React.useCallback(
(e) => { (e: any) => {
e.preventDefault() e.preventDefault()
toggleDarkMode() toggleDarkMode()
}, },

View File

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

View File

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

View File

@@ -19,7 +19,8 @@
line-height: 1.5; line-height: 1.5;
color: rgb(55, 53, 47); color: rgb(55, 53, 47);
caret-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'; 'Apple Color Emoji', Arial, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol';
background-color: var(--bg-color); 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, site,
recordMap, recordMap,
pageId pageId
}: PageProps): Promise<PageProps> { }: PageProps): Promise<PageProps | undefined> {
if (!site) { if (!site) {
return { return {
error: { error: {

View File

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

View File

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

View File

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

View File

@@ -6,11 +6,13 @@ if (!rawSiteConfig) {
} }
// allow environment variables to override site.config.ts // allow environment variables to override site.config.ts
let siteConfigOverrides: SiteConfig let siteConfigOverrides: SiteConfig | undefined
try { try {
if (process.env.NEXT_PUBLIC_SITE_CONFIG) { 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) { } catch (err) {
console.error('Invalid config "NEXT_PUBLIC_SITE_CONFIG" failed to parse') console.error('Invalid config "NEXT_PUBLIC_SITE_CONFIG" failed to parse')
@@ -22,25 +24,36 @@ const siteConfig: SiteConfig = {
...siteConfigOverrides ...siteConfigOverrides
} }
export function getSiteConfig<T>(key: string, defaultValue?: T): T { export function getSiteConfig<T, TDefault>(
const value = siteConfig[key] key: string,
defaultValue?: TDefault
): TDefault extends undefined ? T | undefined : T {
const value = siteConfig[key as keyof SiteConfig]
if (value !== undefined) { if (value !== undefined) {
return value return value as T
} }
if (defaultValue !== undefined) { return defaultValue as TDefault extends undefined ? T | undefined : T
return defaultValue }
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}"`) throw new Error(`Config error: missing required site config value "${key}"`)
} }
export const isServer = typeof window === 'undefined'
export function getEnv( export function getEnv(
key: string, key: string,
defaultValue?: string, defaultValue?: string,
env = process.env env = process.env
): string { ): string | undefined {
const value = env[key] const value = env[key]
if (value !== undefined) { if (value !== undefined) {
@@ -51,5 +64,7 @@ export function getEnv(
return defaultValue 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> { export async function getSiteMap(): Promise<types.SiteMap> {
const partialSiteMap = await getAllPages( const partialSiteMap = await getAllPages(
config.rootNotionPageId, config.rootNotionPageId,
config.rootNotionSpaceId config.rootNotionSpaceId ?? undefined
) )
return { return {
@@ -25,14 +25,19 @@ const getAllPages = pMemoize(getAllPagesImpl, {
cacheKey: (...args) => JSON.stringify(args) cacheKey: (...args) => JSON.stringify(args)
}) })
const getPage = async (pageId: string, ...args) => { const getPage = async (pageId: string, opts?: any) => {
console.log('\nnotion getPage', uuidToId(pageId)) console.log('\nnotion getPage', uuidToId(pageId))
return notion.getPage(pageId, ...args) return notion.getPage(pageId, {
kyOptions: {
timeout: 30_000
},
...opts
})
} }
async function getAllPagesImpl( async function getAllPagesImpl(
rootNotionPageId: string, rootNotionPageId: string,
rootNotionSpaceId: string rootNotionSpaceId?: string
): Promise<Partial<types.SiteMap>> { ): Promise<Partial<types.SiteMap>> {
const pageMap = await getAllPagesInSpace( const pageMap = await getAllPagesInSpace(
rootNotionPageId, rootNotionPageId,
@@ -41,7 +46,7 @@ async function getAllPagesImpl(
) )
const canonicalPageMap = Object.keys(pageMap).reduce( const canonicalPageMap = Object.keys(pageMap).reduce(
(map, pageId: string) => { (map: Record<string, string>, pageId: string) => {
const recordMap = pageMap[pageId] const recordMap = pageMap[pageId]
if (!recordMap) { if (!recordMap) {
throw new Error(`Error loading page "${pageId}"`) throw new Error(`Error loading page "${pageId}"`)
@@ -49,14 +54,14 @@ async function getAllPagesImpl(
const block = recordMap.block[pageId]?.value const block = recordMap.block[pageId]?.value
if ( if (
!(getPageProperty<boolean | null>('Public', block, recordMap) ?? true) !(getPageProperty<boolean | null>('Public', block!, recordMap) ?? true)
) { ) {
return map return map
} }
const canonicalPageId = getCanonicalPageId(pageId, recordMap, { const canonicalPageId = getCanonicalPageId(pageId, recordMap, {
uuid uuid
}) })!
if (map[canonicalPageId]) { if (map[canonicalPageId]) {
// you can have multiple pages in different collections that have the same id // you can have multiple pages in different collections that have the same id

View File

@@ -1,6 +1,6 @@
import { api, host } from './config' import { api, host } from './config'
export function getSocialImageUrl(pageId: string) { export function getSocialImageUrl(pageId: string | undefined) {
try { try {
const url = new URL(api.getSocialImage, host) const url = new URL(api.getSocialImage, host)
@@ -8,7 +8,7 @@ export function getSocialImageUrl(pageId: string) {
url.searchParams.set('id', pageId) url.searchParams.set('id', pageId)
return url.toString() return url.toString()
} }
} catch (err) { } catch (err: any) {
console.warn('error invalid social image url', pageId, err.message) 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) { if (cachedTweet || cachedTweet === null) {
return cachedTweet return cachedTweet
} }
} catch (err) { } catch (err: any) {
// ignore redis errors // ignore redis errors
console.warn(`redis error get "${cacheKey}"`, err.message) console.warn(`redis error get "${cacheKey}"`, err.message)
} }
@@ -47,7 +47,7 @@ async function getTweetImpl(tweetId: string): Promise<any> {
try { try {
await db.set(cacheKey, tweetData) await db.set(cacheKey, tweetData)
} catch (err) { } catch (err: any) {
// ignore redis errors // ignore redis errors
console.warn(`redis error set "${cacheKey}"`, err.message) console.warn(`redis error set "${cacheKey}"`, err.message)
} }

View File

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

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ export async function getPreviewImageMap(
const urls: string[] = getPageImageUrls(recordMap, { const urls: string[] = getPageImageUrls(recordMap, {
mapImageUrl mapImageUrl
}) })
.concat([defaultPageIcon, defaultPageCover]) .concat([defaultPageIcon, defaultPageCover].filter(Boolean))
.filter(Boolean) .filter(Boolean)
const previewImagesMap = Object.fromEntries( const previewImagesMap = Object.fromEntries(
@@ -48,7 +48,7 @@ async function createPreviewImage(
if (cachedPreviewImage) { if (cachedPreviewImage) {
return cachedPreviewImage return cachedPreviewImage
} }
} catch (err) { } catch (err: any) {
// ignore redis errors // ignore redis errors
console.warn(`redis error get "${cacheKey}"`, err.message) console.warn(`redis error get "${cacheKey}"`, err.message)
} }
@@ -65,13 +65,13 @@ async function createPreviewImage(
try { try {
await db.set(cacheKey, previewImage) await db.set(cacheKey, previewImage)
} catch (err) { } catch (err: any) {
// ignore redis errors // ignore redis errors
console.warn(`redis error set "${cacheKey}"`, err.message) console.warn(`redis error set "${cacheKey}"`, err.message)
} }
return previewImage return previewImage
} catch (err) { } catch (err: any) {
console.warn('failed to create preview image', url, err.message) console.warn('failed to create preview image', url, err.message)
return null 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 { type ExtendedRecordMap } from 'notion-types'
import { parsePageId } from 'notion-utils' import { parsePageId } from 'notion-utils'
import type { PageProps } from './types'
import * as acl from './acl' import * as acl from './acl'
import { environment, pageUrlAdditions, pageUrlOverrides, site } from './config' import { environment, pageUrlAdditions, pageUrlOverrides, site } from './config'
import { db } from './db' import { db } from './db'
import { getSiteMap } from './get-site-map' import { getSiteMap } from './get-site-map'
import { getPage } from './notion' import { getPage } from './notion'
export async function resolveNotionPage(domain: string, rawPageId?: string) { export async function resolveNotionPage(
let pageId: string domain: string,
rawPageId?: string
): Promise<PageProps> {
let pageId: string | undefined
let recordMap: ExtendedRecordMap let recordMap: ExtendedRecordMap
if (rawPageId && rawPageId !== 'index') { if (rawPageId && rawPageId !== 'index') {
pageId = parsePageId(rawPageId) pageId = parsePageId(rawPageId)!
if (!pageId) { if (!pageId) {
// check if the site configuration provides an override or a fallback for // 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] pageUrlOverrides[rawPageId] || pageUrlAdditions[rawPageId]
if (override) { 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) pageId = await db.get(cacheKey)
// console.log(`redis get "${cacheKey}"`, pageId) // console.log(`redis get "${cacheKey}"`, pageId)
} catch (err) { } catch (err: any) {
// ignore redis errors // ignore redis errors
console.warn(`redis error get "${cacheKey}"`, err.message) 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) await db.set(cacheKey, pageId, cacheTTL)
// console.log(`redis set "${cacheKey}"`, pageId, { cacheTTL }) // console.log(`redis set "${cacheKey}"`, pageId, { cacheTTL })
} catch (err) { } catch (err: any) {
// ignore redis errors // ignore redis errors
console.warn(`redis error set "${cacheKey}"`, err.message) console.warn(`redis error set "${cacheKey}"`, err.message)
} }
@@ -86,6 +90,6 @@ export async function resolveNotionPage(domain: string, rawPageId?: string) {
recordMap = await getPage(pageId) recordMap = await getPage(pageId)
} }
const props = { site, recordMap, pageId } const props: PageProps = { site, recordMap, pageId }
return { ...props, ...(await acl.pageAcl(props)) } return { ...props, ...(await acl.pageAcl(props)) }
} }

View File

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

View File

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

View File

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

2
next-env.d.ts vendored
View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
// NOTE: This file should not be edited // 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 path from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import bundleAnalyzer from '@next/bundle-analyzer'
const withBundleAnalyzer = bundleAnalyzer({ const withBundleAnalyzer = bundleAnalyzer({
// eslint-disable-next-line no-process-env
enabled: process.env.ANALYZE === 'true' enabled: process.env.ANALYZE === 'true'
}) })
@@ -22,7 +24,7 @@ export default withBundleAnalyzer({
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;" contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;"
}, },
webpack: (config, _context) => { webpack: (config) => {
// Workaround for ensuring that `react` and `react-dom` resolve correctly // Workaround for ensuring that `react` and `react-dom` resolve correctly
// when using a locally-linked version of `react-notion-x`. // when using a locally-linked version of `react-notion-x`.
// @see https://github.com/vercel/next.js/issues/50391 // @see https://github.com/vercel/next.js/issues/50391

View File

@@ -6,6 +6,7 @@
"author": "Travis Fischer <travis@transitivebullsh.it>", "author": "Travis Fischer <travis@transitivebullsh.it>",
"repository": "transitive-bullshit/nextjs-notion-starter-kit", "repository": "transitive-bullshit/nextjs-notion-starter-kit",
"license": "MIT", "license": "MIT",
"packageManager": "pnpm@10.11.1",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@@ -37,38 +38,51 @@
"classnames": "^2.5.1", "classnames": "^2.5.1",
"expiry-map": "^2.0.0", "expiry-map": "^2.0.0",
"fathom-client": "^3.4.1", "fathom-client": "^3.4.1",
"ky": "^1.7.2", "ky": "^1.8.1",
"lqip-modern": "^2.2.1", "lqip-modern": "^2.2.1",
"next": "^15.0.3", "next": "^15.3.3",
"notion-client": "^7.1.3", "notion-client": "^7.4.0",
"notion-types": "^7.1.3", "notion-types": "^7.4.0",
"notion-utils": "^7.1.3", "notion-utils": "^7.4.0",
"p-map": "^7.0.2", "p-map": "^7.0.3",
"p-memoize": "^7.1.1", "p-memoize": "^7.1.1",
"posthog-js": "^1.181.0", "posthog-js": "^1.249.4",
"prismjs": "^1.29.0", "prismjs": "^1.30.0",
"react": "^18.2.0", "react": "^19.1.0",
"react-body-classname": "^1.3.1", "react-body-classname": "^1.3.1",
"react-dom": "^18.2.0", "react-dom": "^19.1.0",
"react-notion-x": "^7.2.3", "react-notion-x": "^7.4.0",
"react-tweet": "^3.2.1", "react-tweet": "^3.2.2",
"react-use": "^17.4.2", "react-use": "^17.6.0",
"rss": "^1.2.2" "rss": "^1.2.2"
}, },
"devDependencies": { "devDependencies": {
"@fisch0920/eslint-config": "^1.4.0", "@fisch0920/config": "^1.1.2",
"@next/bundle-analyzer": "^15.0.2", "@next/bundle-analyzer": "^15.3.3",
"@types/node": "^22.8.6", "@types/node": "^22.15.30",
"@types/react": "^18.0.21", "@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", "cross-env": "^7.0.2",
"eslint": "^8.57.1", "eslint": "^9.28.0",
"npm-run-all2": "^7.0.1", "npm-run-all2": "^8.0.4",
"prettier": "^3.3.3", "prettier": "^3.5.3",
"typescript": "^5.6.3" "typescript": "^5.8.3"
}, },
"overrides": { "overrides": {
"cacheable-request": { "cacheable-request": {
"keyv": "npm:@keyvhq/core@~1.6.6" "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 ( export const getStaticProps: GetStaticProps<PageProps, Params> = async (
context context
) => { ) => {
const rawPageId = context.params.pageId as string const rawPageId = context.params?.pageId as string
try { try {
const props = await resolveNotionPage(domain, rawPageId) const props = await resolveNotionPage(domain, rawPageId)
@@ -48,6 +48,6 @@ export async function getStaticPaths() {
return staticPaths return staticPaths
} }
export default function NotionDomainDynamicPage(props) { export default function NotionDomainDynamicPage(props: PageProps) {
return <NotionPage {...props} /> return <NotionPage {...props} />
} }

View File

@@ -16,7 +16,7 @@ import 'styles/prism-theme.css'
import type { AppProps } from 'next/app' import type { AppProps } from 'next/app'
import * as Fathom from 'fathom-client' import * as Fathom from 'fathom-client'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import posthog from 'posthog-js' import { posthog } from 'posthog-js'
import * as React from 'react' import * as React from 'react'
import { bootstrap } from '@/lib/bootstrap-client' 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' import Document, { Head, Html, Main, NextScript } from 'next/document'
export default class MyDocument extends Document { export default class MyDocument extends Document {
render() { override render() {
return ( return (
<IconContext.Provider value={{ style: { verticalAlign: 'middle' } }}> <IconContext.Provider value={{ style: { verticalAlign: 'middle' } }}>
<Html lang='en'> <Html lang='en'>

View File

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

View File

@@ -1,3 +1,4 @@
import type { PageProps } from '@/lib/types'
import { NotionPage } from '@/components/NotionPage' import { NotionPage } from '@/components/NotionPage'
import { domain } from '@/lib/config' import { domain } from '@/lib/config'
import { resolveNotionPage } from '@/lib/resolve-notion-page' 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} /> 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": { "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": ".", "baseUrl": ".",
"typeRoots": ["./node_modules/@types"],
"incremental": true,
"paths": { "paths": {
"@/components/*": ["components/*"], "@/components/*": ["components/*"],
"@/lib/*": ["lib/*"], "@/lib/*": ["lib/*"],
"@/styles/*": ["styles/*"] "@/styles/*": ["styles/*"]
} }
}, },
"exclude": ["node_modules"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "*.config.ts"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "site.config.ts"] "exclude": ["node_modules"]
} }