create link card plugin · Tsuuuuuuun/DigitalGarden@e0f5ca1
Contribute to Tsuuuuuuun/DigitalGarden development by creating an account on GitHub.
create link card plugin · Tsuuuuuuun/DigitalGarden@e0f5ca1 favicon https://github.com/Tsuuuuuuun/DigitalGarden/commit/e0f5ca1aed60ed8647cd3d5d429f389e93acc5f8
create link card plugin · Tsuuuuuuun/DigitalGarden@e0f5ca1
画像が404だったときにリンクカードに表示しないように修正 · Tsuuuuuuun/DigitalGarden@f2dc3f2
Contribute to Tsuuuuuuun/DigitalGarden development by creating an account on GitHub.
画像が404だったときにリンクカードに表示しないように修正 · Tsuuuuuuun/DigitalGarden@f2dc3f2 favicon https://github.com/Tsuuuuuuun/DigitalGarden/commit/f2dc3f298a2849bbc7c49b02352d86d59427989c
画像が404だったときにリンクカードに表示しないように修正 · Tsuuuuuuun/DigitalGarden@f2dc3f2
このように

以下では Quartz v4 のデフォルト構成に、外部リンクをカードで表示する “Link Card プラグイン” を組み込むまでの手順を解説します。

  • ブログ記事やニュース URL を貼ると、自動で OGP 画像・タイトル・抜粋 を取得してカード化してくれるやつです。NotionとかQiitaとかZennとかでみたことがあるかもしれません。
  • Quartz は標準でカード UI を持たないため、自作プラグインで Markdown → HTML の変換を追加します。プラグイン機構の全体像は公式ドキュメント「Making your own plugins」が詳しいです。

Quartz のビルドは

  1. Markdown → AST
  2. Transformer Plugin で AST を編集
  3. コンポーネントとレイアウトを合成
  4. Lightning CSS / beforeDOMLoaded / afterBody スクリプト注入

という流れで行われます。Link Card は 2 と 4 の両方にフックします。

実装ステップ

GitHub - gladevise/remark-link-card
Contribute to gladevise/remark-link-card development by creating an account on GitHub.
GitHub - gladevise/remark-link-card favicon https://github.com/gladevise/remark-link-card
GitHub - gladevise/remark-link-card

これが下敷きになっているのでインストールしてください。

npm i remark-link-card

transformer を実装

quartz/plugins/transformers/linkcard.ts を新規作成し、QuartzTransformerPlugin を返す関数を定義します。Markdown 内のリンクを検出し、<a class="rlc-container" …> に差し替えるロジックです。

quartz/plugins/transformers/linkcard.ts
// @ts-ignore
import remarkLinkCard from "remark-link-card"
import { QuartzTransformerPlugin } from "../types"
 
export interface Options {
    cache?: boolean
    shortenUrl?: boolean
    showDescription?: boolean
}
 
const defaultOptions: Options = {
    cache: false,
    shortenUrl: false,
    showDescription: false,
}
 
export const LinkCard: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
    const opts = { ...defaultOptions, ...userOpts }
    return {
        name: "LinkCard",
        markdownPlugins() {
            return [
                [
                    remarkLinkCard,
                    {
                        cache: opts.cache,
                        shortenUrl: opts.shortenUrl,
                        showDescription: opts.showDescription,
                    },
                ],
            ]
        },
    }
}

作成後、quartz/plugins/transformers/index.ts に追加してビルドに登録します。

SCSS によるカードデザイン

