mirror of
https://github.com/quartz-community/og-image.git
synced 2026-07-22 02:50:23 +00:00
refactor: rename LocalQuartzPluginData to SocialImageFileData and export it
The type was a deliberate narrowing of QuartzPluginData that documents which fields the og-image emitter populates before invoking a user's imageStructure callback (including the plugin-synthesised 'text' field). The old name implied accidental duplication, inviting 'helpful' future refactors that would inline it into QuartzPluginData and silently widen the public API. Rename to SocialImageFileData, export it from the package entry, and add a docstring explaining why it must stay narrow. The type was already visible in the generated .d.ts through ImageOptions, so making the export explicit matches what consumers already see in their IDE. Also adds a test suite (og-image previously had no tests): - SocialImageFileData contract tests locking in the documented shape - imageStructure callback signature compatibility - defaultImage called with populated / minimal / readingTimeText override payloads returns a valid preact VNode
This commit is contained in:
parent
46e145d658
commit
d81079740c
5 changed files with 208 additions and 7 deletions
18
dist/index.d.ts
vendored
18
dist/index.d.ts
vendored
|
|
@ -10,7 +10,19 @@ type Frontmatter = {
|
|||
socialImage?: string;
|
||||
tags?: string[];
|
||||
} & Record<string, unknown>;
|
||||
type LocalQuartzPluginData = {
|
||||
/**
|
||||
* Page data passed to user-provided `imageStructure` callbacks.
|
||||
*
|
||||
* This is deliberately narrower than `@quartz-community/types`'
|
||||
* `QuartzPluginData`: it documents exactly which fields the og-image
|
||||
* emitter promises to populate before invoking `imageStructure`. The
|
||||
* extra `text?` field is synthesised by this plugin from the page's
|
||||
* rendered content prior to the callback.
|
||||
*
|
||||
* Do NOT replace this with `QuartzPluginData`: doing so would advertise
|
||||
* fields (e.g. `htmlAst`) that this emitter does not pass through.
|
||||
*/
|
||||
type SocialImageFileData = {
|
||||
slug?: FullSlug;
|
||||
frontmatter?: Frontmatter;
|
||||
description?: string;
|
||||
|
|
@ -37,9 +49,9 @@ type ImageOptions = {
|
|||
description: string;
|
||||
fonts: SatoriOptions["fonts"];
|
||||
cfg: GlobalConfiguration;
|
||||
fileData: LocalQuartzPluginData;
|
||||
fileData: SocialImageFileData;
|
||||
};
|
||||
declare const CustomOgImagesEmitterName = "CustomOgImages";
|
||||
declare const CustomOgImages: QuartzEmitterPlugin<Partial<SocialImageOptions>>;
|
||||
|
||||
export { CustomOgImages, CustomOgImagesEmitterName, type ImageOptions, type SocialImageOptions, type UserOpts };
|
||||
export { CustomOgImages, CustomOgImagesEmitterName, type ImageOptions, type SocialImageFileData, type SocialImageOptions, type UserOpts };
|
||||
|
|
|
|||
2
dist/index.js.map
vendored
2
dist/index.js.map
vendored
File diff suppressed because one or more lines are too long
|
|
@ -32,7 +32,19 @@ type Frontmatter = {
|
|||
tags?: string[];
|
||||
} & Record<string, unknown>;
|
||||
|
||||
type LocalQuartzPluginData = {
|
||||
/**
|
||||
* Page data passed to user-provided `imageStructure` callbacks.
|
||||
*
|
||||
* This is deliberately narrower than `@quartz-community/types`'
|
||||
* `QuartzPluginData`: it documents exactly which fields the og-image
|
||||
* emitter promises to populate before invoking `imageStructure`. The
|
||||
* extra `text?` field is synthesised by this plugin from the page's
|
||||
* rendered content prior to the callback.
|
||||
*
|
||||
* Do NOT replace this with `QuartzPluginData`: doing so would advertise
|
||||
* fields (e.g. `htmlAst`) that this emitter does not pass through.
|
||||
*/
|
||||
export type SocialImageFileData = {
|
||||
slug?: FullSlug;
|
||||
frontmatter?: Frontmatter;
|
||||
description?: string;
|
||||
|
|
@ -190,7 +202,7 @@ export type ImageOptions = {
|
|||
description: string;
|
||||
fonts: SatoriOptions["fonts"];
|
||||
cfg: GlobalConfiguration;
|
||||
fileData: LocalQuartzPluginData;
|
||||
fileData: SocialImageFileData;
|
||||
};
|
||||
|
||||
export const defaultImage: SocialImageOptions["imageStructure"] = ({
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export { CustomOgImages, CustomOgImagesEmitterName } from "./emitter.js";
|
||||
export type { SocialImageOptions, ImageOptions, UserOpts } from "./emitter.js";
|
||||
export type { SocialImageFileData, SocialImageOptions, ImageOptions, UserOpts } from "./emitter.js";
|
||||
export type { QuartzEmitterPlugin } from "@quartz-community/types";
|
||||
|
|
|
|||
177
test/socialImageFileData.test.ts
Normal file
177
test/socialImageFileData.test.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { JSX } from "preact";
|
||||
import type { FullSlug, GlobalConfiguration } from "@quartz-community/types";
|
||||
import {
|
||||
defaultImage,
|
||||
type ImageOptions,
|
||||
type SocialImageFileData,
|
||||
type SocialImageOptions,
|
||||
type UserOpts,
|
||||
} from "../src/emitter";
|
||||
import type { Theme } from "../src/theme";
|
||||
|
||||
function makeTheme(): Theme {
|
||||
const palette = {
|
||||
light: "#fff",
|
||||
lightgray: "#eee",
|
||||
gray: "#888",
|
||||
darkgray: "#444",
|
||||
dark: "#111",
|
||||
secondary: "#284b63",
|
||||
tertiary: "#84a59d",
|
||||
highlight: "rgba(143,159,169,0.15)",
|
||||
textHighlight: "#fff23688",
|
||||
};
|
||||
return {
|
||||
cdnCaching: true,
|
||||
fontOrigin: "googleFonts",
|
||||
typography: {
|
||||
header: "Schibsted Grotesk",
|
||||
body: "Source Sans Pro",
|
||||
code: "IBM Plex Mono",
|
||||
},
|
||||
colors: {
|
||||
lightMode: palette,
|
||||
darkMode: palette,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeCfg(): GlobalConfiguration {
|
||||
return {
|
||||
locale: "en-US",
|
||||
baseUrl: "example.test",
|
||||
theme: makeTheme() as unknown as GlobalConfiguration["theme"],
|
||||
};
|
||||
}
|
||||
|
||||
function makeUserOpts(): UserOpts {
|
||||
return {
|
||||
colorScheme: "lightMode",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
excludeRoot: false,
|
||||
};
|
||||
}
|
||||
|
||||
function makeImageOptions(overrides: Partial<ImageOptions> = {}): ImageOptions {
|
||||
return {
|
||||
title: "My Page",
|
||||
description: "My Description",
|
||||
fonts: [],
|
||||
cfg: makeCfg(),
|
||||
fileData: {
|
||||
slug: "notes/example" as FullSlug,
|
||||
frontmatter: { title: "My Page", tags: ["tag-a"] },
|
||||
description: "My Description",
|
||||
text: "Some rendered body text",
|
||||
filePath: "content/notes/example.md",
|
||||
dates: { created: new Date("2024-01-01"), modified: new Date("2024-02-01") },
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("SocialImageFileData contract", () => {
|
||||
it("allows the documented fields via object-literal assignment", () => {
|
||||
const sample: SocialImageFileData = {
|
||||
slug: "notes/a" as FullSlug,
|
||||
frontmatter: {
|
||||
title: "t",
|
||||
description: "d",
|
||||
socialDescription: "sd",
|
||||
socialImage: "/cover.png",
|
||||
tags: ["x", "y"],
|
||||
},
|
||||
description: "d",
|
||||
text: "body",
|
||||
filePath: "content/notes/a.md",
|
||||
dates: {
|
||||
created: new Date("2024-01-01"),
|
||||
modified: new Date("2024-02-01"),
|
||||
published: new Date("2024-03-01"),
|
||||
},
|
||||
};
|
||||
expect(sample.slug).toBe("notes/a");
|
||||
expect(sample.frontmatter?.tags).toEqual(["x", "y"]);
|
||||
expect(sample.text).toBe("body");
|
||||
});
|
||||
|
||||
it("allows every documented field to be omitted", () => {
|
||||
const empty: SocialImageFileData = {};
|
||||
expect(empty).toEqual({});
|
||||
});
|
||||
|
||||
it("accepts arbitrary extra keys on frontmatter via Record<string, unknown>", () => {
|
||||
const sample: SocialImageFileData = {
|
||||
frontmatter: {
|
||||
title: "t",
|
||||
draft: true,
|
||||
custom: { nested: 1 },
|
||||
},
|
||||
};
|
||||
expect((sample.frontmatter as { draft?: unknown }).draft).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("user-provided imageStructure contract", () => {
|
||||
it("receives a SocialImageFileData fileData", () => {
|
||||
let captured: SocialImageFileData | undefined;
|
||||
const userImageStructure: SocialImageOptions["imageStructure"] = ({ fileData }) => {
|
||||
captured = fileData;
|
||||
return null as unknown as JSX.Element;
|
||||
};
|
||||
|
||||
userImageStructure({
|
||||
...makeImageOptions(),
|
||||
userOpts: makeUserOpts(),
|
||||
iconBase64: undefined,
|
||||
});
|
||||
|
||||
expect(captured?.slug).toBe("notes/example");
|
||||
expect(captured?.frontmatter?.tags).toEqual(["tag-a"]);
|
||||
expect(captured?.text).toBe("Some rendered body text");
|
||||
});
|
||||
});
|
||||
|
||||
describe("defaultImage", () => {
|
||||
it("returns a preact VNode when given a populated SocialImageFileData", () => {
|
||||
const result = defaultImage({
|
||||
...makeImageOptions(),
|
||||
userOpts: makeUserOpts(),
|
||||
iconBase64: undefined,
|
||||
});
|
||||
|
||||
const vnode = result as unknown as { type?: unknown; props?: unknown };
|
||||
expect(vnode).toBeDefined();
|
||||
expect(vnode.type).toBeDefined();
|
||||
expect(vnode.props).toBeDefined();
|
||||
});
|
||||
|
||||
it("tolerates minimal SocialImageFileData (no text, no dates, empty frontmatter)", () => {
|
||||
const result = defaultImage({
|
||||
...makeImageOptions({
|
||||
fileData: {
|
||||
slug: "notes/minimal" as FullSlug,
|
||||
frontmatter: {},
|
||||
},
|
||||
}),
|
||||
userOpts: makeUserOpts(),
|
||||
iconBase64: undefined,
|
||||
});
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it("uses the readingTimeText override when provided", () => {
|
||||
const userOpts: UserOpts = {
|
||||
...makeUserOpts(),
|
||||
readingTimeText: (minutes) => `${minutes} minutes read override`,
|
||||
};
|
||||
const result = defaultImage({
|
||||
...makeImageOptions(),
|
||||
userOpts,
|
||||
iconBase64: undefined,
|
||||
});
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue