diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 42adb44..f73eb96 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,11 +1,20 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file - version: 2 updates: - package-ecosystem: "npm" directory: "/" schedule: interval: "weekly" + groups: + production-dependencies: + applies-to: "version-updates" + patterns: + - "*" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + ci-dependencies: + applies-to: "version-updates" + patterns: + - "*" diff --git a/.github/workflows/build-preview.yaml b/.github/workflows/build-preview.yaml new file mode 100644 index 0000000..ff723a1 --- /dev/null +++ b/.github/workflows/build-preview.yaml @@ -0,0 +1,43 @@ +name: Build Preview Deployment + +on: + pull_request: + types: [opened, synchronize] + workflow_dispatch: + +jobs: + build-preview: + if: ${{ github.repository == 'jackyzha0/quartz' }} + runs-on: ubuntu-latest + name: Build Preview + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - run: npm ci + + - name: Check types and style + run: npm run check + + - name: Build Quartz + run: npx quartz build -d docs -v + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: preview-build + path: public diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 56107cf..c584fde 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,6 +7,7 @@ on: push: branches: - v4 + workflow_dispatch: jobs: build-and-test: @@ -18,17 +19,17 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 - name: Cache dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -44,25 +45,25 @@ jobs: run: npm test - name: Ensure Quartz builds, check bundle info - run: npx quartz build --bundleInfo + run: npx quartz build --bundleInfo -d docs publish-tag: - if: ${{ github.repository == 'jackyzha0/quartz' }} + if: ${{ github.repository == 'jackyzha0/quartz' && github.ref == 'refs/heads/v4' }} runs-on: ubuntu-latest permissions: contents: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 - name: Get package version run: node -p -e '`PACKAGE_VERSION=${require("./package.json").version}`' >> $GITHUB_ENV - name: Create release tag - uses: pkgdeps/git-tag-action@v2 + uses: pkgdeps/git-tag-action@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} github_repo: ${{ github.repository }} diff --git a/.github/workflows/deploy-preview.yaml b/.github/workflows/deploy-preview.yaml new file mode 100644 index 0000000..3a1432a --- /dev/null +++ b/.github/workflows/deploy-preview.yaml @@ -0,0 +1,37 @@ +name: Upload Preview Deployment +on: + workflow_run: + workflows: ["Build Preview Deployment"] + types: + - completed + +permissions: + actions: read + deployments: write + contents: read + pull-requests: write + +jobs: + deploy-preview: + if: ${{ github.repository == 'jackyzha0/quartz' && github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + name: Deploy Preview to Cloudflare Pages + steps: + - name: Download build artifact + uses: actions/download-artifact@v4 + id: preview-build-artifact + with: + name: preview-build + path: build + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + + - name: Deploy to Cloudflare Pages + uses: AdrianGonz97/refined-cf-pages-action@v1 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + githubToken: ${{ secrets.GITHUB_TOKEN }} + projectName: quartz + deploymentName: Branch Preview + directory: ${{ steps.preview-build-artifact.outputs.download-path }} diff --git a/.github/workflows/docker-build-push.yaml b/.github/workflows/docker-build-push.yaml new file mode 100644 index 0000000..ee7efa7 --- /dev/null +++ b/.github/workflows/docker-build-push.yaml @@ -0,0 +1,88 @@ +name: Docker build & push image + +on: + push: + branches: [v4] + tags: ["v*"] + pull_request: + branches: [v4] + paths: + - .github/workflows/docker-build-push.yaml + - quartz/** + workflow_dispatch: + +jobs: + build: + if: ${{ github.repository == 'jackyzha0/quartz' }} # Comment this out if you want to publish your own images on a fork! + runs-on: ubuntu-latest + steps: + - name: Set lowercase repository owner environment variable + run: | + echo "OWNER_LOWERCASE=${OWNER,,}" >> ${GITHUB_ENV} + env: + OWNER: "${{ github.repository_owner }}" + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@v5.1.0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + install: true + driver-opts: | + image=moby/buildkit:master + network=host + - name: Install cosign + if: github.event_name != 'pull_request' + uses: sigstore/cosign-installer@v3.8.2 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + if: github.event_name != 'pull_request' + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata tags and labels on PRs + if: github.event_name == 'pull_request' + id: meta-pr + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ env.OWNER_LOWERCASE }}/quartz + tags: | + type=raw,value=sha-${{ env.GITHUB_SHA_SHORT }} + labels: | + org.opencontainers.image.source="https://github.com/${{ github.repository_owner }}/quartz" + - name: Extract metadata tags and labels for main, release or tag + if: github.event_name != 'pull_request' + id: meta + uses: docker/metadata-action@v5 + with: + flavor: | + latest=auto + images: ghcr.io/${{ env.OWNER_LOWERCASE }}/quartz + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}}.{{minor}}.{{patch}} + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} + type=raw,value=sha-${{ env.GITHUB_SHA_SHORT }} + labels: | + maintainer=${{ github.repository_owner }} + org.opencontainers.image.source="https://github.com/${{ github.repository_owner }}/quartz" + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v6 + with: + push: ${{ github.event_name != 'pull_request' }} + build-args: | + GIT_SHA=${{ env.GITHUB_SHA }} + DOCKER_LABEL=sha-${{ env.GITHUB_SHA_SHORT }} + tags: ${{ steps.meta.outputs.tags || steps.meta-pr.outputs.tags }} + labels: ${{ steps.meta.outputs.labels || steps.meta-pr.outputs.labels }} + cache-from: type=gha + cache-to: type=gha diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..aebd91c --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +v22.16.0 diff --git a/Dockerfile b/Dockerfile index 964c254..e9c3734 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:latest as convert +FROM python:latest AS convert WORKDIR /usr/src/app COPY content/ ./ COPY custom/convert-furigana/program/convert-furigana.py ./ @@ -9,13 +9,13 @@ RUN python -m pip install beautifulsoup4 RUN python convert-furigana.py . RUN python convert-usage.py . -FROM node:20-slim as builder +FROM node:22-slim AS builder WORKDIR /usr/src/app COPY package.json . COPY package-lock.json* . RUN npm ci -FROM node:20-slim +FROM node:22-slim WORKDIR /usr/src/app COPY --from=builder /usr/src/app/ /usr/src/app/ COPY . . diff --git a/docs/advanced/creating components.md b/docs/advanced/creating components.md index 27369ab..369405b 100644 --- a/docs/advanced/creating components.md +++ b/docs/advanced/creating components.md @@ -129,11 +129,11 @@ export default (() => { return } - YourComponent.beforeDOM = ` + YourComponent.beforeDOMLoaded = ` console.log("hello from before the page loads!") ` - YourComponent.afterDOM = ` + YourComponent.afterDOMLoaded = ` document.getElementById('btn').onclick = () => { alert('button clicked!') } @@ -161,6 +161,18 @@ document.addEventListener("nav", () => { }) ``` +You can also add the equivalent of a `beforeunload` event for [[SPA Routing]] via the `prenav` event. + +```ts +document.addEventListener("prenav", () => { + // executed after an SPA navigation is triggered but + // before the page is replaced + // one usage pattern is to store things in sessionStorage + // in the prenav and then conditionally load then in the consequent + // nav +}) +``` + It is best practice to track any event handlers via `window.addCleanup` to prevent memory leaks. This will get called on page navigation. @@ -180,7 +192,7 @@ export default (() => { return } - YourComponent.afterDOM = script + YourComponent.afterDOMLoaded = script return YourComponent }) satisfies QuartzComponentConstructor ``` diff --git a/docs/advanced/making plugins.md b/docs/advanced/making plugins.md index b2bacf0..f5cb199 100644 --- a/docs/advanced/making plugins.md +++ b/docs/advanced/making plugins.md @@ -25,10 +25,11 @@ The following sections will go into detail for what methods can be implemented f - `BuildCtx` is defined in `quartz/ctx.ts`. It consists of - `argv`: The command line arguments passed to the Quartz [[build]] command - `cfg`: The full Quartz [[configuration]] - - `allSlugs`: a list of all the valid content slugs (see [[paths]] for more information on what a `ServerSlug` is) + - `allSlugs`: a list of all the valid content slugs (see [[paths]] for more information on what a slug is) - `StaticResources` is defined in `quartz/resources.tsx`. It consists of - - `css`: a list of URLs for stylesheets that should be loaded + - `css`: a list of CSS style definitions that should be loaded. A CSS style is described with the `CSSResource` type which is also defined in `quartz/resources.tsx`. It accepts either a source URL or the inline content of the stylesheet. - `js`: a list of scripts that should be loaded. A script is described with the `JSResource` type which is also defined in `quartz/resources.tsx`. It allows you to define a load time (either before or after the DOM has been loaded), whether it should be a module, and either the source URL or the inline content of the script. + - `additionalHead`: a list of JSX elements or functions that return JSX elements to be added to the `
` tag of the page. Functions receive the page's data as an argument and can conditionally render elements. ## Transformers @@ -37,7 +38,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) => PartialCool Header!
+} +``` + +> [!example]- Local fonts +> +> For cases where you use a local fonts under `static` folder, make sure to set the correct `@font-face` in `custom.scss` +> +> ```scss title="custom.scss" +> @font-face { +> font-family: "Newsreader"; +> font-style: normal; +> font-weight: normal; +> font-display: swap; +> src: url("/static/Newsreader.woff2") format("woff2"); +> } +> ``` +> +> Then in `quartz/util/og.tsx`, you can load the Satori fonts like so: +> +> ```tsx title="quartz/util/og.tsx" +> import { joinSegments, QUARTZ } from "../path" +> import fs from "fs" +> import path from "path" +> +> const newsreaderFontPath = joinSegments(QUARTZ, "static", "Newsreader.woff2") +> export async function getSatoriFonts(headerFont: FontSpecification, bodyFont: FontSpecification) { +> // ... rest of implementation remains same +> const fonts: SatoriOptions["fonts"] = [ +> ...headerFontData.map((data, idx) => ({ +> name: headerFontName, +> data, +> weight: headerWeights[idx], +> style: "normal" as const, +> })), +> ...bodyFontData.map((data, idx) => ({ +> name: bodyFontName, +> data, +> weight: bodyWeights[idx], +> style: "normal" as const, +> })), +> { +> name: "Newsreader", +> data: await fs.promises.readFile(path.resolve(newsreaderFontPath)), +> weight: 400, +> style: "normal" as const, +> }, +> ] +> +> return fonts +> } +> ``` +> +> This font then can be used with your custom structure. + +## Examples + +Here are some example image components you can use as a starting point: + +### Basic Example + +This example will generate images that look as follows: + +| Light | Dark | +| ------------------------------------------ | ----------------------------------------- | +| ![[custom-social-image-preview-light.png]] | ![[custom-social-image-preview-dark.png]] | + +```tsx +import { SatoriOptions } from "satori/wasm" +import { GlobalConfiguration } from "../cfg" +import { SocialImageOptions, UserOpts } from "./imageHelper" +import { QuartzPluginData } from "../plugins/vfile" + +export const customImage: SocialImageOptions["imageStructure"] = ( + cfg: GlobalConfiguration, + userOpts: UserOpts, + title: string, + description: string, + fonts: SatoriOptions["fonts"], + fileData: QuartzPluginData, +) => { + // How many characters are allowed before switching to smaller font + const fontBreakPoint = 22 + const useSmallerFont = title.length > fontBreakPoint + + const { colorScheme } = userOpts + return ( ++ {title} +
++ {description} +
++ {description} +
+- {segmentsElements} + {segments}
) } else { diff --git a/quartz/components/Darkmode.tsx b/quartz/components/Darkmode.tsx index 8ed7c99..afc23d7 100644 --- a/quartz/components/Darkmode.tsx +++ b/quartz/components/Darkmode.tsx @@ -1,6 +1,4 @@ -// @ts-ignore: this is safe, we don't want to actually make darkmode.inline.ts a module as -// modules are automatically deferred and we don't want that to happen for critical beforeDOMLoads -// see: https://v8.dev/features/modules#defer +// @ts-ignore import darkmodeScript from "./scripts/darkmode.inline" import styles from "./styles/darkmode.scss" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" @@ -9,41 +7,38 @@ import { classNames } from "../util/lang" const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { return ( -