diff --git a/next.config.js b/next.config.js
index 272cd4d8..153ed944 100644
--- a/next.config.js
+++ b/next.config.js
@@ -44,6 +44,10 @@ module.exports = withBundleAnalyzer({
webpack(cfg) {
validateRequiredEnv()
+
+ // disable resolving canvas module for react-pdf in Next.js
+ // https://github.com/wojtekmaj/react-pdf?tab=readme-ov-file#nextjs
+ cfg.resolve.alias.canvas = false
return cfg
},
images: {
diff --git a/site.config.js b/site.config.js
index 2bf33377..38f5e7d9 100644
--- a/site.config.js
+++ b/site.config.js
@@ -17,7 +17,7 @@ const normalizeId = (id) => {
// env-var cannot read `NEXT_PUBLIC_` prefix env variables on client-side
const currentEnv = process.env.NEXT_PUBLIC_APP_ENV || 'production'
-module.exports = {
+const siteConfig = {
aws: {
s3bucket: env.get('AWS_S3_BUCKET').asString(),
},
@@ -41,7 +41,6 @@ module.exports = {
url: env.get('CACHE_CLIENT_API_URL').asString(),
},
cdnHost: 'static.dazedbear.pro',
- currentEnv,
failsafe: {
// AWS S3 upload limit rate: 3500 per sec, ref: https://docs.aws.amazon.com/zh_tw/AmazonS3/latest/userguide/optimizing-performance.html
// concurrency limit to 30 since redis max connection is fixed to 30 based on the basic plan, ref: https://redis.com/redis-enterprise-cloud/pricing/
@@ -272,3 +271,8 @@ module.exports = {
},
},
}
+
+siteConfig.currentEnv = currentEnv
+siteConfig.currentWebsite = siteConfig.website[currentEnv]
+
+module.exports = siteConfig
diff --git a/src/app/[pageName]/[pageSlug]/page.tsx b/src/app/[pageName]/[pageSlug]/page.tsx
new file mode 100644
index 00000000..a835ca9c
--- /dev/null
+++ b/src/app/[pageName]/[pageSlug]/page.tsx
@@ -0,0 +1,52 @@
+import NotionArticleDetailPage from '../../notion/article-detail-page'
+import { getNotionContent } from '../../notion/content'
+import { PAGE_TYPE_NOTION_ARTICLE_DETAIL_PAGE } from '../../../libs/constant'
+import { getPageMeta } from '../../../libs/util'
+import { getPageProperty } from '../../../libs/notion'
+import log from '../../../libs/server/log'
+
+export async function generateMetadata({ params, searchParams }) {
+ const { pageName, pageSlug } = params
+
+ // hack way to get fetched article property.
+ // TODO: need to find a way to pass property instead of redundant request.
+ const { pageContent, pageId } = await getNotionContent({
+ pageType: PAGE_TYPE_NOTION_ARTICLE_DETAIL_PAGE,
+ pageName,
+ pageSlug,
+ searchParams,
+ })
+ const property: any = getPageProperty({ pageId, recordMap: pageContent })
+ const metaOverride = {
+ title: property?.PageTitle,
+ }
+
+ return getPageMeta(metaOverride, pageName)
+}
+
+const ArticleListPage = async ({ params, searchParams }) => {
+ const { pageName, pageSlug } = params
+ const { menuItems, pageContent, pageId, toc } = await getNotionContent({
+ pageType: PAGE_TYPE_NOTION_ARTICLE_DETAIL_PAGE,
+ pageName,
+ pageSlug,
+ searchParams,
+ })
+
+ log({
+ category: PAGE_TYPE_NOTION_ARTICLE_DETAIL_PAGE,
+ message: `dumpaccess to /${pageName}/${pageSlug}`,
+ level: 'info',
+ })
+ return (
+
+ )
+}
+
+export default ArticleListPage
diff --git a/src/app/[pageName]/page.tsx b/src/app/[pageName]/page.tsx
new file mode 100644
index 00000000..237bb578
--- /dev/null
+++ b/src/app/[pageName]/page.tsx
@@ -0,0 +1,33 @@
+import NotionArticleListPage from '../notion/article-list-page'
+import { getNotionContent } from '../notion/content'
+import { PAGE_TYPE_NOTION_ARTICLE_LIST_PAGE } from '../../libs/constant'
+import { getPageMeta } from '../../libs/util'
+import log from '../../libs/server/log'
+
+export async function generateMetadata({ params: { pageName } }) {
+ return getPageMeta({}, pageName)
+}
+
+const ArticleListPage = async ({ params, searchParams }) => {
+ const { pageName } = params
+ const { menuItems, articleStream } = await getNotionContent({
+ pageType: PAGE_TYPE_NOTION_ARTICLE_LIST_PAGE,
+ pageName,
+ searchParams,
+ })
+
+ log({
+ category: PAGE_TYPE_NOTION_ARTICLE_LIST_PAGE,
+ message: `dumpaccess to /${pageName}`,
+ level: 'info',
+ })
+ return (
+
+ )
+}
+
+export default ArticleListPage
diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx
new file mode 100644
index 00000000..7e68b381
--- /dev/null
+++ b/src/app/about/page.tsx
@@ -0,0 +1,28 @@
+import { getPageMeta } from '../../libs/util'
+import NotionSinglePage from '../notion/single-page'
+import { getNotionContent } from '../notion/content'
+import { PAGE_TYPE_NOTION_SINGLE_PAGE } from '../../libs/constant'
+import log from '../../libs/server/log'
+
+const pageName = 'about'
+
+export async function generateMetadata() {
+ return getPageMeta({}, pageName)
+}
+
+const AboutPage = async ({ searchParams }) => {
+ const { pageContent } = await getNotionContent({
+ pageType: PAGE_TYPE_NOTION_SINGLE_PAGE,
+ pageName,
+ searchParams,
+ })
+
+ log({
+ category: PAGE_TYPE_NOTION_SINGLE_PAGE,
+ message: `dumpaccess to /${pageName}`,
+ level: 'info',
+ })
+ return
+}
+
+export default AboutPage
diff --git a/src/pages/api/sitemap.ts b/src/app/api/sitemap/route.ts
similarity index 56%
rename from src/pages/api/sitemap.ts
rename to src/app/api/sitemap/route.ts
index 94f5c3ca..0fa0f4aa 100644
--- a/src/pages/api/sitemap.ts
+++ b/src/app/api/sitemap/route.ts
@@ -1,42 +1,35 @@
-import { NextApiRequest, NextApiResponse } from 'next'
-import {
- getCategory,
- validateRequest,
- setAPICacheHeaders,
-} from '../../libs/server/api'
+import type { NextRequest } from 'next/server'
import get from 'lodash/get'
import { SitemapStream, streamToPromise } from 'sitemap'
import { Readable } from 'stream'
import pMap from 'p-map'
import * as dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
-import log from '../../libs/server/log'
-import { fetchArticleStream } from '../../libs/server/page'
+import log from '../../../libs/server/log'
+import { fetchArticleStream } from '../../../libs/server/page'
import {
transformArticleStream,
transformPageUrls,
-} from '../../libs/server/transformer'
+} from '../../../libs/server/transformer'
import {
currentEnv,
pages,
notion,
cache as cacheConfig,
website,
-} from '../../../site.config'
-import cacheClient from '../../libs/server/cache'
-import { FORCE_CACHE_REFRESH_QUERY } from '../../libs/constant'
-
-const route = '/sitemap'
-const methods = ['GET']
+} from '../../../../site.config'
+import cacheClient from '../../../libs/server/cache'
+import { FORCE_CACHE_REFRESH_QUERY } from '../../../libs/constant'
dayjs.extend(utc)
-const category = getCategory(route)
-const generateSiteMapXml = async (req) => {
+const category = 'API route: /api/sitemap'
+
+const generateSiteMapXml = async () => {
// get all enabled static page paths
const pageUrls = Object.values(pages)
- .map((item) => item.enabled && item.page)
- .filter((path) => path)
+ .filter((item) => item.enabled)
+ .map((item) => item.page)
// get all enabled notion list page paths
const currentNotionListUrls: string[][] = await pMap(
@@ -48,14 +41,12 @@ const generateSiteMapXml = async (req) => {
log({
category,
message: `skip generate urls since this pageName is disabled | pageName: ${pageName}`,
- req,
})
return []
}
switch (pageType) {
case 'stream': {
const response = await fetchArticleStream({
- req,
pageName,
category,
})
@@ -80,7 +71,7 @@ const generateSiteMapXml = async (req) => {
)
// all collected urls
- const urls = [].concat(pageUrls, notionUrls).map((url) => ({ url }))
+ const urls = [...pageUrls, ...notionUrls].map((url) => ({ url }))
// generate sitemap xml
const stream = new SitemapStream({
@@ -95,32 +86,57 @@ const generateSiteMapXml = async (req) => {
return sitemapXml
}
-const handler = async (req: NextApiRequest, res: NextApiResponse) => {
- try {
- validateRequest(req, { route, methods })
+export async function GET(req: NextRequest) {
+ const searchParams = req.nextUrl.searchParams
+ const headers = req.headers
+ try {
log({
category,
message: 'dumpaccess',
- req,
})
// sitemap cache
- cacheConfig.forceRefresh = req.query[FORCE_CACHE_REFRESH_QUERY] === '1'
+ cacheConfig.forceRefresh =
+ searchParams.get(FORCE_CACHE_REFRESH_QUERY) === '1'
+
const sitemapXmlKey = `sitemap_${dayjs.utc().format('YYYY-MM-DD')}`
const sitemapXml = await cacheClient.proxy(
sitemapXmlKey,
'/api/sitemap',
- generateSiteMapXml.bind(this, req),
+ generateSiteMapXml.bind(this),
{ ttl: cacheConfig.ttls.sitemap }
)
- setAPICacheHeaders(res)
- res.setHeader('Content-Type', 'application/xml')
- res.status(200).end(sitemapXml)
+
+ const newHeaders = {
+ ...headers,
+ 'Content-Type': 'application/xml',
+ /**
+ * < s-maxage: data is fresh, serve cache. X-Vercel-Cache HIT
+ * s-maxage - stale-while-revalidate: data is stale, still serve cache and start background new cache generation. X-Vercel-Cache STALE
+ * > stale-while-revalidate: data is stale and cache won't be used any more. X-Vercel-Cache MISS
+ *
+ * @see https://vercel.com/docs/concepts/edge-network/caching#serverless-functions---lambdas
+ * @see https://vercel.com/docs/concepts/edge-network/x-vercel-cache
+ * @see https://web.dev/stale-while-revalidate/
+ */
+ 'Cache-Control': 'public, s-maxage=30, stale-while-revalidate=86400',
+ }
+
+ return new Response(sitemapXml, {
+ status: 200,
+ headers: newHeaders,
+ })
} catch (err) {
const statusCode = err.status || 500
- res.status(statusCode).send(err.message)
+
+ log({
+ category,
+ message: err.message,
+ })
+
+ return new Response('Oops, something went wrong.', {
+ status: statusCode,
+ })
}
}
-
-export default handler
diff --git a/src/app/app.tsx b/src/app/app.tsx
new file mode 100644
index 00000000..2d70254b
--- /dev/null
+++ b/src/app/app.tsx
@@ -0,0 +1,37 @@
+'use client'
+
+import { usePathname } from 'next/navigation'
+import Header from '../components/header'
+import Footer from '../components/footer'
+import {
+ useCodeSyntaxHighlight,
+ useResizeHandler,
+ useInitLogRocket,
+} from '../libs/client/hooks'
+import wrapper from '../libs/client/store'
+
+// TODO: new progress bar while route change
+
+const App = ({
+ // Layouts must accept a children prop.
+ // This will be populated with nested layouts or pages
+ children,
+}: {
+ children: React.ReactNode
+}) => {
+ useInitLogRocket()
+ useResizeHandler()
+ useCodeSyntaxHighlight()
+
+ const pathname = usePathname()
+
+ return (
+
+ )
+}
+
+export default wrapper.withRedux(App)
diff --git a/src/app/custom-script.tsx b/src/app/custom-script.tsx
new file mode 100644
index 00000000..16f81bb7
--- /dev/null
+++ b/src/app/custom-script.tsx
@@ -0,0 +1,79 @@
+'use client'
+
+import {
+ FAILSAFE_PAGE_GENERATION_QUERY,
+ END_TO_END_TEST_QUERY,
+} from '../libs/constant'
+import {
+ communityFeatures,
+ currentEnv,
+ trackingSettings,
+} from '../../site.config'
+
+const isLocal = currentEnv === 'development'
+const prependCheck = (inputScript) => `if(!window.DBS.isBot){${inputScript}}`
+
+const CustomScript = () => {
+ return (
+ <>
+ {/* custom script for bot query detection */}
+