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..8e0a0a48b 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..d95097a7a --- /dev/null +++ b/docs/advanced/event system.md @@ -0,0 +1,175 @@ +--- +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. diff --git a/docs/plugins/Encrypt.md b/docs/plugins/Encrypt.md new file mode 100644 index 000000000..276a588c1 --- /dev/null +++ b/docs/plugins/Encrypt.md @@ -0,0 +1,106 @@ +--- +title: "Encrypt" +tags: + - plugin/transformer +encrypt: true +encryptConfig: + password: "quartz" + message: '^ Password is "quartz"' +--- + +This plugin enables content encryption for sensitive pages in your Quartz site. It uses AES encryption with password-based access control, allowing you to protect specific pages or entire folders with passwords. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +## Configuration + +```typescript +Plugin.Encrypt({ + algorithm: "aes-256-cbc", // Encryption algorithm + ttl: 3600 * 24 * 7, // Password cache TTL in seconds (7 days) + message: "This content is encrypted.", // Default message shown + encryptedFolders: { + // Simple password for a folder + "private/": "folder-password", + + // Advanced configuration for a folder + "secure/": { + password: "advanced-password", + algorithm: "aes-256-gcm", + ttl: 3600 * 24 * 30, // 30 days + message: "Authorized access only", + }, + }, +}) +``` + +> [!warning] +> Important security notes: +> +> - All non-markdown files remain unencrypted in the final build +> - Encrypted content is still visible in your source repository if it's public +> - Use this for access control, not for storing highly sensitive secrets + +### Configuration Options + +- `algorithm`: Encryption algorithm to use + - `"aes-256-cbc"` (default): AES-256 in CBC mode + - `"aes-256-gcm"`: AES-256 in GCM mode (authenticated encryption) + - Key length is automatically inferred from the algorithm (e.g., 256-bit = 32 bytes) +- `encryptedFolders`: Object mapping folder paths to passwords or configuration objects for folder-level encryption +- `ttl`: Time-to-live for cached passwords in seconds (default: 604800 = 7 days, set to 0 for session-only) +- `message`: Message to be displayed in the decryption page + +## How Configuration Works + +### Configuration Inheritance + +Settings cascade down through your folder structure: + +```typescript +encryptedFolders: { + "docs/": { + password: "docs-password", + algorithm: "aes-256-gcm" + }, + "docs/internal/": { + password: "internal-password" + // Inherits algorithm from parent folder + } +} +``` + +In this example: + +- `docs/page.md` uses `"docs-password"` with `"aes-256-gcm"` +- `docs/internal/report.md` uses `"internal-password"` but still uses `"aes-256-gcm"` (inherited) + +### Configuration Priority + +When multiple configurations apply, the priority is: + +1. **Page frontmatter** (highest priority) +2. **Deepest matching folder** +3. **Parent folders** (inherited settings) +4. **Global defaults** (lowest priority) + +## Security Features + +### Password Caching + +- Passwords are stored in browser localStorage +- Automatic expiration based on TTL settings +- Cached passwords are tried automatically when navigating + +### Protection Levels + +- **Content**: Entire page HTML is encrypted +- **Search/RSS**: Only generic descriptions are exposed +- **Navigation**: Encrypted pages appear in navigation but require passwords to view + +## API + +- Category: Transformer +- Function name: `Plugin.Encrypt()` +- Source: [`quartz/plugins/transformers/encrypt.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/encrypt.ts) diff --git a/docs/plugins/Frontmatter.md b/docs/plugins/Frontmatter.md index 9dfe53378..5fc293362 100644 --- a/docs/plugins/Frontmatter.md +++ b/docs/plugins/Frontmatter.md @@ -64,6 +64,13 @@ Quartz supports the following frontmatter: - `published` - `publishDate` - `date` +- encrypt + - `encrypt` + - `encrypted` +- encryptConfig + - Overrides for the [[plugins/Encrypt|encryptConfig]] +- password + - `password` ## API diff --git a/index.d.ts b/index.d.ts index 9011ee38f..62dcdb2b2 100644 --- a/index.d.ts +++ b/index.d.ts @@ -7,9 +7,15 @@ declare module "*.scss" { interface CustomEventMap { prenav: CustomEvent<{}> nav: CustomEvent<{ url: FullSlug }> + render: CustomEvent<{ htmlElement: HTMLElement }> + decrypt: CustomEvent<{ filePath: FullSlug; password: string }> themechange: CustomEvent<{ theme: "light" | "dark" }> readermodechange: CustomEvent<{ mode: "on" | "off" }> } -type ContentIndex = Record${config.message}
` : ""} +