diff --git a/docs/advanced/architecture.md b/docs/advanced/architecture.md
index 33da89d90..cbeda5230 100644
--- a/docs/advanced/architecture.md
+++ b/docs/advanced/architecture.md
@@ -45,8 +45,12 @@ This question is best answered by tracing what happens when a user (you!) runs `
1. The browser opens a Quartz page and loads the HTML. The `
` also links to page styles (emitted to `public/index.css`) and page-critical JS (emitted to `public/prescript.js`)
2. Then, once the body is loaded, the browser loads the non-critical JS (emitted to `public/postscript.js`)
-3. Once the page is done loading, the page will then dispatch a custom synthetic browser event `"nav"`. This is used so client-side scripts declared by components can 'setup' anything that requires access to the page DOM.
- 1. If the [[SPA Routing|enableSPA option]] is enabled in the [[configuration]], this `"nav"` event is also fired on any client-navigation to allow for components to unregister and reregister any event handlers and state.
- 2. If it's not, we wire up the `"nav"` event to just be fired a single time after page load to allow for consistency across how state is setup across both SPA and non-SPA contexts.
+3. Once the page is done loading, the page will dispatch two custom synthetic browser events:
+ 1. **`"nav"`** event: Fired when the user navigates to a new page. This is used for navigation-specific logic like updating URL-dependent state, analytics tracking, etc.
+ - Contains `e.detail.url` with the current page URL
+ - Fired on initial page load and on client-side navigation (if [[SPA Routing|enableSPA option]] is enabled)
+ 2. **`"render"`** event: Fired when content needs to be processed or re-rendered. This is used for DOM manipulation, setting up event listeners, and other content-specific logic.
+ - Contains `e.detail.htmlElement` with the DOM element that was updated
+ - Fired on initial page load (with `document.body`) and whenever content is dynamically updated (e.g., in popovers, search results, after decryption)
The architecture and design of the plugin system was intentionally left pretty vague here as this is described in much more depth in the guide on [[making plugins|making your own plugin]].
diff --git a/docs/advanced/creating components.md b/docs/advanced/creating components.md
index 84e038012..885acc48d 100644
--- a/docs/advanced/creating components.md
+++ b/docs/advanced/creating components.md
@@ -149,15 +149,46 @@ As the names suggest, the `.beforeDOMLoaded` scripts are executed _before_ the p
The `.afterDOMLoaded` script executes once the page has been completely loaded. This is a good place to setup anything that should last for the duration of a site visit (e.g. getting something saved from local storage).
-If you need to create an `afterDOMLoaded` script that depends on _page specific_ elements that may change when navigating to a new page, you can listen for the `"nav"` event that gets fired whenever a page loads (which may happen on navigation if [[SPA Routing]] is enabled).
+If you need to create an `afterDOMLoaded` script that depends on _page specific_ elements that may change when navigating to a new page, you have two options:
+
+**For navigation-specific logic**, listen for the `"nav"` event that gets fired whenever the user navigates to a new page:
```ts
-document.addEventListener("nav", () => {
- // do page specific logic here
- // e.g. attach event listeners
- const toggleSwitch = document.querySelector("#switch") as HTMLInputElement
- toggleSwitch.addEventListener("change", switchTheme)
- window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
+document.addEventListener("nav", (e) => {
+ // runs only on page navigation
+ // e.detail.url contains the new page URL
+ const currentUrl = e.detail.url
+ console.log(`Navigated to: ${currentUrl}`)
+})
+```
+
+**For rendering/re-rendering content**, use the `"render"` event which is fired when content needs to be processed or updated:
+
+```ts
+document.addEventListener("render", (e) => {
+ // runs when content is rendered or re-rendered
+ // e.detail.htmlElement contains the DOM element that was updated
+ const container = e.detail.htmlElement
+
+ // attach event listeners to elements within this container
+ const toggleSwitch = container.querySelector("#switch") as HTMLInputElement
+ if (toggleSwitch) {
+ toggleSwitch.addEventListener("change", switchTheme)
+ window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
+ }
+})
+```
+
+You can also use the utility function from `"./util"` to simplify render event handling:
+
+```ts
+import { addRenderListener } from "./util"
+
+addRenderListener((container) => {
+ // your rendering logic here
+ // container is the DOM element that was updated
+ const elements = container.querySelectorAll(".my-component")
+ elements.forEach(setupElement)
})
```
diff --git a/docs/advanced/event system.md b/docs/advanced/event system.md
new file mode 100644
index 000000000..d0f3543bc
--- /dev/null
+++ b/docs/advanced/event system.md
@@ -0,0 +1,171 @@
+---
+title: Event System
+---
+
+Quartz uses a custom event system to coordinate between navigation and content rendering. Understanding these events is crucial for creating interactive components that work correctly with [[SPA Routing]].
+
+## Event Types
+
+### Navigation Event (`nav`)
+
+The `nav` event is fired when the user navigates to a new page. This should be used for logic that needs to run once per page navigation.
+
+```ts
+document.addEventListener("nav", (e: CustomEventMap["nav"]) => {
+ // Access the current page URL
+ const currentUrl = e.detail.url
+ console.log(`User navigated to: ${currentUrl}`)
+
+ // Good for:
+ // - Analytics tracking
+ // - URL-dependent state updates
+ // - Setting up page-level event handlers
+ // - Theme/mode initialization
+})
+```
+
+**When it fires:**
+- On initial page load
+- On client-side navigation (if SPA routing is enabled)
+- Does NOT fire on content re-renders
+
+### Render Event (`render`)
+
+The `render` event is fired when content needs to be processed or updated. This should be used for DOM manipulation and content-specific logic.
+
+```ts
+document.addEventListener("render", (e: CustomEventMap["render"]) => {
+ // Access the container that was updated
+ const container = e.detail.htmlElement
+
+ // Process elements within this container
+ const codeBlocks = container.querySelectorAll("pre code")
+ codeBlocks.forEach(addSyntaxHighlighting)
+
+ // Good for:
+ // - Setting up event listeners on new content
+ // - Processing dynamic content (syntax highlighting, math rendering, etc.)
+ // - Initializing interactive components
+})
+```
+
+**When it fires:**
+- On initial page load (with `document.body` as the container)
+- When popover content is loaded
+- When search results are displayed
+- After content is decrypted
+- Whenever `dispatchRenderEvent()` is called
+
+## Utility Functions
+
+Quartz provides utility functions in `quartz/components/scripts/util.ts` to make working with these events easier:
+
+### `addRenderListener(fn)`
+
+A convenience function for listening to render events:
+
+```ts
+import { addRenderListener } from "./util"
+
+addRenderListener((container: HTMLElement) => {
+ // Your rendering logic here
+ // container is the DOM element that was updated
+ const myElements = container.querySelectorAll(".my-component")
+ myElements.forEach(setupMyComponent)
+})
+```
+
+This is equivalent to manually adding a render event listener but with cleaner syntax.
+
+### `dispatchRenderEvent(htmlElement)`
+
+Triggers a render event for a specific DOM element:
+
+```ts
+import { dispatchRenderEvent } from "./util"
+
+// After dynamically creating or updating content
+const myContainer = document.getElementById("dynamic-content")
+// ... update the container content ...
+dispatchRenderEvent(myContainer)
+```
+
+This will cause all render event listeners to process the specified container.
+
+## Best Practices
+
+### When to use `nav` vs `render`
+
+- **Use `nav` for:** Page-level setup, URL tracking, global state management
+- **Use `render` for:** Content processing, element-specific event handlers, DOM manipulation
+
+### Memory Management
+
+Always clean up event handlers to prevent memory leaks:
+
+```ts
+addRenderListener((container) => {
+ const buttons = container.querySelectorAll(".my-button")
+
+ const handleClick = (e) => { /* ... */ }
+
+ buttons.forEach(button => {
+ button.addEventListener("click", handleClick)
+ // Clean up when navigating away
+ window.addCleanup(() => {
+ button.removeEventListener("click", handleClick)
+ })
+ })
+})
+```
+
+The `window.addCleanup()` function ensures handlers are removed when navigating to a new page.
+
+### Scoped Processing
+
+Always scope your render logic to the provided container:
+
+```ts
+// ✅ Good - only processes elements within the updated container
+addRenderListener((container) => {
+ const elements = container.querySelectorAll(".my-element")
+ elements.forEach(process)
+})
+
+// ❌ Bad - processes all elements on the page
+addRenderListener((container) => {
+ const elements = document.querySelectorAll(".my-element")
+ elements.forEach(process)
+})
+```
+
+This ensures your logic only runs on newly updated content and avoids duplicate processing.
+
+## Migration from Old System
+
+If you have existing code that used the old `rerender` flag pattern:
+
+```ts
+// Old pattern ❌
+document.addEventListener("nav", (e) => {
+ if (e.detail.rerender) return // Skip rerender events
+ // ... setup logic
+})
+```
+
+You should split this into separate event handlers:
+
+```ts
+// New pattern ✅
+document.addEventListener("nav", (e) => {
+ // Navigation-only logic
+ updateURL(e.detail.url)
+})
+
+addRenderListener((container) => {
+ // Content rendering logic
+ setupComponents(container)
+})
+```
+
+This provides cleaner separation of concerns and better performance.
\ No newline at end of file
diff --git a/index.d.ts b/index.d.ts
index 70ec9c26f..03cff3763 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -6,7 +6,8 @@ declare module "*.scss" {
// dom custom event
interface CustomEventMap {
prenav: CustomEvent<{}>
- nav: CustomEvent<{ url: FullSlug; rerender?: boolean }>
+ nav: CustomEvent<{ url: FullSlug }>
+ render: CustomEvent<{ htmlElement: HTMLElement }>
themechange: CustomEvent<{ theme: "light" | "dark" }>
readermodechange: CustomEvent<{ mode: "on" | "off" }>
}
diff --git a/quartz/components/scripts/callout.inline.ts b/quartz/components/scripts/callout.inline.ts
index 242ce514e..46f0ff489 100644
--- a/quartz/components/scripts/callout.inline.ts
+++ b/quartz/components/scripts/callout.inline.ts
@@ -1,3 +1,5 @@
+import { addRenderListener } from "./util"
+
function toggleCallout(this: HTMLElement) {
const outerBlock = this.parentElement!
outerBlock.classList.toggle("is-collapsed")
@@ -7,8 +9,8 @@ function toggleCallout(this: HTMLElement) {
content.style.gridTemplateRows = collapsed ? "0fr" : "1fr"
}
-function setupCallout() {
- const collapsible = document.getElementsByClassName(
+function setupCallout(container: HTMLElement) {
+ const collapsible = container.getElementsByClassName(
`callout is-collapsible`,
) as HTMLCollectionOf
for (const div of collapsible) {
@@ -24,4 +26,4 @@ function setupCallout() {
}
}
-document.addEventListener("nav", setupCallout)
+addRenderListener(setupCallout)
diff --git a/quartz/components/scripts/checkbox.inline.ts b/quartz/components/scripts/checkbox.inline.ts
index 50ab0425a..189df2220 100644
--- a/quartz/components/scripts/checkbox.inline.ts
+++ b/quartz/components/scripts/checkbox.inline.ts
@@ -1,9 +1,10 @@
import { getFullSlug } from "../../util/path"
+import { addRenderListener } from "./util"
const checkboxId = (index: number) => `${getFullSlug(window)}-checkbox-${index}`
-document.addEventListener("nav", () => {
- const checkboxes = document.querySelectorAll(
+addRenderListener((container: HTMLElement) => {
+ const checkboxes = container.querySelectorAll(
"input.checkbox-toggle",
) as NodeListOf
checkboxes.forEach((el, index) => {
diff --git a/quartz/components/scripts/clipboard.inline.ts b/quartz/components/scripts/clipboard.inline.ts
index e16c11299..be7cf192a 100644
--- a/quartz/components/scripts/clipboard.inline.ts
+++ b/quartz/components/scripts/clipboard.inline.ts
@@ -1,10 +1,12 @@
+import { addRenderListener } from "./util"
+
const svgCopy =
''
const svgCheck =
''
-document.addEventListener("nav", () => {
- const els = document.getElementsByTagName("pre")
+addRenderListener((container: HTMLElement) => {
+ const els = container.getElementsByTagName("pre")
for (let i = 0; i < els.length; i++) {
const codeBlock = els[i].getElementsByTagName("code")[0]
if (codeBlock) {
diff --git a/quartz/components/scripts/comments.inline.ts b/quartz/components/scripts/comments.inline.ts
index 2b876bf6b..ef7bde01a 100644
--- a/quartz/components/scripts/comments.inline.ts
+++ b/quartz/components/scripts/comments.inline.ts
@@ -1,3 +1,5 @@
+import { addRenderListener } from "./util"
+
const changeTheme = (e: CustomEventMap["themechange"]) => {
const theme = e.detail.theme
const iframe = document.querySelector("iframe.giscus-frame") as HTMLIFrameElement
@@ -59,8 +61,8 @@ type GiscusElement = Omit & {
}
}
-document.addEventListener("nav", () => {
- const giscusContainer = document.querySelector(".giscus") as GiscusElement
+addRenderListener((container: HTMLElement) => {
+ const giscusContainer = container.querySelector(".giscus") as GiscusElement
if (!giscusContainer) {
return
}
diff --git a/quartz/components/scripts/encrypt.inline.ts b/quartz/components/scripts/encrypt.inline.ts
index 8f4fe0061..495ed0ba3 100644
--- a/quartz/components/scripts/encrypt.inline.ts
+++ b/quartz/components/scripts/encrypt.inline.ts
@@ -8,6 +8,7 @@ import {
EncryptionResult,
} from "../../util/encryption"
import { FullSlug, getFullSlug } from "../../util/path"
+import { addRenderListener, dispatchRenderEvent } from "./util"
const showLoading = (container: Element, show: boolean) => {
const loadingDiv = container.querySelector(".decrypt-loading") as HTMLElement
@@ -78,6 +79,9 @@ const decryptWithPassword = async (
contentWrapper.className = "decrypted-content-wrapper"
contentWrapper.innerHTML = decryptedContent
container.parentNode!.replaceChild(contentWrapper, container)
+ // set data-decrypted of the original container to true
+ containerElement.dataset.decrypted = "true"
+
return true
}
@@ -121,13 +125,6 @@ const decryptWithPassword = async (
}
}
-const notifyNav = (url: FullSlug) => {
- const event: CustomEventMap["nav"] = new CustomEvent("nav", {
- detail: { url, rerender: true },
- })
- document.dispatchEvent(event)
-}
-
function updateTitle(container: HTMLElement | null) {
console.log(container)
if (container) {
@@ -138,32 +135,22 @@ function updateTitle(container: HTMLElement | null) {
}
}
-const tryAutoDecrypt = async (container: HTMLElement): Promise => {
- const filePath = getFullSlug(window)
+const tryAutoDecrypt = async (parent: HTMLElement, container: HTMLElement): Promise => {
const fullSlug = container.dataset.slug as FullSlug
const config = JSON.parse(container.dataset.config!) as EncryptionConfig
const hash = JSON.parse(container.dataset.hash!) as Hash
- const parent =
- (container.closest(".popover") as HTMLElement) ||
- (container.closest(".preview-inner") as HTMLElement) ||
- (container.closest(".center") as HTMLElement) ||
- null
const password = await searchForValidPassword(fullSlug, hash, config)
if (password && (await decryptWithPassword(container, password, false))) {
- notifyNav(filePath)
+ dispatchRenderEvent(parent)
updateTitle(parent)
return true
}
return false
}
-const manualDecrypt = async (container: HTMLElement) => {
- const parent =
- (container.closest(".preview-inner") as HTMLElement) ||
- (container.closest(".center") as HTMLElement) ||
- null
+const manualDecrypt = async (parent: HTMLElement, container: HTMLElement) => {
const passwordInput = container.querySelector(".decrypt-password") as HTMLInputElement
const password = passwordInput.value
@@ -173,29 +160,28 @@ const manualDecrypt = async (container: HTMLElement) => {
}
if (await decryptWithPassword(container, password, true)) {
- const filePath = getFullSlug(window)
- notifyNav(filePath)
+ dispatchRenderEvent(parent)
updateTitle(parent)
}
}
-document.addEventListener("nav", async () => {
- // Try auto-decryption for all encrypted content
- const encryptedElements = document.querySelectorAll(
- ".encrypted-content",
+addRenderListener(async (element) => {
+ // Try auto-decryption for all encrypted content with data-decrypted="false"
+ const encryptedElements = element.querySelectorAll(
+ ".encrypted-content[data-decrypted='false']",
) as NodeListOf
for (const encryptedContainer of encryptedElements) {
- await tryAutoDecrypt(encryptedContainer)
+ await tryAutoDecrypt(element, encryptedContainer)
}
// Manual decryption handlers
- const buttons = document.querySelectorAll(".decrypt-button")
+ const buttons = element.querySelectorAll(".decrypt-button")
buttons.forEach((button) => {
const handleClick = async function (this: HTMLElement) {
const encryptedContainer = this.closest(".encrypted-content")!
- await manualDecrypt(encryptedContainer as HTMLElement)
+ await manualDecrypt(element, encryptedContainer as HTMLElement)
}
button.addEventListener("click", handleClick)
@@ -206,12 +192,12 @@ document.addEventListener("nav", async () => {
})
// Enter key handler
- document.querySelectorAll(".decrypt-password").forEach((input) => {
+ element.querySelectorAll(".decrypt-password").forEach((input) => {
const handleKeypress = async function (this: HTMLInputElement, e: Event) {
const keyEvent = e as KeyboardEvent
if (keyEvent.key === "Enter") {
const encryptedContainer = this.closest(".encrypted-content")!
- await manualDecrypt(encryptedContainer as HTMLElement)
+ await manualDecrypt(element, encryptedContainer as HTMLElement)
}
}
diff --git a/quartz/components/scripts/mermaid.inline.ts b/quartz/components/scripts/mermaid.inline.ts
index 533e38555..eeb834196 100644
--- a/quartz/components/scripts/mermaid.inline.ts
+++ b/quartz/components/scripts/mermaid.inline.ts
@@ -1,4 +1,4 @@
-import { registerEscapeHandler, removeAllChildren } from "./util"
+import { registerEscapeHandler, removeAllChildren, addRenderListener } from "./util"
interface Position {
x: number
@@ -144,8 +144,8 @@ const cssVars = [
let mermaidImport = undefined
-document.addEventListener("nav", async () => {
- const nodes = document.querySelectorAll(
+addRenderListener(async (container: HTMLElement) => {
+ const nodes = container.querySelectorAll(
"code.mermaid:not([data-processed])",
) as NodeListOf
if (nodes.length === 0) return
diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts
index d48fecf51..e5576758c 100644
--- a/quartz/components/scripts/popover.inline.ts
+++ b/quartz/components/scripts/popover.inline.ts
@@ -1,17 +1,10 @@
import { computePosition, flip, inline, shift } from "@floating-ui/dom"
import { FullSlug, normalizeRelativeURLs } from "../../util/path"
-import { fetchCanonical } from "./util"
+import { fetchCanonical, dispatchRenderEvent, addRenderListener } from "./util"
const p = new DOMParser()
let activeAnchor: HTMLAnchorElement | null = null
-function notifyNav(url: FullSlug) {
- const event: CustomEventMap["nav"] = new CustomEvent("nav", {
- detail: { url, rerender: true },
- })
- document.dispatchEvent(event)
-}
-
async function mouseEnterHandler(
this: HTMLAnchorElement,
{ clientX, clientY }: { clientX: number; clientY: number },
@@ -45,7 +38,7 @@ async function mouseEnterHandler(
}
}
- notifyNav(link.getAttribute("href") as FullSlug)
+ dispatchRenderEvent(popoverInner)
}
const targetUrl = new URL(link.href)
@@ -129,8 +122,9 @@ function clearActivePopover() {
allPopoverElements.forEach((popoverElement) => popoverElement.classList.remove("active-popover"))
}
-document.addEventListener("nav", () => {
- const links = [...document.querySelectorAll("a.internal")] as HTMLAnchorElement[]
+addRenderListener((element) => {
+ const links = [...element.querySelectorAll("a.internal")] as HTMLAnchorElement[]
+
for (const link of links) {
link.addEventListener("mouseenter", mouseEnterHandler)
link.addEventListener("mouseleave", clearActivePopover)
diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts
index 3183f47b9..38f36de32 100644
--- a/quartz/components/scripts/search.inline.ts
+++ b/quartz/components/scripts/search.inline.ts
@@ -1,5 +1,5 @@
import FlexSearch from "flexsearch"
-import { registerEscapeHandler, removeAllChildren } from "./util"
+import { registerEscapeHandler, removeAllChildren, dispatchRenderEvent } from "./util"
import { FullSlug, normalizeRelativeURLs, resolveRelative } from "../../util/path"
import { contentDecryptedEventListener, decryptContent } from "../../util/encryption"
@@ -143,13 +143,6 @@ function highlightHTML(searchTerm: string, el: HTMLElement) {
return html.body
}
-function notifyNav(url: FullSlug) {
- const event: CustomEventMap["nav"] = new CustomEvent("nav", {
- detail: { url, rerender: true },
- })
- document.dispatchEvent(event)
-}
-
async function setupSearch(searchElement: Element, currentSlug: FullSlug, data: ContentIndex) {
const container = searchElement.querySelector(".search-container") as HTMLElement
if (!container) return
@@ -274,10 +267,8 @@ async function setupSearch(searchElement: Element, currentSlug: FullSlug, data:
const slug = idDataMap[id]
let title = data[slug].title
- if (data[slug].decrypted === false) {
- title = "🔒 " + title
- } else if (data[slug].decrypted === true) {
- title = "🔓 " + title
+ if (data[slug].encryptionResult) {
+ title = (data[slug].decrypted ? "🔓 " : "🔒 ") + data[slug].title
}
return {
@@ -406,7 +397,7 @@ async function setupSearch(searchElement: Element, currentSlug: FullSlug, data:
highlights[0]?.scrollIntoView({ block: "start" })
await new Promise((resolve) => setTimeout(resolve, 100))
- notifyNav(slug)
+ dispatchRenderEvent(previewInner)
}
async function onType(e: HTMLElementEventMap["input"]) {
@@ -544,9 +535,6 @@ async function fillDocument(data: ContentIndex) {
}
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
- // Ignore rerender events
- if (e.detail.rerender) return
-
const currentSlug = e.detail.url
const data = await fetchData
const searchElement = document.getElementsByClassName("search")
diff --git a/quartz/components/scripts/spa.inline.ts b/quartz/components/scripts/spa.inline.ts
index 22fcd72b4..230aa874c 100644
--- a/quartz/components/scripts/spa.inline.ts
+++ b/quartz/components/scripts/spa.inline.ts
@@ -1,6 +1,6 @@
import micromorph from "micromorph"
import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path"
-import { fetchCanonical } from "./util"
+import { fetchCanonical, dispatchRenderEvent } from "./util"
// adapted from `micromorph`
// https://github.com/natemoo-re/micromorph
@@ -38,6 +38,9 @@ const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined
function notifyNav(url: FullSlug) {
const event: CustomEventMap["nav"] = new CustomEvent("nav", { detail: { url } })
document.dispatchEvent(event)
+
+ // Also trigger render event for the whole document body
+ dispatchRenderEvent(document.body)
}
const cleanupFns: Set<(...args: any[]) => void> = new Set()
diff --git a/quartz/components/scripts/toc.inline.ts b/quartz/components/scripts/toc.inline.ts
index 4148fa235..1741d17e8 100644
--- a/quartz/components/scripts/toc.inline.ts
+++ b/quartz/components/scripts/toc.inline.ts
@@ -1,3 +1,5 @@
+import { addRenderListener } from "./util"
+
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
const slug = entry.target.id
@@ -34,11 +36,11 @@ function setupToc() {
}
}
-document.addEventListener("nav", () => {
+addRenderListener((container: HTMLElement) => {
setupToc()
// update toc entry highlighting
observer.disconnect()
- const headers = document.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]")
+ const headers = container.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]")
headers.forEach((header) => observer.observe(header))
})
diff --git a/quartz/components/scripts/util.ts b/quartz/components/scripts/util.ts
index f71790104..8068e7d94 100644
--- a/quartz/components/scripts/util.ts
+++ b/quartz/components/scripts/util.ts
@@ -44,3 +44,16 @@ export async function fetchCanonical(url: URL): Promise {
const [_, redirect] = text.match(canonicalRegex) ?? []
return redirect ? fetch(`${new URL(redirect, url)}`) : res
}
+
+export function addRenderListener(renderFn: (container: HTMLElement) => void) {
+ document.addEventListener("render", (e: CustomEventMap["render"]) => {
+ renderFn(e.detail.htmlElement)
+ })
+}
+
+export function dispatchRenderEvent(htmlElement: HTMLElement) {
+ const event: CustomEventMap["render"] = new CustomEvent("render", {
+ detail: { htmlElement },
+ })
+ document.dispatchEvent(event)
+}
diff --git a/quartz/components/styles/encrypt.scss b/quartz/components/styles/encrypt.scss
index d7327351f..0fb77d756 100644
--- a/quartz/components/styles/encrypt.scss
+++ b/quartz/components/styles/encrypt.scss
@@ -6,10 +6,11 @@
justify-content: center;
.encryption-notice {
- background: color-mix(in srgb, var(--lightgray) 60%, var(--light));
+ /* background: color-mix(in srgb, var(--lightgray) 60%, var(--light));
+ */
border: 1px solid var(--lightgray);
+ padding: 2rem 1.5rem 2rem;
border-radius: 5px;
- padding: 2rem 1.5rem;
max-width: 450px;
width: 100%;
text-align: center;
@@ -29,10 +30,12 @@
color: var(--darkgray);
line-height: 1.5;
font-size: 0.95rem;
- margin: 0 0 1rem 0;
+ margin: 0;
+ font-style: italic;
}
.decrypt-form {
+ margin: 1rem 0 0 0;
display: flex;
flex-direction: column;
gap: 1rem;
@@ -88,16 +91,6 @@
font-size: 16px; // Prevent zoom on iOS
}
}
-
- .encrypted-message-footnote {
- color: var(--gray);
- font-size: 0.85rem;
- opacity: 0.8;
- text-align: center;
- width: 100%;
- font-style: italic;
- margin: 0;
- }
}
.decrypt-loading {
@@ -144,12 +137,18 @@
}
// Hide the decrypt form when encrypted content appears in popover and search
-.popover .encrypted-content .encryption-notice .decrypt-form {
- display: none;
-}
+.search-space, .popover {
+ .encrypted-content .encryption-notice .decrypt-form {
+ display: none;
+ }
-.search-space .encrypted-content .encryption-notice .decrypt-form {
- display: none;
+ .encryption-notice {
+ /*
+ border: 0;
+ padding: 0;
+ */
+ margin: 1rem 0;
+ }
}
.decrypted-content-wrapper {
diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts
index 61e2ba858..a42a36c87 100644
--- a/quartz/plugins/emitters/componentResources.ts
+++ b/quartz/plugins/emitters/componentResources.ts
@@ -211,6 +211,8 @@ function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentReso
window.addCleanup = () => {}
const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } })
document.dispatchEvent(event)
+ const renderEvent = new CustomEvent("render", { detail: { htmlElement: document.body } })
+ document.dispatchEvent(renderEvent)
`)
}
}
diff --git a/quartz/plugins/transformers/encrypt.ts b/quartz/plugins/transformers/encrypt.ts
index 647b1d748..a52395d9c 100644
--- a/quartz/plugins/transformers/encrypt.ts
+++ b/quartz/plugins/transformers/encrypt.ts
@@ -117,6 +117,7 @@ export const Encrypt: QuartzTransformerPlugin> = (userOpts) =>
`data-encrypted='${JSON.stringify(encryptionResult)}'`,
`data-hash='${JSON.stringify(file.data.hash)}'`,
`data-slug='${file.data.slug}'`,
+ `data-decrypted='false'`,
].join(" ")
// Create a new tree with encrypted content placeholder