diff --git a/mhhung/README.md b/mhhung/README.md
new file mode 100644
index 0000000..9e43796
--- /dev/null
+++ b/mhhung/README.md
@@ -0,0 +1,70 @@
+# Typography 主題架構說明
+
+本專案是部落格佈景主題(Typography),主要透過 `index.js` 匯出多種佈局組件,方便在 Next.js / Notion-Blog 專案中依照情境載入。下方整理目前的檔案架構與各模組職責,以及常見的使用方式。
+
+## 專案結構
+
+```
+.
+├── components/ # 主題客製化的視覺組件(TopBar、Footer、BlogPostBar…)
+├── config.js # 主題對外設定(標題、選單顯示開關…)
+├── index.js # 封裝後的佈局出口,對外統一匯出
+├── layouts/ # 各種頁面佈局拆分檔案
+│ ├── ArchiveLayout.jsx # 文章歸檔頁
+│ ├── BaseLayout.jsx # 共同外殼(搜尋、導覽、跳頂按鈕…)
+│ ├── ListLayouts.jsx # 首頁、文章列表、搜尋列表
+│ ├── NotFoundLayout.jsx # 404 頁面
+│ ├── SlugLayout.jsx # 單篇文章頁
+│ └── TaxonomyLayouts.jsx # 分類與標籤頁
+├── style.js # 只針對此主題作用的全域樣式
+└── utils/
+ └── groupArticlesByYear.js # 文章依年份分組的小工具
+```
+
+## 主要佈局與職責
+
+- `LayoutBase`:最外層骨架,負責載入樣式、導覽列、頁尾、搜尋與 Loading 狀態,其他頁面內容會以 children 注入。
+- `LayoutIndex` / `LayoutPostList`:顯示文章列表或首頁,會掛上 `BlogPostBar` 與 `BlogListPage`。
+- `LayoutSearch`:在文章列表基礎上,透過 `replaceSearchResult` 將關鍵字高亮。
+- `LayoutArchive`:使用 `groupArticlesByYearArray` 將文章依年份分組後渲染。
+- `LayoutSlug`:單篇文章頁面,負責文章資訊、廣告、前後文推薦與留言區。
+- `LayoutCategoryIndex` / `LayoutTagIndex`:顯示分類與標籤的索引列表。
+- `Layout404`:等待 Notion 內容載入後做導向,或直接顯示 404。
+
+## 使用說明
+
+1. **在 Next.js 主題系統中載入**
+ ```jsx
+ import { LayoutBase, LayoutIndex } from 'mhhung'
+
+ const Home = props => (
+
+
+
+ )
+
+ export default Home
+ ```
+ - `LayoutBase` 會處理共同外殼;`LayoutIndex` 則是此頁的實際內容。
+ - 其他頁面可依需求替換為 `LayoutArchive`、`LayoutSlug`…等對應佈局。
+
+2. **設定主題參數**
+ - 編輯 `config.js`,可調整部落格名稱、是否展示分類/標籤/歸檔選單,以及文章推薦、封面等開關。
+ - 也可以透過環境變數(如 `NEXT_PUBLIC_TYPOGRAPHY_BLOG_NAME`)覆寫預設值,方便在不同部署環境中使用。
+
+3. **擴充樣式或功能**
+ - 共用樣式可以在 `style.js` 中調整或新增。
+ - 若要新增新的頁面佈局,建議在 `layouts/` 下建立新的 JSX 檔案,並於 `index.js` 匯出,以維持統一入口。
+ - 共用的邏輯或工具函式,可集中在 `utils/` 目錄內,減少重複程式碼。
+
+4. **整合既有元件**
+ - 所有 `layouts/` 中引用的元件皆放於 `components/`。若要替換視覺元素,如導覽列或頁尾,直接在對應元件修改即可。
+ - 動態載入 (`next/dynamic`) 會避免在 SSR 階段載入僅限瀏覽器的套件,若新增依賴,請依照既有範例設定 `ssr: false`。
+
+## 開發建議
+
+- 調整佈局時,先確認是否為共用邏輯(放在 `BaseLayout` 或工具函式),再決定是否要拆分。
+- 新增功能建議寫在對應的 `layouts/` 或 `components/` 中,並保持檔案職責單一,讓後續維護更容易。
+- 若有多語系或 Theme 變化需求,可以在 `config.js` 中增加新的設定值,並於各佈局透過 `siteConfig` 取得。
+
+有任何額外需求都可以在 README 中持續補充,讓後續維護人員快速上手。
diff --git a/mhhung/index.js b/mhhung/index.js
index b5fa092..8b8bfca 100644
--- a/mhhung/index.js
+++ b/mhhung/index.js
@@ -1,372 +1,8 @@
-import { AdSlot } from '@/components/GoogleAdsense'
-import replaceSearchResult from '@/components/Mark'
-import NotionPage from '@/components/NotionPage'
-import { siteConfig } from '@/lib/config'
-import { useGlobal } from '@/lib/global'
-import { isBrowser } from '@/lib/utils'
-import dynamic from 'next/dynamic'
-import SmartLink from '@/components/SmartLink'
-import { useRouter } from 'next/router'
-import { createContext, useContext, useEffect, useRef } from 'react'
-import BlogPostBar from './components/BlogPostBar'
-import CONFIG from './config'
-import { Style } from './style'
-import Catalog from './components/Catalog'
-
-const AlgoliaSearchModal = dynamic(
- () => import('@/components/AlgoliaSearchModal'),
- { ssr: false }
-)
-
-// 主题组件
-
-const BlogArchiveItem = dynamic(() => import('./components/BlogArchiveItem'), {
- ssr: false
-})
-const ArticleLock = dynamic(() => import('./components/ArticleLock'), {
- ssr: false
-})
-const ArticleInfo = dynamic(() => import('./components/ArticleInfo'), {
- ssr: false
-})
-const Comment = dynamic(() => import('@/components/Comment'), { ssr: false })
-const ArticleAround = dynamic(() => import('./components/ArticleAround'), {
- ssr: false
-})
-const TopBar = dynamic(() => import('./components/TopBar'), { ssr: false })
-const NavBar = dynamic(() => import('./components/NavBar'), { ssr: false })
-const JumpToTopButton = dynamic(() => import('./components/JumpToTopButton'), {
- ssr: false
-})
-const Footer = dynamic(() => import('./components/Footer'), { ssr: false })
-const WWAds = dynamic(() => import('@/components/WWAds'), { ssr: false })
-const BlogListPage = dynamic(() => import('./components/BlogListPage'), {
- ssr: false
-})
-const RecommendPosts = dynamic(() => import('./components/RecommendPosts'), {
- ssr: false
-})
-
-// 主题全局状态
-const ThemeGlobalSimple = createContext()
-export const useSimpleGlobal = () => useContext(ThemeGlobalSimple)
-
-/**
- * 基础布局
- *
- * @param {*} props
- * @returns
- */
-const LayoutBase = props => {
- const { children } = props
- const { onLoading, fullWidth } = useGlobal()
- // const onLoading = true
- const searchModal = useRef(null)
-
- return (
-
-
-
-
- {siteConfig('SIMPLE_TOP_BAR', null, CONFIG) &&
}
-
-
- {/* 主体 - 使用 flex 布局 */}
- {/* 文章详情才显示 */}
- {/* {props.post && (
-
-
-
- )} */}
-
- {/* 左侧内容区域 - 可滚动 */}
-
- {/* 移动端导航 - 显示在顶部 */}
-
-
-
- {onLoading ? (
- // loading 时显示 spinner
-
- ) : (
- <>{children}>
- )}
-
- {/* 移动端页脚 - 显示在底部 */}
-
-
-
-
-
-
- {/* 右侧导航和页脚 - 固定不滚动 */}
-
-
-
-
-
-
-
-
-
-
- {/* 搜索框 */}
-
-
-
- )
-}
-
-/**
- * 博客首页
- * 首页就是列表
- * @param {*} props
- * @returns
- */
-const LayoutIndex = props => {
- return
-}
-/**
- * 博客列表
- * @param {*} props
- * @returns
- */
-const LayoutPostList = props => {
- return (
- <>
-
-
- >
- )
-}
-
-/**
- * 搜索页
- * 也是博客列表
- * @param {*} props
- * @returns
- */
-const LayoutSearch = props => {
- const { keyword } = props
-
- useEffect(() => {
- if (isBrowser) {
- replaceSearchResult({
- doms: document.getElementById('posts-wrapper'),
- search: keyword,
- target: {
- element: 'span',
- className: 'text-red-500 border-b border-dashed'
- }
- })
- }
- }, [])
-
- return
-}
-
- function groupArticlesByYearArray(articles) {
- const grouped = {};
-
- for (const article of articles) {
- const year = new Date(article.publishDate).getFullYear().toString();
- if (!grouped[year]) {
- grouped[year] = [];
- }
- grouped[year].push(article);
- }
-
- for (const year in grouped) {
- grouped[year].sort((a, b) => b.publishDate - a.publishDate);
- }
-
- // 转成数组并按年份倒序
- return Object.entries(grouped)
- .sort(([a], [b]) => b - a)
- .map(([year, posts]) => ({ year, posts }));
-}
-
-
-
-/**
- * 归档页
- * @param {*} props
- * @returns
- */
-const LayoutArchive = props => {
- const { posts } = props
- const sortPosts = groupArticlesByYearArray(posts)
- return (
- <>
-
- {sortPosts.map(p => (
-
- ))}
-
- >
- )
-}
-
-/**
- * 文章详情
- * @param {*} props
- * @returns
- */
-const LayoutSlug = props => {
- const { post, lock, validPassword, prev, next, recommendPosts } = props
- const { fullWidth } = useGlobal()
-
- return (
- <>
- {lock && }
-
- {!lock && post && (
-
- {/* 文章信息 */}
-
-
- {/* 广告嵌入 */}
- {/*
*/}
-
-
-
- {/* Notion 文章主体 */}
- {!lock && }
-
-
- {/* 分享 */}
- {/*
*/}
-
- {/* 广告嵌入 */}
-
-
- {post?.type === 'Post' && (
- <>
-
-
- >
- )}
-
- {/* 评论区 */}
-
-
- )}
- >
- )
-}
-
-/**
- * 404
- * @param {*} props
- * @returns
- */
-const Layout404 = props => {
- const { post } = props
- const router = useRouter()
- const waiting404 = siteConfig('POST_WAITING_TIME_FOR_404') * 1000
- useEffect(() => {
- // 404
- if (!post) {
- setTimeout(() => {
- if (isBrowser) {
- const article = document.querySelector(
- '#article-wrapper #notion-article'
- )
- if (!article) {
- router.push('/404').then(() => {
- console.warn('找不到页面', router.asPath)
- })
- }
- }
- }, waiting404)
- }
- }, [post])
- return <>404 Not found.>
-}
-
-/**
- * 分类列表
- * @param {*} props
- * @returns
- */
-const LayoutCategoryIndex = props => {
- const { categoryOptions } = props
- return (
- <>
-
- {categoryOptions?.map(category => {
- return (
-
-
-
- {category.name}({category.count})
-
-
- )
- })}
-
- >
- )
-}
-
-/**
- * 标签列表
- * @param {*} props
- * @returns
- */
-const LayoutTagIndex = props => {
- const { tagOptions } = props
- return (
- <>
-
- >
- )
-}
-
-export {
- Layout404,
- LayoutArchive,
- LayoutBase,
- LayoutCategoryIndex,
- LayoutIndex,
- LayoutPostList,
- LayoutSearch,
- LayoutSlug,
- LayoutTagIndex,
- CONFIG as THEME_CONFIG
-}
+export { LayoutBase, useSimpleGlobal } from './layouts/BaseLayout'
+export { LayoutIndex, LayoutPostList, LayoutSearch } from './layouts/ListLayouts'
+export { LayoutArchive } from './layouts/ArchiveLayout'
+export { LayoutSlug } from './layouts/SlugLayout'
+export { Layout404 } from './layouts/NotFoundLayout'
+export { LayoutCategoryIndex, LayoutTagIndex } from './layouts/TaxonomyLayouts'
+export { default as CONFIG } from './config'
+export { default as THEME_CONFIG } from './config'
diff --git a/mhhung/layouts/ArchiveLayout.jsx b/mhhung/layouts/ArchiveLayout.jsx
new file mode 100644
index 0000000..606e6fd
--- /dev/null
+++ b/mhhung/layouts/ArchiveLayout.jsx
@@ -0,0 +1,25 @@
+import dynamic from 'next/dynamic'
+import { groupArticlesByYearArray } from '../utils/groupArticlesByYear'
+
+const BlogArchiveItem = dynamic(() => import('../components/BlogArchiveItem'), {
+ ssr: false
+})
+
+const LayoutArchive = props => {
+ const { posts } = props
+ const sortPosts = groupArticlesByYearArray(posts)
+
+ return (
+
+ {sortPosts.map(p => (
+
+ ))}
+
+ )
+}
+
+export { LayoutArchive }
diff --git a/mhhung/layouts/BaseLayout.jsx b/mhhung/layouts/BaseLayout.jsx
new file mode 100644
index 0000000..87bcb2f
--- /dev/null
+++ b/mhhung/layouts/BaseLayout.jsx
@@ -0,0 +1,79 @@
+import { AdSlot } from '@/components/GoogleAdsense'
+import dynamic from 'next/dynamic'
+import { createContext, useContext, useRef } from 'react'
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import CONFIG from '../config'
+import { Style } from '../style'
+
+const AlgoliaSearchModal = dynamic(
+ () => import('@/components/AlgoliaSearchModal'),
+ { ssr: false }
+)
+const TopBar = dynamic(() => import('../components/TopBar'), { ssr: false })
+const NavBar = dynamic(() => import('../components/NavBar'), { ssr: false })
+const JumpToTopButton = dynamic(
+ () => import('../components/JumpToTopButton'),
+ {
+ ssr: false
+ }
+)
+const Footer = dynamic(() => import('../components/Footer'), { ssr: false })
+
+const ThemeGlobalSimple = createContext()
+
+const useSimpleGlobal = () => useContext(ThemeGlobalSimple)
+
+const LayoutBase = props => {
+ const { children } = props
+ const { onLoading } = useGlobal()
+ const searchModal = useRef(null)
+
+ return (
+
+
+
+
+ {siteConfig('SIMPLE_TOP_BAR', null, CONFIG) &&
}
+
+
+
+
+
+
+
+ {onLoading ? (
+
+ ) : (
+ <>{children}>
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export { LayoutBase, useSimpleGlobal }
diff --git a/mhhung/layouts/ListLayouts.jsx b/mhhung/layouts/ListLayouts.jsx
new file mode 100644
index 0000000..16d6b0d
--- /dev/null
+++ b/mhhung/layouts/ListLayouts.jsx
@@ -0,0 +1,39 @@
+import dynamic from 'next/dynamic'
+import { useEffect } from 'react'
+import replaceSearchResult from '@/components/Mark'
+import { isBrowser } from '@/lib/utils'
+import BlogPostBar from '../components/BlogPostBar'
+
+const BlogListPage = dynamic(() => import('../components/BlogListPage'), {
+ ssr: false
+})
+
+const LayoutPostList = props => (
+ <>
+
+
+ >
+)
+
+const LayoutIndex = props =>
+
+const LayoutSearch = props => {
+ const { keyword } = props
+
+ useEffect(() => {
+ if (isBrowser) {
+ replaceSearchResult({
+ doms: document.getElementById('posts-wrapper'),
+ search: keyword,
+ target: {
+ element: 'span',
+ className: 'text-red-500 border-b border-dashed'
+ }
+ })
+ }
+ }, [keyword])
+
+ return
+}
+
+export { LayoutIndex, LayoutPostList, LayoutSearch }
diff --git a/mhhung/layouts/NotFoundLayout.jsx b/mhhung/layouts/NotFoundLayout.jsx
new file mode 100644
index 0000000..d58f16d
--- /dev/null
+++ b/mhhung/layouts/NotFoundLayout.jsx
@@ -0,0 +1,34 @@
+import { useRouter } from 'next/router'
+import { useEffect } from 'react'
+import { siteConfig } from '@/lib/config'
+import { isBrowser } from '@/lib/utils'
+
+const Layout404 = props => {
+ const { post } = props
+ const router = useRouter()
+ const waiting404 = siteConfig('POST_WAITING_TIME_FOR_404') * 1000
+
+ useEffect(() => {
+ if (post) {
+ return
+ }
+
+ const timer = setTimeout(() => {
+ if (!isBrowser) {
+ return
+ }
+ const article = document.querySelector('#article-wrapper #notion-article')
+ if (!article) {
+ router.push('/404').then(() => {
+ console.warn('找不到页面', router.asPath)
+ })
+ }
+ }, waiting404)
+
+ return () => clearTimeout(timer)
+ }, [post, router, waiting404])
+
+ return <>404 Not found.>
+}
+
+export { Layout404 }
diff --git a/mhhung/layouts/SlugLayout.jsx b/mhhung/layouts/SlugLayout.jsx
new file mode 100644
index 0000000..c1fce92
--- /dev/null
+++ b/mhhung/layouts/SlugLayout.jsx
@@ -0,0 +1,58 @@
+import { AdSlot } from '@/components/GoogleAdsense'
+import NotionPage from '@/components/NotionPage'
+import { useGlobal } from '@/lib/global'
+import dynamic from 'next/dynamic'
+
+const ArticleLock = dynamic(() => import('../components/ArticleLock'), {
+ ssr: false
+})
+const ArticleInfo = dynamic(() => import('../components/ArticleInfo'), {
+ ssr: false
+})
+const Comment = dynamic(() => import('@/components/Comment'), { ssr: false })
+const ArticleAround = dynamic(() => import('../components/ArticleAround'), {
+ ssr: false
+})
+const RecommendPosts = dynamic(() => import('../components/RecommendPosts'), {
+ ssr: false
+})
+const WWAds = dynamic(() => import('@/components/WWAds'), { ssr: false })
+
+const LayoutSlug = props => {
+ const { post, lock, validPassword, prev, next, recommendPosts } = props
+ const { fullWidth } = useGlobal()
+
+ if (lock) {
+ return
+ }
+
+ if (!post) {
+ return null
+ }
+
+ const isPost = post?.type === 'Post'
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {isPost && (
+ <>
+
+
+ >
+ )}
+
+
+
+ )
+}
+
+export { LayoutSlug }
diff --git a/mhhung/layouts/TaxonomyLayouts.jsx b/mhhung/layouts/TaxonomyLayouts.jsx
new file mode 100644
index 0000000..4985939
--- /dev/null
+++ b/mhhung/layouts/TaxonomyLayouts.jsx
@@ -0,0 +1,46 @@
+import SmartLink from '@/components/SmartLink'
+
+const LayoutCategoryIndex = props => {
+ const { categoryOptions } = props
+
+ return (
+
+ {categoryOptions?.map(category => (
+
+
+
+ {category.name}({category.count})
+
+
+ ))}
+
+ )
+}
+
+const LayoutTagIndex = props => {
+ const { tagOptions } = props
+
+ return (
+
+ )
+}
+
+export { LayoutCategoryIndex, LayoutTagIndex }
diff --git a/mhhung/utils/groupArticlesByYear.js b/mhhung/utils/groupArticlesByYear.js
new file mode 100644
index 0000000..abfea46
--- /dev/null
+++ b/mhhung/utils/groupArticlesByYear.js
@@ -0,0 +1,21 @@
+const groupArticlesByYearArray = articles => {
+ const grouped = {}
+
+ for (const article of articles) {
+ const year = new Date(article.publishDate).getFullYear().toString()
+ if (!grouped[year]) {
+ grouped[year] = []
+ }
+ grouped[year].push(article)
+ }
+
+ for (const year in grouped) {
+ grouped[year].sort((a, b) => b.publishDate - a.publishDate)
+ }
+
+ return Object.entries(grouped)
+ .sort(([a], [b]) => b - a)
+ .map(([year, posts]) => ({ year, posts }))
+}
+
+export { groupArticlesByYearArray }