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 ( +
+
+
{children}
+
+
+ ) +} + +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 */} +