import cs from 'classnames' import dynamic from 'next/dynamic' import Image from 'next/legacy/image' import Link from 'next/link' import { useRouter } from 'next/router' import { type PageBlock } from 'notion-types' import { formatDate, getBlockTitle, getPageProperty } from 'notion-utils' import * as React from 'react' import BodyClassName from 'react-body-classname' import { type NotionComponents, NotionRenderer, useNotionContext } from 'react-notion-x' import { EmbeddedTweet, TweetNotFound, TweetSkeleton } from 'react-tweet' import { useSearchParam } from 'react-use' import type * as types from '@/lib/types' import * as config from '@/lib/config' import { mapImageUrl } from '@/lib/map-image-url' import { getCanonicalPageUrl, mapPageUrl } from '@/lib/map-page-url' import { searchNotion } from '@/lib/search-notion' import { useDarkMode } from '@/lib/use-dark-mode' import { Footer } from './Footer' import { GitHubShareButton } from './GitHubShareButton' import { Loading } from './Loading' import { NotionPageHeader } from './NotionPageHeader' import { Page404 } from './Page404' import { PageAside } from './PageAside' import { PageHead } from './PageHead' import styles from './styles.module.css' // ----------------------------------------------------------------------------- // dynamic imports for optional components // ----------------------------------------------------------------------------- const Code = dynamic(() => import('react-notion-x/build/third-party/code').then(async (m) => { // add / remove any prism syntaxes here await Promise.allSettled([ // @ts-expect-error Ignore prisma types import('prismjs/components/prism-markup-templating.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-markup.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-bash.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-c.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-cpp.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-csharp.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-docker.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-java.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-js-templates.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-coffeescript.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-diff.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-git.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-go.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-graphql.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-handlebars.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-less.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-makefile.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-markdown.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-objectivec.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-ocaml.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-python.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-reason.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-rust.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-sass.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-scss.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-solidity.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-sql.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-stylus.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-swift.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-wasm.js'), // @ts-expect-error Ignore prisma types import('prismjs/components/prism-yaml.js') ]) return m.Code }) ) 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 } ) const Modal = dynamic( () => import('react-notion-x/build/third-party/modal').then((m) => { m.Modal.setAppElement('.notion-viewport') return m.Modal }), { ssr: false } ) // Helper function to filter recordMap by language function filterRecordMapByLanguage( recordMap: types.ExtendedRecordMap, targetLang: string ): types.ExtendedRecordMap { // Deep clone collection_query to avoid mutating the original const newRecordMap: types.ExtendedRecordMap = { ...recordMap, collection_query: structuredClone( recordMap.collection_query ) as types.ExtendedRecordMap['collection_query'] } for (const collectionId of Object.keys(newRecordMap.collection_query || {})) { const collection = newRecordMap.collection[collectionId]?.value if (!collection) continue const schema = collection.schema if (!schema) continue const langPropId = Object.keys(schema).find( (key) => schema[key]?.name?.toLowerCase() === 'language' ) if (!langPropId) continue const views = newRecordMap.collection_query[collectionId] if (!views) continue for (const viewId of Object.keys(views)) { const view = views[viewId] if (view?.collection_group_results?.blockIds) { const originalBlockIds = view.collection_group_results.blockIds const filteredBlockIds = originalBlockIds.filter((blockId: string) => { const block = newRecordMap.block[blockId]?.value if (!block) return false const propValue = block.properties?.[langPropId] // If no language property is set, show the post if (!propValue) return true // Validate the property format if ( !Array.isArray(propValue) || propValue.length === 0 || !Array.isArray(propValue[0]) || typeof propValue[0][0] !== 'string' ) { // If the language property is present but not in the expected format, // treat it as if no specific language is set and keep the block. return true } const langText = propValue[0][0].toLowerCase().trim() // Show if language matches or is set to 'all' return langText.includes(targetLang) || langText === 'all' }) view.collection_group_results.blockIds = filteredBlockIds } } } return newRecordMap } function Tweet({ id }: { id: string }) { const { recordMap } = useNotionContext() const tweet = (recordMap as types.ExtendedTweetRecordMap)?.tweets?.[id] return ( }> {tweet ? : } ) } const propertyLastEditedTimeValue = ( { block, pageHeader }: any, defaultFn: () => React.ReactNode ) => { if (pageHeader && block?.last_edited_time) { return `Last updated ${formatDate(block?.last_edited_time, { month: 'long' })}` } return defaultFn() } const propertyDateValue = ( { data, schema, pageHeader }: any, defaultFn: () => React.ReactNode ) => { if (pageHeader && schema?.name?.toLowerCase() === 'published') { const publishDate = data?.[0]?.[1]?.[0]?.[1]?.start_date if (publishDate) { return `${formatDate(publishDate, { month: 'long' })}` } } return defaultFn() } const propertyTextValue = ( { schema, pageHeader }: any, defaultFn: () => React.ReactNode ) => { if (pageHeader && schema?.name?.toLowerCase() === 'author') { return {defaultFn()} } return defaultFn() } export function NotionPage({ site, recordMap, error, pageId }: types.PageProps) { const router = useRouter() const lite = useSearchParam('lite') // Use state to store filtered recordMap, initialized with original const [filteredRecordMap, setFilteredRecordMap] = React.useState(() => { if (config.isI18nEnabled && recordMap) { return filterRecordMapByLanguage(recordMap, 'en') } return recordMap }) // Apply language filtering only after mount to avoid hydration mismatch React.useEffect(() => { if (!config.isI18nEnabled || !recordMap) { setFilteredRecordMap(recordMap) return } const browserLang = typeof navigator !== 'undefined' && navigator.language ? navigator.language : 'en' if (!browserLang) { setFilteredRecordMap(recordMap) return } const langCode = (browserLang.split('-')[0] || 'en').toLowerCase() setFilteredRecordMap(filterRecordMapByLanguage(recordMap, langCode)) }, [recordMap, config.isI18nEnabled]) const components = React.useMemo>( () => ({ nextLegacyImage: Image, nextLink: Link, Code, Collection, Equation, Pdf, Modal, Tweet, Header: NotionPageHeader, propertyLastEditedTimeValue, propertyTextValue, propertyDateValue }), [] ) // lite mode is for oembed const isLiteMode = lite === 'true' const { isDarkMode } = useDarkMode() const siteMapPageUrl = React.useMemo(() => { const params: any = {} if (lite) params.lite = lite const searchParams = new URLSearchParams(params) return site ? mapPageUrl(site, filteredRecordMap!, searchParams) : undefined }, [site, filteredRecordMap, lite]) const keys = Object.keys(filteredRecordMap?.block || {}) const block = filteredRecordMap?.block?.[keys[0]!]?.value // const isRootPage = // parsePageId(block?.id) === parsePageId(site?.rootNotionPageId) const isBlogPost = block?.type === 'page' && block?.parent_table === 'collection' const showTableOfContents = !!isBlogPost const minTableOfContentsItems = 3 const pageAside = React.useMemo( () => ( ), [block, filteredRecordMap, isBlogPost] ) const footer = React.useMemo(() =>