diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 42adb4474..f73eb9666 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/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..a5bab096d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,31 @@ + diff --git a/.github/workflows/build-preview.yaml b/.github/workflows/build-preview.yaml new file mode 100644 index 000000000..7ba11fd14 --- /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@v6 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v6 + 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@v5 + with: + name: preview-build + path: public diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f0fc1fd18..b7d4799d6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,14 +19,14 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: 20 + node-version: 22 - name: Cache dependencies uses: actions/cache@v4 @@ -45,7 +45,7 @@ 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' && github.ref == 'refs/heads/v4' }} @@ -53,13 +53,13 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: 20 + node-version: 22 - name: Get package version run: node -p -e '`PACKAGE_VERSION=${require("./package.json").version}`' >> $GITHUB_ENV - name: Create release tag diff --git a/.github/workflows/deploy-preview.yaml b/.github/workflows/deploy-preview.yaml new file mode 100644 index 000000000..eba0d4fb5 --- /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@v6 + 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 000000000..26cf223f9 --- /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@v6 + with: + fetch-depth: 1 + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@v5.4.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@v4.0.0 + - 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 index 805b5a4e0..aebd91c52 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -v20.9.0 +v22.16.0 diff --git a/Dockerfile b/Dockerfile index 1d9e5915f..f8a6f2684 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,10 @@ -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 628d5aa29..84e038012 100644 --- a/docs/advanced/creating components.md +++ b/docs/advanced/creating components.md @@ -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. @@ -214,9 +226,11 @@ Then, you can use it like any other component in `quartz.layout.ts` via `Compone As Quartz components are just functions that return React components, you can compositionally use them in other Quartz components. ```tsx title="quartz/components/AnotherComponent.tsx" -import YourComponent from "./YourComponent" +import YourComponentConstructor from "./YourComponent" export default (() => { + const YourComponent = YourComponentConstructor() + function AnotherComponent(props: QuartzComponentProps) { return (
Cool 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 f64aad636..afc23d758 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,12 +7,12 @@ import { classNames } from "../util/lang" const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { return ( -