2026/3/5
Bug【已解决】
问题:代码过长导致在标注中无法完全显示问题
现状:已解决quartz\styles\custom.scss . . . + // Fix: Ensure code blocks can scroll horizontally when content is too long + figure[data-rehype-pretty-code-figure] { + overflow-x: auto; + max-width: 100%; + } + + pre { + overflow-x: auto; + max-width: 100%; + } + + pre > code { + overflow-x: auto; + } . . .
2026/2/20
添加文章密码加密功能
第一步:创建文章加密组件
01创建密码门控组件主文件
quartz\components\PasswordGate.tsx import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" // @ts-ignore import gateScript from "./scripts/passwordGate.inline" // @ts-ignore import gateStyle from "./styles/passwordGate.scss" const PasswordGate: QuartzComponent = ({ fileData }: QuartzComponentProps) => { const fmAny = fileData.frontmatter as any const protectedFlag = fmAny?.protected === true || fmAny?.protected === "true" if (!protectedFlag) { return null } const hash = typeof fmAny?.passwordHash === "string" && fmAny.passwordHash.trim() !== "" ? (fmAny.passwordHash as string) : undefined let pwd: string | undefined = undefined if (typeof fmAny?.password === "string") { const s = (fmAny.password as string).trim() if (s.length > 0) pwd = s } else if (typeof fmAny?.password === "number") { const s = String(fmAny.password).trim() if (s.length > 0) pwd = s } const hasCredentials = (pwd && pwd.length > 0) || (hash && hash.length > 0) if (!hasCredentials) { return null } return ( <div class="password-gate" data-require="true" data-hash={hash} data-password={pwd} aria-hidden="false" > <div class="gate-card"> <div class="gate-title">此文章已加密</div> <div class="gate-desc">请输入密码以查看正文内容</div> <div class="gate-form"> <input class="gate-input" type="password" placeholder="输入密码" autocomplete="new-password" /> <button class="gate-btn" type="button"> 解锁 </button> </div> <div class="gate-msg" role="alert"></div> </div> </div> ) } PasswordGate.afterDOMLoaded = gateScript PasswordGate.css = gateStyle export default (() => PasswordGate) satisfies QuartzComponentConstructor02创建密码门控组件样式文件
quartz\components\styles\passwordGate.scss @use "../../styles/variables.scss" as *; .password-gate { position: fixed; z-index: 1000; display: block; padding: 0; } .password-gate .gate-card { width: min(480px, 92vw); border: 1px solid var(--lightgray); border-radius: 10px; background: var(--light); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); padding: 1.25rem 1.25rem 1rem 1.25rem; } .password-gate .gate-title { font-family: var(--headerFont); font-size: 1.25rem; color: var(--dark); } .password-gate .gate-desc { margin-top: 0.35rem; color: var(--darkgray); } .password-gate .gate-form { margin-top: 0.9rem; display: flex; gap: 0.5rem; } .password-gate .gate-input { flex: 1; border: 1px solid var(--lightgray); border-radius: 6px; padding: 0.6rem 0.7rem; font-size: 1rem; color: var(--darkgray); background: var(--light); } .password-gate .gate-btn { border: 1px solid var(--secondary); background: var(--secondary); color: var(--light); border-radius: 6px; padding: 0.55rem 0.9rem; font-weight: 600; cursor: pointer; } .password-gate .gate-btn:hover { filter: brightness(0.95); } .password-gate .gate-msg { margin-top: 0.6rem; color: var(--secondary); min-height: 1.2rem; } .center .article-content.locked { filter: blur(10px); pointer-events: none; user-select: none; } @media all and ($mobile) { .mobile-no-scroll .password-gate { display: none; } .password-gate .gate-card { margin: 0 1rem; width: auto; max-width: calc(100vw - 2rem); } }03创建客户端脚本文件
quartz\components\scripts\passwordGate.inline.ts document.addEventListener("nav", () => { const gate = document.querySelector(".password-gate") as HTMLElement | null const articleContent = document.querySelector(".center .article-content") as HTMLElement | null if (!gate) return if (gate.dataset.require !== "true") return articleContent?.classList.add("locked") const input = gate.querySelector(".gate-input") as HTMLInputElement | null const btn = gate.querySelector(".gate-btn") as HTMLButtonElement | null const msg = gate.querySelector(".gate-msg") as HTMLElement | null if (!input || !btn) return const gateEl = gate as HTMLElement const inputEl = input as HTMLInputElement const btnEl = btn as HTMLButtonElement const msgEl = msg as HTMLElement | null // reset state on navigation inputEl.value = "" if (msgEl) msgEl.textContent = "" gateEl.style.removeProperty("display") const updateVisibilityWithGraph = () => { const overlay = document.querySelector( ".graph > .global-graph-outer.active, .graph > .local-graph-outer.active", ) as HTMLElement | null if (overlay) { gateEl.style.visibility = "hidden" gateEl.style.pointerEvents = "none" } else { gateEl.style.visibility = "" gateEl.style.pointerEvents = "" } } const positionGate = () => { if (!gate || !articleContent) return const rect = articleContent.getBoundingClientRect() const centerX = rect.left + rect.width / 2 const offsetTop = Math.min(window.innerHeight * 0.5) const centerY = offsetTop gate.style.position = "fixed" gate.style.left = `${centerX}px` gate.style.top = `${centerY}px` gate.style.transform = "translate(-50%, -50%)" gate.style.zIndex = "1000" const availableWidth = Math.min(rect.width, window.innerWidth) gate.style.maxWidth = `${availableWidth}px` updateVisibilityWithGraph() } positionGate() window.addEventListener("resize", positionGate) window.addCleanup(() => window.removeEventListener("resize", positionGate)) const graphRoot = document.querySelector(".graph") as HTMLElement | null if (graphRoot) { const observer = new MutationObserver(updateVisibilityWithGraph) observer.observe(graphRoot, { subtree: true, attributes: true, attributeFilter: ["class"], }) window.addCleanup(() => observer.disconnect()) } async function sha256Hex(s: string): Promise<string> { const buf = new TextEncoder().encode(s) const digest = await crypto.subtle.digest("SHA-256", buf) return Array.from(new Uint8Array(digest)) .map((b) => b.toString(16).padStart(2, "0")) .join("") } async function unlock() { const provided = (inputEl.value ?? "").trim() const plain = (gateEl.dataset.password ?? "").trim() const hash = (gateEl.dataset.hash ?? "").trim() let ok = false if (plain.length > 0) { ok = provided === plain } else if (hash.length > 0) { const hex = await sha256Hex(provided) ok = hex.toLowerCase() === hash.toLowerCase() } if (ok) { articleContent?.classList.remove("locked") gateEl.style.display = "none" if (msgEl) msgEl.textContent = "" window.removeEventListener("resize", positionGate) } else { if (msgEl) msgEl.textContent = "密码错误" } } const onKey = (e: KeyboardEvent) => { if (e.key === "Enter") unlock() } const onInput = () => { if (msgEl) msgEl.textContent = "" } btnEl.addEventListener("click", unlock) inputEl.addEventListener("keydown", onKey) inputEl.addEventListener("input", onInput) window.addCleanup(() => btnEl.removeEventListener("click", unlock)) window.addCleanup(() => inputEl.removeEventListener("keydown", onKey)) window.addCleanup(() => inputEl.removeEventListener("input", onInput)) })第二步:应用文章加密组件
01导出 PasswordGate 组件
quartz\components\Graph.tsx . . . + import PasswordGate from "./PasswordGate" . . . export { . . . + PasswordGate, . . . }02给文章添加了 protectedFlag 逻辑和 locked 类
quartz\components\pages\Content.tsx import { ComponentChildren } from "preact" import { htmlToJsx } from "../../util/jsx" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" const Content: QuartzComponent = ({ fileData, tree }: QuartzComponentProps) => { const content = htmlToJsx(fileData.filePath!, tree) as ComponentChildren const classes: string[] = fileData.frontmatter?.cssclasses ?? [] - const classString = ["popover-hint", ...classes].join(" ") - return <article class={classString}>{content}</article> + const fmAny = fileData.frontmatter as any + const protectedFlag = fmAny?.protected === true || fmAny?.protected === "true" + const classString = ["popover-hint", ...classes].filter((v) => v && v.length > 0).join(" ") + return ( + <article class={classString}> + <div class={`article-content ${protectedFlag ? "locked" : ""}`}>{content}</div> + </article> + ) } export default (() => Content) satisfies QuartzComponentConstructor03在布局中添加了 PasswordGate 组件
quartz.layout.ts . . . export const defaultContentPageLayout: PageLayout = { beforeBody: [ Component.ConditionalRender({ component: Component.Breadcrumbs(), condition: (page) => page.fileData.slug !== "index", }), + Component.PasswordGate(), Component.ArticleTitle(), Component.ContentMeta(), Component.TagList(), ], . . . }
2026/2/12
增加本地Graph知识关系图谱
第一步:增强视图组件按钮
quartz\components\Graph.tsx <div class={classNames(displayClass, "graph")}> <h3>{i18n(cfg.locale).components.graph.title}</h3> <div class="graph-outer"> <div class="graph-container" data-cfg={JSON.stringify(localGraph)}></div> - <button class="global-graph-icon" aria-label="Global Graph"> - <svg - ... - </svg> - </button> + <div class="graph-icon-group"> + <button class="local-graph-icon" aria-label="Local Graph"> + <svg + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + stroke-width="1" + stroke-linecap="round" + stroke-linejoin="round" + > + <circle cx="18" cy="5" r="3" /> + <circle cx="6" cy="12" r="3" /> + <circle cx="18" cy="19" r="3" /> + <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" /> + <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" /> + </svg> + </button> + <button class="global-graph-icon" aria-label="Global Graph"> + <svg + version="1.1" + xmlns="http://www.w3.org/2000/svg" + xmlnsXlink="http://www.w3.org/1999/xlink" + x="0px" + y="0px" + viewBox="0 0 55 55" + fill="currentColor" + xmlSpace="preserve" + > + <path + d="M49,0c-3.309,0-6,2.691-6,6c0,1.035,0.263,2.009,0.726,2.86l-9.829,9.829C32.542,17.634,30.846,17,29,17 + s-3.542,0.634-4.898,1.688l-7.669-7.669C16.785,10.424,17,9.74,17,9c0-2.206-1.794-4-4-4S9,6.794,9,9s1.794,4,4,4 + c0.74,0,1.424-0.215,2.019-0.567l7.669,7.669C21.634,21.458,21,23.154,21,25s0.634,3.542,1.688,4.897L10.024,42.562 + C8.958,41.595,7.549,41,6,41c-3.309,0-6,2.691-6,6s2.691,6,6,6s6-2.691,6-6c0-1.035-0.263-2.009-0.726-2.86l12.829-12.829 + c1.106,0.86,2.44,1.436,3.898,1.619v10.16c-2.833,0.478-5,2.942-5,5.91c0,3.309,2.691,6,6,6s6-2.691,6-6c0-2.967-2.167-5.431-5-5.91 + v-10.16c1.458-0.183,2.792-0.759,3.898-1.619l7.669,7.669C41.215,39.576,41,40.26,41,41c0,2.206,1.794,4,4,4s4-1.794,4-4 + s-1.794-4-4-4c-0.74,0-1.424,0.215-2.019,0.567l-7.669-7.669C36.366,28.542,37,26.846,37,25s-0.634-3.542-1.688-4.897l9.665-9.665 + C46.042,11.405,47.451,12,49,12c3.309,0,6-2.691,6-6S52.309,0,49,0z M11,9c0-1.103,0.897-2,2-2s2,0.897,2,2s-0.897,2-2,2 + S11,10.103,11,9z M6,51c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S8.206,51,6,51z M33,49c0,2.206-1.794,4-4,4s-4-1.794-4-4 + s1.794-4,4-4S33,46.794,33,49z M29,31c-3.309,0-6-2.691-6-6s2.691-6,6-6s6,2.691,6,6S32.309,31,29,31z M47,41c0,1.103-0.897,2-2,2 + s-2-0.897-2-2s0.897-2,2-2S47,39.897,47,41z M49,10c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S51.206,10,49,10z" + /> + </svg> + </button> + </div> </div> //<div class="graph-outer"> + <div class="local-graph-outer"> + <div class="local-graph-container" data-cfg= {JSON.stringify(localGraph)}></div> + </div> <div class="global-graph-outer"> <div class="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div> </div> </div>第二步:实现本地关系图谱弹出功能
quartz\components\scripts\graph.inline.ts ... const containers = [...document.getElementsByClassName("global-graph-outer")] as HTMLElement[] + const localContainers = [...document.getElementsByClassName("local-graph-outer")] as HTMLElement[] ... function hideGlobalGraph() { cleanupGlobalGraphs() for (const container of containers) { container.classList.remove("active") const sidebar = container.closest(".sidebar") as HTMLElement if (sidebar) { sidebar.style.zIndex = "" } } } ... + async function renderLocalGraphPopover() { + const slug = getFullSlug(window) + for (const container of localContainers) { + container.classList.add("active") + const sidebar = container.closest(".sidebar") as HTMLElement + if (sidebar) { + sidebar.style.zIndex = "1" + } + + const graphContainer = container.querySelector( + ".local-graph-container", + ) as HTMLElement + registerEscapeHandler(container, hideLocalGraphPopover) + if (graphContainer) { + globalGraphCleanups.push(await renderGraph(graphContainer, slug)) + } + } + } + + function hideLocalGraphPopover() { + cleanupGlobalGraphs() + for (const container of localContainers) { + container.classList.remove("active") + const sidebar = container.closest(".sidebar") as HTMLElement + if (sidebar) { + sidebar.style.zIndex = "" + } + } + } ... const containerIcons = document.getElementsByClassName("global-graph-icon") Array.from(containerIcons).forEach((icon) => { icon.addEventListener("click", renderGlobalGraph) window.addCleanup(() => icon.removeEventListener("click", renderGlobalGraph)) }) ... + const localContainerIcons = document.getElementsByClassName("local-graph-icon") + Array.from(localContainerIcons).forEach((icon) => { + icon.addEventListener("click", renderLocalGraphPopover) + window.addCleanup(() => icon.removeEventListener("click", renderLocalGraphPopover)) + }) ...第三步:优化样式与布局
quartz\components\styles\graph.scss ... - & > .global-graph-icon { - cursor: pointer; - background: none; - border: none; - color: var(--dark); - opacity: 0.5; - width: 24px; - height: 24px; - position: absolute; - padding: 0.2rem; - margin: 0.3rem; - top: 0; - right: 0; - border-radius: 4px; - background-color: transparent; - transition: background-color 0.5s ease; - cursor: pointer; - &:hover { - background-color: var(--lightgray); - } - } ... + & > .graph-icon-group { + position: absolute; + top: 0; + right: 0; + display: flex; + gap: 0.1rem; + padding: 0.2rem; + margin: 0.1rem; + + & > .global-graph-icon, + & > .local-graph-icon { + cursor: pointer; + background: none; + border: none; + color: var(--dark); + opacity: 0.5; + width: 24px; + height: 24px; + padding: 0.2rem; + border-radius: 4px; + background-color: transparent; + transition: background-color 0.5s ease; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background-color: var(--lightgray); + opacity: 1; + } + + & > svg { + width: 18px; + height: 18px; + } + } + } ... - & > .global-graph-outer { + & > .global-graph-outer, + & > .local-graph-outer { ... - & > .global-graph-outer { + & > .global-graph-outer, + & > .local-graph-outer { ...
2026/2/11
添加ViewImage组件
第一步:在transformers新建ViewImage组件
quartz\plugins\transformers\viewImage.ts /// Reference: https://github.com/jackyzha0/quartz/pull/2074 import { QuartzTransformerPlugin } from "../types" // ViewImage.js export const ViewImage: QuartzTransformerPlugin = () => { return { name: "ViewImage", externalResources() { return { js: [ { src: "https://cdn.jsdelivr.net/gh/Tokinx/ViewImage/view-image.min.js", loadTime: "afterDOMReady", contentType: "external", }, { script: ` document.addEventListener('DOMContentLoaded', function() { if (window.ViewImage) { ViewImage.init('article img, .content img'); const style = document.createElement('style'); style.textContent = 'article img, .content img { cursor: zoom-in; } document.head.appendChild(style); } }); `, loadTime: "afterDOMReady", contentType: "inline", }, ], } }, } } // 告诉TypeScript我们添加的内容 declare module "vfile" { interface DataMap { viewImage?: boolean } }第二步:在index.ts中导入viewImage组件
quartz\plugins\transformers\index.ts ... + export { ViewImage } from "./viewImage" ...第三步:在quartz.config.ts中导入viewImage组件
quartz.config.ts ... plugins: { transformers: [ ... + Plugin.ViewImage(), ... ] + } ...
排除对特定图片的View效果 【拓展功能】
document.addEventListener('DOMContentLoaded', function() { if (window.ViewImage) { - ViewImage.init('article img, .content img'); + const selector = 'article img:not([src*="banner.svg"]):not([src*="NKN(NKN).svg"]), .content img:not([src*="banner.svg"]):not([src*="NKN(NKN).svg"])'; + ViewImage.init(selector); const style = document.createElement('style'); - style.textContent = 'article img, .content img { cursor: zoom-in; }'; + style.textContent = selector + ' { cursor: zoom-in; }'; document.head.appendChild(style); } });
2026/2/9
Bug【以解决】
问题:Vecel和claudelfare部署构建时无法识别文件名特殊符号,比如+、_等
util/path.ts function sluggify(s: string): string { return s .split("/") .map((segment) => segment ... + .replace(/\+/g, "") + .replace(/\_/g, ""), ) ...
2026/2/5
发现适配问题 【已解决】
问题:部署到 Vercel 时,Vercel无法识别在带有+号的目录路径下的带有中文的笔记。
方法:但可以将笔记名字改为英文,在笔记formatter配置中文
总结:目录尽量不要带有+
2026/1/24
部署到Vercel
quartz.config.ts const config: QuartzConfig = { ... ! baseUrl: "blog-quartz.vercel.app", ... }部署到 Vercel 时,请修改为Vercel给的域名 如 tbq666.vercel.appz
部署到 Vercel 时,可以添加多个自定义域名,无需再修改baseUrl
部署到Cloudflare Pages时,会自动重定向,无需该配置vercel.json + { + "cleanUrls": true, + "trailingSlash": false + }若无vercel.json,则新建vercel.json
2026/1/20
Bug【已解决】
问题:文章目录过长导致超出底部
现状:已解决styles/toc.css .toc { ... ! flex: 0 1 auto; ... }
test
BibTex file
styles/toc.css .toc { ... ! flex: 0 1 auto; ... }