mirror of
https://github.com/d0zingcat/nextjs-notion-starter-kit.git
synced 2026-05-21 15:10:27 +00:00
Merge branch 'main' into fix/page-social-homepage
This commit is contained in:
20
.env.example
20
.env.example
@@ -7,17 +7,15 @@
|
||||
# @see https://github.com/rolodato/dotenv-safe for more details.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Optional (for preview image support)
|
||||
#GOOGLE_APPLICATION_CREDENTIALS=
|
||||
|
||||
# Optional (for preview image support)
|
||||
#GCLOUD_PROJECT=
|
||||
|
||||
# Optional (for preview image support)
|
||||
#FIREBASE_COLLECTION_IMAGES=
|
||||
|
||||
# Optional (for fathom analytics)
|
||||
#NEXT_PUBLIC_FATHOM_ID=
|
||||
|
||||
# Optional (for rendering tweets efficiently)
|
||||
TWITTER_ACCESS_TOKEN=
|
||||
# Optional (for rendering tweets more efficiently)
|
||||
#TWITTER_ACCESS_TOKEN=
|
||||
|
||||
# Optional (for persisting preview images to redis)
|
||||
# NOTE: if you want to enable redis, only REDIS_HOST and REDIS_PASSWORD are required
|
||||
#REDIS_HOST=
|
||||
#REDIS_PASSWORD=
|
||||
#REDIS_USER='default'
|
||||
#REDIS_NAMESPACE='preview-images'
|
||||
38
.eslintrc
38
.eslintrc
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"ecmaFeatures": {
|
||||
"legacyDecorators": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"ignorePatterns": ["**/*.js", "**/*.jsx"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"standard",
|
||||
"standard-react",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"env": { "browser": true, "node": true },
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": 0,
|
||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||
"no-use-before-define": 0,
|
||||
"@typescript-eslint/no-use-before-define": 0,
|
||||
"space-before-function-paren": 0,
|
||||
"react/prop-types": 0,
|
||||
"react/jsx-handler-names": 0,
|
||||
"react/jsx-fragments": 0,
|
||||
"react/no-unused-prop-types": 0,
|
||||
"import/export": 0,
|
||||
"standard/no-callback-literal": 0
|
||||
}
|
||||
}
|
||||
27
.eslintrc.json
Normal file
27
.eslintrc.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"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": {
|
||||
"@typescript-eslint/no-explicit-any": 0,
|
||||
"@typescript-eslint/no-non-null-assertion": 0,
|
||||
"@typescript-eslint/no-unused-vars": 2,
|
||||
"react/prop-types": 0
|
||||
}
|
||||
}
|
||||
17
.github/workflows/build.yml
vendored
Normal file
17
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Build
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
Build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
cache: yarn
|
||||
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
@@ -1,5 +0,0 @@
|
||||
language: node_js
|
||||
cache: yarn
|
||||
node_js:
|
||||
- 12
|
||||
- 14
|
||||
@@ -1,86 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import got from 'got'
|
||||
import lqip from 'lqip-modern'
|
||||
|
||||
import { isPreviewImageSupportEnabled } from '../lib/config'
|
||||
import * as types from '../lib/types'
|
||||
import * as db from '../lib/db'
|
||||
|
||||
export default async (
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
): Promise<void> => {
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).send({ error: 'method not allowed' })
|
||||
}
|
||||
|
||||
if (!isPreviewImageSupportEnabled) {
|
||||
return res.status(418).send({
|
||||
error: 'preview image support has been disabled for this deployment'
|
||||
})
|
||||
}
|
||||
|
||||
const { url, id } = req.body
|
||||
|
||||
const result = await createPreviewImage(url, id)
|
||||
|
||||
res.setHeader(
|
||||
'Cache-Control',
|
||||
result.error
|
||||
? 'public, s-maxage=60, max-age=60, stale-while-revalidate=60'
|
||||
: 'public, immutable, s-maxage=31536000, max-age=31536000, stale-while-revalidate=60'
|
||||
)
|
||||
res.status(200).json(result)
|
||||
}
|
||||
|
||||
export async function createPreviewImage(
|
||||
url: string,
|
||||
id: string
|
||||
): Promise<types.PreviewImage> {
|
||||
console.log('createPreviewImage lambda', { url, id })
|
||||
const doc = db.images.doc(id)
|
||||
|
||||
try {
|
||||
const model = await doc.get()
|
||||
if (model.exists) {
|
||||
return model.data() as types.PreviewImage
|
||||
}
|
||||
|
||||
const { body } = await got(url, { responseType: 'buffer' })
|
||||
const result = await lqip(body)
|
||||
console.log('lqip', result.metadata)
|
||||
|
||||
const image = {
|
||||
url,
|
||||
originalWidth: result.metadata.originalWidth,
|
||||
originalHeight: result.metadata.originalHeight,
|
||||
width: result.metadata.width,
|
||||
height: result.metadata.height,
|
||||
type: result.metadata.type,
|
||||
dataURIBase64: result.metadata.dataURIBase64
|
||||
}
|
||||
|
||||
await doc.create(image)
|
||||
return image
|
||||
} catch (err) {
|
||||
console.error('lqip error', err)
|
||||
|
||||
try {
|
||||
const error: any = {
|
||||
url,
|
||||
error: err.message || 'unknown error'
|
||||
}
|
||||
|
||||
if (err?.response?.statusCode) {
|
||||
error.statusCode = err?.response?.statusCode
|
||||
}
|
||||
|
||||
await doc.create(error)
|
||||
return error
|
||||
} catch (err) {
|
||||
// ignore errors
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react'
|
||||
import { FaTwitter, FaGithub, FaLinkedin } from 'react-icons/fa'
|
||||
import { FaTwitter, FaZhihu, FaGithub, FaLinkedin } from 'react-icons/fa'
|
||||
import { IoSunnyOutline, IoMoonSharp } from 'react-icons/io5'
|
||||
import * as config from 'lib/config'
|
||||
|
||||
@@ -26,7 +26,7 @@ export const Footer: React.FC<{
|
||||
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
<div className={styles.copyright}>Copyright 2021 {config.author}</div>
|
||||
<div className={styles.copyright}>Copyright 2022 {config.author}</div>
|
||||
|
||||
{hasMounted ? (
|
||||
<div className={styles.settings}>
|
||||
@@ -53,6 +53,18 @@ export const Footer: React.FC<{
|
||||
</a>
|
||||
)}
|
||||
|
||||
{config.zhihu && (
|
||||
<a
|
||||
className={styles.zhihu}
|
||||
href={`https://zhihu.com/people/${config.zhihu}`}
|
||||
title={`Zhihu @${config.zhihu}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<FaZhihu />
|
||||
</a>
|
||||
)}
|
||||
|
||||
{config.github && (
|
||||
<a
|
||||
className={styles.github}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react'
|
||||
import Head from 'next/head'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import dynamic from 'next/dynamic'
|
||||
import cs from 'classnames'
|
||||
import { useRouter } from 'next/router'
|
||||
@@ -12,12 +13,12 @@ import { PageBlock } from 'notion-types'
|
||||
import { Tweet, TwitterContextProvider } from 'react-static-tweets'
|
||||
|
||||
// core notion renderer
|
||||
import { NotionRenderer, Code, Collection, CollectionRow } from 'react-notion-x'
|
||||
import { NotionRenderer } from 'react-notion-x'
|
||||
|
||||
// utils
|
||||
import { getBlockTitle } from 'notion-utils'
|
||||
import { mapPageUrl, getCanonicalPageUrl } from 'lib/map-page-url'
|
||||
import { mapNotionImageUrl } from 'lib/map-image-url'
|
||||
import { mapImageUrl } from 'lib/map-image-url'
|
||||
import { getPageDescription } from 'lib/get-page-description'
|
||||
import { getPageTweet } from 'lib/get-page-tweet'
|
||||
import { searchNotion } from 'lib/search-notion'
|
||||
@@ -33,43 +34,35 @@ import { PageActions } from './PageActions'
|
||||
import { Footer } from './Footer'
|
||||
import { PageSocial } from './PageSocial'
|
||||
import { GitHubShareButton } from './GitHubShareButton'
|
||||
import { ReactUtterances } from './ReactUtterances'
|
||||
|
||||
import styles from './styles.module.css'
|
||||
|
||||
// const Code = dynamic(() =>
|
||||
// import('react-notion-x').then((notion) => notion.Code)
|
||||
// )
|
||||
//
|
||||
// const Collection = dynamic(() =>
|
||||
// import('react-notion-x').then((notion) => notion.Collection)
|
||||
// )
|
||||
//
|
||||
// const CollectionRow = dynamic(
|
||||
// () => import('react-notion-x').then((notion) => notion.CollectionRow),
|
||||
// {
|
||||
// ssr: false
|
||||
// }
|
||||
// )
|
||||
// -----------------------------------------------------------------------------
|
||||
// dynamic imports for optional components
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// TODO: PDF support via "react-pdf" package has numerous troubles building
|
||||
// with next.js
|
||||
// const Pdf = dynamic(
|
||||
// () => import('react-notion-x').then((notion) => notion.Pdf),
|
||||
// { ssr: false }
|
||||
// )
|
||||
|
||||
const Equation = dynamic(() =>
|
||||
import('react-notion-x').then((notion) => notion.Equation)
|
||||
const Code = dynamic(() =>
|
||||
import('react-notion-x/build/third-party/code').then((m) => m.Code)
|
||||
)
|
||||
const Collection = dynamic(() =>
|
||||
import('react-notion-x/build/third-party/collection').then(
|
||||
(m) => m.Collection
|
||||
)
|
||||
)
|
||||
const Equation = dynamic(() =>
|
||||
import('react-notion-x/build/third-party/equation').then((m) => m.Equation)
|
||||
)
|
||||
const Pdf = dynamic(
|
||||
() => import('react-notion-x/build/third-party/pdf').then((m) => m.Pdf),
|
||||
{
|
||||
ssr: false
|
||||
}
|
||||
)
|
||||
|
||||
// we're now using a much lighter-weight tweet renderer react-static-tweets
|
||||
// instead of the official iframe-based embed widget from twitter
|
||||
// const Tweet = dynamic(() => import('react-tweet-embed'))
|
||||
|
||||
const Modal = dynamic(
|
||||
() => import('react-notion-x').then((notion) => notion.Modal),
|
||||
{ ssr: false }
|
||||
() => import('react-notion-x/build/third-party/modal').then((m) => m.Modal),
|
||||
{
|
||||
ssr: false
|
||||
}
|
||||
)
|
||||
|
||||
export const NotionPage: React.FC<types.PageProps> = ({
|
||||
@@ -131,30 +124,18 @@ export const NotionPage: React.FC<types.PageProps> = ({
|
||||
const showTableOfContents = !!isBlogPost
|
||||
const minTableOfContentsItems = 3
|
||||
|
||||
const socialImage = mapNotionImageUrl(
|
||||
const socialImage = mapImageUrl(
|
||||
(block as PageBlock).format?.page_cover || config.defaultPageCover,
|
||||
block
|
||||
)
|
||||
|
||||
const socialDescription =
|
||||
getPageDescription(block, recordMap) ?? config.description
|
||||
|
||||
let comments: React.ReactNode = null
|
||||
|
||||
let pageAside: React.ReactNode = null
|
||||
|
||||
// only display comments and page actions on blog post pages
|
||||
if (isBlogPost) {
|
||||
if (config.utterancesGitHubRepo) {
|
||||
comments = (
|
||||
<ReactUtterances
|
||||
repo={config.utterancesGitHubRepo}
|
||||
issueMap='issue-term'
|
||||
issueTerm='title'
|
||||
theme={darkMode.value ? 'photon-dark' : 'github-light'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const tweet = getPageTweet(block, recordMap)
|
||||
if (tweet) {
|
||||
pageAside = <PageActions tweet={tweet} />
|
||||
@@ -225,42 +206,21 @@ export const NotionPage: React.FC<types.PageProps> = ({
|
||||
pageId === site.rootNotionPageId && 'index-page'
|
||||
)}
|
||||
components={{
|
||||
pageLink: ({
|
||||
href,
|
||||
as,
|
||||
passHref,
|
||||
prefetch,
|
||||
replace,
|
||||
scroll,
|
||||
shallow,
|
||||
locale,
|
||||
...props
|
||||
}) => (
|
||||
<Link
|
||||
href={href}
|
||||
as={as}
|
||||
passHref={passHref}
|
||||
prefetch={prefetch}
|
||||
replace={replace}
|
||||
scroll={scroll}
|
||||
shallow={shallow}
|
||||
locale={locale}
|
||||
>
|
||||
<a {...props} />
|
||||
</Link>
|
||||
),
|
||||
code: Code,
|
||||
collection: Collection,
|
||||
collectionRow: CollectionRow,
|
||||
tweet: Tweet,
|
||||
modal: Modal,
|
||||
equation: Equation
|
||||
nextImage: Image,
|
||||
nextLink: Link,
|
||||
Code,
|
||||
Collection,
|
||||
Equation,
|
||||
Pdf,
|
||||
Modal,
|
||||
Tweet
|
||||
}}
|
||||
recordMap={recordMap}
|
||||
rootPageId={site.rootNotionPageId}
|
||||
rootDomain={site.domain}
|
||||
fullPage={!isLiteMode}
|
||||
darkMode={darkMode.value}
|
||||
previewImages={site.previewImages !== false}
|
||||
previewImages={!!recordMap.preview_images}
|
||||
showCollectionViewDropdown={false}
|
||||
showTableOfContents={showTableOfContents}
|
||||
minTableOfContentsItems={minTableOfContentsItems}
|
||||
@@ -268,9 +228,8 @@ export const NotionPage: React.FC<types.PageProps> = ({
|
||||
defaultPageCover={config.defaultPageCover}
|
||||
defaultPageCoverPosition={config.defaultPageCoverPosition}
|
||||
mapPageUrl={siteMapPageUrl}
|
||||
mapImageUrl={mapNotionImageUrl}
|
||||
mapImageUrl={mapImageUrl}
|
||||
searchNotion={searchNotion}
|
||||
pageFooter={comments}
|
||||
pageAside={pageAside}
|
||||
footer={
|
||||
<Footer
|
||||
|
||||
@@ -28,7 +28,8 @@ export const Page404: React.FC<types.PageProps> = ({ site, pageId, error }) => {
|
||||
) : (
|
||||
pageId && (
|
||||
<p>
|
||||
Make sure that Notion page "{pageId}" is publicly accessible.
|
||||
Make sure that Notion page "{pageId}" is publicly
|
||||
accessible.
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import styles from './styles.module.css'
|
||||
|
||||
export type MappingType =
|
||||
| 'pathname'
|
||||
| 'url'
|
||||
| 'title'
|
||||
| 'og:title'
|
||||
| 'issue-number'
|
||||
| 'issue-term'
|
||||
|
||||
export type Theme =
|
||||
| 'github-light'
|
||||
| 'github-dark'
|
||||
| 'preferred-color-scheme'
|
||||
| 'github-dark-orange'
|
||||
| 'icy-dark'
|
||||
| 'dark-blue'
|
||||
| 'photon-dark'
|
||||
|
||||
interface ReactUtterancesProps {
|
||||
repo: string
|
||||
issueMap: MappingType
|
||||
issueTerm?: string
|
||||
issueNumber?: number
|
||||
label?: string
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
interface ReactUtterancesState {
|
||||
pending: boolean
|
||||
}
|
||||
|
||||
export class ReactUtterances extends React.Component<
|
||||
ReactUtterancesProps,
|
||||
ReactUtterancesState
|
||||
> {
|
||||
reference: React.RefObject<HTMLDivElement>
|
||||
scriptElement: any
|
||||
|
||||
constructor(props: ReactUtterancesProps) {
|
||||
super(props)
|
||||
|
||||
if (props.issueMap === 'issue-term' && props.issueTerm === undefined) {
|
||||
throw Error(
|
||||
"Property 'issueTerm' must be provided with issueMap 'issue-term'"
|
||||
)
|
||||
}
|
||||
|
||||
if (props.issueMap === 'issue-number' && props.issueNumber === undefined) {
|
||||
throw Error(
|
||||
"Property 'issueNumber' must be provided with issueMap 'issue-number'"
|
||||
)
|
||||
}
|
||||
|
||||
this.reference = React.createRef<HTMLDivElement>()
|
||||
this.state = { pending: true }
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(props) {
|
||||
// this.scriptElement.setAttribute('theme', props.theme)
|
||||
const iframe = document.querySelector('iframe.utterances-frame') as any
|
||||
|
||||
if (iframe) {
|
||||
iframe.contentWindow.postMessage(
|
||||
{ type: 'set-theme', theme: props.theme },
|
||||
'https://utteranc.es/'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
const { repo, issueMap, issueTerm, issueNumber, label, theme } = this.props
|
||||
const scriptElement = document.createElement('script')
|
||||
scriptElement.src = 'https://utteranc.es/client.js'
|
||||
scriptElement.async = true
|
||||
scriptElement.defer = true
|
||||
scriptElement.setAttribute('repo', repo)
|
||||
scriptElement.setAttribute('crossorigin', 'annonymous')
|
||||
scriptElement.setAttribute('theme', theme)
|
||||
scriptElement.onload = () => this.setState({ pending: false })
|
||||
|
||||
if (label) {
|
||||
scriptElement.setAttribute('label', label)
|
||||
}
|
||||
|
||||
if (issueMap === 'issue-number') {
|
||||
scriptElement.setAttribute('issue-number', issueNumber.toString())
|
||||
} else if (issueMap === 'issue-term') {
|
||||
scriptElement.setAttribute('issue-term', issueTerm)
|
||||
} else {
|
||||
scriptElement.setAttribute('issue-term', issueMap)
|
||||
}
|
||||
|
||||
// TODO: Check current availability
|
||||
this.scriptElement = scriptElement
|
||||
this.reference.current.appendChild(scriptElement)
|
||||
}
|
||||
|
||||
render(): React.ReactElement {
|
||||
return (
|
||||
<div className={styles.comments}>
|
||||
<div className={styles.utterances} ref={this.reference}>
|
||||
{this.state.pending && <p>Loading Comments...</p>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,10 @@
|
||||
color: #2795e9;
|
||||
}
|
||||
|
||||
.zhihu:hover {
|
||||
color: #0066FF;
|
||||
}
|
||||
|
||||
.github:hover {
|
||||
color: #c9510c;
|
||||
}
|
||||
@@ -107,19 +111,6 @@
|
||||
border-top: 1px solid var(--fg-color-0);
|
||||
}
|
||||
|
||||
.utterances {
|
||||
margin-top: 2em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 567px) {
|
||||
.utterances {
|
||||
width: calc(100% + 60px);
|
||||
position: relative;
|
||||
left: -60px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 566px) {
|
||||
.footer {
|
||||
flex-direction: column;
|
||||
|
||||
83
contributing.md
Normal file
83
contributing.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Contributing
|
||||
|
||||
Suggestions and pull requests are highly encouraged. Have a look at the [open issues](https://github.com/NotionX/react-notion-x/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22+sort%3Areactions-%2B1-desc), especially [the easy ones](https://github.com/NotionX/react-notion-x/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+sort%3Areactions-%2B1-desc).
|
||||
|
||||
## Development
|
||||
|
||||
To develop the project locally, you'll need a recent version of Node.js and `yarn` v1 installed globally.
|
||||
|
||||
To get started, clone the repo and run `yarn` from the root directory:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/transitive-bullshit/nextjs-notion-starter-kit
|
||||
cd nextjs-notion-starter-kit
|
||||
yarn
|
||||
```
|
||||
|
||||
Now that your dependencies are installed, you can run the local Next.js dev server:
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
You should now be able to open `http://localhost:3000` to view the webapp.
|
||||
|
||||
## Production
|
||||
|
||||
To build for production, you can run:
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
Which just runs `next build` under the hood.
|
||||
|
||||
### Local-linked react-notion-x
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
# from react-notion-x clone
|
||||
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`:
|
||||
|
||||
```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
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
@@ -36,6 +36,9 @@ export const pageUrlAdditions = cleanPageUrlMap(
|
||||
'pageUrlAdditions'
|
||||
)
|
||||
|
||||
export const isDev =
|
||||
process.env.NODE_ENV === 'development' || !process.env.NODE_ENV
|
||||
|
||||
// general site config
|
||||
export const name: string = getSiteConfig('name')
|
||||
export const author: string = getSiteConfig('author')
|
||||
@@ -44,6 +47,7 @@ export const description: string = getSiteConfig('description', 'Notion Blog')
|
||||
|
||||
// social accounts
|
||||
export const twitter: string | null = getSiteConfig('twitter', null)
|
||||
export const zhihu: string | null = getSiteConfig('zhihu', null)
|
||||
export const github: string | null = getSiteConfig('github', null)
|
||||
export const linkedin: string | null = getSiteConfig('linkedin', null)
|
||||
|
||||
@@ -70,24 +74,17 @@ export const defaultPageCoverPosition: number = getSiteConfig(
|
||||
0.5
|
||||
)
|
||||
|
||||
// Optional utteranc.es comments via GitHub issue comments
|
||||
export const utterancesGitHubRepo: string | null = getSiteConfig(
|
||||
'utterancesGitHubRepo',
|
||||
null
|
||||
)
|
||||
|
||||
// Optional image CDN host to proxy all image requests through
|
||||
export const imageCDNHost: string | null = getSiteConfig('imageCDNHost', null)
|
||||
|
||||
// Optional whether or not to enable support for LQIP preview images
|
||||
// (requires a Google Firebase collection)
|
||||
export const isPreviewImageSupportEnabled: boolean = getSiteConfig(
|
||||
'isPreviewImageSupportEnabled',
|
||||
false
|
||||
)
|
||||
|
||||
export const isDev =
|
||||
process.env.NODE_ENV === 'development' || !process.env.NODE_ENV
|
||||
// Optional whether or not to enable support for LQIP preview images
|
||||
export const isTweetEmbedSupportEnabled: boolean = getSiteConfig(
|
||||
'isTweetEmbedSupportEnabled',
|
||||
true
|
||||
)
|
||||
|
||||
// where it all starts -- the site's root Notion page
|
||||
export const includeNotionIdInUrls: boolean = getSiteConfig(
|
||||
@@ -97,15 +94,33 @@ export const includeNotionIdInUrls: boolean = getSiteConfig(
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Optional redis instance for persisting preview images
|
||||
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 redisUrl = getEnv(
|
||||
'REDIS_URL',
|
||||
`redis://${redisUser}:${redisPassword}@${redisHost}`
|
||||
)
|
||||
export const redisNamespace: string | null = getEnv(
|
||||
'REDIS_NAMESPACE',
|
||||
'preview-images'
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export const isServer = typeof window === 'undefined'
|
||||
|
||||
export const port = getEnv('PORT', '3000')
|
||||
export const host = isDev ? `http://localhost:${port}` : `https://${domain}`
|
||||
|
||||
export const apiBaseUrl = `${host}/api`
|
||||
export const apiBaseUrl = `/api`
|
||||
|
||||
export const api = {
|
||||
createPreviewImage: `${apiBaseUrl}/create-preview-image`,
|
||||
searchNotion: `${apiBaseUrl}/search-notion`
|
||||
}
|
||||
|
||||
@@ -119,46 +134,6 @@ export const fathomConfig = fathomId
|
||||
}
|
||||
: undefined
|
||||
|
||||
const defaultEnvValueForPreviewImageSupport =
|
||||
isPreviewImageSupportEnabled && isServer ? undefined : null
|
||||
|
||||
export const googleProjectId = getEnv(
|
||||
'GCLOUD_PROJECT',
|
||||
defaultEnvValueForPreviewImageSupport
|
||||
)
|
||||
|
||||
export const googleApplicationCredentials = getGoogleApplicationCredentials()
|
||||
|
||||
export const firebaseCollectionImages = getEnv(
|
||||
'FIREBASE_COLLECTION_IMAGES',
|
||||
defaultEnvValueForPreviewImageSupport
|
||||
)
|
||||
|
||||
// this hack is necessary because vercel doesn't support secret files so we need to encode our google
|
||||
// credentials a base64-encoded string of the JSON-ified content
|
||||
function getGoogleApplicationCredentials() {
|
||||
if (!isPreviewImageSupportEnabled || !isServer) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const googleApplicationCredentialsBase64 = getEnv(
|
||||
'GOOGLE_APPLICATION_CREDENTIALS',
|
||||
defaultEnvValueForPreviewImageSupport
|
||||
)
|
||||
|
||||
return JSON.parse(
|
||||
Buffer.from(googleApplicationCredentialsBase64, 'base64').toString()
|
||||
)
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Firebase config error: invalid "GOOGLE_APPLICATION_CREDENTIALS" should be base64-encoded JSON\n'
|
||||
)
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
function cleanPageUrlMap(
|
||||
pageUrlMap: PageUrlOverridesMap,
|
||||
label: string
|
||||
|
||||
22
lib/db.ts
22
lib/db.ts
@@ -1,14 +1,14 @@
|
||||
import * as firestore from '@google-cloud/firestore'
|
||||
import * as config from './config'
|
||||
import Keyv from 'keyv'
|
||||
import KeyvRedis from '@keyv/redis'
|
||||
|
||||
export let db: firestore.Firestore = null
|
||||
export let images: firestore.CollectionReference = null
|
||||
import { isRedisEnabled, redisUrl, redisNamespace } from './config'
|
||||
|
||||
if (config.isPreviewImageSupportEnabled) {
|
||||
db = new firestore.Firestore({
|
||||
projectId: config.googleProjectId,
|
||||
credentials: config.googleApplicationCredentials
|
||||
})
|
||||
|
||||
images = db.collection(config.firebaseCollectionImages)
|
||||
let db: Keyv
|
||||
if (isRedisEnabled) {
|
||||
const keyvRedis = new KeyvRedis(redisUrl)
|
||||
db = new Keyv({ store: keyvRedis, namespace: redisNamespace || undefined })
|
||||
} else {
|
||||
db = new Keyv()
|
||||
}
|
||||
|
||||
export { db }
|
||||
|
||||
@@ -8,7 +8,9 @@ import { getCanonicalPageId } from './get-canonical-page-id'
|
||||
|
||||
const uuid = !!includeNotionIdInUrls
|
||||
|
||||
export const getAllPages = pMemoize(getAllPagesImpl, { maxAge: 60000 * 5 })
|
||||
export const getAllPages = pMemoize(getAllPagesImpl, {
|
||||
cacheKey: (...args) => JSON.stringify(args)
|
||||
})
|
||||
|
||||
export async function getAllPagesImpl(
|
||||
rootNotionPageId: string,
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import crypto from 'crypto'
|
||||
import got from 'got'
|
||||
import pMap from 'p-map'
|
||||
|
||||
import { api, isPreviewImageSupportEnabled } from './config'
|
||||
import * as types from './types'
|
||||
import * as db from './db'
|
||||
|
||||
function sha256(input: Buffer | string) {
|
||||
const buffer = Buffer.isBuffer(input) ? input : Buffer.from(input)
|
||||
return crypto.createHash('sha256').update(buffer).digest('hex')
|
||||
}
|
||||
|
||||
export async function getPreviewImages(
|
||||
images: string[]
|
||||
): Promise<types.PreviewImageMap> {
|
||||
if (!isPreviewImageSupportEnabled) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const imageDocRefs = images.map((url) => {
|
||||
const id = sha256(url)
|
||||
return db.images.doc(id)
|
||||
})
|
||||
|
||||
if (!imageDocRefs.length) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const imageDocs = await db.db.getAll(...imageDocRefs)
|
||||
const results = await pMap(imageDocs, async (model, index) => {
|
||||
if (model.exists) {
|
||||
return model.data() as types.PreviewImage
|
||||
} else {
|
||||
const json = {
|
||||
url: images[index],
|
||||
id: model.id
|
||||
}
|
||||
console.log('createPreviewImage server-side', json)
|
||||
|
||||
// TODO: should we fire and forget here to speed up builds?
|
||||
return got
|
||||
.post(api.createPreviewImage, { json })
|
||||
.json() as Promise<types.PreviewImage>
|
||||
}
|
||||
})
|
||||
|
||||
return results
|
||||
.filter(Boolean)
|
||||
.filter((image) => !image.error)
|
||||
.reduce(
|
||||
(acc, result) => ({
|
||||
...acc,
|
||||
[result.url]: result
|
||||
}),
|
||||
{}
|
||||
)
|
||||
}
|
||||
@@ -1,56 +1,12 @@
|
||||
import { Block } from 'notion-types'
|
||||
import { imageCDNHost } from './config'
|
||||
import { defaultMapImageUrl } from 'react-notion-x'
|
||||
|
||||
export const mapNotionImageUrl = (url: string, block: Block) => {
|
||||
if (!url) {
|
||||
return null
|
||||
}
|
||||
import { defaultPageIcon, defaultPageCover } from './config'
|
||||
|
||||
if (url.startsWith('data:')) {
|
||||
export const mapImageUrl = (url: string, block: Block) => {
|
||||
if (url === defaultPageCover || url === defaultPageIcon) {
|
||||
return url
|
||||
}
|
||||
|
||||
if (imageCDNHost && url.startsWith(imageCDNHost)) {
|
||||
return url
|
||||
}
|
||||
|
||||
// const origUrl = url
|
||||
|
||||
if (url.startsWith('/images')) {
|
||||
url = `https://www.notion.so${url}`
|
||||
}
|
||||
|
||||
// more recent versions of notion don't proxy unsplash images
|
||||
if (!url.startsWith('https://images.unsplash.com')) {
|
||||
url = `https://www.notion.so${
|
||||
url.startsWith('/image') ? url : `/image/${encodeURIComponent(url)}`
|
||||
}`
|
||||
|
||||
const notionImageUrlV2 = new URL(url)
|
||||
let table = block.parent_table === 'space' ? 'block' : block.parent_table
|
||||
if (table === 'collection') {
|
||||
table = 'block'
|
||||
}
|
||||
notionImageUrlV2.searchParams.set('table', table)
|
||||
notionImageUrlV2.searchParams.set('id', block.id)
|
||||
notionImageUrlV2.searchParams.set('cache', 'v2')
|
||||
|
||||
url = notionImageUrlV2.toString()
|
||||
}
|
||||
|
||||
// console.log({ url, origUrl })
|
||||
return mapImageUrl(url)
|
||||
}
|
||||
|
||||
export const mapImageUrl = (imageUrl: string) => {
|
||||
if (imageUrl.startsWith('data:')) {
|
||||
return imageUrl
|
||||
}
|
||||
|
||||
if (imageCDNHost) {
|
||||
// Our proxy uses Cloudflare's global CDN to cache these image assets
|
||||
return `${imageCDNHost}/${encodeURIComponent(imageUrl)}`
|
||||
} else {
|
||||
return imageUrl
|
||||
}
|
||||
return defaultMapImageUrl(url, block)
|
||||
}
|
||||
|
||||
103
lib/notion.ts
103
lib/notion.ts
@@ -1,9 +1,12 @@
|
||||
import { NotionAPI } from 'notion-client'
|
||||
import { ExtendedRecordMap, SearchParams, SearchResults } from 'notion-types'
|
||||
import { getPreviewImages } from './get-preview-images'
|
||||
import { mapNotionImageUrl } from './map-image-url'
|
||||
import { fetchTweetAst } from 'static-tweets'
|
||||
import pMap from 'p-map'
|
||||
|
||||
import { getPreviewImageMap } from './preview-images'
|
||||
import { getTweetAstMap } from './tweet-embeds'
|
||||
import {
|
||||
isPreviewImageSupportEnabled,
|
||||
isTweetEmbedSupportEnabled
|
||||
} from './config'
|
||||
|
||||
export const notion = new NotionAPI({
|
||||
apiBaseUrl: process.env.NOTION_API_BASE_URL
|
||||
@@ -11,92 +14,16 @@ export const notion = new NotionAPI({
|
||||
|
||||
export async function getPage(pageId: string): Promise<ExtendedRecordMap> {
|
||||
const recordMap = await notion.getPage(pageId)
|
||||
const blockIds = Object.keys(recordMap.block)
|
||||
|
||||
const imageUrls: string[] = blockIds
|
||||
.map((blockId) => {
|
||||
const block = recordMap.block[blockId]?.value
|
||||
if (isPreviewImageSupportEnabled) {
|
||||
const previewImageMap = await getPreviewImageMap(recordMap)
|
||||
;(recordMap as any).preview_images = previewImageMap
|
||||
}
|
||||
|
||||
if (block) {
|
||||
if (block.type === 'image') {
|
||||
const source = block.properties?.source?.[0]?.[0]
|
||||
|
||||
if (source) {
|
||||
return {
|
||||
block,
|
||||
url: source
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((block.format as any)?.page_cover) {
|
||||
const source = (block.format as any).page_cover
|
||||
|
||||
return {
|
||||
block,
|
||||
url: source
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
.filter(Boolean)
|
||||
.map(({ block, url }) => mapNotionImageUrl(url, block))
|
||||
.filter(Boolean)
|
||||
|
||||
const urls = Array.from(new Set(imageUrls))
|
||||
const previewImageMap = await getPreviewImages(urls)
|
||||
;(recordMap as any).preview_images = previewImageMap
|
||||
|
||||
const tweetIds: string[] = blockIds
|
||||
.map((blockId) => {
|
||||
const block = recordMap.block[blockId]?.value
|
||||
|
||||
if (block) {
|
||||
if (block.type === 'tweet') {
|
||||
const src = block.properties?.source?.[0]?.[0]
|
||||
|
||||
if (src) {
|
||||
const id = src.split('?')[0].split('/').pop()
|
||||
if (id) return id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
const tweetAsts = await pMap(
|
||||
tweetIds,
|
||||
async (tweetId) => {
|
||||
try {
|
||||
return {
|
||||
tweetId,
|
||||
tweetAst: await fetchTweetAst(tweetId)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('error fetching tweet info', tweetId, err)
|
||||
}
|
||||
},
|
||||
{
|
||||
concurrency: 4
|
||||
}
|
||||
)
|
||||
|
||||
const tweetAstMap = tweetAsts.reduce((acc, { tweetId, tweetAst }) => {
|
||||
if (tweetAst) {
|
||||
return {
|
||||
...acc,
|
||||
[tweetId]: tweetAst
|
||||
}
|
||||
} else {
|
||||
return acc
|
||||
}
|
||||
}, {})
|
||||
|
||||
;(recordMap as any).tweetAstMap = tweetAstMap
|
||||
if (isTweetEmbedSupportEnabled) {
|
||||
const tweetAstMap = await getTweetAstMap(recordMap)
|
||||
;(recordMap as any).tweetAstMap = tweetAstMap
|
||||
}
|
||||
|
||||
return recordMap
|
||||
}
|
||||
|
||||
61
lib/preview-images.ts
Normal file
61
lib/preview-images.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import got from 'got'
|
||||
import lqip from 'lqip-modern'
|
||||
import pMap from 'p-map'
|
||||
import pMemoize from 'p-memoize'
|
||||
import { ExtendedRecordMap, PreviewImage, PreviewImageMap } from 'notion-types'
|
||||
import { getPageImageUrls } from 'notion-utils'
|
||||
|
||||
import { defaultPageIcon, defaultPageCover } from './config'
|
||||
import { db } from './db'
|
||||
import { mapImageUrl } from './map-image-url'
|
||||
|
||||
// NOTE: this is just an example of how to pre-compute preview images.
|
||||
// Depending on how many images you're working with, this can potentially be
|
||||
// very expensive to recompute, so in production we recommend that you cache
|
||||
// the preview image results in a key-value database of your choosing.
|
||||
// If you're not sure where to start, check out https://github.com/jaredwray/keyv
|
||||
|
||||
export async function getPreviewImageMap(
|
||||
recordMap: ExtendedRecordMap
|
||||
): Promise<PreviewImageMap> {
|
||||
const urls: string[] = getPageImageUrls(recordMap, { mapImageUrl })
|
||||
.concat([defaultPageIcon, defaultPageCover])
|
||||
.filter(Boolean)
|
||||
|
||||
const previewImagesMap = Object.fromEntries(
|
||||
await pMap(urls, async (url) => [url, await getPreviewImage(url)], {
|
||||
concurrency: 8
|
||||
})
|
||||
)
|
||||
|
||||
return previewImagesMap
|
||||
}
|
||||
|
||||
async function createPreviewImage(url: string): Promise<PreviewImage | null> {
|
||||
const cacheKey = url
|
||||
|
||||
try {
|
||||
const cachedPreviewImage = await db.get(cacheKey)
|
||||
if (cachedPreviewImage) {
|
||||
return cachedPreviewImage
|
||||
}
|
||||
|
||||
const { body } = await got(url, { responseType: 'buffer' })
|
||||
const result = await lqip(body)
|
||||
console.log('lqip', result.metadata)
|
||||
|
||||
const previewImage = {
|
||||
originalWidth: result.metadata.originalWidth,
|
||||
originalHeight: result.metadata.originalHeight,
|
||||
dataURIBase64: result.metadata.dataURIBase64
|
||||
}
|
||||
|
||||
await db.set(cacheKey, previewImage)
|
||||
return previewImage
|
||||
} catch (err) {
|
||||
console.warn('error creating preview image', url, err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const getPreviewImage = pMemoize(createPreviewImage)
|
||||
@@ -1,11 +1,15 @@
|
||||
// import ky from 'ky'
|
||||
import fetch from 'isomorphic-unfetch'
|
||||
import pMemoize from 'p-memoize'
|
||||
import ExpiryMap from 'expiry-map'
|
||||
|
||||
import { api } from './config'
|
||||
import * as types from './types'
|
||||
|
||||
export const searchNotion = pMemoize(searchNotionImpl, { maxAge: 10000 })
|
||||
export const searchNotion = pMemoize(searchNotionImpl, {
|
||||
cacheKey: (args) => args[0]?.query,
|
||||
cache: new ExpiryMap(10000)
|
||||
})
|
||||
|
||||
async function searchNotionImpl(
|
||||
params: types.SearchParams
|
||||
@@ -18,8 +22,6 @@ async function searchNotionImpl(
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
console.log(res)
|
||||
|
||||
if (res.ok) {
|
||||
return res
|
||||
}
|
||||
|
||||
55
lib/tweet-embeds.ts
Normal file
55
lib/tweet-embeds.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { ExtendedRecordMap } from 'notion-types'
|
||||
import { fetchTweetAst } from 'static-tweets'
|
||||
import pMap from 'p-map'
|
||||
|
||||
export async function getTweetAstMap(recordMap: ExtendedRecordMap) {
|
||||
const blockIds = Object.keys(recordMap.block)
|
||||
const tweetIds: string[] = blockIds
|
||||
.map((blockId) => {
|
||||
const block = recordMap.block[blockId]?.value
|
||||
|
||||
if (block) {
|
||||
if (block.type === 'tweet') {
|
||||
const src = block.properties?.source?.[0]?.[0]
|
||||
|
||||
if (src) {
|
||||
const id = src.split('?')[0].split('/').pop()
|
||||
if (id) return id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
const tweetAsts = await pMap(
|
||||
tweetIds,
|
||||
async (tweetId) => {
|
||||
try {
|
||||
return {
|
||||
tweetId,
|
||||
tweetAst: await fetchTweetAst(tweetId)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('error fetching tweet info', tweetId, err)
|
||||
}
|
||||
},
|
||||
{
|
||||
concurrency: 4
|
||||
}
|
||||
)
|
||||
|
||||
const tweetAstMap = tweetAsts.reduce((acc, { tweetId, tweetAst }) => {
|
||||
if (tweetAst) {
|
||||
return {
|
||||
...acc,
|
||||
[tweetId]: tweetAst
|
||||
}
|
||||
} else {
|
||||
return acc
|
||||
}
|
||||
}, {})
|
||||
|
||||
return tweetAstMap
|
||||
}
|
||||
2
license
2
license
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Travis Fischer
|
||||
Copyright (c) 2022 Travis Fischer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
1
next-env.d.ts
vendored
1
next-env.d.ts
vendored
@@ -1,5 +1,4 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
// const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
enabled: process.env.ANALYZE === 'true'
|
||||
})
|
||||
|
||||
module.exports = withBundleAnalyzer({
|
||||
staticPageGenerationTimeout: 300,
|
||||
images: {
|
||||
domains: ['pbs.twimg.com']
|
||||
domains: [
|
||||
'www.notion.so',
|
||||
'notion.so',
|
||||
'images.unsplash.com',
|
||||
'pbs.twimg.com',
|
||||
'abs.twimg.com',
|
||||
'transitivebullsh.it'
|
||||
],
|
||||
formats: ['image/avif', 'image/webp']
|
||||
}
|
||||
})
|
||||
|
||||
54
package.json
54
package.json
@@ -7,67 +7,63 @@
|
||||
"repository": "transitive-bullshit/nextjs-notion-starter-kit",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=12"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"deploy": "vercel --prod",
|
||||
"deploy": "vercel deploy",
|
||||
"deps": "run-s deps:*",
|
||||
"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 ] && yarn link notion-client notion-types notion-utils react-notion-x || echo 'Skipping deps:link on CI'",
|
||||
"analyze": "cross-env ANALYZE=true next build",
|
||||
"analyze:server": "cross-env BUNDLE_ANALYZE=server next build",
|
||||
"analyze:browser": "cross-env BUNDLE_ANALYZE=browser next build",
|
||||
"test": "run-s test:*",
|
||||
"test:lint": "eslint .",
|
||||
"test:prettier": "prettier '**/*.{js,jsx,ts,tsx}' --check",
|
||||
"posttest": "run-s build"
|
||||
"test": "run-p test:*",
|
||||
"test:lint": "eslint '**/*.{ts,tsx}'",
|
||||
"test:prettier": "prettier '**/*.{js,jsx,ts,tsx}' --check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/firestore": "^4.10.1",
|
||||
"@keyv/redis": "^2.2.3",
|
||||
"classnames": "^2.3.1",
|
||||
"date-fns": "^2.25.0",
|
||||
"expiry-map": "^2.0.0",
|
||||
"fathom-client": "^3.0.0",
|
||||
"got": "^11.8.2",
|
||||
"isomorphic-unfetch": "^3.1.0",
|
||||
"keyv": "^4.1.1",
|
||||
"lqip-modern": "^1.2.0",
|
||||
"next": "^11.1.2",
|
||||
"next": "^12.1.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"notion-client": "^4.10.0",
|
||||
"notion-types": "^4.10.0",
|
||||
"notion-utils": "^4.10.0",
|
||||
"p-map": "^4.0.0",
|
||||
"p-memoize": "^4.0.0",
|
||||
"notion-client": "^6.0.8",
|
||||
"notion-types": "^6.0.6",
|
||||
"notion-utils": "^6.0.8",
|
||||
"p-map": "^5.3.0",
|
||||
"p-memoize": "^6.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-body-classname": "^1.3.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-notion-x": "^4.11.0",
|
||||
"react-static-tweets": "^0.5.3",
|
||||
"react-use": "^17.2.4",
|
||||
"static-tweets": "^0.5.3",
|
||||
"react-notion-x": "^6.0.8",
|
||||
"react-static-tweets": "^0.7.1",
|
||||
"react-use": "^17.3.2",
|
||||
"static-tweets": "^0.7.1",
|
||||
"use-dark-mode": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "^11.1.2",
|
||||
"@next/bundle-analyzer": "^12.1.0",
|
||||
"@types/classnames": "^2.3.1",
|
||||
"@types/node": "^16.11.2",
|
||||
"@types/node-fetch": "^3.0.3",
|
||||
"@types/react": "^17.0.31",
|
||||
"@typescript-eslint/eslint-plugin": "^5.1.0",
|
||||
"@typescript-eslint/parser": "^5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.15.0",
|
||||
"@typescript-eslint/parser": "^5.15.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-config-standard-react": "^11.0.1",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"eslint-plugin-node": "^11.0.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-promise": "^5.1.1",
|
||||
"eslint-plugin-react": "^7.26.1",
|
||||
"eslint": "^8.11.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.4.1",
|
||||
"typescript": "^4.4.4"
|
||||
|
||||
@@ -48,20 +48,20 @@ export default function App({ Component, pageProps }) {
|
||||
const router = useRouter()
|
||||
|
||||
React.useEffect(() => {
|
||||
function onRouteChangeComplete() {
|
||||
Fathom.trackPageview()
|
||||
}
|
||||
|
||||
if (fathomId) {
|
||||
Fathom.load(fathomId, fathomConfig)
|
||||
|
||||
function onRouteChangeComplete() {
|
||||
Fathom.trackPageview()
|
||||
}
|
||||
|
||||
router.events.on('routeChangeComplete', onRouteChangeComplete)
|
||||
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', onRouteChangeComplete)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
}, [router.events])
|
||||
|
||||
return <Component {...pageProps} />
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import { host } from '../lib/config'
|
||||
import { host } from '../../lib/config'
|
||||
|
||||
export default async (
|
||||
req: NextApiRequest,
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import * as types from '../lib/types'
|
||||
import { search } from '../lib/notion'
|
||||
import * as types from '../../lib/types'
|
||||
import { search } from '../../lib/notion'
|
||||
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method !== 'POST') {
|
||||
@@ -10,8 +10,9 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
|
||||
const searchParams: types.SearchParams = req.body
|
||||
|
||||
console.log('lambda search-notion', searchParams)
|
||||
console.log('<<< lambda search-notion', searchParams)
|
||||
const results = await search(searchParams)
|
||||
console.log('>>> lambda search-notion', results)
|
||||
|
||||
res.setHeader(
|
||||
'Cache-Control',
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import { SiteMap } from '../lib/types'
|
||||
import { host } from '../lib/config'
|
||||
import { getSiteMaps } from '../lib/get-site-maps'
|
||||
import { SiteMap } from '../../lib/types'
|
||||
import { host } from '../../lib/config'
|
||||
import { getSiteMaps } from '../../lib/get-site-maps'
|
||||
|
||||
export default async (
|
||||
req: NextApiRequest,
|
||||
@@ -24,9 +24,9 @@ export default async (
|
||||
res.end()
|
||||
}
|
||||
|
||||
const createSitemap = (
|
||||
siteMap: SiteMap
|
||||
) => `<?xml version="1.0" encoding="UTF-8"?>
|
||||
const createSitemap = (siteMap: SiteMap) =>
|
||||
`
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>${host}</loc>
|
||||
@@ -46,4 +46,5 @@ const createSitemap = (
|
||||
)
|
||||
.join('')}
|
||||
</urlset>
|
||||
`
|
||||
</xml>
|
||||
`
|
||||
150
readme.md
150
readme.md
@@ -1,6 +1,6 @@
|
||||
<p align="center">
|
||||
<a href="https://transitivebullsh.it/nextjs-notion-starter-kit">
|
||||
<img alt="Example article page" src="https://ssfy.io/https%3A%2F%2Fwww.notion.so%2Fimage%2Fhttps%253A%252F%252Fs3-us-west-2.amazonaws.com%252Fsecure.notion-static.com%252Fd147d76c-28a4-4cdd-a503-2d6bcc50a787%252Ftransitivebullsh.it__(5)-opt.jpg%3Ftable%3Dblock%26id%3D5b87b717-ca5b-49da-b17c-12c3eab1644a%26cache%3Dv2" width="689">
|
||||
<img alt="Example article page" src="https://user-images.githubusercontent.com/552829/160132094-12875e09-41ec-450a-80fc-ae8cd488129d.jpg" width="689">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -8,15 +8,13 @@
|
||||
|
||||
> The perfect starter kit for building websites with Next.js and Notion.
|
||||
|
||||
[](https://travis-ci.com/transitive-bullshit/nextjs-notion-starter-kit) [](https://prettier.io)
|
||||
[](https://github.com/transitive-bullshit/nextjs-notion-starter-kit/actions/workflows/build.yml) [](https://prettier.io)
|
||||
|
||||
## Intro
|
||||
|
||||
This repo is what I use to power my personal blog / portfolio site [transitivebullsh.it](https://transitivebullsh.it).
|
||||
This repo is what I use to power my personal blog and portfolio site [transitivebullsh.it](https://transitivebullsh.it).
|
||||
|
||||
It uses Notion as a CMS, fetching content from Notion and then uses [Next.js](https://nextjs.org/) and [react-notion-x](https://github.com/NotionX/react-notion-x) to render everything.
|
||||
|
||||
The site is then deployed to [Vercel](http://vercel.com).
|
||||
It uses Notion as a CMS, [react-notion-x](https://github.com/NotionX/react-notion-x), [Next.js](https://nextjs.org/), and [Vercel](http://vercel.com).
|
||||
|
||||
## Features
|
||||
|
||||
@@ -24,14 +22,12 @@ The site is then deployed to [Vercel](http://vercel.com).
|
||||
- Robust support for Notion content via [react-notion-x](https://github.com/NotionX/react-notion-x)
|
||||
- Next.js / TS / React / Notion
|
||||
- Excellent page speeds
|
||||
- Sexy LQIP image previews
|
||||
- Embedded GitHub comments
|
||||
- Automatic open graph images
|
||||
- Smooth image previews
|
||||
- Automatic pretty URLs
|
||||
- Automatic table of contents
|
||||
- Full support for dark mode
|
||||
- Quick search via CMD+P just like in Notion
|
||||
- Responsive for desktop / tablet / mobile
|
||||
- Quick search via CMD+K / CMD+P
|
||||
- Responsive for different devices
|
||||
- Optimized for Next.js and Vercel
|
||||
|
||||
## Setup
|
||||
@@ -50,25 +46,46 @@ All you really need to do to get started is edit `rootNotionPageId`. It defaults
|
||||
|
||||
You'll want to make your root Notion page **public** and then copy the link to your clipboard. Then extract the last part of the URL that looks like `d1b5dcf8b9ff425b8aef5ce6f0730202`, which is your page's Notion iD.
|
||||
|
||||
In order to find your Notion workspace ID (optional), just load any of your site's pages into your browser and open up the developer console. There will be a global variable that you can access called `block` which is the Notion data for the current page, and you just have to type `block.space_id` which will print out your page's workspace ID.
|
||||
In order to find your Notion workspace ID (optional), just load any of your site's pages into your browser and open up the developer console. There will be a global variable that you can access called `block` which is the Notion data for the current page. If you enter `block.space_id`, it will print out your page's workspace ID.
|
||||
|
||||
I recommend setting up a collection on your home page (optional; I use an inline gallery [here](https://notion.so/78fc5a4b88d74b0e824e29407e9f1ec1)) that contains all of your articles / projects / content. There are no structural constraints on your Notion workspace, however, so feel free to add content as you would normally in Notion. There are a few parts of the code with logic to only show comments on blog post pages (collection item detail pages).
|
||||
I recommend setting up a collection on your home page (optional; I use an inline gallery [here](https://notion.so/78fc5a4b88d74b0e824e29407e9f1ec1)) that contains all of your articles / projects / content. There are no structural constraints on your Notion workspace, however, so feel free to add content as you would normally in Notion.
|
||||
|
||||
## URL Paths
|
||||
|
||||
The app defaults to slightly different pathnames in dev and prod (though pasting any dev pathname into prod will work and vice-versa).
|
||||
The app defaults to slightly different URL paths in dev vs prod (though pasting any dev pathname into prod will work and vice-versa).
|
||||
|
||||
In development, it will use `/nextjs-notion-blog-d1b5dcf8b9ff425b8aef5ce6f0730202` which is a slugified version of the page's title suffixed with its Notion ID. I've found that it's really useful to always have the Notion Page ID front and center during local development.
|
||||
|
||||
In production, it will use `/nextjs-notion-blog` which is a bit nicer as it gets rid of the extra ID clutter.
|
||||
|
||||
The mapping of Notion ID to slugified page titles is done automatically for you as part of the build process. Just keep in mind that if you plan on changing page titles over time, you probably want to make sure old links will still work, and we don't currently provide a solution for detecting old links aside from Next.js built-in [support for redirects](https://nextjs.org/docs/api-reference/next.config.js/redirects).
|
||||
The mapping of Notion ID to slugified page titles is done automatically as part of the build process. Just keep in mind that if you plan on changing page titles over time, you probably want to make sure old links will still work, and we don't currently provide a solution for detecting old links aside from Next.js's built-in [support for redirects](https://nextjs.org/docs/api-reference/next.config.js/redirects).
|
||||
|
||||
See [mapPageUrl](./lib/map-page-url.ts) and [getCanonicalPageId](https://github.com/NotionX/react-notion-x/blob/master/packages/notion-utils/src/get-canonical-page-id.ts) from for more details.
|
||||
See [mapPageUrl](./lib/map-page-url.ts) and [getCanonicalPageId](https://github.com/NotionX/react-notion-x/blob/master/packages/notion-utils/src/get-canonical-page-id.ts) for more details.
|
||||
|
||||
NOTE: if you have multiple pages in your workspace with the same slugified name, the app will throw an error letting you know that there are duplicate URL pathnames.
|
||||
|
||||
## Theming
|
||||
## Preview Images
|
||||
|
||||
<p align="center">
|
||||
<img alt="Example preview image" src="https://user-images.githubusercontent.com/552829/160142320-35343317-aa9e-4710-bcf7-67e5cdec586d.gif" width="458">
|
||||
</p>
|
||||
|
||||
We use [next/image](https://nextjs.org/docs/api-reference/next/image) to serve efficient images, with preview images optionally generated via [lqip-modern](https://github.com/transitive-bullshit/lqip-modern). This gives us extremely optimized image support for sexy smooth images.
|
||||
|
||||
Preview images are **enabled by default**, but they can be slow to generate, so if you want to disable them, set `isPreviewImageSupportEnabled` to `false` in `site.config.js`.
|
||||
|
||||
If you want to cache generated preview images to speed up subsequent builds, you'll need to first set up an external [Redis](https://redis.io) data store. To enable redis caching, set `isRedisEnabled` to `true` in `site.config.js` and then set `REDIS_HOST` and `REDIS_PASSWORD` environment variables to point to your redis instance.
|
||||
|
||||
You can do this locally by adding a `.env` file:
|
||||
|
||||
```bash
|
||||
REDIS_HOST='TODO'
|
||||
REDIS_PASSWORD='TODO'
|
||||
```
|
||||
|
||||
Note that preview images and redis caching are both optional features. If you’d rather not deal with them, just disable them in your site config.
|
||||
|
||||
## Styles
|
||||
|
||||
All CSS styles that customize Notion content are located in [styles/notion.css](./styles/notion.css).
|
||||
|
||||
@@ -76,76 +93,17 @@ They mainly target global CSS classes exported by react-notion-x [styles.css](ht
|
||||
|
||||
It should be pretty easy to customize most styling-related things, especially with local development and hot reload.
|
||||
|
||||
### Dark Mode
|
||||
## Dark Mode
|
||||
|
||||
<p align="center">
|
||||
<img alt="Light Mode" src="https://ssfy.io/https%3A%2F%2Fwww.notion.so%2Fimage%2Fhttps%253A%252F%252Fs3-us-west-2.amazonaws.com%252Fsecure.notion-static.com%252F83ea9f0f-4761-4c0b-b53e-1913627975fc%252Ftransitivebullsh.it_-opt.jpg%3Ftable%3Dblock%26id%3Ded7e8f60-c6d1-449e-840b-5c7762505c44%26cache%3Dv2" width="45%">
|
||||
<img alt="Light Mode" src="https://transitive-bs.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F83ea9f0f-4761-4c0b-b53e-1913627975fc%2Ftransitivebullsh.it_-opt.jpg?table=block&id=ed7e8f60-c6d1-449e-840b-5c7762505c44&spaceId=fde5ac74-eea3-4527-8f00-4482710e1af3&width=2000&userId=&cache=v2" width="45%">
|
||||
|
||||
<img alt="Dark Mode" src="https://ssfy.io/https%3A%2F%2Fwww.notion.so%2Fimage%2Fhttps%253A%252F%252Fs3-us-west-2.amazonaws.com%252Fsecure.notion-static.com%252Fc0839d6c-7141-48df-8afd-69b27fed84aa%252Ftransitivebullsh.it__(1)-opt.jpg%3Ftable%3Dblock%26id%3D23b11fe5-d6df-422d-9674-39cf7f547523%26cache%3Dv2" width="45%">
|
||||
<img alt="Dark Mode" src="https://transitive-bs.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fc0839d6c-7141-48df-8afd-69b27fed84aa%2Ftransitivebullsh.it__(1)-opt.jpg?table=block&id=23b11fe5-d6df-422d-9674-39cf7f547523&spaceId=fde5ac74-eea3-4527-8f00-4482710e1af3&width=2000&userId=&cache=v2" width="45%">
|
||||
</p>
|
||||
|
||||
Dark mode is fully supported and can be toggled via the sun / moon icon in the footer.
|
||||
|
||||
## Extras
|
||||
|
||||
All extra dependencies are optional -- the project should work just fine out of the box.
|
||||
|
||||
If you want to copy some of the fancier elements of my site, then you'll have to set up a few extras.
|
||||
|
||||
### Fathom Analytics
|
||||
|
||||
[Fathom](https://usefathom.com/ref/42TFOZ) provides a lightweight alternative to Google Analytics.
|
||||
|
||||
It's optional, but I really love how simple and elegant their solution is.
|
||||
|
||||
To enable analytics, just add a `NEXT_PUBLIC_FATHOM_ID` environment variable.
|
||||
|
||||
This environment variable will only be taken into account in production, so you don't have to worry about messing up your analytics with localhost development.
|
||||
|
||||
### GitHub Comments
|
||||
|
||||
<p align="center">
|
||||
<img alt="Embedded GitHub Comments" src="https://ssfy.io/https%3A%2F%2Fwww.notion.so%2Fimage%2Fhttps%253A%252F%252Fs3-us-west-2.amazonaws.com%252Fsecure.notion-static.com%252Fa43f996c-de07-4d8a-8461-b35f9d43e4b2%252Fcomments-desktop-opt.jpg%3Ftable%3Dblock%26id%3Ded07d7c2-57c9-4aba-81b3-f5fa069371d4%26cache%3Dv2" width="420">
|
||||
</p>
|
||||
|
||||
[Utteranc.es](https://utteranc.es/) is an amazing [open source project](https://github.com/utterance/utterances) which enables developers to embed GitHub issues as a comments section on their websites. Genius.
|
||||
|
||||
The integration is really simple. Just edit the `utterancesGitHubRepo` config value to point to the repo you'd like to use for issue comments.
|
||||
|
||||
You probably want to read through the Utterances docs before enabling this in production, since there are some subtleties around how issues get mapped to pages on your site, but overall the setup was super easy imho and I love the results.
|
||||
|
||||
### Preview Images
|
||||
|
||||
This is a really cool feature that's inspired by Medium's smooth image loading, where we first load a low quality, blurred version of an image and animate in the full quality version once it loads. It's such a nice effect, but it does add a bit of work to set up.
|
||||
|
||||
If `isPreviewImageSupportEnabled` is set to `true`, then the app will compute LQIP images via [lqip-modern](https://github.com/transitive-bullshit/lqip-modern) for all images referenced by your Notion workspace. These will be stored in a Google Firebase collection (as base64 JPEG data), so they only need to be computed once.
|
||||
|
||||
You'll have to set up your own Google Firebase instance of Firestore and supply three environment variables:
|
||||
|
||||
```bash
|
||||
# base64-encoded string containing your google credentials json file
|
||||
GOOGLE_APPLICATION_CREDENTIALS=
|
||||
|
||||
# name of your google cloud project
|
||||
GCLOUD_PROJECT=
|
||||
|
||||
# name of the firebase collection to store images in
|
||||
FIREBASE_COLLECTION_IMAGES=
|
||||
```
|
||||
|
||||
The actual work happens in the [create-preview-image](./api/create-preview-image) serverless function.
|
||||
|
||||
### Automatic Social Images
|
||||
|
||||
<p align="center">
|
||||
<img alt="Auto-generated social image" src="https://ssfy.io/https%3A%2F%2Fwww.notion.so%2Fimage%2Fhttps%253A%252F%252Fs3-us-west-2.amazonaws.com%252Fsecure.notion-static.com%252Fe1877c31-0bc9-46b7-8aaf-7bcae21baf2b%252Fsocial-image-opt.jpeg%3Ftable%3Dblock%26id%3D735b04d2-2a77-4035-8942-a17f8d41fe83%26cache%3Dv2" width="420">
|
||||
</p>
|
||||
|
||||
Open Graph images like this one will be generated for each page of your site automatically based each page's content.
|
||||
|
||||
Note that you shouldn't have to do anything extra to enable this feature as long as you're deploying to Vercel.
|
||||
|
||||
### Automatic Table of Contents
|
||||
## Automatic Table of Contents
|
||||
|
||||
<p align="center">
|
||||
<img alt="Smooth ToC Scrollspy" src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fcb2df62d-9028-440b-964b-117711450921%2Ftoc2.gif?table=block&id=d7e9951b-289c-4ff2-8b82-b0a61fe260b1&cache=v2" width="240">
|
||||
@@ -155,33 +113,27 @@ By default, every article page will have a table of contents displayed as an `as
|
||||
|
||||
If a page has less than `minTableOfContentsItems` (default 3), the table of contents will be hidden. It is also hidden on the index page and if the browser window is too small.
|
||||
|
||||
This table of contents uses the same logic that Notion uses for its built-in Table of Contents block (see [getPageTableOfContents](https://github.com/NotionX/react-notion-x/blob/master/packages/notion-utils/src/get-page-table-of-contents.ts) for the underlying logic and associated unit tests).
|
||||
This table of contents uses the same logic that Notion uses for its built-in Table of Contents block (see [getPageTableOfContents](https://github.com/NotionX/react-notion-x/blob/master/packages/notion-utils/src/get-page-table-of-contents.ts) for the underlying logic).
|
||||
|
||||
## Screenshots
|
||||
|
||||
### Mobile Article Page
|
||||
## Responsive
|
||||
|
||||
<p align="center">
|
||||
<a href="https://transitivebullsh.it/free-resources-for-indie-saas-devs">
|
||||
<img alt="Mobile Article Page" src="https://ssfy.io/https%3A%2F%2Fwww.notion.so%2Fimage%2Fhttps%253A%252F%252Fs3-us-west-2.amazonaws.com%252Fsecure.notion-static.com%252F6c05a0f9-59a0-4322-bef9-3f08fe4efc6a%252Farticle-mobile-opt.jpg%3Ftable%3Dblock%26id%3Da1eb2263-fdf1-4d51-a3d4-8a02cb32bbba%26cache%3Dv2" width="300">
|
||||
</a>
|
||||
<img alt="Mobile article page" src="https://user-images.githubusercontent.com/552829/160132983-c2dd5830-80b3-4a0e-a8f1-abab5dbeed11.jpg" width="300">
|
||||
</p>
|
||||
|
||||
### Desktop Home Page
|
||||
All pages are designed to be responsive across common device sizes.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://transitivebullsh.it">
|
||||
<img alt="Desktop Home Page" src="https://ssfy.io/https%3A%2F%2Fwww.notion.so%2Fimage%2Fhttps%253A%252F%252Fs3-us-west-2.amazonaws.com%252Fsecure.notion-static.com%252F1d3ab4b2-60af-4b95-b35d-cac5d440b8ca%252Ftransitivebullsh.it_-opt.jpg%3Ftable%3Dblock%26id%3D97f445e8-2da1-41cd-996a-5ad0e73a1d79%26cache%3Dv2" width="600">
|
||||
</a>
|
||||
</p>
|
||||
## Fathom Analytics
|
||||
|
||||
### Desktop Article Page (Dark Mode)
|
||||
[Fathom](https://usefathom.com/ref/42TFOZ) provides a lightweight alternative to Google Analytics.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://transitivebullsh.it/free-resources-for-indie-saas-devs">
|
||||
<img alt="Desktop Article Page" src="https://ssfy.io/https%3A%2F%2Fwww.notion.so%2Fimage%2Fhttps%253A%252F%252Fs3-us-west-2.amazonaws.com%252Fsecure.notion-static.com%252Fb564d13f-b71b-4473-8531-65b5dd9b995f%252Ftransitivebullsh.it__(4)-opt.jpg%3Ftable%3Dblock%26id%3D16e03de2-0df7-4232-a129-e1666505c4d2%26cache%3Dv2" width="600">
|
||||
</a>
|
||||
</p>
|
||||
To enable analytics, just add a `NEXT_PUBLIC_FATHOM_ID` environment variable, which will only be used in production.
|
||||
|
||||
Note that this feature is completely optional.
|
||||
|
||||
## Contributing
|
||||
|
||||
See the [contribution guide](contributing.md) and join our amazing list of [contributors](https://github.com/transitive-bullshit/nextjs-notion-starter-kit/graphs/contributors)!
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
// where it all starts -- the site's root Notion page (required)
|
||||
rootNotionPageId: '78fc5a4b88d74b0e824e29407e9f1ec1',
|
||||
|
||||
@@ -27,17 +27,13 @@ module.exports = {
|
||||
defaultPageCover: null,
|
||||
defaultPageCoverPosition: 0.5,
|
||||
|
||||
// image CDN host to proxy all image requests through (optional)
|
||||
// NOTE: this requires you to set up an external image proxy
|
||||
imageCDNHost: null,
|
||||
|
||||
// Utteranc.es comments via GitHub issue comments (optional)
|
||||
utterancesGitHubRepo: null,
|
||||
|
||||
// whether or not to enable support for LQIP preview images (optional)
|
||||
// NOTE: this requires you to set up Google Firebase and add the environment
|
||||
// variables specified in .env.example
|
||||
isPreviewImageSupportEnabled: false,
|
||||
isPreviewImageSupportEnabled: true,
|
||||
|
||||
// whether or not redis is enabled for caching generated preview images (optional)
|
||||
// NOTE: if you enable redis, you need to set the `REDIS_HOST` and `REDIS_PASSWORD`
|
||||
// environment variables. see the readme for more info
|
||||
isRedisEnabled: false,
|
||||
|
||||
// map of notion page IDs to URL paths (optional)
|
||||
// any pages defined here will override their default URL paths
|
||||
|
||||
@@ -19,10 +19,6 @@ body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.utterances {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.static-tweet blockquote {
|
||||
margin: 0;
|
||||
margin-block-start: 0;
|
||||
|
||||
@@ -85,7 +85,6 @@
|
||||
|
||||
.notion-collection-card-cover {
|
||||
border-radius: 16px;
|
||||
overflow: visible;
|
||||
box-shadow: 2px 2px 8px 4px rgba(15, 15, 15, 0.1);
|
||||
}
|
||||
|
||||
@@ -179,14 +178,21 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.notion-page-cover {
|
||||
max-width: 1200px;
|
||||
.notion-page-cover-wrapper,
|
||||
.notion-page-cover-wrapper span,
|
||||
.notion-page-cover-wrapper img {
|
||||
max-width: 1200px !important;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.notion-page-cover-wrapper {
|
||||
box-shadow: 2px 2px 8px 4px rgba(15, 15, 15, 0.1);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.notion-page-cover {
|
||||
.notion-page-cover-wrapper,
|
||||
.notion-page-cover-wrapper span,
|
||||
.notion-page-cover-wrapper img {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
@@ -318,11 +324,15 @@
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
/* if you don't want rounded page images, remove this */
|
||||
.notion-page-icon-wrapper img.notion-page-icon {
|
||||
/* if you don't want rounded page icon images, remove this */
|
||||
.notion-page-icon-hero.notion-page-icon-image {
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 8px 40px 0 rgb(0 0 0 / 21%);
|
||||
}
|
||||
.notion-page-icon-hero.notion-page-icon-image span,
|
||||
.notion-page-icon-hero.notion-page-icon-image img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.notion-header {
|
||||
background: hsla(0, 0%, 100%, 0.8);
|
||||
|
||||
@@ -23,15 +23,15 @@
|
||||
}
|
||||
|
||||
.dark-mode .notion .notion-inline-code {
|
||||
background: rgb(71,76,80) !important;
|
||||
background: rgb(71, 76, 80) !important;
|
||||
color: #ff4081;
|
||||
padding: .2em .4em !important;
|
||||
padding: 0.2em 0.4em !important;
|
||||
}
|
||||
|
||||
.light-mode .notion .notion-inline-code {
|
||||
color: #ff4081;
|
||||
background: rgba(55,53,47, .1) !important;
|
||||
padding: .2em .4em !important;
|
||||
background: rgba(55, 53, 47, 0.1) !important;
|
||||
padding: 0.2em 0.4em !important;
|
||||
}
|
||||
|
||||
.token.cdata,
|
||||
@@ -118,3 +118,8 @@ pre[class*='language-'] > code {
|
||||
box-shadow: none !important;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
pre[class*='language-']:before,
|
||||
pre[class*='language-']:after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"baseUrl": ".",
|
||||
"typeRoots": ["./node_modules/@types"]
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
"incremental": true
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
|
||||
|
||||
Reference in New Issue
Block a user