fix: locally linked

This commit is contained in:
Travis Fischer
2024-10-31 20:49:33 -05:00
parent 0e40674e88
commit f6510ea9a1
51 changed files with 4744 additions and 4161 deletions

View File

@@ -1,27 +1,17 @@
{ {
"root": true, "root": true,
"parser": "@typescript-eslint/parser", "extends": ["@fisch0920/eslint-config"],
"plugins": ["@typescript-eslint", "react", "react-hooks"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"prettier"
],
"settings": {
"react": {
"version": "detect"
}
},
"env": {
"browser": true,
"node": true
},
"rules": { "rules": {
"@typescript-eslint/no-explicit-any": 0, "react/prop-types": "off",
"@typescript-eslint/no-non-null-assertion": 0, "unicorn/no-array-reduce": "off",
"@typescript-eslint/no-unused-vars": 2, "unicorn/filename-case": "off",
"react/prop-types": 0 "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

@@ -7,14 +7,5 @@
"bracketSpacing": true, "bracketSpacing": true,
"bracketSameLine": false, "bracketSameLine": false,
"arrowParens": "always", "arrowParens": "always",
"trailingComma": "none", "trailingComma": "none"
"importOrder": [
"^(react/(.*)$)|^(react$)|^(next/(.*)$)|^(next$)",
"<THIRD_PARTY_MODULES>",
"^(@/lib/(.*)$)|^(@/components/(.*)$)|^(@/styles/(.*)$)",
"^[./]"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"importOrderGroupNamespaceSpecifiers": true
} }

1
.vscode/launch.json vendored
View File

@@ -8,7 +8,6 @@
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/next", "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/next",
"runtimeArgs": ["dev"], "runtimeArgs": ["dev"],
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"port": 9229,
"smartStep": true, "smartStep": true,
"console": "integratedTerminal", "console": "integratedTerminal",
"skipFiles": ["<node_internals>/**"], "skipFiles": ["<node_internals>/**"],

View File

@@ -1,9 +1,7 @@
import * as React from 'react'
import { PageHead } from './PageHead' import { PageHead } from './PageHead'
import styles from './styles.module.css' import styles from './styles.module.css'
export const ErrorPage: React.FC<{ statusCode: number }> = ({ statusCode }) => { export function ErrorPage({ statusCode }: { statusCode: number }) {
const title = 'Error' const title = 'Error'
return ( return (

View File

@@ -1,5 +1,3 @@
import * as React from 'react'
import { FaEnvelopeOpenText } from '@react-icons/all-files/fa/FaEnvelopeOpenText' import { FaEnvelopeOpenText } from '@react-icons/all-files/fa/FaEnvelopeOpenText'
import { FaGithub } from '@react-icons/all-files/fa/FaGithub' import { FaGithub } from '@react-icons/all-files/fa/FaGithub'
import { FaLinkedin } from '@react-icons/all-files/fa/FaLinkedin' import { FaLinkedin } from '@react-icons/all-files/fa/FaLinkedin'
@@ -9,6 +7,7 @@ import { FaYoutube } from '@react-icons/all-files/fa/FaYoutube'
import { FaZhihu } from '@react-icons/all-files/fa/FaZhihu' import { FaZhihu } from '@react-icons/all-files/fa/FaZhihu'
import { IoMoonSharp } from '@react-icons/all-files/io5/IoMoonSharp' import { IoMoonSharp } from '@react-icons/all-files/io5/IoMoonSharp'
import { IoSunnyOutline } from '@react-icons/all-files/io5/IoSunnyOutline' import { IoSunnyOutline } from '@react-icons/all-files/io5/IoSunnyOutline'
import * as React from 'react'
import * as config from '@/lib/config' import * as config from '@/lib/config'
import { useDarkMode } from '@/lib/use-dark-mode' import { useDarkMode } from '@/lib/use-dark-mode'
@@ -17,7 +16,7 @@ import styles from './styles.module.css'
// TODO: merge the data and icons from PageSocial with the social links in Footer // TODO: merge the data and icons from PageSocial with the social links in Footer
export const FooterImpl: React.FC = () => { export function FooterImpl() {
const [hasMounted, setHasMounted] = React.useState(false) const [hasMounted, setHasMounted] = React.useState(false)
const { isDarkMode, toggleDarkMode } = useDarkMode() const { isDarkMode, toggleDarkMode } = useDarkMode()
const currentYear = new Date().getFullYear() const currentYear = new Date().getFullYear()
@@ -36,7 +35,9 @@ export const FooterImpl: React.FC = () => {
return ( return (
<footer className={styles.footer}> <footer className={styles.footer}>
<div className={styles.copyright}>Copyright {currentYear} {config.author}</div> <div className={styles.copyright}>
Copyright {currentYear} {config.author}
</div>
<div className={styles.settings}> <div className={styles.settings}>
{hasMounted && ( {hasMounted && (

View File

@@ -1,8 +1,6 @@
import * as React from 'react'
import styles from './styles.module.css' import styles from './styles.module.css'
export const GitHubShareButton: React.FC = () => { export function GitHubShareButton() {
return ( return (
<a <a
href='https://github.com/transitive-bullshit/nextjs-notion-starter-kit' href='https://github.com/transitive-bullshit/nextjs-notion-starter-kit'

View File

@@ -1,10 +1,10 @@
import * as React from 'react'
import { LoadingIcon } from './LoadingIcon' import { LoadingIcon } from './LoadingIcon'
import styles from './styles.module.css' import styles from './styles.module.css'
export const Loading: React.FC = () => ( export function Loading() {
<div className={styles.container}> return (
<LoadingIcon /> <div className={styles.container}>
</div> <LoadingIcon />
) </div>
)
}

View File

@@ -1,10 +1,8 @@
import * as React from 'react'
import cs from 'classnames' import cs from 'classnames'
import styles from './styles.module.css' import styles from './styles.module.css'
export const LoadingIcon = (props) => { export function LoadingIcon(props: any) {
const { className, ...rest } = props const { className, ...rest } = props
return ( return (
<svg <svg

View File

@@ -1,19 +1,18 @@
import * as React from 'react' import cs from 'classnames'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import Image from 'next/image' import Image from 'next/legacy/image'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { type PageBlock } from 'notion-types'
import cs from 'classnames'
import { PageBlock } from 'notion-types'
import { formatDate, getBlockTitle, getPageProperty } from 'notion-utils' import { formatDate, getBlockTitle, getPageProperty } from 'notion-utils'
import * as React from 'react'
import BodyClassName from 'react-body-classname' import BodyClassName from 'react-body-classname'
import { NotionRenderer } from 'react-notion-x' import { NotionRenderer } from 'react-notion-x'
import TweetEmbed from 'react-tweet-embed' import TweetEmbed from 'react-tweet-embed'
import { useSearchParam } from 'react-use' import { useSearchParam } from 'react-use'
import type * as types from '@/lib/types'
import * as config from '@/lib/config' import * as config from '@/lib/config'
import * as types from '@/lib/types'
import { mapImageUrl } from '@/lib/map-image-url' import { mapImageUrl } from '@/lib/map-image-url'
import { getCanonicalPageUrl, mapPageUrl } from '@/lib/map-page-url' import { getCanonicalPageUrl, mapPageUrl } from '@/lib/map-page-url'
import { searchNotion } from '@/lib/search-notion' import { searchNotion } from '@/lib/search-notion'
@@ -97,7 +96,7 @@ const Modal = dynamic(
} }
) )
const Tweet = ({ id }: { id: string }) => { function Tweet({ id }: { id: string }) {
return <TweetEmbed tweetId={id} /> return <TweetEmbed tweetId={id} />
} }
@@ -142,12 +141,12 @@ const propertyTextValue = (
return defaultFn() return defaultFn()
} }
export const NotionPage: React.FC<types.PageProps> = ({ export function NotionPage({
site, site,
recordMap, recordMap,
error, error,
pageId pageId
}) => { }: types.PageProps) {
const router = useRouter() const router = useRouter()
const lite = useSearchParam('lite') const lite = useSearchParam('lite')

View File

@@ -1,9 +1,8 @@
import * as React from 'react' import type * as types from 'notion-types'
import * as types from 'notion-types'
import { IoMoonSharp } from '@react-icons/all-files/io5/IoMoonSharp' import { IoMoonSharp } from '@react-icons/all-files/io5/IoMoonSharp'
import { IoSunnyOutline } from '@react-icons/all-files/io5/IoSunnyOutline' import { IoSunnyOutline } from '@react-icons/all-files/io5/IoSunnyOutline'
import cs from 'classnames' import cs from 'classnames'
import * as React from 'react'
import { Breadcrumbs, Header, Search, useNotionContext } from 'react-notion-x' import { Breadcrumbs, Header, Search, useNotionContext } from 'react-notion-x'
import { isSearchEnabled, navigationLinks, navigationStyle } from '@/lib/config' import { isSearchEnabled, navigationLinks, navigationStyle } from '@/lib/config'
@@ -11,7 +10,7 @@ import { useDarkMode } from '@/lib/use-dark-mode'
import styles from './styles.module.css' import styles from './styles.module.css'
const ToggleThemeButton = () => { function ToggleThemeButton() {
const [hasMounted, setHasMounted] = React.useState(false) const [hasMounted, setHasMounted] = React.useState(false)
const { isDarkMode, toggleDarkMode } = useDarkMode() const { isDarkMode, toggleDarkMode } = useDarkMode()
@@ -33,9 +32,11 @@ const ToggleThemeButton = () => {
) )
} }
export const NotionPageHeader: React.FC<{ export function NotionPageHeader({
block
}: {
block: types.CollectionViewPageBlock | types.PageBlock block: types.CollectionViewPageBlock | types.PageBlock
}> = ({ block }) => { }) {
const { components, mapPageUrl } = useNotionContext() const { components, mapPageUrl } = useNotionContext()
if (navigationStyle === 'default') { if (navigationStyle === 'default') {

View File

@@ -1,11 +1,9 @@
import * as React from 'react' import type * as types from '@/lib/types'
import * as types from '@/lib/types'
import { PageHead } from './PageHead' import { PageHead } from './PageHead'
import styles from './styles.module.css' import styles from './styles.module.css'
export const Page404: React.FC<types.PageProps> = ({ site, pageId, error }) => { export function Page404({ site, pageId, error }: types.PageProps) {
const title = site?.name || 'Notion Page Not Found' const title = site?.name || 'Notion Page Not Found'
return ( return (

View File

@@ -1,5 +1,3 @@
import * as React from 'react'
import { AiOutlineRetweet } from '@react-icons/all-files/ai/AiOutlineRetweet' import { AiOutlineRetweet } from '@react-icons/all-files/ai/AiOutlineRetweet'
import { IoHeartOutline } from '@react-icons/all-files/io5/IoHeartOutline' import { IoHeartOutline } from '@react-icons/all-files/io5/IoHeartOutline'
@@ -8,7 +6,7 @@ import styles from './styles.module.css'
/** /**
* @see https://developer.twitter.com/en/docs/twitter-for-websites/web-intents/overview * @see https://developer.twitter.com/en/docs/twitter-for-websites/web-intents/overview
*/ */
export const PageActions: React.FC<{ tweet: string }> = ({ tweet }) => { export function PageActions({ tweet }: { tweet: string }) {
return ( return (
<div className={styles.pageActions}> <div className={styles.pageActions}>
<a <a

View File

@@ -1,17 +1,19 @@
import * as React from 'react' import { type Block, type ExtendedRecordMap } from 'notion-types'
import { Block, ExtendedRecordMap } from 'notion-types'
import { getPageTweet } from '@/lib/get-page-tweet' import { getPageTweet } from '@/lib/get-page-tweet'
import { PageActions } from './PageActions' import { PageActions } from './PageActions'
import { PageSocial } from './PageSocial' import { PageSocial } from './PageSocial'
export const PageAside: React.FC<{ export function PageAside({
block,
recordMap,
isBlogPost
}: {
block: Block block: Block
recordMap: ExtendedRecordMap recordMap: ExtendedRecordMap
isBlogPost: boolean isBlogPost: boolean
}> = ({ block, recordMap, isBlogPost }) => { }) {
if (!block) { if (!block) {
return null return null
} }

View File

@@ -1,18 +1,22 @@
import * as React from 'react'
import Head from 'next/head' import Head from 'next/head'
import type * as types from '@/lib/types'
import * as config from '@/lib/config' import * as config from '@/lib/config'
import * as types from '@/lib/types'
import { getSocialImageUrl } from '@/lib/get-social-image-url' import { getSocialImageUrl } from '@/lib/get-social-image-url'
export const PageHead: React.FC< export function PageHead({
types.PageProps & { site,
title?: string title,
description?: string description,
image?: string pageId,
url?: string image,
} url
> = ({ site, title, description, pageId, image, url }) => { }: types.PageProps & {
title?: string
description?: string
image?: string
url?: string
}) {
const rssFeedUrl = `${config.host}/feed` const rssFeedUrl = `${config.host}/feed`
title = title ?? site?.name title = title ?? site?.name
@@ -30,13 +34,20 @@ export const PageHead: React.FC<
/> />
<meta name='apple-mobile-web-app-capable' content='yes' /> <meta name='apple-mobile-web-app-capable' content='yes' />
<meta <meta name='apple-mobile-web-app-status-bar-style' content='black' />
name='apple-mobile-web-app-status-bar-style'
content='black'
/>
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fefffe" key="theme-color-light"/> <meta
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#2d3439" key="theme-color-dark"/> name='theme-color'
media='(prefers-color-scheme: light)'
content='#fefffe'
key='theme-color-light'
/>
<meta
name='theme-color'
media='(prefers-color-scheme: dark)'
content='#2d3439'
key='theme-color-dark'
/>
<meta name='robots' content='index,follow' /> <meta name='robots' content='index,follow' />
<meta property='og:type' content='website' /> <meta property='og:type' content='website' />

View File

@@ -101,10 +101,10 @@
} }
.youtube .actionBgPane { .youtube .actionBgPane {
background: #FF0000; background: #ff0000;
} }
.youtube:hover { .youtube:hover {
border-color: #FF0000; border-color: #ff0000;
} }
.medium .actionBgPane { .medium .actionBgPane {

View File

@@ -1,5 +1,4 @@
import * as React from 'react' import type * as React from 'react'
import cs from 'classnames' import cs from 'classnames'
import * as config from '@/lib/config' import * as config from '@/lib/config'
@@ -70,7 +69,7 @@ const socialLinks: SocialLink[] = [
} }
].filter(Boolean) ].filter(Boolean)
export const PageSocial: React.FC = () => { export function PageSocial() {
return ( return (
<div className={styles.pageSocial}> <div className={styles.pageSocial}>
{socialLinks.map((action) => ( {socialLinks.map((action) => (

View File

@@ -4,20 +4,20 @@ Suggestions and pull requests are highly encouraged. Have a look at the [open is
## Development ## Development
To develop the project locally, you'll need a recent version of Node.js and `yarn` v1 installed globally. To develop the project locally, you'll need a recent version of Node.js and `pnpm` installed globally.
To get started, clone the repo and run `yarn` from the root directory: To get started, clone the repo and run `pnpm` from the root directory:
```bash ```bash
git clone https://github.com/transitive-bullshit/nextjs-notion-starter-kit git clone https://github.com/transitive-bullshit/nextjs-notion-starter-kit
cd nextjs-notion-starter-kit cd nextjs-notion-starter-kit
yarn pnpm
``` ```
Now that your dependencies are installed, you can run the local Next.js dev server: Now that your dependencies are installed, you can run the local Next.js dev server:
```bash ```bash
yarn dev pnpm dev
``` ```
You should now be able to open `http://localhost:3000` to view the webapp. You should now be able to open `http://localhost:3000` to view the webapp.
@@ -27,7 +27,7 @@ You should now be able to open `http://localhost:3000` to view the webapp.
To build for production, you can run: To build for production, you can run:
```bash ```bash
yarn build pnpm build
``` ```
Which just runs `next build` under the hood. Which just runs `next build` under the hood.
@@ -36,48 +36,18 @@ Which just runs `next build` under the hood.
If you are making changes to `react-notion-x` and want to test them out with `nextjs-notion-starter-kit`, you'll first need to [set up and build `react-notion-x` locally](https://github.com/NotionX/react-notion-x/blob/master/contributing.md). If you are making changes to `react-notion-x` and want to test them out with `nextjs-notion-starter-kit`, you'll first need to [set up and build `react-notion-x` locally](https://github.com/NotionX/react-notion-x/blob/master/contributing.md).
Once you have `react-notion-x` set up locally, run `yarn link` from each `react-notion-x` package: Once you have `react-notion-x` set up and built locally, you can link these local deps into `nextjs-notion-starter-kit`:
```bash ```bash
# from react-notion-x clone pnpm deps:link
cd packages/react-notion-x
yarn link
cd ../packages/notion-utils
yarn link
cd ../packages/notion-types
yarn link
cd ../packages/notion-client
yarn link
``` ```
Now you can link these local deps into `nextjs-notion-starter-kit`: With this setup, in one tab, you can run `pnpm dev` to keep `react-notion-x` up-to-date, and in another tab, you can run `pnpm dev` to keep `nextjs-notion-starter-kit` up-to-date.
```bash
# from nextjs-notion-starter-kit
yarn deps:link
```
The last step is to make sure that the Next.js project and these local dependencies are all pointing to the same versions of `react` and `react-dom`.
```bash
# from react-notion-x clone
cd node_modules/react
yarn link
cd ../react-dom
yarn link
```
```bash
# from nextjs-notion-starter-kit
yarn link react react-dom
```
With this setup, in one tab, you can run `yarn dev` to keep `react-notion-x` up-to-date, and in another tab, you can run `yarn dev` to keep `nextjs-notion-starter-kit` up-to-date.
### Gotchas ### Gotchas
Whenever you make a change to one of the `react-notion-x` packages, it will automatically be recompiled into its respective `build` folder, and the `yarn dev` from `nextjs-notion-starter-kit` should hot-reload it in the browser. Whenever you make a change to one of the `react-notion-x` packages, it will automatically be recompiled into its respective `build` folder, and the `pnpm dev` from `nextjs-notion-starter-kit` should hot-reload it in the browser.
Sometimes, this process gets a little out of whack, and if you're not sure what's going on, I usually just quit one or both of the `yarn dev` commands and restart them. Sometimes, this process gets a little out of whack, and if you're not sure what's going on, I usually just quit one or both of the `pnpm dev` commands and restart them.
If you're seeing something unexpected while debugging with Next.js, try running `rm -rf .next` to refresh the Next.js cache before running `yarn dev` again. If you're seeing something unexpected while debugging with Next.js, try running `rm -rf .next` to refresh the Next.js cache before running `pnpm dev` again.

View File

@@ -1,4 +1,4 @@
import { PageProps } from './types' import { type PageProps } from './types'
export async function pageAcl({ export async function pageAcl({
site, site,

View File

@@ -7,7 +7,7 @@ export function bootstrap() {
██║ ██╔══██╗██╔══██║██║╚██╗██║╚════██║██║ ██║ ██║╚██╗ ██╔╝██╔══╝ ██╔══██╗╚════██║ ██║ ██╔══██╗██╔══██║██║╚██╗██║╚════██║██║ ██║ ██║╚██╗ ██╔╝██╔══╝ ██╔══██╗╚════██║
██║ ██║ ██║██║ ██║██║ ╚████║███████║██║ ██║ ██║ ╚████╔╝ ███████╗ ██████╔╝███████║ ██║ ██║ ██║██║ ██║██║ ╚████║███████║██║ ██║ ██║ ╚████╔╝ ███████╗ ██████╔╝███████║
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═══╝ ╚══════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═══╝ ╚══════╝ ╚═════╝ ╚══════╝
This site is built using Notion, Next.js, and https://github.com/NotionX/react-notion-x. This site is built using Notion, Next.js, and https://github.com/NotionX/react-notion-x.
`) `)
} }

View File

@@ -5,15 +5,15 @@
* for optional depenencies. * for optional depenencies.
*/ */
import { parsePageId } from 'notion-utils' import { parsePageId } from 'notion-utils'
import { PostHogConfig } from 'posthog-js' import { type PostHogConfig } from 'posthog-js'
import { getEnv, getSiteConfig } from './get-config-value' import { getEnv, getSiteConfig } from './get-config-value'
import { NavigationLink } from './site-config' import { type NavigationLink } from './site-config'
import { import {
NavigationStyle, type NavigationStyle,
PageUrlOverridesInverseMap, type PageUrlOverridesInverseMap,
PageUrlOverridesMap, type PageUrlOverridesMap,
Site type Site
} from './types' } from './types'
export const rootNotionPageId: string = parsePageId( export const rootNotionPageId: string = parsePageId(

View File

@@ -1,4 +1,4 @@
import { ExtendedRecordMap } from 'notion-types' import { type ExtendedRecordMap } from 'notion-types'
import { import {
getCanonicalPageId as getCanonicalPageIdImpl, getCanonicalPageId as getCanonicalPageIdImpl,
parsePageId parsePageId

View File

@@ -1,5 +1,5 @@
import rawSiteConfig from '../site.config' import rawSiteConfig from '../site.config'
import { SiteConfig } from './site-config' import { type SiteConfig } from './site-config'
if (!rawSiteConfig) { if (!rawSiteConfig) {
throw new Error(`Config error: invalid site.config.ts`) throw new Error(`Config error: invalid site.config.ts`)

View File

@@ -1,6 +1,6 @@
import { getPageProperty } from 'notion-utils' import { getPageProperty } from 'notion-utils'
import * as types from './types' import type * as types from './types'
export function getPageTweet( export function getPageTweet(
block: types.Block, block: types.Block,

View File

@@ -1,8 +1,8 @@
import { getAllPagesInSpace, uuidToId, getPageProperty } from 'notion-utils' import { getAllPagesInSpace, getPageProperty, uuidToId } from 'notion-utils'
import pMemoize from 'p-memoize' import pMemoize from 'p-memoize'
import type * as types from './types'
import * as config from './config' import * as config from './config'
import * as types from './types'
import { includeNotionIdInUrls } from './config' import { includeNotionIdInUrls } from './config'
import { getCanonicalPageId } from './get-canonical-page-id' import { getCanonicalPageId } from './get-canonical-page-id'
import { notion } from './notion-api' import { notion } from './notion-api'
@@ -25,15 +25,15 @@ const getAllPages = pMemoize(getAllPagesImpl, {
cacheKey: (...args) => JSON.stringify(args) cacheKey: (...args) => JSON.stringify(args)
}) })
const getPage = async (pageId: string, ...args) => {
console.log('\nnotion getPage', uuidToId(pageId))
return notion.getPage(pageId, ...args)
}
async function getAllPagesImpl( async function getAllPagesImpl(
rootNotionPageId: string, rootNotionPageId: string,
rootNotionSpaceId: string rootNotionSpaceId: string
): Promise<Partial<types.SiteMap>> { ): Promise<Partial<types.SiteMap>> {
const getPage = async (pageId: string, ...args) => {
console.log('\nnotion getPage', uuidToId(pageId))
return notion.getPage(pageId, ...args)
}
const pageMap = await getAllPagesInSpace( const pageMap = await getAllPagesInSpace(
rootNotionPageId, rootNotionPageId,
rootNotionSpaceId, rootNotionSpaceId,
@@ -48,7 +48,9 @@ async function getAllPagesImpl(
} }
const block = recordMap.block[pageId]?.value const block = recordMap.block[pageId]?.value
if (!(getPageProperty<boolean|null>('Public', block, recordMap) ?? true)) { if (
!(getPageProperty<boolean | null>('Public', block, recordMap) ?? true)
) {
return map return map
} }

View File

@@ -1,9 +1,9 @@
import { Block } from 'notion-types' import { type Block } from 'notion-types'
import { defaultMapImageUrl } from 'react-notion-x' import { defaultMapImageUrl } from 'react-notion-x'
import { defaultPageCover, defaultPageIcon } from './config' import { defaultPageCover, defaultPageIcon } from './config'
export const mapImageUrl = (url: string, block: Block) => { export const mapImageUrl = (url: string | undefined, block: Block) => {
if (url === defaultPageCover || url === defaultPageIcon) { if (url === defaultPageCover || url === defaultPageIcon) {
return url return url
} }

View File

@@ -1,9 +1,9 @@
import { ExtendedRecordMap } from 'notion-types' import { type ExtendedRecordMap } from 'notion-types'
import { parsePageId, uuidToId } from 'notion-utils' import { parsePageId, uuidToId } from 'notion-utils'
import { includeNotionIdInUrls } from './config' import { includeNotionIdInUrls } from './config'
import { getCanonicalPageId } from './get-canonical-page-id' import { getCanonicalPageId } from './get-canonical-page-id'
import { Site } from './types' import { type Site } from './types'
// include UUIDs in page URLs during local development but not in production // include UUIDs in page URLs during local development but not in production
// (they're nice for debugging and speed up local dev) // (they're nice for debugging and speed up local dev)

View File

@@ -1,4 +1,8 @@
import { ExtendedRecordMap, SearchParams, SearchResults } from 'notion-types' import {
type ExtendedRecordMap,
type SearchParams,
type SearchResults
} from 'notion-types'
import { mergeRecordMaps } from 'notion-utils' import { mergeRecordMaps } from 'notion-utils'
import pMap from 'p-map' import pMap from 'p-map'
import pMemoize from 'p-memoize' import pMemoize from 'p-memoize'

View File

@@ -1,6 +1,10 @@
import got from 'got' import ky from 'ky'
import lqip from 'lqip-modern' import lqip from 'lqip-modern'
import { ExtendedRecordMap, PreviewImage, PreviewImageMap } from 'notion-types' import {
type ExtendedRecordMap,
type PreviewImage,
type PreviewImageMap
} from 'notion-types'
import { getPageImageUrls, normalizeUrl } from 'notion-utils' import { getPageImageUrls, normalizeUrl } from 'notion-utils'
import pMap from 'p-map' import pMap from 'p-map'
import pMemoize from 'p-memoize' import pMemoize from 'p-memoize'
@@ -49,7 +53,7 @@ async function createPreviewImage(
console.warn(`redis error get "${cacheKey}"`, err.message) console.warn(`redis error get "${cacheKey}"`, err.message)
} }
const { body } = await got(url, { responseType: 'buffer' }) const body = await ky(url).arrayBuffer()
const result = await lqip(body) const result = await lqip(body)
console.log('lqip', { ...result.metadata, url, cacheKey }) console.log('lqip', { ...result.metadata, url, cacheKey })

View File

@@ -1,4 +1,4 @@
import { ExtendedRecordMap } from 'notion-types' import { type ExtendedRecordMap } from 'notion-types'
import { parsePageId } from 'notion-utils' import { parsePageId } from 'notion-utils'
import * as acl from './acl' import * as acl from './acl'

View File

@@ -1,14 +1,12 @@
// import ky from 'ky'
import ExpiryMap from 'expiry-map' import ExpiryMap from 'expiry-map'
import fetch from 'isomorphic-unfetch'
import pMemoize from 'p-memoize' import pMemoize from 'p-memoize'
import * as types from './types' import type * as types from './types'
import { api } from './config' import { api } from './config'
export const searchNotion = pMemoize(searchNotionImpl, { export const searchNotion = pMemoize(searchNotionImpl, {
cacheKey: (args) => args[0]?.query, cacheKey: (args) => args[0]?.query,
cache: new ExpiryMap(10000) cache: new ExpiryMap(10_000)
}) })
async function searchNotionImpl( async function searchNotionImpl(
@@ -29,7 +27,7 @@ async function searchNotionImpl(
// convert non-2xx HTTP responses into errors // convert non-2xx HTTP responses into errors
const error: any = new Error(res.statusText) const error: any = new Error(res.statusText)
error.response = res error.response = res
return Promise.reject(error) throw error
}) })
.then((res) => res.json()) .then((res) => res.json())

View File

@@ -1,4 +1,4 @@
import * as types from './types' import type * as types from './types'
export interface SiteConfig { export interface SiteConfig {
rootNotionPageId: string rootNotionPageId: string
@@ -16,7 +16,7 @@ export interface SiteConfig {
newsletter?: string newsletter?: string
youtube?: string youtube?: string
zhihu?: string zhihu?: string
mastodon?: string; mastodon?: string
defaultPageIcon?: string | null defaultPageIcon?: string | null
defaultPageCover?: string | null defaultPageCover?: string | null

View File

@@ -1,5 +1,6 @@
import { ExtendedRecordMap, PageMap } from 'notion-types' import { type ParsedUrlQuery } from 'node:querystring'
import { ParsedUrlQuery } from 'querystring'
import { type ExtendedRecordMap, type PageMap } from 'notion-types'
export * from 'notion-types' export * from 'notion-types'

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2023 Travis Fischer Copyright (c) 2024 Travis Fischer
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

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/basic-features/typescript for more information. // see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.

View File

@@ -1,22 +1,65 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires import bundleAnalyzer from '@next/bundle-analyzer'
const withBundleAnalyzer = require('@next/bundle-analyzer')({ import path from 'node:path'
import { fileURLToPath } from 'node:url'
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true' enabled: process.env.ANALYZE === 'true'
}) })
module.exports = withBundleAnalyzer({ export default withBundleAnalyzer({
staticPageGenerationTimeout: 300, staticPageGenerationTimeout: 300,
images: { images: {
domains: [ remotePatterns: [
'www.notion.so', {
'notion.so', protocol: 'https',
'images.unsplash.com', hostname: 'www.notion.so',
'pbs.twimg.com', pathname: '**'
'abs.twimg.com', },
's3.us-west-2.amazonaws.com', {
'transitivebullsh.it' protocol: 'https',
hostname: 'notion.so',
pathname: '**'
},
{
protocol: 'https',
hostname: 'images.unsplash.com',
pathname: '**'
},
{
protocol: 'https',
hostname: 'pbs.twimg.com',
pathname: '**'
},
{
protocol: 'https',
hostname: 'abs.twimg.com',
pathname: '**'
},
{
protocol: 'https',
hostname: 's3.us-west-2.amazonaws.com',
pathname: '**'
},
{
protocol: 'https',
hostname: 'transitivebullsh.it',
pathname: '**'
}
], ],
formats: ['image/avif', 'image/webp'], formats: ['image/avif', 'image/webp'],
dangerouslyAllowSVG: true, dangerouslyAllowSVG: true,
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;" contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;"
},
webpack: (config, _context) => {
// 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
const dirname = path.dirname(fileURLToPath(import.meta.url))
config.resolve.alias.react = path.resolve(dirname, 'node_modules/react')
config.resolve.alias['react-dom'] = path.resolve(
dirname,
'node_modules/react-dom'
)
return config
} }
}) })

View File

@@ -7,21 +7,25 @@
"repository": "transitive-bullshit/nextjs-notion-starter-kit", "repository": "transitive-bullshit/nextjs-notion-starter-kit",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=16" "node": ">=18"
}, },
"type": "module",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"deploy": "vercel deploy", "deploy": "vercel deploy",
"deps": "run-s deps:*", "deps:update": "[ -z $GITHUB_ACTIONS ] && pnpm up -L notion-client notion-types notion-utils react-notion-x || echo 'Skipping deps:update on CI'",
"deps:update": "[ -z $GITHUB_ACTIONS ] && yarn add notion-client notion-types notion-utils react-notion-x || echo 'Skipping deps:update on CI'", "deps:link": "[ -z $GITHUB_ACTIONS ] && run-s deps:link:* || echo 'Skipping deps:update on CI'",
"deps:link": "[ -z $GITHUB_ACTIONS ] && yarn link notion-client notion-types notion-utils react-notion-x || echo 'Skipping deps:link on CI'", "deps:link:notion-types": "pnpm link ../react-notion-x/packages/notion-types",
"deps:link:notion-utils": "pnpm link ../react-notion-x/packages/notion-utils",
"deps:link:notion-client": "pnpm link ../react-notion-x/packages/notion-client",
"deps:link:react-notion-x": "pnpm link ../react-notion-x/packages/react-notion-x",
"analyze": "cross-env ANALYZE=true next build", "analyze": "cross-env ANALYZE=true next build",
"analyze:server": "cross-env BUNDLE_ANALYZE=server next build", "analyze:server": "cross-env BUNDLE_ANALYZE=server next build",
"analyze:browser": "cross-env BUNDLE_ANALYZE=browser next build", "analyze:browser": "cross-env BUNDLE_ANALYZE=browser next build",
"test": "run-p test:*", "test": "run-p test:*",
"test:lint": "eslint '**/*.{ts,tsx}'", "test:lint": "eslint .",
"test:prettier": "prettier '**/*.{js,jsx,ts,tsx}' --check" "test:prettier": "prettier '**/*.{js,jsx,ts,tsx}' --check"
}, },
"dependencies": { "dependencies": {
@@ -29,21 +33,21 @@
"@keyvhq/core": "^1.6.9", "@keyvhq/core": "^1.6.9",
"@keyvhq/redis": "^1.6.10", "@keyvhq/redis": "^1.6.10",
"@react-icons/all-files": "^4.1.0", "@react-icons/all-files": "^4.1.0",
"@vercel/og": "^0.0.19", "@vercel/og": "^0.6.3",
"classnames": "^2.3.1", "classnames": "^2.5.1",
"date-fns": "^2.28.0", "date-fns": "^2.30.0",
"expiry-map": "^2.0.0", "expiry-map": "^2.0.0",
"fathom-client": "^3.4.1", "fathom-client": "^3.4.1",
"got": "^12.0.3", "ky": "^1.7.2",
"isomorphic-unfetch": "^3.1.0", "lqip-modern": "^2.1.0",
"lqip-modern": "^2.0.0", "next": "^15.0.2",
"next": "12",
"notion-client": "^6.15.6", "notion-client": "^6.15.6",
"notion-types": "^6.15.6", "notion-types": "^6.15.6",
"notion-utils": "^6.15.6", "notion-utils": "^6.15.6",
"p-map": "^5.3.0", "p-map": "^7.0.2",
"p-memoize": "^6.0.1", "p-memoize": "^7.1.1",
"posthog-js": "^1.20.2", "posthog-js": "^1.20.2",
"prismjs": "^1.29.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-body-classname": "^1.3.1", "react-body-classname": "^1.3.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@@ -53,20 +57,29 @@
"rss": "^1.2.2" "rss": "^1.2.2"
}, },
"devDependencies": { "devDependencies": {
"@next/bundle-analyzer": "^12.3.1", "@fisch0920/eslint-config": "^1.4.0",
"@trivago/prettier-plugin-sort-imports": "^3.3.1", "@next/bundle-analyzer": "^15.0.2",
"@types/node": "^18.8.5", "@types/node": "^22.8.6",
"@types/react": "^18.0.21", "@types/react": "^18.0.21",
"@typescript-eslint/eslint-plugin": "^5.40.0",
"@typescript-eslint/parser": "^5.40.0",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"eslint": "^8.25.0", "eslint": "^8.57.1",
"eslint-config-prettier": "^8.5.0", "npm-run-all2": "^7.0.1",
"eslint-plugin-react": "^7.31.10", "prettier": "^3.3.3",
"eslint-plugin-react-hooks": "^4.6.0", "typescript": "^5.6.3"
"npm-run-all": "^4.1.5", },
"prettier": "^2.7.1", "dependenciesMeta": {
"typescript": "^4.8.4" "notion-client": {
"injected": true
},
"notion-types": {
"injected": true
},
"notion-utils": {
"injected": true
},
"react-notion-x": {
"injected": true
}
}, },
"overrides": { "overrides": {
"cacheable-request": { "cacheable-request": {

View File

@@ -1,3 +1 @@
import { Page404 } from '@/components/Page404' export { Page404 as default } from '@/components/Page404'
export default Page404

View File

@@ -1,11 +1,10 @@
import * as React from 'react' import { type GetStaticProps } from 'next'
import { GetStaticProps } from 'next'
import { NotionPage } from '@/components/NotionPage' import { NotionPage } from '@/components/NotionPage'
import { domain, isDev } from '@/lib/config' import { domain, isDev } from '@/lib/config'
import { getSiteMap } from '@/lib/get-site-map' import { getSiteMap } from '@/lib/get-site-map'
import { resolveNotionPage } from '@/lib/resolve-notion-page' import { resolveNotionPage } from '@/lib/resolve-notion-page'
import { PageProps, Params } from '@/lib/types' import { type PageProps, type Params } from '@/lib/types'
export const getStaticProps: GetStaticProps<PageProps, Params> = async ( export const getStaticProps: GetStaticProps<PageProps, Params> = async (
context context

View File

@@ -1,16 +1,10 @@
// global styles shared across the entire site
import * as React from 'react'
import type { AppProps } from 'next/app'
import { useRouter } from 'next/router'
import * as Fathom from 'fathom-client'
// used for rendering equations (optional) // used for rendering equations (optional)
import 'katex/dist/katex.min.css' import 'katex/dist/katex.min.css'
import posthog from 'posthog-js'
// used for code syntax highlighting (optional) // used for code syntax highlighting (optional)
import 'prismjs/themes/prism-coy.css' import 'prismjs/themes/prism-coy.css'
// core styles shared by all of react-notion-x (required) // core styles shared by all of react-notion-x (required)
import 'react-notion-x/src/styles.css' import 'react-notion-x/src/styles.css'
// global styles shared across the entire site
import 'styles/global.css' import 'styles/global.css'
// this might be better for dark mode // this might be better for dark mode
// import 'prismjs/themes/prism-okaidia.css' // import 'prismjs/themes/prism-okaidia.css'
@@ -19,6 +13,12 @@ import 'styles/notion.css'
// global style overrides for prism theme (optional) // global style overrides for prism theme (optional)
import 'styles/prism-theme.css' 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 * as React from 'react'
import { bootstrap } from '@/lib/bootstrap-client' import { bootstrap } from '@/lib/bootstrap-client'
import { import {
fathomConfig, fathomConfig,

View File

@@ -1,7 +1,5 @@
import * as React from 'react'
import Document, { Head, Html, Main, NextScript } from 'next/document'
import { IconContext } from '@react-icons/all-files' import { IconContext } from '@react-icons/all-files'
import Document, { Head, Html, Main, NextScript } from 'next/document'
export default class MyDocument extends Document { export default class MyDocument extends Document {
render() { render() {

View File

@@ -1,3 +1 @@
import { ErrorPage } from '@/components/ErrorPage' export { ErrorPage as default } from '@/components/ErrorPage'
export default ErrorPage

View File

@@ -1,7 +1,6 @@
import { NextApiRequest, NextApiResponse } from 'next' import ky from 'ky'
import { type NextApiRequest, type NextApiResponse } from 'next'
import got from 'got' import { type PageBlock } from 'notion-types'
import { PageBlock } from 'notion-types'
import { import {
getBlockIcon, getBlockIcon,
getBlockTitle, getBlockTitle,
@@ -13,9 +12,12 @@ import {
import * as libConfig from '@/lib/config' import * as libConfig from '@/lib/config'
import { mapImageUrl } from '@/lib/map-image-url' import { mapImageUrl } from '@/lib/map-image-url'
import { notion } from '@/lib/notion-api' import { notion } from '@/lib/notion-api'
import { NotionPageInfo } from '@/lib/types' import { type NotionPageInfo } from '@/lib/types'
export default async (req: NextApiRequest, res: NextApiResponse) => { export default async function notionPageInfo(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') { if (req.method !== 'POST') {
return res.status(405).send({ error: 'method not allowed' }) return res.status(405).send({ error: 'method not allowed' })
} }
@@ -125,9 +127,9 @@ async function isUrlReachable(url: string | null): Promise<boolean> {
} }
try { try {
await got.head(url) await ky.head(url)
return true return true
} catch (err) { } catch {
return false return false
} }
} }

View File

@@ -1,9 +1,12 @@
import { NextApiRequest, NextApiResponse } from 'next' import { type NextApiRequest, type NextApiResponse } from 'next'
import * as types from '../../lib/types' import type * as types from '../../lib/types'
import { search } from '../../lib/notion' import { search } from '../../lib/notion'
export default async (req: NextApiRequest, res: NextApiResponse) => { export default async function searchNotion(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') { if (req.method !== 'POST') {
return res.status(405).send({ error: 'method not allowed' }) return res.status(405).send({ error: 'method not allowed' })
} }

View File

@@ -1,18 +1,17 @@
import * as React from 'react'
import { NextRequest } from 'next/server'
import { ImageResponse } from '@vercel/og' import { ImageResponse } from '@vercel/og'
import ky from 'ky'
import { type NextRequest } from 'next/server'
import { api, apiHost, rootNotionPageId } from '@/lib/config' import { api, apiHost, rootNotionPageId } from '@/lib/config'
import { NotionPageInfo } from '@/lib/types' import { type NotionPageInfo } from '@/lib/types'
const interRegularFontP = fetch( const interRegularFontP = ky(
new URL('../../public/fonts/Inter-Regular.ttf', import.meta.url) new URL('../../public/fonts/Inter-Regular.ttf', import.meta.url)
).then((res) => res.arrayBuffer()) ).arrayBuffer()
const interBoldFontP = fetch( const interBoldFontP = ky(
new URL('../../public/fonts/Inter-SemiBold.ttf', import.meta.url) new URL('../../public/fonts/Inter-SemiBold.ttf', import.meta.url)
).then((res) => res.arrayBuffer()) ).arrayBuffer()
export const config = { export const config = {
runtime: 'experimental-edge' runtime: 'experimental-edge'

View File

@@ -1,6 +1,5 @@
import type { GetServerSideProps } from 'next' import type { GetServerSideProps } from 'next'
import { type ExtendedRecordMap } from 'notion-types'
import { ExtendedRecordMap } from 'notion-types'
import { import {
getBlockParentPage, getBlockParentPage,
getBlockTitle, getBlockTitle,
@@ -67,8 +66,8 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
const date = lastUpdatedTime const date = lastUpdatedTime
? new Date(lastUpdatedTime) ? new Date(lastUpdatedTime)
: publishedTime : publishedTime
? new Date(publishedTime) ? new Date(publishedTime)
: undefined : undefined
const socialImageUrl = getSocialImageUrl(pageId) const socialImageUrl = getSocialImageUrl(pageId)
feed.item({ feed.item({
@@ -98,4 +97,6 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
return { props: {} } return { props: {} }
} }
export default () => null export default function noop() {
return null
}

View File

@@ -1,5 +1,3 @@
import * as React from 'react'
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'

View File

@@ -42,4 +42,6 @@ Sitemap: ${host}/sitemap.xml
} }
} }
export default () => null export default function noop() {
return null
}

View File

@@ -1,8 +1,8 @@
import type { GetServerSideProps } from 'next' import type { GetServerSideProps } from 'next'
import type { SiteMap } from '@/lib/types'
import { host } from '@/lib/config' import { host } from '@/lib/config'
import { getSiteMap } from '@/lib/get-site-map' import { getSiteMap } from '@/lib/get-site-map'
import type { SiteMap } from '@/lib/types'
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
if (req.method !== 'GET') { if (req.method !== 'GET') {
@@ -54,4 +54,6 @@ const createSitemap = (siteMap: SiteMap) =>
</urlset> </urlset>
` `
export default () => null export default function noop() {
return null
}

4458
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,15 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2016", "target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": false, "strict": false,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"experimentalDecorators": true,
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,
"module": "esnext", "module": "esnext",
"moduleResolution": "node", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "preserve",

3892
yarn.lock

File diff suppressed because it is too large Load Diff