Compare commits

...

10 Commits

Author SHA1 Message Date
Jacky Zhao
2213424195 docs: make role of getQuartzComponents more clear and also make it optional
Some checks are pending
Build and Test / build-and-test (macos-latest) (push) Waiting to run
Build and Test / build-and-test (windows-latest) (push) Waiting to run
Build and Test / build-and-test (ubuntu-latest) (push) Has been skipped
Build and Test / publish-tag (push) Has been skipped
2025-03-05 18:34:02 -08:00
Jacky Zhao
5b13ff2199 feat: support emitters defining external resources, emit link from contentindex directly 2025-03-05 18:16:24 -08:00
Aaron Pham
5a39719898
fix(graph): set container as renderGroup to avoid redrawing multiple times (#1736)
Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
2025-03-05 17:33:16 -08:00
Jacky Zhao
3c8ccde624 chore(og-image): force twemoji for emoji util 2025-03-05 17:21:19 -08:00
Stephen Tse
c97fd7089a
Added emoji support to Satori when generating OG images (#1593) 2025-03-05 17:14:06 -08:00
Jacky Zhao
2acfa0fa23 fix(og-image): overflow ellipses in title and description 2025-03-05 17:13:19 -08:00
Jacky Zhao
f6f417a505 fix: engine reqiurements 2025-03-05 16:49:43 -08:00
dependabot[bot]
a3b6201365
chore(deps): bump the production-dependencies group with 6 updates (#1804)
* chore(deps): bump the production-dependencies group with 6 updates

Bumps the production-dependencies group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [hast-util-to-jsx-runtime](https://github.com/syntax-tree/hast-util-to-jsx-runtime) | `2.3.4` | `2.3.5` |
| [pixi.js](https://github.com/pixijs/pixijs) | `8.8.0` | `8.8.1` |
| [preact](https://github.com/preactjs/preact) | `10.26.2` | `10.26.4` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `22.13.5` | `22.13.9` |
| [prettier](https://github.com/prettier/prettier) | `3.5.2` | `3.5.3` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.7.3` | `5.8.2` |


Updates `hast-util-to-jsx-runtime` from 2.3.4 to 2.3.5
- [Release notes](https://github.com/syntax-tree/hast-util-to-jsx-runtime/releases)
- [Commits](https://github.com/syntax-tree/hast-util-to-jsx-runtime/compare/2.3.4...2.3.5)

Updates `pixi.js` from 8.8.0 to 8.8.1
- [Release notes](https://github.com/pixijs/pixijs/releases)
- [Commits](https://github.com/pixijs/pixijs/compare/v8.8.0...v8.8.1)

Updates `preact` from 10.26.2 to 10.26.4
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.26.2...10.26.4)

Updates `@types/node` from 22.13.5 to 22.13.9
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `prettier` from 3.5.2 to 3.5.3
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.5.2...3.5.3)

Updates `typescript` from 5.7.3 to 5.8.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.7.3...v5.8.2)

---
updated-dependencies:
- dependency-name: hast-util-to-jsx-runtime
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: pixi.js
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: preact
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* type fixes

* fix more types

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2025-03-05 16:45:02 -08:00
Jacky Zhao
a1162b978a fix(analytics): spa tracking for other providers 2025-03-05 16:27:08 -08:00
Emile Bangma
c6f10b44f6
feat(rss): configurable RSS feed URL (#1806)
* feat(rss): configurable RSS feed URL

* Update docs/features/RSS Feed.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update docs/features/RSS Feed.md

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2025-03-05 15:54:11 -08:00
24 changed files with 232 additions and 115 deletions

View File

@ -37,7 +37,7 @@ Transformers **map** over content, taking a Markdown file and outputting modifie
```ts
export type QuartzTransformerPluginInstance = {
name: string
textTransform?: (ctx: BuildCtx, src: string | Buffer) => string | Buffer
textTransform?: (ctx: BuildCtx, src: string) => string
markdownPlugins?: (ctx: BuildCtx) => PluggableList
htmlPlugins?: (ctx: BuildCtx) => PluggableList
externalResources?: (ctx: BuildCtx) => Partial<StaticResources>
@ -99,8 +99,6 @@ export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => {
},
],
}
} else {
return {}
}
},
}

View File

@ -1,5 +1,10 @@
Quartz emits an RSS feed for all the content on your site by generating an `index.xml` file that RSS readers can subscribe to. Because of the RSS spec, this requires the `baseUrl` property in your [[configuration]] to be set properly for RSS readers to pick it up properly.
> [!info]
> After deploying, the generated RSS link will be available at `https://${baseUrl}/index.xml` by default.
>
> The `index.xml` path can be customized by passing the `rssSlug` option to the [[ContentIndex]] plugin.
## Configuration
This functionality is provided by the [[ContentIndex]] plugin. See the plugin page for customization options.

View File

@ -17,6 +17,7 @@ This plugin accepts the following configuration options:
- `enableRSS`: If `true` (default), produces an RSS feed (`index.xml`) with recent content updates.
- `rssLimit`: Defines the maximum number of entries to include in the RSS feed, helping to focus on the most recent or relevant content. Defaults to `10`.
- `rssFullHtml`: If `true`, the RSS feed includes full HTML content. Otherwise it includes just summaries.
- `rssSlug`: Slug to the generated RSS feed XML file. Defaults to `"index"`.
- `includeEmptyFiles`: If `true` (default), content files with no body text are included in the generated index and resources.
## API

65
package-lock.json generated
View File

@ -25,7 +25,7 @@
"globby": "^14.1.0",
"gray-matter": "^4.0.3",
"hast-util-to-html": "^9.0.5",
"hast-util-to-jsx-runtime": "^2.3.4",
"hast-util-to-jsx-runtime": "^2.3.5",
"hast-util-to-string": "^3.0.1",
"is-absolute-url": "^4.0.1",
"js-yaml": "^4.1.0",
@ -34,8 +34,8 @@
"mdast-util-to-hast": "^13.2.0",
"mdast-util-to-string": "^4.0.0",
"micromorph": "^0.4.5",
"pixi.js": "^8.8.0",
"preact": "^10.26.2",
"pixi.js": "^8.8.1",
"preact": "^10.26.4",
"preact-render-to-string": "^6.5.13",
"pretty-bytes": "^6.1.1",
"pretty-time": "^1.1.0",
@ -79,15 +79,15 @@
"@types/d3": "^7.4.3",
"@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.13.5",
"@types/node": "^22.13.9",
"@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10",
"@types/ws": "^8.5.14",
"@types/yargs": "^17.0.33",
"esbuild": "^0.25.0",
"prettier": "^3.5.2",
"prettier": "^3.5.3",
"tsx": "^4.19.3",
"typescript": "^5.7.3"
"typescript": "^5.8.2"
},
"engines": {
"node": "20 || >=22",
@ -1916,9 +1916,9 @@
}
},
"node_modules/@types/node": {
"version": "22.13.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz",
"integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==",
"version": "22.13.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
"integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3224,6 +3224,15 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/gifuct-js": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz",
"integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==",
"license": "MIT",
"dependencies": {
"js-binary-schema-parser": "^2.0.3"
}
},
"node_modules/github-slugger": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
@ -3541,9 +3550,9 @@
}
},
"node_modules/hast-util-to-jsx-runtime": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.4.tgz",
"integrity": "sha512-2GSifZSlBD35z6/+sp+btB333wHFPck/rrlKZMc9IOUJk6anHuQuqC/oNI80Pj717wo8JCPdXjjasVqQu3UH8Q==",
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.5.tgz",
"integrity": "sha512-gHD+HoFxOMmmXLuq9f2dZDMQHVcplCVpMfBNRpJsF03yyLZvJGzsFORe8orVuYDX9k2w0VH0uF8oryFd1whqKQ==",
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
@ -3928,6 +3937,12 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/js-binary-schema-parser": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz",
"integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==",
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@ -5468,9 +5483,9 @@
}
},
"node_modules/pixi.js": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.8.0.tgz",
"integrity": "sha512-0xW8tKa+uF28mi1SwvnNscMpYJSQrqLN7jJs6Ore37FZoXmIRzQNrGA6drpHDVTuEmoqJlSiGLCk5cUgz3ODgQ==",
"version": "8.8.1",
"resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.8.1.tgz",
"integrity": "sha512-Zmox3Vy52Kl6X/uxknKlxJWPVEFiP63nsX8soqB4butTkIOK3y7c9C204wcDfAgkwO1OlwYxscWtHv+ef4gqgA==",
"license": "MIT",
"dependencies": {
"@pixi/colord": "^2.9.6",
@ -5480,6 +5495,7 @@
"@xmldom/xmldom": "^0.8.10",
"earcut": "^2.2.4",
"eventemitter3": "^5.0.1",
"gifuct-js": "^2.1.2",
"ismobilejs": "^1.1.1",
"parse-svg-path": "^0.1.2"
}
@ -5491,9 +5507,9 @@
"license": "MIT"
},
"node_modules/preact": {
"version": "10.26.2",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.26.2.tgz",
"integrity": "sha512-0gNmv4qpS9HaN3+40CLBAnKe0ZfyE4ZWo5xKlC1rVrr0ckkEvJvAQqKaHANdFKsGstoxrY4AItZ7kZSGVoVjgg==",
"version": "10.26.4",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.26.4.tgz",
"integrity": "sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w==",
"license": "MIT",
"funding": {
"type": "opencollective",
@ -5509,9 +5525,9 @@
}
},
"node_modules/prettier": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz",
"integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==",
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
"bin": {
@ -6968,10 +6984,11 @@
}
},
"node_modules/typescript": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@ -21,7 +21,7 @@
},
"engines": {
"npm": ">=9.3.1",
"node": "20 || >=22"
"node": ">=20"
},
"keywords": [
"site generator",
@ -51,7 +51,7 @@
"globby": "^14.1.0",
"gray-matter": "^4.0.3",
"hast-util-to-html": "^9.0.5",
"hast-util-to-jsx-runtime": "^2.3.4",
"hast-util-to-jsx-runtime": "^2.3.5",
"hast-util-to-string": "^3.0.1",
"is-absolute-url": "^4.0.1",
"js-yaml": "^4.1.0",
@ -60,8 +60,8 @@
"mdast-util-to-hast": "^13.2.0",
"mdast-util-to-string": "^4.0.0",
"micromorph": "^0.4.5",
"pixi.js": "^8.8.0",
"preact": "^10.26.2",
"pixi.js": "^8.8.1",
"preact": "^10.26.4",
"preact-render-to-string": "^6.5.13",
"pretty-bytes": "^6.1.1",
"pretty-time": "^1.1.0",
@ -102,14 +102,14 @@
"@types/d3": "^7.4.3",
"@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.13.5",
"@types/node": "^22.13.9",
"@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10",
"@types/ws": "^8.5.14",
"@types/yargs": "^17.0.33",
"esbuild": "^0.25.0",
"prettier": "^3.5.2",
"prettier": "^3.5.3",
"tsx": "^4.19.3",
"typescript": "^5.7.3"
"typescript": "^5.8.2"
}
}

