feat(layout): page frames

This commit is contained in:
saberzero1 2026-02-28 21:59:31 +01:00
parent ad617ac4d6
commit 6f48471cde
No known key found for this signature in database
7 changed files with 298 additions and 129 deletions

View File

@ -173,6 +173,7 @@ quartz.config.yaml
│ b. Clone/update from GitHub via isomorphic-git (gitLoader.ts)
│ c. Read manifest from package.json "quartz" field
│ d. Load components into ComponentRegistry (componentLoader.ts)
│ e. Load frames into FrameRegistry (frameLoader.ts)
3. Validate dependencies
@ -242,19 +243,20 @@ Each plugin declares metadata in its `package.json` under the `"quartz"` field:
}
```
| Field | Type | Description |
| ---------------- | ----------------------------------- | --------------------------------------------------------------------------- |
| `name` | `string` | Plugin identifier (kebab-case) |
| `displayName` | `string` | Human-readable name |
| `category` | `string \| string[]` | One or more of: `transformer`, `filter`, `emitter`, `pageType`, `component` |
| `version` | `string` | Plugin version |
| `quartzVersion` | `string` | Compatible Quartz version range |
| `dependencies` | `string[]` | Required plugin sources (e.g., `"github:quartz-community/crawl-links"`) |
| `defaultOrder` | `number` | Default execution order (0-100, lower = runs first) |
| `defaultEnabled` | `boolean` | Whether enabled by default on install |
| `defaultOptions` | `object` | Default options merged with user config |
| `configSchema` | `object` | JSON Schema for options validation |
| `components` | `Record<string, ComponentManifest>` | UI components provided by this plugin |
| Field | Type | Description |
| ---------------- | ---------------------------------------- | --------------------------------------------------------------------------- |
| `name` | `string` | Plugin identifier (kebab-case) |
| `displayName` | `string` | Human-readable name |
| `category` | `string \| string[]` | One or more of: `transformer`, `filter`, `emitter`, `pageType`, `component` |
| `version` | `string` | Plugin version |
| `quartzVersion` | `string` | Compatible Quartz version range |
| `dependencies` | `string[]` | Required plugin sources (e.g., `"github:quartz-community/crawl-links"`) |
| `defaultOrder` | `number` | Default execution order (0-100, lower = runs first) |
| `defaultEnabled` | `boolean` | Whether enabled by default on install |
| `defaultOptions` | `object` | Default options merged with user config |
| `configSchema` | `object` | JSON Schema for options validation |
| `components` | `Record<string, ComponentManifest>` | UI components provided by this plugin |
| `frames` | `Record<string, { exportName: string }>` | Page frames provided by this plugin (see [Page Frames](#page-frames)) |
### Plugin Template Structure
@ -645,7 +647,7 @@ The layout builder looks up components by:
1. Deep-clones the hast tree (to preserve the cached version)
2. Resolves transclusions (see below)
3. Destructures the layout into Head, header[], beforeBody[], Content, afterBody[], left[], right[], Footer, and frame name
4. Resolves the frame via `resolveFrame(frameName)` — falls back to `DefaultFrame` if unset
4. Resolves the frame via `resolveFrame(frameName)`checks plugin-registered frames (FrameRegistry) first, then built-in frames, then falls back to `DefaultFrame`
5. Delegates the inner page structure to `frame.render({...all slots...})`
6. Wraps the frame output in the stable outer shell: `<html>``<Head/>``<body data-slug>``<div id="quartz-root" data-frame={frame.name}>``<Body>`
7. Serializes to HTML string via `preact-render-to-string`
@ -703,17 +705,26 @@ interface PageFrameProps {
| Frame | Name | Description | Used By |
| ------------------ | -------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| **DefaultFrame** | `"default"` | Original three-column layout: left sidebar + center (header, beforeBody, content, afterBody) + right sidebar + footer | All page types by default |
| **FullWidthFrame** | `"full-width"` | No sidebars. Single center column spanning full width with header, content, afterBody, and footer | `canvas-page` |
| **FullWidthFrame** | `"full-width"` | No sidebars. Single center column spanning full width with header, content, afterBody, and footer | |
| **MinimalFrame** | `"minimal"` | No sidebars, no header/beforeBody chrome. Only content + footer | `404` page |
#### Plugin-Provided Frames
Plugins can register their own frames via the Frame Registry. For example, the `canvas-page` plugin provides a `"canvas"` frame:
| Frame | Name | Source Plugin | Description |
| --------------- | ---------- | ------------- | ------------------------------------------------------------ |
| **CanvasFrame** | `"canvas"` | `canvas-page` | Fullscreen canvas with togglable left sidebar for navigation |
#### Frame Resolution Priority
Frames are resolved in the `PageTypeDispatcher.resolveLayout()` function with this priority chain:
```
1. YAML config override: layout.byPageType.<name>.template
2. Page type declaration: pageType.frame (in plugin source)
3. Default: "default"
1. YAML config override: layout.byPageType.<name>.template
2. Plugin-registered frame: FrameRegistry (loaded from plugin ./frames exports)
3. Built-in frame: builtinFrames map in quartz/components/frames/index.ts
4. Default: "default"
```
This means site authors can override any page type's frame via configuration without touching plugin code:
@ -727,10 +738,33 @@ layout:
#### Creating Custom Frames
New frames are added in `quartz/components/frames/`:
There are two ways to add custom frames:
**1. Plugin-provided frames** (recommended for reusable, distributable frames):
Plugins can ship frames by exporting them from a `./frames` subpath and declaring them in the `package.json` manifest under `"quartz"."frames"`. The config loader calls `loadFramesFromPackage()` during plugin initialization, which imports the frame and registers it in the `FrameRegistry`.
```json title="package.json (excerpt)"
{
"exports": {
"./frames": {
"import": "./dist/frames/index.js"
}
},
"quartz": {
"frames": {
"CanvasFrame": { "exportName": "CanvasFrame" }
}
}
}
```
**2. Core frames** (for project-specific frames):
New frames can be added in `quartz/components/frames/`:
1. Create a new `.tsx` file implementing the `PageFrame` interface
2. Register it in `quartz/components/frames/index.ts``builtinFrames` registry
2. Register it in `quartz/components/frames/index.ts``builtinFrames` map
3. Reference it by name in page type plugins or YAML config
```typescript
@ -781,13 +815,15 @@ Frame changes between pages are SPA-safe because:
#### Source Files
| File | Purpose |
| --------------------------------------------- | ------------------------------------------- |
| `quartz/components/frames/types.ts` | `PageFrame` and `PageFrameProps` interfaces |
| `quartz/components/frames/DefaultFrame.tsx` | Default three-column layout |
| `quartz/components/frames/FullWidthFrame.tsx` | Full-width single-column layout |
| `quartz/components/frames/MinimalFrame.tsx` | Minimal content-only layout |
| `quartz/components/frames/index.ts` | Frame registry and `resolveFrame()` |
| File | Purpose |
| --------------------------------------------- | ------------------------------------------------------ |
| `quartz/components/frames/types.ts` | `PageFrame` and `PageFrameProps` interfaces |
| `quartz/components/frames/DefaultFrame.tsx` | Default three-column layout |
| `quartz/components/frames/FullWidthFrame.tsx` | Full-width single-column layout |
| `quartz/components/frames/MinimalFrame.tsx` | Minimal content-only layout |
| `quartz/components/frames/registry.ts` | `FrameRegistry` singleton for plugin-registered frames |
| `quartz/components/frames/index.ts` | `resolveFrame()` and built-in frame map |
| `quartz/plugins/loader/frameLoader.ts` | Loads frames from plugin `./frames` exports |
### Transclusion
@ -989,13 +1025,13 @@ Produce output files (HTML, JSON, XML, images, etc.).
Define how different types of pages are matched, generated, and rendered. All PageType plugins are dual-category (`pageType` + `component`).
| Plugin | Match Strategy | Generates Virtual Pages? | Description | Repository |
| ---------------- | ---------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| **content-page** | `match.all()` (lowest priority fallback) | No | Default page type for regular markdown content | [quartz-community/content-page](https://github.com/quartz-community/content-page) |
| **folder-page** | Folder index pages | Yes (one per folder) | Renders folder listing pages with file trees | [quartz-community/folder-page](https://github.com/quartz-community/folder-page) |
| **tag-page** | Tag index pages | Yes (one per tag + tag index) | Renders tag listing pages | [quartz-community/tag-page](https://github.com/quartz-community/tag-page) |
| **canvas-page** | `match.ext(".canvas")` | No (reads `.canvas` files) | Interactive JSON Canvas visualization (pan/zoom) | [quartz-community/canvas-page](https://github.com/quartz-community/canvas-page) |
| **bases-page** | `match.ext(".base")` | Yes (view-based aggregate pages) | Extensible view-based pages from `.base` files (table, list, board, gallery, calendar) | [quartz-community/bases-page](https://github.com/quartz-community/bases-page) |
| Plugin | Match Strategy | Generates Virtual Pages? | Description | Repository |
| ---------------- | ---------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| **content-page** | `match.all()` (lowest priority fallback) | No | Default page type for regular markdown content | [quartz-community/content-page](https://github.com/quartz-community/content-page) |
| **folder-page** | Folder index pages | Yes (one per folder) | Renders folder listing pages with file trees | [quartz-community/folder-page](https://github.com/quartz-community/folder-page) |
| **tag-page** | Tag index pages | Yes (one per tag + tag index) | Renders tag listing pages | [quartz-community/tag-page](https://github.com/quartz-community/tag-page) |
| **canvas-page** | `match.ext(".canvas")` | No (reads `.canvas` files) | Interactive JSON Canvas visualization (pan/zoom). Provides `"canvas"` page frame with fullscreen layout and togglable sidebar | [quartz-community/canvas-page](https://github.com/quartz-community/canvas-page) |
| **bases-page** | `match.ext(".base")` | Yes (view-based aggregate pages) | Extensible view-based pages from `.base` files (table, list, board, gallery, calendar) | [quartz-community/bases-page](https://github.com/quartz-community/bases-page) |
### Component Plugins

View File

@ -100,14 +100,26 @@ The frame system lives in `quartz/components/frames/` and consists of:
- `DefaultFrame.tsx` — Three-column layout (left sidebar, center, right sidebar, footer)
- `FullWidthFrame.tsx` — No sidebars, single center column
- `MinimalFrame.tsx` — No sidebars, no header/beforeBody, just content and footer
- `index.ts` — Registry and `resolveFrame()` function
- `registry.ts``FrameRegistry` singleton for plugin-registered frames
- `index.ts``resolveFrame()` function and built-in frame registry
### Frame Registry
The `FrameRegistry` (`quartz/components/frames/registry.ts`) is a singleton that stores frames registered by community plugins. It mirrors the design of the `ComponentRegistry`. Plugins declare frames in their `package.json` manifest under the `"quartz"."frames"` field, and these are loaded by `quartz/plugins/loader/frameLoader.ts` during plugin initialization.
### Frame Resolution
The rendering pipeline in `quartz/components/renderPage.tsx` delegates to the resolved frame's `render()` function. Frame resolution happens in the `PageTypeDispatcher` emitter (`quartz/plugins/pageTypes/dispatcher.ts`) using this priority:
1. YAML config: `layout.byPageType.<name>.template`
2. Page type plugin: `frame` property
3. Fallback: `"default"`
2. Plugin-registered frame: looked up by name in the `FrameRegistry`
3. Built-in frame: looked up by name in the `builtinFrames` map
4. Fallback: `"default"`
The active frame name is set as a `data-frame` attribute on the `.page` element, enabling frame-specific CSS overrides in `quartz/styles/base.scss`.
### Plugin-Provided Frames
Community plugins can ship their own frames by exporting them from a `./frames` subpath and declaring them in the plugin manifest. For example, the `canvas-page` plugin provides a `"canvas"` frame with a fullscreen layout and togglable sidebar. See [[making plugins#Providing Custom Frames]] for implementation details.
See [[layout#Page Frames]] for user-facing documentation and [[making plugins#Page Types]] for how to set frames in page type plugins.

View File

@ -370,9 +370,102 @@ export interface QuartzPageTypePluginInstance {
- `match`: A function that determines whether a given slug/file should be rendered by this page type.
- `generate`: An optional function that produces virtual pages (pages not backed by files on disk, such as folder listings or tag indices).
- `layout`: The layout configuration key (e.g. `"content"`, `"folder"`, `"tag"`). This determines which `byPageType` entry in `quartz.config.yaml` provides the layout overrides for this page type.
- `frame`: The [[layout#Page Frames|page frame]] to use for this page type. Controls the overall HTML structure (e.g. `"default"`, `"full-width"`, `"minimal"`). If not set, defaults to `"default"`. Can be overridden per-page-type via `layout.byPageType.<name>.template` in `quartz.config.yaml`.
- `frame`: The [[layout#Page Frames|page frame]] to use for this page type. Controls the overall HTML structure (e.g. `"default"`, `"full-width"`, `"minimal"`, or a custom frame provided by your plugin). If not set, defaults to `"default"`. Can be overridden per-page-type via `layout.byPageType.<name>.template` in `quartz.config.yaml`.
- `body`: The Quartz component constructor that renders the page body content.
### Providing Custom Frames
Plugins can ship their own [[layout#Page Frames|page frames]] — custom page layouts that control how the HTML structure (sidebars, header, content area, footer) is arranged. This is useful for page types that need fundamentally different layouts (e.g. a fullscreen canvas, a presentation mode, a dashboard).
To provide a custom frame:
**1. Create the frame file:**
```tsx title="src/frames/MyFrame.tsx"
import type { PageFrame, PageFrameProps } from "@quartz-community/types"
import type { ComponentChildren } from "preact"
export const MyFrame: PageFrame = {
name: "my-frame",
css: `
.page[data-frame="my-frame"] > #quartz-body {
grid-template-columns: 1fr;
grid-template-areas: "center";
}
`,
render({ componentData, pageBody: Content, footer: Footer }: PageFrameProps): unknown {
const renderSlot = (C: (props: typeof componentData) => unknown): ComponentChildren =>
C(componentData) as ComponentChildren
return (
<div class="center">
{(Content as any)(componentData)}
{(Footer as any)(componentData)}
</div>
)
},
}
```
Key requirements:
- `name`: A unique string identifier. This is what page types and YAML config reference.
- `render()`: Receives all layout slots (header, sidebars, content, footer) and returns JSX for the inner page structure.
- `css` (optional): Frame-specific CSS. Scope it with `.page[data-frame="my-frame"]` selectors to avoid conflicts.
**2. Re-export the frame:**
```ts title="src/frames/index.ts"
export { MyFrame } from "./MyFrame"
```
**3. Declare the frame in `package.json`:**
```json title="package.json"
{
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./frames": {
"import": "./dist/frames/index.js",
"types": "./dist/frames/index.d.ts"
}
},
"quartz": {
"frames": {
"MyFrame": { "exportName": "MyFrame" }
}
}
}
```
The `"frames"` field in the `"quartz"` manifest maps export names to frame metadata. The key (e.g. `"MyFrame"`) must match the export name in `src/frames/index.ts`.
**4. Add the frame entry point to your build config:**
```ts title="tsup.config.ts"
export default defineConfig({
entry: ["src/index.ts", "src/frames/index.ts"],
// ...
})
```
**5. Reference the frame in your page type:**
```ts
export const MyPageType: QuartzPageTypePlugin = () => ({
name: "MyPageType",
frame: "my-frame", // References the frame by its name property
// ...
})
```
When a user installs your plugin, Quartz automatically loads the frame from the `./frames` export and registers it in the Frame Registry. The frame is then available by name in any page type or YAML config override.
> [!tip]
> See the [`canvas-page`](https://github.com/quartz-community/canvas-page) plugin for a complete real-world example of a plugin-provided frame.
## Building and Testing
```shell

View File

@ -145,16 +145,19 @@ Quartz ships with three built-in frames:
| Frame | Description | Used by |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| `default` | Three-column layout with left sidebar, center content (header, beforeBody, content, afterBody), right sidebar, and footer. This is the standard Quartz layout. | ContentPage, FolderPage, TagPage, BasesPage |
| `full-width` | No sidebars. Single center column spanning the full width with header, content, afterBody, and footer. | CanvasPage |
| `full-width` | No sidebars. Single center column spanning the full width with header, content, afterBody, and footer. | |
| `minimal` | No sidebars, no header or beforeBody chrome. Only content and footer. | NotFoundPage (404) |
Plugins can also provide their own frames. For example, the `canvas-page` plugin ships a `"canvas"` frame that provides a fullscreen canvas with a togglable sidebar.
#### How frames are resolved
Each page type can declare a default frame in its plugin source code via the `frame` property. The resolution order is:
1. **YAML config override**: `layout.byPageType.<name>.template` in `quartz.config.yaml`
2. **Plugin declaration**: The `frame` property set in the page type plugin's source code
3. **Fallback**: `"default"`
2. **Plugin-registered frame**: Frames registered by plugins via the Frame Registry (loaded from the plugin's `frames` export)
3. **Plugin declaration**: The `frame` property set in the page type plugin's source code
4. **Fallback**: `"default"`
For example, to override canvas pages to use the minimal frame:
@ -167,7 +170,15 @@ layout:
#### Custom frames
You can create custom frames by adding a new `.tsx` file in `quartz/components/frames/` that implements the `PageFrame` interface, then registering it in `quartz/components/frames/index.ts`. See the [[advanced/architecture|architecture overview]] for the full `PageFrame` interface.
There are two ways to provide custom frames:
**1. Plugin-provided frames (recommended for reusable frames):**
Plugins can ship their own frames by declaring them in `package.json` and exporting them from a `./frames` subpath. See [[making plugins#Providing Custom Frames|the plugin guide]] for details. When a plugin with frames is installed, its frames are automatically registered in the Frame Registry and available by name.
**2. Core frames (for project-specific frames):**
You can also create frames directly in `quartz/components/frames/` by implementing the `PageFrame` interface and registering the frame in `quartz/components/frames/index.ts`. See the [[advanced/architecture|architecture overview]] for the full `PageFrame` interface.
Frames are applied as a `data-frame` attribute on the `.page` element, which you can target in CSS:
@ -177,6 +188,8 @@ Frames are applied as a `data-frame` attribute on the `.page` element, which you
}
```
Frame CSS should be scoped with `[data-frame="name"]` selectors to avoid conflicts with other frames.
### Layout breakpoints
Quartz has different layouts depending on the width the screen viewing the website.

View File

@ -4,7 +4,7 @@ tags:
- plugin/pageType
---
This plugin is a page type plugin that renders [JSON Canvas](https://jsoncanvas.org) (`.canvas`) files as interactive, pannable and zoomable canvas pages. It uses the `full-width` [[layout#Page Frames|page frame]] (no sidebars, single column spanning the full width) to give the canvas maximum screen space. It supports the full [JSON Canvas 1.0 spec](https://jsoncanvas.org/spec/1.0/), including text nodes with Markdown rendering, file nodes that link to other pages in your vault, link nodes for external URLs, and group nodes for visual organization. Edges between nodes are rendered as SVG paths with optional labels, arrow markers, and colors.
This plugin is a page type plugin that renders [JSON Canvas](https://jsoncanvas.org) (`.canvas`) files as interactive, pannable and zoomable canvas pages. It uses a custom `"canvas"` [[layout#Page Frames|page frame]] that provides a fullscreen, always-on canvas experience with a togglable left sidebar for navigation. It supports the full [JSON Canvas 1.0 spec](https://jsoncanvas.org/spec/1.0/), including text nodes with Markdown rendering, file nodes that link to other pages in your vault, link nodes for external URLs, and group nodes for visual organization. Edges between nodes are rendered as SVG paths with optional labels, arrow markers, and colors.
> [!note]
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
@ -15,7 +15,25 @@ This plugin accepts the following configuration options:
- `initialZoom`: The initial zoom level when the canvas is first displayed. Default: `1{:ts}`.
- `minZoom`: The minimum zoom level allowed when zooming out. Default: `0.1{:ts}`.
- `maxZoom`: The maximum zoom level allowed when zooming in. Default: `5{:ts}`.
- `defaultFullscreen`: Whether canvas pages default to fullscreen mode. When enabled, the canvas fills the entire viewport on load. Users can toggle fullscreen with the button in the top-right corner, or press `Escape` to exit. Default: `false{:ts}`.
### Canvas Frame
The canvas-page plugin provides its own `"canvas"` page frame via the [[layout#Page Frames|Frame Registry]]. This frame:
- Renders the canvas in **fullscreen mode** by default (100vw × 100vh), giving the canvas maximum screen space — leaning into the "endless canvas" concept of JSON Canvas.
- Provides a **togglable left sidebar** that slides in from the left edge. This is the only layout slot available — it renders the same components as the `left` sidebar on content pages (e.g., Explorer, Search, Page Title).
- The sidebar toggle button (hamburger/close icon) is positioned in the top-left corner.
- Canvas controls (zoom in, zoom out, reset) are positioned on the right side.
- On mobile, the sidebar overlays the canvas rather than pushing it aside.
Users can override this frame via `quartz.config.yaml` if needed:
```yaml title="quartz.config.yaml"
layout:
byPageType:
canvas:
template: default # Use standard three-column layout instead
```
### Features
@ -24,7 +42,7 @@ This plugin accepts the following configuration options:
- **Link nodes**: Reference external URLs.
- **Group nodes**: Visual grouping containers with optional labels and background colors.
- **Edges**: SVG connections between nodes with optional labels, arrow markers, and colors. Supports all four sides (top, right, bottom, left) and both preset colors (16) and custom hex colors.
- **Fullscreen mode**: Toggle button to expand the canvas to fill the viewport. Configurable default via `defaultFullscreen`.
- **Togglable sidebar**: Hamburger button in the top-left corner toggles the left sidebar for navigation. Press `Escape` or click the close button to dismiss.
- **Preset colors**: Six preset colors (red, orange, yellow, green, cyan, purple) plus custom hex colors (`#RRGGBB`) for nodes and edges.
## API

View File

@ -4,242 +4,242 @@
"alias-redirects": {
"source": "github:quartz-community/alias-redirects",
"resolved": "https://github.com/quartz-community/alias-redirects.git",
"commit": "efb7a5af5bd4ff7366719ff008aff5225b8145f5",
"installedAt": "2026-02-25T15:09:37.092Z"
"commit": "3449ce4162c4a2f05e7d2de83e8fba6a958970a0",
"installedAt": "2026-02-28T20:09:12.996Z"
},
"article-title": {
"source": "github:quartz-community/article-title",
"resolved": "https://github.com/quartz-community/article-title.git",
"commit": "4d81b1ec63c89e02ed2db1ae73e351eda42dfe2d",
"installedAt": "2026-02-26T15:50:17.569Z"
"commit": "757ee632ad75f09ac6844630f20cd03fa8af8f7d",
"installedAt": "2026-02-28T20:09:13.506Z"
},
"backlinks": {
"source": "github:quartz-community/backlinks",
"resolved": "https://github.com/quartz-community/backlinks.git",
"commit": "cf7463f1d0ab50a6ef2995789543c40584a650d7",
"installedAt": "2026-02-26T15:50:18.205Z"
"commit": "a1723edee1be6aca3af8554f981276c10544a737",
"installedAt": "2026-02-28T20:09:13.938Z"
},
"bases-page": {
"source": "github:quartz-community/bases-page",
"resolved": "https://github.com/quartz-community/bases-page.git",
"commit": "ef1752bef852bb6c3992d2fc0ddb2ebbeafe12e6",
"installedAt": "2026-02-26T19:20:19.854Z"
"commit": "7f6439ed711f4aa9d38c5017e88a7a9757cd2d5c",
"installedAt": "2026-02-28T20:09:14.396Z"
},
"breadcrumbs": {
"source": "github:quartz-community/breadcrumbs",
"resolved": "https://github.com/quartz-community/breadcrumbs.git",
"commit": "71355a0f3e815c117e74095597246e9921beeecb",
"installedAt": "2026-02-26T15:50:19.290Z"
"commit": "899f9c23455fd6d85b5bff85b795a8f143d8881b",
"installedAt": "2026-02-28T20:09:14.961Z"
},
"canvas-page": {
"source": "github:quartz-community/canvas-page",
"resolved": "https://github.com/quartz-community/canvas-page.git",
"commit": "698cb647ec19dfb37279e28ea63ce09bded7b963",
"installedAt": "2026-02-28T03:27:04.482Z"
"commit": "b8faadd0873517bd8243c892f6d1fa7e1a076f65",
"installedAt": "2026-02-28T20:09:15.427Z"
},
"citations": {
"source": "github:quartz-community/citations",
"resolved": "https://github.com/quartz-community/citations.git",
"commit": "38aa0d7defafb242a0cb2b8014edd0bf111396c2",
"installedAt": "2026-02-25T15:09:40.468Z"
"commit": "ce6b699345461e0524d4f4928ddb214dbbc8b8a0",
"installedAt": "2026-02-28T20:09:15.928Z"
},
"cname": {
"source": "github:quartz-community/cname",
"resolved": "https://github.com/quartz-community/cname.git",
"commit": "488e63bf870e979af29d0d8a001d0bca3a6e5941",
"installedAt": "2026-02-25T15:09:41.000Z"
"commit": "49c3e779956526266f1e5648cb311e23df269860",
"installedAt": "2026-02-28T20:09:16.507Z"
},
"comments": {
"source": "github:quartz-community/comments",
"resolved": "https://github.com/quartz-community/comments.git",
"commit": "55bca8df1a325e127325f1d621259bb6292936bf",
"installedAt": "2026-02-25T15:09:41.544Z"
"commit": "f814bfc356dbab302625eccf709070d5bdc77f30",
"installedAt": "2026-02-28T20:09:16.931Z"
},
"content-index": {
"source": "github:quartz-community/content-index",
"resolved": "https://github.com/quartz-community/content-index.git",
"commit": "d2b7900f48f31eab7d6972aa00d5b0cd4dd55ca8",
"installedAt": "2026-02-25T15:09:42.114Z"
"commit": "71eb8a2b745deefaf0bef35aaaa3117849f1a976",
"installedAt": "2026-02-28T20:09:17.391Z"
},
"content-meta": {
"source": "github:quartz-community/content-meta",
"resolved": "https://github.com/quartz-community/content-meta.git",
"commit": "772b27e40cd0fbbd17f66a4a001304568842d806",
"installedAt": "2026-02-26T15:50:22.822Z"
"commit": "2016dce2b0d568afb80ccb07a8f6f65d1fec6e54",
"installedAt": "2026-02-28T20:09:17.833Z"
},
"content-page": {
"source": "github:quartz-community/content-page",
"resolved": "https://github.com/quartz-community/content-page.git",
"commit": "ee899ff7966647116dbd2441bfa44bdc057b7a6a",
"installedAt": "2026-02-26T15:50:23.353Z"
"commit": "2246f6be7b389f000ce63694db50033cd487994b",
"installedAt": "2026-02-28T20:09:18.268Z"
},
"crawl-links": {
"source": "github:quartz-community/crawl-links",
"resolved": "https://github.com/quartz-community/crawl-links.git",
"commit": "c3118c97329b746ea4bf04de0c2361e5ff913be4",
"installedAt": "2026-02-25T15:09:43.817Z"
"commit": "06611992b7742eb67aa7779ab23c17b12a77d36b",
"installedAt": "2026-02-28T20:09:18.696Z"
},
"created-modified-date": {
"source": "github:quartz-community/created-modified-date",
"resolved": "https://github.com/quartz-community/created-modified-date.git",
"commit": "071d4a4f37649937c9c08f2ff8f3c90fbae8a757",
"installedAt": "2026-02-25T15:09:44.387Z"
"commit": "56e7fcdab3af67b730b71b9ab140780b4722bdc0",
"installedAt": "2026-02-28T20:09:19.265Z"
},
"darkmode": {
"source": "github:quartz-community/darkmode",
"resolved": "https://github.com/quartz-community/darkmode.git",
"commit": "923b97c2e69c20943e8bb5517c2653bda0dd5fed",
"installedAt": "2026-02-26T15:50:24.994Z"
"commit": "ddaeb76992bbb8a806c4cd1207d8558b45dd1eae",
"installedAt": "2026-02-28T20:09:19.690Z"
},
"description": {
"source": "github:quartz-community/description",
"resolved": "https://github.com/quartz-community/description.git",
"commit": "0d12d796077af220dc8309bed320b2e6b9d47777",
"installedAt": "2026-02-25T15:09:45.460Z"
"commit": "ed3d22019d2f37ae1d141a62b0a3b42c1f79de09",
"installedAt": "2026-02-28T20:09:20.136Z"
},
"explicit-publish": {
"source": "github:quartz-community/explicit-publish",
"resolved": "https://github.com/quartz-community/explicit-publish.git",
"commit": "c31eb2e73adffe195c743949ab02e9a037b05894",
"installedAt": "2026-02-25T15:09:46.038Z"
"commit": "3788330e95ec2413a390174df3b9488f95165521",
"installedAt": "2026-02-28T20:09:20.589Z"
},
"explorer": {
"source": "github:quartz-community/explorer",
"resolved": "https://github.com/quartz-community/explorer.git",
"commit": "05c6ff66f3f85117f02634f3af4cca7f5221a5d4",
"installedAt": "2026-02-26T21:15:30.250Z"
"commit": "b70d50a96c00e4726fb08614dc4fab116b1c5ca9",
"installedAt": "2026-02-28T20:09:21.150Z"
},
"favicon": {
"source": "github:quartz-community/favicon",
"resolved": "https://github.com/quartz-community/favicon.git",
"commit": "ad7457abf5f839f619c4f9dddd905e6683042cf8",
"installedAt": "2026-02-25T15:09:47.117Z"
"commit": "6d112b5d1f38cfaa5340bcfb9e14975a7e26acb3",
"installedAt": "2026-02-28T20:09:21.599Z"
},
"folder-page": {
"source": "github:quartz-community/folder-page",
"resolved": "https://github.com/quartz-community/folder-page.git",
"commit": "292e5c002d21e5a615361cfc1de57583eb46f500",
"installedAt": "2026-02-26T15:50:27.600Z"
"commit": "0f58791ffa5f34c08a83678b8c0f3587ec669f7e",
"installedAt": "2026-02-28T20:09:22.041Z"
},
"footer": {
"source": "github:quartz-community/footer",
"resolved": "https://github.com/quartz-community/footer.git",
"commit": "6adab4b98e2881e5dd7dcdf317fbf87dd74322b4",
"installedAt": "2026-02-26T15:50:28.130Z"
"commit": "cd779de29b4cbfc824d51c60b1b3175db4a586e7",
"installedAt": "2026-02-28T20:09:22.520Z"
},
"github-flavored-markdown": {
"source": "github:quartz-community/github-flavored-markdown",
"resolved": "https://github.com/quartz-community/github-flavored-markdown.git",
"commit": "764a27197eee724b3d4d9fcb38073b7dc6fcc530",
"installedAt": "2026-02-25T15:09:48.815Z"
"commit": "c77501cc5b4b6a6a796e552eff38288acd57d1a7",
"installedAt": "2026-02-28T20:09:22.973Z"
},
"graph": {
"source": "github:quartz-community/graph",
"resolved": "https://github.com/quartz-community/graph.git",
"commit": "04445c38b21192d99e3fcd4612a436424a0b81e4",
"installedAt": "2026-02-26T15:50:29.402Z"
"commit": "423d068daf8151fab3046d577b84b474efdfb6e8",
"installedAt": "2026-02-28T20:09:23.768Z"
},
"hard-line-breaks": {
"source": "github:quartz-community/hard-line-breaks",
"resolved": "https://github.com/quartz-community/hard-line-breaks.git",
"commit": "73c6ec8e87682812ee417c38e886818fc8f11af7",
"installedAt": "2026-02-25T15:09:49.900Z"
"commit": "5753c3ed81b5471a59abf3b2821f8be9b072413f",
"installedAt": "2026-02-28T20:09:24.355Z"
},
"latex": {
"source": "github:quartz-community/latex",
"resolved": "https://github.com/quartz-community/latex.git",
"commit": "8defa17c69e214b96d9a343fed103b54af681dc2",
"installedAt": "2026-02-25T15:09:50.455Z"
"commit": "323851bcfce5278769ec5d0ae5efe33334b81628",
"installedAt": "2026-02-28T20:09:25.008Z"
},
"note-properties": {
"source": "github:quartz-community/note-properties",
"resolved": "https://github.com/quartz-community/note-properties.git",
"commit": "50c64ed790fdb7d2415b5797dc1864ceaf65840e",
"installedAt": "2026-02-26T15:50:30.939Z"
"commit": "289554d5966097bd439119cd436c64a36aa0b3af",
"installedAt": "2026-02-28T20:09:25.571Z"
},
"obsidian-flavored-markdown": {
"source": "github:quartz-community/obsidian-flavored-markdown",
"resolved": "https://github.com/quartz-community/obsidian-flavored-markdown.git",
"commit": "2084f48063aad122dc684cd227973e14fa7fae7d",
"installedAt": "2026-02-26T15:50:31.548Z"
"commit": "0785abc0b674644f6628b4cb3926d578b40422dd",
"installedAt": "2026-02-28T20:09:26.123Z"
},
"og-image": {
"source": "github:quartz-community/og-image",
"resolved": "https://github.com/quartz-community/og-image.git",
"commit": "4da2eaedcf7227a70c13910d8af3cae32b5672a8",
"installedAt": "2026-02-25T16:09:27.022Z"
"commit": "4ede61e768971ebd8ca27de678f70a390d9e067e",
"installedAt": "2026-02-28T20:09:26.567Z"
},
"ox-hugo": {
"source": "github:quartz-community/ox-hugo",
"resolved": "https://github.com/quartz-community/ox-hugo.git",
"commit": "d7cdae515a08fe42b0c50b04fb9d6057f2a97b82",
"installedAt": "2026-02-25T15:09:52.906Z"
"commit": "241f1a1c7e4aae04227e68dc9b59f4257bdc0450",
"installedAt": "2026-02-28T20:09:27.008Z"
},
"page-title": {
"source": "github:quartz-community/page-title",
"resolved": "https://github.com/quartz-community/page-title.git",
"commit": "51e90261d8dcceb7b17cc06ec29e9c2b2c306ace",
"installedAt": "2026-02-25T15:09:53.462Z"
"commit": "360866cd98f7f0d005639ee1f5cffa4a866c8468",
"installedAt": "2026-02-28T20:09:27.456Z"
},
"reader-mode": {
"source": "github:quartz-community/reader-mode",
"resolved": "https://github.com/quartz-community/reader-mode.git",
"commit": "b84c66171525dc950a10faeb59612e2ea4cfed95",
"installedAt": "2026-02-26T15:50:33.644Z"
"commit": "7e3a034a8e0d6204083f9dc593cec3f067fed158",
"installedAt": "2026-02-28T20:09:27.875Z"
},
"recent-notes": {
"source": "github:quartz-community/recent-notes",
"resolved": "https://github.com/quartz-community/recent-notes.git",
"commit": "b63cfedd807d9e946f08463d68c8a1d58af8092b",
"installedAt": "2026-02-26T15:50:34.283Z"
"commit": "744d5011eeaaca202f937f142f5cd4b5cd05c968",
"installedAt": "2026-02-28T20:09:28.318Z"
},
"remove-draft": {
"source": "github:quartz-community/remove-draft",
"resolved": "https://github.com/quartz-community/remove-draft.git",
"commit": "32d59f57261c2c74778d2484d330f38d0eced465",
"installedAt": "2026-02-25T15:09:55.102Z"
"commit": "8a1964d3794027a253685664747fe1f313d81706",
"installedAt": "2026-02-28T20:09:28.755Z"
},
"roam": {
"source": "github:quartz-community/roam",
"resolved": "https://github.com/quartz-community/roam.git",
"commit": "4ca4269c120fcfcb15cba42a5f07caca8ee5b9de",
"installedAt": "2026-02-25T15:09:55.673Z"
"commit": "40f469df706a34cd7a2ee48df56d431c23f24fc8",
"installedAt": "2026-02-28T20:09:29.199Z"
},
"search": {
"source": "github:quartz-community/search",
"resolved": "https://github.com/quartz-community/search.git",
"commit": "1db52f8a139cc640456408b5b5f8cb4ffa5b74fc",
"installedAt": "2026-02-26T15:50:35.753Z"
"commit": "52403e5753d4a00e2b428904cd15734db4b1fdb2",
"installedAt": "2026-02-28T20:09:29.780Z"
},
"spacer": {
"source": "github:quartz-community/spacer",
"resolved": "https://github.com/quartz-community/spacer.git",
"commit": "e7b3e00171f0693fcfaa1efff97fd80bea1b839e",
"installedAt": "2026-02-26T15:50:36.615Z"
"commit": "fafb24d42e866324ad596aaa86e814cace23e037",
"installedAt": "2026-02-28T20:09:30.352Z"
},
"syntax-highlighting": {
"source": "github:quartz-community/syntax-highlighting",
"resolved": "https://github.com/quartz-community/syntax-highlighting.git",
"commit": "5063da4f2f37e9ea2957b179ed87a6db361263d2",
"installedAt": "2026-02-26T15:50:37.066Z"
"commit": "29d474f82fc041eb970cfe0ce4e9564a489db9c0",
"installedAt": "2026-02-28T20:09:30.941Z"
},
"table-of-contents": {
"source": "github:quartz-community/table-of-contents",
"resolved": "https://github.com/quartz-community/table-of-contents.git",
"commit": "42d8ab92798e3dea02403dc773ba7c7e1b3bbe26",
"installedAt": "2026-02-26T15:50:37.494Z"
"commit": "46b30413c37b294fcf9ff8ae8b893fa3c76f07d3",
"installedAt": "2026-02-28T20:09:31.484Z"
},
"tag-list": {
"source": "github:quartz-community/tag-list",
"resolved": "https://github.com/quartz-community/tag-list.git",
"commit": "cd95255e38fe32e17e42172a8878279910121269",
"installedAt": "2026-02-26T15:50:37.958Z"
"commit": "2fa3bbb34db78b05adee6ca90e93b7bfcea90114",
"installedAt": "2026-02-28T20:09:32.052Z"
},
"tag-page": {
"source": "github:quartz-community/tag-page",
"resolved": "https://github.com/quartz-community/tag-page.git",
"commit": "7aec8562e2a5fe514f6e834c9ec95eaa31ae50d4",
"installedAt": "2026-02-26T15:50:38.444Z"
"commit": "1526aed5ce857e3b2ec813da55a62442b583120b",
"installedAt": "2026-02-28T20:09:32.593Z"
}
}
}

View File

@ -42,10 +42,7 @@ export function resolveFrame(name: string | undefined): PageFrame {
// Fall back to built-in frames
const frame = builtinFrames[name]
if (!frame) {
const allFrameNames = [
...Object.keys(builtinFrames),
...[...frameRegistry.getAll().keys()],
]
const allFrameNames = [...Object.keys(builtinFrames), ...[...frameRegistry.getAll().keys()]]
console.warn(
`Unknown page frame "${name}", falling back to "default". Available frames: ${allFrameNames.join(", ")}`,
)