quartz/components/styles/linkcard.scss
.rlc-container {
    display: flex;
    text-decoration: none;
    border: 1px solid var(--lightgray);
    border-radius: 12px;
    margin: 16px 0;
    background: var(--light);
    transition: all 0.2s ease;
    color: var(--darkgray);
    overflow: hidden;
 
    &:hover {
        border-color: var(--secondary);
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
        text-decoration: none;
        color: var(--darkgray);
        transform: translateY(-2px);
    }
 
    .rlc-info {
        display: flex;
        flex-direction: column;
        justify-content: center;
        flex: 1;
        padding: 12px 16px;
        min-width: 0;
        gap: 4px;
    }
 
    .rlc-content {
        flex: 1;
    }
 
    .rlc-title {
        font-weight: 600;
        font-size: 1rem;
        line-height: 1.3;
        margin: 0 0 4px 0;
        color: var(--dark);
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
 
    .rlc-description {
        display: none;
    }
 
    .rlc-url-container {
        display: flex;
        align-items: center;
        gap: 6px;
        font-size: 0.75rem;
        color: var(--gray);
        margin: 0;
    }
 
    .rlc-favicon {
        width: 16px;
        height: 16px;
        border-radius: 3px;
        flex-shrink: 0;
    }
 
    .rlc-url {
        text-overflow: ellipsis;
        overflow: hidden;
        white-space: nowrap;
    }
 
    .rlc-image-container {
        flex-shrink: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        background: var(--lightgray);
        position: relative;
        overflow: hidden;
        min-width: 120px; // 最小幅を設定
        max-width: 300px; // 最大幅を設定
        height: 120px; // 高さは固定
        width: auto; // 幅は画像に応じて自動調整
 
        &.image-failed {
            display: none;
        }
    }
 
    .rlc-image {
        width: auto;
        height: 100%;
        object-fit: cover;
        object-position: center;
        min-width: 100%; // コンテナの幅は最低限埋める
    }
}
 
@media (max-width: 768px) {
    .rlc-container {
        flex-direction: column;
        min-height: auto;
        margin: 12px 0;
 
        .rlc-info {
            padding: 16px;
        }
 
        .rlc-image-container {
            width: 100%;
            height: 160px;
            order: -1;
            max-width: none; // モバイルでは最大幅制限を解除
 
            &.image-failed {
                display: none;
            }
        }
 
        .rlc-title {
            font-size: 0.95rem;
        }
    }
}
 
@media (max-width: 480px) {
    .rlc-container {
        .rlc-info {
            padding: 12px;
        }
 
        .rlc-image-container {
            height: 140px;
 
            &.image-failed {
                display: none;
            }
        }
 
        .rlc-title {
            font-size: 0.9rem;
        }
 
        .rlc-url-container {
            font-size: 0.7rem;
        }
    }
}

画像エラーハンドラ

クライアント側で <img> の error イベントを捕まえ、親 .rlc-image-containerimage-failed を付与するスクリプトを作成。

quartz/components/scripts/linkcard.inline.ts
// Handle link card image failures
function handleLinkCardImages() {
    const linkCardImages = document.querySelectorAll(".rlc-image")
 
    linkCardImages.forEach((img: Element) => {
        const imgElement = img as HTMLImageElement
        const container = imgElement.closest(".rlc-image-container")
 
        if (!container) return
 
        // 画像が既に読み込みエラーの場合(complete=true かつ naturalWidth=0)
        if (imgElement.complete && imgElement.naturalWidth === 0) {
            container.classList.add("image-failed")
            return
        }
 
        // 画像読み込みエラー時のイベントリスナー
        const handleError = () => {
            container.classList.add("image-failed")
        }
 
        imgElement.addEventListener("error", handleError)
        window.addCleanup(() => imgElement.removeEventListener("error", handleError))
    })
}
 
// ページ読み込み時とナビゲーション時に実行
document.addEventListener("nav", handleLinkCardImages)
document.addEventListener("DOMContentLoaded", handleLinkCardImages)

beforeDOMLoaded フック経由で head にインライン挿入します。

レイアウトにフック

空の JSX を返す LinkCardHandler.tsx を実装し、handler.beforeDOMLoaded = linkCardScript としてスクリプトを関連付け。

quartz/components/LinkCardHandler.tsx
// @ts-ignore
import linkCardScript from "./scripts/linkcard.inline"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
 
const LinkCardHandler: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
  return <></>
}
 
LinkCardHandler.beforeDOMLoaded = linkCardScript
 
export default (() => LinkCardHandler) satisfies QuartzComponentConstructor

quartz/components/index.ts でエクスポートし、quartz.layout.tsafterBody 配列に LinkCardHandler() を追加するだけで全ページに適用されます。

あとは quartz.config.ts の transformer セクションに LinkCard を追加すれば使えます。

まとめ

これにて Quartz に Link Card を導入できました。やったね!