From f319cb4bd83aa21a8777e466f2b17f920826be47 Mon Sep 17 00:00:00 2001
From: RHZHZ <136958965+RHZHZ@users.noreply.github.com>
Date: Mon, 9 Feb 2026 22:26:09 +0800
Subject: [PATCH 001/114] Update PrismMac.js
---
components/PrismMac.js | 405 ++++++++++++++++-------------------------
1 file changed, 158 insertions(+), 247 deletions(-)
diff --git a/components/PrismMac.js b/components/PrismMac.js
index a0aff890233..0b72a92166b 100644
--- a/components/PrismMac.js
+++ b/components/PrismMac.js
@@ -1,273 +1,184 @@
-import { useEffect } from 'react'
-import Prism from 'prismjs'
-// 所有语言的prismjs 使用autoloader引入
-// import 'prismjs/plugins/autoloader/prism-autoloader'
-import 'prismjs/plugins/toolbar/prism-toolbar'
-import 'prismjs/plugins/toolbar/prism-toolbar.min.css'
-import 'prismjs/plugins/show-language/prism-show-language'
-import 'prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard'
-import 'prismjs/plugins/line-numbers/prism-line-numbers'
-import 'prismjs/plugins/line-numbers/prism-line-numbers.css'
-
-// mermaid图
-import { loadExternalResource } from '@/lib/utils'
-import { useRouter } from 'next/navigation'
-import { useGlobal } from '@/lib/global'
-import { siteConfig } from '@/lib/config'
-
/**
- * 代码美化相关
- * @author https://github.com/txs/
- * @returns
- */
-const PrismMac = () => {
- const router = useRouter()
- const { isDarkMode } = useGlobal()
- const codeMacBar = siteConfig('CODE_MAC_BAR')
- const prismjsAutoLoader = siteConfig('PRISM_JS_AUTO_LOADER')
- const prismjsPath = siteConfig('PRISM_JS_PATH')
-
- const prismThemeSwitch = siteConfig('PRISM_THEME_SWITCH')
- const prismThemeDarkPath = siteConfig('PRISM_THEME_DARK_PATH')
- const prismThemeLightPath = siteConfig('PRISM_THEME_LIGHT_PATH')
- const prismThemePrefixPath = siteConfig('PRISM_THEME_PREFIX_PATH')
-
- const mermaidCDN = siteConfig('MERMAID_CDN')
- const codeLineNumbers = siteConfig('CODE_LINE_NUMBERS')
-
- const codeCollapse = siteConfig('CODE_COLLAPSE')
- const codeCollapseExpandDefault = siteConfig('CODE_COLLAPSE_EXPAND_DEFAULT')
-
- useEffect(() => {
- if (codeMacBar) {
- loadExternalResource('/css/prism-mac-style.css', 'css')
- }
- // 加载prism样式
- loadPrismThemeCSS(
- isDarkMode,
- prismThemeSwitch,
- prismThemeDarkPath,
- prismThemeLightPath,
- prismThemePrefixPath
- )
- // 折叠代码
- loadExternalResource(prismjsAutoLoader, 'js').then(url => {
- if (window?.Prism?.plugins?.autoloader) {
- window.Prism.plugins.autoloader.languages_path = prismjsPath
- }
-
- renderPrismMac(codeLineNumbers)
- renderMermaid(mermaidCDN)
- renderCollapseCode(codeCollapse, codeCollapseExpandDefault)
- })
- }, [router, isDarkMode])
-
- return <>>
+ * @author https://github.com/txs
+ * 通用 Mac 风格代码块样式 (NotionNext Universal)
+ **/
+
+/* 1. Mac 窗口容器样式 */
+.code-toolbar {
+ position: relative;
+ width: 100%;
+ margin: 1rem 0;
+ border-radius: 14px;
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ background: rgba(27, 28, 32, 0.94); /* 浅色模式下默认暗底 */
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 18px 44px rgba(0, 0, 0, 0.18);
+ overflow: hidden;
+ transition: box-shadow 0.3s ease, transform 0.3s ease;
}
-/**
- * 加载Prism主题样式
- */
-const loadPrismThemeCSS = (
- isDarkMode,
- prismThemeSwitch,
- prismThemeDarkPath,
- prismThemeLightPath,
- prismThemePrefixPath
-) => {
- let PRISM_THEME
- let PRISM_PREVIOUS
- if (prismThemeSwitch) {
- if (isDarkMode) {
- PRISM_THEME = prismThemeDarkPath
- PRISM_PREVIOUS = prismThemeLightPath
- } else {
- PRISM_THEME = prismThemeLightPath
- PRISM_PREVIOUS = prismThemeDarkPath
- }
- const previousTheme = document.querySelector(
- `link[href="${PRISM_PREVIOUS}"]`
- )
- if (
- previousTheme &&
- previousTheme.parentNode &&
- previousTheme.parentNode.contains(previousTheme)
- ) {
- previousTheme.parentNode.removeChild(previousTheme)
- }
- loadExternalResource(PRISM_THEME, 'css')
- } else {
- loadExternalResource(prismThemePrefixPath, 'css')
- }
+/* 暗色模式适配 */
+html.dark .code-toolbar {
+ border-color: rgba(255, 255, 255, 0.12);
+ background: rgba(27, 28, 32, 0.72);
+ -webkit-backdrop-filter: saturate(140%) blur(12px);
+ backdrop-filter: saturate(140%) blur(12px);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 18px 44px rgba(0, 0, 0, 0.45);
}
-/*
- * 将代码块转为可折叠对象
- */
-const renderCollapseCode = (codeCollapse, codeCollapseExpandDefault) => {
- if (!codeCollapse) {
- return
- }
- const codeBlocks = document.querySelectorAll('.code-toolbar')
- for (const codeBlock of codeBlocks) {
- // 判断当前元素是否被包裹
- if (codeBlock.closest('.collapse-wrapper')) {
- continue // 如果被包裹了,跳过当前循环
- }
+/* 2. Mac 三色点 */
+.pre-mac {
+ position: absolute;
+ left: 12px;
+ top: 11px;
+ z-index: 13;
+ display: flex;
+ gap: 7px;
+}
- const code = codeBlock.querySelector('code')
- const language = code.getAttribute('class').match(/language-(\w+)/)[1]
+.pre-mac > span {
+ width: 10px;
+ height: 10px;
+ border-radius: 999px;
+}
- const collapseWrapper = document.createElement('div')
- collapseWrapper.className = 'collapse-wrapper w-full py-2'
- const panelWrapper = document.createElement('div')
- panelWrapper.className =
- 'border dark:border-gray-600 rounded-md hover:border-indigo-500 duration-200 transition-colors'
+.pre-mac > span:nth-child(1) { background: #ff5f57; }
+.pre-mac > span:nth-child(2) { background: #febc2e; }
+.pre-mac > span:nth-child(3) { background: #28c840; }
+
+/* 3. Toolbar 工具栏 (复制按钮、语言标签) */
+.code-toolbar > .toolbar {
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: 34px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 0 10px;
+ z-index: 12;
+}
- const header = document.createElement('div')
- header.className =
- 'flex justify-between items-center px-4 py-2 cursor-pointer select-none'
- header.innerHTML = `
${language}
`
+.code-toolbar .toolbar-item > button {
+ font-size: 12px !important;
+ line-height: 1 !important;
+ padding: 6px 8px !important;
+ border-radius: 999px !important;
+ border: 1px solid rgba(255, 255, 255, 0.15) !important;
+ background: rgba(255, 255, 255, 0.1) !important;
+ color: rgba(255, 255, 255, 0.82) !important;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
- const panel = document.createElement('div')
- panel.className =
- 'invisible h-0 transition-transform duration-200 border-t border-gray-300'
+.code-toolbar .toolbar-item > button:hover {
+ background: rgba(255, 255, 255, 0.2) !important;
+ color: #fff !important;
+}
- panelWrapper.appendChild(header)
- panelWrapper.appendChild(panel)
- collapseWrapper.appendChild(panelWrapper)
+/* 4. 代码正文排版 */
+pre.notion-code {
+ font-size: 0.92em !important;
+ line-height: 1.6 !important;
+ margin: 0 !important;
+ padding: 46px 1.1rem 1rem !important;
+ border-radius: 0 !important;
+ border: none !important;
+ background: transparent !important;
+ color: rgba(255, 255, 255, 0.9) !important;
+ overflow: auto;
+ -webkit-overflow-scrolling: touch;
+}
- codeBlock.parentNode.insertBefore(collapseWrapper, codeBlock)
- panel.appendChild(codeBlock)
+/* 5. 智能折叠 S1 极简 UI */
+.collapse-wrapper {
+ margin: 1rem 0;
+}
- function collapseCode() {
- panel.classList.toggle('invisible')
- panel.classList.toggle('h-0')
- panel.classList.toggle('h-auto')
- header.querySelector('svg').classList.toggle('rotate-180')
- panelWrapper.classList.toggle('border-gray-300')
- }
+.collapse-panel-wrapper {
+ border-radius: 14px;
+ border: 1px solid rgba(0, 0, 0, 0.08);
+ background: rgba(255, 255, 255, 0.55);
+ -webkit-backdrop-filter: saturate(160%) blur(10px);
+ backdrop-filter: saturate(160%) blur(10px);
+ overflow: hidden;
+ transition: all 0.3s ease;
+}
- // 点击后折叠展开代码
- header.addEventListener('click', collapseCode)
- // 是否自动展开
- if (codeCollapseExpandDefault) {
- header.click()
- }
- }
+html.dark .collapse-panel-wrapper {
+ border-color: rgba(255, 255, 255, 0.12);
+ background: rgba(27, 28, 32, 0.6);
}
-/**
- * 将mermaid语言 渲染成图片
- */
-const renderMermaid = mermaidCDN => {
- const observer = new MutationObserver(mutationsList => {
- for (const m of mutationsList) {
- if (m.target.className === 'notion-code language-mermaid') {
- const chart = m.target.querySelector('code').textContent
- if (chart && !m.target.querySelector('.mermaid')) {
- const mermaidChart = document.createElement('pre')
- mermaidChart.className = 'mermaid'
- mermaidChart.innerHTML = chart
- m.target.appendChild(mermaidChart)
- }
+.collapse-header {
+ width: 100%;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 12px;
+ cursor: pointer;
+ user-select: none;
+ border: none;
+ background: transparent;
+ color: rgba(60, 60, 67, 0.6);
+}
- const mermaidsSvg = document.querySelectorAll('.mermaid')
- if (mermaidsSvg) {
- let needLoad = false
- for (const e of mermaidsSvg) {
- if (e?.firstChild?.nodeName !== 'svg') {
- needLoad = true
- }
- }
- if (needLoad) {
- loadExternalResource(mermaidCDN, 'js').then(url => {
- setTimeout(() => {
- const mermaid = window.mermaid
- mermaid?.contentLoaded()
- }, 100)
- })
- }
- }
- }
- }
- })
- if (document.querySelector('#notion-article')) {
- observer.observe(document.querySelector('#notion-article'), {
- attributes: true,
- subtree: true
- })
- }
+html.dark .collapse-header {
+ color: rgba(235, 235, 245, 0.6);
}
-function renderPrismMac(codeLineNumbers) {
- const container = document?.getElementById('notion-article')
+.collapse-label {
+ font-size: 13px;
+ letter-spacing: 0.02em;
+}
- // Add line numbers
- if (codeLineNumbers) {
- const codeBlocks = container?.getElementsByTagName('pre')
- if (codeBlocks) {
- Array.from(codeBlocks).forEach(item => {
- if (!item.classList.contains('line-numbers')) {
- item.classList.add('line-numbers')
- item.style.whiteSpace = 'pre-wrap'
- }
- })
- }
- }
- // 重新渲染之前检查所有的多余text
+.collapse-chevron {
+ width: 18px;
+ height: 18px;
+ transition: transform 0.3s ease;
+ opacity: 0.8;
+}
- try {
- Prism.highlightAll()
- } catch (err) {
- console.log('代码渲染', err)
- }
+.collapse-panel-wrapper.is-expanded .collapse-chevron {
+ transform: rotate(180deg);
+}
- const codeToolBars = container?.getElementsByClassName('code-toolbar')
- // Add pre-mac element for Mac Style UI
- if (codeToolBars) {
- Array.from(codeToolBars).forEach(item => {
- const existPreMac = item.getElementsByClassName('pre-mac')
- if (existPreMac.length < codeToolBars.length) {
- const preMac = document.createElement('div')
- preMac.classList.add('pre-mac')
- preMac.innerHTML = ''
- item?.appendChild(preMac, item)
- }
- })
- }
+.collapse-panel {
+ max-height: 0;
+ overflow: hidden;
+ border-top: 1px solid rgba(0, 0, 0, 0.06);
+ transition: max-height 0.32s ease;
+}
- // 折叠代码行号bug
- if (codeLineNumbers) {
- fixCodeLineStyle()
- }
+html.dark .collapse-panel {
+ border-top-color: rgba(255, 255, 255, 0.08);
}
-/**
- * 行号样式在首次渲染或被detail折叠后行高判断错误
- * 在此手动resize计算
- */
-const fixCodeLineStyle = () => {
- const observer = new MutationObserver(mutationsList => {
- for (const m of mutationsList) {
- if (m.target.nodeName === 'DETAILS') {
- const preCodes = m.target.querySelectorAll('pre.notion-code')
- for (const preCode of preCodes) {
- Prism.plugins.lineNumbers.resize(preCode)
- }
- }
- }
- })
- observer.observe(document.querySelector('#notion-article'), {
- attributes: true,
- subtree: true
- })
- setTimeout(() => {
- const preCodes = document.querySelectorAll('pre.notion-code')
- for (const preCode of preCodes) {
- Prism.plugins.lineNumbers.resize(preCode)
- }
- }, 10)
+.collapse-panel.is-expanded {
+ max-height: 3000px;
}
-export default PrismMac
+/* 6. Prism 代码高亮补丁 (暗底优化) */
+.code-toolbar .token.comment,
+.code-toolbar .token.prolog,
+.code-toolbar .token.doctype,
+.code-toolbar .token.cdata { color: rgba(235, 235, 245, 0.46); }
+.code-toolbar .token.punctuation { color: rgba(235, 235, 245, 0.6); }
+.code-toolbar .token.property,
+.code-toolbar .token.tag,
+.code-toolbar .token.boolean,
+.code-toolbar .token.number,
+.code-toolbar .token.constant,
+.code-toolbar .token.symbol,
+.code-toolbar .token.deleted { color: #7ee787; }
+.code-toolbar .token.selector,
+.code-toolbar .token.attr-name,
+.code-toolbar .token.string,
+.code-toolbar .token.char,
+.code-toolbar .token.builtin,
+.code-toolbar .token.inserted { color: #a5d6ff; }
+.code-toolbar .token.atrule,
+.code-toolbar .token.attr-value,
+.code-toolbar .token.keyword { color: #ff7ab2; }
+.code-toolbar .token.function,
+.code-toolbar .token.class-name { color: #ffd479; }
From 567f379c94d9f9c2909d124ff9767fe76aa0cbbd Mon Sep 17 00:00:00 2001
From: RHZHZ <136958965+RHZHZ@users.noreply.github.com>
Date: Mon, 9 Feb 2026 22:26:39 +0800
Subject: [PATCH 002/114] Update PrismMac.js
---
components/PrismMac.js | 423 ++++++++++++++++++++++++++---------------
1 file changed, 265 insertions(+), 158 deletions(-)
diff --git a/components/PrismMac.js b/components/PrismMac.js
index 0b72a92166b..2b8d422b33e 100644
--- a/components/PrismMac.js
+++ b/components/PrismMac.js
@@ -1,184 +1,291 @@
+import { useEffect } from 'react'
+import Prism from 'prismjs'
+// 所有语言的prismjs 使用autoloader引入
+// import 'prismjs/plugins/autoloader/prism-autoloader'
+import 'prismjs/plugins/toolbar/prism-toolbar'
+import 'prismjs/plugins/toolbar/prism-toolbar.min.css'
+import 'prismjs/plugins/show-language/prism-show-language'
+import 'prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard'
+import 'prismjs/plugins/line-numbers/prism-line-numbers'
+import 'prismjs/plugins/line-numbers/prism-line-numbers.css'
+
+// mermaid图
+import { loadExternalResource } from '@/lib/utils'
+import { useRouter } from 'next/navigation'
+import { useGlobal } from '@/lib/global'
+import { siteConfig } from '@/lib/config'
+
/**
- * @author https://github.com/txs
- * 通用 Mac 风格代码块样式 (NotionNext Universal)
- **/
-
-/* 1. Mac 窗口容器样式 */
-.code-toolbar {
- position: relative;
- width: 100%;
- margin: 1rem 0;
- border-radius: 14px;
- border: 1px solid rgba(0, 0, 0, 0.1);
- background: rgba(27, 28, 32, 0.94); /* 浅色模式下默认暗底 */
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 18px 44px rgba(0, 0, 0, 0.18);
- overflow: hidden;
- transition: box-shadow 0.3s ease, transform 0.3s ease;
-}
+ * 代码美化相关
+ * @author https://github.com/txs/
+ * @returns
+ */
+const PrismMac = () => {
+ const router = useRouter()
+ const { isDarkMode } = useGlobal()
+ const codeMacBar = siteConfig('CODE_MAC_BAR')
+ const prismjsAutoLoader = siteConfig('PRISM_JS_AUTO_LOADER')
+ const prismjsPath = siteConfig('PRISM_JS_PATH')
-/* 暗色模式适配 */
-html.dark .code-toolbar {
- border-color: rgba(255, 255, 255, 0.12);
- background: rgba(27, 28, 32, 0.72);
- -webkit-backdrop-filter: saturate(140%) blur(12px);
- backdrop-filter: saturate(140%) blur(12px);
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 18px 44px rgba(0, 0, 0, 0.45);
-}
+ const prismThemeSwitch = siteConfig('PRISM_THEME_SWITCH')
+ const prismThemeDarkPath = siteConfig('PRISM_THEME_DARK_PATH')
+ const prismThemeLightPath = siteConfig('PRISM_THEME_LIGHT_PATH')
+ const prismThemePrefixPath = siteConfig('PRISM_THEME_PREFIX_PATH')
-/* 2. Mac 三色点 */
-.pre-mac {
- position: absolute;
- left: 12px;
- top: 11px;
- z-index: 13;
- display: flex;
- gap: 7px;
-}
+ const mermaidCDN = siteConfig('MERMAID_CDN')
+ const codeLineNumbers = siteConfig('CODE_LINE_NUMBERS')
-.pre-mac > span {
- width: 10px;
- height: 10px;
- border-radius: 999px;
-}
+ const codeCollapse = siteConfig('CODE_COLLAPSE')
+ const codeCollapseExpandDefault = siteConfig('CODE_COLLAPSE_EXPAND_DEFAULT')
-.pre-mac > span:nth-child(1) { background: #ff5f57; }
-.pre-mac > span:nth-child(2) { background: #febc2e; }
-.pre-mac > span:nth-child(3) { background: #28c840; }
-
-/* 3. Toolbar 工具栏 (复制按钮、语言标签) */
-.code-toolbar > .toolbar {
- position: absolute;
- top: 0;
- right: 0;
- height: 34px;
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 0 10px;
- z-index: 12;
-}
+ useEffect(() => {
+ if (codeMacBar) {
+ loadExternalResource('/css/prism-mac-style.css', 'css')
+ }
+ // 加载prism样式
+ loadPrismThemeCSS(
+ isDarkMode,
+ prismThemeSwitch,
+ prismThemeDarkPath,
+ prismThemeLightPath,
+ prismThemePrefixPath
+ )
+ // 折叠代码
+ loadExternalResource(prismjsAutoLoader, 'js').then(url => {
+ if (window?.Prism?.plugins?.autoloader) {
+ window.Prism.plugins.autoloader.languages_path = prismjsPath
+ }
-.code-toolbar .toolbar-item > button {
- font-size: 12px !important;
- line-height: 1 !important;
- padding: 6px 8px !important;
- border-radius: 999px !important;
- border: 1px solid rgba(255, 255, 255, 0.15) !important;
- background: rgba(255, 255, 255, 0.1) !important;
- color: rgba(255, 255, 255, 0.82) !important;
- cursor: pointer;
- transition: all 0.2s ease;
-}
+ renderPrismMac(codeLineNumbers)
+ renderMermaid(mermaidCDN)
+ renderCollapseCode(codeCollapse, codeCollapseExpandDefault)
+ })
+ }, [router, isDarkMode])
-.code-toolbar .toolbar-item > button:hover {
- background: rgba(255, 255, 255, 0.2) !important;
- color: #fff !important;
+ return <>>
}
-/* 4. 代码正文排版 */
-pre.notion-code {
- font-size: 0.92em !important;
- line-height: 1.6 !important;
- margin: 0 !important;
- padding: 46px 1.1rem 1rem !important;
- border-radius: 0 !important;
- border: none !important;
- background: transparent !important;
- color: rgba(255, 255, 255, 0.9) !important;
- overflow: auto;
- -webkit-overflow-scrolling: touch;
+/**
+ * 加载Prism主题样式
+ */
+const loadPrismThemeCSS = (
+ isDarkMode,
+ prismThemeSwitch,
+ prismThemeDarkPath,
+ prismThemeLightPath,
+ prismThemePrefixPath
+) => {
+ let PRISM_THEME
+ let PRISM_PREVIOUS
+ if (prismThemeSwitch) {
+ if (isDarkMode) {
+ PRISM_THEME = prismThemeDarkPath
+ PRISM_PREVIOUS = prismThemeLightPath
+ } else {
+ PRISM_THEME = prismThemeLightPath
+ PRISM_PREVIOUS = prismThemeDarkPath
+ }
+ const previousTheme = document.querySelector(
+ `link[href="${PRISM_PREVIOUS}"]`
+ )
+ if (
+ previousTheme &&
+ previousTheme.parentNode &&
+ previousTheme.parentNode.contains(previousTheme)
+ ) {
+ previousTheme.parentNode.removeChild(previousTheme)
+ }
+ loadExternalResource(PRISM_THEME, 'css')
+ } else {
+ loadExternalResource(prismThemePrefixPath, 'css')
+ }
}
-/* 5. 智能折叠 S1 极简 UI */
-.collapse-wrapper {
- margin: 1rem 0;
-}
+/*
+ * 将代码块转为可折叠对象
+ */
+const renderCollapseCode = (codeCollapse, codeCollapseExpandDefault) => {
+ if (!codeCollapse) {
+ return
+ }
-.collapse-panel-wrapper {
- border-radius: 14px;
- border: 1px solid rgba(0, 0, 0, 0.08);
- background: rgba(255, 255, 255, 0.55);
- -webkit-backdrop-filter: saturate(160%) blur(10px);
- backdrop-filter: saturate(160%) blur(10px);
- overflow: hidden;
- transition: all 0.3s ease;
-}
+ const COLLAPSE_MIN_LINES = Number(siteConfig('CODE_COLLAPSE_MIN_LINES', 18))
+ const codeBlocks = document.querySelectorAll('.code-toolbar')
-html.dark .collapse-panel-wrapper {
- border-color: rgba(255, 255, 255, 0.12);
- background: rgba(27, 28, 32, 0.6);
-}
+ for (const codeBlock of codeBlocks) {
+ if (codeBlock.closest('.collapse-wrapper')) {
+ continue
+ }
-.collapse-header {
- width: 100%;
- height: 36px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0 12px;
- cursor: pointer;
- user-select: none;
- border: none;
- background: transparent;
- color: rgba(60, 60, 67, 0.6);
-}
+ const code = codeBlock.querySelector('code')
+ if (!code) {
+ continue
+ }
-html.dark .collapse-header {
- color: rgba(235, 235, 245, 0.6);
-}
+ const className = code.getAttribute('class') || ''
+ const languageMatch = className.match(/language-([\w-]+)/)
+ const language = languageMatch ? languageMatch[1] : ''
-.collapse-label {
- font-size: 13px;
- letter-spacing: 0.02em;
-}
+ const text = code.textContent || ''
+ const lineCount = text ? text.split('\n').length : 0
-.collapse-chevron {
- width: 18px;
- height: 18px;
- transition: transform 0.3s ease;
- opacity: 0.8;
-}
+ // 方案 C:仅当代码行数超过阈值时才启用折叠
+ if (lineCount && lineCount < COLLAPSE_MIN_LINES) {
+ continue
+ }
+
+ const collapseWrapper = document.createElement('div')
+ collapseWrapper.className = 'collapse-wrapper w-full py-2'
-.collapse-panel-wrapper.is-expanded .collapse-chevron {
- transform: rotate(180deg);
+ const panelWrapper = document.createElement('div')
+ panelWrapper.className = 'collapse-panel-wrapper'
+
+ const header = document.createElement('button')
+ header.type = 'button'
+ header.className = 'collapse-header'
+
+ const label = language
+ ? `${language.toUpperCase()} · ${lineCount} lines`
+ : `${lineCount} lines`
+
+ header.innerHTML = `${label}`
+
+ const panel = document.createElement('div')
+ panel.className = 'collapse-panel'
+
+ panelWrapper.appendChild(header)
+ panelWrapper.appendChild(panel)
+ collapseWrapper.appendChild(panelWrapper)
+
+ codeBlock.parentNode.insertBefore(collapseWrapper, codeBlock)
+ panel.appendChild(codeBlock)
+
+ function setExpanded(expanded) {
+ panelWrapper.classList.toggle('is-expanded', expanded)
+ panel.classList.toggle('is-expanded', expanded)
+ header.setAttribute('aria-expanded', expanded ? 'true' : 'false')
+ }
+
+ header.addEventListener('click', () => {
+ const expanded = panelWrapper.classList.contains('is-expanded')
+ setExpanded(!expanded)
+ })
+
+ setExpanded(Boolean(codeCollapseExpandDefault))
+ }
}
-.collapse-panel {
- max-height: 0;
- overflow: hidden;
- border-top: 1px solid rgba(0, 0, 0, 0.06);
- transition: max-height 0.32s ease;
+/**
+ * 将mermaid语言 渲染成图片
+ */
+const renderMermaid = mermaidCDN => {
+ const observer = new MutationObserver(mutationsList => {
+ for (const m of mutationsList) {
+ if (m.target.className === 'notion-code language-mermaid') {
+ const chart = m.target.querySelector('code').textContent
+ if (chart && !m.target.querySelector('.mermaid')) {
+ const mermaidChart = document.createElement('pre')
+ mermaidChart.className = 'mermaid'
+ mermaidChart.innerHTML = chart
+ m.target.appendChild(mermaidChart)
+ }
+
+ const mermaidsSvg = document.querySelectorAll('.mermaid')
+ if (mermaidsSvg) {
+ let needLoad = false
+ for (const e of mermaidsSvg) {
+ if (e?.firstChild?.nodeName !== 'svg') {
+ needLoad = true
+ }
+ }
+ if (needLoad) {
+ loadExternalResource(mermaidCDN, 'js').then(url => {
+ setTimeout(() => {
+ const mermaid = window.mermaid
+ mermaid?.contentLoaded()
+ }, 100)
+ })
+ }
+ }
+ }
+ }
+ })
+ if (document.querySelector('#notion-article')) {
+ observer.observe(document.querySelector('#notion-article'), {
+ attributes: true,
+ subtree: true
+ })
+ }
}
-html.dark .collapse-panel {
- border-top-color: rgba(255, 255, 255, 0.08);
+function renderPrismMac(codeLineNumbers) {
+ const container = document?.getElementById('notion-article')
+
+ // Add line numbers
+ if (codeLineNumbers) {
+ const codeBlocks = container?.getElementsByTagName('pre')
+ if (codeBlocks) {
+ Array.from(codeBlocks).forEach(item => {
+ if (!item.classList.contains('line-numbers')) {
+ item.classList.add('line-numbers')
+ item.style.whiteSpace = 'pre-wrap'
+ }
+ })
+ }
+ }
+ // 重新渲染之前检查所有的多余text
+
+ try {
+ Prism.highlightAll()
+ } catch (err) {
+ console.log('代码渲染', err)
+ }
+
+ const codeToolBars = container?.getElementsByClassName('code-toolbar')
+ // Add pre-mac element for Mac Style UI
+ if (codeToolBars) {
+ Array.from(codeToolBars).forEach(item => {
+ const existPreMac = item.getElementsByClassName('pre-mac')
+ if (existPreMac.length < codeToolBars.length) {
+ const preMac = document.createElement('div')
+ preMac.classList.add('pre-mac')
+ preMac.innerHTML = ''
+ item?.appendChild(preMac, item)
+ }
+ })
+ }
+
+ // 折叠代码行号bug
+ if (codeLineNumbers) {
+ fixCodeLineStyle()
+ }
}
-.collapse-panel.is-expanded {
- max-height: 3000px;
+/**
+ * 行号样式在首次渲染或被detail折叠后行高判断错误
+ * 在此手动resize计算
+ */
+const fixCodeLineStyle = () => {
+ const observer = new MutationObserver(mutationsList => {
+ for (const m of mutationsList) {
+ if (m.target.nodeName === 'DETAILS') {
+ const preCodes = m.target.querySelectorAll('pre.notion-code')
+ for (const preCode of preCodes) {
+ Prism.plugins.lineNumbers.resize(preCode)
+ }
+ }
+ }
+ })
+ observer.observe(document.querySelector('#notion-article'), {
+ attributes: true,
+ subtree: true
+ })
+ setTimeout(() => {
+ const preCodes = document.querySelectorAll('pre.notion-code')
+ for (const preCode of preCodes) {
+ Prism.plugins.lineNumbers.resize(preCode)
+ }
+ }, 10)
}
-/* 6. Prism 代码高亮补丁 (暗底优化) */
-.code-toolbar .token.comment,
-.code-toolbar .token.prolog,
-.code-toolbar .token.doctype,
-.code-toolbar .token.cdata { color: rgba(235, 235, 245, 0.46); }
-.code-toolbar .token.punctuation { color: rgba(235, 235, 245, 0.6); }
-.code-toolbar .token.property,
-.code-toolbar .token.tag,
-.code-toolbar .token.boolean,
-.code-toolbar .token.number,
-.code-toolbar .token.constant,
-.code-toolbar .token.symbol,
-.code-toolbar .token.deleted { color: #7ee787; }
-.code-toolbar .token.selector,
-.code-toolbar .token.attr-name,
-.code-toolbar .token.string,
-.code-toolbar .token.char,
-.code-toolbar .token.builtin,
-.code-toolbar .token.inserted { color: #a5d6ff; }
-.code-toolbar .token.atrule,
-.code-toolbar .token.attr-value,
-.code-toolbar .token.keyword { color: #ff7ab2; }
-.code-toolbar .token.function,
-.code-toolbar .token.class-name { color: #ffd479; }
+export default PrismMac
From 2fed028b808e1dbc062ebb4eb640420ec59aa521 Mon Sep 17 00:00:00 2001
From: RHZHZ <136958965+RHZHZ@users.noreply.github.com>
Date: Mon, 9 Feb 2026 22:27:20 +0800
Subject: [PATCH 003/114] Update prism-mac-style.css
---
public/css/prism-mac-style.css | 190 +++++++++++++++++++++++++++------
1 file changed, 158 insertions(+), 32 deletions(-)
diff --git a/public/css/prism-mac-style.css b/public/css/prism-mac-style.css
index 2162f359bce..0b72a92166b 100644
--- a/public/css/prism-mac-style.css
+++ b/public/css/prism-mac-style.css
@@ -1,58 +1,184 @@
/**
* @author https://github.com/txs
- * 当配置文件 CODE_MAC_BAR 开启时,此样式会被动态引入,将开启代码组件左上角的mac图标
+ * 通用 Mac 风格代码块样式 (NotionNext Universal)
**/
+
+/* 1. Mac 窗口容器样式 */
.code-toolbar {
position: relative;
- padding-top: 0 !important;
- padding-bottom: 0 !important;
width: 100%;
- border-radius: 0.5rem;
- margin-bottom: 0.5rem;
+ margin: 1rem 0;
+ border-radius: 14px;
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ background: rgba(27, 28, 32, 0.94); /* 浅色模式下默认暗底 */
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 18px 44px rgba(0, 0, 0, 0.18);
+ overflow: hidden;
+ transition: box-shadow 0.3s ease, transform 0.3s ease;
}
-.collapse-wrapper .code-toolbar {
- margin-bottom: 0;
+/* 暗色模式适配 */
+html.dark .code-toolbar {
+ border-color: rgba(255, 255, 255, 0.12);
+ background: rgba(27, 28, 32, 0.72);
+ -webkit-backdrop-filter: saturate(140%) blur(12px);
+ backdrop-filter: saturate(140%) blur(12px);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 18px 44px rgba(0, 0, 0, 0.45);
}
-.toolbar-item{
- white-space: nowrap;
+/* 2. Mac 三色点 */
+.pre-mac {
+ position: absolute;
+ left: 12px;
+ top: 11px;
+ z-index: 13;
+ display: flex;
+ gap: 7px;
}
-.toolbar-item > button {
- margin-top: -0.1rem;
+.pre-mac > span {
+ width: 10px;
+ height: 10px;
+ border-radius: 999px;
}
-pre[class*='language-'] {
- margin-top: 0rem !important;
- // margin-bottom: 0rem !important;
- padding-top: 1.5rem !important;
+.pre-mac > span:nth-child(1) { background: #ff5f57; }
+.pre-mac > span:nth-child(2) { background: #febc2e; }
+.pre-mac > span:nth-child(3) { background: #28c840; }
+/* 3. Toolbar 工具栏 (复制按钮、语言标签) */
+.code-toolbar > .toolbar {
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: 34px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 0 10px;
+ z-index: 12;
}
-.pre-mac {
- position: absolute;
- left: 0.9rem;
- top: 0.5rem;
- z-index: 10;
+.code-toolbar .toolbar-item > button {
+ font-size: 12px !important;
+ line-height: 1 !important;
+ padding: 6px 8px !important;
+ border-radius: 999px !important;
+ border: 1px solid rgba(255, 255, 255, 0.15) !important;
+ background: rgba(255, 255, 255, 0.1) !important;
+ color: rgba(255, 255, 255, 0.82) !important;
+ cursor: pointer;
+ transition: all 0.2s ease;
}
-.pre-mac > span {
- width: 10px;
- height: 10px;
- border-radius: 50%;
- margin-right: 5px;
- float: left;
+.code-toolbar .toolbar-item > button:hover {
+ background: rgba(255, 255, 255, 0.2) !important;
+ color: #fff !important;
+}
+
+/* 4. 代码正文排版 */
+pre.notion-code {
+ font-size: 0.92em !important;
+ line-height: 1.6 !important;
+ margin: 0 !important;
+ padding: 46px 1.1rem 1rem !important;
+ border-radius: 0 !important;
+ border: none !important;
+ background: transparent !important;
+ color: rgba(255, 255, 255, 0.9) !important;
+ overflow: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+/* 5. 智能折叠 S1 极简 UI */
+.collapse-wrapper {
+ margin: 1rem 0;
}
-.pre-mac > span:nth-child(1) {
- background: red;
+.collapse-panel-wrapper {
+ border-radius: 14px;
+ border: 1px solid rgba(0, 0, 0, 0.08);
+ background: rgba(255, 255, 255, 0.55);
+ -webkit-backdrop-filter: saturate(160%) blur(10px);
+ backdrop-filter: saturate(160%) blur(10px);
+ overflow: hidden;
+ transition: all 0.3s ease;
}
-.pre-mac > span:nth-child(2) {
- background: sandybrown;
+html.dark .collapse-panel-wrapper {
+ border-color: rgba(255, 255, 255, 0.12);
+ background: rgba(27, 28, 32, 0.6);
}
-.pre-mac > span:nth-child(3) {
- background: limegreen;
+.collapse-header {
+ width: 100%;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 12px;
+ cursor: pointer;
+ user-select: none;
+ border: none;
+ background: transparent;
+ color: rgba(60, 60, 67, 0.6);
+}
+
+html.dark .collapse-header {
+ color: rgba(235, 235, 245, 0.6);
+}
+
+.collapse-label {
+ font-size: 13px;
+ letter-spacing: 0.02em;
+}
+
+.collapse-chevron {
+ width: 18px;
+ height: 18px;
+ transition: transform 0.3s ease;
+ opacity: 0.8;
+}
+
+.collapse-panel-wrapper.is-expanded .collapse-chevron {
+ transform: rotate(180deg);
+}
+
+.collapse-panel {
+ max-height: 0;
+ overflow: hidden;
+ border-top: 1px solid rgba(0, 0, 0, 0.06);
+ transition: max-height 0.32s ease;
+}
+
+html.dark .collapse-panel {
+ border-top-color: rgba(255, 255, 255, 0.08);
+}
+
+.collapse-panel.is-expanded {
+ max-height: 3000px;
}
+
+/* 6. Prism 代码高亮补丁 (暗底优化) */
+.code-toolbar .token.comment,
+.code-toolbar .token.prolog,
+.code-toolbar .token.doctype,
+.code-toolbar .token.cdata { color: rgba(235, 235, 245, 0.46); }
+.code-toolbar .token.punctuation { color: rgba(235, 235, 245, 0.6); }
+.code-toolbar .token.property,
+.code-toolbar .token.tag,
+.code-toolbar .token.boolean,
+.code-toolbar .token.number,
+.code-toolbar .token.constant,
+.code-toolbar .token.symbol,
+.code-toolbar .token.deleted { color: #7ee787; }
+.code-toolbar .token.selector,
+.code-toolbar .token.attr-name,
+.code-toolbar .token.string,
+.code-toolbar .token.char,
+.code-toolbar .token.builtin,
+.code-toolbar .token.inserted { color: #a5d6ff; }
+.code-toolbar .token.atrule,
+.code-toolbar .token.attr-value,
+.code-toolbar .token.keyword { color: #ff7ab2; }
+.code-toolbar .token.function,
+.code-toolbar .token.class-name { color: #ffd479; }
From 001f4172879dfde2f4afbc433e6206afffa1376e Mon Sep 17 00:00:00 2001
From: RHZHZ <136958965+RHZHZ@users.noreply.github.com>
Date: Mon, 9 Feb 2026 22:28:03 +0800
Subject: [PATCH 004/114] Update blog.config.js
---
blog.config.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/blog.config.js b/blog.config.js
index 24c2a049cf2..755b4d6b711 100644
--- a/blog.config.js
+++ b/blog.config.js
@@ -24,6 +24,8 @@ const BLOG = {
BEI_AN_LINK: process.env.NEXT_PUBLIC_BEI_AN_LINK || 'https://beian.miit.gov.cn/', // 备案查询链接,如果用了萌备等备案请在这里填写
BEI_AN_GONGAN: process.env.NEXT_PUBLIC_BEI_AN_GONGAN || '', // 公安备案号,例如 '浙公网安备3xxxxxxxx8号'
+ CODE_COLLAPSE_MIN_LINES: 30,//“长代码”阈值
+
// RSS订阅
ENABLE_RSS: process.env.NEXT_PUBLIC_ENABLE_RSS || true, // 是否开启RSS订阅功能
@@ -71,4 +73,4 @@ const BLOG = {
UUID_REDIRECT: process.env.UUID_REDIRECT || false
}
-module.exports = BLOG
\ No newline at end of file
+module.exports = BLOG
From c7db7c71751b8b3de35644b8acbe0548e224dcbf Mon Sep 17 00:00:00 2001
From: hiderx <3193997905@qq.com>
Date: Mon, 16 Feb 2026 13:20:47 +0800
Subject: [PATCH 005/114] feat: claude theme changes without global entrypoint
edits
---
Claude-Theme-README-en.md | 356 +++
Claude-Theme-README.md | 725 +++++
lib/server/claude/contributionStore.js | 510 ++++
pages/api/claude/contribution-refresh.js | 52 +
.../AnthropicSans-Text-Medium-Static.otf | Bin 0 -> 61528 bytes
.../AnthropicSans-Text-Regular-Static.otf | Bin 0 -> 61144 bytes
...nthropicSans-Text-RegularItalic-Static.otf | Bin 0 -> 55916 bytes
.../AnthropicSans-Text-Semibold-Static.otf | Bin 0 -> 62380 bytes
.../AnthropicSerif-Display-Regular-Static.otf | Bin 0 -> 68796 bytes
...AnthropicSerif-Display-Semibold-Static.otf | Bin 0 -> 66856 bytes
.../claude/github-markdown-css/.editorconfig | 12 +
.../claude/github-markdown-css/.gitattributes | 1 +
.../github-markdown-css/.github/security.md | 3 +
.../claude/github-markdown-css/.gitignore | 2 +
.../themes/claude/github-markdown-css/.npmrc | 1 +
.../github-markdown-dark-colorblind.css | 1124 ++++++++
.../github-markdown-dark-dimmed.css | 1124 ++++++++
.../github-markdown-dark-high-contrast.css | 1124 ++++++++
.../github-markdown-dark.css | 1124 ++++++++
.../github-markdown-light-colorblind.css | 1124 ++++++++
.../github-markdown-light.css | 1124 ++++++++
.../github-markdown-css/github-markdown.css | 1248 ++++++++
.../claude/github-markdown-css/index.html | 1889 ++++++++++++
.../themes/claude/github-markdown-css/license | 9 +
.../claude/github-markdown-css/package.json | 48 +
.../claude/github-markdown-css/readme.md | 76 +
styles/claude-readme.css | 1 +
themes/claude/components/ArticleAround.js | 32 +
themes/claude/components/ArticleInfo.js | 64 +
themes/claude/components/ArticleLock.js | 52 +
themes/claude/components/BlogArchiveItem.js | 36 +
themes/claude/components/BlogItem.js | 76 +
themes/claude/components/BlogListPage.js | 74 +
themes/claude/components/BlogListScroll.js | 70 +
themes/claude/components/BlogPostBar.js | 29 +
themes/claude/components/Catalog.js | 266 ++
themes/claude/components/DarkModeButton.js | 96 +
.../components/ExampleRecentComments.js | 35 +
themes/claude/components/Footer.js | 24 +
themes/claude/components/JumpToTopButton.js | 35 +
themes/claude/components/MenuItemCollapse.js | 92 +
themes/claude/components/MenuItemDrop.js | 78 +
themes/claude/components/MenuList.js | 101 +
themes/claude/components/NavBar.js | 247 ++
themes/claude/components/ProfileHome.js | 956 +++++++
themes/claude/components/RecommendPosts.js | 32 +
themes/claude/components/SocialButton.js | 68 +
themes/claude/components/Title.js | 19 +
themes/claude/components/TopBar.js | 19 +
themes/claude/config.js | 40 +
themes/claude/index.js | 384 +++
themes/claude/style.js | 2543 +++++++++++++++++
52 files changed, 17145 insertions(+)
create mode 100644 Claude-Theme-README-en.md
create mode 100644 Claude-Theme-README.md
create mode 100644 lib/server/claude/contributionStore.js
create mode 100644 pages/api/claude/contribution-refresh.js
create mode 100644 public/themes/claude/fonts/AnthropicSans-Text-Medium-Static.otf
create mode 100644 public/themes/claude/fonts/AnthropicSans-Text-Regular-Static.otf
create mode 100644 public/themes/claude/fonts/AnthropicSans-Text-RegularItalic-Static.otf
create mode 100644 public/themes/claude/fonts/AnthropicSans-Text-Semibold-Static.otf
create mode 100644 public/themes/claude/fonts/AnthropicSerif-Display-Regular-Static.otf
create mode 100644 public/themes/claude/fonts/AnthropicSerif-Display-Semibold-Static.otf
create mode 100644 public/themes/claude/github-markdown-css/.editorconfig
create mode 100644 public/themes/claude/github-markdown-css/.gitattributes
create mode 100644 public/themes/claude/github-markdown-css/.github/security.md
create mode 100644 public/themes/claude/github-markdown-css/.gitignore
create mode 100644 public/themes/claude/github-markdown-css/.npmrc
create mode 100644 public/themes/claude/github-markdown-css/github-markdown-dark-colorblind.css
create mode 100644 public/themes/claude/github-markdown-css/github-markdown-dark-dimmed.css
create mode 100644 public/themes/claude/github-markdown-css/github-markdown-dark-high-contrast.css
create mode 100644 public/themes/claude/github-markdown-css/github-markdown-dark.css
create mode 100644 public/themes/claude/github-markdown-css/github-markdown-light-colorblind.css
create mode 100644 public/themes/claude/github-markdown-css/github-markdown-light.css
create mode 100644 public/themes/claude/github-markdown-css/github-markdown.css
create mode 100644 public/themes/claude/github-markdown-css/index.html
create mode 100644 public/themes/claude/github-markdown-css/license
create mode 100644 public/themes/claude/github-markdown-css/package.json
create mode 100644 public/themes/claude/github-markdown-css/readme.md
create mode 100644 styles/claude-readme.css
create mode 100644 themes/claude/components/ArticleAround.js
create mode 100644 themes/claude/components/ArticleInfo.js
create mode 100644 themes/claude/components/ArticleLock.js
create mode 100644 themes/claude/components/BlogArchiveItem.js
create mode 100644 themes/claude/components/BlogItem.js
create mode 100644 themes/claude/components/BlogListPage.js
create mode 100644 themes/claude/components/BlogListScroll.js
create mode 100644 themes/claude/components/BlogPostBar.js
create mode 100644 themes/claude/components/Catalog.js
create mode 100644 themes/claude/components/DarkModeButton.js
create mode 100644 themes/claude/components/ExampleRecentComments.js
create mode 100644 themes/claude/components/Footer.js
create mode 100644 themes/claude/components/JumpToTopButton.js
create mode 100644 themes/claude/components/MenuItemCollapse.js
create mode 100644 themes/claude/components/MenuItemDrop.js
create mode 100644 themes/claude/components/MenuList.js
create mode 100644 themes/claude/components/NavBar.js
create mode 100644 themes/claude/components/ProfileHome.js
create mode 100644 themes/claude/components/RecommendPosts.js
create mode 100644 themes/claude/components/SocialButton.js
create mode 100644 themes/claude/components/Title.js
create mode 100644 themes/claude/components/TopBar.js
create mode 100644 themes/claude/config.js
create mode 100644 themes/claude/index.js
create mode 100644 themes/claude/style.js
diff --git a/Claude-Theme-README-en.md b/Claude-Theme-README-en.md
new file mode 100644
index 00000000000..40d59af53d6
--- /dev/null
+++ b/Claude-Theme-README-en.md
@@ -0,0 +1,356 @@
+# Claude 3.5 Theme
+
+> Applies to: `themes/claude` (with some global changes in `pages/_app.js`, `pages/index.js`, and `components/SEO.js`)
+
+This theme combines the clean reading experience of **Claude Docs** with the rich personal profile structure of **GitHub**, creating a professional yet personal blog for developers.
+
+### Core Philosophy
+
+1. **Reading Experience**: Minimal distractions, clear typography (Anthropic Sans/Serif), and excellent code block readability.
+2. **Personal Branding**: A homepage that mirrors your GitHub profile (Bio, Activity, Heatmap).
+3. **Data Persistence**: Activity and contributions are tracked via Supabase for historical accuracy.
+4. **Performance**: Multi-layer caching ensures fast loads and stable rendering.
+
+---
+
+## Quick Start
+
+1. **Configure Environment**:
+ Add to your `.env` or `.env.local` (see below for details):
+ ```bash
+ # Activate Theme
+ NEXT_PUBLIC_THEME=claude # or configure in notion configuration page
+ NOTION_PAGE_ID=
+
+ # [Optional] Enable Contribution Persistence (Recommended)
+ SUPABASE_URL=
+ SUPABASE_SECRET_KEY=
+ CLAUDE_CONTRIBUTION_TRIGGER_TOKEN=
+ ```
+
+2. **Create Profile README**:
+ Create a Notion page with the slug `readme.md`. This page will be automatically rendered on your blog homepage.
+
+3. **Launch**:
+ ```bash
+ yarn dev
+ ```
+ Your blog is now running with the Claude theme!
+
+---
+
+## 1. Design Strategy
+
+### Desktop View
+The layout uses a 3-column structure inspired by technical documentation sites:
+- **Left Sidebar**: Persistent navigation, profile card, contact info, and a simulated terminal prompt.
+- **Center Content**: The main reading area, optimized for long-form content.
+- **Right Sidebar**: Table of Contents (TOC) that tracks scrolling position (Article pages only).
+
+### Mobile Adaptation
+Mobile design is a first-class citizen, not an afterthought:
+- **Navigation**: Collapses into a clean top bar.
+- **Heatmap**: Preserves the square aspect ratio of contribution cells; allows horizontal scrolling instead of shrinking cells.
+- **Typography**: Maintains readability with appropriate font scaling.
+- **Interactions**: Touch-friendly targets for all clickable elements.
+
+---
+
+## 2. Features Details
+
+### Contribution Heatmap & Activity
+The homepage features a GitHub-style contribution graph and activity feed.
+
+- **Heatmap**: Displays daily contribution levels (0-4) based on article creation and updates.
+- **Activity Feed**: A chronological stream of your "commits" (article updates) and "created repositories" (new articles).
+- **Yearly View**: Switch between a rolling 1-year window or specific calendar years.
+
+### README Rendering
+Your `readme.md` Notion page is rendered directly on the homepage, serving as your "Profile README".
+- **Rendering Pipeline**: Notion Blocks -> Markdown -> HTML.
+- **Engine**: Prioritizes GitHub's `/markdown` API for perfect GFM (GitHub Flavored Markdown) fidelity, falling back to a local parser if the API limit is reached.
+- **Caching**: Rendered HTML is cached to prevent redundant API calls and speed up page loads.
+
+### Terminal Widget
+A fun, dynamic element in the sidebar that shows:
+- Last login time (simulated).
+- Current "user" and "machine" (e.g., `user@Macintosh ~ %`).
+- Typing effect for the blog title.
+
+---
+
+## 3. Configuration
+
+Config file: `themes/claude/config.js`
+
+| Config Key | Description | Default |
+| :--- | :--- | :--- |
+| `CLAUDE_BLOG_NAME` | Main blog title | '活字印刷' |
+| `CLAUDE_BLOG_NAME_EN` | Subtitle / English title | 'Typography' |
+| `CLAUDE_MENU_CATEGORY` | Show sidebar categories | `true` |
+| `CLAUDE_MENU_TAG` | Show sidebar tags | `true` |
+| `CLAUDE_MENU_ARCHIVE` | Show sidebar archives | `true` |
+| `CLAUDE_TOC_ENABLE` | Enable Table of Contents | `true` |
+| `CLAUDE_TOC_SHOW_LEVEL3` | Show H3 in TOC | `true` |
+| `CLAUDE_TOC_SCROLL_BEHAVIOR` | TOC scroll animation | 'instant' (or 'smooth') |
+| `CLAUDE_PROFILE_AVATAR` | Custom avatar URL | `''` (Use global avatar) |
+| `CLAUDE_README_CACHE_ENABLED` | Cache README HTML | `true` |
+| `CLAUDE_CONTRIBUTION_PERSIST_ENABLED` | Enable Supabase persistence | `true` |
+| `CLAUDE_CONTRIBUTION_EVENT_LIMIT` | Max events to fetch | `50000` |
+
+> Most options can be overridden via `NEXT_PUBLIC_` env vars.
+
+---
+
+## 4. Environment Variables
+
+To fully enable all features, especially contribution persistence, configure the following in `.env.local` or your deployment platform.
+
+### Basic Setup
+```bash
+NEXT_PUBLIC_THEME=claude
+NOTION_PAGE_ID=
+```
+
+### Supabase (Required for Persistent Contributions)
+Without this, the heatmap is generated on-the-fly from the current post list, which loses historical accuracy (e.g., deleted posts, precise update times).
+
+```bash
+NEXT_PUBLIC_CLAUDE_CONTRIBUTION_PERSIST_ENABLED=true
+
+# Supabase Connection
+SUPABASE_URL=https://your-project.supabase.co
+SUPABASE_SECRET_KEY=your-service-role-key-or-secret
+```
+
+### Advanced Cache Control
+```bash
+# Enable internal caching (recommended)
+ENABLE_CACHE=true
+
+# Optional: Use Redis for distributed caching
+REDIS_URL=redis://user:pass@host:port
+```
+
+---
+
+## 5. Database Schema (Supabase)
+
+If using Supabase, create these two tables to store contribution data.
+
+### 1. Events Table (`claude_contribution_events_v1`)
+Stores individual contribution events (create/update).
+
+```sql
+create table if not exists public.claude_contribution_events_v1 (
+ event_id text primary key, -- e_md5(type|repoId|ts)
+ event_type text not null check (event_type in ('create', 'update')),
+ repository_id text not null, -- Normalized Post ID
+ timestamp_ms bigint not null,
+ title text default '',
+ slug text default ''
+);
+
+-- Indices for performance
+create index if not exists idx_claude_contrib_events_ts
+ on public.claude_contribution_events_v1 (timestamp_ms desc);
+create index if not exists idx_claude_contrib_events_repo
+ on public.claude_contribution_events_v1 (repository_id);
+```
+
+### 2. Snapshots Table (`claude_contribution_snapshots_v1`)
+Tracks the state of posts to detect changes during builds.
+
+```sql
+create table if not exists public.claude_contribution_snapshots_v1 (
+ repository_id text primary key,
+ title text default '',
+ slug text default '',
+ created_at_ms bigint not null default 0,
+ updated_at_ms bigint not null default 0,
+ synced_at_ms bigint not null default 0
+);
+
+create index if not exists idx_claude_contrib_snapshots_updated
+ on public.claude_contribution_snapshots_v1 (updated_at_ms desc);
+```
+
+### Update Logic
+1. **Sync**: On build (`getStaticProps` of index), the system compares current Notion posts against `snapshots`.
+2. **Detect**:
+ * New post? -> Insert `create` event.
+ * Updated post (`updatedAt > lastSnapshot.updatedAt`)? -> Insert `update` event.
+3. **Persist**: Updates are upserted to Supabase.
+4. **Display**: The heatmap reads from the `events` table (filtered to exclude today to prevent jitter).
+
+---
+
+## 6. Caching Strategy
+
+The theme employs a multi-level caching strategy for stability.
+
+1. **Contribution Daily Cache** (Local Memory):
+ * Aggregates events for the day.
+ * Reduces database reads.
+ * Refreshes on new builds or via API.
+
+2. **README HTML Cache**:
+ * Caches the expensive GitHub API markdown rendering.
+ * Key: `readme_render_snapshot_v2_${pageId}`.
+
+3. **GitHub Markdown API Cache**:
+ * Caches the raw response from GitHub to avoid rate limits (60 requests/hr for unauthenticated IPs).
+ * Fallback: If the API fails or limits, falls back to a local `marked` + `highlight.js` renderer.
+
+---
+
+## 7. Troubleshooting
+
+**Q: My heatmap is empty.**
+* Ensure `NEXT_PUBLIC_CLAUDE_CONTRIBUTION_PERSIST_ENABLED=true`.
+* Check if Supabase tables exist and keys are correct.
+* Verify `NOTION_PAGE_ID` allows access to the posts.
+
+**Q: Changes made today aren't showing.**
+* By design, the heatmap shows data *up to yesterday* to ensure the grid is stable and "complete". Today's dots appear tomorrow.
+* You can force a refresh manually if needed via the refresh API.
+
+**Q: The README styling looks different.**
+* This usually means the GitHub API rate limit was hit, and the theme fell back to the local renderer. It will recover automatically when the cache expires or the limit resets.
+
+---
+
+## 8. Sidebar Persistence Architecture
+
+### Problem
+
+In Next.js Pages Router, every client-side navigation (clicking a link) can re-render or even **remount** `LayoutBase`. This causes the left sidebar (avatar, terminal widget, navigation) to reload on every page transition — a poor user experience.
+
+### Three-Layer Solution
+
+The theme employs three layers to ensure the sidebar **only refreshes on browser refresh**, not on link-based navigation:
+
+#### Layer 1: `pages/_app.js` — Stabilize the Layout Component Reference
+
+> **⚠️ MERGE WARNING: This modification is in the global `pages/_app.js`, NOT inside the claude theme directory. Pay special attention to this file during merges.**
+
+The original code had two problems:
+1. `theme`'s `useMemo` depended on the entire `route` object (`[route]`). Since `useRouter()` returns a new object reference on every route change, `theme` was recalculated unnecessarily.
+2. `GLayout` was a wrapper component defined inside `MyApp` via `useCallback`, calling `getBaseLayoutByTheme(theme)` on every render.
+
+The fix:
+
+```javascript
+// Depend on specific values, not the entire route object
+const theme = useMemo(() => {
+ return (
+ getQueryParam(route.asPath, 'theme') ||
+ pageProps?.NOTION_CONFIG?.THEME ||
+ BLOG.THEME
+ )
+}, [route.asPath, pageProps?.NOTION_CONFIG?.THEME])
+
+// Memoize Layout component — stable reference as long as theme doesn't change
+const Layout = useMemo(() => getBaseLayoutByTheme(theme), [theme])
+
+// Use Layout directly, no GLayout wrapper
+
+
+
+
+```
+
+This ensures React always sees the same component type at the same tree position, so it **reuses** the `LayoutBase` instance (re-render) instead of destroying and recreating it (remount).
+
+#### Layer 2: `themes/claude/index.js` — Memoized SidebarContent
+
+The desktop sidebar is wrapped in `React.memo(() => true)`:
+
+```javascript
+const SidebarContent = memo(function SidebarContent(props) {
+ return (
+
+ )
+}, () => true) // Always returns true → blocks all prop-change re-renders
+```
+
+- `React.memo`'s comparator `() => true` tells React "props are always equal", preventing any parent re-render from propagating.
+- `MenuList` inside `NavBar` uses `useRouter()` (React Context), so active menu state still updates correctly — Context changes bypass `React.memo`.
+
+#### Layer 3: `themes/claude/components/NavBar.js` — Module-Level Terminal Session Cache
+
+The terminal login time and tty number are stored in a **JavaScript module-level variable**, outside of React's component lifecycle:
+
+```javascript
+let _cachedTerminalSession = null
+function getOrCreateTerminalSession() {
+ if (!_cachedTerminalSession) {
+ _cachedTerminalSession = {
+ loginTime: formatTerminalLoginTime(new Date()),
+ tty: `ttys00${Math.floor(Math.random() * 10)}`
+ }
+ }
+ return _cachedTerminalSession
+}
+```
+
+- Module-level variables persist across component mount/unmount cycles.
+- Only a full browser refresh (which reloads the JS module) resets this value.
+
+### Files Affected
+
+| File | Scope | Change |
+|---|---|---|
+| `pages/_app.js` | **Global** (not inside theme dir) | Removed `GLayout`; memoized `Layout` reference |
+| `themes/claude/index.js` | Theme | Added `SidebarContent` memo wrapper |
+| `themes/claude/components/NavBar.js` | Theme | Terminal session → module-level cache |
+
+---
+
+## 9. Development
+
+### Project Structure
+* `themes/claude/components/`: UI components (NavBar, Catalog, etc.).
+* `themes/claude/style.js`: CSS variables and global styles.
+* `lib/server/claude/contributionStore.js`: Subabase logic.
+* `pages/api/claude/`: API endpoints for cache revalidation.
+
+### Commands
+* `yarn dev`: Run locally.
+* `yarn build`: Production build (triggers contribution sync).
+
+---
+
+## 10. Additional Global Changes (RSS + Homepage Title)
+
+These changes are outside the `themes/claude` directory but directly affect runtime behavior.
+
+### 10.1 Stop RSS content fetching when RSS is disabled
+
+* File: `pages/index.js`
+* Change: `generateRss(props)` is no longer unconditional; it now runs only when `ENABLE_RSS=true`.
+* Result:
+ * When RSS is disabled, `getPostBlocks(..., 'rss-content')` is not called.
+ * Server logs such as `from:rss-content` disappear.
+
+### 10.2 Remove subtitle from homepage ``
+
+* File: `components/SEO.js`
+* Route: `/` (homepage)
+* Change: homepage title changed from `site title | site description` to `site title` only.
+* Result:
+ * No fallback subtitle like `这是一个由NotionNext生成的站点` in browser tabs.
+ * No separator `|` on homepage title.
+
+### 10.3 ⚠️ Merge / Upgrade Notes (Consolidated)
+
+When pulling upstream updates, verify all of the following remain intact:
+
+1. In `pages/_app.js`, `Layout` is still cached with `useMemo(() => getBaseLayoutByTheme(theme), [theme])` (see Section 8, Layer 1).
+2. In `pages/_app.js`, `theme`'s `useMemo` dependencies are still `[route.asPath, pageProps?.NOTION_CONFIG?.THEME]`, **NOT** `[route]`.
+3. In `pages/_app.js`, no `useCallback` wrapper component is used to indirectly call `getBaseLayoutByTheme`.
+4. RSS generation in `pages/index.js` is still gated by `ENABLE_RSS`.
+5. Homepage title in `components/SEO.js` still uses site title only (no appended description).
diff --git a/Claude-Theme-README.md b/Claude-Theme-README.md
new file mode 100644
index 00000000000..ef8f42d4c7b
--- /dev/null
+++ b/Claude-Theme-README.md
@@ -0,0 +1,725 @@
+# Claude Theme README
+
+> 适用目录:`themes/claude`(部分修改涉及全局 `pages/_app.js`、`pages/index.js`、`components/SEO.js`)
+>
+> 本文档描述当前 `claude` 主题的实际实现,重点覆盖:
+> 1. 主题特性与视觉设计目标
+> 2. 文章页(Claude Code Docs 风格)与首页(GitHub Profile 风格)
+> 3. 移动端复刻与优化策略
+> 4. Contribution 热力图生成逻辑
+> 5. 数据库设计与更新逻辑
+> 6. 缓存设计
+> 7. 配置项与环境变量说明
+> 8. 如何启用并使用该主题
+> 9. **侧边栏持久化架构**(含全局 `_app.js` 修改,合并时需注意)
+
+---
+
+## 1. 主题定位与核心特性
+
+`claude` 主题是一个“混合型”主题:
+
+- 文章阅读体验:参考 Claude Code Docs 的排版与色彩体系。
+- 首页信息架构:1:1 借鉴 GitHub 个人主页(头像、资料、联系方式、贡献热力图、活动流)。
+- 交互策略:内容优先、低干扰、轻动效。
+- 数据策略:首页活动数据支持持久化到 Supabase,避免每次都从前端即时推导。
+
+主要能力:
+
+- 三栏布局(左侧资料栏 / 中间正文 / 右侧 TOC)。
+- 文章页 Notion 原生渲染(`NotionPage`)+ 主题化样式。
+- 首页 README 卡片:Notion blockMap 转 Markdown,再转 HTML 渲染。
+- GitHub 风格 Contribution 热力图 + 活动摘要流。
+- Contribution 事件持久化(`create` / `update`)与去重。
+- 多层缓存(页面缓存、README 缓存、Contrib 日缓存)与失败回退。
+
+---
+
+## 快速开始 (Quick Start)
+
+只需要简单三步即可体验:
+
+1. **配置环境变量**:
+ 在 `.env` 或 `.env.local` 中添加(完整配置见下文):
+ ```bash
+ # 启用主题
+ NEXT_PUBLIC_THEME=claude # 或者在notion配置页面中配置
+ NOTION_PAGE_ID=
+
+ # [可选] 启用贡献热力图持久化 (推荐)
+ SUPABASE_URL=
+ SUPABASE_SECRET_KEY=
+ CLAUDE_CONTRIBUTION_TRIGGER_TOKEN=
+ ```
+
+2. **创建个人资料页**:
+ 在 Notion 中新建一个页面,并将其 **slug** 设置为 `readme.md`。该内容将显示在首页。
+
+3. **启动**:
+ ```bash
+ yarn dev
+ ```
+
+---
+
+## 2. 目录结构与关键文件
+
+主题核心文件:
+
+- `themes/claude/index.js`
+ - 主题布局入口(`LayoutBase` / `LayoutIndex` / `LayoutSlug` 等)。
+- `themes/claude/style.js`
+ - 主题变量与全量样式(含桌面/移动端规则)。
+- `themes/claude/config.js`
+ - 主题配置项与默认值。
+- `themes/claude/components/ProfileHome.js`
+ - 首页 README、热力图、Contribution activity 逻辑。
+- `themes/claude/components/NavBar.js`
+ - 左侧资料栏(头像、联系方式、导航、终端模拟块)。
+- `themes/claude/components/MenuList.js`
+ - 菜单渲染与 icon 规则(支持 Notion icon 字段写 Font Awesome)。
+- `themes/claude/components/Catalog.js`
+ - 右侧目录(TOC)与滚动联动。
+
+服务端数据链路相关:
+
+- `pages/index.js`
+ - 首页 `getStaticProps`:README 渲染、Contrib 同步、缓存回退。
+- `lib/server/claude/contributionStore.js`
+ - Supabase 读写、事件生成、去重、日缓存。
+- `lib/db/notion/notionBlocksToHtml.js`
+ - Notion blockMap -> Markdown -> HTML(GitHub API 优先,本地回退)。
+- `pages/api/claude/contribution-refresh.js`
+ - 手动刷新 Contribution 缓存与 ISR 触发。
+
+---
+
+## 3. 文章样式(仿 Claude Code Docs)
+
+### 3.1 布局风格
+
+文章页采用 `LayoutSlug` + `NotionPage` 渲染正文,外层由 `LayoutBase` 提供:
+
+- 左侧固定宽资料/导航区域。
+- 中间内容区(多档 `max-w-*` 宽度控制)。
+- 右侧目录区(文章页 + 桌面端 + TOC 开启时显示)。
+
+### 3.2 样式体系
+
+`themes/claude/style.js` 中定义了大量 CSS 变量:
+
+- 配色变量:正文、边框、强调色、暗色模式等。
+- 字体变量:
+ - 标题:`Anthropic Serif Display`
+ - 正文:`Anthropic Sans Text`
+ - 等宽:`JetBrains Mono` fallback 链
+- Notion 内容区域样式覆写:链接、引用、代码、表格、callout、目录高亮等。
+
+### 3.3 目录(TOC)行为
+
+目录组件:`themes/claude/components/Catalog.js`
+
+- 支持 L1/L2 必显。
+- L3 由 `CLAUDE_TOC_SHOW_LEVEL3` 控制。
+- 滚动监听并高亮当前 section,同时联动父级。
+- `On this page` 标题可回到顶部。
+- 滚动行为支持 `smooth` 或 `instant`(`CLAUDE_TOC_SCROLL_BEHAVIOR`)。
+
+---
+
+## 4. 首页(1:1 GitHub Profile 复刻)
+
+首页组件:`themes/claude/components/ProfileHome.js`
+
+从上到下主要区块:
+
+1. README 卡片(`README.md` 标签头 + 内容区)
+2. Contribution 热力图区块
+3. Contribution activity 时间流
+
+左侧资料栏组件:`themes/claude/components/NavBar.js`
+
+- 圆形头像(GitHub 风格)。
+- 昵称 + Bio。
+- 联系方式(GitHub / Email)。
+- 导航菜单(支持图标)。
+- 终端模拟区:
+ - 第一行:`Last login: ... on ttys00x`
+ - 第二行:`{author}@Macintosh ~ % {blogName}` + 光标
+ - 通过 `ResizeObserver` 自动缩放字体,尽量保证同一行展示。
+
+---
+
+## 5. 移动端复刻与优化
+
+移动端保留“与桌面一致的字体风格和信息层级”,但对结构做适配:
+
+- 左侧栏折叠为顶部简化导航。
+- Contribution 热力图容器允许横向滚动(保持 cell 尺寸,不压缩方块)。
+- 滚动条隐藏但可滑动。
+- Year 选择器改为移动端友好的下拉菜单,放在 `Contribution activity` 标题行。
+- README / 活动卡片边距与圆角在窄屏下重新平衡。
+
+实现重点:
+
+- 桌面端 cell 尺寸随宽度动态计算。
+- 移动端强制固定 `contribCellSize=11`,避免字体/格子被缩放破坏视觉一致性。
+
+---
+
+## 6. Contribution 热力图生成逻辑
+
+核心代码:`themes/claude/components/ProfileHome.js`
+
+### 6.1 输入数据
+
+优先使用持久化事件(`props.contributionEvents`):
+
+- `type`: `create` 或 `update`
+- `repositoryId`
+- `timestampMs`
+- `title` / `slug`
+
+如果持久化不可用,则回退到前端从 `posts` 直接推导:
+
+- 每篇文章产生一个 `create`(createdAt)
+- 若更新时间与创建时间不同,再产生一个 `update`(updatedAt)
+
+### 6.2 统计区间
+
+两种模式:
+
+- 默认模式:最近 1 年(滚动窗口)
+- 年份模式:固定某一年(1 月 1 日到 12 月 31 日)
+
+区间会对齐到整周边界:
+
+- 起点对齐到周日
+- 终点对齐到周六
+
+### 6.3 颜色分级(level)
+
+`CONTRIBUTION_LEVEL_THRESHOLDS`:
+
+- `0`: 无贡献
+- `1`: `count === 1`
+- `2`: `count >= 2`
+- `3`: `count >= 3`
+- `4`: `count >= 6`
+
+说明:`1 contribution/day` 必须稳定映射到同一颜色等级。
+
+### 6.4 月标签策略
+
+默认滚动年模式:
+
+- 按周列起始日期所属月份生成 marker(接近 GitHub 视觉规则)。
+
+固定年份模式:
+
+- 每个月从当月第一天所在周列生成 marker。
+
+### 6.5 交互
+
+- Hover cell 显示 tooltip(延迟触发,避免抖动)。
+- 点击某一天可过滤下方 activity(再次点击取消)。
+- `Less`/`More` legend 与热力图颜色等级一致。
+
+---
+
+## 7. Contribution Activity 生成逻辑
+
+核心代码仍在 `themes/claude/components/ProfileHome.js`。
+
+渲染策略:
+
+- 默认按“月”分组(如 March 2026)。
+- 点选某一天后,切换成“日”分组(如 March 3, 2026)。
+- 组内分别聚合:
+ - `update` 事件 -> commit summary(按仓库聚合,统计 commitCount)
+ - `create` 事件 -> created repositories 列表
+
+显示逻辑:
+
+- 无数据时展示 empty state。
+- 有更新和创建则分别渲染摘要行。
+- 链接点击跳转对应文章。
+
+---
+
+## 8. 数据库设计(Supabase)
+
+服务端存储实现:`lib/server/claude/contributionStore.js`
+
+使用两张表:
+
+- `claude_contribution_events_v1`
+- `claude_contribution_snapshots_v1`
+
+### 8.1 推荐表结构
+
+```sql
+create table if not exists public.claude_contribution_events_v1 (
+ event_id text primary key,
+ event_type text not null check (event_type in ('create', 'update')),
+ repository_id text not null,
+ timestamp_ms bigint not null,
+ title text default '',
+ slug text default ''
+);
+
+create index if not exists idx_claude_contrib_events_ts
+ on public.claude_contribution_events_v1 (timestamp_ms desc);
+
+create index if not exists idx_claude_contrib_events_repo
+ on public.claude_contribution_events_v1 (repository_id);
+
+create table if not exists public.claude_contribution_snapshots_v1 (
+ repository_id text primary key,
+ title text default '',
+ slug text default '',
+ created_at_ms bigint not null default 0,
+ updated_at_ms bigint not null default 0,
+ synced_at_ms bigint not null default 0
+);
+
+create index if not exists idx_claude_contrib_snapshots_updated
+ on public.claude_contribution_snapshots_v1 (updated_at_ms desc);
+```
+
+### 8.2 字段语义
+
+`events` 表:
+
+- `event_id`:事件主键,规则为 `e_${md5(type|repositoryId|timestampMs)}`
+- `event_type`:`create` / `update`
+- `repository_id`:文章 ID 归一化(去 `-` + 小写)
+- `timestamp_ms`:事件时间戳(毫秒)
+- `title` / `slug`:冗余展示信息
+
+`snapshots` 表:
+
+- `repository_id`:文章唯一标识(主键)
+- `created_at_ms`:创建时间
+- `updated_at_ms`:最近更新时间
+- `synced_at_ms`:本次同步时间
+
+---
+
+## 9. 更新逻辑(从 Notion 到数据库)
+
+入口:`pages/index.js` 的 `getStaticProps`。
+
+### 9.1 同步步骤
+
+1. 获取已发布文章(排除 `readme.md`)。
+2. 每篇文章构建 snapshot:
+ - `repositoryId`
+ - `createdAtMs`
+ - `updatedAtMs`
+3. 调用 `syncContributionSnapshots(snapshots)`:
+ - upsert snapshot(冲突键 `repository_id`)
+ - 根据“新旧快照差异”生成事件
+4. 拉取事件 `listContributionEvents(limit)`。
+5. 过滤到“昨天为止”:
+ - `filterContributionEventsUntilYesterday`
+ - 当天事件不显示在首页(稳定 UI,避免当天多次刷新抖动)
+6. 写入本地日缓存,返回给前端。
+
+### 9.2 事件生成规则(关键)
+
+在 `syncContributionSnapshots` 内:
+
+- 若快照不存在(新文章):
+ - 创建 `create` 事件(`created_at_ms`)
+ - 如果 `updated_at_ms > created_at_ms`,再创建 `update` 事件
+- 若快照已存在:
+ - 仅当 `updated_at_ms` 大于旧快照时,新增 `update` 事件
+- 事件写入前按 `event_id` 去重,保证同一逻辑事件只存在一份。
+
+---
+
+## 10. 缓存设计
+
+本主题使用多层缓存,目标是减少重复请求并提高稳定性。
+
+### 10.1 Contribution 日缓存(进程内)
+
+位置:`lib/server/claude/contributionStore.js`
+
+- 缓存键:`globalThis.__claude_contribution_daily_cache_v1`
+- 内容:`dayKey` / `events` / `updatedAtMs` / `dirty`
+- 刷新条件:
+ - 手动强制(`CLAUDE_CONTRIBUTION_FORCE_REFRESH=true`)
+ - build/export 阶段
+ - 当日尚未刷新
+ - 通过 API 标记 `dirty`
+
+失败回退:
+
+- 刷新失败时优先使用 stale 缓存(`allowStale=true`)
+- 仍不可用则回退到前端即时计算
+
+### 10.2 README 快照缓存
+
+位置:`pages/index.js`
+
+- 键:`readme_render_snapshot_v2_${pageId}_${locale}`
+- 缓存内容:
+ - `bodyFingerprint`
+ - `excerpt`
+ - `readmeHtml`
+ - `readmeHtmlSource`
+- 逻辑:
+ - 若正文指纹未变,直接复用缓存 HTML
+ - 若变化,重新执行转换与渲染
+
+### 10.3 GitHub Markdown API 缓存
+
+位置:`lib/db/notion/notionBlocksToHtml.js`
+
+- 键:`readme_github_md_${md5(markdown)}`
+- 策略:
+ 1. 先查缓存,命中直接返回。
+ 2. 调 GitHub `/markdown` API。
+ 3. API 失败/超限,再查一次缓存。
+ 4. 仍失败则回退到本地 `marked + highlight.js`。
+
+说明:GitHub 匿名接口有速率限制,本层缓存用于显著降低超限概率。
+
+### 10.4 全站缓存后端
+
+统一缓存门面:`lib/cache/cache_manager.js`
+
+- 优先 Redis(`REDIS_URL`)
+- 否则文件缓存(`ENABLE_FILE_CACHE`)
+- 否则内存缓存(开发 120 分钟,生产 10 分钟)
+
+---
+
+## 11. README 渲染链路
+
+目标:在首页 README 卡片中稳定展示富文本与代码高亮,避免 hydration 相关问题。
+
+当前实现:
+
+1. 从 Notion 拉 `readme.md` 的 `blockMap`
+2. `notionBlocksToMarkdown(blockMap, pageId)` 转 Markdown
+3. `renderMarkdownToHtml(markdown)` 转 HTML
+ - 优先 GitHub API
+ - 失败则本地 fallback
+4. 前端 `ProfileHome` 直接渲染:
+ - ``
+
+样式来源:
+
+- `styles/claude-readme.css` 导入 GitHub Markdown CSS
+- `_app.js` 导入 `highlight.js` 主题(本地 fallback 时生效)
+
+---
+
+## 12. 配置项说明(Claude 主题)
+
+配置文件:`themes/claude/config.js`
+
+以下配置可由环境变量覆盖(`NEXT_PUBLIC_*`),并可被 Notion 配置页同名项再覆盖:
+
+| 配置项 | 默认值 | 说明 |
+|---|---|---|
+| `CLAUDE_BLOG_NAME` | `活字印刷` | 主题主标题 |
+| `CLAUDE_BLOG_NAME_EN` | 同主标题 | 副标题/英文标题 |
+| `CLAUDE_POST_AD_ENABLE` | `false` | 列表插广告 |
+| `CLAUDE_POST_COVER_ENABLE` | `false` | 列表显示封面 |
+| `CLAUDE_ARTICLE_RECOMMEND_POSTS` | `true` | 文章页推荐文章 |
+| `CLAUDE_MENU_CATEGORY` | `true` | 显示分类菜单 |
+| `CLAUDE_MENU_TAG` | `true` | 显示标签菜单 |
+| `CLAUDE_MENU_ARCHIVE` | `true` | 显示归档菜单 |
+| `CLAUDE_TOC_ENABLE` | `true` | 启用右侧目录 |
+| `CLAUDE_TOC_SHOW_LEVEL3` | `true` | 目录显示三级标题 |
+| `CLAUDE_TOC_SCROLL_BEHAVIOR` | `instant` | TOC 点击/联动滚动行为 |
+| `CLAUDE_SUBTITLE_DARK_ONLY` | `false` | 副标题仅暗色显示 |
+| `CLAUDE_PROFILE_AVATAR` | `''` | 侧栏头像 URL |
+| `CLAUDE_FOOTER_COPYRIGHT` | `''` | 自定义页脚版权文案 |
+| `CLAUDE_README_CACHE_ENABLED` | `true` | README 快照缓存开关 |
+| `CLAUDE_CONTRIBUTION_PERSIST_ENABLED` | `true` | Contribution 持久化开关 |
+| `CLAUDE_CONTRIBUTION_EVENT_LIMIT` | `50000` | 拉取事件上限 |
+
+---
+
+## 13. 环境变量说明
+
+### 13.1 必需(最小可运行)
+
+```bash
+NEXT_PUBLIC_THEME=claude
+NOTION_PAGE_ID=
+```
+
+### 13.2 Notion 访问(可选,私有库常用)
+
+```bash
+NOTION_TOKEN_V2=
+NOTION_ACTIVE_USER=
+```
+
+说明:
+
+- `NOTION_TOKEN_V2`:用于访问非公开 Notion 数据。
+- `NOTION_ACTIVE_USER`:可选,不填时使用 token 仍可工作(取决于 Notion 侧权限)。
+
+### 13.3 Contribution 持久化(Supabase)
+
+```bash
+NEXT_PUBLIC_CLAUDE_CONTRIBUTION_PERSIST_ENABLED=true
+NEXT_PUBLIC_CLAUDE_CONTRIBUTION_EVENT_LIMIT=50000
+
+SUPABASE_URL=
+SUPABASE_SECRET_KEY= # 或 SUPABASE_SERVICE_ROLE_KEY
+
+# 可选前端命名回退
+NEXT_PUBLIC_SUPABASE_URL=
+NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=
+```
+
+补充控制项:
+
+```bash
+CLAUDE_CONTRIBUTION_FORCE_REFRESH=false
+CLAUDE_CONTRIBUTION_TRIGGER_TOKEN=
+```
+
+### 13.4 README 缓存与全站缓存
+
+```bash
+NEXT_PUBLIC_CLAUDE_README_CACHE_ENABLED=true
+ENABLE_CACHE=true
+REDIS_URL=
+```
+
+---
+
+## 14. 如何使用该主题
+
+### 步骤 1:切换主题
+
+在 `.env.local`:
+
+```bash
+NEXT_PUBLIC_THEME=claude
+```
+
+### 步骤 2:准备首页 README 页面
+
+在 Notion 中准备一个页面,slug 必须为:
+
+```text
+readme.md
+```
+
+首页会自动识别该页面并渲染到 README 卡片。
+
+### 步骤 3:可选启用 Contribution 持久化
+
+1. 在 Supabase 创建两张表(见第 8 节 SQL)。
+2. 配置 Supabase 环境变量。
+3. 打开 `NEXT_PUBLIC_CLAUDE_CONTRIBUTION_PERSIST_ENABLED=true`。
+
+### 步骤 4:启动
+
+```bash
+yarn dev
+```
+
+生产构建:
+
+```bash
+yarn build
+yarn start
+```
+
+### 步骤 5:手动触发贡献刷新(可选)
+
+接口:`/api/claude/contribution-refresh`
+
+示例:
+
+```bash
+curl "http://localhost:3000/api/claude/contribution-refresh?token=&revalidate=1&path=/"
+```
+
+---
+
+## 15. 运行日志与排障
+
+### 15.1 日志前缀
+
+- Contribution:`[Contrib] ...`
+- README 渲染:`[README] ...`
+
+注意:这些日志在服务端终端输出,不在浏览器控制台。
+
+### 15.2 常见问题
+
+1. 热力图无数据
+
+- 检查 `NEXT_PUBLIC_CLAUDE_CONTRIBUTION_PERSIST_ENABLED` 是否为 `true`
+- 检查 Supabase 连接变量是否正确
+- 检查两张表是否已创建
+
+2. 当天更新未显示
+
+- 当前逻辑默认过滤“今天”的事件,只显示到昨天(设计行为)
+
+3. README 代码块高亮不稳定
+
+- 若 GitHub `/markdown` 超限会自动回退到本地 `marked + highlight.js`
+- 可通过缓存复用此前成功渲染结果
+
+---
+
+## 16. 侧边栏持久化架构
+
+### 16.1 问题背景
+
+在 Next.js Pages Router 中,每次客户端导航(点击链接跳转)都会重新渲染甚至重新挂载(remount)`LayoutBase`。这导致左侧栏(头像、终端模拟块、导航菜单)每次跳转都重新加载,用户体验不佳。
+
+### 16.2 三层防护方案
+
+本主题通过三层机制确保左侧栏**仅在浏览器刷新时**才重新加载:
+
+#### 第一层:`pages/_app.js` — 从根源消除 Layout 组件 remount
+
+> **⚠️ 合并注意:此修改位于全局 `pages/_app.js`,非 claude 主题目录内。合并时请特别关注此文件。**
+
+原始代码存在两个问题:
+
+1. `theme` 的 `useMemo` 依赖整个 `route` 对象(`[route]`)。`useRouter()` 每次路由变化都返回新的对象引用,导致 `theme` 每次都重新计算。
+2. `GLayout` 是在组件内部通过 `useCallback` 定义的包装组件,每次渲染都在内部调用 `getBaseLayoutByTheme(theme)`。
+
+修复后:
+
+```javascript
+// 依赖改为具体值,而非整个 route 对象
+const theme = useMemo(() => {
+ return (
+ getQueryParam(route.asPath, 'theme') ||
+ pageProps?.NOTION_CONFIG?.THEME ||
+ BLOG.THEME
+ )
+}, [route.asPath, pageProps?.NOTION_CONFIG?.THEME])
+
+// 用 useMemo 缓存 Layout 组件引用,相同 theme 下始终返回同一个组件
+const Layout = useMemo(() => getBaseLayoutByTheme(theme), [theme])
+
+// 直接使用 Layout,不再通过 GLayout 包装
+
+
+
+
+```
+
+关键效果:`Layout`(即 `LayoutBase`)在同一主题下始终是同一个组件引用,React 不会因组件类型变化而 remount 整棵子树。
+
+#### 第二层:`themes/claude/index.js` — SidebarContent 记忆化
+
+桌面端侧边栏用 `React.memo(() => true)` 包裹:
+
+```javascript
+const SidebarContent = memo(function SidebarContent(props) {
+ return (
+
+ )
+}, () => true) // 始终返回 true → 阻止所有来自父组件的 prop 变化触发重渲染
+```
+
+- `React.memo` 的第二个参数 `() => true` 表示"props 始终相等",阻止父组件 re-render 传播。
+- `MenuList` 内部的 `useRouter()` 基于 React Context,路由变化仍会绕过 memo 正常更新菜单激活状态。
+
+#### 第三层:`themes/claude/components/NavBar.js` — 模块级终端会话缓存
+
+终端区域的登录时间和 tty 编号存储在 **JS 模块级变量**(非 React 状态)中:
+
+```javascript
+let _cachedTerminalSession = null
+function getOrCreateTerminalSession() {
+ if (!_cachedTerminalSession) {
+ _cachedTerminalSession = {
+ loginTime: formatTerminalLoginTime(new Date()),
+ tty: `ttys00${Math.floor(Math.random() * 10)}`
+ }
+ }
+ return _cachedTerminalSession
+}
+```
+
+- 模块级变量在 JS 模块作用域中,不属于任何 React 组件实例。
+- 即使极端情况下组件被 remount,缓存值不会丢失。
+- 只有浏览器刷新(JS 模块重新加载)时才重置。
+
+### 16.3 涉及文件清单
+
+| 文件 | 所属 | 修改内容 |
+|---|---|---|
+| `pages/_app.js` | **全局**(非主题目录) | 移除 `GLayout`,用 `useMemo` 缓存 Layout 引用 |
+| `themes/claude/index.js` | 主题 | 新增 `SidebarContent` memo 组件 |
+| `themes/claude/components/NavBar.js` | 主题 | 终端会话改为模块级缓存 |
+
+---
+
+## 17. 设计约束与已知行为
+
+- `claude` 主题只影响自身主题目录,**但侧边栏持久化修改涉及全局 `pages/_app.js`**(见第 16 节)。
+- Contribution 事件是幂等写入,不应重复产生同一 `create` 事件。
+- README 渲染采用“服务端转换 + 前端静态 HTML 注入”,目标是稳定优先。
+- 移动端优先保持桌面视觉语言一致,不随屏宽自动降级字体粗细/大小。
+
+---
+
+## 18. 维护建议
+
+- 如果你修改了热力图规则,请同步更新:
+ - `themes/claude/components/ProfileHome.js`
+ - 本 README 的第 6、7 节
+- 如果你修改了表字段,请同步更新:
+ - `lib/server/claude/contributionStore.js`
+ - 本 README 的第 8、9 节 SQL 与字段说明
+- 如果你修改了缓存策略,请同步更新:
+ - `lib/cache/*`
+ - `pages/index.js`
+ - `lib/db/notion/notionBlocksToHtml.js`
+
+---
+
+## 19. 全局改动补充(RSS 与首页标题)
+
+以下变更位于主题目录之外,但会直接影响 `claude` 主题实际运行行为:
+
+### 19.1 RSS 关闭时不再触发 RSS 内容抓取
+
+- 文件:`pages/index.js`
+- 变更:`generateRss(props)` 从“无条件执行”改为“仅在 `ENABLE_RSS=true` 时执行”。
+- 结果:
+ - 当你禁用 RSS 后,不再调用 `getPostBlocks(..., 'rss-content')`。
+ - 服务端日志中的 `from:rss-content` 不会再出现。
+
+### 19.2 首页标签页标题不再拼接副标题
+
+- 文件:`components/SEO.js`
+- 路由:`/`(首页)
+- 变更:首页 title 从 `site title | site description` 改为仅显示 `site title`。
+- 结果:
+ - 未配置副标题时,不会再出现默认文案“这是一个由NotionNext生成的站点”。
+ - 分隔符 `|` 也不会显示。
+
+### 19.3 ⚠️ 合并 / 升级注意(汇总)
+
+若后续合并上游更新,请统一检查以下项是否仍保留:
+
+1. `pages/_app.js` 中 `Layout` 仍通过 `useMemo(() => getBaseLayoutByTheme(theme), [theme])` 缓存引用(见第 16.2 节第一层)。
+2. `pages/_app.js` 中 `theme` 的 `useMemo` 依赖仍为 `[route.asPath, pageProps?.NOTION_CONFIG?.THEME]`,**而非** `[route]`。
+3. `pages/_app.js` 中仍不使用 `useCallback` 包装组件来间接调用 `getBaseLayoutByTheme`。
+4. `pages/index.js` 里 RSS 生成仍受 `ENABLE_RSS` 开关控制。
+5. `components/SEO.js` 里首页 title 仍仅使用主标题,不拼接 description。
diff --git a/lib/server/claude/contributionStore.js b/lib/server/claude/contributionStore.js
new file mode 100644
index 00000000000..2e724140601
--- /dev/null
+++ b/lib/server/claude/contributionStore.js
@@ -0,0 +1,510 @@
+import md5 from 'js-md5'
+import { createClient } from '@supabase/supabase-js'
+
+const EVENTS_TABLE = 'claude_contribution_events_v1'
+const SNAPSHOTS_TABLE = 'claude_contribution_snapshots_v1'
+const LOCAL_CACHE_KEY = '__claude_contribution_daily_cache_v1'
+
+let supabaseClient = null
+let legacyCleanupPromise = null
+
+const getSupabaseUrl = () => {
+ return process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL || ''
+}
+
+const getSupabaseKey = () => {
+ return (
+ process.env.SUPABASE_SECRET_KEY ||
+ process.env.SUPABASE_SERVICE_ROLE_KEY ||
+ process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY ||
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
+ ''
+ )
+}
+
+const toTimestampMs = value => {
+ if (value === null || value === undefined || value === '') return 0
+ if (typeof value === 'number') {
+ if (!Number.isFinite(value) || value <= 0) return 0
+ return Math.trunc(value)
+ }
+ const parsed = Date.parse(String(value))
+ if (!Number.isFinite(parsed) || parsed <= 0) return 0
+ return parsed
+}
+
+const normalizeRepositoryId = value => {
+ if (!value) return ''
+ return String(value).replace(/-/g, '').trim().toLowerCase()
+}
+
+const normalizeText = value => {
+ return typeof value === 'string' ? value : ''
+}
+
+const buildHrefFromSlug = slug => {
+ const normalizedSlug = normalizeText(slug).trim()
+ if (!normalizedSlug) return ''
+ if (/^https?:\/\//i.test(normalizedSlug)) return normalizedSlug
+ return normalizedSlug.startsWith('/') ? normalizedSlug : `/${normalizedSlug}`
+}
+
+const buildEventId = (type, repositoryId, timestampMs) => {
+ return `e_${md5(`${type}|${repositoryId}|${timestampMs}`)}`
+}
+
+const chunkArray = (arr, size = 200) => {
+ const list = Array.isArray(arr) ? arr : []
+ const safeSize = Math.max(1, Math.min(1000, Number(size) || 200))
+ const chunks = []
+ for (let i = 0; i < list.length; i += safeSize) {
+ chunks.push(list.slice(i, i + safeSize))
+ }
+ return chunks
+}
+
+const formatDayKey = date => {
+ const year = date.getFullYear()
+ const month = String(date.getMonth() + 1).padStart(2, '0')
+ const day = String(date.getDate()).padStart(2, '0')
+ return `${year}-${month}-${day}`
+}
+
+const getTodayKey = (nowMs = Date.now()) => {
+ return formatDayKey(new Date(nowMs))
+}
+
+const getYesterdayEndMs = (nowMs = Date.now()) => {
+ const date = new Date(nowMs)
+ date.setHours(0, 0, 0, 0)
+ date.setMilliseconds(-1)
+ return date.getTime()
+}
+
+const getLocalDailyCache = () => {
+ if (typeof globalThis === 'undefined') {
+ return { dayKey: '', events: [], updatedAtMs: 0, dirty: true }
+ }
+
+ if (!globalThis[LOCAL_CACHE_KEY]) {
+ globalThis[LOCAL_CACHE_KEY] = {
+ dayKey: '',
+ events: [],
+ updatedAtMs: 0,
+ dirty: true
+ }
+ }
+ return globalThis[LOCAL_CACHE_KEY]
+}
+
+export const markContributionCacheDirty = () => {
+ const cache = getLocalDailyCache()
+ cache.dirty = true
+ cache.updatedAtMs = Date.now()
+}
+
+export const shouldRefreshContributionDailyCache = ({
+ forceRefresh = false,
+ isBuild = false,
+ nowMs = Date.now()
+} = {}) => {
+ if (forceRefresh || isBuild) return true
+ const cache = getLocalDailyCache()
+ if (cache.dirty) return true
+ if (!Array.isArray(cache.events)) return true
+ return cache.dayKey !== getTodayKey(nowMs)
+}
+
+export const setContributionEventsToLocalCache = (events, nowMs = Date.now()) => {
+ const cache = getLocalDailyCache()
+ cache.dayKey = getTodayKey(nowMs)
+ cache.events = Array.isArray(events) ? events : []
+ cache.updatedAtMs = nowMs
+ cache.dirty = false
+ return cache
+}
+
+export const getContributionEventsFromLocalCache = ({
+ limit = 50000,
+ allowStale = false,
+ nowMs = Date.now()
+} = {}) => {
+ const cache = getLocalDailyCache()
+ if (!Array.isArray(cache.events)) return null
+ if (!allowStale) {
+ if (cache.dirty) return null
+ if (cache.dayKey !== getTodayKey(nowMs)) return null
+ }
+ const safeLimit = Math.max(1, Math.min(100000, Number(limit) || 50000))
+ const events = cache.events
+ if (events.length <= safeLimit) return events
+ return events.slice(events.length - safeLimit)
+}
+
+export const filterContributionEventsUntilYesterday = (
+ events,
+ nowMs = Date.now()
+) => {
+ const cutoffMs = getYesterdayEndMs(nowMs)
+ return (Array.isArray(events) ? events : []).filter(event => {
+ const timestampMs = toTimestampMs(event?.timestampMs || event?.timestamp)
+ return timestampMs > 0 && timestampMs <= cutoffMs
+ })
+}
+
+const getSupabaseClient = () => {
+ const url = getSupabaseUrl()
+ const key = getSupabaseKey()
+ if (!url || !key) return null
+
+ if (supabaseClient) return supabaseClient
+ supabaseClient = createClient(url, key, {
+ auth: {
+ persistSession: false,
+ autoRefreshToken: false
+ }
+ })
+ return supabaseClient
+}
+
+const ensureLegacyHrefCleanup = async client => {
+ if (!client) return false
+ if (legacyCleanupPromise) return legacyCleanupPromise
+
+ legacyCleanupPromise = (async () => {
+ const ignoreMissingColumn = error => {
+ if (!error) return false
+ const code = String(error.code || '')
+ const message = String(error.message || '').toLowerCase()
+ return (
+ code === '42703' ||
+ code === 'PGRST204' ||
+ (message.includes('column') && message.includes('href'))
+ )
+ }
+
+ const { error: eventCleanupError } = await client
+ .from(EVENTS_TABLE)
+ .update({ href: '' })
+ .neq('href', '')
+ if (eventCleanupError && !ignoreMissingColumn(eventCleanupError)) {
+ console.warn(
+ `[Contrib] Supabase 清理旧 href 失败(${EVENTS_TABLE}): ${
+ eventCleanupError.message || eventCleanupError.code || eventCleanupError
+ }`
+ )
+ }
+
+ const { error: snapshotCleanupError } = await client
+ .from(SNAPSHOTS_TABLE)
+ .update({ href: '' })
+ .neq('href', '')
+ if (snapshotCleanupError && !ignoreMissingColumn(snapshotCleanupError)) {
+ console.warn(
+ `[Contrib] Supabase 清理旧 href 失败(${SNAPSHOTS_TABLE}): ${
+ snapshotCleanupError.message || snapshotCleanupError.code || snapshotCleanupError
+ }`
+ )
+ }
+
+ return true
+ })()
+
+ return legacyCleanupPromise
+}
+
+const getReadyClient = async () => {
+ const client = getSupabaseClient()
+ if (!client) return null
+ await ensureLegacyHrefCleanup(client)
+ return client
+}
+
+export const isContributionStoreEnabled = () => {
+ return Boolean(getSupabaseUrl() && getSupabaseKey())
+}
+
+export const buildContributionPostSnapshot = post => {
+ const repositoryId = normalizeRepositoryId(post?.id)
+ if (!repositoryId) return null
+
+ const createdAtMs = toTimestampMs(
+ post?.createdTime || post?.publishDate || post?.date?.start_date
+ )
+ const updatedAtMs = Math.max(toTimestampMs(post?.lastEditedDate), createdAtMs)
+
+ return {
+ repositoryId,
+ title: normalizeText(post?.title),
+ slug: normalizeText(post?.slug),
+ createdAtMs,
+ updatedAtMs
+ }
+}
+
+const normalizeEventType = value => (value === 'create' ? 'create' : 'update')
+
+const normalizeRawEvent = raw => {
+ if (!raw || typeof raw !== 'object') return null
+ const type = normalizeEventType(raw.type)
+ const repositoryId = normalizeRepositoryId(raw.repositoryId || raw.identifier || raw.postId)
+ const timestampMs = toTimestampMs(raw.timestampMs || raw.timestamp || raw.date || raw.time)
+ if (!repositoryId || !timestampMs) return null
+
+ const candidateEventId = normalizeText(raw.eventId)
+ const eventId = candidateEventId || buildEventId(type, repositoryId, timestampMs)
+
+ return {
+ event_id: eventId,
+ event_type: type,
+ repository_id: repositoryId,
+ timestamp_ms: timestampMs,
+ title: normalizeText(raw.title),
+ slug: normalizeText(raw.slug)
+ }
+}
+
+const loadSnapshotMap = async (client, repositoryIds) => {
+ const map = new Map()
+ const uniqueIds = Array.from(new Set((repositoryIds || []).filter(Boolean)))
+ if (!uniqueIds.length) return map
+
+ for (const chunk of chunkArray(uniqueIds, 200)) {
+ const { data, error } = await client
+ .from(SNAPSHOTS_TABLE)
+ .select('repository_id, title, slug, created_at_ms, updated_at_ms, synced_at_ms')
+ .in('repository_id', chunk)
+
+ if (error) throw error
+ ;(data || []).forEach(row => {
+ const repositoryId = normalizeRepositoryId(row.repository_id)
+ if (!repositoryId) return
+ map.set(repositoryId, {
+ repositoryId,
+ title: normalizeText(row.title),
+ slug: normalizeText(row.slug),
+ createdAtMs: toTimestampMs(row.created_at_ms),
+ updatedAtMs: toTimestampMs(row.updated_at_ms),
+ syncedAtMs: toTimestampMs(row.synced_at_ms)
+ })
+ })
+ }
+
+ return map
+}
+
+const loadExistingEventIds = async (client, eventIds) => {
+ const set = new Set()
+ const uniqueIds = Array.from(new Set((eventIds || []).filter(Boolean)))
+ if (!uniqueIds.length) return set
+
+ for (const chunk of chunkArray(uniqueIds, 300)) {
+ const { data, error } = await client
+ .from(EVENTS_TABLE)
+ .select('event_id')
+ .in('event_id', chunk)
+
+ if (error) throw error
+ ;(data || []).forEach(row => {
+ if (row?.event_id) set.add(normalizeText(row.event_id))
+ })
+ }
+ return set
+}
+
+const upsertSnapshots = async (client, snapshots) => {
+ if (!snapshots.length) return
+
+ for (const chunk of chunkArray(snapshots, 200)) {
+ const { error } = await client
+ .from(SNAPSHOTS_TABLE)
+ .upsert(chunk, { onConflict: 'repository_id' })
+ if (error) throw error
+ }
+}
+
+const upsertEvents = async (client, events) => {
+ if (!events.length) return
+
+ for (const chunk of chunkArray(events, 200)) {
+ const { error } = await client
+ .from(EVENTS_TABLE)
+ .upsert(chunk, { onConflict: 'event_id', ignoreDuplicates: true })
+ if (error) throw error
+ }
+}
+
+export const upsertContributionEvents = async rawEvents => {
+ const client = await getReadyClient()
+ if (!client) {
+ return { enabled: false, attempted: 0, inserted: 0 }
+ }
+
+ const events = Array.isArray(rawEvents)
+ ? rawEvents.map(normalizeRawEvent).filter(Boolean)
+ : []
+ if (!events.length) {
+ return { enabled: true, attempted: 0, inserted: 0 }
+ }
+
+ const existingIds = await loadExistingEventIds(
+ client,
+ events.map(event => event.event_id)
+ )
+ const pendingEvents = events.filter(event => !existingIds.has(event.event_id))
+ await upsertEvents(client, pendingEvents)
+
+ return {
+ enabled: true,
+ attempted: events.length,
+ inserted: pendingEvents.length
+ }
+}
+
+export const syncContributionSnapshots = async postSnapshots => {
+ const client = await getReadyClient()
+ if (!client) {
+ return { enabled: false, scanned: 0, addedEvents: 0, attemptedEvents: 0 }
+ }
+
+ const snapshots = Array.isArray(postSnapshots) ? postSnapshots : []
+ if (!snapshots.length) {
+ return { enabled: true, scanned: 0, addedEvents: 0, attemptedEvents: 0 }
+ }
+
+ const normalizedSnapshots = snapshots
+ .map(snapshot => {
+ const repositoryId = normalizeRepositoryId(snapshot?.repositoryId)
+ if (!repositoryId) return null
+
+ const createdAtMs = toTimestampMs(snapshot.createdAtMs)
+ const updatedAtMs = Math.max(toTimestampMs(snapshot.updatedAtMs), createdAtMs)
+
+ return {
+ repositoryId,
+ title: normalizeText(snapshot.title),
+ slug: normalizeText(snapshot.slug),
+ createdAtMs: createdAtMs || updatedAtMs,
+ updatedAtMs
+ }
+ })
+ .filter(Boolean)
+
+ if (!normalizedSnapshots.length) {
+ return { enabled: true, scanned: 0, addedEvents: 0, attemptedEvents: 0 }
+ }
+
+ const prevMap = await loadSnapshotMap(
+ client,
+ normalizedSnapshots.map(snapshot => snapshot.repositoryId)
+ )
+
+ const nowMs = Date.now()
+ const eventsToInsert = []
+ const snapshotsToUpsert = normalizedSnapshots.map(snapshot => ({
+ repository_id: snapshot.repositoryId,
+ title: snapshot.title,
+ slug: snapshot.slug,
+ created_at_ms: snapshot.createdAtMs,
+ updated_at_ms: snapshot.updatedAtMs,
+ synced_at_ms: nowMs
+ }))
+
+ normalizedSnapshots.forEach(snapshot => {
+ const prev = prevMap.get(snapshot.repositoryId)
+ if (!prev) {
+ const createTimestamp = snapshot.createdAtMs || snapshot.updatedAtMs
+ if (createTimestamp) {
+ eventsToInsert.push({
+ event_id: buildEventId('create', snapshot.repositoryId, createTimestamp),
+ event_type: 'create',
+ repository_id: snapshot.repositoryId,
+ timestamp_ms: createTimestamp,
+ title: snapshot.title,
+ slug: snapshot.slug
+ })
+ }
+
+ const hasHistoricalUpdate =
+ snapshot.updatedAtMs &&
+ createTimestamp &&
+ snapshot.updatedAtMs > createTimestamp
+ if (hasHistoricalUpdate) {
+ eventsToInsert.push({
+ event_id: buildEventId('update', snapshot.repositoryId, snapshot.updatedAtMs),
+ event_type: 'update',
+ repository_id: snapshot.repositoryId,
+ timestamp_ms: snapshot.updatedAtMs,
+ title: snapshot.title,
+ slug: snapshot.slug
+ })
+ }
+ return
+ }
+
+ const previousUpdatedAtMs = toTimestampMs(prev.updatedAtMs)
+ const shouldAppendUpdate = snapshot.updatedAtMs && snapshot.updatedAtMs > previousUpdatedAtMs
+ if (shouldAppendUpdate) {
+ const updateTimestamp = snapshot.updatedAtMs
+ eventsToInsert.push({
+ event_id: buildEventId('update', snapshot.repositoryId, updateTimestamp),
+ event_type: 'update',
+ repository_id: snapshot.repositoryId,
+ timestamp_ms: updateTimestamp,
+ title: snapshot.title,
+ slug: snapshot.slug
+ })
+ }
+ })
+
+ const existingIds = await loadExistingEventIds(
+ client,
+ eventsToInsert.map(event => event.event_id)
+ )
+ const pendingEvents = eventsToInsert.filter(event => !existingIds.has(event.event_id))
+
+ await upsertSnapshots(client, snapshotsToUpsert)
+ await upsertEvents(client, pendingEvents)
+
+ return {
+ enabled: true,
+ scanned: normalizedSnapshots.length,
+ attemptedEvents: eventsToInsert.length,
+ addedEvents: pendingEvents.length
+ }
+}
+
+export const listContributionEvents = async ({ limit = 50000 } = {}) => {
+ const client = await getReadyClient()
+ if (!client) return []
+
+ const safeLimit = Math.max(1, Math.min(100000, Number(limit) || 50000))
+ const { data, error } = await client
+ .from(EVENTS_TABLE)
+ .select('event_id, event_type, repository_id, timestamp_ms, title, slug')
+ .order('timestamp_ms', { ascending: false })
+ .limit(safeLimit)
+
+ if (error) throw error
+
+ return (data || [])
+ .slice()
+ .sort((a, b) => toTimestampMs(a?.timestamp_ms) - toTimestampMs(b?.timestamp_ms))
+ .map(row => {
+ const repositoryId = normalizeRepositoryId(row.repository_id)
+ const timestampMs = toTimestampMs(row.timestamp_ms)
+ const type = row.event_type === 'create' ? 'create' : 'update'
+ if (!repositoryId || !timestampMs) return null
+ return {
+ eventId: normalizeText(row.event_id),
+ type,
+ repositoryId,
+ identifier: repositoryId,
+ timestampMs,
+ title: normalizeText(row.title),
+ slug: normalizeText(row.slug),
+ href: buildHrefFromSlug(row.slug)
+ }
+ })
+ .filter(Boolean)
+}
diff --git a/pages/api/claude/contribution-refresh.js b/pages/api/claude/contribution-refresh.js
new file mode 100644
index 00000000000..4888864cf54
--- /dev/null
+++ b/pages/api/claude/contribution-refresh.js
@@ -0,0 +1,52 @@
+import { markContributionCacheDirty } from '@/lib/server/claude/contributionStore'
+
+const getTokenFromRequest = req => {
+ const queryToken = Array.isArray(req.query?.token)
+ ? req.query.token[0]
+ : req.query?.token
+ const headerToken =
+ req.headers['x-contribution-trigger-token'] ||
+ req.headers['x-contrib-trigger-token'] ||
+ ''
+ return String(queryToken || headerToken || '')
+}
+
+export default async function handler(req, res) {
+ if (!['GET', 'POST'].includes(req.method || '')) {
+ return res.status(405).json({ ok: false, message: 'Method Not Allowed' })
+ }
+
+ const expectedToken = process.env.CLAUDE_CONTRIBUTION_TRIGGER_TOKEN || ''
+ if (expectedToken) {
+ const receivedToken = getTokenFromRequest(req)
+ if (!receivedToken || receivedToken !== expectedToken) {
+ return res.status(401).json({ ok: false, message: 'Unauthorized' })
+ }
+ }
+
+ markContributionCacheDirty()
+
+ const shouldRevalidate = String(req.query?.revalidate || '1') !== '0'
+ const path =
+ (Array.isArray(req.query?.path) ? req.query.path[0] : req.query?.path) || '/'
+
+ let revalidated = false
+ let revalidateError = ''
+ if (shouldRevalidate && typeof res.revalidate === 'function') {
+ try {
+ await res.revalidate(path)
+ revalidated = true
+ } catch (error) {
+ revalidateError = String(error?.message || error)
+ }
+ }
+
+ return res.status(200).json({
+ ok: true,
+ revalidated,
+ path,
+ revalidateError,
+ message:
+ 'Contribution local cache marked dirty. Next index regeneration will refresh from database.'
+ })
+}
diff --git a/public/themes/claude/fonts/AnthropicSans-Text-Medium-Static.otf b/public/themes/claude/fonts/AnthropicSans-Text-Medium-Static.otf
new file mode 100644
index 0000000000000000000000000000000000000000..88b70ee9bea816e64ff68b8d7729e3685b81f365
GIT binary patch
literal 61528
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
Q7Br|irvD@1Ug#xx0nUbcN&o-=
literal 0
HcmV?d00001
diff --git a/public/themes/claude/fonts/AnthropicSans-Text-Regular-Static.otf b/public/themes/claude/fonts/AnthropicSans-Text-Regular-Static.otf
new file mode 100644
index 0000000000000000000000000000000000000000..70e36860fc137f41e8ed4364117d19777639c04f
GIT binary patch
literal 61144
zcmbST34Bzw*3Z2&_fC7K9ZH93p`AN3Ezp*&bZ^;8_l31k*0Qy<1uCU2rDatF1UD2>
zP{9R}#SIh$MMM-(MBK#z;cbaMW{<+f@#P_}Ly^r~&$xTj9PLh+8ljIzR
zjuo1#K+-aEa$K`}-R>sD_9Y=(qI0vdb8N%f9Vg^~kr1*pw_s@D6B{;uitoD#
zam~#w9GP)zc~O5ten7F`*Fy_?#;sCD=M!Q+g^c3T86`CV>-x9|u~ZUb%9~bRQZ~-L
z(TpRENy`j9fv`wGfkgcJ~QD^dbNhv8Qrxi!3IQk%Yi5`*b8m
z>C+M$uaO=9ad;N62yzmcE{kNC9`p1~LHZCdp`?PxJcwba_b_>p$70yE*AX6U4yTdF
z^AWCHL~KGBhV;Y6WHPcPNaKO$reItOoc|(7ok$TJ}-vm<|GO8sQkCAl<4;u@}n130LA^*m3AHMJWufljk
zHp%0`SWi0q+wcNtdEweG2=k0c3mRFAu)!OyZ5Fg^%Mea@^RFF6`YO^Xi0h4n!(RK~
zhOo>y1Ni<$xQfGS)Z8#&zV;|0+x&y^IKQ70JChU=Guo
zLodYt2!`Lt_!|S}X1dgmas|~BjG?YJScmnGcaE7H}YYf}Ta4CsQFuY5~8upOk
zhC)*PHyH|9ccVP{8!r>G;l??X2J}>qptCr*2Hz)0ahvsec>#}k9xvnh|7+c5`VCQJKf*|a`OV>YbG#Gr
z@|J!g{cqtw`v^l%gdXT?4>E%Hndk9X&+ADFP(B9V1qemPd&yjU*C1Vk_-|$C-(3bH
zdAcRYM@W`526_Y`OAy{h$ZrlE4g1J6(HFP|khx+pInD#dxt0eXH<_Doh|CRPB->{V
zcnNq~V2lg&IVdm&>f>1jd=&^ITlOG{2!Z%s(n15$Z{3*Y-w5wF$2|R?gbjdmAQUx+
z9*FPadB#12mpyDcK&F{}$+VWX+n0v(!h?uEMZ6iY8=<~AJc&FT((@1~()S_$TF5575&H8yuIHK#G(x9MI0Sw6
z_k