View File

@ -2,13 +2,13 @@ import { QuartzConfig } from "./quartz/cfg"
import * as Plugin from "./quartz/plugins"
/**
* Quartz 4.0 Configuration
* Quartz 4 Configuration
*
* See https://quartz.jzhao.xyz/configuration for more information.
*/
const config: QuartzConfig = {
configuration: {
pageTitle: "🪴 Quartz 4.0",
pageTitle: "🪴 Quartz 4",
pageTitleSuffix: "",
enableSPA: true,
enablePopovers: true,
@ -19,7 +19,7 @@ const config: QuartzConfig = {
baseUrl: "quartz.jzhao.xyz",
ignorePatterns: ["private", "templates", ".obsidian"],
defaultDateType: "created",
generateSocialImages: false,
generateSocialImages: true,
theme: {
fontOrigin: "googleFonts",
cdnCaching: true,

View File

@ -4,6 +4,7 @@ import { CSSResourceToStyleElement, JSResourceToScriptElement } from "../util/re
import { googleFontHref } from "../util/theme"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import satori, { SatoriOptions } from "satori"
import { loadEmoji, getIconCode } from "../util/emoji"
import fs from "fs"
import sharp from "sharp"
import { ImageOptions, SocialImageOptions, getSatoriFont, defaultImage } from "../util/og"
@ -24,7 +25,18 @@ async function generateSocialImage(
// JSX that will be used to generate satori svg
const imageComponent = userOpts.imageStructure(cfg, userOpts, title, description, fonts, fileData)
const svg = await satori(imageComponent, { width, height, fonts })
const svg = await satori(imageComponent, {
width,
height,
fonts,
loadAdditionalAsset: async (languageCode: string, segment: string) => {
if (languageCode === "emoji") {
return `data:image/svg+xml;base64,${btoa(await loadEmoji(getIconCode(segment)))}`
}
return languageCode
},
})
// Convert svg directly to webp (with additional compression)
const compressed = await sharp(Buffer.from(svg)).webp({ quality: 40 }).toBuffer()
@ -98,7 +110,7 @@ export default (() => {
if (fileName) {
// Generate social image (happens async)
generateSocialImage(
void generateSocialImage(
{
title,
description,
@ -115,7 +127,7 @@ export default (() => {
}
}
const { css, js } = externalResources
const { css, js, additionalHead } = externalResources
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
const path = url.pathname as FullSlug
@ -165,7 +177,7 @@ export default (() => {
<link rel="stylesheet" href={googleFontHref(cfg.theme)} />
</>
)}
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossOrigin={"anonymous"} />
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossOrigin="anonymous" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{/* OG/Twitter meta tags */}
<meta name="og:site_name" content={cfg.pageTitle}></meta>
@ -201,6 +213,13 @@ export default (() => {
{js
.filter((resource) => resource.loadTime === "beforeDOMReady")
.map((res) => JSResourceToScriptElement(res, true))}
{additionalHead.map((resource) => {
if (typeof resource === "function") {
return resource(fileData)
} else {
return resource
}
})}
</head>
)
}

View File

@ -54,6 +54,7 @@ export function pageResources(
},
...staticResources.js,
],
additionalHead: staticResources.additionalHead,
}
if (fileData.hasMermaidDiagram) {

View File

@ -370,9 +370,9 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
const stage = app.stage
stage.interactive = false
const labelsContainer = new Container<Text>({ zIndex: 3 })
const nodesContainer = new Container<Graphics>({ zIndex: 2 })
const linkContainer = new Container<Graphics>({ zIndex: 1 })
const labelsContainer = new Container<Text>({ zIndex: 3, isRenderGroup: true })
const nodesContainer = new Container<Graphics>({ zIndex: 2, isRenderGroup: true })
const linkContainer = new Container<Graphics>({ zIndex: 1, isRenderGroup: true })
stage.addChild(nodesContainer, labelsContainer, linkContainer)
for (const n of graphData.nodes) {

View File

@ -6,9 +6,6 @@ import { getAliasSlugs } from "../transformers/frontmatter"
export const AliasRedirects: QuartzEmitterPlugin = () => ({
name: "AliasRedirects",
getQuartzComponents() {
return []
},
async getDependencyGraph(ctx, content, _resources) {
const graph = new DepGraph<FilePath>()
@ -22,7 +19,6 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
return graph
},
async emit(ctx, content, _resources): Promise<FilePath[]> {
const { argv } = ctx
const fps: FilePath[] = []
for (const [_tree, file] of content) {

View File

@ -15,9 +15,6 @@ const filesToCopy = async (argv: Argv, cfg: QuartzConfig) => {
export const Assets: QuartzEmitterPlugin = () => {
return {
name: "Assets",
getQuartzComponents() {
return []
},
async getDependencyGraph(ctx, _content, _resources) {
const { argv, cfg } = ctx
const graph = new DepGraph<FilePath>()

View File

@ -11,9 +11,6 @@ export function extractDomainFromBaseUrl(baseUrl: string) {
export const CNAME: QuartzEmitterPlugin = () => ({
name: "CNAME",
getQuartzComponents() {
return []
},
async getDependencyGraph(_ctx, _content, _resources) {
return new DepGraph<FilePath>()
},

View File

@ -24,7 +24,7 @@ type ComponentResources = {
function getComponentResources(ctx: BuildCtx): ComponentResources {
const allComponents: Set<QuartzComponent> = new Set()
for (const emitter of ctx.cfg.plugins.emitters) {
const components = emitter.getQuartzComponents(ctx)
const components = emitter.getQuartzComponents?.(ctx) ?? []
for (const component of components) {
allComponents.add(component)
}
@ -116,35 +116,53 @@ function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentReso
const umamiScript = document.createElement("script")
umamiScript.src = "${cfg.analytics.host ?? "https://analytics.umami.is"}/script.js"
umamiScript.setAttribute("data-website-id", "${cfg.analytics.websiteId}")
umamiScript.setAttribute("data-auto-track", "false")
umamiScript.async = true
document.head.appendChild(umamiScript)
document.addEventListener("nav", () => {
umami.track();
})
`)
} else if (cfg.analytics?.provider === "goatcounter") {
componentResources.afterDOMLoaded.push(`
const goatcounterScript = document.createElement("script")
goatcounterScript.src = "${cfg.analytics.scriptSrc ?? "https://gc.zgo.at/count.js"}"
goatcounterScript.async = true
goatcounterScript.setAttribute("data-goatcounter",
"https://${cfg.analytics.websiteId}.${cfg.analytics.host ?? "goatcounter.com"}/count")
document.head.appendChild(goatcounterScript)
window.goatcounter = { no_onload: true }
document.addEventListener("nav", () => {
const goatcounterScript = document.createElement("script")
goatcounterScript.src = "${cfg.analytics.scriptSrc ?? "https://gc.zgo.at/count.js"}"
goatcounterScript.async = true
goatcounterScript.setAttribute("data-goatcounter",
"https://${cfg.analytics.websiteId}.${cfg.analytics.host ?? "goatcounter.com"}/count")
document.head.appendChild(goatcounterScript)
goatcounter.count({ path: location.pathname })
})
`)
} else if (cfg.analytics?.provider === "posthog") {
componentResources.afterDOMLoaded.push(`
const posthogScript = document.createElement("script")
posthogScript.innerHTML= \`!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('${cfg.analytics.apiKey}',{api_host:'${cfg.analytics.host ?? "https://app.posthog.com"}'})\`
posthog.init('${cfg.analytics.apiKey}', {
api_host: '${cfg.analytics.host ?? "https://app.posthog.com"}',
capture_pageview: false,
})\`
document.head.appendChild(posthogScript)
document.addEventListener("nav", () => {
posthog.capture('$pageview', { path: location.pathname })
})
`)
} else if (cfg.analytics?.provider === "tinylytics") {
const siteId = cfg.analytics.siteId
componentResources.afterDOMLoaded.push(`
const tinylyticsScript = document.createElement("script")
tinylyticsScript.src = "https://tinylytics.app/embed/${siteId}.js"
tinylyticsScript.src = "https://tinylytics.app/embed/${siteId}.js?spa"
tinylyticsScript.defer = true
document.head.appendChild(tinylyticsScript)
document.addEventListener("nav", () => {
window.tinylytics.triggerUpdate()
})
`)
} else if (cfg.analytics?.provider === "cabin") {
componentResources.afterDOMLoaded.push(`
@ -182,9 +200,6 @@ function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentReso
export const ComponentResources: QuartzEmitterPlugin = () => {
return {
name: "ComponentResources",
getQuartzComponents() {
return []
},
async getDependencyGraph(_ctx, _content, _resources) {
return new DepGraph<FilePath>()
},

View File

@ -9,7 +9,7 @@ import { write } from "./helpers"
import { i18n } from "../../i18n"
import DepGraph from "../../depgraph"
export type ContentIndex = Map<FullSlug, ContentDetails>
export type ContentIndexMap = Map<FullSlug, ContentDetails>
export type ContentDetails = {
title: string
links: SimpleSlug[]
@ -25,6 +25,7 @@ interface Options {
enableRSS: boolean
rssLimit?: number
rssFullHtml: boolean
rssSlug: string
includeEmptyFiles: boolean
}
@ -33,10 +34,11 @@ const defaultOptions: Options = {
enableRSS: true,
rssLimit: 10,
rssFullHtml: false,
rssSlug: "index",
includeEmptyFiles: true,
}
function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string {
function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndexMap): string {
const base = cfg.baseUrl ?? ""
const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<url>
<loc>https://${joinSegments(base, encodeURI(slug))}</loc>
@ -48,7 +50,7 @@ function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string {
return `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">${urls}</urlset>`
}
function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: number): string {
function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndexMap, limit?: number): string {
const base = cfg.baseUrl ?? ""
const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<item>
@ -116,7 +118,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
async emit(ctx, content, _resources) {
const cfg = ctx.cfg.configuration
const emitted: FilePath[] = []
const linkIndex: ContentIndex = new Map()
const linkIndex: ContentIndexMap = new Map()
for (const [tree, file] of content) {
const slug = file.data.slug!
const date = getDate(ctx.cfg.configuration, file.data) ?? new Date()
@ -151,7 +153,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
await write({
ctx,
content: generateRSSFeed(cfg, linkIndex, opts.rssLimit),
slug: "index" as FullSlug,
slug: (opts?.rssSlug ?? "index") as FullSlug,
ext: ".xml",
}),
)
@ -180,6 +182,19 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
return emitted
},
getQuartzComponents: () => [],
externalResources: (ctx) => {
if (opts?.enableRSS) {
return {
additionalHead: [
<link
rel="alternate"
type="application/rss+xml"
title="RSS Feed"
href={`https://${ctx.cfg.configuration.baseUrl}/index.xml`}
/>,
],
}
}
},
}
}

View File

@ -1,7 +1,7 @@
export { ContentPage } from "./contentPage"
export { TagPage } from "./tagPage"
export { FolderPage } from "./folderPage"
export { ContentIndex } from "./contentIndex"
export { ContentIndex as ContentIndex } from "./contentIndex"
export { AliasRedirects } from "./aliases"
export { Assets } from "./assets"
export { Static } from "./static"

View File

@ -6,9 +6,6 @@ import DepGraph from "../../depgraph"
export const Static: QuartzEmitterPlugin = () => ({
name: "Static",
getQuartzComponents() {
return []
},
async getDependencyGraph({ argv, cfg }, _content, _resources) {
const graph = new DepGraph<FilePath>()

View File

@ -6,9 +6,10 @@ export function getStaticResourcesFromPlugins(ctx: BuildCtx) {
const staticResources: StaticResources = {
css: [],
js: [],
additionalHead: [],
}
for (const transformer of ctx.cfg.plugins.transformers) {
for (const transformer of [...ctx.cfg.plugins.transformers, ...ctx.cfg.plugins.emitters]) {
const res = transformer.externalResources ? transformer.externalResources(ctx) : {}
if (res?.js) {
staticResources.js.push(...res.js)
@ -16,6 +17,9 @@ export function getStaticResourcesFromPlugins(ctx: BuildCtx) {
if (res?.css) {
staticResources.css.push(...res.css)
}
if (res?.additionalHead) {
staticResources.additionalHead.push(...res.additionalHead)
}
}
// if serving locally, listen for rebuilds and reload the page

View File

@ -67,7 +67,8 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
[remarkFrontmatter, ["yaml", "toml"]],
() => {
return (_, file) => {
const { data } = matter(Buffer.from(file.value), {
const fileData = Buffer.from(file.value as Uint8Array)
const { data } = matter(fileData, {
...opts,
engines: {
yaml: (s) => yaml.load(s, { schema: yaml.JSON_SCHEMA }) as object,

View File

@ -59,8 +59,6 @@ export const Latex: QuartzTransformerPlugin<Partial<Options>> = (opts) => {
},
],
}
default:
return { css: [], js: [] }
}
},
}

View File

@ -156,20 +156,12 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
textTransform(_ctx, src) {
// do comments at text level
if (opts.comments) {
if (src instanceof Buffer) {
src = src.toString()
}
src = (src as string).replace(commentRegex, "")
src = src.replace(commentRegex, "")
}
// pre-transform blockquotes
if (opts.callouts) {
if (src instanceof Buffer) {
src = src.toString()
}
src = (src as string).replace(calloutLineRegex, (value) => {
src = src.replace(calloutLineRegex, (value) => {
// force newline after title of callout
return value + "\n> "
})
@ -177,12 +169,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
// pre-transform wikilinks (fix anchors to things that may contain illegal syntax e.g. codeblocks, latex)
if (opts.wikilinks) {
if (src instanceof Buffer) {
src = src.toString()
}
// replace all wikilinks inside a table first
src = (src as string).replace(tableRegex, (value) => {
src = src.replace(tableRegex, (value) => {
// escape all aliases and headers in wikilinks inside a table
return value.replace(tableWikilinkRegex, (_value, raw) => {
// const [raw]: (string | undefined)[] = capture
@ -196,7 +184,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
})
// replace all other wikilinks
src = (src as string).replace(wikilinkRegex, (value, ...capture) => {
src = src.replace(wikilinkRegex, (value, ...capture) => {
const [rawFp, rawHeader, rawAlias]: (string | undefined)[] = capture
const [fp, anchor] = splitAnchor(`${rawFp ?? ""}${rawHeader ?? ""}`)

View File

@ -13,15 +13,16 @@ export interface PluginTypes {
}
type OptionType = object | undefined
type ExternalResourcesFn = (ctx: BuildCtx) => Partial<StaticResources> | undefined
export type QuartzTransformerPlugin<Options extends OptionType = undefined> = (
opts?: Options,
) => QuartzTransformerPluginInstance
export type QuartzTransformerPluginInstance = {
name: string
textTransform?: (ctx: BuildCtx, src: string | Buffer) => string | Buffer
textTransform?: (ctx: BuildCtx, src: string) => string
markdownPlugins?: (ctx: BuildCtx) => PluggableList
htmlPlugins?: (ctx: BuildCtx) => PluggableList
externalResources?: (ctx: BuildCtx) => Partial<StaticResources>
externalResources?: ExternalResourcesFn
}
export type QuartzFilterPlugin<Options extends OptionType = undefined> = (
@ -38,10 +39,16 @@ export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (
export type QuartzEmitterPluginInstance = {
name: string
emit(ctx: BuildCtx, content: ProcessedContent[], resources: StaticResources): Promise<FilePath[]>
getQuartzComponents(ctx: BuildCtx): QuartzComponent[]
/**
* Returns the components (if any) that are used in rendering the page.
* This helps Quartz optimize the page by only including necessary resources
* for components that are actually used.
*/
getQuartzComponents?: (ctx: BuildCtx) => QuartzComponent[]
getDependencyGraph?(
ctx: BuildCtx,
content: ProcessedContent[],
resources: StaticResources,
): Promise<DepGraph<FilePath>>
externalResources?: ExternalResourcesFn
}

38
quartz/util/emoji.ts Normal file
View File

@ -0,0 +1,38 @@
const U200D = String.fromCharCode(8205)
const UFE0Fg = /\uFE0F/g
export function getIconCode(char: string) {
return toCodePoint(char.indexOf(U200D) < 0 ? char.replace(UFE0Fg, "") : char)
}
function toCodePoint(unicodeSurrogates: string) {
const r = []
let c = 0,
p = 0,
i = 0
while (i < unicodeSurrogates.length) {
c = unicodeSurrogates.charCodeAt(i++)
if (p) {
r.push((65536 + ((p - 55296) << 10) + (c - 56320)).toString(16))
p = 0
} else if (55296 <= c && c <= 56319) {
p = c
} else {
r.push(c.toString(16))
}
}
return r.join("-")
}
const twemoji = (code: string) =>
`https://cdnjs.cloudflare.com/ajax/libs/twemoji/15.1.0/svg/${code.toLowerCase()}.svg`
const emojiCache: Record<string, Promise<any>> = {}
export function loadEmoji(code: string) {
const type = "twemoji"
const key = type + ":" + code
if (key in emojiCache) return emojiCache[key]
return (emojiCache[key] = fetch(twemoji(code)).then((r) => r.text()))
}

View File

@ -143,12 +143,10 @@ export const defaultImage: SocialImageOptions["imageStructure"] = (
fonts: SatoriOptions["fonts"],
_fileData: QuartzPluginData,
) => {
// How many characters are allowed before switching to smaller font
const fontBreakPoint = 22
const useSmallerFont = title.length > fontBreakPoint
// Setup to access image
const iconPath = `https://${cfg.baseUrl}/static/icon.png`
return (
<div
style={{
@ -160,43 +158,66 @@ export const defaultImage: SocialImageOptions["imageStructure"] = (
width: "100%",
backgroundColor: cfg.theme.colors[colorScheme].light,
gap: "2rem",
paddingTop: "1.5rem",
paddingBottom: "1.5rem",
paddingLeft: "5rem",
paddingRight: "5rem",
padding: "1.5rem 5rem",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
width: "100%",
flexDirection: "row",
gap: "2.5rem",
}}
>
<img src={iconPath} width={135} height={135} />
<p
<div
style={{
display: "flex",
color: cfg.theme.colors[colorScheme].dark,
fontSize: useSmallerFont ? 70 : 82,
fontFamily: fonts[0].name,
maxWidth: "70%",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{title}
</p>
<p
style={{
margin: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{title}
</p>
</div>
</div>
<p
<div
style={{
display: "flex",
color: cfg.theme.colors[colorScheme].dark,
fontSize: 44,
lineClamp: 3,
fontFamily: fonts[1].name,
maxWidth: "100%",
maxHeight: "40%",
overflow: "hidden",
}}
>
{description}
</p>
<p
style={{
margin: 0,
display: "-webkit-box",
WebkitBoxOrient: "vertical",
WebkitLineClamp: 3,
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{description}
</p>
</div>
</div>
)
}

View File

@ -1,5 +1,6 @@
import { randomUUID } from "crypto"
import { JSX } from "preact/jsx-runtime"
import { QuartzPluginData } from "../plugins/vfile"
export type JSResource = {
loadTime: "beforeDOMReady" | "afterDOMReady"
@ -62,4 +63,5 @@ export function CSSResourceToStyleElement(resource: CSSResource, preserve?: bool
export interface StaticResources {
css: CSSResource[]
js: JSResource[]
additionalHead: (JSX.Element | ((pageData: QuartzPluginData) => JSX.Element))[]
}