mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-22 05:55:42 -05:00
Migrate ArticleTitle, TagList, PageTitle, Darkmode, ReaderMode, ContentMeta, and Footer from internal components to community plugins. Update layout to use Plugin.X() pattern, remove internal component files and their styles/scripts. Add MIGRATION_TASKS.md documenting the full migration roadmap.
1117 lines
58 KiB
Markdown
1117 lines
58 KiB
Markdown
# Quartz v5 Migration Tasks
|
|
|
|
> Roadmap for migrating internal components to community plugins and adopting `@quartz-community/types` in core.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Plan Overview](#plan-overview)
|
|
2. [Architecture Summary](#architecture-summary)
|
|
3. [Component Analysis](#component-analysis)
|
|
4. [Migration Plan](#migration-plan)
|
|
- [Phase A: Delete Internal Duplicates](#phase-a-delete-internal-duplicates)
|
|
- [Phase B: Migrate Feature Components](#phase-b-migrate-feature-components-optional)
|
|
- [Phase C: Adopt @quartz-community/types in Core](#phase-c-adopt-quartz-communitytypes-in-core)
|
|
- [Phase D: PageType System](#phase-d-pagetype-system)
|
|
5. [Execution Order](#execution-order)
|
|
6. [Migration Checklist Templates](#migration-checklist-templates)
|
|
- [Component Migration Checklist](#component-migration-checklist)
|
|
- [PageType Migration Checklist](#pagetype-migration-checklist)
|
|
|
|
---
|
|
|
|
## Plan Overview
|
|
|
|
Quartz v5 is transitioning from a monolithic component architecture to a plugin-first model where community plugins are first-class citizens. This migration has four phases:
|
|
|
|
1. **Phase A — Delete Internal Duplicates**: Remove internal components that already have community plugin equivalents.
|
|
2. **Phase B — Migrate Feature Components**: Extract additional feature components to community plugins.
|
|
3. **Phase C — Type Unification**: Adopt `@quartz-community/types` in Quartz core's `quartz/components/types.ts`, eliminating `as QuartzComponent` casts in `quartz.layout.ts`.
|
|
4. **Phase D — PageType System**: Introduce PageType as a new first-class plugin category. Reclassify current emitters into PageTypes, FileEmitters, and StaticFiles. Migrate all three page-rendering emitters (Content, Folder, Tags) to community plugins. Keep 404 as a core default fallback.
|
|
|
|
### Current State
|
|
|
|
- **10 community plugins** exist across `quartz-community/*` repos: `explorer`, `graph`, `search`, `table-of-contents`, `backlinks`, `comments`, `breadcrumbs`, `recent-notes`, `latex`, and the `plugin-template`.
|
|
- **6 of these** have corresponding internal duplicates still in `quartz/components/` that should be removed.
|
|
- **`quartz.layout.ts`** uses `as QuartzComponent` casts to bridge type incompatibility between core and community types.
|
|
- All community plugins import from `@quartz-community/types` (via `github:quartz-community/types`).
|
|
|
|
### Goal State
|
|
|
|
- Internal duplicates deleted from `quartz/components/`.
|
|
- Additional feature components migrated to community plugins.
|
|
- Core `quartz/components/types.ts` re-exports from `@quartz-community/types` (or aligns structurally), eliminating the need for `as QuartzComponent` casts.
|
|
- `quartz.layout.ts` uses plugin components without type casts.
|
|
- PageType introduced as a new first-class plugin category — page-rendering emitters replaced by declarative PageType plugins.
|
|
- All three page types (Content, Folder, Tags) migrated to community plugins. 404 stays in core as a default fallback.
|
|
- Current emitters reclassified into PageTypes, FileEmitters, StaticFiles, and core infrastructure.
|
|
- Layout configuration restructured to `layout.defaults` (universal) + `layout.byPageType` (per-page-type slots).
|
|
|
|
---
|
|
|
|
## Architecture Summary
|
|
|
|
### How Components Are Used
|
|
|
|
```
|
|
quartz.config.ts → declares externalPlugins (community plugin sources)
|
|
quartz.layout.ts → composes components into page layouts
|
|
quartz/components/ → internal component definitions
|
|
.quartz/plugins/ → installed community plugins (git-cloned)
|
|
|
|
Emitters (contentPage, tagPage, folderPage, 404) receive layouts and call:
|
|
renderPage(cfg, slug, componentData, layout, resources)
|
|
├─ Head (from sharedPageComponents.head — hardcoded in layout)
|
|
├─ Body (hardcoded in renderPage.tsx as BodyConstructor)
|
|
├─ Header (hardcoded in renderPage.tsx as HeaderConstructor)
|
|
├─ beforeBody (from layout — configurable)
|
|
├─ left (from layout — configurable)
|
|
├─ pageBody (from emitter — Content, TagContent, FolderContent, NotFound)
|
|
├─ right (from layout — configurable)
|
|
├─ afterBody (from layout — configurable)
|
|
└─ Footer (from sharedPageComponents.footer — hardcoded in layout)
|
|
```
|
|
|
|
### The Type Cast Problem
|
|
|
|
Core defines `QuartzComponent` as:
|
|
|
|
```typescript
|
|
// quartz/components/types.ts
|
|
type QuartzComponent = ComponentType<QuartzComponentProps> & {
|
|
css?: StringResource
|
|
beforeDOMLoaded?: StringResource
|
|
afterDOMLoaded?: StringResource
|
|
}
|
|
```
|
|
|
|
Community types define it as:
|
|
|
|
```typescript
|
|
// @quartz-community/types
|
|
type QuartzComponent = ((props: QuartzComponentProps) => unknown) & {
|
|
css?: string | string[] | undefined
|
|
beforeDOMLoaded?: string | string[] | undefined
|
|
afterDOMLoaded?: string | string[] | undefined
|
|
}
|
|
```
|
|
|
|
The difference: core uses `ComponentType<P>` (preact-specific, returns `VNode | null`), community uses `(props) => unknown` (preact-agnostic). Because community plugins have their own `node_modules` with separate preact/hast types, TypeScript sees them as distinct types even though they're structurally compatible at runtime.
|
|
|
|
This is why `quartz.layout.ts` currently needs:
|
|
|
|
```typescript
|
|
const searchComponent = Plugin.Search() as QuartzComponent
|
|
```
|
|
|
|
**Eliminating these casts** is Phase B of this migration.
|
|
|
|
---
|
|
|
|
## Component Analysis
|
|
|
|
### Classification Legend
|
|
|
|
| Category | Meaning |
|
|
| ---------------------- | --------------------------------------------------------------------------- |
|
|
| 🔴 Core Infrastructure | Must stay in core — hardcoded in rendering pipeline or emitters |
|
|
| 🟡 Core Utility | Layout wrappers / composition — must stay, used to arrange other components |
|
|
| 🟢 Already Migrated | Community plugin exists, internal copy is a duplicate to be deleted |
|
|
| 🔵 Migration Candidate | Feature component that can be extracted to a community plugin |
|
|
| ⚪ Supporting | Helper component, not a QuartzComponent — used internally by others |
|
|
|
|
---
|
|
|
|
### 🔴 Core Infrastructure (MUST stay in core)
|
|
|
|
These are hardcoded in emitters or `renderPage.tsx` and are fundamental to the build pipeline.
|
|
|
|
| Component | File | Used By | Why Core |
|
|
| ------------------ | ------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
| **Head** | `Head.tsx` | All emitters via `sharedPageComponents.head` | Renders `<head>` with meta, SEO, styles, scripts. Required for every page. |
|
|
| **Body** | `Body.tsx` | `renderPage.tsx` (as `BodyConstructor`) | Page `<body>` wrapper. Includes clipboard script. Hardcoded in render pipeline. |
|
|
| **Header** | `Header.tsx` | `renderPage.tsx` (as `HeaderConstructor`) | Page header wrapper. Hardcoded in render pipeline. |
|
|
| **Content** | `pages/Content.tsx` | `contentPage.tsx` emitter | Renders article HTML content. Tightly coupled to emitter. |
|
|
| **TagContent** | `pages/TagContent.tsx` | `tagPage.tsx` emitter | Renders tag listing page. Depends on `PageList`. Tightly coupled to emitter. |
|
|
| **FolderContent** | `pages/FolderContent.tsx` | `folderPage.tsx` emitter | Renders folder listing page. Depends on `PageList`. Tightly coupled to emitter. |
|
|
| **NotFound** | `pages/404.tsx` | `404.tsx` emitter | 404 page content. Tightly coupled to emitter. |
|
|
| **registry.ts** | `registry.ts` | `external.ts`, `componentResources.ts` emitter | Component registration system for dynamic plugin lookup. |
|
|
| **external.ts** | `external.ts` | `quartz.layout.ts` (via `External()`) | External component loader. Required for plugin system. |
|
|
| **types.ts** | `types.ts` | Every component | Type definitions (`QuartzComponent`, `QuartzComponentProps`, `QuartzComponentConstructor`). |
|
|
| **index.ts** | `index.ts` | Everywhere | Barrel exports for all built-in components. |
|
|
| **renderPage.tsx** | `renderPage.tsx` | All page emitters | Core rendering engine. Composes layouts, handles transclusions, injects resources. |
|
|
|
|
---
|
|
|
|
### 🟡 Core Utility (Layout wrappers — stay in core)
|
|
|
|
These are composition utilities that take `QuartzComponent` as arguments. They must remain in core to work with both internal and external components.
|
|
|
|
| Component | File | Dependencies | Purpose |
|
|
| --------------------- | ----------------------- | -------------------------- | ------------------------------------------------- |
|
|
| **DesktopOnly** | `DesktopOnly.tsx` | Component types | Wraps a component to show only on desktop. |
|
|
| **MobileOnly** | `MobileOnly.tsx` | Component types | Wraps a component to show only on mobile. |
|
|
| **Flex** | `Flex.tsx` | Component types, resources | Arranges multiple components in a flex row. |
|
|
| **ConditionalRender** | `ConditionalRender.tsx` | Component types | Renders a component only when a condition is met. |
|
|
| **Spacer** | `Spacer.tsx` | `lang` utility | Simple layout spacer element. |
|
|
|
|
---
|
|
|
|
### 🟢 Already Migrated (Delete internal duplicates)
|
|
|
|
These components have community plugin equivalents in `quartz-community/*` repos. The internal copies are **dead code** and should be removed along with their associated styles and scripts.
|
|
|
|
| Component | Internal File | Community Plugin | Associated Styles | Associated Scripts |
|
|
| ------------------- | --------------------- | ------------------------------------------- | ------------------------------------------ | ---------------------------- |
|
|
| **Backlinks** | `Backlinks.tsx` | `github:quartz-community/backlinks` | `styles/backlinks.scss` | (uses `OverflowList`) |
|
|
| **Breadcrumbs** | `Breadcrumbs.tsx` | `github:quartz-community/breadcrumbs` | `styles/breadcrumbs.scss` | — |
|
|
| **RecentNotes** | `RecentNotes.tsx` | `github:quartz-community/recent-notes` | `styles/recentNotes.scss` | — |
|
|
| **Search** | `Search.tsx` | `github:quartz-community/search` | `styles/search.scss` | `scripts/search.inline.ts` |
|
|
| **TableOfContents** | `TableOfContents.tsx` | `github:quartz-community/table-of-contents` | `styles/toc.scss`, `styles/legacyToc.scss` | `scripts/toc.inline.ts` |
|
|
| **Comments** | `Comments.tsx` | `github:quartz-community/comments` | — | `scripts/comments.inline.ts` |
|
|
|
|
**Note**: `OverflowList.tsx` is a helper used by the internal `Backlinks.tsx` and `TableOfContents.tsx`. After deleting these internal duplicates, verify whether `OverflowList.tsx` is still imported by any remaining core component. If not, it can also be removed.
|
|
|
|
---
|
|
|
|
### 🔵 Migration Candidates (Feature components)
|
|
|
|
These are user-facing, swappable components with no hard coupling to the build pipeline. They can be extracted to community plugins.
|
|
|
|
| Component | File | Dependencies | Scripts | Styles | Complexity | Notes |
|
|
| ---------------- | ------------------ | -------------------------------- | ------------------------------ | ------------------------- | ---------- | ------------------------------------------------------------------ |
|
|
| **Darkmode** | `Darkmode.tsx` | `i18n`, `lang` | `scripts/darkmode.inline.ts` | `styles/darkmode.scss` | Low | Theme toggle button. Self-contained. Interacts with CSS variables. |
|
|
| **ReaderMode** | `ReaderMode.tsx` | `i18n`, `lang` | `scripts/readermode.inline.ts` | `styles/readermode.scss` | Low | Reader mode toggle. Self-contained. Hides sidebars. |
|
|
| **PageTitle** | `PageTitle.tsx` | `path`, `i18n`, `lang` | — | Inline CSS | Low | Site title in sidebar. Uses `pathToRoot` for link. |
|
|
| **ArticleTitle** | `ArticleTitle.tsx` | `lang` | — | Inline CSS | Very Low | Renders frontmatter title. Minimal logic. |
|
|
| **ContentMeta** | `ContentMeta.tsx` | `Date` component, `i18n`, `lang` | — | `styles/contentMeta.scss` | Low | Shows date and reading time. Uses the `Date` utility. |
|
|
| **TagList** | `TagList.tsx` | `path`, `lang` | — | Inline CSS | Low | Renders tag links. Uses `resolveRelative` for URLs. |
|
|
| **Footer** | `Footer.tsx` | `i18n` | — | `styles/footer.scss` | Low | Shows version and links. Commonly customized. |
|
|
|
|
---
|
|
|
|
### ⚪ Supporting Components (NOT QuartzComponents)
|
|
|
|
These are helper components or utilities used internally by other components. They don't follow the `QuartzComponentConstructor` factory pattern.
|
|
|
|
| Component | File | Used By | Migration Impact |
|
|
| ---------------- | ------------------ | ---------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
| **Date** | `Date.tsx` | `ContentMeta`, `RecentNotes` (community), `PageList` | Stays in core. Community plugins that need date formatting bundle their own. |
|
|
| **PageList** | `PageList.tsx` | `TagContent`, `FolderContent` | Stays in core. Used by page-type components which are core. |
|
|
| **OverflowList** | `OverflowList.tsx` | `Backlinks` (internal), `TableOfContents` (internal) | Review after deleting internal duplicates. May become unused. |
|
|
|
|
---
|
|
|
|
### Scripts Inventory
|
|
|
|
| Script | File | Used By | Category |
|
|
| ---------------------- | ---------- | ------------------------------------------ | -------------------------- |
|
|
| `clipboard.inline.ts` | `scripts/` | `Body.tsx` | 🔴 Core |
|
|
| `spa.inline.ts` | `scripts/` | `componentResources.ts` emitter | 🔴 Core |
|
|
| `popover.inline.ts` | `scripts/` | `componentResources.ts` emitter | 🔴 Core |
|
|
| `callout.inline.ts` | `scripts/` | Transformer (not a component) | 🔴 Core |
|
|
| `checkbox.inline.ts` | `scripts/` | Transformer (not a component) | 🔴 Core |
|
|
| `mermaid.inline.ts` | `scripts/` | Transformer (not a component) | 🔴 Core |
|
|
| `darkmode.inline.ts` | `scripts/` | `Darkmode.tsx` | 🔵 Migrate with Darkmode |
|
|
| `readermode.inline.ts` | `scripts/` | `ReaderMode.tsx` | 🔵 Migrate with ReaderMode |
|
|
| `search.inline.ts` | `scripts/` | `Search.tsx` (internal duplicate) | 🟢 Delete |
|
|
| `toc.inline.ts` | `scripts/` | `TableOfContents.tsx` (internal duplicate) | 🟢 Delete |
|
|
| `comments.inline.ts` | `scripts/` | `Comments.tsx` (internal duplicate) | 🟢 Delete |
|
|
|
|
### Styles Inventory
|
|
|
|
| Style | File | Used By | Category |
|
|
| --------------------- | --------- | ------------------------------------------ | --------------------------- |
|
|
| `clipboard.scss` | `styles/` | `Body.tsx` | 🔴 Core |
|
|
| `popover.scss` | `styles/` | `componentResources.ts` emitter | 🔴 Core |
|
|
| `listPage.scss` | `styles/` | `TagContent`, `FolderContent` | 🔴 Core |
|
|
| `mermaid.inline.scss` | `styles/` | Transformer (not a component) | 🔴 Core |
|
|
| `backlinks.scss` | `styles/` | `Backlinks.tsx` (internal duplicate) | 🟢 Delete |
|
|
| `breadcrumbs.scss` | `styles/` | `Breadcrumbs.tsx` (internal duplicate) | 🟢 Delete |
|
|
| `recentNotes.scss` | `styles/` | `RecentNotes.tsx` (internal duplicate) | 🟢 Delete |
|
|
| `search.scss` | `styles/` | `Search.tsx` (internal duplicate) | 🟢 Delete |
|
|
| `toc.scss` | `styles/` | `TableOfContents.tsx` (internal duplicate) | 🟢 Delete |
|
|
| `legacyToc.scss` | `styles/` | `TableOfContents.tsx` (internal duplicate) | 🟢 Delete |
|
|
| `darkmode.scss` | `styles/` | `Darkmode.tsx` | 🔵 Migrate with Darkmode |
|
|
| `readermode.scss` | `styles/` | `ReaderMode.tsx` | 🔵 Migrate with ReaderMode |
|
|
| `contentMeta.scss` | `styles/` | `ContentMeta.tsx` | 🔵 Migrate with ContentMeta |
|
|
| `footer.scss` | `styles/` | `Footer.tsx` | 🔵 Migrate with Footer |
|
|
|
|
---
|
|
|
|
## Migration Plan
|
|
|
|
### Phase A: Delete Internal Duplicates
|
|
|
|
**Goal**: Remove the 6 internal components that already have community plugin equivalents.
|
|
|
|
**What to delete**:
|
|
|
|
1. **Component files**:
|
|
- `quartz/components/Backlinks.tsx`
|
|
- `quartz/components/Breadcrumbs.tsx`
|
|
- `quartz/components/RecentNotes.tsx`
|
|
- `quartz/components/Search.tsx`
|
|
- `quartz/components/TableOfContents.tsx`
|
|
- `quartz/components/Comments.tsx`
|
|
|
|
2. **Associated styles**:
|
|
- `quartz/components/styles/backlinks.scss`
|
|
- `quartz/components/styles/breadcrumbs.scss`
|
|
- `quartz/components/styles/recentNotes.scss`
|
|
- `quartz/components/styles/search.scss`
|
|
- `quartz/components/styles/toc.scss`
|
|
- `quartz/components/styles/legacyToc.scss`
|
|
|
|
3. **Associated scripts**:
|
|
- `quartz/components/scripts/search.inline.ts`
|
|
- `quartz/components/scripts/toc.inline.ts`
|
|
- `quartz/components/scripts/comments.inline.ts`
|
|
|
|
4. **Update barrel exports** in `quartz/components/index.ts` — remove the deleted components from the export list.
|
|
|
|
5. **Verify `OverflowList.tsx`** — check if it's still imported by any remaining component. If not, delete it too.
|
|
|
|
**Validation**: Run `tsc --noEmit` and `npx quartz build` to ensure nothing breaks.
|
|
|
|
---
|
|
|
|
### Phase B: Migrate Feature Components (Optional)
|
|
|
|
**Goal**: Extract additional feature components to community plugins using the established plugin template pattern.
|
|
|
|
**Recommended migration order** (by independence and complexity):
|
|
|
|
| Priority | Component | Reason |
|
|
| -------- | ---------------- | -------------------------------------------------------------------------------------------------- |
|
|
| 1 | **ArticleTitle** | Simplest component. Zero dependencies beyond `lang`. Good proof of concept. |
|
|
| 2 | **TagList** | Simple. Only needs `path` utilities. |
|
|
| 3 | **PageTitle** | Simple. Needs `pathToRoot` utility. |
|
|
| 4 | **Darkmode** | Self-contained with script. Needs inline script bundling. |
|
|
| 5 | **ReaderMode** | Same pattern as Darkmode. |
|
|
| 6 | **ContentMeta** | Slightly more complex — uses `Date` helper component. Plugin must include its own date formatting. |
|
|
| 7 | **Footer** | Simple, but commonly customized. Consider keeping in core as a default. |
|
|
|
|
**For each migration**, follow the [Migration Checklist Template](#migration-checklist-template) below.
|
|
|
|
**After migrating each component**:
|
|
|
|
1. Delete internal component file + its styles/scripts.
|
|
2. Add the new community plugin to `externalPlugins` in `quartz.config.ts`.
|
|
3. Update `quartz.layout.ts` to use the plugin component.
|
|
4. Remove the component from `quartz/components/index.ts`.
|
|
5. Run `tsc --noEmit` and `npx quartz build`.
|
|
|
|
---
|
|
|
|
### Phase C: Adopt `@quartz-community/types` in Core
|
|
|
|
**Goal**: Unify the type definitions so community plugin components are directly assignable to `QuartzComponent` without casts.
|
|
|
|
**Prerequisite**: Phase A must be complete. Phase B is recommended but not required.
|
|
|
|
**Strategy**:
|
|
|
|
There are two approaches, in order of preference:
|
|
|
|
#### Option 1: Re-export from community types (Recommended)
|
|
|
|
Make core's `quartz/components/types.ts` re-export from `@quartz-community/types`:
|
|
|
|
```typescript
|
|
// quartz/components/types.ts
|
|
export type {
|
|
QuartzComponent,
|
|
QuartzComponentConstructor,
|
|
QuartzComponentProps,
|
|
StringResource,
|
|
} from "@quartz-community/types"
|
|
```
|
|
|
|
**Pros**: Single source of truth. Eliminates type mismatch entirely.
|
|
**Cons**: Core loses preact-specific return type (`VNode | null` → `unknown`). Internal components would still work at runtime, but TypeScript wouldn't catch cases where a component returns a non-JSX value.
|
|
|
|
**Mitigation**: Internal components already return JSX correctly. The looser return type only matters for new component development, and the factory pattern enforced by `satisfies QuartzComponentConstructor` provides a guardrail.
|
|
|
|
#### Option 2: Structural alignment
|
|
|
|
Keep separate type definitions but make them structurally compatible:
|
|
|
|
```typescript
|
|
// quartz/components/types.ts
|
|
export type QuartzComponent = ((props: QuartzComponentProps) => unknown) & {
|
|
css?: string | string[]
|
|
beforeDOMLoaded?: string | string[]
|
|
afterDOMLoaded?: string | string[]
|
|
}
|
|
```
|
|
|
|
**Pros**: Core controls its own types. Can be stricter internally.
|
|
**Cons**: Still separate type identities. May not resolve `as QuartzComponent` casts if `QuartzComponentProps` also differs.
|
|
|
|
#### Steps for Option 1
|
|
|
|
1. Add `@quartz-community/types` as a dependency in `package.json`:
|
|
|
|
```json
|
|
"dependencies": {
|
|
"@quartz-community/types": "github:quartz-community/types"
|
|
}
|
|
```
|
|
|
|
2. Update `quartz/components/types.ts` to re-export:
|
|
|
|
```typescript
|
|
export type {
|
|
QuartzComponent,
|
|
QuartzComponentConstructor,
|
|
QuartzComponentProps,
|
|
StringResource,
|
|
} from "@quartz-community/types"
|
|
```
|
|
|
|
3. Resolve any type conflicts in internal components:
|
|
- Internal components use concrete preact types (`JSX.Element`, `VNode`).
|
|
- These are assignable to `unknown`, so they should work.
|
|
- Run `tsc --noEmit` to verify.
|
|
|
|
4. Update `quartz.layout.ts`:
|
|
- Remove all `as QuartzComponent` casts.
|
|
- Remove the `import { QuartzComponent } from "./quartz/components/types"` line.
|
|
|
|
5. Verify:
|
|
- `tsc --noEmit` passes.
|
|
- `npx quartz build` succeeds.
|
|
- Site renders correctly.
|
|
|
|
---
|
|
|
|
### Phase D: PageType System
|
|
|
|
**Goal**: Introduce PageType as a new first-class plugin category (alongside transformers, filters, emitters, and components), reclassify all current emitters into purpose-specific categories, and migrate all three page-rendering emitters to community plugins.
|
|
|
|
**Prerequisites**: Phase A and Phase C must be complete. Phase B is recommended.
|
|
|
|
#### D.1: Design Overview
|
|
|
|
Currently, every HTML-rendering emitter (ContentPage, FolderPage, TagPage, NotFoundPage) follows identical boilerplate:
|
|
|
|
1. Filter content files to find matching pages
|
|
2. Merge shared + page layout into `FullPageLayout`
|
|
3. Instantiate Header and Body constructors
|
|
4. Build `componentData`
|
|
5. Call `renderPage(cfg, slug, componentData, layout, resources)`
|
|
6. Write the resulting HTML to output
|
|
|
|
The only differences between emitters are **which files they match** (step 1) and **what body component they render** (the `pageBody` slot). This means the rendering pipeline is duplicated four times with only the filtering and body varying.
|
|
|
|
**PageType** extracts this pattern into a declarative plugin interface:
|
|
|
|
```typescript
|
|
interface PageTypePlugin {
|
|
name: string
|
|
priority?: number // higher wins conflicts, default 0
|
|
match: PageMatcher // which source files this page type owns
|
|
generate?: PageGenerator // create virtual pages (e.g., tag index, folder index)
|
|
layout: string // layout key (references layout.byPageType)
|
|
body: QuartzComponentConstructor // the page body component
|
|
}
|
|
```
|
|
|
|
A new **PageType dispatcher** in core replaces all individual page-emitting emitters. It iterates over registered PageTypes, runs their matchers and generators, resolves layout, and calls `renderPage` once per page.
|
|
|
|
#### D.2: Emitter Reclassification
|
|
|
|
Every current emitter is reclassified into one of four categories:
|
|
|
|
| Current Emitter | New Category | New Location | Rationale |
|
|
| -------------------- | ------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
| `contentPage` | **PageType** plugin | `github:quartz-community/content-page` | Renders individual content pages. Match: all `.md` files. Body: `Content` component. |
|
|
| `folderPage` | **PageType** plugin | `github:quartz-community/folder-page` | Generates folder index pages. Match: none (virtual). Generate: one page per folder. Body: `FolderContent`. |
|
|
| `tagPage` | **PageType** plugin | `github:quartz-community/tag-page` | Generates tag index pages. Match: none (virtual). Generate: one page per tag + tag listing. Body: `TagContent`. |
|
|
| `404` | **PageType** (core) | `quartz/plugins/pageTypes/404.ts` | Default 404 fallback. Stays in core. Match: none. Generate: single `/404` page. |
|
|
| `componentResources` | **Core infra** | `quartz/plugins/emitters/componentResources.ts` (unchanged) | Aggregates CSS/JS from all components, handles analytics, SPA router, font loading. Cannot be a plugin — it needs access to all registered components. |
|
|
| `contentIndex` | **FileEmitter** | `quartz/plugins/emitters/contentIndex.tsx` (stays, reclassified) | Generates sitemap.xml, RSS feed, contentIndex.json. Cross-page aggregation — not a page type. |
|
|
| `aliases` | **FileEmitter** | `quartz/plugins/emitters/aliases.tsx` (stays, reclassified) | Generates HTML redirect stubs from frontmatter aliases. Bulk generation, not a page type. |
|
|
| `ogImages` | **FileEmitter** | `quartz/plugins/emitters/ogImages.tsx` (stays, reclassified) | Generates OG image binaries. Asset generation, not a page type. |
|
|
| `assets` | **StaticFiles** | `quartz/plugins/emitters/assets.ts` (stays, reclassified) | Copies non-markdown files from content directory verbatim. |
|
|
| `static` | **StaticFiles** | `quartz/plugins/emitters/static.ts` (stays, reclassified) | Copies `quartz/static/` directory verbatim. |
|
|
| `favicon` | **StaticFiles** | `quartz/plugins/emitters/favicon.tsx` (stays, reclassified) | Generates or copies favicon files. |
|
|
| `cname` | **StaticFiles** | `quartz/plugins/emitters/cname.ts` (stays, reclassified) | Writes CNAME file for custom domains. |
|
|
|
|
**Category definitions**:
|
|
|
|
- **PageType**: Owns a set of routes, provides a body component and layout reference, produces rendered HTML pages.
|
|
- **FileEmitter**: Produces non-HTML output files (XML, JSON, binary) by aggregating data across all content. Keeps the existing `QuartzEmitterPlugin` interface.
|
|
- **StaticFiles**: Copies or generates files that don't depend on content processing. Keeps the existing `QuartzEmitterPlugin` interface.
|
|
- **Core infrastructure**: `ComponentResources` — must remain in core as it aggregates resources from all registered components.
|
|
|
|
> **Note**: FileEmitter and StaticFiles are conceptual reclassifications for clarity. They continue to use the existing `QuartzEmitterPlugin` interface and `"emitter"` category in the plugin manifest. No code changes are required for these — only the page-rendering emitters are replaced by PageTypes.
|
|
|
|
#### D.3: Composable Matchers
|
|
|
|
PageTypes use composable matcher functions to declare which source files they own:
|
|
|
|
```typescript
|
|
// Primitive matchers
|
|
match.ext(".md") // match by file extension
|
|
match.ext(".canvas") // for future Canvas page type
|
|
match.slugPrefix("tags/") // match by URL prefix
|
|
match.frontmatter("type", (v) => v === "map") // match by frontmatter field
|
|
|
|
// Combinators
|
|
match.and(
|
|
match.ext(".md"),
|
|
match.frontmatter("draft", (v) => !v),
|
|
)
|
|
match.or(match.ext(".md"), match.ext(".mdx"))
|
|
match.not(match.frontmatter("draft", (v) => v === true))
|
|
|
|
// Convenience
|
|
match.all() // matches everything (default content)
|
|
match.none() // matches nothing (virtual-only page types)
|
|
```
|
|
|
|
**Conflict resolution**: When multiple PageTypes match the same file, the one with the highest `priority` wins. If priorities are equal, the most recently registered PageType wins (community plugins override core defaults).
|
|
|
|
**Matcher function signature**:
|
|
|
|
```typescript
|
|
type PageMatcher = (args: {
|
|
slug: FullSlug
|
|
fileData: QuartzPluginData
|
|
cfg: GlobalConfiguration
|
|
}) => boolean
|
|
|
|
// Helper namespace
|
|
const match = {
|
|
ext: (extension: string) => PageMatcher,
|
|
slugPrefix: (prefix: string) => PageMatcher,
|
|
frontmatter: (key: string, predicate: (value: unknown) => boolean) => PageMatcher,
|
|
and: (...matchers: PageMatcher[]) => PageMatcher,
|
|
or: (...matchers: PageMatcher[]) => PageMatcher,
|
|
not: (matcher: PageMatcher) => PageMatcher,
|
|
all: () => PageMatcher,
|
|
none: () => PageMatcher,
|
|
}
|
|
```
|
|
|
|
#### D.4: Virtual Page Generation
|
|
|
|
Some page types don't match source files — they generate "virtual" pages from aggregated data:
|
|
|
|
```typescript
|
|
type PageGenerator = (args: {
|
|
content: ProcessedContent[] // all processed source files
|
|
cfg: GlobalConfiguration
|
|
ctx: BuildCtx
|
|
}) => VirtualPage[]
|
|
|
|
interface VirtualPage {
|
|
slug: FullSlug // the URL this page will be served at
|
|
title: string // page title
|
|
data: Partial<QuartzPluginData> // synthetic file data for the page
|
|
}
|
|
```
|
|
|
|
**Examples**:
|
|
|
|
- **FolderPage**: Generates one virtual page per unique folder in the content tree.
|
|
- **TagPage**: Generates one virtual page per unique tag found in frontmatter, plus a `/tags` index page listing all tags.
|
|
- **404**: Generates a single virtual page at `/404`.
|
|
|
|
A PageType can use both `match` and `generate` — for example, a future "Collection" page type might match `.collection` files AND generate index pages.
|
|
|
|
#### D.5: Layout Configuration
|
|
|
|
The current layout model uses `sharedPageComponents` + `pageLayouts` (one per page type) which are merged by each emitter. The new model makes this explicit and extensible:
|
|
|
|
```typescript
|
|
// quartz.layout.ts (new format)
|
|
|
|
export const layout = {
|
|
// Truly universal slots — applied to EVERY page type.
|
|
// Keep this minimal. Only things that genuinely belong on every page.
|
|
defaults: {
|
|
head: Component.Head(),
|
|
},
|
|
|
|
// Per-page-type slot configuration.
|
|
// Each key corresponds to a PageType plugin's `layout` field.
|
|
byPageType: {
|
|
content: {
|
|
beforeBody: [
|
|
Component.Breadcrumbs(),
|
|
Component.ArticleTitle(),
|
|
Component.ContentMeta(),
|
|
Component.TagList(),
|
|
],
|
|
left: [
|
|
Component.PageTitle(),
|
|
Component.MobileOnly(Component.Spacer()),
|
|
Component.Search(),
|
|
Component.Darkmode(),
|
|
Component.Explorer(),
|
|
],
|
|
right: [Component.Graph(), Component.TableOfContents(), Component.Backlinks()],
|
|
afterBody: [Component.Comments()],
|
|
footer: Component.Footer(),
|
|
},
|
|
folder: {
|
|
beforeBody: [Component.Breadcrumbs(), Component.ArticleTitle(), Component.ContentMeta()],
|
|
left: [
|
|
Component.PageTitle(),
|
|
Component.MobileOnly(Component.Spacer()),
|
|
Component.Search(),
|
|
Component.Darkmode(),
|
|
],
|
|
right: [],
|
|
afterBody: [],
|
|
footer: Component.Footer(),
|
|
},
|
|
tag: {
|
|
beforeBody: [Component.Breadcrumbs(), Component.ArticleTitle(), Component.ContentMeta()],
|
|
left: [
|
|
Component.PageTitle(),
|
|
Component.MobileOnly(Component.Spacer()),
|
|
Component.Search(),
|
|
Component.Darkmode(),
|
|
],
|
|
right: [],
|
|
afterBody: [],
|
|
footer: Component.Footer(),
|
|
},
|
|
// A future Canvas page type — no footer, no sidebars, full-width body.
|
|
// canvas: {
|
|
// beforeBody: [],
|
|
// left: [],
|
|
// right: [],
|
|
// afterBody: [],
|
|
// },
|
|
},
|
|
}
|
|
```
|
|
|
|
**Key design decisions**:
|
|
|
|
- `layout.defaults` contains **only truly universal slots** — things that belong on literally every page. Currently that's just `head`. Footer is NOT in defaults because some page types (e.g., Canvas) may not want it.
|
|
- `layout.byPageType` maps PageType names to their slot configuration. Each PageType plugin declares a `layout: string` field that references a key here.
|
|
- If a PageType references a layout key that doesn't exist in `byPageType`, the dispatcher falls back to `defaults` only (head, empty slots). This allows community PageTypes to work without requiring layout configuration — they'll render with just a head and body.
|
|
- The `footer` slot is per-page-type, not a global shared slot.
|
|
|
|
**Layout resolution** (performed by the PageType dispatcher):
|
|
|
|
```
|
|
finalLayout = {
|
|
...layout.defaults, // start with universal defaults (head)
|
|
...layout.byPageType[pageType.layout], // overlay page-type-specific slots
|
|
}
|
|
```
|
|
|
|
#### D.6: Core Infrastructure Changes
|
|
|
|
##### D.6.1: Plugin System Extensions
|
|
|
|
The plugin loader must recognize `"pageType"` as a new plugin category:
|
|
|
|
**`quartz/plugins/loader/types.ts`**:
|
|
|
|
```typescript
|
|
// Current
|
|
export type PluginCategory = "transformer" | "filter" | "emitter"
|
|
|
|
// New
|
|
export type PluginCategory = "transformer" | "filter" | "emitter" | "pageType"
|
|
```
|
|
|
|
**`quartz/plugins/types.ts`** — add the `QuartzPageTypePlugin` interface:
|
|
|
|
```typescript
|
|
export interface QuartzPageTypePlugin {
|
|
name: string
|
|
priority?: number
|
|
match: PageMatcher
|
|
generate?: PageGenerator
|
|
layout: string
|
|
body: QuartzComponentConstructor
|
|
}
|
|
|
|
export interface PluginTypes {
|
|
transformers: QuartzTransformerPlugin[]
|
|
filters: QuartzFilterPlugin[]
|
|
emitters: QuartzEmitterPlugin[]
|
|
pageTypes: QuartzPageTypePlugin[] // new
|
|
}
|
|
```
|
|
|
|
**`quartz/plugins/loader/index.ts`**:
|
|
|
|
- Update `detectPluginType()` to recognize PageType exports (looks for `match`, `body`, `layout` exports).
|
|
- Update `extractPluginFactory()` to handle PageType plugins.
|
|
- Update `loadExternalPlugins()` to sort loaded plugins into the `pageTypes` array.
|
|
|
|
##### D.6.2: PageType Dispatcher
|
|
|
|
A new core module replaces the individual page-rendering emitters:
|
|
|
|
**`quartz/plugins/pageTypes/dispatcher.ts`**:
|
|
|
|
The dispatcher is a `QuartzEmitterPlugin` (it plugs into the existing emit pipeline) that:
|
|
|
|
1. Collects all registered `QuartzPageTypePlugin` instances (from core + community plugins).
|
|
2. For each source file in `content`:
|
|
- Iterates page types by descending `priority`.
|
|
- Finds the first PageType whose `match()` returns `true`.
|
|
- Records the assignment: `file → pageType`.
|
|
3. For each PageType with a `generate()` function:
|
|
- Calls `generate()` with all content.
|
|
- Records the virtual pages: `virtualPage → pageType`.
|
|
4. For each assigned page (source + virtual):
|
|
- Resolves layout: `layout.defaults` merged with `layout.byPageType[pageType.layout]`.
|
|
- Instantiates `pageType.body` as the `pageBody` component.
|
|
- Calls `renderPage()` with the resolved layout.
|
|
- Writes HTML to output.
|
|
|
|
```typescript
|
|
// Pseudocode
|
|
const PageTypeDispatcher: QuartzEmitterPlugin = {
|
|
name: "PageTypeDispatcher",
|
|
async emit(ctx, content, resources) {
|
|
const pageTypes = ctx.cfg.plugins.pageTypes // sorted by priority desc
|
|
|
|
// Phase 1: Match source files
|
|
const assignments = new Map<FullSlug, QuartzPageTypePlugin>()
|
|
for (const [tree, fileData] of content) {
|
|
for (const pt of pageTypes) {
|
|
if (pt.match({ slug: fileData.slug!, fileData, cfg: ctx.cfg.configuration })) {
|
|
assignments.set(fileData.slug!, pt)
|
|
break // first match wins (highest priority)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Phase 2: Generate virtual pages
|
|
const virtualPages: Array<{ page: VirtualPage; pageType: QuartzPageTypePlugin }> = []
|
|
for (const pt of pageTypes) {
|
|
if (pt.generate) {
|
|
const pages = pt.generate({ content, cfg: ctx.cfg.configuration, ctx })
|
|
for (const page of pages) {
|
|
virtualPages.push({ page, pageType: pt })
|
|
}
|
|
}
|
|
}
|
|
|
|
// Phase 3: Render all pages
|
|
const fps: FilePath[] = []
|
|
// ... render source pages from assignments
|
|
// ... render virtual pages from virtualPages
|
|
// ... each using resolved layout + pageType.body
|
|
return fps
|
|
},
|
|
}
|
|
```
|
|
|
|
##### D.6.3: renderPage.tsx Refactoring
|
|
|
|
`renderPage.tsx` currently hardcodes `HeaderConstructor` and `BodyConstructor` imports. These must become parameters:
|
|
|
|
**Current** (simplified):
|
|
|
|
```typescript
|
|
import HeaderConstructor from "./Header"
|
|
import BodyConstructor from "./Body"
|
|
|
|
export function renderPage(cfg, slug, componentData, components, resources) {
|
|
const Header = HeaderConstructor()
|
|
const Body = BodyConstructor()
|
|
// ... render using Header, Body, and components
|
|
}
|
|
```
|
|
|
|
**New**:
|
|
|
|
```typescript
|
|
export function renderPage(cfg, slug, componentData, components, resources) {
|
|
// Header and Body are now part of the components/layout passed in,
|
|
// OR are hardcoded as core structural components (they're not swappable).
|
|
// The key change: pageBody comes from the PageType, not from the emitter.
|
|
}
|
|
```
|
|
|
|
> **Design note**: Header and Body are structural wrappers (`<header>`, `<body>` HTML elements), not content components. They can remain hardcoded in `renderPage`. The important refactoring is that `pageBody` — the actual page content component — comes from the PageType plugin rather than being hardcoded per emitter.
|
|
|
|
##### D.6.4: Configuration Changes
|
|
|
|
**`quartz.config.ts`** — PageType plugins are loaded via `externalPlugins` just like other plugins:
|
|
|
|
```typescript
|
|
externalPlugins: [
|
|
// Components
|
|
{ source: "github:quartz-community/search" },
|
|
{ source: "github:quartz-community/explorer" },
|
|
{ source: "github:quartz-community/backlinks" },
|
|
// ...
|
|
|
|
// Page Types
|
|
{ source: "github:quartz-community/content-page" },
|
|
{ source: "github:quartz-community/folder-page" },
|
|
{ source: "github:quartz-community/tag-page" },
|
|
],
|
|
```
|
|
|
|
The plugin loader auto-detects the plugin type. No separate `pageTypes` array is needed in config — detection is based on the plugin's exports (`match`, `body`, `layout` fields identify a PageType plugin).
|
|
|
|
#### D.7: Community Plugin Migrations
|
|
|
|
Three new community plugin repositories:
|
|
|
|
##### `github:quartz-community/content-page`
|
|
|
|
- **Match**: `match.ext(".md")` (or `match.all()` with low priority as a catch-all)
|
|
- **Generate**: none (source files only)
|
|
- **Layout**: `"content"`
|
|
- **Body**: `Content` component (moved from `quartz/components/pages/Content.tsx`)
|
|
- **Priority**: `0` (default — other page types with higher priority can override for specific files)
|
|
|
|
##### `github:quartz-community/folder-page`
|
|
|
|
- **Match**: `match.none()` (virtual pages only)
|
|
- **Generate**: Scans all content files, extracts unique folder paths, generates one virtual page per folder.
|
|
- **Layout**: `"folder"`
|
|
- **Body**: `FolderContent` component (moved from `quartz/components/pages/FolderContent.tsx`)
|
|
- **Dependencies**: `PageList` helper component. The plugin must bundle its own copy or import from a shared utils package.
|
|
|
|
##### `github:quartz-community/tag-page`
|
|
|
|
- **Match**: `match.none()` (virtual pages only)
|
|
- **Generate**: Scans all content files, extracts unique tags from frontmatter, generates one virtual page per tag plus a `/tags` index page.
|
|
- **Layout**: `"tag"`
|
|
- **Body**: `TagContent` component (moved from `quartz/components/pages/TagContent.tsx`)
|
|
- **Dependencies**: `PageList` helper component. Same bundling consideration as FolderPage.
|
|
|
|
##### Core: `quartz/plugins/pageTypes/404.ts`
|
|
|
|
- **Match**: `match.none()` (virtual page only)
|
|
- **Generate**: Produces a single virtual page at slug `/404`.
|
|
- **Layout**: falls back to `defaults` only (just `head`)
|
|
- **Body**: `NotFound` component (stays in `quartz/components/pages/404.tsx`)
|
|
- **Priority**: `-1` (lowest — never conflicts with other page types)
|
|
|
|
#### D.8: Type Updates (`@quartz-community/types`)
|
|
|
|
Add the following types to `~/Repos/types/src/index.ts`:
|
|
|
|
```typescript
|
|
// --- PageType Plugin Types ---
|
|
|
|
/** Matcher function: determines if a source file belongs to this page type. */
|
|
export type PageMatcher = (args: {
|
|
slug: string
|
|
fileData: Record<string, unknown>
|
|
cfg: Record<string, unknown>
|
|
}) => boolean
|
|
|
|
/** Virtual page descriptor for page types that generate pages from aggregated data. */
|
|
export interface VirtualPage {
|
|
slug: string
|
|
title: string
|
|
data: Record<string, unknown>
|
|
}
|
|
|
|
/** Generator function: produces virtual pages from all processed content. */
|
|
export type PageGenerator = (args: {
|
|
content: Array<[unknown, Record<string, unknown>]>
|
|
cfg: Record<string, unknown>
|
|
ctx: Record<string, unknown>
|
|
}) => VirtualPage[]
|
|
|
|
/** A PageType plugin definition. */
|
|
export interface QuartzPageTypePlugin {
|
|
name: string
|
|
priority?: number
|
|
match: PageMatcher
|
|
generate?: PageGenerator
|
|
layout: string
|
|
body: QuartzComponentConstructor
|
|
}
|
|
```
|
|
|
|
> **Note**: The community types package uses `Record<string, unknown>` for core-specific types (`QuartzPluginData`, `GlobalConfiguration`, `BuildCtx`) because community plugins don't have access to Quartz core's internal type definitions. At runtime, the actual Quartz types are passed in — the `Record` types are a structural stand-in that avoids coupling community types to core internals.
|
|
|
|
#### D.9: Plugin Template Updates
|
|
|
|
Update `~/Repos/plugin-template` to include a PageType example:
|
|
|
|
**`src/index.ts`** — add a PageType export example (commented out):
|
|
|
|
```typescript
|
|
// --- PageType Example ---
|
|
// Uncomment and modify to create a PageType plugin instead of a component plugin.
|
|
//
|
|
// import { MyPageBody } from "./pages/MyPageBody"
|
|
// import type { QuartzPageTypePlugin, PageMatcher } from "@quartz-community/types"
|
|
//
|
|
// const matchMyPages: PageMatcher = ({ fileData }) => {
|
|
// return fileData.frontmatter?.type === "my-custom-type"
|
|
// }
|
|
//
|
|
// export const myPageType: QuartzPageTypePlugin = {
|
|
// name: "my-page-type",
|
|
// priority: 10,
|
|
// match: matchMyPages,
|
|
// layout: "my-page-type",
|
|
// body: MyPageBody,
|
|
// }
|
|
```
|
|
|
|
**`src/types.ts`** — add re-exports for PageType types:
|
|
|
|
```typescript
|
|
export type {
|
|
PageMatcher,
|
|
PageGenerator,
|
|
VirtualPage,
|
|
QuartzPageTypePlugin,
|
|
} from "@quartz-community/types"
|
|
```
|
|
|
|
#### D.10: Files to Create
|
|
|
|
| File | Purpose |
|
|
| ---------------------------------------- | --------------------------------------------------------------- |
|
|
| `quartz/plugins/pageTypes/dispatcher.ts` | Core PageType dispatcher (replaces individual page emitters) |
|
|
| `quartz/plugins/pageTypes/404.ts` | Core 404 page type (default fallback) |
|
|
| `quartz/plugins/pageTypes/matchers.ts` | Composable matcher helpers (`match.ext()`, `match.and()`, etc.) |
|
|
| `quartz/plugins/pageTypes/index.ts` | Barrel exports for the pageTypes module |
|
|
|
|
#### D.11: Files to Modify
|
|
|
|
| File | Change |
|
|
| -------------------------------------- | ---------------------------------------------------------------- |
|
|
| `quartz/plugins/types.ts` | Add `QuartzPageTypePlugin`, update `PluginTypes` |
|
|
| `quartz/plugins/loader/types.ts` | Add `"pageType"` to `PluginCategory` |
|
|
| `quartz/plugins/loader/index.ts` | Update detection, extraction, and loading for PageType plugins |
|
|
| `quartz/cfg.ts` | Update `FullPageLayout` or add layout resolution types |
|
|
| `quartz/components/renderPage.tsx` | Accept `pageBody` as parameter instead of hardcoding per-emitter |
|
|
| `quartz.config.ts` | Add content-page, folder-page, tag-page to `externalPlugins` |
|
|
| `quartz.layout.ts` | Restructure to `layout.defaults` + `layout.byPageType` format |
|
|
| `~/Repos/types/src/index.ts` | Add PageType-related types |
|
|
| `~/Repos/plugin-template/src/index.ts` | Add PageType example |
|
|
| `~/Repos/plugin-template/src/types.ts` | Add PageType type re-exports |
|
|
|
|
#### D.12: Files to Delete (after migration)
|
|
|
|
| File | Replaced By |
|
|
| ------------------------------------------- | --------------------------------------------------- |
|
|
| `quartz/plugins/emitters/contentPage.tsx` | `github:quartz-community/content-page` + dispatcher |
|
|
| `quartz/plugins/emitters/folderPage.tsx` | `github:quartz-community/folder-page` + dispatcher |
|
|
| `quartz/plugins/emitters/tagPage.tsx` | `github:quartz-community/tag-page` + dispatcher |
|
|
| `quartz/plugins/emitters/404.tsx` | `quartz/plugins/pageTypes/404.ts` |
|
|
| `quartz/components/pages/Content.tsx` | Bundled into content-page community plugin |
|
|
| `quartz/components/pages/FolderContent.tsx` | Bundled into folder-page community plugin |
|
|
| `quartz/components/pages/TagContent.tsx` | Bundled into tag-page community plugin |
|
|
|
|
> **Note**: `quartz/components/pages/404.tsx` (the NotFound body component) stays in core since the 404 PageType stays in core.
|
|
|
|
#### D.13: Key Design Decisions
|
|
|
|
| Decision | Rationale |
|
|
| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| PageType is a new plugin category, not a subtype of emitter | Clean separation of concerns. Emitters are a low-level escape hatch; PageTypes are a high-level declarative abstraction. |
|
|
| 404 stays in core | Every site needs a 404 page. It's a sensible default that shouldn't require installing a community plugin. |
|
|
| All three page types migrate to community plugins | The v5 plugin system is the core vision. Partial migration would be inconsistent and confusing. |
|
|
| `layout.defaults` is minimal (just `head`) | Not all page types want the same chrome. Canvas pages may want no footer, no sidebars. Only truly universal elements belong in defaults. |
|
|
| Footer is per-page-type, not in defaults | Explicitly requested: Canvas pages should not have footer forced on them. |
|
|
| FileEmitter/StaticFiles are conceptual, not new interfaces | Avoids unnecessary API churn. The existing `QuartzEmitterPlugin` interface works fine for these. The reclassification is for documentation and mental model clarity. |
|
|
| Matchers are composable functions, not config objects | Functions are more flexible and can express arbitrary logic. The `match.*` helpers provide convenience without limiting power. |
|
|
| Plugin loader auto-detects PageType | Consistent with existing auto-detection for transformers/filters/emitters. No separate config array needed. |
|
|
| `PageList` helper is bundled into page type plugins | Avoids creating a shared utils dependency just for one helper. Each plugin is self-contained. |
|
|
|
|
---
|
|
|
|
## Execution Order
|
|
|
|
```
|
|
Step 1: Delete 6 internal duplicates (Phase A)
|
|
├─ Delete component files, styles, scripts
|
|
├─ Update quartz/components/index.ts
|
|
├─ Check OverflowList.tsx usage
|
|
└─ Verify: tsc --noEmit, npx quartz build
|
|
|
|
Step 2: Migrate feature components (Phase B) — one at a time
|
|
├─ Create community plugin repo from template
|
|
├─ Migrate component code (see checklist)
|
|
├─ Add to externalPlugins in quartz.config.ts
|
|
├─ Update quartz.layout.ts
|
|
├─ Delete internal copy
|
|
└─ Verify: tsc --noEmit, npx quartz build
|
|
|
|
Step 3: Adopt @quartz-community/types in core (Phase C)
|
|
├─ Add types dependency
|
|
├─ Update quartz/components/types.ts
|
|
├─ Remove as QuartzComponent casts from quartz.layout.ts
|
|
└─ Verify: tsc --noEmit, npx quartz build
|
|
|
|
Step 4: Build PageType infrastructure (Phase D — core)
|
|
├─ Add PageType types to @quartz-community/types
|
|
├─ Add QuartzPageTypePlugin to quartz/plugins/types.ts
|
|
├─ Update PluginCategory in quartz/plugins/loader/types.ts
|
|
├─ Create quartz/plugins/pageTypes/matchers.ts (composable matchers)
|
|
├─ Create quartz/plugins/pageTypes/dispatcher.ts (core dispatcher)
|
|
├─ Create quartz/plugins/pageTypes/404.ts (core 404 page type)
|
|
├─ Update quartz/plugins/loader/index.ts (detection + loading)
|
|
├─ Refactor quartz/components/renderPage.tsx (accept pageBody as parameter)
|
|
└─ Verify: tsc --noEmit
|
|
|
|
Step 5: Migrate page types to community plugins (Phase D — plugins)
|
|
├─ Create github:quartz-community/content-page
|
|
│ ├─ Move Content component + styles
|
|
│ ├─ Define match, layout, body
|
|
│ └─ Build + verify
|
|
├─ Create github:quartz-community/folder-page
|
|
│ ├─ Move FolderContent component + PageList helper + styles
|
|
│ ├─ Define generate, layout, body
|
|
│ └─ Build + verify
|
|
├─ Create github:quartz-community/tag-page
|
|
│ ├─ Move TagContent component + PageList helper + styles
|
|
│ ├─ Define generate, layout, body
|
|
│ └─ Build + verify
|
|
└─ Verify: all three plugins build independently
|
|
|
|
Step 6: Integrate and cut over (Phase D — integration)
|
|
├─ Add content-page, folder-page, tag-page to externalPlugins
|
|
├─ Restructure quartz.layout.ts to defaults + byPageType format
|
|
├─ Delete old emitters: contentPage.tsx, folderPage.tsx, tagPage.tsx, 404.tsx
|
|
├─ Delete old page components: Content.tsx, FolderContent.tsx, TagContent.tsx
|
|
├─ Update quartz/components/index.ts
|
|
├─ Update plugin-template with PageType example
|
|
└─ Verify: tsc --noEmit, npx quartz build, site renders correctly
|
|
```
|
|
|
|
---
|
|
|
|
## Migration Checklist Templates
|
|
|
|
### Component Migration Checklist
|
|
|
|
Use this checklist for each component being migrated to a community plugin.
|
|
|
|
#### Setup
|
|
|
|
- [ ] Create new repository at `github.com/quartz-community/{component-name}`
|
|
- [ ] Clone `plugin-template` as starting point
|
|
- [ ] Update `package.json`:
|
|
- [ ] Set `name` to `@quartz-community/{component-name}`
|
|
- [ ] Set `repository` URL
|
|
- [ ] Verify `@quartz-community/types` dependency
|
|
- [ ] Verify `@quartz-community/utils` dependency (if needed)
|
|
|
|
#### Component Migration
|
|
|
|
- [ ] Copy component file to `src/components/{ComponentName}.tsx`
|
|
- [ ] Convert to factory function pattern with `satisfies QuartzComponentConstructor`
|
|
- [ ] Replace imports:
|
|
- [ ] `"./types"` → `"@quartz-community/types"`
|
|
- [ ] `"../util/path"` → local `src/util/path.ts` or `@quartz-community/utils`
|
|
- [ ] `"../i18n"` → local `src/i18n/` with required locale strings
|
|
- [ ] `"../util/lang"` → local `src/util/lang.ts`
|
|
- [ ] Extract SCSS to `src/components/styles/{name}.scss`
|
|
- [ ] Extract inline scripts (if any) to `src/components/scripts/{name}.inline.ts`
|
|
- [ ] Add type stubs: `src/components/styles.d.ts`, `src/components/scripts.d.ts`
|
|
- [ ] Attach resources: `Component.css = style`, `Component.afterDOMLoaded = script`
|
|
|
|
#### Exports
|
|
|
|
- [ ] Create `src/components/index.ts` with default export
|
|
- [ ] Create `src/index.ts` re-exporting from components
|
|
- [ ] Verify `tsup.config.ts` entry points match `package.json` exports
|
|
|
|
#### Build & Test
|
|
|
|
- [ ] Run `npm run build` — verify `dist/` output
|
|
- [ ] Run `npm run check` — typecheck, lint, format
|
|
- [ ] Install in Quartz: add to `externalPlugins` in `quartz.config.ts`
|
|
- [ ] Use in layout: update `quartz.layout.ts`
|
|
- [ ] Run `tsc --noEmit` in Quartz
|
|
- [ ] Run `npx quartz build` and verify site renders
|
|
|
|
#### Cleanup (in Quartz core)
|
|
|
|
- [ ] Delete internal component file
|
|
- [ ] Delete associated style file(s)
|
|
- [ ] Delete associated script file(s)
|
|
- [ ] Remove from `quartz/components/index.ts`
|
|
- [ ] Verify no remaining imports of deleted files
|
|
|
|
#### Publish
|
|
|
|
- [ ] Commit and push community plugin repo
|
|
- [ ] Verify Quartz build with `github:quartz-community/{component-name}` specifier
|
|
|
|
---
|
|
|
|
### PageType Migration Checklist
|
|
|
|
Use this checklist for each page type being migrated to a community plugin.
|
|
|
|
#### Setup
|
|
|
|
- [ ] Create new repository at `github.com/quartz-community/{page-type-name}`
|
|
- [ ] Clone `plugin-template` as starting point
|
|
- [ ] Update `package.json`:
|
|
- [ ] Set `name` to `@quartz-community/{page-type-name}`
|
|
- [ ] Set `repository` URL
|
|
- [ ] Verify `@quartz-community/types` dependency
|
|
|
|
#### Page Body Component
|
|
|
|
- [ ] Copy page body component to `src/pages/{PageBody}.tsx`
|
|
- ContentPage: `Content.tsx` from `quartz/components/pages/Content.tsx`
|
|
- FolderPage: `FolderContent.tsx` from `quartz/components/pages/FolderContent.tsx`
|
|
- TagPage: `TagContent.tsx` from `quartz/components/pages/TagContent.tsx`
|
|
- [ ] Copy helper components if needed (`PageList.tsx` for folder/tag page types)
|
|
- [ ] Replace imports:
|
|
- [ ] `"../types"` → `"@quartz-community/types"`
|
|
- [ ] `"../../util/path"` → local `src/util/path.ts` or `@quartz-community/utils`
|
|
- [ ] `"../../i18n"` → local `src/i18n/` with required locale strings
|
|
- [ ] Extract associated styles to `src/pages/styles/`
|
|
- [ ] Add type stubs for styles if needed
|
|
|
|
#### PageType Definition
|
|
|
|
- [ ] Create `src/index.ts` with PageType export:
|
|
- [ ] Define `match` using composable matchers (or `match.none()` for virtual-only)
|
|
- [ ] Define `generate` function (for folder/tag types that create virtual pages)
|
|
- [ ] Set `layout` key (must match a key in `layout.byPageType`)
|
|
- [ ] Set `body` to the page body component constructor
|
|
- [ ] Set `name` and `priority`
|
|
- [ ] Verify export satisfies `QuartzPageTypePlugin` type
|
|
|
|
#### Virtual Page Generation (if applicable)
|
|
|
|
- [ ] Implement `generate()` function:
|
|
- [ ] Scan all content for relevant data (folders, tags, etc.)
|
|
- [ ] Return `VirtualPage[]` with correct slugs, titles, and data
|
|
- [ ] Verify generated slugs don't conflict with source file slugs
|
|
|
|
#### Build & Test
|
|
|
|
- [ ] Run `npm run build` — verify `dist/` output
|
|
- [ ] Run `npm run check` — typecheck, lint, format
|
|
- [ ] Install in Quartz: add to `externalPlugins` in `quartz.config.ts`
|
|
- [ ] Add layout entry in `quartz.layout.ts` under `layout.byPageType`
|
|
- [ ] Run `tsc --noEmit` in Quartz
|
|
- [ ] Run `npx quartz build` and verify pages render correctly
|
|
|
|
#### Cleanup (in Quartz core)
|
|
|
|
- [ ] Delete old emitter file (`quartz/plugins/emitters/{name}.tsx`)
|
|
- [ ] Delete old page body component (`quartz/components/pages/{Name}.tsx`)
|
|
- [ ] Update emitter barrel exports
|
|
- [ ] Update component barrel exports
|
|
- [ ] Verify no remaining imports of deleted files
|
|
|
|
#### Publish
|
|
|
|
- [ ] Commit and push community plugin repo
|
|
- [ ] Verify Quartz build with `github:quartz-community/{page-type-name}` specifier
|