From 6b58728e7cda4228102991412f63b00bd424675a Mon Sep 17 00:00:00 2001 From: Struchkov Mark Date: Mon, 16 Mar 2026 14:57:32 +0300 Subject: [PATCH] feat: add FrontmatterFields component Configurable component that displays frontmatter fields (area, parent, etc.) with auto-detection of markdown links and published page resolution. Co-Authored-By: Claude Opus 4.6 (1M context) --- quartz/components/_FrontmatterFields.tsx | 70 +++++++++++++++++++ quartz/components/index.ts | 2 + .../components/styles/_frontmatterFields.scss | 18 +++++ 3 files changed, 90 insertions(+) create mode 100644 quartz/components/_FrontmatterFields.tsx create mode 100644 quartz/components/styles/_frontmatterFields.scss diff --git a/quartz/components/_FrontmatterFields.tsx b/quartz/components/_FrontmatterFields.tsx new file mode 100644 index 000000000..1098dadd4 --- /dev/null +++ b/quartz/components/_FrontmatterFields.tsx @@ -0,0 +1,70 @@ +import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { resolveRelative } from "../util/path" +import { JSX } from "preact" +// @ts-ignore +import style from "./styles/_frontmatterFields.scss" + +interface FieldConfig { + /** frontmatter key */ + key: string + /** display label */ + label: string +} + +interface FrontmatterFieldsOptions { + fields: FieldConfig[] +} + +/** Parse markdown link `[Title]()` or `[Title](File.md)` */ +function parseMdLink(value: string): { title: string; filename: string } | null { + const match = value.match(/^\[([^\]]+)\]\()]+)>?\)$/) + if (!match) return null + return { title: match[1], filename: match[2].replace(/\.md$/, "") } +} + +export default ((opts: Partial) => { + const fields = opts?.fields ?? [] + + function FrontmatterFields({ fileData, allFiles, displayClass }: QuartzComponentProps) { + const fm = fileData.frontmatter + if (!fm || fields.length === 0) return null + + const titleToSlug = new Map( + allFiles + .filter((f) => f.frontmatter?.title && f.slug) + .map((f) => [f.frontmatter!.title as string, f.slug!]), + ) + + const items: JSX.Element[] = [] + for (const field of fields) { + const raw = fm[field.key] + if (!raw || (typeof raw === "string" && raw.trim() === "")) continue + + const value = Array.isArray(raw) ? raw.join(", ") : String(raw) + const parsed = parseMdLink(value) + const displayText = parsed ? parsed.title : value + const targetSlug = parsed ? titleToSlug.get(parsed.title) : undefined + + items.push( +
+ {field.label}:{" "} + {targetSlug ? ( + + {displayText} + + ) : ( + {displayText} + )} +
, + ) + } + + if (items.length === 0) return null + + return
{items}
+ } + + FrontmatterFields.css = style + + return FrontmatterFields +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/index.ts b/quartz/components/index.ts index 1c459c8a9..710da4378 100644 --- a/quartz/components/index.ts +++ b/quartz/components/index.ts @@ -29,6 +29,7 @@ import GithubSource from "./_GithubSource" import RandomPageButton from "./_RandomPageButton" import Ads from "./_Ads" import YandexMetrika from "./_YandexMetrika" +import FrontmatterFields from "./_FrontmatterFields" // import SocialShare from "./_SocialShare" export { @@ -63,5 +64,6 @@ export { RandomPageButton, Ads, YandexMetrika, + FrontmatterFields, // SocialShare } diff --git a/quartz/components/styles/_frontmatterFields.scss b/quartz/components/styles/_frontmatterFields.scss new file mode 100644 index 000000000..574050567 --- /dev/null +++ b/quartz/components/styles/_frontmatterFields.scss @@ -0,0 +1,18 @@ +.frontmatter-fields { + margin: 0.5rem 0; + font-size: 0.85rem; + color: var(--darkgray); +} + +.frontmatter-field { + margin-bottom: 0.25rem; +} + +.frontmatter-field-label { + font-weight: 600; + opacity: 0.7; +} + +.frontmatter-field-value { + color: var(--dark); +}