diff --git a/packages/astro-ghostcms-theme-default/package.json b/packages/astro-ghostcms-theme-default/package.json index bf39b09f..44018b4b 100644 --- a/packages/astro-ghostcms-theme-default/package.json +++ b/packages/astro-ghostcms-theme-default/package.json @@ -1,7 +1,7 @@ { "name": "@matthiesenxyz/astro-ghostcms-theme-default", "description": "Default Theme for astro-ghostcms", - "version": "0.1.6", + "version": "0.1.7", "homepage": "https://astro-ghostcms.xyz/", "type": "module", "license": "MIT", @@ -50,7 +50,7 @@ "astro": "^4.2.1" }, "dependencies": { - "@matthiesenxyz/astro-ghostcms": "^3.1.5", + "@matthiesenxyz/astro-ghostcms": "^3.1.6", "astro-font": "^0.0.77", "sass": "^1.70.0" } diff --git a/packages/astro-ghostcms-theme-default/src/layouts/default.astro b/packages/astro-ghostcms-theme-default/src/layouts/default.astro index 3a7f48cd..54814fda 100644 --- a/packages/astro-ghostcms-theme-default/src/layouts/default.astro +++ b/packages/astro-ghostcms-theme-default/src/layouts/default.astro @@ -15,6 +15,17 @@ export type Props = { permalink?: string; }; +export const getOgImagePath = (filename: string = "index") => { + if (filename.startsWith("/")) filename = filename.substring(1); + if (filename.endsWith("/")) + filename = filename.substring(0, filename.length - 1); + if (filename === "") filename = "index"; + return `./open-graph/${filename}.png`; +}; + +const ogImage = new URL(getOgImagePath(Astro.url.pathname), Astro.url.origin) + .href; + const { content, permalink, image, settings, bodyClass = "" } = Astro.props as Props; const ghostAccentColor = settings.accent_color; --- @@ -58,14 +69,14 @@ const ghostAccentColor = settings.accent_color; {permalink && } {content?.description && } - {image && } + {permalink && } {content?.description && } - {image && } + { post.primary_author ? ( diff --git a/packages/astro-ghostcms/index.ts b/packages/astro-ghostcms/index.ts index 27c836bb..070791ff 100644 --- a/packages/astro-ghostcms/index.ts +++ b/packages/astro-ghostcms/index.ts @@ -8,6 +8,8 @@ import { loadEnv } from 'vite'; import { fromZodError } from "zod-validation-error"; import { addVirtualImport } from "./src/utils/add-virtual-import"; +export * from "./types.js" + /** INTERNAL CONSTANTS */ const IC = { /** INTERNAL PACKAGE NAME */ @@ -139,6 +141,31 @@ export default function GhostCMS(options: UserConfig): AstroIntegration { }); } else { if( !GCD.dCO ) { logger.info(IC.idRSS)}} + injectRoute({ + pattern: '/open-graph/[slug].png', + entrypoint: `${IC.PKG}/open-graph/[slug].png.ts` + }); + injectRoute({ + pattern: '/open-graph/index.png', + entrypoint: `${IC.PKG}/open-graph/index.png.ts` + }); + injectRoute({ + pattern: '/open-graph/authors.png', + entrypoint: `${IC.PKG}/open-graph/authors.png.ts` + }); + injectRoute({ + pattern: '/open-graph/author/[slug].png', + entrypoint: `${IC.PKG}/open-graph/author/[slug].png.ts` + }); + injectRoute({ + pattern: '/open-graph/tags.png', + entrypoint: `${IC.PKG}/open-graph/tags.png.ts` + }); + injectRoute({ + pattern: '/open-graph/tag/[slug].png', + entrypoint: `${IC.PKG}/open-graph/tag/[slug].png.ts` + }); + // THEME ROUTES if( !GCD.dCO ) { logger.info( IC.ITR )} @@ -203,7 +230,15 @@ export default function GhostCMS(options: UserConfig): AstroIntegration { // FINAL STEP TO KEEP INTEGRATION LIVE try { updateConfig( { // UPDATE ASTRO CONFIG WITH INTEGRATED INTEGRATIONS - integrations: [ ghostSitemap( GCD.SM ), ghostRobots( GCD.RTXT ) ], + integrations: [ + ghostSitemap(GCD.SM), + ghostRobots(GCD.RTXT) + ], + vite: { + optimizeDeps: { + exclude: ["@resvg/resvg-js"], + } + } // LOAD VITE AND SETUP viteGhostCMS Configs }) } catch ( e ) { logger.error( e as string ); diff --git a/packages/astro-ghostcms/package.json b/packages/astro-ghostcms/package.json index d1156057..fe64582b 100644 --- a/packages/astro-ghostcms/package.json +++ b/packages/astro-ghostcms/package.json @@ -1,7 +1,7 @@ { "name": "@matthiesenxyz/astro-ghostcms", "description": "Astro GhostCMS integration to allow easier importing of GhostCMS Content", - "version": "3.1.5", + "version": "3.1.6", "homepage": "https://astro-ghostcms.xyz/", "type": "module", "license": "MIT", @@ -53,7 +53,13 @@ "./404.astro": "./src/default-routes/404/404.astro", "./rss.xml.ts": "./src/default-routes/rss.xml.ts", "./config": "./src/integrations/virtual-config.ts", - "./types": "./types.ts" + "./types": "./types.ts", + "./open-graph/[slug].png.ts": "./src/default-routes/open-graph/[slug].png.ts", + "./open-graph/index.png.ts": "./src/default-routes/open-graph/index.png.ts", + "./open-graph/authors.png.ts": "./src/default-routes/open-graph/authors.png.ts", + "./open-graph/author/[slug].png.ts": "./src/default-routes/open-graph/author/[slug].png.ts", + "./open-graph/tags.png.ts": "./src/default-routes/open-graph/tags.png.ts", + "./open-graph/tag/[slug].png.ts": "./src/default-routes/open-graph/tag/[slug].png.ts" }, "scripts": { "test": "vitest run", @@ -62,7 +68,7 @@ "test:ci": "vitest run --coverage.enabled --coverage.reporter='text-summary'" }, "peerDependencies": { - "astro": "^4.2.3" + "astro": "^4.2.6" }, "devDependencies": { "@astrojs/check": "^0.4.1", @@ -82,11 +88,14 @@ "vitest-fetch-mock": "^0.2.2" }, "dependencies": { - "@matthiesenxyz/astro-ghostcms-theme-default": "^0.1.3", + "@matthiesenxyz/astro-ghostcms-theme-default": "^0.1.7", "@astrojs/rss": "^4.0.4", "@astrojs/sitemap": "^3.0.5", + "@resvg/resvg-js": "^2.6.0", "@ts-ghost/core-api": "^5.1.2", "astro-robots-txt": "^1.0.0", + "satori": "^0.10.11", + "satori-html": "^0.3.2", "vite": "^5.0.12", "vite-tsconfig-paths": "^4.2.2", "zod": "^3.22.4", diff --git a/packages/astro-ghostcms/src/default-routes/open-graph/[slug].png.ts b/packages/astro-ghostcms/src/default-routes/open-graph/[slug].png.ts new file mode 100644 index 00000000..39bc29f0 --- /dev/null +++ b/packages/astro-ghostcms/src/default-routes/open-graph/[slug].png.ts @@ -0,0 +1,49 @@ +import type { APIRoute, GetStaticPaths, GetStaticPathsItem, InferGetStaticPropsType } from "astro"; +import { satoriOG } from "../../integrations/satori.js"; +import { html } from "satori-html"; +import { invariant, getAllPosts, getAllPages, getSettings } from "../../api/index.js"; + +export const getStaticPaths: GetStaticPaths = async () => { + const result: GetStaticPathsItem[] = []; + const [posts, pages, settings] = await Promise.all([getAllPosts(), await getAllPages(), await getSettings()]); + const allPosts = [...posts, ...pages]; + invariant(settings, "Settings are required"); + + allPosts.map(allPosts => { + result.push({ + params: {slug: allPosts.slug}, + props: { + title: allPosts.title, + image: allPosts.feature_image + } + }) + }) + return result + } +export type Props = InferGetStaticPropsType; + + +export const GET: APIRoute = async ({ props, site}) => { + const settings = await getSettings(); + invariant(settings, "Settings are required"); + const fontFile = await fetch( + "https://og-playground.vercel.app/inter-latin-ext-700-normal.woff", + ); + const fontData: ArrayBuffer = await fontFile.arrayBuffer(); + + return await satoriOG({ + template: html`
${settings.title} - ${props.title}
${site}
`, + width: 1920, + height: 1080, + }).toResponse({ + satori: { + fonts: [ + { + name: "Inter Latin", + data: fontData, + style: "normal", + }, + ], + }, + }); +}; \ No newline at end of file diff --git a/packages/astro-ghostcms/src/default-routes/open-graph/author/[slug].png.ts b/packages/astro-ghostcms/src/default-routes/open-graph/author/[slug].png.ts new file mode 100644 index 00000000..ed79a8df --- /dev/null +++ b/packages/astro-ghostcms/src/default-routes/open-graph/author/[slug].png.ts @@ -0,0 +1,55 @@ +import type { APIRoute, GetStaticPaths, GetStaticPathsItem, InferGetStaticParamsType, InferGetStaticPropsType } from "astro"; +import { satoriOG } from "../../../integrations/satori.js"; +import { html } from "satori-html"; +import { invariant, getAllPosts, getSettings, getAllAuthors } from "../../../api/index.js"; + +export const getStaticPaths: GetStaticPaths = async () => { + const result: GetStaticPathsItem[] = []; + const posts = await getAllPosts(); + const { authors } = await getAllAuthors(); + invariant(authors, "Settings are required"); + const settings = await getSettings(); + invariant(settings, "Settings are required"); + + authors.map((author) => { + const filteredPosts = posts.filter((post) => + post.authors?.map((author) => author.slug).includes(author.slug) + ); + result.push( { + params: { slug: author.slug }, + props: { + posts: filteredPosts, + settings, + author, + }, + }); + }); + return result; + } +export type Props = InferGetStaticPropsType; + + +export const GET: APIRoute = async ({ props, site }) => { + const settings = await getSettings(); + invariant(settings, "Settings are required"); + const fontFile = await fetch( + "https://og-playground.vercel.app/inter-latin-ext-700-normal.woff", + ); + const fontData: ArrayBuffer = await fontFile.arrayBuffer(); + + return await satoriOG({ + template: html`
${settings.title} - ${props.author.name}
${site}
`, + width: 1920, + height: 1080, + }).toResponse({ + satori: { + fonts: [ + { + name: "Inter Latin", + data: fontData, + style: "normal", + }, + ], + }, + }); +}; \ No newline at end of file diff --git a/packages/astro-ghostcms/src/default-routes/open-graph/authors.png.ts b/packages/astro-ghostcms/src/default-routes/open-graph/authors.png.ts new file mode 100644 index 00000000..65438463 --- /dev/null +++ b/packages/astro-ghostcms/src/default-routes/open-graph/authors.png.ts @@ -0,0 +1,49 @@ +import type { APIRoute, GetStaticPaths, GetStaticPathsItem, InferGetStaticPropsType } from "astro"; +import { satoriOG } from "../../integrations/satori.js"; +import { html } from "satori-html"; +import { invariant, getAllPosts, getAllPages, getSettings } from "../../api/index.js"; + +export const getStaticPaths: GetStaticPaths = async () => { + const result: GetStaticPathsItem[] = []; + const [posts, pages, settings] = await Promise.all([getAllPosts(), await getAllPages(), await getSettings()]); + const allPosts = [...posts, ...pages]; + invariant(settings, "Settings are required"); + + allPosts.map(allPosts => { + result.push({ + params: {slug: allPosts.slug}, + props: { + title: allPosts.title, + image: allPosts.feature_image + } + }) + }) + return result + } +export type Props = InferGetStaticPropsType; + + +export const GET: APIRoute = async ({ props, site}) => { + const settings = await getSettings(); + invariant(settings, "Settings are required"); + const fontFile = await fetch( + "https://og-playground.vercel.app/inter-latin-ext-700-normal.woff", + ); + const fontData: ArrayBuffer = await fontFile.arrayBuffer(); + + return await satoriOG({ + template: html`
${settings.title} - Authors
${site}
`, + width: 1920, + height: 1080, + }).toResponse({ + satori: { + fonts: [ + { + name: "Inter Latin", + data: fontData, + style: "normal", + }, + ], + }, + }); +}; \ No newline at end of file diff --git a/packages/astro-ghostcms/src/default-routes/open-graph/index.png.ts b/packages/astro-ghostcms/src/default-routes/open-graph/index.png.ts new file mode 100644 index 00000000..54091b24 --- /dev/null +++ b/packages/astro-ghostcms/src/default-routes/open-graph/index.png.ts @@ -0,0 +1,49 @@ +import type { APIRoute, GetStaticPaths, GetStaticPathsItem, InferGetStaticPropsType } from "astro"; +import { satoriOG } from "../../integrations/satori.js"; +import { html } from "satori-html"; +import { invariant, getAllPosts, getAllPages, getSettings } from "../../api/index.js"; + +export const getStaticPaths: GetStaticPaths = async () => { + const result: GetStaticPathsItem[] = []; + const [posts, pages, settings] = await Promise.all([getAllPosts(), await getAllPages(), await getSettings()]); + const allPosts = [...posts, ...pages]; + invariant(settings, "Settings are required"); + + allPosts.map(allPosts => { + result.push({ + params: {slug: allPosts.slug}, + props: { + title: allPosts.title, + image: allPosts.feature_image + } + }) + }) + return result + } +export type Props = InferGetStaticPropsType; + + +export const GET: APIRoute = async ({ props, site}) => { + const settings = await getSettings(); + invariant(settings, "Settings are required"); + const fontFile = await fetch( + "https://og-playground.vercel.app/inter-latin-ext-700-normal.woff", + ); + const fontData: ArrayBuffer = await fontFile.arrayBuffer(); + + return await satoriOG({ + template: html`
${settings.title} - Index
${site}
`, + width: 1920, + height: 1080, + }).toResponse({ + satori: { + fonts: [ + { + name: "Inter Latin", + data: fontData, + style: "normal", + }, + ], + }, + }); +}; \ No newline at end of file diff --git a/packages/astro-ghostcms/src/default-routes/open-graph/tag/[slug].png.ts b/packages/astro-ghostcms/src/default-routes/open-graph/tag/[slug].png.ts new file mode 100644 index 00000000..a1f7f2aa --- /dev/null +++ b/packages/astro-ghostcms/src/default-routes/open-graph/tag/[slug].png.ts @@ -0,0 +1,54 @@ +import type { APIRoute, GetStaticPaths, GetStaticPathsItem, InferGetStaticPropsType } from "astro"; +import { satoriOG } from "../../../integrations/satori.js"; +import { html } from "satori-html"; +import { invariant, getAllPosts, getSettings, getAllTags } from "../../../api/index.js"; + +export const getStaticPaths: GetStaticPaths = async () => { + const result: GetStaticPathsItem[] = []; + const posts = await getAllPosts(); + const { tags } = await getAllTags(); + const settings = await getSettings(); + invariant(settings, "Settings are required"); + + tags.map((tag) => { + const filteredPosts = posts.filter((post) => + post.tags?.map((tag) => tag.slug).includes(tag.slug) + ); + result.push( { + params: { slug: tag.slug }, + props: { + posts: filteredPosts, + settings, + tag, + }, + }); + }); + return result; + } +export type Props = InferGetStaticPropsType; + + +export const GET: APIRoute = async ({ props, site}) => { + const settings = await getSettings(); + invariant(settings, "Settings are required"); + const fontFile = await fetch( + "https://og-playground.vercel.app/inter-latin-ext-700-normal.woff", + ); + const fontData: ArrayBuffer = await fontFile.arrayBuffer(); + + return await satoriOG({ + template: html`
${settings.title} - Tag: ${props.tag.name}
${site}
`, + width: 1920, + height: 1080, + }).toResponse({ + satori: { + fonts: [ + { + name: "Inter Latin", + data: fontData, + style: "normal", + }, + ], + }, + }); +}; \ No newline at end of file diff --git a/packages/astro-ghostcms/src/default-routes/open-graph/tags.png.ts b/packages/astro-ghostcms/src/default-routes/open-graph/tags.png.ts new file mode 100644 index 00000000..efe43795 --- /dev/null +++ b/packages/astro-ghostcms/src/default-routes/open-graph/tags.png.ts @@ -0,0 +1,49 @@ +import type { APIRoute, GetStaticPaths, GetStaticPathsItem, InferGetStaticPropsType } from "astro"; +import { satoriOG } from "../../integrations/satori.js"; +import { html } from "satori-html"; +import { invariant, getAllPosts, getAllPages, getSettings } from "../../api/index.js"; + +export const getStaticPaths: GetStaticPaths = async () => { + const result: GetStaticPathsItem[] = []; + const [posts, pages, settings] = await Promise.all([getAllPosts(), await getAllPages(), await getSettings()]); + const allPosts = [...posts, ...pages]; + invariant(settings, "Settings are required"); + + allPosts.map(allPosts => { + result.push({ + params: {slug: allPosts.slug}, + props: { + title: allPosts.title, + image: allPosts.feature_image + } + }) + }) + return result + } +export type Props = InferGetStaticPropsType; + + +export const GET: APIRoute = async ({ props, site}) => { + const settings = await getSettings(); + invariant(settings, "Settings are required"); + const fontFile = await fetch( + "https://og-playground.vercel.app/inter-latin-ext-700-normal.woff", + ); + const fontData: ArrayBuffer = await fontFile.arrayBuffer(); + + return await satoriOG({ + template: html`
${settings.title} - Tags
${site}
`, + width: 1920, + height: 1080, + }).toResponse({ + satori: { + fonts: [ + { + name: "Inter Latin", + data: fontData, + style: "normal", + }, + ], + }, + }); +}; \ No newline at end of file diff --git a/packages/astro-ghostcms/src/integrations/satori.ts b/packages/astro-ghostcms/src/integrations/satori.ts new file mode 100644 index 00000000..77579a26 --- /dev/null +++ b/packages/astro-ghostcms/src/integrations/satori.ts @@ -0,0 +1,49 @@ +import { Resvg } from "@resvg/resvg-js"; +import satori from "satori"; +import type { + SatoriAstroOGOptions, + ToSvgOptions, + ToImageOptions, + ToResponseOptions, +} from "../../types.js"; + +export const satoriOG = ({ + width, + height, + template, +}: SatoriAstroOGOptions) => { + return { + async toSvg(options: ToSvgOptions) { + return await satori(template, { width, height, ...options }); + }, + async toImage({ + satori: satoriOptions, + resvg: _resvgOptions, + }: ToImageOptions) { + const resvgOptions = + typeof _resvgOptions === "function" + ? _resvgOptions({ width, height }) + : _resvgOptions; + + return new Resvg(await this.toSvg(satoriOptions), { + fitTo: { mode: "width", value: width }, + ...resvgOptions, + }) + .render() + .asPng(); + }, + async toResponse({ response: init, ...rest }: ToResponseOptions) { + const image = await this.toImage(rest); + + return new Response(image, { + ...init, + headers: { + "Content-Type": "image/png", + "Content-Length": image.length.toString(), + "Cache-Control": "public, max-age=31536000, immutable", + ...init?.headers, + }, + }); + }, + }; +}; \ No newline at end of file diff --git a/packages/astro-ghostcms/types.ts b/packages/astro-ghostcms/types.ts index fa8c9574..5e40c593 100644 --- a/packages/astro-ghostcms/types.ts +++ b/packages/astro-ghostcms/types.ts @@ -1,3 +1,6 @@ +import type { Resvg } from "@resvg/resvg-js"; +import type satori from "satori"; + export type { UserConfig } from './src/schemas'; export type { @@ -7,4 +10,23 @@ export type { } from './src/api'; -export type { ContentAPICredentials, APIVersions } from "@ts-ghost/core-api"; \ No newline at end of file +export type { ContentAPICredentials, APIVersions } from "@ts-ghost/core-api"; + +type SatoriParameters = Parameters; +type SatoriOptions = SatoriParameters[1]; +type ResvgOptions = NonNullable[1]>; + +export type SatoriAstroOGOptions = { + template: SatoriParameters[0]; + width: number; + height: number; +}; + +export type ToSvgOptions = Omit; +export type ToImageOptions = { + satori: ToSvgOptions; + resvg?: + | ResvgOptions + | ((params: { width: number; height: number }) => ResvgOptions); +}; +export type ToResponseOptions = ToImageOptions & { response?: ResponseInit }; \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 498f05c2..83bafb88 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,7 +27,7 @@ importers: specifier: 3.1.5 version: link:../packages/astro-ghostcms '@matthiesenxyz/astro-ghostcms-theme-default': - specifier: 0.1.5 + specifier: 0.1.6 version: link:../packages/astro-ghostcms-theme-default astro: specifier: ^4.2.6 @@ -51,15 +51,24 @@ importers: '@matthiesenxyz/astro-ghostcms-theme-default': specifier: ^0.1.3 version: link:../astro-ghostcms-theme-default + '@resvg/resvg-js': + specifier: ^2.6.0 + version: 2.6.0 '@ts-ghost/core-api': specifier: ^5.1.2 version: 5.1.2 astro: - specifier: ^4.2.3 - version: 4.2.4(@types/node@20.11.10)(typescript@5.3.3) + specifier: ^4.2.6 + version: 4.2.6(@types/node@20.11.10)(typescript@5.3.3) astro-robots-txt: specifier: ^1.0.0 version: 1.0.0 + satori: + specifier: ^0.10.11 + version: 0.10.13 + satori-html: + specifier: ^0.3.2 + version: 0.3.2 vite: specifier: ^5.0.12 version: 5.0.12(@types/node@20.11.10) @@ -1592,6 +1601,132 @@ packages: engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} dev: true + /@resvg/resvg-js-android-arm-eabi@2.6.0: + resolution: {integrity: sha512-lJnZ/2P5aMocrFMW7HWhVne5gH82I8xH6zsfH75MYr4+/JOaVcGCTEQ06XFohGMdYRP3v05SSPLPvTM/RHjxfA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-android-arm64@2.6.0: + resolution: {integrity: sha512-N527f529bjMwYWShZYfBD60dXA4Fux+D695QsHQ93BDYZSHUoOh1CUGUyICevnTxs7VgEl98XpArmUWBZQVMfQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-darwin-arm64@2.6.0: + resolution: {integrity: sha512-MabUKLVayEwlPo0mIqAmMt+qESN8LltCvv5+GLgVga1avpUrkxj/fkU1TKm8kQegutUjbP/B0QuMuUr0uhF8ew==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-darwin-x64@2.6.0: + resolution: {integrity: sha512-zrFetdnSw/suXjmyxSjfDV7i61hahv6DDG6kM7BYN2yJ3Es5+BZtqYZTcIWogPJedYKmzN1YTMWGd/3f0ubFiA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-linux-arm-gnueabihf@2.6.0: + resolution: {integrity: sha512-sH4gxXt7v7dGwjGyzLwn7SFGvwZG6DQqLaZ11MmzbCwd9Zosy1TnmrMJfn6TJ7RHezmQMgBPi18bl55FZ1AT4A==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-linux-arm64-gnu@2.6.0: + resolution: {integrity: sha512-fCyMncqCJtrlANADIduYF4IfnWQ295UKib7DAxFXQhBsM9PLDTpizr0qemZcCNadcwSVHnAIzL4tliZhCM8P6A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-linux-arm64-musl@2.6.0: + resolution: {integrity: sha512-ouLjTgBQHQyxLht4FdMPTvuY8xzJigM9EM2Tlu0llWkN1mKyTQrvYWi6TA6XnKdzDJHy7ZLpWpjZi7F5+Pg+Vg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-linux-x64-gnu@2.6.0: + resolution: {integrity: sha512-n3zC8DWsvxC1AwxpKFclIPapDFibs5XdIRoV/mcIlxlh0vseW1F49b97F33BtJQRmlntsqqN6GMMqx8byB7B+Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-linux-x64-musl@2.6.0: + resolution: {integrity: sha512-n4tasK1HOlAxdTEROgYA1aCfsEKk0UOFDNd/AQTTZlTmCbHKXPq+O8npaaKlwXquxlVK8vrkcWbksbiGqbCAcw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-win32-arm64-msvc@2.6.0: + resolution: {integrity: sha512-X2+EoBJFwDI5LDVb51Sk7ldnVLitMGr9WwU/i21i3fAeAXZb3hM16k67DeTy16OYkT2dk/RfU1tP1wG+rWbz2Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-win32-ia32-msvc@2.6.0: + resolution: {integrity: sha512-L7oevWjQoUgK5W1fCKn0euSVemhDXVhrjtwqpc7MwBKKimYeiOshO1Li1pa8bBt5PESahenhWgdB6lav9O0fEg==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-win32-x64-msvc@2.6.0: + resolution: {integrity: sha512-8lJlghb+Unki5AyKgsnFbRJwkEj9r1NpwyuBG8yEJiG1W9eEGl03R3I7bsVa3haof/3J1NlWf0rzSa1G++A2iw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js@2.6.0: + resolution: {integrity: sha512-Tf3YpbBKcQn991KKcw/vg7vZf98v01seSv6CVxZBbRkL/xyjnoYB6KgrFL6zskT1A4dWC/vg77KyNOW+ePaNlA==} + engines: {node: '>= 10'} + optionalDependencies: + '@resvg/resvg-js-android-arm-eabi': 2.6.0 + '@resvg/resvg-js-android-arm64': 2.6.0 + '@resvg/resvg-js-darwin-arm64': 2.6.0 + '@resvg/resvg-js-darwin-x64': 2.6.0 + '@resvg/resvg-js-linux-arm-gnueabihf': 2.6.0 + '@resvg/resvg-js-linux-arm64-gnu': 2.6.0 + '@resvg/resvg-js-linux-arm64-musl': 2.6.0 + '@resvg/resvg-js-linux-x64-gnu': 2.6.0 + '@resvg/resvg-js-linux-x64-musl': 2.6.0 + '@resvg/resvg-js-win32-arm64-msvc': 2.6.0 + '@resvg/resvg-js-win32-ia32-msvc': 2.6.0 + '@resvg/resvg-js-win32-x64-msvc': 2.6.0 + dev: false + /@rollup/rollup-android-arm-eabi@4.9.6: resolution: {integrity: sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==} cpu: [arm] @@ -1683,6 +1818,15 @@ packages: requiresBuild: true optional: true + /@shuding/opentype.js@1.4.0-beta.0: + resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} + engines: {node: '>= 8.0.0'} + hasBin: true + dependencies: + fflate: 0.7.4 + string.prototype.codepointat: 0.2.1 + dev: false + /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true @@ -2416,8 +2560,8 @@ packages: - typescript dev: false - /astro@4.2.4(@types/node@20.11.10)(typescript@5.3.3): - resolution: {integrity: sha512-z1f52lXkHf71M5HSLKrd5G1PH5/Zfq4kMp0iUT7Na5VHcPDma/NYFPFPewDxqV6UPmyxupj3xuooFaN3j8zaow==} + /astro@4.2.6(@types/node@20.11.10)(typescript@5.3.3): + resolution: {integrity: sha512-k5i8pEI2r45JTkoE0I4JyhOH/dZFpjUA4AONbRd9Gr1LtnGOhKHDftiYOrRLUGx91q7BzoW3DOk+h4yZM4yC3g==} engines: {node: '>=18.14.1', npm: '>=6.14.0'} hasBin: true dependencies: @@ -2441,6 +2585,7 @@ packages: clsx: 2.1.0 common-ancestor-path: 1.0.1 cookie: 0.6.0 + cssesc: 3.0.0 debug: 4.3.4 deterministic-object-hash: 2.0.2 devalue: 4.3.2 @@ -2620,6 +2765,11 @@ packages: resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} dev: false + /base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + dev: false + /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} dev: false @@ -2772,6 +2922,10 @@ packages: engines: {node: '>=14.16'} dev: false + /camelize@1.0.1: + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + dev: false + /caniuse-lite@1.0.30001579: resolution: {integrity: sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==} dev: false @@ -2996,10 +3150,31 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /css-background-parser@0.1.0: + resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==} + dev: false + + /css-box-shadow@1.0.0-3: + resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==} + dev: false + + /css-color-keywords@1.0.0: + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} + engines: {node: '>=4'} + dev: false + /css-selector-parser@3.0.4: resolution: {integrity: sha512-pnmS1dbKsz6KA4EW4BznyPL2xxkNDRg62hcD0v8g6DEw2W7hxOln5M953jsp9hmw5Dg57S6o/A8GOn37mbAgcQ==} dev: false + /css-to-react-native@3.2.0: + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} + dependencies: + camelize: 1.0.1 + css-color-keywords: 1.0.0 + postcss-value-parser: 4.2.0 + dev: false + /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -3372,6 +3547,10 @@ packages: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -3756,6 +3935,10 @@ packages: dependencies: reusify: 1.0.4 + /fflate@0.7.4: + resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} + dev: false + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -4312,6 +4495,11 @@ packages: space-separated-tokens: 2.0.2 dev: false + /hex-rgb@4.3.0: + resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} + engines: {node: '>=6'} + dev: false + /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -4742,6 +4930,13 @@ packages: type-check: 0.4.0 dev: true + /linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + dev: false + /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -5785,6 +5980,10 @@ packages: '@pagefind/windows-x64': 1.0.4 dev: false + /pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + dev: false + /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -5792,6 +5991,13 @@ packages: callsites: 3.1.0 dev: true + /parse-css-color@0.2.1: + resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} + dependencies: + color-name: 1.1.4 + hex-rgb: 4.3.0 + dev: false + /parse-entities@4.0.1: resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} dependencies: @@ -5922,6 +6128,10 @@ packages: cssesc: 3.0.0 util-deprecate: 1.0.2 + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: false + /postcss@8.4.33: resolution: {integrity: sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==} engines: {node: ^10 || ^12 || >=14} @@ -6401,6 +6611,28 @@ packages: immutable: 4.3.4 source-map-js: 1.0.2 + /satori-html@0.3.2: + resolution: {integrity: sha512-wjTh14iqADFKDK80e51/98MplTGfxz2RmIzh0GqShlf4a67+BooLywF17TvJPD6phO0Hxm7Mf1N5LtRYvdkYRA==} + dependencies: + ultrahtml: 1.5.2 + dev: false + + /satori@0.10.13: + resolution: {integrity: sha512-klCwkVYMQ/ZN5inJLHzrUmGwoRfsdP7idB5hfpJ1jfiJk1ErDitK8Hkc6Kll1+Ox2WtqEuGecSZLnmup3CGzvQ==} + engines: {node: '>=16'} + dependencies: + '@shuding/opentype.js': 1.4.0-beta.0 + css-background-parser: 0.1.0 + css-box-shadow: 1.0.0-3 + css-to-react-native: 3.2.0 + emoji-regex: 10.3.0 + escape-html: 1.0.3 + linebreak: 1.1.0 + parse-css-color: 0.2.1 + postcss-value-parser: 4.2.0 + yoga-wasm-web: 0.3.3 + dev: false + /sax@1.3.0: resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} dev: false @@ -6729,6 +6961,10 @@ packages: strip-ansi: 7.1.0 dev: false + /string.prototype.codepointat@0.2.1: + resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} + dev: false + /string.prototype.trim@1.2.8: resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} engines: {node: '>= 0.4'} @@ -6922,6 +7158,10 @@ packages: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true + /tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + dev: false + /tinybench@2.6.0: resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} dev: true @@ -7117,6 +7357,10 @@ packages: resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==} dev: true + /ultrahtml@1.5.2: + resolution: {integrity: sha512-qh4mBffhlkiXwDAOxvSGxhL0QEQsTbnP9BozOK3OYPEGvPvdWzvAUaXNtUSMdNsKDtuyjEbyVUPFZ52SSLhLqw==} + dev: false + /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -7133,6 +7377,13 @@ packages: resolution: {integrity: sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==} dev: false + /unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + dev: false + /unified@10.1.2: resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} dependencies: @@ -7491,7 +7742,7 @@ packages: vitest: '>=0.16.0' dependencies: cross-fetch: 3.1.8 - vitest: 1.2.1(@types/node@20.11.10) + vitest: 1.2.1 transitivePeerDependencies: - encoding dev: true @@ -7927,6 +8178,10 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} + /yoga-wasm-web@0.3.3: + resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} + dev: false + /zod-validation-error@3.0.0(zod@3.22.4): resolution: {integrity: sha512-x+agsJJG9rvC7axF0xqTEdZhJkLHyIZkdOAWDJSmwGPzxNHMHwtU6w2yDOAAP6yuSfTAUhAMJRBfhVGY64ySEQ==} engines: {node: '>=18.0.0'}