feat(v5): add plugin system (#2295)

* feat(plugins): v5 plugin system

* feat(plugins): explorer as community plugin

* feat(plugins): graph as community plugin

* chore: update package-lock.json

* chore: update package-lock.json

* docs: updated plugin-specific docs

* chore: update package-lock.json

* chore: update package-lock.json

* chore: update package-lock.json

* Implement Git-based plugin system with dogfooding for community plugins

- Remove npm dependencies for @quartz-community/* plugins

- Add gitLoader.ts for installing plugins from GitHub

- Update quartz.layout.ts to import from .quartz/plugins/

- Add install-plugins.ts script for prebuild hook

- Add .quartz/ to .gitignore

* Add comprehensive Git-based plugin CLI with lockfile support

- Create quartz.lock.json format for tracking exact plugin commits

- Add 'npx quartz plugin' commands: install, add, remove, update, list, restore

- Plugin state is fully reproducible via lockfile

- No npm dependencies required for community plugins

* Fix TypeScript errors in git-installed plugins

- Install @quartz-community/types as devDependency

- Fix plugin imports to define types locally

- Fix search inline script fetchData bug

- Format code with prettier

* fix(types): install types from github

* docs: updated plugin-specific docs

* Update Dockerfile and add CI/CD documentation

- Add plugin install step to Dockerfile

- Create docs/ci-cd.md with pipeline configuration guide

* Update GitHub Actions workflows for v5 branch and Git-based plugins

- Change branch references from v4 to v5

- Add plugin caching to speed up builds

- Use 'npx quartz plugin install' instead of 'restore'

- Update Docker workflow branch trigger

* Update quartz.lock.json with fixed plugin versions

* fix(docker): install command

* docs: add plugin migration analysis document

Comprehensive analysis of which Quartz v4 components and plugins
can be migrated to separate repositories, including:
- Component analysis (25 components)
- Plugin analysis (transformers, emitters, filters)
- Migration strategies for different plugin types
- Lessons learned from Explorer/Graph/Search migrations
- Recommended migration order

* chore: updated plugins

* chore: updated plugins

* chore: updated dependencies

* chore: updated plugins

* chore: updated plugins

* chore: updated plugins

* chore: updated plugins

* chore: updated plugins

* chore: updated plugins

* chore: updated plugins

* chore: updated plugins

* chore: updated plugins

* chore: tsconfig

* feat: build installed plugins

* chore: updated plugins

* chore: updated plugins

* chore: update explorer plugin with duplication fix

* docs: Quartz v5

* chore: update graph plugin with navigation fix

* fix: update explorer plugin with toggle fix

* fix: update explorer plugin - ensure toggle buttons always work

* fix: create plugin components once to prevent duplicate script registration

* chore: updated plugins

* chore: updated plugins

* feat: migrate 7 feature components to community plugins (Phase B)

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.

* chore: updated plugins

* refactor: delete 6 internal component duplicates (Phase A)

Remove Backlinks, Breadcrumbs, RecentNotes, Search, TableOfContents,
Comments, and OverflowList — all replaced by community plugins.
Delete associated styles (6) and scripts (3). Switch layout to use
Plugin.Breadcrumbs() instead of Component.Breadcrumbs().

* refactor: unify QuartzComponent type to structural interface (Phase C)

- Changed QuartzComponent from ComponentType<QuartzComponentProps> to callable type ((props: QuartzComponentProps) => any)
- Added optional displayName property for better debugging
- Removed ComponentType import from preact
- Removed all 13 'as QuartzComponent' type casts from quartz.layout.ts
- Community plugin components now directly assignable without casts

* feat: add PageType plugin infrastructure (Phase D Step 4)

* feat: add PageTypePluginEntry for cross-boundary type compatibility

Introduce PageTypePluginEntry with never[] parameter types to accept
both internal and community PageType plugins in config arrays without
casts, working around branded FullSlug contravariance mismatch.

* refactor: update dispatcher to cast PageTypePluginEntry at boundary

Add getPageTypes() helper that casts config's PageTypePluginEntry[]
to QuartzPageTypePluginInstance[] in one place. Cast VirtualPage.slug
to FullSlug at emitPage/defaultProcessedContent call sites.

* feat: integrate community PageType plugins (Phase D Step 6)

Replace old page-rendering emitters with PageTypeDispatcher emitter
and pageTypes array. Restructure quartz.layout.ts from three separate
exports to unified layout object with defaults and byPageType record.
Install content-page, folder-page, tag-page community plugins.

* refactor: delete old page-rendering emitters

Remove ContentPage, FolderPage, TagPage, and NotFoundPage emitters
now replaced by community PageType plugins and the PageTypeDispatcher.

* refactor: remove migrated page body components

Delete Content, FolderContent, TagContent page components now provided
by community PageType plugins. Update components barrel export.

* fix: update lockfile to fixed folder-page and tag-page commits

Points to commits that remove duplicate PageList/SortFn re-exports,
fixing TS2300 duplicate identifier errors in generated plugin index.

* chore: updated plugins

* fix: populate ctx.trie in PageTypeDispatcher before rendering

Components like FolderContent depend on ctx.trie for folder hierarchy.
The dispatcher now lazily initializes it via trieFromAllFiles in emit
and force-rebuilds it in partialEmit to reflect file changes.

* chore: update lockfile to fixed folder-page commit

* chore: updated plugins

* chore: update explorer plugin to fix SPA folder navigation

* feat: extract transformers to community plugins and fix type compatibility

- Delete 12 internal transformer files (keep FrontMatter as internal)
- Switch quartz.config.ts to use ExternalPlugin.* for all transformers
- Align branded types with @quartz-community/types (_brand, FullSlug etc.)
- Add vfile DataMap augmentations for fields from extracted transformers
- Update all 29 plugins to @quartz-community/types v0.2.1

* Migrate filters to external plugins (remove-draft, explicit-publish)

Delete internal RemoveDrafts and ExplicitPublish filter implementations,
install them as community plugins, and update quartz.config.ts to use
ExternalPlugin.RemoveDrafts().

* Migrate emitters to external plugins (alias-redirects, cname, favicon, content-index, og-image)

* refactor: remove inline scripts/styles migrated to plugins

Delete dead code: callout, checkbox, mermaid inline scripts and styles
are now bundled by the obsidian-flavored-markdown plugin. Clipboard
script and styles moved to the syntax-highlighting plugin. listPage.scss
was unreferenced. Body.tsx simplified to a pure layout wrapper.

* refactor: consolidate utils to re-export from @quartz-community/utils

* fix: use dangerouslySetInnerHTML for inline CSS to prevent HTML-escaping

Preact was escaping & characters in SCSS-compiled CSS (e.g. & nesting)
into &amp;, breaking CSS rules. Using dangerouslySetInnerHTML bypasses
the escaping, matching how browsers expect style element content.

* chore: update plugins with inline script transpilation fix

* chore: updated plugins

* docs: update plugin API sections for v5 community plugins

* docs: rewrite documentation for v5 plugin system

Update feature docs, hosting, CI/CD, getting started, configuration,
layout, architecture, creating components, making plugins, and
migration guide to reflect the v5 community plugin architecture.

* docs: fix outdated v4 references in documentation

* chore: remove completed migration planning docs

* chore: updated plugins

* chore: cleanup

* chore: cleanup

* chore: bump version to 5.0.0

* chore: updated dependencies

* feat: integrate CanvasPage plugin with types, assets, config, layout, and documentation

* chore: updated dependencies

* chore: updated dependencies

* chore: updated linter

* chore: update canvas-page plugin to c942fcb

* chore: updated plugins

* chore: update canvas-page plugin to f88f1b9

* chore: updated plugins

* chore: update canvas-page plugin to 079304c

* chore: updated plugins

* chore: canvas layout

* chore: update canvas-page plugin to 38d49e1

* chore: updated plugins

* chore: update canvas-page plugin to 505c099

* chore: updated plugins

* chore: updated plugins

* fix: Obsidian flavored markdown

* fix: Obsidian flavored markdown

* fix: Obsidian flavored markdown

* chore: cleanup

* chore: updated plugins

* feat: configuration files

* feat: Quartz TUI

* feat(tui): YAML configuration

* chore: tsup

* chore: tsup

* feat: support array categories in plugin manifests

Plugins like note-properties export both transformer and component
functionality. Allow PluginManifest.category to be a single value
or an array, with config-loader resolving to the first processing
category (transformer/filter/emitter/pageType) for dispatch.

* refactor: remove built-in FrontMatter transformer

Frontmatter processing is now handled by the note-properties plugin,
which provides the same YAML/TOML parsing plus link extraction and
a visual properties panel. The built-in transformer is no longer needed.

* feat: add note-properties plugin to default configuration

Register note-properties as the first plugin (order 5) in both
the user config and the default config. Placed in beforeBody layout
zone with priority 15 (between article-title at 10 and content-meta at 20).

* docs: add plugin management strategy and syncer v5 notes

Document the plugin management system design decisions and provide
implementation guidance for the Quartz Syncer v5 integration.

* feat: add bases-page plugin to default configuration

Enable Obsidian Bases (.base) file support with bases page type
and layout entry in both user and default config.

* docs: update syncer notes with bases-page, note-properties, and spacer

Add all three new plugins to the quick reference table (40 total).
Add content, canvas, and bases page types to byPageType documentation.

* chore: updated plugins

* fix: update CI to Node 24 and regenerate lockfiles for clean install

* fix: resolve type errors for CI checks

* chore: updated plugins

* chore: updated plugins

* fix: plugin mapping from configuration

* fix: CI

* fix: CI

* docs: rewrite Frontmatter documentation for note-properties plugin

* chore: updated plugins

* docs: Quartz v5

* chore: updated plugins

* chore: updated plugins

* refactor: extract TUI to standalone plugin repository

* chore: linting

* docs: Quartz v5

* feat: update and upgrade commands

* chore: updated plugins

* chore: updated plugins

* chore: cleanup

* chore: cleanup

* chore: cleanup

* chore: cleanup

* chore: cleanup

* fix: layout group priority

* fix: view classes

* fix: include virtual pages in content index for explorer visibility

* docs: add board, gallery, and cards view examples to navigation page

* chore: updated plugins

* fix: include virtualPages in worker serializable build context

* fix: set relativePath on virtual pages to prevent explorer crash

* fix: exclude 404

* fix(links): virtual page links

* fix(links): virtual page transclusion

* docs: architecture overview

* fix: only call scripts one per page

* fix: type error in component registry instantiate method

* fix: left layout order

* fix(layout): remove tag-list by default

* docs(plugins): updated plugin list defaults

* fix(layout): priorities

* feat: add PageFrame system for custom page layouts

* feat: integrate PageFrame into rendering pipeline

* feat: add frame resolution to page type dispatcher and config loader

* style: add CSS grid overrides for full-width and minimal page frames

* feat: set minimal frame for 404 and update canvas-page plugin

* docs: add PageFrame system to architecture overview

* fix: wrap frame.render() in array to satisfy Body children type

* chore: format

* fix: use absolute asset paths for 404 page so it works in subdirectories

* fix(layout): priorities

* docs: page frames

* feat: add FrameRegistry for plugin-provided page frames

Plugins can now register custom page frames via their manifest's
'frames' field. Frames are loaded alongside components during plugin
initialization and resolved by name at render time with fallback
to built-in frames.

* feat(layout): page frames

* fix(layout): linting

* fix: inject frame CSS into page so plugin-provided frames render correctly

* chore: updated plugins

* chore: updated plugins

* chore: updated plugins

* chore: updated plugins

* docs: canvas

* chore: updated plugins

* chore: updated plugins

* chore: updated plugins

* feat: add TreeTransform hook, fix multi-category plugins, and resolve cross-plugin dependencies

- Add TreeTransform type and treeTransforms hook to pageType plugins, enabling
  render-time HAST tree mutations (e.g. bases-page inline codeblock resolution)
- Fix config-loader to push multi-category plugins into ALL matching processing
  buckets instead of only the first match
- Add side-effect import for component-only plugins so view registrations
  (e.g. leaflet-map via globalThis ViewRegistry) execute at load time
- Add npm prune --omit=dev and cross-plugin peer dependency symlinking to
  buildPlugin() to prevent duplicate-singleton issues from nested node_modules

* chore: format

* chore: test docs

* chore: updated plugins

* fix: prevent HTML-escaping of inline style and script content in htmlToJsx

Add dangerouslySetInnerHTML overrides for <style> and <script> elements
so that CSS/JS injected by tree transforms is not HTML-escaped during
preact-render-to-string serialization.

* chore: update plugin lockfile for htmlToJsx migration

* chore: update leaflet-map plugin (fix deferred L.Control)

* chore: updated plugins

* chore: updated plugins

* chore: updated plugins

* chore: updated plugins

* chore: updated plugins

* chore: updated plugins

* chore: updated plugins

* chore: test npx quartz upgrade

* feat(templates): add obsidian, ttrpg, blog templates

* docs: move bases

* docs: removed leaflet demo

* feat(cli): configure baseUrl during create

* docs: updated cli commands

* docs: updated documentation for v5

* feat(cli): prune and resolve

* chore: rebuild lockfile

* docs: cli documentation

* docs: plugin development and setup guide

* chore: deleted redundant files

* fix(build): fallback config

* chore: updated lockfile

* docs: removed outdated v3 setup

* feat(cli): allow non-default branch plugins

* docs: install branch commands

* feat(cli): allow local plugins

* docs: install local commands

* feat: add render event type and listener for in-place DOM re-initialization

* docs: add EncryptedPages plugin documentation

* docs: add encrypted pages live demo page

- New password-protected demo page (password: quartz) showing the plugin in action
- Link to demo from EncryptedPages plugin page with password hint callout

* feat: add encrypted-pages plugin to all templates

- Enabled by default in default, obsidian, and ttrpg templates
- Disabled by default in blog template

* chore: updated plugins

* chore: updated layouts

* chore: updated plugins

* feat: stacked pages

* feat: added stacked page panes

* docs: touch-ups
This commit is contained in:
Emile Bangma
2026-03-14 18:10:02 +01:00
committed by GitHub
parent bc99b4a636
commit ab346fa66a
254 changed files with 14718 additions and 11192 deletions
+38
View File
@@ -15,6 +15,12 @@ export const CommonArgv = {
export const CreateArgv = {
...CommonArgv,
template: {
string: true,
alias: ["t"],
choices: ["default", "obsidian", "ttrpg", "blog"],
describe: "template to use for initial configuration",
},
source: {
string: true,
alias: ["s"],
@@ -26,6 +32,11 @@ export const CreateArgv = {
choices: ["new", "copy", "symlink"],
describe: "strategy for content folder setup",
},
baseUrl: {
string: true,
alias: ["b"],
describe: "base URL for your Quartz site (e.g. mysite.github.io/quartz)",
},
links: {
string: true,
alias: ["l"],
@@ -106,3 +117,30 @@ export const BuildArgv = {
describe: "how many threads to use to parse notes",
},
}
export const PluginInstallArgv = {
...CommonArgv,
_: {
type: "string",
demandOption: true,
describe: "package names to install",
},
}
export const PluginUninstallArgv = {
...CommonArgv,
_: {
type: "string",
demandOption: true,
describe: "package names to uninstall",
},
}
export const PluginSearchArgv = {
...CommonArgv,
query: {
string: true,
alias: ["q"],
describe: "search query for plugins",
},
}
+2 -1
View File
@@ -6,7 +6,8 @@ import { readFileSync } from "fs"
*/
export const ORIGIN_NAME = "origin"
export const UPSTREAM_NAME = "upstream"
export const QUARTZ_SOURCE_BRANCH = "v4"
export const QUARTZ_SOURCE_BRANCH = "v5"
export const QUARTZ_SOURCE_REPO = "https://github.com/jackyzha0/quartz.git"
export const cwd = process.cwd()
export const cacheDir = path.join(cwd, ".quartz-cache")
export const cacheFile = "./quartz/.quartz-cache/transpiled-build.mjs"
+135 -22
View File
@@ -23,9 +23,24 @@ import {
popContentFolder,
stashContentFolder,
} from "./helpers.js"
import {
handlePluginRestore,
handlePluginCheck,
handlePluginUpdate,
} from "./plugin-git-handlers.js"
import {
configExists,
createConfigFromDefault,
createConfigFromTemplate,
readPluginsJson,
writePluginsJson,
extractPluginName,
updateGlobalConfig,
} from "./plugin-data.js"
import {
UPSTREAM_NAME,
QUARTZ_SOURCE_BRANCH,
QUARTZ_SOURCE_REPO,
ORIGIN_NAME,
version,
fp,
@@ -53,6 +68,8 @@ export async function handleCreate(argv) {
let setupStrategy = argv.strategy?.toLowerCase()
let linkResolutionStrategy = argv.links?.toLowerCase()
const sourceDirectory = argv.source
let template = argv.template?.toLowerCase()
let baseUrl = argv.baseUrl
// If all cmd arguments were provided, check if they're valid
if (setupStrategy && linkResolutionStrategy) {
@@ -104,6 +121,32 @@ export async function handleCreate(argv) {
}
}
// Template selection
if (!template) {
template = exitIfCancel(
await select({
message: "Choose a template for your Quartz configuration",
options: [
{ value: "default", label: "Default", hint: "clean Quartz setup with sensible defaults" },
{
value: "obsidian",
label: "Obsidian",
hint: "optimized for Obsidian vaults with full OFM support",
},
{
value: "ttrpg",
label: "TTRPG",
hint: "Obsidian + map plugin + ITS Theme for D&D/TTRPG wikis",
},
{
value: "blog",
label: "Blog",
hint: "recent notes and comments enabled for blogging",
},
],
}),
)
}
// Use cli process if cmd args werent provided
if (!setupStrategy) {
setupStrategy = exitIfCancel(
@@ -181,12 +224,18 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
)
}
// Obsidian and TTRPG templates auto-set link resolution to "shortest"
const skipLinkPrompt = template === "obsidian" || template === "ttrpg"
if (skipLinkPrompt) {
linkResolutionStrategy = "shortest"
}
// Use cli process if cmd args werent provided
if (!linkResolutionStrategy) {
// get a preferred link resolution strategy
linkResolutionStrategy = exitIfCancel(
await select({
message: `Choose how Quartz should resolve links in your content. This should match Obsidian's link format. You can change this later in \`quartz.config.ts\`.`,
message: `Choose how Quartz should resolve links in your content. This should match Obsidian's link format. You can change this later in \`quartz.config.yaml\`.`,
options: [
{
value: "shortest",
@@ -206,23 +255,60 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
)
}
// now, do config changes
const configFilePath = path.join(cwd, "quartz.config.ts")
let configContent = await fs.promises.readFile(configFilePath, { encoding: "utf-8" })
configContent = configContent.replace(
/markdownLinkResolution: '(.+)'/,
`markdownLinkResolution: '${linkResolutionStrategy}'`,
)
await fs.promises.writeFile(configFilePath, configContent)
// Base URL prompt
if (!baseUrl) {
baseUrl = exitIfCancel(
await text({
message: "Enter the base URL for your Quartz site (e.g. mysite.github.io/quartz)",
placeholder: "mysite.github.io",
validate(value) {
if (!value || value.trim().length === 0) {
return "Base URL cannot be empty"
}
},
}),
)
}
// Strip protocol prefix if user included it
baseUrl = baseUrl.replace(/^https?:\/\//, "").replace(/\/+$/, "")
// Create config if it doesn't exist
if (!configExists()) {
if (template && template !== "default") {
createConfigFromTemplate(template)
console.log(styleText("green", `Created quartz.config.yaml from '${template}' template`))
} else {
createConfigFromTemplate("default")
console.log(styleText("green", "Created quartz.config.yaml from defaults"))
}
}
// Update markdownLinkResolution in the crawl-links plugin options via YAML config
const json = readPluginsJson()
if (json?.plugins) {
const crawlLinksIndex = json.plugins.findIndex(
(p) => extractPluginName(p.source) === "crawl-links",
)
if (crawlLinksIndex !== -1) {
json.plugins[crawlLinksIndex].options = {
...json.plugins[crawlLinksIndex].options,
markdownLinkResolution: linkResolutionStrategy,
}
writePluginsJson(json)
}
}
// Update baseUrl in configuration
updateGlobalConfig({ baseUrl })
// setup remote
execSync(
`git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`,
{ stdio: "ignore" },
)
execSync(`git remote show upstream || git remote add upstream ${QUARTZ_SOURCE_REPO}`, {
stdio: "ignore",
})
outro(`You're all set! Not sure what to do next? Try:
• Customizing Quartz a bit more by editing \`quartz.config.ts\`
• Customizing Quartz a bit more by editing \`quartz.config.yaml\`
• Running \`npx quartz build --serve\` to preview your Quartz locally
• Hosting your Quartz online (see: https://quartz.jzhao.xyz/hosting)
`)
@@ -487,16 +573,15 @@ export async function handleBuild(argv) {
}
/**
* Handles `npx quartz update`
* @param {*} argv arguments for `update`
* Handles `npx quartz upgrade`
* Upgrades the Quartz framework itself by pulling latest changes from upstream.
* @param {*} argv arguments for `upgrade`
*/
export async function handleUpdate(argv) {
export async function handleUpgrade(argv) {
const contentFolder = resolveContentPath(argv.directory)
console.log(`\n${styleText(["bgGreen", "black"], ` Quartz v${version} `)} \n`)
console.log("Backing up your content")
execSync(
`git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`,
)
execSync(`git remote show upstream || git remote add upstream ${QUARTZ_SOURCE_REPO}`)
await stashContentFolder(contentFolder)
console.log(
"Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.",
@@ -511,6 +596,16 @@ export async function handleUpdate(argv) {
}
await popContentFolder(contentFolder)
// Read the new version after pulling
const newPkg = JSON.parse(fs.readFileSync("./package.json").toString())
const newVersion = newPkg.version
if (newVersion !== version) {
console.log(styleText("cyan", `Upgraded Quartz: v${version} → v${newVersion}`))
} else {
console.log(styleText("gray", `Quartz is already up to date (v${version})`))
}
console.log("Ensuring dependencies are up to date")
/*
@@ -518,7 +613,7 @@ export async function handleUpdate(argv) {
as it will be unable to find `npm`. This is often the case on systems
where `npm` is installed via a package manager.
This means `npx quartz update` will not actually update dependencies
This means `npx quartz upgrade` will not actually update dependencies
on Windows, without a manual `npm i` from the caller.
However, by spawning a shell, we are able to call `npm.cmd`.
@@ -532,10 +627,28 @@ export async function handleUpdate(argv) {
const res = spawnSync("npm", ["i"], opts)
if (res.status === 0) {
console.log(styleText("green", "Done!"))
console.log(styleText("green", "Dependencies updated!"))
} else {
console.log(styleText("red", "An error occurred above while installing dependencies."))
}
console.log("Restoring plugins from lockfile...")
await handlePluginRestore()
console.log("Checking plugin compatibility...")
await handlePluginCheck()
console.log(styleText("green", "Done!"))
}
/**
* Handles `npx quartz update`
* Shortcut for `npx quartz plugin update` — updates all installed plugins.
* @param {*} argv arguments for `update`
*/
export async function handleUpdate(argv) {
console.log(`\n${styleText(["bgGreen", "black"], ` Quartz v${version} `)} \n`)
await handlePluginUpdate(argv.names)
}
/**
+1 -1
View File
@@ -33,7 +33,7 @@ export async function stashContentFolder(contentFolder) {
}
export function gitPull(origin, branch) {
const flags = ["--no-rebase", "--autostash", "-s", "recursive", "-X", "ours", "--no-edit"]
const flags = ["--no-rebase", "--autostash", "--no-edit"]
const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" })
if (out.stderr) {
throw new Error(styleText("red", `Error while pulling updates: ${out.stderr}`))
+191
View File
@@ -0,0 +1,191 @@
import fs from "fs"
import path from "path"
import { spawnSync } from "child_process"
import { styleText } from "util"
import YAML from "yaml"
const CWD = process.cwd()
const QUARTZ_TS_PATH = path.join(CWD, "quartz.ts")
const CONFIG_YAML_PATH = path.join(CWD, "quartz.config.yaml")
const DEFAULT_CONFIG_YAML_PATH = path.join(CWD, "quartz.config.default.yaml")
const LEGACY_DEFAULT_JSON_PATH = path.join(CWD, "quartz.plugins.default.json")
const LOCKFILE_PATH = path.join(CWD, "quartz.lock.json")
const PLUGINS_DIR = path.join(CWD, ".quartz", "plugins")
const PACKAGE_JSON_PATH = path.join(CWD, "package.json")
function readJson(filePath) {
if (!fs.existsSync(filePath)) return null
try {
return JSON.parse(fs.readFileSync(filePath, "utf-8"))
} catch {
return null
}
}
function hasTsx() {
const pkg = readJson(PACKAGE_JSON_PATH)
return Boolean(pkg?.devDependencies?.tsx || pkg?.dependencies?.tsx)
}
function extractWithTsx() {
const script = `
const { default: config } = await import("./quartz.ts")
const { layout } = await import("./quartz.ts")
const result = {
configuration: config?.configuration ?? null,
layoutInfo: {
defaults: {
afterBody: Array.isArray(layout?.defaults?.afterBody) ? layout.defaults.afterBody.length : 0,
hasFooter: Boolean(layout?.defaults?.footer),
},
pageTypes: layout?.byPageType ? Object.keys(layout.byPageType) : [],
},
}
console.log(JSON.stringify(result))
`
const res = spawnSync("node", ["--import", "tsx/esm", "--input-type=module", "-e", script], {
encoding: "utf-8",
cwd: CWD,
})
if (res.error || res.status !== 0) {
return { ok: false, error: res.error ?? res.stderr }
}
try {
return { ok: true, data: JSON.parse(res.stdout.trim()) }
} catch (error) {
return { ok: false, error }
}
}
function readManifest(pluginDir) {
const pkgPath = path.join(pluginDir, "package.json")
if (!fs.existsSync(pkgPath)) return null
try {
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
return pkg.quartz ?? null
} catch {
return null
}
}
function ensureLayoutDefaults(layout) {
if (!layout.groups) layout.groups = {}
if (!layout.groups.toolbar) {
layout.groups.toolbar = { direction: "row", gap: "0.5rem" }
}
if (!layout.byPageType) layout.byPageType = {}
if (!layout.byPageType["404"]) {
layout.byPageType["404"] = { positions: { beforeBody: [], left: [], right: [] } }
} else if (!layout.byPageType["404"].positions) {
layout.byPageType["404"].positions = { beforeBody: [], left: [], right: [] }
}
return layout
}
function buildPluginEntry(name, entry) {
const pluginDir = path.join(PLUGINS_DIR, name)
const manifest = readManifest(pluginDir)
const source = entry?.source ?? `github:quartz-community/${name}`
const pluginEntry = {
source,
enabled: manifest?.defaultEnabled ?? true,
options: manifest?.defaultOptions ?? {},
order: manifest?.defaultOrder ?? 50,
}
if (manifest?.components) {
const component = Object.values(manifest.components).find((comp) => comp?.defaultPosition)
if (component?.defaultPosition) {
pluginEntry.layout = {
position: component.defaultPosition,
priority: component.defaultPriority ?? 50,
display: "all",
}
}
}
return pluginEntry
}
export async function handleMigrate() {
console.log(styleText("cyan", "Migrating Quartz configuration..."))
if (!fs.existsSync(QUARTZ_TS_PATH)) {
console.log(styleText("red", "✗ quartz.ts not found. Aborting migration."))
return
}
if (fs.existsSync(CONFIG_YAML_PATH)) {
console.log(styleText("yellow", "⚠ quartz.config.yaml already exists. Overwriting."))
}
const defaultJson = readJson(DEFAULT_CONFIG_YAML_PATH) ?? readJson(LEGACY_DEFAULT_JSON_PATH)
let configuration = defaultJson?.configuration ?? {}
let layout = ensureLayoutDefaults(defaultJson?.layout ?? {})
let layoutInfo = null
console.log(styleText("gray", "→ Extracting configuration..."))
if (hasTsx()) {
const extracted = extractWithTsx()
if (extracted.ok) {
configuration = extracted.data?.configuration ?? configuration
layoutInfo = extracted.data?.layoutInfo ?? null
} else {
console.log(styleText("yellow", "⚠ Failed to import TS config with tsx. Using defaults."))
}
} else {
console.log(styleText("yellow", "⚠ tsx not found. Using defaults."))
}
if (layoutInfo?.pageTypes?.length) {
for (const pageType of layoutInfo.pageTypes) {
if (!layout.byPageType[pageType]) {
layout.byPageType[pageType] = {}
}
}
}
console.log(styleText("gray", "→ Reading plugin lockfile..."))
const lockfile = readJson(LOCKFILE_PATH)
const plugins = []
if (lockfile?.plugins) {
for (const [name, entry] of Object.entries(lockfile.plugins)) {
plugins.push(buildPluginEntry(name, entry))
}
} else if (defaultJson?.plugins) {
console.log(styleText("yellow", "⚠ quartz.lock.json not found. Using default plugins."))
for (const plugin of defaultJson.plugins) {
plugins.push(plugin)
}
} else {
console.log(styleText("yellow", "⚠ No lockfile or default plugins found. Writing empty list."))
}
const outputJson = {
configuration,
plugins,
layout,
}
const header = "# yaml-language-server: $schema=./quartz/plugins/quartz-plugins.schema.json\n"
fs.writeFileSync(CONFIG_YAML_PATH, header + YAML.stringify(outputJson, { lineWidth: 120 }))
const quartzTsTemplate =
'import { loadQuartzConfig, loadQuartzLayout } from "./quartz/plugins/loader/config-loader"\n' +
"\n" +
"const config = await loadQuartzConfig()\n" +
"export default config\n" +
"export const layout = await loadQuartzLayout()\n"
fs.writeFileSync(QUARTZ_TS_PATH, quartzTsTemplate)
console.log(styleText("green", "✓ Created quartz.config.yaml"))
console.log(styleText("green", "✓ Replaced quartz.ts"))
console.log()
console.log(styleText("yellow", "⚠ Verify plugin options in quartz.config.yaml"))
console.log(styleText("gray", `Plugins migrated: ${plugins.length}`))
}
+358
View File
@@ -0,0 +1,358 @@
import fs from "fs"
import path from "path"
import { execSync } from "child_process"
import YAML from "yaml"
const LOCKFILE_PATH = path.join(process.cwd(), "quartz.lock.json")
const PLUGINS_DIR = path.join(process.cwd(), ".quartz", "plugins")
const CONFIG_YAML_PATH = path.join(process.cwd(), "quartz.config.yaml")
const DEFAULT_CONFIG_YAML_PATH = path.join(process.cwd(), "quartz.config.default.yaml")
const TEMPLATES_DIR = path.join(process.cwd(), "quartz", "cli", "templates")
const LEGACY_PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.json")
const LEGACY_DEFAULT_PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.default.json")
function resolveConfigPath() {
if (fs.existsSync(CONFIG_YAML_PATH)) return CONFIG_YAML_PATH
if (fs.existsSync(LEGACY_PLUGINS_JSON_PATH)) return LEGACY_PLUGINS_JSON_PATH
if (fs.existsSync(DEFAULT_CONFIG_YAML_PATH)) return DEFAULT_CONFIG_YAML_PATH
if (fs.existsSync(LEGACY_DEFAULT_PLUGINS_JSON_PATH)) return LEGACY_DEFAULT_PLUGINS_JSON_PATH
return CONFIG_YAML_PATH
}
function resolveDefaultConfigPath() {
if (fs.existsSync(DEFAULT_CONFIG_YAML_PATH)) return DEFAULT_CONFIG_YAML_PATH
if (fs.existsSync(LEGACY_DEFAULT_PLUGINS_JSON_PATH)) return LEGACY_DEFAULT_PLUGINS_JSON_PATH
return DEFAULT_CONFIG_YAML_PATH
}
function readFileAsData(filePath) {
if (!fs.existsSync(filePath)) return null
try {
const raw = fs.readFileSync(filePath, "utf-8")
if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
return YAML.parse(raw)
}
return JSON.parse(raw)
} catch {
return null
}
}
function writeDataToFile(filePath, data) {
if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
const header = "# yaml-language-server: $schema=./quartz/plugins/quartz-plugins.schema.json\n"
fs.writeFileSync(filePath, header + YAML.stringify(data, { lineWidth: 120 }))
} else {
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n")
}
}
export function readPluginsJson() {
const configPath = resolveConfigPath()
return readFileAsData(configPath)
}
export function writePluginsJson(data) {
const { $schema, ...rest } = data
writeDataToFile(CONFIG_YAML_PATH, rest)
}
export function readDefaultPluginsJson() {
const defaultPath = resolveDefaultConfigPath()
return readFileAsData(defaultPath)
}
export function readLockfile() {
if (!fs.existsSync(LOCKFILE_PATH)) return null
try {
return JSON.parse(fs.readFileSync(LOCKFILE_PATH, "utf-8"))
} catch {
return null
}
}
export function writeLockfile(lockfile) {
if (lockfile.plugins) {
const sorted = {}
for (const key of Object.keys(lockfile.plugins).sort()) {
sorted[key] = lockfile.plugins[key]
}
lockfile = { ...lockfile, plugins: sorted }
}
fs.writeFileSync(LOCKFILE_PATH, JSON.stringify(lockfile, null, 2) + "\n")
}
export function isLocalSource(source) {
if (source.startsWith("./") || source.startsWith("../") || source.startsWith("/")) {
return true
}
// Windows absolute paths (e.g. C:\ or D:/)
if (/^[A-Za-z]:[\\/]/.test(source)) {
return true
}
return false
}
export function extractPluginName(source) {
if (isLocalSource(source)) {
return path.basename(source.replace(/[\/]+$/, ""))
}
if (source.startsWith("github:")) {
const withoutPrefix = source.replace("github:", "")
const [repoPath] = withoutPrefix.split("#")
const parts = repoPath.split("/")
return parts[parts.length - 1]
}
if (source.startsWith("git+") || source.startsWith("https://")) {
const url = source.replace("git+", "")
const match = url.match(/\/([^/]+?)(?:\.git)?(?:#|$)/)
return match?.[1] ?? source
}
return source
}
export function readManifestFromPackageJson(pluginDir) {
const pkgPath = path.join(pluginDir, "package.json")
if (!fs.existsSync(pkgPath)) return null
try {
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
return pkg.quartz ?? null
} catch {
return null
}
}
export function parseGitSource(source) {
if (isLocalSource(source)) {
const resolved = path.resolve(source)
const name = path.basename(resolved)
return { name, url: resolved, ref: undefined, local: true }
}
if (source.startsWith("github:")) {
const [repoPath, ref] = source.replace("github:", "").split("#")
const [owner, repo] = repoPath.split("/")
return { name: repo, url: `https://github.com/${owner}/${repo}.git`, ref }
}
if (source.startsWith("git+")) {
const raw = source.replace("git+", "")
const [url, ref] = raw.split("#")
const name = path.basename(url, ".git")
return { name, url, ref }
}
if (source.startsWith("https://")) {
const [url, ref] = source.split("#")
const name = path.basename(url, ".git")
return { name, url, ref }
}
throw new Error(`Cannot parse plugin source: ${source}`)
}
export function getGitCommit(pluginDir) {
try {
return execSync("git rev-parse HEAD", { cwd: pluginDir, encoding: "utf-8" }).trim()
} catch {
return "unknown"
}
}
export function getPluginDir(name) {
return path.join(PLUGINS_DIR, name)
}
export function pluginDirExists(name) {
return fs.existsSync(path.join(PLUGINS_DIR, name))
}
export function ensurePluginsDir() {
if (!fs.existsSync(PLUGINS_DIR)) {
fs.mkdirSync(PLUGINS_DIR, { recursive: true })
}
}
/**
* Merges quartz.config.yaml, quartz.lock.json, and on-disk manifest data
* into enriched plugin entries with: name, displayName, source, enabled,
* options, order, layout, category, installed, locked, manifest,
* currentCommit, modified.
*/
export function getEnrichedPlugins() {
const pluginsJson = readPluginsJson()
const lockfile = readLockfile()
if (!pluginsJson?.plugins) return []
return pluginsJson.plugins.map((entry, index) => {
const name = extractPluginName(entry.source)
const pluginDir = path.join(PLUGINS_DIR, name)
const installed = fs.existsSync(pluginDir)
const locked = lockfile?.plugins?.[name] ?? null
const manifest = installed ? readManifestFromPackageJson(pluginDir) : null
const currentCommit = installed ? getGitCommit(pluginDir) : null
const modified = locked && currentCommit ? currentCommit !== locked.commit : false
return {
index,
name,
displayName: manifest?.displayName ?? name,
source: entry.source,
enabled: entry.enabled ?? true,
options: entry.options ?? {},
order: entry.order ?? 50,
layout: entry.layout ?? null,
category: manifest?.category ?? "unknown",
installed,
locked,
manifest,
currentCommit,
modified,
}
})
}
export function getLayoutConfig() {
const pluginsJson = readPluginsJson()
return pluginsJson?.layout ?? null
}
export function getGlobalConfig() {
const pluginsJson = readPluginsJson()
return pluginsJson?.configuration ?? null
}
export function updatePluginEntry(index, updates) {
const json = readPluginsJson()
if (!json?.plugins?.[index]) return false
Object.assign(json.plugins[index], updates)
writePluginsJson(json)
return true
}
export function updateGlobalConfig(updates) {
const json = readPluginsJson()
if (!json) return false
json.configuration = { ...json.configuration, ...updates }
writePluginsJson(json)
return true
}
export function updateLayoutConfig(layout) {
const json = readPluginsJson()
if (!json) return false
json.layout = layout
writePluginsJson(json)
return true
}
export function reorderPlugin(fromIndex, toIndex) {
const json = readPluginsJson()
if (!json?.plugins) return false
const [moved] = json.plugins.splice(fromIndex, 1)
json.plugins.splice(toIndex, 0, moved)
writePluginsJson(json)
return true
}
export function removePluginEntry(index) {
const json = readPluginsJson()
if (!json?.plugins?.[index]) return false
json.plugins.splice(index, 1)
writePluginsJson(json)
return true
}
export function addPluginEntry(entry) {
const json = readPluginsJson()
if (!json) return false
if (!json.plugins) json.plugins = []
json.plugins.push(entry)
writePluginsJson(json)
return true
}
export function configExists() {
return fs.existsSync(CONFIG_YAML_PATH) || fs.existsSync(LEGACY_PLUGINS_JSON_PATH)
}
export function createConfigFromDefault() {
const defaultData = readDefaultPluginsJson()
if (!defaultData) {
// No default available — create minimal config
const minimal = {
configuration: {
pageTitle: "Quartz",
enableSPA: true,
enablePopovers: true,
analytics: { provider: "plausible" },
locale: "en-US",
baseUrl: "quartz.jzhao.xyz",
ignorePatterns: ["private", "templates", ".obsidian"],
defaultDateType: "created",
theme: {
cdnCaching: true,
typography: {
header: "Schibsted Grotesk",
body: "Source Sans Pro",
code: "IBM Plex Mono",
},
colors: {
lightMode: {
light: "#faf8f8",
lightgray: "#e5e5e5",
gray: "#b8b8b8",
darkgray: "#4e4e4e",
dark: "#2b2b2b",
secondary: "#284b63",
tertiary: "#84a59d",
highlight: "rgba(143, 159, 169, 0.15)",
textHighlight: "#fff23688",
},
darkMode: {
light: "#161618",
lightgray: "#393639",
gray: "#646464",
darkgray: "#d4d4d4",
dark: "#ebebec",
secondary: "#7b97aa",
tertiary: "#84a59d",
highlight: "rgba(143, 159, 169, 0.15)",
textHighlight: "#fff23688",
},
},
},
},
plugins: [],
layout: { groups: {}, byPageType: {} },
}
writePluginsJson(minimal)
return minimal
}
const { $schema, ...rest } = defaultData
writePluginsJson(rest)
return rest
}
const VALID_TEMPLATES = ["default", "obsidian", "ttrpg", "blog"]
export function createConfigFromTemplate(templateName) {
if (!VALID_TEMPLATES.includes(templateName)) {
throw new Error(
`Unknown template: ${templateName}. Valid templates: ${VALID_TEMPLATES.join(", ")}`,
)
}
const templatePath = path.join(TEMPLATES_DIR, `${templateName}.yaml`)
const templateData = readFileAsData(templatePath)
if (!templateData) {
// Template file missing — fall back to default config creation
return createConfigFromDefault()
}
const { $schema, ...rest } = templateData
writePluginsJson(rest)
return rest
}
export const PLUGINS_JSON_PATH = CONFIG_YAML_PATH
export const DEFAULT_PLUGINS_JSON_PATH = DEFAULT_CONFIG_YAML_PATH
export { LOCKFILE_PATH, PLUGINS_DIR }
File diff suppressed because it is too large Load Diff
+130
View File
@@ -0,0 +1,130 @@
import { styleText } from "util"
import { execSync, spawnSync } from "child_process"
import fs from "fs"
import path from "path"
export async function handlePluginInstall(packageNames) {
console.log(`\n${styleText(["bgGreen", "black"], " Quartz Plugin Manager ")}\n`)
if (packageNames.length === 0) {
console.log(styleText("red", "Error: No package names provided"))
console.log("Usage: npx quartz plugin install <package-name> [package-name...]")
process.exit(1)
}
console.log(`Installing ${packageNames.length} plugin(s)...`)
const npmArgs = ["install", ...packageNames]
const result = spawnSync("npm", npmArgs, { stdio: "inherit" })
if (result.status !== 0) {
console.log(styleText("red", "Failed to install plugins"))
process.exit(1)
}
console.log(styleText("green", "✓ Plugins installed successfully"))
console.log("\nAdd them to your quartz.config.yaml:")
for (const pkg of packageNames) {
console.log(` import { Plugin } from "${pkg}"`)
}
}
export async function handlePluginList() {
console.log(`\n${styleText(["bgGreen", "black"], " Quartz Plugin Manager ")}\n`)
const packageJsonPath = path.join(process.cwd(), "package.json")
if (!fs.existsSync(packageJsonPath)) {
console.log(styleText("red", "No package.json found"))
process.exit(1)
}
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
const allDeps = {
...packageJson.dependencies,
...packageJson.devDependencies,
}
const quartzPlugins = Object.entries(allDeps).filter(([name]) => {
return (
name.startsWith("@quartz/") ||
name.startsWith("quartz-") ||
name.startsWith("@quartz-community/")
)
})
if (quartzPlugins.length === 0) {
console.log("No Quartz plugins found in this project.")
console.log("Install plugins with: npx quartz plugin install <package-name>")
return
}
console.log(`Found ${quartzPlugins.length} Quartz plugin(s):\n`)
for (const [name, version] of quartzPlugins) {
console.log(` ${styleText("cyan", name)}@${version}`)
}
}
export async function handlePluginSearch(query) {
console.log(`\n${styleText(["bgGreen", "black"], " Quartz Plugin Manager ")}\n`)
const searchQuery = query || "quartz-plugin"
console.log(`Searching npm for packages matching "${searchQuery}"...`)
console.log(styleText("grey", "(This may take a moment)\n"))
try {
const result = execSync(`npm search ${searchQuery} --json`, { encoding: "utf-8" })
const packages = JSON.parse(result)
const quartzPlugins = packages.filter(
(pkg) =>
pkg.name.startsWith("@quartz/") ||
pkg.name.startsWith("quartz-") ||
pkg.name.startsWith("@quartz-community/"),
)
if (quartzPlugins.length === 0) {
console.log("No Quartz plugins found matching your query.")
return
}
console.log(`Found ${quartzPlugins.length} Quartz plugin(s):\n`)
for (const pkg of quartzPlugins.slice(0, 20)) {
console.log(` ${styleText("cyan", pkg.name)}@${pkg.version}`)
if (pkg.description) {
console.log(` ${styleText("grey", pkg.description)}`)
}
console.log()
}
} catch {
console.log(styleText("yellow", "Could not search npm. Try visiting:"))
console.log(" https://www.npmjs.com/search?q=quartz-plugin")
}
}
export async function handlePluginUninstall(packageNames) {
console.log(`\n${styleText(["bgGreen", "black"], " Quartz Plugin Manager ")}\n`)
if (packageNames.length === 0) {
console.log(styleText("red", "Error: No package names provided"))
console.log("Usage: npx quartz plugin uninstall <package-name> [package-name...]")
process.exit(1)
}
console.log(`Uninstalling ${packageNames.length} plugin(s)...`)
const npmArgs = ["uninstall", ...packageNames]
const result = spawnSync("npm", npmArgs, { stdio: "inherit" })
if (result.status !== 0) {
console.log(styleText("red", "Failed to uninstall plugins"))
process.exit(1)
}
console.log(styleText("green", "✓ Plugins uninstalled successfully"))
console.log(styleText("yellow", "Don't forget to remove them from your quartz.config.yaml!"))
}
+282
View File
@@ -0,0 +1,282 @@
# yaml-language-server: $schema=../../plugins/quartz-plugins.schema.json
# Template: blog
# A blog-focused setup with recent notes and comments enabled.
configuration:
pageTitle: Quartz 5
pageTitleSuffix: ""
enableSPA: true
enablePopovers: true
analytics:
provider: plausible
locale: en-US
baseUrl: quartz.jzhao.xyz
ignorePatterns:
- private
- templates
- .obsidian
defaultDateType: modified
theme:
fontOrigin: googleFonts
cdnCaching: true
typography:
header: Schibsted Grotesk
body: Source Sans Pro
code: IBM Plex Mono
colors:
lightMode:
light: "#faf8f8"
lightgray: "#e5e5e5"
gray: "#b8b8b8"
darkgray: "#4e4e4e"
dark: "#2b2b2b"
secondary: "#284b63"
tertiary: "#84a59d"
highlight: rgba(143, 159, 169, 0.15)
textHighlight: "#fff23688"
darkMode:
light: "#161618"
lightgray: "#393639"
gray: "#646464"
darkgray: "#d4d4d4"
dark: "#ebebec"
secondary: "#7b97aa"
tertiary: "#84a59d"
highlight: rgba(143, 159, 169, 0.15)
textHighlight: "#b3aa0288"
plugins:
- source: github:quartz-community/created-modified-date
enabled: true
options:
priority:
- frontmatter
- git
- filesystem
order: 10
- source: github:quartz-community/syntax-highlighting
enabled: true
options:
theme:
light: github-light
dark: github-dark
keepBackground: false
order: 20
- source: github:quartz-community/obsidian-flavored-markdown
enabled: true
options:
enableInHtmlEmbed: false
enableCheckbox: true
order: 30
- source: github:quartz-community/github-flavored-markdown
enabled: true
order: 40
- source: github:quartz-community/table-of-contents
enabled: true
order: 50
- source: github:quartz-community/crawl-links
enabled: true
options:
markdownLinkResolution: shortest
order: 60
- source: github:quartz-community/description
enabled: true
order: 70
- source: github:quartz-community/latex
enabled: true
options:
renderEngine: katex
order: 80
- source: github:quartz-community/citations
enabled: false
order: 85
- source: github:quartz-community/hard-line-breaks
enabled: false
order: 90
- source: github:quartz-community/ox-hugo
enabled: false
order: 91
- source: github:quartz-community/roam
enabled: false
order: 92
- source: github:quartz-community/remove-draft
enabled: true
- source: github:quartz-community/explicit-publish
enabled: false
- source: github:quartz-community/encrypted-pages
enabled: false
- source: github:quartz-community/stacked-pages
enabled: false
layout:
position: afterBody
priority: 50
display: all
- source: github:quartz-community/alias-redirects
enabled: true
- source: github:quartz-community/content-index
enabled: true
options:
enableSiteMap: true
enableRSS: true
- source: github:quartz-community/favicon
enabled: true
- source: github:quartz-community/og-image
enabled: true
- source: github:quartz-community/cname
enabled: true
- source: github:quartz-community/canvas-page
enabled: true
- source: github:quartz-community/content-page
enabled: true
- source: github:quartz-community/folder-page
enabled: true
- source: github:quartz-community/tag-page
enabled: true
- source: github:quartz-community/explorer
enabled: true
layout:
position: left
priority: 50
- source: github:quartz-community/graph
enabled: true
layout:
position: right
priority: 10
- source: github:quartz-community/search
enabled: true
layout:
position: left
priority: 20
group: toolbar
groupOptions:
grow: true
- source: github:quartz-community/backlinks
enabled: true
layout:
position: right
priority: 30
- source: github:quartz-community/article-title
enabled: true
layout:
position: beforeBody
priority: 10
- source: github:quartz-community/content-meta
enabled: true
layout:
position: beforeBody
priority: 20
- source: github:quartz-community/tag-list
enabled: false
layout:
position: beforeBody
priority: 30
- source: github:quartz-community/page-title
enabled: true
layout:
position: left
priority: 10
- source: github:quartz-community/darkmode
enabled: true
layout:
position: left
priority: 30
group: toolbar
- source: github:quartz-community/reader-mode
enabled: true
layout:
position: left
priority: 35
group: toolbar
- source: github:quartz-community/breadcrumbs
enabled: true
layout:
position: beforeBody
priority: 5
condition: not-index
- source: github:quartz-community/comments
enabled: true
options:
provider: giscus
options:
repo: "TODO:username/repo-name"
repoId: "TODO:your-repo-id"
category: Announcements
categoryId: "TODO:your-category-id"
mapping: url
strict: true
reactionsEnabled: true
inputPosition: bottom
lightTheme: light
darkTheme: dark
lang: en
layout:
position: afterBody
priority: 10
- source: github:quartz-community/footer
enabled: true
options:
links:
GitHub: https://github.com/jackyzha0/quartz
Discord Community: https://discord.gg/cRFFHYye7t
- source: github:quartz-community/recent-notes
enabled: true
options:
title: Recent Notes
limit: 5
linkToMore: false
showTags: true
layout:
position: left
priority: 25
- source: github:quartz-community/spacer
enabled: true
options: {}
order: 25
layout:
position: left
priority: 25
display: mobile-only
- source: github:quartz-community/bases-page
enabled: true
options: {}
order: 50
- source: github:quartz-community/note-properties
enabled: true
options:
includeAll: false
includedProperties:
- description
- tags
- aliases
excludedProperties: []
hidePropertiesView: false
delimiters: "---"
language: yaml
order: 5
layout:
position: beforeBody
priority: 15
display: all
layout:
groups:
toolbar:
priority: 35
direction: row
gap: 0.5rem
byPageType:
"404":
positions:
beforeBody: []
left: []
right: []
content: {}
folder:
exclude:
- reader-mode
positions:
right: []
tag:
exclude:
- reader-mode
positions:
right: []
canvas: {}
bases: {}
+263
View File
@@ -0,0 +1,263 @@
# yaml-language-server: $schema=../../plugins/quartz-plugins.schema.json
# Template: default
# A clean Quartz setup with sensible defaults.
configuration:
pageTitle: Quartz 5
pageTitleSuffix: ""
enableSPA: true
enablePopovers: true
analytics:
provider: plausible
locale: en-US
baseUrl: quartz.jzhao.xyz
ignorePatterns:
- private
- templates
- .obsidian
defaultDateType: modified
theme:
fontOrigin: googleFonts
cdnCaching: true
typography:
header: Schibsted Grotesk
body: Source Sans Pro
code: IBM Plex Mono
colors:
lightMode:
light: "#faf8f8"
lightgray: "#e5e5e5"
gray: "#b8b8b8"
darkgray: "#4e4e4e"
dark: "#2b2b2b"
secondary: "#284b63"
tertiary: "#84a59d"
highlight: rgba(143, 159, 169, 0.15)
textHighlight: "#fff23688"
darkMode:
light: "#161618"
lightgray: "#393639"
gray: "#646464"
darkgray: "#d4d4d4"
dark: "#ebebec"
secondary: "#7b97aa"
tertiary: "#84a59d"
highlight: rgba(143, 159, 169, 0.15)
textHighlight: "#b3aa0288"
plugins:
- source: github:quartz-community/created-modified-date
enabled: true
options:
priority:
- frontmatter
- git
- filesystem
order: 10
- source: github:quartz-community/syntax-highlighting
enabled: true
options:
theme:
light: github-light
dark: github-dark
keepBackground: false
order: 20
- source: github:quartz-community/obsidian-flavored-markdown
enabled: true
options:
enableInHtmlEmbed: false
enableCheckbox: true
order: 30
- source: github:quartz-community/github-flavored-markdown
enabled: true
order: 40
- source: github:quartz-community/table-of-contents
enabled: true
order: 50
- source: github:quartz-community/crawl-links
enabled: true
options:
markdownLinkResolution: shortest
order: 60
- source: github:quartz-community/description
enabled: true
order: 70
- source: github:quartz-community/latex
enabled: true
options:
renderEngine: katex
order: 80
- source: github:quartz-community/citations
enabled: false
order: 85
- source: github:quartz-community/hard-line-breaks
enabled: false
order: 90
- source: github:quartz-community/ox-hugo
enabled: false
order: 91
- source: github:quartz-community/roam
enabled: false
order: 92
- source: github:quartz-community/remove-draft
enabled: true
- source: github:quartz-community/explicit-publish
enabled: false
- source: github:quartz-community/encrypted-pages
enabled: true
- source: github:quartz-community/stacked-pages
enabled: false
layout:
position: afterBody
priority: 50
display: all
- source: github:quartz-community/alias-redirects
enabled: true
- source: github:quartz-community/content-index
enabled: true
options:
enableSiteMap: true
enableRSS: true
- source: github:quartz-community/favicon
enabled: true
- source: github:quartz-community/og-image
enabled: true
- source: github:quartz-community/cname
enabled: true
- source: github:quartz-community/canvas-page
enabled: true
- source: github:quartz-community/content-page
enabled: true
- source: github:quartz-community/folder-page
enabled: true
- source: github:quartz-community/tag-page
enabled: true
- source: github:quartz-community/explorer
enabled: true
layout:
position: left
priority: 50
- source: github:quartz-community/graph
enabled: true
layout:
position: right
priority: 10
- source: github:quartz-community/search
enabled: true
layout:
position: left
priority: 20
group: toolbar
groupOptions:
grow: true
- source: github:quartz-community/backlinks
enabled: true
layout:
position: right
priority: 30
- source: github:quartz-community/article-title
enabled: true
layout:
position: beforeBody
priority: 10
- source: github:quartz-community/content-meta
enabled: true
layout:
position: beforeBody
priority: 20
- source: github:quartz-community/tag-list
enabled: false
layout:
position: beforeBody
priority: 30
- source: github:quartz-community/page-title
enabled: true
layout:
position: left
priority: 10
- source: github:quartz-community/darkmode
enabled: true
layout:
position: left
priority: 30
group: toolbar
- source: github:quartz-community/reader-mode
enabled: true
layout:
position: left
priority: 35
group: toolbar
- source: github:quartz-community/breadcrumbs
enabled: true
layout:
position: beforeBody
priority: 5
condition: not-index
- source: github:quartz-community/comments
enabled: false
options:
provider: giscus
options: {}
layout:
position: afterBody
priority: 10
- source: github:quartz-community/footer
enabled: true
options:
links:
GitHub: https://github.com/jackyzha0/quartz
Discord Community: https://discord.gg/cRFFHYye7t
- source: github:quartz-community/recent-notes
enabled: false
- source: github:quartz-community/spacer
enabled: true
options: {}
order: 25
layout:
position: left
priority: 25
display: mobile-only
- source: github:quartz-community/bases-page
enabled: true
options: {}
order: 50
- source: github:quartz-community/note-properties
enabled: true
options:
includeAll: false
includedProperties:
- description
- tags
- aliases
excludedProperties: []
hidePropertiesView: false
delimiters: "---"
language: yaml
order: 5
layout:
position: beforeBody
priority: 15
display: all
layout:
groups:
toolbar:
priority: 35
direction: row
gap: 0.5rem
byPageType:
"404":
positions:
beforeBody: []
left: []
right: []
content: {}
folder:
exclude:
- reader-mode
positions:
right: []
tag:
exclude:
- reader-mode
positions:
right: []
canvas: {}
bases: {}
+277
View File
@@ -0,0 +1,277 @@
# yaml-language-server: $schema=../../plugins/quartz-plugins.schema.json
# Template: obsidian
# Optimized for Obsidian vaults with full OFM support and shortest link resolution.
configuration:
pageTitle: Quartz 5
pageTitleSuffix: ""
enableSPA: true
enablePopovers: true
analytics:
provider: plausible
locale: en-US
baseUrl: quartz.jzhao.xyz
ignorePatterns:
- private
- templates
- .obsidian
defaultDateType: modified
theme:
fontOrigin: googleFonts
cdnCaching: true
typography:
header: Schibsted Grotesk
body: Source Sans Pro
code: IBM Plex Mono
colors:
lightMode:
light: "#faf8f8"
lightgray: "#e5e5e5"
gray: "#b8b8b8"
darkgray: "#4e4e4e"
dark: "#2b2b2b"
secondary: "#284b63"
tertiary: "#84a59d"
highlight: rgba(143, 159, 169, 0.15)
textHighlight: "#fff23688"
darkMode:
light: "#161618"
lightgray: "#393639"
gray: "#646464"
darkgray: "#d4d4d4"
dark: "#ebebec"
secondary: "#7b97aa"
tertiary: "#84a59d"
highlight: rgba(143, 159, 169, 0.15)
textHighlight: "#b3aa0288"
plugins:
- source: github:quartz-community/created-modified-date
enabled: true
options:
priority:
- frontmatter
- git
- filesystem
order: 10
- source: github:quartz-community/syntax-highlighting
enabled: true
options:
theme:
light: github-light
dark: github-dark
keepBackground: false
order: 20
- source: github:quartz-community/obsidian-flavored-markdown
enabled: true
options:
comments: true
highlight: true
wikilinks: true
callouts: true
mermaid: true
parseTags: true
parseArrows: true
parseBlockReferences: true
enableInHtmlEmbed: false
enableYouTubeEmbed: true
enableVideoEmbed: true
enableCheckbox: true
order: 30
- source: github:quartz-community/github-flavored-markdown
enabled: true
order: 40
- source: github:quartz-community/table-of-contents
enabled: true
order: 50
- source: github:quartz-community/crawl-links
enabled: true
options:
markdownLinkResolution: shortest
order: 60
- source: github:quartz-community/description
enabled: true
order: 70
- source: github:quartz-community/latex
enabled: true
options:
renderEngine: katex
order: 80
- source: github:quartz-community/citations
enabled: false
order: 85
- source: github:quartz-community/hard-line-breaks
enabled: true
order: 90
- source: github:quartz-community/ox-hugo
enabled: false
order: 91
- source: github:quartz-community/roam
enabled: false
order: 92
- source: github:quartz-community/remove-draft
enabled: true
- source: github:quartz-community/explicit-publish
enabled: false
- source: github:quartz-community/encrypted-pages
enabled: true
- source: github:quartz-community/stacked-pages
enabled: false
layout:
position: afterBody
priority: 50
display: all
- source: github:quartz-community/alias-redirects
enabled: true
- source: github:quartz-community/content-index
enabled: true
options:
enableSiteMap: true
enableRSS: true
- source: github:quartz-community/favicon
enabled: true
- source: github:quartz-community/og-image
enabled: true
- source: github:quartz-community/cname
enabled: true
- source: github:quartz-community/canvas-page
enabled: true
- source: github:quartz-community/content-page
enabled: true
- source: github:quartz-community/folder-page
enabled: true
- source: github:quartz-community/tag-page
enabled: true
- source: github:quartz-community/explorer
enabled: true
layout:
position: left
priority: 50
- source: github:quartz-community/graph
enabled: true
layout:
position: right
priority: 10
- source: github:quartz-community/search
enabled: true
layout:
position: left
priority: 20
group: toolbar
groupOptions:
grow: true
- source: github:quartz-community/backlinks
enabled: true
layout:
position: right
priority: 30
- source: github:quartz-community/article-title
enabled: true
layout:
position: beforeBody
priority: 10
- source: github:quartz-community/content-meta
enabled: true
layout:
position: beforeBody
priority: 20
- source: github:quartz-community/tag-list
enabled: false
layout:
position: beforeBody
priority: 30
- source: github:quartz-community/page-title
enabled: true
layout:
position: left
priority: 10
- source: github:quartz-community/darkmode
enabled: true
layout:
position: left
priority: 30
group: toolbar
- source: github:quartz-community/reader-mode
enabled: true
layout:
position: left
priority: 35
group: toolbar
- source: github:quartz-community/breadcrumbs
enabled: true
layout:
position: beforeBody
priority: 5
condition: not-index
- source: github:quartz-community/comments
enabled: false
options:
provider: giscus
options: {}
layout:
position: afterBody
priority: 10
- source: github:quartz-community/footer
enabled: true
options:
links:
GitHub: https://github.com/jackyzha0/quartz
Discord Community: https://discord.gg/cRFFHYye7t
- source: github:quartz-community/recent-notes
enabled: false
- source: github:quartz-community/spacer
enabled: true
options: {}
order: 25
layout:
position: left
priority: 25
display: mobile-only
- source: github:quartz-community/bases-page
enabled: true
options: {}
order: 50
- source: github:quartz-community/note-properties
enabled: true
options:
includeAll: false
includedProperties:
- description
- tags
- aliases
excludedProperties: []
hidePropertiesView: false
delimiters: "---"
language: yaml
order: 5
layout:
position: beforeBody
priority: 15
display: all
- source: github:saberzero1/quartz-themes
enabled: true
options:
theme: "default"
layout:
groups:
toolbar:
priority: 35
direction: row
gap: 0.5rem
byPageType:
"404":
positions:
beforeBody: []
left: []
right: []
content: {}
folder:
exclude:
- reader-mode
positions:
right: []
tag:
exclude:
- reader-mode
positions:
right: []
canvas: {}
bases: {}
+281
View File
@@ -0,0 +1,281 @@
# yaml-language-server: $schema=../../plugins/quartz-plugins.schema.json
# Template: ttrpg
# Obsidian-based setup with map plugin and ITS Theme for TTRPG/D&D wikis.
configuration:
pageTitle: Quartz 5
pageTitleSuffix: ""
enableSPA: true
enablePopovers: true
analytics:
provider: plausible
locale: en-US
baseUrl: quartz.jzhao.xyz
ignorePatterns:
- private
- templates
- .obsidian
defaultDateType: modified
theme:
fontOrigin: googleFonts
cdnCaching: true
typography:
header: Schibsted Grotesk
body: Source Sans Pro
code: IBM Plex Mono
colors:
lightMode:
light: "#faf8f8"
lightgray: "#e5e5e5"
gray: "#b8b8b8"
darkgray: "#4e4e4e"
dark: "#2b2b2b"
secondary: "#284b63"
tertiary: "#84a59d"
highlight: rgba(143, 159, 169, 0.15)
textHighlight: "#fff23688"
darkMode:
light: "#161618"
lightgray: "#393639"
gray: "#646464"
darkgray: "#d4d4d4"
dark: "#ebebec"
secondary: "#7b97aa"
tertiary: "#84a59d"
highlight: rgba(143, 159, 169, 0.15)
textHighlight: "#b3aa0288"
plugins:
- source: github:quartz-community/created-modified-date
enabled: true
options:
priority:
- frontmatter
- git
- filesystem
order: 10
- source: github:quartz-community/syntax-highlighting
enabled: true
options:
theme:
light: github-light
dark: github-dark
keepBackground: false
order: 20
- source: github:quartz-community/obsidian-flavored-markdown
enabled: true
options:
comments: true
highlight: true
wikilinks: true
callouts: true
mermaid: true
parseTags: true
parseArrows: true
parseBlockReferences: true
enableInHtmlEmbed: false
enableYouTubeEmbed: true
enableVideoEmbed: true
enableCheckbox: true
order: 30
- source: github:quartz-community/github-flavored-markdown
enabled: true
order: 40
- source: github:quartz-community/table-of-contents
enabled: true
order: 50
- source: github:quartz-community/crawl-links
enabled: true
options:
markdownLinkResolution: shortest
order: 60
- source: github:quartz-community/description
enabled: true
order: 70
- source: github:quartz-community/latex
enabled: true
options:
renderEngine: katex
order: 80
- source: github:quartz-community/citations
enabled: false
order: 85
- source: github:quartz-community/hard-line-breaks
enabled: true
order: 90
- source: github:quartz-community/ox-hugo
enabled: false
order: 91
- source: github:quartz-community/roam
enabled: false
order: 92
- source: github:quartz-community/remove-draft
enabled: true
- source: github:quartz-community/explicit-publish
enabled: false
- source: github:quartz-community/encrypted-pages
enabled: true
- source: github:quartz-community/stacked-pages
enabled: false
layout:
position: afterBody
priority: 50
display: all
- source: github:quartz-community/alias-redirects
enabled: true
- source: github:quartz-community/content-index
enabled: true
options:
enableSiteMap: true
enableRSS: true
- source: github:quartz-community/favicon
enabled: true
- source: github:quartz-community/og-image
enabled: true
- source: github:quartz-community/cname
enabled: true
- source: github:quartz-community/canvas-page
enabled: true
- source: github:quartz-community/content-page
enabled: true
- source: github:quartz-community/folder-page
enabled: true
- source: github:quartz-community/tag-page
enabled: true
- source: github:quartz-community/explorer
enabled: true
layout:
position: left
priority: 50
- source: github:quartz-community/graph
enabled: true
layout:
position: right
priority: 10
- source: github:quartz-community/search
enabled: true
layout:
position: left
priority: 20
group: toolbar
groupOptions:
grow: true
- source: github:quartz-community/backlinks
enabled: true
layout:
position: right
priority: 30
- source: github:quartz-community/article-title
enabled: true
layout:
position: beforeBody
priority: 10
- source: github:quartz-community/content-meta
enabled: true
layout:
position: beforeBody
priority: 20
- source: github:quartz-community/tag-list
enabled: false
layout:
position: beforeBody
priority: 30
- source: github:quartz-community/page-title
enabled: true
layout:
position: left
priority: 10
- source: github:quartz-community/darkmode
enabled: true
layout:
position: left
priority: 30
group: toolbar
- source: github:quartz-community/reader-mode
enabled: true
layout:
position: left
priority: 35
group: toolbar
- source: github:quartz-community/breadcrumbs
enabled: true
layout:
position: beforeBody
priority: 5
condition: not-index
- source: github:quartz-community/comments
enabled: false
options:
provider: giscus
options: {}
layout:
position: afterBody
priority: 10
- source: github:quartz-community/footer
enabled: true
options:
links:
GitHub: https://github.com/jackyzha0/quartz
Discord Community: https://discord.gg/cRFFHYye7t
- source: github:quartz-community/recent-notes
enabled: false
- source: github:quartz-community/spacer
enabled: true
options: {}
order: 25
layout:
position: left
priority: 25
display: mobile-only
- source: github:quartz-community/bases-page
enabled: true
options: {}
order: 50
- source: github:quartz-community/note-properties
enabled: true
options:
includeAll: false
includedProperties:
- description
- tags
- aliases
excludedProperties: []
hidePropertiesView: false
delimiters: "---"
language: yaml
order: 5
layout:
position: beforeBody
priority: 15
display: all
# TTRPG-specific plugins
- source: github:quartz-community/external-quartz-leaflet-map-plugin
enabled: true
- source: github:saberzero1/quartz-themes
enabled: true
options:
theme: "its-theme"
variation: "ttrpg-dnd"
layout:
groups:
toolbar:
priority: 35
direction: row
gap: 0.5rem
byPageType:
"404":
positions:
beforeBody: []
left: []
right: []
content: {}
folder:
exclude:
- reader-mode
positions:
right: []
tag:
exclude:
- reader-mode
positions:
right: []
canvas: {}
bases: {}