diff --git a/MIGRATION_TASKS.md b/MIGRATION_TASKS.md deleted file mode 100644 index 15c99ff8e..000000000 --- a/MIGRATION_TASKS.md +++ /dev/null @@ -1,1116 +0,0 @@ -# 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 & { - 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

` (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 `` with meta, SEO, styles, scripts. Required for every page. | -| **Body** | `Body.tsx` | `renderPage.tsx` (as `BodyConstructor`) | Page `` 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 // 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() - 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 (`

`, `` 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 - cfg: Record -}) => boolean - -/** Virtual page descriptor for page types that generate pages from aggregated data. */ -export interface VirtualPage { - slug: string - title: string - data: Record -} - -/** Generator function: produces virtual pages from all processed content. */ -export type PageGenerator = (args: { - content: Array<[unknown, Record]> - cfg: Record - ctx: Record -}) => 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` 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 diff --git a/PLUGIN_MIGRATION_ANALYSIS.md b/PLUGIN_MIGRATION_ANALYSIS.md deleted file mode 100644 index ff3b8a9a0..000000000 --- a/PLUGIN_MIGRATION_ANALYSIS.md +++ /dev/null @@ -1,548 +0,0 @@ -# Quartz Plugin Migration Analysis - -This document provides a comprehensive analysis of which Quartz v4 plugins and components can be migrated to separate repositories, along with strategies and recommendations for each migration. - -## Table of Contents - -1. [Executive Summary](#executive-summary) -2. [Migration Categories](#migration-categories) -3. [Component Analysis](#component-analysis) -4. [Plugin Analysis](#plugin-analysis) -5. [Migration Strategies](#migration-strategies) -6. [Lessons Learned](#lessons-learned) -7. [Recommended Migration Order](#recommended-migration-order) - ---- - -## Executive Summary - -Quartz v4 consists of **25 components** and **25 plugins** (13 transformers, 2 filters, 12 emitters). Not all of these are suitable for migration to external repositories. This analysis categorizes each based on: - -- **Complexity** - How much code and dependencies are involved -- **Independence** - Whether it can function without deep Quartz integration -- **Community Value** - Whether external plugins would benefit the ecosystem -- **Customization Demand** - How often users want to customize this feature - -### Summary Statistics - -| Category | Total | Good Candidates | Moderate | Core Only | -| ------------ | ----- | --------------- | -------- | --------- | -| Components | 25 | 8 | 7 | 10 | -| Transformers | 13 | 5 | 4 | 4 | -| Emitters | 12 | 3 | 4 | 5 | -| Filters | 2 | 1 | 1 | 0 | - ---- - -## Migration Categories - -### ✅ Good Candidates - -Components/plugins that are: - -- Self-contained with clear boundaries -- Frequently customized by users -- Have potential for community variations -- Can work with the standard plugin API - -### ⚠️ Moderate Candidates - -Components/plugins that: - -- Have some dependencies on Quartz internals -- Might need API adjustments to externalize -- Could benefit from being plugins but require more work - -### ❌ Core Only - -Components/plugins that: - -- Are fundamental to Quartz's operation -- Have deep integration with build system -- Would create versioning nightmares if externalized -- Are rarely customized - ---- - -## Component Analysis - -### ✅ Good Candidates for Migration - -#### 1. Explorer (Already Migrated) - -**Status**: ✅ Migrated to `@quartz-community/explorer` - -**Complexity**: High (FileTrieNode, state persistence, mobile navigation) -**Dependencies**: Content index data -**Value**: High - users frequently want custom sorting, filtering, and styling - -**Migration Notes**: - -- Required reimplementing FileTrieNode for client-side -- Needed path utilities (simplifySlug, folder prefix detection) -- State persistence via localStorage works well as standalone - -#### 2. Graph (Already Migrated) - -**Status**: ✅ Migrated to `@quartz-community/graph` - -**Complexity**: Very High (D3 force simulation, PixiJS rendering) -**Dependencies**: Content index, D3, PixiJS (loaded from CDN) -**Value**: High - visual feature that differentiates Quartz - -**Migration Notes**: - -- Loads D3/PixiJS from CDN to avoid bundling large dependencies -- Configuration is self-contained via data attributes -- Works well as standalone plugin - -#### 3. Search (Already Migrated) - -**Status**: ✅ Migrated to `@quartz-community/search` - -**Complexity**: High (FlexSearch indexing, CJK support, preview) -**Dependencies**: Content index, FlexSearch -**Value**: High - core feature with customization demand - -**Migration Notes**: - -- FlexSearch can be imported as ESM module -- Preview fetching works via standard fetch API -- Keyboard shortcuts are self-contained - -#### 4. TableOfContents - -**Status**: 📋 Recommended for migration - -**Complexity**: Medium -**Dependencies**: Heading data from transformer -**Value**: Medium - users want different TOC styles - -**Migration Strategy**: - -``` -1. Extract TableOfContents.tsx and toc.inline.ts -2. Pass heading data via data attributes -3. Style variations via CSS variables -4. Support different depth/scroll behavior options -``` - -#### 5. Backlinks - -**Status**: 📋 Recommended for migration - -**Complexity**: Medium -**Dependencies**: Content index (backlinks data) -**Value**: Medium - some users want custom backlink displays - -**Migration Strategy**: - -``` -1. Extract Backlinks.tsx -2. Fetch backlinks from contentIndex.json -3. Support custom rendering templates -4. Add filtering options -``` - -#### 6. RecentNotes - -**Status**: 📋 Recommended for migration - -**Complexity**: Low-Medium -**Dependencies**: Content metadata -**Value**: Medium - popular feature with customization needs - -**Migration Strategy**: - -``` -1. Extract component and date sorting logic -2. Make limit, filter, and sort configurable -3. Support custom card templates -``` - -#### 7. Comments - -**Status**: 📋 Recommended for migration - -**Complexity**: Low (wrapper for external services) -**Dependencies**: External comment providers (Giscus, Utterances, etc.) -**Value**: High - many comment system variations exist - -**Migration Strategy**: - -``` -1. Create base Comments interface -2. Implement Giscus, Utterances, Disqus variants -3. Each provider as separate plugin or single plugin with providers -``` - -#### 8. Breadcrumbs - -**Status**: 📋 Recommended for migration - -**Complexity**: Low -**Dependencies**: Path parsing -**Value**: Low-Medium - some customization demand - -**Migration Strategy**: - -``` -1. Extract component with path utilities -2. Make separator, styling configurable -3. Add options for hiding/showing parts -``` - -### ⚠️ Moderate Candidates - -#### 9. Darkmode - -**Status**: ⚠️ Consider migration - -**Complexity**: Low -**Dependencies**: Theme system (CSS variables) -**Concerns**: Tightly coupled to Quartz's theme system - -**Migration Consideration**: - -- Could work if theme contract is stable -- Would need to document required CSS variables -- Risk: theme changes could break external plugins - -#### 10. ReaderMode - -**Status**: ⚠️ Consider migration - -**Complexity**: Low -**Dependencies**: Layout system, CSS -**Concerns**: Affects layout which is Quartz-controlled - -#### 11. TagList - -**Status**: ⚠️ Consider migration - -**Complexity**: Low -**Dependencies**: Frontmatter tags -**Value**: Low - simple component - -#### 12. ContentMeta - -**Status**: ⚠️ Consider migration - -**Complexity**: Low -**Dependencies**: Frontmatter, date formatting -**Value**: Low-Medium - -#### 13. Footer - -**Status**: ⚠️ Consider migration - -**Complexity**: Low -**Dependencies**: Site configuration -**Value**: Low - simple customization - -#### 14. Flex - -**Status**: ⚠️ Consider migration - -**Complexity**: Very Low -**Dependencies**: None -**Value**: Low - utility component - -#### 15. ConditionalRender - -**Status**: ⚠️ Consider migration - -**Complexity**: Very Low -**Dependencies**: Page context -**Value**: Low - utility component - -### ❌ Core Only (Do Not Migrate) - -#### 16. Content, TagContent, FolderContent - -**Reason**: Core page rendering, deeply integrated with build system - -#### 17. NotFound (404) - -**Reason**: Needs to integrate with routing system - -#### 18. Head - -**Reason**: SEO and metadata, tightly coupled to configuration - -#### 19. PageTitle - -**Reason**: Simple, rarely customized, core branding - -#### 20. ArticleTitle - -**Reason**: Simple, part of core layout - -#### 21. Spacer - -**Reason**: Trivial utility, not worth externalizing - -#### 22. DesktopOnly / MobileOnly - -**Reason**: Layout utilities, simple CSS wrappers - ---- - -## Plugin Analysis - -### Transformer Plugins - -#### ✅ Good Candidates - -| Plugin | Complexity | Value | Migration Notes | -| ---------------------------- | ---------- | --------- | -------------------------------------------- | -| **ObsidianFlavoredMarkdown** | High | Very High | Popular, many want customization | -| **RoamFlavoredMarkdown** | Medium | Medium | Niche but valuable for Roam users | -| **OxHugoFlavouredMarkdown** | Medium | Medium | Niche for Hugo migrants | -| **Citations** | Medium | Medium | Academic users want variations | -| **Latex** | Medium | High | Math rendering variations (KaTeX vs MathJax) | - -#### ⚠️ Moderate Candidates - -| Plugin | Complexity | Concerns | -| ---------------------- | ---------- | ------------------------------------------- | -| **SyntaxHighlighting** | Medium | Many themes exist, could externalize themes | -| **TableOfContents** | Low | Generates heading data for component | -| **Description** | Low | Simple, stable | -| **HardLineBreaks** | Very Low | Trivial | - -#### ❌ Core Only - -| Plugin | Reason | -| -------------------------- | ---------------------------------- | -| **FrontMatter** | Fundamental to content parsing | -| **GitHubFlavoredMarkdown** | Base markdown features | -| **CreatedModifiedDate** | Deep git integration | -| **CrawlLinks** | Link resolution integral to Quartz | - -### Emitter Plugins - -#### ✅ Good Candidates - -| Plugin | Complexity | Value | Migration Notes | -| ------------------ | ---------- | ------ | ------------------------------------ | -| **CustomOgImages** | High | High | Users want custom OG generation | -| **CNAME** | Very Low | Medium | Simple but useful for custom domains | -| **Favicon** | Low | Low | Simple file operations | - -#### ⚠️ Moderate Candidates - -| Plugin | Complexity | Concerns | -| ------------------ | ---------- | ---------------------------------------------- | -| **ContentIndex** | Medium | Core data structure, careful versioning needed | -| **AliasRedirects** | Low | Simple but integrated with routing | -| **Static** | Low | File copying, stable | -| **Assets** | Low | File copying, stable | - -#### ❌ Core Only - -| Plugin | Reason | -| ---------------------- | -------------------- | -| **ContentPage** | Core page generation | -| **TagPage** | Core page type | -| **FolderPage** | Core page type | -| **ComponentResources** | CSS/JS bundling | -| **NotFoundPage** | 404 generation | - -### Filter Plugins - -| Plugin | Status | Notes | -| ------------------- | ----------- | ----------------------------- | -| **RemoveDrafts** | ⚠️ Moderate | Simple, could have variations | -| **ExplicitPublish** | ⚠️ Moderate | Alternative publishing model | - ---- - -## Migration Strategies - -### Strategy 1: Client-Side Component (Recommended for UI) - -Best for: Explorer, Graph, Search, TableOfContents, Backlinks - -``` -Components with client-side interactivity: -1. Export TSX component with factory pattern -2. Inline scripts bundled as strings -3. SCSS styles bundled as strings -4. Configuration via data attributes -5. Data fetched from /static/contentIndex.json -``` - -**Pros**: Complete independence, clear boundaries -**Cons**: Requires reimplementing utilities - -### Strategy 2: Transformer Plugin - -Best for: Markdown extensions, syntax variations - -``` -Transformer plugins: -1. Export factory function returning plugin object -2. Implement markdownPlugins() for remark plugins -3. Implement htmlPlugins() for rehype plugins -4. Optionally inject CSS/JS via externalResources() -``` - -**Pros**: Clean API, well-documented pattern -**Cons**: Limited to markdown/HTML transformation - -### Strategy 3: Emitter Plugin - -Best for: Output generation, file processing - -``` -Emitter plugins: -1. Export factory function returning plugin object -2. Implement emit() for file generation -3. Optionally implement partialEmit() for incremental builds -4. Declare dependencies via getQuartzComponents() -``` - -**Pros**: Full control over output -**Cons**: More complex API, needs build system understanding - -### Strategy 4: Hybrid (Component + Emitter) - -Best for: Features needing both UI and data generation - -``` -Graph-like features: -1. Component plugin for UI rendering -2. Emitter plugin for data generation -3. Clear data contract between them -``` - -**Pros**: Flexibility, separation of concerns -**Cons**: More complex to maintain - ---- - -## Lessons Learned - -From migrating Explorer, Graph, and Search: - -### 1. Path Utilities Are Essential - -Every component needs path helpers: - -- `simplifySlug()` - Remove trailing `/index` -- `getCurrentSlug()` - Parse current URL -- Folder prefix detection for navigation - -**Recommendation**: Create `@quartz-community/utils` package - -### 2. Content Index Is The Data Contract - -All components fetch from `/static/contentIndex.json`. Document this format: - -```typescript -interface ContentIndex { - [slug: string]: { - title: string - links: string[] - tags: string[] - content: string - // ... other fields - } -} -``` - -### 3. Event System Is Stable - -Quartz's navigation events work reliably: - -- `nav` - After page load/navigation -- `prenav` - Before navigation (save state) -- `window.addCleanup()` - Register cleanup handlers - -### 4. CDN Loading Works Well - -For large dependencies (D3, PixiJS, FlexSearch): - -- Load from CDN at runtime -- Avoids bundling bloat -- Users can self-host if needed - -### 5. TypeScript Limitations - -Inline scripts run in browser context: - -- Use `@ts-nocheck` directive -- Document expected globals -- Provide type stubs if needed - -### 6. State Persistence Patterns - -Standard patterns work: - -- `localStorage` for persistent state (folder states) -- `sessionStorage` for session state (scroll positions) -- URL hash for shareable state (if needed) - ---- - -## Recommended Migration Order - -Based on value, complexity, and community demand: - -### Phase 1: High Value, Already Migrated ✅ - -1. ~~Explorer~~ - Complete -2. ~~Graph~~ - Complete -3. ~~Search~~ - Complete - -### Phase 2: High Value, Medium Complexity - -4. **Comments** - Many provider variations needed -5. **TableOfContents** - Frequently customized -6. **Backlinks** - Popular feature - -### Phase 3: Medium Value, Low Complexity - -7. **RecentNotes** - Common customization -8. **Breadcrumbs** - Simple migration -9. **Latex** - Math rendering variations - -### Phase 4: Transformer Plugins - -10. **ObsidianFlavoredMarkdown** - High demand -11. **Citations** - Academic users -12. **RoamFlavoredMarkdown** - Niche but valuable - -### Phase 5: Emitter Plugins - -13. **CustomOgImages** - High customization demand -14. **ContentIndex** - Careful versioning needed - -### Phase 6: Evaluate Based on Demand - -- Darkmode (if theme API stabilizes) -- SyntaxHighlighting themes -- Other minor components - ---- - -## Appendix: Migration Checklist - -For each plugin migration: - -- [ ] Create repository from plugin-template -- [ ] Copy component/plugin code -- [ ] Replace Quartz imports with `@quartz-community/types` -- [ ] Reimplement required utilities (path helpers, etc.) -- [ ] Add `@ts-nocheck` for inline scripts -- [ ] Update data fetching to use `/static/contentIndex.json` -- [ ] Handle both data formats (`data.content || data`) -- [ ] Test with `npm run build` -- [ ] Test with actual Quartz site -- [ ] Document configuration options -- [ ] Add to plugin registry - ---- - -_This document was generated based on analysis of Quartz v4 codebase and lessons learned from migrating Explorer, Graph, and Search plugins._