diff --git a/themes/example/Layout404.js b/themes/example/Layout404.js
new file mode 100644
index 00000000..cd28a607
--- /dev/null
+++ b/themes/example/Layout404.js
@@ -0,0 +1,6 @@
+
+export const Layout404 = () => {
+ return
+ 404 Not found.
+
+}
diff --git a/themes/example/LayoutArchive.js b/themes/example/LayoutArchive.js
new file mode 100644
index 00000000..04a6d647
--- /dev/null
+++ b/themes/example/LayoutArchive.js
@@ -0,0 +1,69 @@
+import BLOG from '@/blog.config'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import LayoutBase from './LayoutBase'
+
+export const LayoutArchive = props => {
+ const { posts } = props
+ const { locale } = useGlobal()
+ const postsSortByDate = Object.create(posts)
+
+ postsSortByDate.sort((a, b) => {
+ const dateA = new Date(a?.date.start_date || a.createdTime)
+ const dateB = new Date(b?.date.start_date || b.createdTime)
+ return dateB - dateA
+ })
+
+ const meta = {
+ title: `${locale.NAV.ARCHIVE} | ${BLOG.TITLE}`,
+ description: BLOG.DESCRIPTION,
+ type: 'website'
+ }
+
+ const archivePosts = {}
+
+ postsSortByDate.forEach(post => {
+ const date = post.date.start_date.slice(0, 7)
+ if (archivePosts[date]) {
+ archivePosts[date].push(post)
+ } else {
+ archivePosts[date] = [post]
+ }
+ })
+ return (
+
+
+ {Object.keys(archivePosts).map(archiveTitle => (
+
+
+ {archiveTitle}
+
+
+ {archivePosts[archiveTitle].map(post => (
+ -
+
+
+ {post.date.start_date}
+ {' '}
+
+
+
+ {post.title}
+
+
+
+
+ ))}
+
+
+ ))}
+
+
+ )
+}
diff --git a/themes/example/LayoutBase.js b/themes/example/LayoutBase.js
new file mode 100644
index 00000000..7898c47d
--- /dev/null
+++ b/themes/example/LayoutBase.js
@@ -0,0 +1,135 @@
+import CommonHead from '@/components/CommonHead'
+import Live2D from '@/components/Live2D'
+import Link from 'next/link'
+import React from 'react'
+import BLOG from '@/blog.config'
+import { useGlobal } from '@/lib/global'
+/**
+ * 基础布局 采用左右两侧布局,移动端使用顶部导航栏
+
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const LayoutBase = props => {
+ const { children, meta, customNav } = props
+ const { locale } = useGlobal()
+ const d = new Date()
+ const currentYear = d.getFullYear()
+ const startYear = BLOG.SINCE && BLOG.SINCE !== currentYear && BLOG.SINCE + '-'
+
+ let links = [
+ { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search' },
+ { icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive' },
+ { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category' },
+ { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag' }
+ ]
+
+ if (customNav) {
+ links = links.concat(customNav)
+ }
+
+ return (
+
+
+ {/* 导航菜单 */}
+
+
+
+
+
+
+
+ {/* 内容主体 */}
+
+ {children}
+
+
+
+ {/* 页脚 */}
+
+
+ )
+}
+
+export default LayoutBase
diff --git a/themes/example/LayoutCategory.js b/themes/example/LayoutCategory.js
new file mode 100644
index 00000000..85d399f6
--- /dev/null
+++ b/themes/example/LayoutCategory.js
@@ -0,0 +1,48 @@
+import BLOG from '@/blog.config'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { useState } from 'react'
+import LayoutBase from './LayoutBase'
+
+export const LayoutCategory = props => {
+ const { category, posts } = props
+ const { locale } = useGlobal()
+
+ const [page, updatePage] = useState(1)
+ let hasMore = false
+ const postsToShow = posts
+ ? Object.assign(posts).slice(0, BLOG.POSTS_PER_PAGE * page)
+ : []
+
+ if (posts) {
+ const totalCount = posts.length
+ hasMore = page * BLOG.POSTS_PER_PAGE < totalCount
+ }
+ const handleGetMore = () => {
+ if (!hasMore) return
+ updatePage(page + 1)
+ }
+
+ return (
+
+ Category - {category}
+ {postsToShow.map(p => (
+
+ ))}
+
+
+ {' '}
+ {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
+
+
+
+ )
+}
diff --git a/themes/example/LayoutCategoryIndex.js b/themes/example/LayoutCategoryIndex.js
new file mode 100644
index 00000000..b748b140
--- /dev/null
+++ b/themes/example/LayoutCategoryIndex.js
@@ -0,0 +1,25 @@
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import LayoutBase from './LayoutBase'
+
+export const LayoutCategoryIndex = (props) => {
+ const { categories } = props
+ const { locale } = useGlobal()
+ return
+
+
+ {locale.COMMON.CATEGORY}:
+
+
+ {categories && categories.map(category => {
+ return
+
+ {category.name}({category.count})
+
+
+ })}
+
+
+
+}
diff --git a/themes/example/LayoutIndex.js b/themes/example/LayoutIndex.js
new file mode 100644
index 00000000..817bcdfb
--- /dev/null
+++ b/themes/example/LayoutIndex.js
@@ -0,0 +1,52 @@
+import BLOG from '@/blog.config'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import LayoutBase from './LayoutBase'
+
+export const LayoutIndex = props => {
+ const { posts, postCount } = props
+
+ const { locale } = useGlobal()
+ const router = useRouter()
+ const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
+
+ const page = 1
+ const showNext = page < totalPage && posts.length === BLOG.POSTS_PER_PAGE && posts.length < postCount
+
+ const currentPage = +page
+ return (
+
+ {posts.map(p => (
+
+ ))}
+
+
+
+ )
+}
diff --git a/themes/example/LayoutPage.js b/themes/example/LayoutPage.js
new file mode 100644
index 00000000..2f5f795a
--- /dev/null
+++ b/themes/example/LayoutPage.js
@@ -0,0 +1,52 @@
+import BLOG from '@/blog.config'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import LayoutBase from './LayoutBase'
+
+export const LayoutPage = (props) => {
+ const { page } = props
+ const { posts, postCount } = props
+
+ const { locale } = useGlobal()
+ const router = useRouter()
+ const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
+
+ const showNext = page < totalPage && posts.length === BLOG.POSTS_PER_PAGE && posts.length < postCount
+
+ const currentPage = +page
+ return (
+
+ {posts.map(p => (
+
+ ))}
+
+
+
+ )
+}
diff --git a/themes/example/LayoutSearch.js b/themes/example/LayoutSearch.js
new file mode 100644
index 00000000..d24024a5
--- /dev/null
+++ b/themes/example/LayoutSearch.js
@@ -0,0 +1,60 @@
+import BLOG from '@/blog.config'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { useEffect, useState } from 'react'
+import SearchInput from './components/SearchInput'
+import LayoutBase from './LayoutBase'
+
+export const LayoutSearch = props => {
+ const { keyword, posts } = props
+ useEffect(() => {
+ setTimeout(() => {
+ const container = document.getElementById('container')
+ if (container && container.innerHTML) {
+ const re = new RegExp(`${keyword}`, 'gim')
+ container.innerHTML = container.innerHTML.replace(re, `${keyword}`)
+ }
+ }, 100)
+ })
+
+ const { locale } = useGlobal()
+
+ const [page, updatePage] = useState(1)
+ let hasMore = false
+ const postsToShow = posts
+ ? Object.assign(posts).slice(0, BLOG.POSTS_PER_PAGE * page)
+ : []
+
+ if (posts) {
+ const totalCount = posts.length
+ hasMore = page * BLOG.POSTS_PER_PAGE < totalCount
+ }
+ const handleGetMore = () => {
+ if (!hasMore) return
+ updatePage(page + 1)
+ }
+
+ return (
+
+ Search - {keyword}
+
+ {postsToShow.map(p => (
+
+ ))}
+
+
+ {' '}
+ {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
+
+
+
+ )
+}
diff --git a/themes/example/LayoutSlug.js b/themes/example/LayoutSlug.js
new file mode 100644
index 00000000..7ff9b624
--- /dev/null
+++ b/themes/example/LayoutSlug.js
@@ -0,0 +1,100 @@
+import BLOG from '@/blog.config'
+import { getPageTableOfContents } from 'notion-utils'
+import 'prismjs'
+import 'prismjs/components/prism-bash'
+import 'prismjs/components/prism-javascript'
+import 'prismjs/components/prism-markup'
+import 'prismjs/components/prism-python'
+import 'prismjs/components/prism-typescript'
+import {
+ Code,
+ Collection,
+ CollectionRow,
+ Equation,
+ NotionRenderer
+} from 'react-notion-x'
+import LayoutBase from './LayoutBase'
+import { useRef, useEffect } from 'react'
+import { ArticleLock } from './components/ArticleLock'
+import mediumZoom from 'medium-zoom'
+
+const mapPageUrl = id => {
+ return 'https://www.notion.so/' + id.replace(/-/g, '')
+}
+
+export const LayoutSlug = props => {
+ const { post, lock, validPassword } = props
+ const meta = {
+ title: `${post.title} | ${BLOG.TITLE}`,
+ description: post.summary,
+ type: 'article',
+ tags: post.tags
+ }
+
+ if (!lock && post?.blockMap?.block) {
+ post.content = Object.keys(post.blockMap.block)
+ post.toc = getPageTableOfContents(post, post.blockMap)
+ }
+
+ const zoom =
+ typeof window !== 'undefined' &&
+ mediumZoom({
+ container: '.notion-viewport',
+ background: 'rgba(0, 0, 0, 0.2)',
+ margin: getMediumZoomMargin()
+ })
+ const zoomRef = useRef(zoom ? zoom.clone() : null)
+ useEffect(() => {
+ // 将所有container下的所有图片添加medium-zoom
+ const container = document.getElementById('notion-article')
+ const imgList = container?.getElementsByTagName('img')
+ if (imgList && zoomRef.current) {
+ for (let i = 0; i < imgList.length; i++) {
+ zoomRef.current.attach(imgList[i])
+ }
+ }
+ })
+ return (
+
+
+
{post?.title}
+
+ {lock &&
}
+
+ {!lock &&
+ {post.blockMap && (
+
+ )}
+ }
+
+
+
+ )
+}
+
+function getMediumZoomMargin () {
+ const width = window.innerWidth
+
+ if (width < 500) {
+ return 8
+ } else if (width < 800) {
+ return 20
+ } else if (width < 1280) {
+ return 30
+ } else if (width < 1600) {
+ return 40
+ } else if (width < 1920) {
+ return 48
+ } else {
+ return 72
+ }
+}
diff --git a/themes/example/LayoutTag.js b/themes/example/LayoutTag.js
new file mode 100644
index 00000000..e1a549f4
--- /dev/null
+++ b/themes/example/LayoutTag.js
@@ -0,0 +1,49 @@
+import BLOG from '@/blog.config'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { useState } from 'react'
+import LayoutBase from './LayoutBase'
+
+export const LayoutTag = props => {
+ const { tag, posts } = props
+ const { locale } = useGlobal()
+
+ const [page, updatePage] = useState(1)
+
+ let hasMore = false
+ const postsToShow = posts
+ ? Object.assign(posts).slice(0, BLOG.POSTS_PER_PAGE * page)
+ : []
+
+ if (posts) {
+ const totalCount = posts.length
+ hasMore = page * BLOG.POSTS_PER_PAGE < totalCount
+ }
+ const handleGetMore = () => {
+ if (!hasMore) return
+ updatePage(page + 1)
+ }
+
+ return (
+
+ Tag - {tag}
+ {postsToShow.map(p => (
+
+ ))}
+
+
+ {' '}
+ {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
+
+
+
+ )
+}
diff --git a/themes/example/LayoutTagIndex.js b/themes/example/LayoutTagIndex.js
new file mode 100644
index 00000000..3fb41c9f
--- /dev/null
+++ b/themes/example/LayoutTagIndex.js
@@ -0,0 +1,24 @@
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import LayoutBase from './LayoutBase'
+
+export const LayoutTagIndex = (props) => {
+ const { tags } = props
+ const { locale } = useGlobal()
+ return
+
+
{locale.COMMON.TAGS}:
+
+
+}
diff --git a/themes/example/components/ArticleLock.js b/themes/example/components/ArticleLock.js
new file mode 100644
index 00000000..bfdd00ce
--- /dev/null
+++ b/themes/example/components/ArticleLock.js
@@ -0,0 +1,40 @@
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 加密文章校验组件
+ * @param {password, validPassword} props
+ * @param password 正确的密码
+ * @param validPassword(bool) 回调函数,校验正确回调入参为true
+ * @returns
+ */
+export const ArticleLock = props => {
+ const { password, validPassword } = props
+ const { locale } = useGlobal()
+
+ const submitPassword = () => {
+ const p = document.getElementById('password')
+ if (p && p.value && p.value === password) {
+ validPassword(true)
+ } else {
+ const tips = document.getElementById('tips')
+ if (tips) {
+ tips.innerHTML = ''
+ tips.innerHTML = `${locale.COMMON.PASSWORD_ERROR}
`
+ }
+ }
+ }
+
+ return
+
+
{locale.COMMON.ARTICLE_LOCK_TIPS}
+
+
+
+ {locale.COMMON.SUBMIT}
+
+
+
+
+
+
+}
diff --git a/themes/example/components/SearchInput.js b/themes/example/components/SearchInput.js
new file mode 100644
index 00000000..6a95ba01
--- /dev/null
+++ b/themes/example/components/SearchInput.js
@@ -0,0 +1,87 @@
+import { useRouter } from 'next/router'
+import { useGlobal } from '@/lib/global'
+import { useImperativeHandle, useRef, useState } from 'react'
+
+let lock = false
+
+const SearchInput = ({ currentTag, currentSearch, cRef }) => {
+ const { locale } = useGlobal()
+ const router = useRouter()
+ const searchInputRef = useRef(null)
+ useImperativeHandle(cRef, () => {
+ return {
+ focus: () => {
+ searchInputRef?.current?.focus()
+ }
+ }
+ })
+ const handleSearch = () => {
+ const key = searchInputRef.current.value
+ if (key && key !== '') {
+ router.push({ pathname: '/search/' + key }).then(r => {
+ console.log('搜索', key)
+ })
+ } else {
+ router.push({ pathname: '/' }).then(r => {
+ })
+ }
+ }
+ const handleKeyUp = (e) => {
+ if (e.keyCode === 13) { // 回车
+ handleSearch(searchInputRef.current.value)
+ } else if (e.keyCode === 27) { // ESC
+ cleanSearch()
+ }
+ }
+ const cleanSearch = () => {
+ searchInputRef.current.value = ''
+ setShowClean(false)
+ }
+ function lockSearchInput () {
+ lock = true
+ }
+
+ function unLockSearchInput () {
+ lock = false
+ }
+ const [showClean, setShowClean] = useState(false)
+ const updateSearchKey = (val) => {
+ if (lock) {
+ return
+ }
+ searchInputRef.current.value = val
+ if (val) {
+ setShowClean(true)
+ } else {
+ setShowClean(false)
+ }
+ }
+
+ return
+}
+
+export default SearchInput
diff --git a/themes/example/config_empty.js b/themes/example/config_empty.js
new file mode 100644
index 00000000..9f1ac567
--- /dev/null
+++ b/themes/example/config_empty.js
@@ -0,0 +1,4 @@
+const CONFIG_EMPTY = {
+ TEST_CONFIG: 'TESET'
+}
+export default CONFIG_EMPTY
diff --git a/themes/example/index.js b/themes/example/index.js
new file mode 100644
index 00000000..95b0a6b2
--- /dev/null
+++ b/themes/example/index.js
@@ -0,0 +1,25 @@
+import CONFIG_EMPTY from './config_empty'
+import { LayoutIndex } from './LayoutIndex'
+import { LayoutSearch } from './LayoutSearch'
+import { LayoutArchive } from './LayoutArchive'
+import { LayoutSlug } from './LayoutSlug'
+import { Layout404 } from './Layout404'
+import { LayoutCategory } from './LayoutCategory'
+import { LayoutCategoryIndex } from './LayoutCategoryIndex'
+import { LayoutPage } from './LayoutPage'
+import { LayoutTag } from './LayoutTag'
+import { LayoutTagIndex } from './LayoutTagIndex'
+
+export {
+ CONFIG_EMPTY as THEME_CONFIG,
+ LayoutIndex,
+ LayoutSearch,
+ LayoutArchive,
+ LayoutSlug,
+ Layout404,
+ LayoutCategory,
+ LayoutCategoryIndex,
+ LayoutPage,
+ LayoutTag,
+ LayoutTagIndex
+}