The Trans Directory
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(open-graph): generate OG images + further OG support (#740)

* Quartz sync: Aug 29, 2023, 10:17 PM

* feat: add basic satori og image generation

* Squashed commit of the following:

commit fa69c2a5656254251b74dbd5545bef000f67af2f
Author: Ben Schlegel <31989404+benschlegel@users.noreply.github.com>
Date: Thu Sep 21 19:35:11 2023 +0200

fix(explorer): increase consistency, explicitly use font-family (#496)

* fix(explorer): display name for folders without `index` file

* docs(explorer): add section for folder display names

* docs(explorer): fix broken wikilink

* fix(consistency): explicitly set font + label/link fix

Use consistent styling between folders with `folderClickBehavior: "link"` and `"collapse`

* Update quartz/components/styles/explorer.scss

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* Update quartz/components/styles/explorer.scss

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

---------

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

commit 8eb1554b13532a2441b41d2018800c56cfa84ce9
Author: Ben Schlegel <31989404+benschlegel@users.noreply.github.com>
Date: Thu Sep 21 18:54:33 2023 +0200

fix(explorer): display names for folders without frontmatter (#494)

* fix(explorer): display name for folders without `index` file

* docs(explorer): add section for folder display names

commit dcdeae4e7bd527945b887ca347b3b4408c03055b
Author: Ben Schlegel <31989404+benschlegel@users.noreply.github.com>
Date: Thu Sep 21 18:53:19 2023 +0200

docs(explorer): update default config + new example (#493)

commit 48452231d5fcd14ef218928bde9ae7e5bc745f4a
Author: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Wed Sep 20 16:09:18 2023 -0700

perf: memoize filetree computation (#490)

* perf: memoize filetree computation

* format

* var -> let

commit 16d33fb77193710bede887d6a177d2144b78fb67
Author: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Wed Sep 20 16:08:54 2023 -0700

feat: display name for folders, expand explorer a little bit (#489)

* feat: display name for folders, expand explorer a little bit

* update docs

commit b029eeadabe0877df6ec11443c68743f1494bc40
Author: Ben Schlegel <31989404+benschlegel@users.noreply.github.com>
Date: Wed Sep 20 22:55:29 2023 +0200

feat(explorer): improve accessibility and consistency (+ bug fix) (#488)

* feat(consistency): use `all: unset` on button

* style: improve accessibility and consistency for explorer

* fix: localStorage bug with folder name changes

* chore: bump quartz version

commit 6a9e6352e88aa9ff18e5b33cf2de442a250bd960
Author: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Wed Sep 20 13:52:45 2023 -0700

Revert "feat: Making Quartz available offline by making it a PWA (#465)"

This reverts commit d6301fae90d9f922618bf0f413e273156731eef7.

commit 70e029d151ccbb9aeab30a0f811b9f529b7f8818
Author: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Wed Sep 20 13:52:29 2023 -0700

Revert "docs: wording changes for offline support"

This reverts commit 52a172d1a4911080444ff797183e29ba8175741e.

commit 0bad3ce7990aa4ef417128f9d74c2947fe5117fd
Author: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Wed Sep 20 11:58:52 2023 -0700

docs: document enableToc

commit 52a172d1a4911080444ff797183e29ba8175741e
Author: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Wed Sep 20 11:40:36 2023 -0700

docs: wording changes for offline support

commit d6301fae90d9f922618bf0f413e273156731eef7
Author: Adam Brangenberg <adambrangenberg@proton.me>
Date: Wed Sep 20 20:38:13 2023 +0200

feat: Making Quartz available offline by making it a PWA (#465)

* Adding PWA and chaching for offline aviability

* renamed workbox config to fit Quartz' scheme

* Documenting new configuration

* Added missig umami documentation

* Fixed formatting so the build passes, thank you prettier :)

* specified caching strategies to improve performance

* formatting...

* fixing "404 manifest.json not found" on subdirectories by adding a / to manifestpath

* turning it into a plugin

* Removed Workbox-cli and updated @types/node

* Added Serviceworkercode to offline.ts

* formatting

* Removing workbox from docs

* applied suggestions

* Removed path.join for sw path

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* Removed path.join for manifest path

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* Removing path module import

* Added absolute path to manifests start_url and manifest "import" using baseUrl

* Adding protocol to baseurl

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* Adding protocol to start_url too then

* formatting...

* Adding fallback page

* Documenting offline plugin

* formatting...

* merge suggestion

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* merge suggestion

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* merge suggestion

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* merge suggestion

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* merge suggestion

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* merge suggestion

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* merge suggestion

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* merge suggestion

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* merge suggestion

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* merge suggestion

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* merge suggestion

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* merge suggestion

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* formatting...

* Fixing manifest path, all these nits hiding the actual issues .-.

* Offline fallback page through plugins, most things taken from 404 Plugin

* adding Offline Plugin to config

* formatting...

* Turned offline off as default and removed offline.md

---------

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

commit 27a6087dd5a25dd5031b86b9917adde6ef4b211a
Author: rwutscher <richard.wutscher@gmail.com>
Date: Tue Sep 19 21:26:30 2023 +0200

fix: tag regex no longer includes purely numerical 'tags' (#485)

* fix: tag regex no longer includes purely numerical 'tags'

* fix: formatting

* fix: use guard in findAndReplace() instead of expanding the regex

commit 1bf7e3d8b3966590ebfa3418d6fb2ce6a520c846
Author: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Tue Sep 19 10:22:39 2023 -0700

fix(nit): make defaultOptions on explorer not a function

commit cc31a40b0cb53cba7f51187cb6d68076c3f54c0f
Author: David Fischer <david@konst.fish>
Date: Tue Sep 19 18:25:51 2023 +0200

feat: support changes in system theme (#484)

* feat: support changes in system theme

* fix: run prettier

* fix: add content/.gitkeep

commit 0d3cf2922618774fc397dca8cb92fcf76fb0db02
Author: Ben Schlegel <31989404+benschlegel@users.noreply.github.com>
Date: Mon Sep 18 23:32:00 2023 +0200

docs: fix explorer example (#483)

* feat: dynamically generate og images, write to fs as png

* fix: og preview on discord

* feat: use `sharp` to convert to webp, add content headers

* feat: add config for theme (light or dark)

* feat: improve image margins, add font breakpoint

* feat: use config header + body fonts for satori

* perf: memoize fonts

* feat: use default og image if no path exists

* feat: add config option for social images

* feat: support custom og images via frontmatter

* refactor: clean font helpers, rename fonts helper

* refactor: make image generation cleaner

* refactor: move default image to own component

* chore: add todos

* fix: only set width/height header if known

* feat: remove html from description

* feat: make image dimensions configurable

* feat: pass userOpts to image generator

* feat: option for users to provide own image struct (satori)

This allows users to pass their own jsx for generating the default og image

* refactor: rename `defaultImage.tsx` > `socialImage.tsx`

* chore: improve comments + types

* refactor: rename socialImage frontmatter property

* feat: add frontmatter aliases for cover image

* feat: add frontmatter alias for obsidian publish

* docs: add documentation for social images

* feat: add `generateSocialImages` prop to config

* chore: update lock file

* fix: fix type error

* chore: update package.json

* chore: update package-lock.json

* docs: update docs

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* fix: clean url for use in metadata

* refactor: clean function signature

* feat: pass `fileData` to image generator

* CI: run format

* fix: file system import

* fix: merge paths using `joinSegments`

* fix: get output dir via `ctx.argv.output`

* chore: add explanation to font regex

* Squashed commit of the following:

commit 7164857f6e32aeba3da80112d604244aa8f557f4
Author: Aaron Pham <29749331+aarnphm@users.noreply.github.com>
Date: Fri Mar 15 21:17:42 2024 -0400

chore(ofm): remove unused (#999)

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

commit 47024022e834e1bb6c70f671cb32597f42aabd94
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri Mar 15 18:29:14 2024 -0400

chore(deps-dev): bump @types/node from 20.11.24 to 20.11.25 (#990)

Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.11.24 to 20.11.25.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
dependency-type: direct:development
update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit b98e4be66548e452419a1e4138d9d6d1981f891e
Author: Mara-Li <lili.simonetti@outlook.fr>
Date: Fri Mar 15 23:28:31 2024 +0100

feat(i18n): Add French translation for reading time (#998)

Signed-off-by: Mara-Li <lili.simonetti@outlook.fr>

commit 8be51a0504a7d819a9dab66d854dbef77878520a
Author: catcodeme <1020082805@qq.com>
Date: Fri Mar 15 14:25:01 2024 +0800

fix: wikiLink in table (#993)

* fix: wikiLink in table

- update regexp to make '\' to group in alias
- handle alias using block_id

* style: format with prettier

* style: add comment for block_ref(without alias) in table

---------

Co-authored-by: hulinjiang <hulinjiang@58.com>

commit 92cc23dc456ffc23285b83728fbc3434bbca5472
Author: Linus Sehn <37184648+linozen@users.noreply.github.com>
Date: Wed Mar 13 08:59:37 2024 +0100

feat(plugin): citations (#984)

* feat: add rehype-citations

* feat: add citations transformer plugin

* feat: add rehype-rewrite

* feat: add csl option and add no-popover to citation links

* revert: add rehype-rewrite

04b2692 'feat: add rehype-rewrite'

* feat: use existing package for html manipulation

* fix: remove `console.log()`

commit 097abc3cda0d9a6f3cfedfa3c6351648efd8d6b8
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon Mar 11 13:41:48 2024 -0700

chore(deps): bump async-mutex from 0.4.1 to 0.5.0 (#991)

Bumps [async-mutex](https://github.com/DirtyHairy/async-mutex) from 0.4.1 to 0.5.0.
- [Changelog](https://github.com/DirtyHairy/async-mutex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/DirtyHairy/async-mutex/compare/v0.4.1...v0.5.0)

---
updated-dependencies:
- dependency-name: async-mutex
dependency-type: direct:production
update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit a00324ddfdea9adf6aaec03abf4f076cb756ee7a
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon Mar 11 13:41:41 2024 -0700

chore(deps-dev): bump typescript from 5.3.3 to 5.4.2 (#989)

Bumps [typescript](https://github.com/Microsoft/TypeScript) from 5.3.3 to 5.4.2.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v5.3.3...v5.4.2)

---
updated-dependencies:
- dependency-name: typescript
dependency-type: direct:development
update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 9fff6d7d0dbaacad0f9988d4017b72738e6f6c58
Author: Mara-Li <lili.simonetti@outlook.fr>
Date: Mon Mar 11 17:46:53 2024 +0100

fix: spelling error (#987)

I really don't know why I translated this like that into "pas trouvé", and it bugged me a lot. I finally fixed it…

Signed-off-by: Mara-Li <lili.simonetti@outlook.fr>

commit 0f5a9d7b661a1f8610d7001f80a3fd2c52661e51
Author: Matt Vogel <mainmoniker@googlemail.com>
Date: Sun Mar 10 12:57:10 2024 -0400

feat: separated content meta (#929)

to allow for CSS styling

commit b4236e5142c31829cf809c0fbc8370ac22b6d1ba
Author: kabirgh <15871468+kabirgh@users.noreply.github.com>
Date: Sun Mar 10 00:42:23 2024 +0000

feat(perf:fast-rebuilds): Stop mutating resources param in ComponentResources emitter (#977)

* Stop mutating resources param in ComponentResources emitter

* Add done rebuilding log for fast rebuilds

* Move google font loading to Head component

* Simplify code and fix comment

commit 6e0c10297095a918109a058762beb47efc384a21
Author: Emile Bangma <ewjbangma@hotmail.com>
Date: Sun Mar 10 01:14:31 2024 +0100

fix(transclusion): prevent duplicate transclusion if multiple transclusions are present. (#982)

commit 94a54698ab7f29a609ca90033c1384a7ec5f5e65
Author: Emile Bangma <ewjbangma@hotmail.com>
Date: Sat Mar 9 17:59:55 2024 +0100

fix(resources): Use full path to font when cdnCache is false (#976)

commit 2e9a0c21db717c324a74f761fb0910b1218fdd72
Author: Emile Bangma <ewjbangma@hotmail.com>
Date: Sat Mar 9 17:43:40 2024 +0100

fix(description): first sentence no longer repeats until max length (#981)

commit b30a200bd4ddc64f4fd3d2124fcda0b354847073
Author: Aaron Pham <29749331+aarnphm@users.noreply.github.com>
Date: Fri Mar 8 12:14:22 2024 -0500

fix(i18n): make sure to use correct fileData for manual localization (#975)

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

commit 6d59aa8201a1fd3abea32ef36206af6471d85435
Author: Emile Bangma <ewjbangma@hotmail.com>
Date: Fri Mar 8 10:04:44 2024 +0100

fix(description): counts characters instead of words (#972)

* fix(description): make sure description counts characters instead of words

* ref: removed duplicate ternary

* CI: fix package log post merge

* CI: fix more merge artifacts

* CI: fix package-lock.json

* feat: add new default image template

* feat: use icon.png for image generation

* chore: update satori and sharp version

* feat(image-generator): add new default template

* Update quartz/components/Head.tsx

* Update quartz/components/Head.tsx

* Update quartz/components/Head.tsx

* Update docs/features/social images.md

* Update quartz/components/Head.tsx

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* feat(og-image): add config option to use default og image for root path

* docs(og-image): add `excludeRoot` config + update preview images

* docs(open-graph): add examples section

* chore: remove unused `socialImage2.tsx` component

* feat(open-graph): add frontmatter aliases for socialImage/cover/image

* fix(open-graph): only load satori font if config option is enabled

* refactor(open-graph): dont use async promise inside `fetchTtf()`

* chore: renaming and finished copywriting

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* chore: update typo

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* chore: update hinting for socialImage

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

---------

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
Co-authored-by: Emile Bangma <ewjbangma@hotmail.com>
Co-authored-by: Emile Bangma <github@emilebangma.com>
Co-authored-by: Aaron Pham <contact@aarnphm.xyz>

+1446 -8
+401
docs/features/social images.md
··· 1 + --- 2 + title: "Social Media Preview Cards" 3 + --- 4 + 5 + A lot of social media platforms can display a rich preview for your website when sharing a link (most notably, a cover image, a title and a description). Quartz automatically handles most of this for you with reasonable defaults, but for more control, you can customize these by setting [[social images#Frontmatter Properties]]. 6 + Quartz can also dynamically generate and use new cover images for every page to be used in link previews on social media for you. To get started with this, set `generateSocialImages: true` in `quartz.config.ts`. 7 + 8 + ## Showcase 9 + 10 + After enabling `generateSocialImages` in `quartz.config.ts`, the social media link preview for [[authoring content | Authoring Content]] looks like this: 11 + 12 + | Light | Dark | 13 + | ----------------------------------- | ---------------------------------- | 14 + | ![[social-image-preview-light.png]] | ![[social-image-preview-dark.png]] | 15 + 16 + For testing, it is recommended to use [opengraph.xyz](https://www.opengraph.xyz/) to see what the link to your page will look like on various platforms (more info under [[social images#local testing]]). 17 + 18 + ## Customization 19 + 20 + You can customize how images will be generated in the quartz config. 21 + 22 + For example, here's what the default configuration looks like if you set `generateSocialImages: true`: 23 + 24 + ```typescript title="quartz.config.ts" 25 + generateSocialImages: { 26 + colorScheme: "lightMode", // what colors to use for generating image, same as theme colors from config, valid values are "darkMode" and "lightMode" 27 + width: 1200, // width to generate with (in pixels) 28 + height: 630, // height to generate with (in pixels) 29 + excludeRoot: false, // wether to exclude "/" index path to be excluded from auto generated images (false = use auto, true = use default og image) 30 + } 31 + ``` 32 + 33 + --- 34 + 35 + ### Frontmatter Properties 36 + 37 + > [!tip] Hint 38 + > 39 + > Overriding social media preview properties via frontmatter still works even if `generateSocialImages` is disabled. 40 + 41 + The following properties can be used to customize your link previews: 42 + 43 + | Property | Alias | Summary | 44 + | ------------------- | ---------------- | ----------------------------------- | 45 + | `socialDescription` | `description` | Description to be used for preview. | 46 + | `socialImage` | `image`, `cover` | Link to preview image. | 47 + 48 + The `socialImage` property should contain a link to an image relative to `quartz/static`. If you have a folder for all your images in `quartz/static/my-images`, an example for `socialImage` could be `"my-images/cover.png"`. 49 + 50 + > [!info] Info 51 + > 52 + > The priority for what image will be used for the cover image looks like the following: `frontmatter property > generated image (if enabled) > default image`. 53 + > 54 + > The default image (`quartz/static/og-image.png`) will only be used as a fallback if nothing else is set. If `generateSocialImages` is enabled, it will be treated as the new default per page, but can be overwritten by setting the `socialImage` frontmatter property for that page. 55 + 56 + --- 57 + 58 + ### Fully customized image generation 59 + 60 + You can fully customize how the images being generated look by passing your own component to `generateSocialImages.imageStructure`. This component takes html/css + some page metadata/config options and converts it to an image using [satori](https://github.com/vercel/satori). Vercel provides an [online playground](https://og-playground.vercel.app/) that can be used to preview how your html/css looks like as a picture. This is ideal for prototyping your custom design. 61 + 62 + It is recommended to write your own image components in `quartz/util/og.tsx` or any other `.tsx` file, as passing them to the config won't work otherwise. An example of the default image component can be found in `og.tsx` in `defaultImage()`. 63 + 64 + > [!tip] Hint 65 + > 66 + > Satori only supports a subset of all valid CSS properties. All supported properties can be found in their [documentation](https://github.com/vercel/satori#css). 67 + 68 + Your custom image component should have the `SocialImageOptions["imageStructure"]` type, to make development easier for you. Using a component of this type, you will be passed the following variables: 69 + 70 + ```ts 71 + imageStructure: ( 72 + cfg: GlobalConfiguration, // global Quartz config (useful for getting theme colors and other info) 73 + userOpts: UserOpts, // options passed to `generateSocialImage` 74 + title: string, // title of current page 75 + description: string, // description of current page 76 + fonts: SatoriOptions["fonts"], // header + body font 77 + ) => JSXInternal.Element 78 + ``` 79 + 80 + Now, you can let your creativity flow and design your own image component! For reference and some cool tips, you can check how the markup for the default image looks. 81 + 82 + > [!example] Examples 83 + > 84 + > Here are some examples for markup you may need to get started: 85 + > 86 + > - Get a theme color 87 + > 88 + > `cfg.theme.colors[colorScheme].<colorName>`, where `<colorName>` corresponds to a key in `ColorScheme` (defined at the top of `quartz/util/theme.ts`) 89 + > 90 + > - Use the page title/description 91 + > 92 + > `<p>{title}</p>`/`<p>{description}</p>` 93 + > 94 + > - Use a font family 95 + > 96 + > Detailed in the Fonts chapter below 97 + 98 + --- 99 + 100 + ### Fonts 101 + 102 + You will also be passed an array containing a header and a body font (where the first entry is header and the second is body). The fonts matches the ones selected in `theme.typography.header` and `theme.typography.body` from `quartz.config.ts` and will be passed in the format required by [`satori`](https://github.com/vercel/satori). To use them in CSS, use the `.name` property (e.g. `fontFamily: fonts[1].name` to use the "body" font family). 103 + 104 + An example of a component using the header font could look like this: 105 + 106 + ```tsx title="socialImage.tsx" 107 + export const myImage: SocialImageOptions["imageStructure"] = (...) => { 108 + return <p style={{ fontFamily: fonts[0].name }}>Cool Header!</p> 109 + } 110 + ``` 111 + 112 + > [!example]- Local fonts 113 + > 114 + > For cases where you use a local fonts under `static` folder, make sure to set the correct `@font-face` in `custom.scss` 115 + > 116 + > ```scss title="custom.scss" 117 + > @font-face { 118 + > font-family: "Newsreader"; 119 + > font-style: normal; 120 + > font-weight: normal; 121 + > font-display: swap; 122 + > src: url("/static/Newsreader.woff2") format("woff2"); 123 + > } 124 + > ``` 125 + > 126 + > Then in `quartz/util/og.tsx`, you can load the satori fonts like so: 127 + > 128 + > ```tsx title="quartz/util/og.tsx" 129 + > const headerFont = joinSegments("static", "Newsreader.woff2") 130 + > const bodyFont = joinSegments("static", "Newsreader.woff2") 131 + > 132 + > export async function getSatoriFont(cfg: GlobalConfiguration): Promise<SatoriOptions["fonts"]> { 133 + > const headerWeight: FontWeight = 700 134 + > const bodyWeight: FontWeight = 400 135 + > 136 + > const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) 137 + > 138 + > const [header, body] = await Promise.all( 139 + > [headerFont, bodyFont].map((font) => 140 + > fetch(`${url.toString()}/${font}`).then((res) => res.arrayBuffer()), 141 + > ), 142 + > ) 143 + > 144 + > return [ 145 + > { name: cfg.theme.typography.header, data: header, weight: headerWeight, style: "normal" }, 146 + > { name: cfg.theme.typography.body, data: body, weight: bodyWeight, style: "normal" }, 147 + > ] 148 + > } 149 + > ``` 150 + > 151 + > This font then can be used with your custom structure 152 + 153 + ### Local testing 154 + 155 + To test how the full preview of your page is going to look even before deploying, you can forward the port you're serving quartz on. In VSCode, this can easily be achieved following [this guide](https://code.visualstudio.com/docs/editor/port-forwarding) (make sure to set `Visibility` to `public` if testing on external tools like [opengraph.xyz](https://www.opengraph.xyz/)). 156 + 157 + If you have `generateSocialImages` enabled, you can check out all generated images under `public/static/social-images`. 158 + 159 + ## Technical info 160 + 161 + Images will be generated as `.webp` files, which helps to keep images small (the average image takes ~`19kB`). They are also compressed further using [sharp](https://sharp.pixelplumbing.com/). 162 + 163 + When using images, the appropriate [Open Graph](https://ogp.me/) and [Twitter](https://developer.twitter.com/en/docs/twitter-for-websites/cards/guides/getting-started) meta tags will be set to ensure they work and look as expected. 164 + 165 + ## Examples 166 + 167 + Besides the template for the default image generation (found under `quartz/util/og.tsx`), you can also add your own! To do this, you can either edit the source code of that file (not recommended) or create a new one (e.g. `customSocialImage.tsx`, source shown below). 168 + 169 + After adding that file, you can update `quartz.config.ts` to use your image generation template as follows: 170 + 171 + ```ts 172 + // Import component at start of file 173 + import { customImage } from "./quartz/util/customSocialImage.tsx" 174 + 175 + // In main config 176 + const config: QuartzConfig = { 177 + ... 178 + generateSocialImages: { 179 + ... 180 + imageStructure: customImage, // tells quartz to use your component when generating images 181 + }, 182 + } 183 + ``` 184 + 185 + The following example will generate images that look as follows: 186 + 187 + | Light | Dark | 188 + | ------------------------------------------ | ----------------------------------------- | 189 + | ![[custom-social-image-preview-light.png]] | ![[custom-social-image-preview-dark.png]] | 190 + 191 + This example (and the default template) use colors and fonts from your theme specified in the quartz config. Fonts get passed in as a prop, where `fonts[0]` will contain the header font and `fonts[1]` will contain the body font (more info in the [[#fonts]] section). 192 + 193 + ```tsx 194 + import { SatoriOptions } from "satori/wasm" 195 + import { GlobalConfiguration } from "../cfg" 196 + import { SocialImageOptions, UserOpts } from "./imageHelper" 197 + import { QuartzPluginData } from "../plugins/vfile" 198 + 199 + export const customImage: SocialImageOptions["imageStructure"] = ( 200 + cfg: GlobalConfiguration, 201 + userOpts: UserOpts, 202 + title: string, 203 + description: string, 204 + fonts: SatoriOptions["fonts"], 205 + fileData: QuartzPluginData, 206 + ) => { 207 + // How many characters are allowed before switching to smaller font 208 + const fontBreakPoint = 22 209 + const useSmallerFont = title.length > fontBreakPoint 210 + 211 + const { colorScheme } = userOpts 212 + return ( 213 + <div 214 + style={{ 215 + display: "flex", 216 + flexDirection: "row", 217 + justifyContent: "flex-start", 218 + alignItems: "center", 219 + height: "100%", 220 + width: "100%", 221 + }} 222 + > 223 + <div 224 + style={{ 225 + display: "flex", 226 + alignItems: "center", 227 + justifyContent: "center", 228 + height: "100%", 229 + width: "100%", 230 + backgroundColor: cfg.theme.colors[colorScheme].light, 231 + flexDirection: "column", 232 + gap: "2.5rem", 233 + paddingTop: "2rem", 234 + paddingBottom: "2rem", 235 + }} 236 + > 237 + <p 238 + style={{ 239 + color: cfg.theme.colors[colorScheme].dark, 240 + fontSize: useSmallerFont ? 70 : 82, 241 + marginLeft: "4rem", 242 + textAlign: "center", 243 + marginRight: "4rem", 244 + fontFamily: fonts[0].name, 245 + }} 246 + > 247 + {title} 248 + </p> 249 + <p 250 + style={{ 251 + color: cfg.theme.colors[colorScheme].dark, 252 + fontSize: 44, 253 + marginLeft: "8rem", 254 + marginRight: "8rem", 255 + lineClamp: 3, 256 + fontFamily: fonts[1].name, 257 + }} 258 + > 259 + {description} 260 + </p> 261 + </div> 262 + <div 263 + style={{ 264 + height: "100%", 265 + width: "2vw", 266 + position: "absolute", 267 + backgroundColor: cfg.theme.colors[colorScheme].tertiary, 268 + opacity: 0.85, 269 + }} 270 + /> 271 + </div> 272 + ) 273 + } 274 + ``` 275 + 276 + > [!example]- Advanced example 277 + > 278 + > The following example includes a customized social image with a custom background and formatted date. 279 + > 280 + > ```typescript title="custom-og.tsx" 281 + > export const og: SocialImageOptions["Component"] = ( 282 + > cfg: GlobalConfiguration, 283 + > fileData: QuartzPluginData, 284 + > { colorScheme }: Options, 285 + > title: string, 286 + > description: string, 287 + > fonts: SatoriOptions["fonts"], 288 + > ) => { 289 + > let created: string | undefined 290 + > let reading: string | undefined 291 + > if (fileData.dates) { 292 + > created = formatDate(getDate(cfg, fileData)!, cfg.locale) 293 + > } 294 + > const { minutes, text: _timeTaken, words: _words } = readingTime(fileData.text!) 295 + > reading = i18n(cfg.locale).components.contentMeta.readingTime({ 296 + > minutes: Math.ceil(minutes), 297 + > }) 298 + > 299 + > const Li = [created, reading] 300 + > 301 + > return ( 302 + > <div 303 + > style={{ 304 + > position: "relative", 305 + > display: "flex", 306 + > flexDirection: "row", 307 + > alignItems: "flex-start", 308 + > height: "100%", 309 + > width: "100%", 310 + > backgroundImage: `url("https://${cfg.baseUrl}/static/og-image.jpeg")`, 311 + > backgroundSize: "100% 100%", 312 + > }} 313 + > > 314 + > <div 315 + > style={{ 316 + > position: "absolute", 317 + > top: 0, 318 + > left: 0, 319 + > right: 0, 320 + > bottom: 0, 321 + > background: "radial-gradient(circle at center, transparent, rgba(0, 0, 0, 0.4) 70%)", 322 + > }} 323 + > /> 324 + > <div 325 + > style={{ 326 + > display: "flex", 327 + > height: "100%", 328 + > width: "100%", 329 + > flexDirection: "column", 330 + > justifyContent: "flex-start", 331 + > alignItems: "flex-start", 332 + > gap: "1.5rem", 333 + > paddingTop: "4rem", 334 + > paddingBottom: "4rem", 335 + > marginLeft: "4rem", 336 + > }} 337 + > > 338 + > <img 339 + > src={`"https://${cfg.baseUrl}/static/icon.jpeg"`} 340 + > style={{ 341 + > position: "relative", 342 + > backgroundClip: "border-box", 343 + > borderRadius: "6rem", 344 + > }} 345 + > width={80} 346 + > /> 347 + > <div 348 + > style={{ 349 + > display: "flex", 350 + > flexDirection: "column", 351 + > textAlign: "left", 352 + > fontFamily: fonts[0].name, 353 + > }} 354 + > > 355 + > <h2 356 + > style={{ 357 + > color: cfg.theme.colors[colorScheme].light, 358 + > fontSize: "3rem", 359 + > fontWeight: 700, 360 + > marginRight: "4rem", 361 + > fontFamily: fonts[0].name, 362 + > }} 363 + > > 364 + > {title} 365 + > </h2> 366 + > <ul 367 + > style={{ 368 + > color: cfg.theme.colors[colorScheme].gray, 369 + > gap: "1rem", 370 + > fontSize: "1.5rem", 371 + > fontFamily: fonts[1].name, 372 + > }} 373 + > > 374 + > {Li.map((item, index) => { 375 + > if (item) { 376 + > return <li key={index}>{item}</li> 377 + > } 378 + > })} 379 + > </ul> 380 + > </div> 381 + > <p 382 + > style={{ 383 + > color: cfg.theme.colors[colorScheme].light, 384 + > fontSize: "1.5rem", 385 + > overflow: "hidden", 386 + > marginRight: "8rem", 387 + > textOverflow: "ellipsis", 388 + > display: "-webkit-box", 389 + > WebkitLineClamp: 7, 390 + > WebkitBoxOrient: "vertical", 391 + > lineClamp: 7, 392 + > fontFamily: fonts[1].name, 393 + > }} 394 + > > 395 + > {description} 396 + > </p> 397 + > </div> 398 + > </div> 399 + > ) 400 + > } 401 + > ```
docs/images/custom-social-image-preview-dark.png

This is a binary file and will not be displayed.

docs/images/custom-social-image-preview-light.png

This is a binary file and will not be displayed.

docs/images/social-image-preview-dark.png

This is a binary file and will not be displayed.

docs/images/social-image-preview-light.png

This is a binary file and will not be displayed.

+648
package-lock.json
··· 58 58 "remark-smartypants": "^3.0.2", 59 59 "rfdc": "^1.4.1", 60 60 "rimraf": "^6.0.1", 61 + "satori": "^0.10.14", 61 62 "serve-handler": "^6.1.6", 63 + "sharp": "^0.33.5", 62 64 "shiki": "^1.22.2", 63 65 "source-map-support": "^0.5.21", 64 66 "to-vfile": "^8.0.0", ··· 284 286 }, 285 287 "funding": { 286 288 "url": "https://github.com/sponsors/sindresorhus" 289 + } 290 + }, 291 + "node_modules/@emnapi/runtime": { 292 + "version": "1.3.1", 293 + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", 294 + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", 295 + "license": "MIT", 296 + "optional": true, 297 + "dependencies": { 298 + "tslib": "^2.4.0" 287 299 } 288 300 }, 289 301 "node_modules/@esbuild/aix-ppc64": { ··· 690 702 "mlly": "^1.7.1" 691 703 } 692 704 }, 705 + "node_modules/@img/sharp-darwin-arm64": { 706 + "version": "0.33.5", 707 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", 708 + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", 709 + "cpu": [ 710 + "arm64" 711 + ], 712 + "license": "Apache-2.0", 713 + "optional": true, 714 + "os": [ 715 + "darwin" 716 + ], 717 + "engines": { 718 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 719 + }, 720 + "funding": { 721 + "url": "https://opencollective.com/libvips" 722 + }, 723 + "optionalDependencies": { 724 + "@img/sharp-libvips-darwin-arm64": "1.0.4" 725 + } 726 + }, 727 + "node_modules/@img/sharp-darwin-x64": { 728 + "version": "0.33.5", 729 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", 730 + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", 731 + "cpu": [ 732 + "x64" 733 + ], 734 + "license": "Apache-2.0", 735 + "optional": true, 736 + "os": [ 737 + "darwin" 738 + ], 739 + "engines": { 740 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 741 + }, 742 + "funding": { 743 + "url": "https://opencollective.com/libvips" 744 + }, 745 + "optionalDependencies": { 746 + "@img/sharp-libvips-darwin-x64": "1.0.4" 747 + } 748 + }, 749 + "node_modules/@img/sharp-libvips-darwin-arm64": { 750 + "version": "1.0.4", 751 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", 752 + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", 753 + "cpu": [ 754 + "arm64" 755 + ], 756 + "license": "LGPL-3.0-or-later", 757 + "optional": true, 758 + "os": [ 759 + "darwin" 760 + ], 761 + "funding": { 762 + "url": "https://opencollective.com/libvips" 763 + } 764 + }, 765 + "node_modules/@img/sharp-libvips-darwin-x64": { 766 + "version": "1.0.4", 767 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", 768 + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", 769 + "cpu": [ 770 + "x64" 771 + ], 772 + "license": "LGPL-3.0-or-later", 773 + "optional": true, 774 + "os": [ 775 + "darwin" 776 + ], 777 + "funding": { 778 + "url": "https://opencollective.com/libvips" 779 + } 780 + }, 781 + "node_modules/@img/sharp-libvips-linux-arm": { 782 + "version": "1.0.5", 783 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", 784 + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", 785 + "cpu": [ 786 + "arm" 787 + ], 788 + "license": "LGPL-3.0-or-later", 789 + "optional": true, 790 + "os": [ 791 + "linux" 792 + ], 793 + "funding": { 794 + "url": "https://opencollective.com/libvips" 795 + } 796 + }, 797 + "node_modules/@img/sharp-libvips-linux-arm64": { 798 + "version": "1.0.4", 799 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", 800 + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", 801 + "cpu": [ 802 + "arm64" 803 + ], 804 + "license": "LGPL-3.0-or-later", 805 + "optional": true, 806 + "os": [ 807 + "linux" 808 + ], 809 + "funding": { 810 + "url": "https://opencollective.com/libvips" 811 + } 812 + }, 813 + "node_modules/@img/sharp-libvips-linux-s390x": { 814 + "version": "1.0.4", 815 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", 816 + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", 817 + "cpu": [ 818 + "s390x" 819 + ], 820 + "license": "LGPL-3.0-or-later", 821 + "optional": true, 822 + "os": [ 823 + "linux" 824 + ], 825 + "funding": { 826 + "url": "https://opencollective.com/libvips" 827 + } 828 + }, 829 + "node_modules/@img/sharp-libvips-linux-x64": { 830 + "version": "1.0.4", 831 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", 832 + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", 833 + "cpu": [ 834 + "x64" 835 + ], 836 + "license": "LGPL-3.0-or-later", 837 + "optional": true, 838 + "os": [ 839 + "linux" 840 + ], 841 + "funding": { 842 + "url": "https://opencollective.com/libvips" 843 + } 844 + }, 845 + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { 846 + "version": "1.0.4", 847 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", 848 + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", 849 + "cpu": [ 850 + "arm64" 851 + ], 852 + "license": "LGPL-3.0-or-later", 853 + "optional": true, 854 + "os": [ 855 + "linux" 856 + ], 857 + "funding": { 858 + "url": "https://opencollective.com/libvips" 859 + } 860 + }, 861 + "node_modules/@img/sharp-libvips-linuxmusl-x64": { 862 + "version": "1.0.4", 863 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", 864 + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", 865 + "cpu": [ 866 + "x64" 867 + ], 868 + "license": "LGPL-3.0-or-later", 869 + "optional": true, 870 + "os": [ 871 + "linux" 872 + ], 873 + "funding": { 874 + "url": "https://opencollective.com/libvips" 875 + } 876 + }, 877 + "node_modules/@img/sharp-linux-arm": { 878 + "version": "0.33.5", 879 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", 880 + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", 881 + "cpu": [ 882 + "arm" 883 + ], 884 + "license": "Apache-2.0", 885 + "optional": true, 886 + "os": [ 887 + "linux" 888 + ], 889 + "engines": { 890 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 891 + }, 892 + "funding": { 893 + "url": "https://opencollective.com/libvips" 894 + }, 895 + "optionalDependencies": { 896 + "@img/sharp-libvips-linux-arm": "1.0.5" 897 + } 898 + }, 899 + "node_modules/@img/sharp-linux-arm64": { 900 + "version": "0.33.5", 901 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", 902 + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", 903 + "cpu": [ 904 + "arm64" 905 + ], 906 + "license": "Apache-2.0", 907 + "optional": true, 908 + "os": [ 909 + "linux" 910 + ], 911 + "engines": { 912 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 913 + }, 914 + "funding": { 915 + "url": "https://opencollective.com/libvips" 916 + }, 917 + "optionalDependencies": { 918 + "@img/sharp-libvips-linux-arm64": "1.0.4" 919 + } 920 + }, 921 + "node_modules/@img/sharp-linux-s390x": { 922 + "version": "0.33.5", 923 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", 924 + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", 925 + "cpu": [ 926 + "s390x" 927 + ], 928 + "license": "Apache-2.0", 929 + "optional": true, 930 + "os": [ 931 + "linux" 932 + ], 933 + "engines": { 934 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 935 + }, 936 + "funding": { 937 + "url": "https://opencollective.com/libvips" 938 + }, 939 + "optionalDependencies": { 940 + "@img/sharp-libvips-linux-s390x": "1.0.4" 941 + } 942 + }, 943 + "node_modules/@img/sharp-linux-x64": { 944 + "version": "0.33.5", 945 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", 946 + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", 947 + "cpu": [ 948 + "x64" 949 + ], 950 + "license": "Apache-2.0", 951 + "optional": true, 952 + "os": [ 953 + "linux" 954 + ], 955 + "engines": { 956 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 957 + }, 958 + "funding": { 959 + "url": "https://opencollective.com/libvips" 960 + }, 961 + "optionalDependencies": { 962 + "@img/sharp-libvips-linux-x64": "1.0.4" 963 + } 964 + }, 965 + "node_modules/@img/sharp-linuxmusl-arm64": { 966 + "version": "0.33.5", 967 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", 968 + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", 969 + "cpu": [ 970 + "arm64" 971 + ], 972 + "license": "Apache-2.0", 973 + "optional": true, 974 + "os": [ 975 + "linux" 976 + ], 977 + "engines": { 978 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 979 + }, 980 + "funding": { 981 + "url": "https://opencollective.com/libvips" 982 + }, 983 + "optionalDependencies": { 984 + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" 985 + } 986 + }, 987 + "node_modules/@img/sharp-linuxmusl-x64": { 988 + "version": "0.33.5", 989 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", 990 + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", 991 + "cpu": [ 992 + "x64" 993 + ], 994 + "license": "Apache-2.0", 995 + "optional": true, 996 + "os": [ 997 + "linux" 998 + ], 999 + "engines": { 1000 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1001 + }, 1002 + "funding": { 1003 + "url": "https://opencollective.com/libvips" 1004 + }, 1005 + "optionalDependencies": { 1006 + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" 1007 + } 1008 + }, 1009 + "node_modules/@img/sharp-wasm32": { 1010 + "version": "0.33.5", 1011 + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", 1012 + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", 1013 + "cpu": [ 1014 + "wasm32" 1015 + ], 1016 + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", 1017 + "optional": true, 1018 + "dependencies": { 1019 + "@emnapi/runtime": "^1.2.0" 1020 + }, 1021 + "engines": { 1022 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1023 + }, 1024 + "funding": { 1025 + "url": "https://opencollective.com/libvips" 1026 + } 1027 + }, 1028 + "node_modules/@img/sharp-win32-ia32": { 1029 + "version": "0.33.5", 1030 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", 1031 + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", 1032 + "cpu": [ 1033 + "ia32" 1034 + ], 1035 + "license": "Apache-2.0 AND LGPL-3.0-or-later", 1036 + "optional": true, 1037 + "os": [ 1038 + "win32" 1039 + ], 1040 + "engines": { 1041 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1042 + }, 1043 + "funding": { 1044 + "url": "https://opencollective.com/libvips" 1045 + } 1046 + }, 1047 + "node_modules/@img/sharp-win32-x64": { 1048 + "version": "0.33.5", 1049 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", 1050 + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", 1051 + "cpu": [ 1052 + "x64" 1053 + ], 1054 + "license": "Apache-2.0 AND LGPL-3.0-or-later", 1055 + "optional": true, 1056 + "os": [ 1057 + "win32" 1058 + ], 1059 + "engines": { 1060 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1061 + }, 1062 + "funding": { 1063 + "url": "https://opencollective.com/libvips" 1064 + } 1065 + }, 693 1066 "node_modules/@isaacs/cliui": { 694 1067 "version": "8.0.2", 695 1068 "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", ··· 1256 1629 "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz", 1257 1630 "integrity": "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==" 1258 1631 }, 1632 + "node_modules/@shuding/opentype.js": { 1633 + "version": "1.4.0-beta.0", 1634 + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", 1635 + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", 1636 + "license": "MIT", 1637 + "dependencies": { 1638 + "fflate": "^0.7.3", 1639 + "string.prototype.codepointat": "^0.2.1" 1640 + }, 1641 + "bin": { 1642 + "ot": "bin/ot" 1643 + }, 1644 + "engines": { 1645 + "node": ">= 8.0.0" 1646 + } 1647 + }, 1259 1648 "node_modules/@sindresorhus/merge-streams": { 1260 1649 "version": "2.3.0", 1261 1650 "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", ··· 1852 2241 "node": ">= 0.8" 1853 2242 } 1854 2243 }, 2244 + "node_modules/camelize": { 2245 + "version": "1.0.1", 2246 + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", 2247 + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", 2248 + "license": "MIT", 2249 + "funding": { 2250 + "url": "https://github.com/sponsors/ljharb" 2251 + } 2252 + }, 1855 2253 "node_modules/ccount": { 1856 2254 "version": "2.0.1", 1857 2255 "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", ··· 2027 2425 "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 2028 2426 } 2029 2427 }, 2428 + "node_modules/color": { 2429 + "version": "4.2.3", 2430 + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", 2431 + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", 2432 + "license": "MIT", 2433 + "dependencies": { 2434 + "color-convert": "^2.0.1", 2435 + "color-string": "^1.9.0" 2436 + }, 2437 + "engines": { 2438 + "node": ">=12.5.0" 2439 + } 2440 + }, 2030 2441 "node_modules/color-convert": { 2031 2442 "version": "2.0.1", 2032 2443 "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", ··· 2042 2453 "version": "1.1.4", 2043 2454 "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 2044 2455 "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 2456 + }, 2457 + "node_modules/color-string": { 2458 + "version": "1.9.1", 2459 + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", 2460 + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", 2461 + "license": "MIT", 2462 + "dependencies": { 2463 + "color-name": "^1.0.0", 2464 + "simple-swizzle": "^0.2.2" 2465 + } 2045 2466 }, 2046 2467 "node_modules/colorjs.io": { 2047 2468 "version": "0.5.2", ··· 2126 2547 "node": ">= 8" 2127 2548 } 2128 2549 }, 2550 + "node_modules/css-background-parser": { 2551 + "version": "0.1.0", 2552 + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", 2553 + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", 2554 + "license": "MIT" 2555 + }, 2556 + "node_modules/css-box-shadow": { 2557 + "version": "1.0.0-3", 2558 + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", 2559 + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", 2560 + "license": "MIT" 2561 + }, 2562 + "node_modules/css-color-keywords": { 2563 + "version": "1.0.0", 2564 + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", 2565 + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", 2566 + "license": "ISC", 2567 + "engines": { 2568 + "node": ">=4" 2569 + } 2570 + }, 2571 + "node_modules/css-to-react-native": { 2572 + "version": "3.2.0", 2573 + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", 2574 + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", 2575 + "license": "MIT", 2576 + "dependencies": { 2577 + "camelize": "^1.0.0", 2578 + "css-color-keywords": "^1.0.0", 2579 + "postcss-value-parser": "^4.0.2" 2580 + } 2581 + }, 2129 2582 "node_modules/css-tree": { 2130 2583 "version": "2.3.1", 2131 2584 "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", ··· 2824 3277 "node": ">=6" 2825 3278 } 2826 3279 }, 3280 + "node_modules/escape-html": { 3281 + "version": "1.0.3", 3282 + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 3283 + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 3284 + "license": "MIT" 3285 + }, 2827 3286 "node_modules/escape-string-regexp": { 2828 3287 "version": "5.0.0", 2829 3288 "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", ··· 2966 3425 "tr46": "~0.0.3", 2967 3426 "webidl-conversions": "^3.0.0" 2968 3427 } 3428 + }, 3429 + "node_modules/fflate": { 3430 + "version": "0.7.4", 3431 + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", 3432 + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", 3433 + "license": "MIT" 2969 3434 }, 2970 3435 "node_modules/fill-range": { 2971 3436 "version": "7.0.1", ··· 3504 3969 "url": "https://opencollective.com/unified" 3505 3970 } 3506 3971 }, 3972 + "node_modules/hex-rgb": { 3973 + "version": "4.3.0", 3974 + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", 3975 + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", 3976 + "license": "MIT", 3977 + "engines": { 3978 + "node": ">=6" 3979 + }, 3980 + "funding": { 3981 + "url": "https://github.com/sponsors/sindresorhus" 3982 + } 3983 + }, 3507 3984 "node_modules/html-encoding-sniffer": { 3508 3985 "version": "4.0.0", 3509 3986 "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", ··· 3636 4113 "type": "github", 3637 4114 "url": "https://github.com/sponsors/wooorm" 3638 4115 } 4116 + }, 4117 + "node_modules/is-arrayish": { 4118 + "version": "0.3.2", 4119 + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 4120 + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", 4121 + "license": "MIT" 3639 4122 }, 3640 4123 "node_modules/is-core-module": { 3641 4124 "version": "2.13.1", ··· 4085 4568 "url": "https://opencollective.com/parcel" 4086 4569 } 4087 4570 }, 4571 + "node_modules/linebreak": { 4572 + "version": "1.1.0", 4573 + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", 4574 + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", 4575 + "license": "MIT", 4576 + "dependencies": { 4577 + "base64-js": "0.0.8", 4578 + "unicode-trie": "^2.0.0" 4579 + } 4580 + }, 4581 + "node_modules/linebreak/node_modules/base64-js": { 4582 + "version": "0.0.8", 4583 + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", 4584 + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", 4585 + "license": "MIT", 4586 + "engines": { 4587 + "node": ">= 0.4" 4588 + } 4589 + }, 4088 4590 "node_modules/local-pkg": { 4089 4591 "version": "0.5.0", 4090 4592 "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", ··· 5284 5786 "integrity": "sha512-VgXbyrSNsml4eHWIvxxG/nTL4wgybMTXCV2Un/+yEc3aDKKU6nQBZjbeP3Pl3qm9Qg92X/1ng4ffvCeD/zwHgg==", 5285 5787 "license": "MIT" 5286 5788 }, 5789 + "node_modules/pako": { 5790 + "version": "0.2.9", 5791 + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", 5792 + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", 5793 + "license": "MIT" 5794 + }, 5795 + "node_modules/parse-css-color": { 5796 + "version": "0.2.1", 5797 + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", 5798 + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", 5799 + "license": "MIT", 5800 + "dependencies": { 5801 + "color-name": "^1.1.4", 5802 + "hex-rgb": "^4.1.0" 5803 + } 5804 + }, 5287 5805 "node_modules/parse-entities": { 5288 5806 "version": "4.0.1", 5289 5807 "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", ··· 5466 5984 "path-data-parser": "0.1.0", 5467 5985 "points-on-curve": "0.2.0" 5468 5986 } 5987 + }, 5988 + "node_modules/postcss-value-parser": { 5989 + "version": "4.2.0", 5990 + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", 5991 + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", 5992 + "license": "MIT" 5469 5993 }, 5470 5994 "node_modules/preact": { 5471 5995 "version": "10.24.3", ··· 6441 6965 "node": ">=14.0.0" 6442 6966 } 6443 6967 }, 6968 + "node_modules/satori": { 6969 + "version": "0.10.14", 6970 + "resolved": "https://registry.npmjs.org/satori/-/satori-0.10.14.tgz", 6971 + "integrity": "sha512-abovcqmwl97WKioxpkfuMeZmndB1TuDFY/R+FymrZyiGP+pMYomvgSzVPnbNMWHHESOPosVHGL352oFbdAnJcA==", 6972 + "license": "MPL-2.0", 6973 + "dependencies": { 6974 + "@shuding/opentype.js": "1.4.0-beta.0", 6975 + "css-background-parser": "^0.1.0", 6976 + "css-box-shadow": "1.0.0-3", 6977 + "css-to-react-native": "^3.0.0", 6978 + "emoji-regex": "^10.2.1", 6979 + "escape-html": "^1.0.3", 6980 + "linebreak": "^1.1.0", 6981 + "parse-css-color": "^0.2.1", 6982 + "postcss-value-parser": "^4.2.0", 6983 + "yoga-wasm-web": "^0.3.3" 6984 + }, 6985 + "engines": { 6986 + "node": ">=16" 6987 + } 6988 + }, 6989 + "node_modules/satori/node_modules/emoji-regex": { 6990 + "version": "10.4.0", 6991 + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", 6992 + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", 6993 + "license": "MIT" 6994 + }, 6444 6995 "node_modules/saxes": { 6445 6996 "version": "6.0.0", 6446 6997 "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", ··· 6464 7015 "node": ">=4" 6465 7016 } 6466 7017 }, 7018 + "node_modules/semver": { 7019 + "version": "7.6.3", 7020 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", 7021 + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", 7022 + "license": "ISC", 7023 + "bin": { 7024 + "semver": "bin/semver.js" 7025 + }, 7026 + "engines": { 7027 + "node": ">=10" 7028 + } 7029 + }, 6467 7030 "node_modules/serve-handler": { 6468 7031 "version": "6.1.6", 6469 7032 "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", ··· 6498 7061 "node": "*" 6499 7062 } 6500 7063 }, 7064 + "node_modules/sharp": { 7065 + "version": "0.33.5", 7066 + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", 7067 + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", 7068 + "hasInstallScript": true, 7069 + "license": "Apache-2.0", 7070 + "dependencies": { 7071 + "color": "^4.2.3", 7072 + "detect-libc": "^2.0.3", 7073 + "semver": "^7.6.3" 7074 + }, 7075 + "engines": { 7076 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 7077 + }, 7078 + "funding": { 7079 + "url": "https://opencollective.com/libvips" 7080 + }, 7081 + "optionalDependencies": { 7082 + "@img/sharp-darwin-arm64": "0.33.5", 7083 + "@img/sharp-darwin-x64": "0.33.5", 7084 + "@img/sharp-libvips-darwin-arm64": "1.0.4", 7085 + "@img/sharp-libvips-darwin-x64": "1.0.4", 7086 + "@img/sharp-libvips-linux-arm": "1.0.5", 7087 + "@img/sharp-libvips-linux-arm64": "1.0.4", 7088 + "@img/sharp-libvips-linux-s390x": "1.0.4", 7089 + "@img/sharp-libvips-linux-x64": "1.0.4", 7090 + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", 7091 + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", 7092 + "@img/sharp-linux-arm": "0.33.5", 7093 + "@img/sharp-linux-arm64": "0.33.5", 7094 + "@img/sharp-linux-s390x": "0.33.5", 7095 + "@img/sharp-linux-x64": "0.33.5", 7096 + "@img/sharp-linuxmusl-arm64": "0.33.5", 7097 + "@img/sharp-linuxmusl-x64": "0.33.5", 7098 + "@img/sharp-wasm32": "0.33.5", 7099 + "@img/sharp-win32-ia32": "0.33.5", 7100 + "@img/sharp-win32-x64": "0.33.5" 7101 + } 7102 + }, 7103 + "node_modules/sharp/node_modules/detect-libc": { 7104 + "version": "2.0.3", 7105 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", 7106 + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", 7107 + "license": "Apache-2.0", 7108 + "engines": { 7109 + "node": ">=8" 7110 + } 7111 + }, 6501 7112 "node_modules/shebang-command": { 6502 7113 "version": "2.0.0", 6503 7114 "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", ··· 6541 7152 "url": "https://github.com/sponsors/isaacs" 6542 7153 } 6543 7154 }, 7155 + "node_modules/simple-swizzle": { 7156 + "version": "0.2.2", 7157 + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 7158 + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", 7159 + "license": "MIT", 7160 + "dependencies": { 7161 + "is-arrayish": "^0.3.1" 7162 + } 7163 + }, 6544 7164 "node_modules/sisteransi": { 6545 7165 "version": "1.0.5", 6546 7166 "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", ··· 6671 7291 "node": ">=8" 6672 7292 } 6673 7293 }, 7294 + "node_modules/string.prototype.codepointat": { 7295 + "version": "0.2.1", 7296 + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", 7297 + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", 7298 + "license": "MIT" 7299 + }, 6674 7300 "node_modules/stringify-entities": { 6675 7301 "version": "4.0.3", 6676 7302 "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz", ··· 6783 7409 "node": ">=14" 6784 7410 } 6785 7411 }, 7412 + "node_modules/tiny-inflate": { 7413 + "version": "1.0.3", 7414 + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", 7415 + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", 7416 + "license": "MIT" 7417 + }, 6786 7418 "node_modules/tinyexec": { 6787 7419 "version": "0.3.1", 6788 7420 "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz", ··· 7340 7972 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", 7341 7973 "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", 7342 7974 "dev": true 7975 + }, 7976 + "node_modules/unicode-trie": { 7977 + "version": "2.0.0", 7978 + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", 7979 + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", 7980 + "license": "MIT", 7981 + "dependencies": { 7982 + "pako": "^0.2.5", 7983 + "tiny-inflate": "^1.0.0" 7984 + } 7343 7985 }, 7344 7986 "node_modules/unicorn-magic": { 7345 7987 "version": "0.1.0", ··· 7941 8583 "engines": { 7942 8584 "node": ">=8" 7943 8585 } 8586 + }, 8587 + "node_modules/yoga-wasm-web": { 8588 + "version": "0.3.3", 8589 + "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz", 8590 + "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==", 8591 + "license": "MIT" 7944 8592 }, 7945 8593 "node_modules/zwitch": { 7946 8594 "version": "2.0.4",
+2
package.json
··· 84 84 "remark-smartypants": "^3.0.2", 85 85 "rfdc": "^1.4.1", 86 86 "rimraf": "^6.0.1", 87 + "satori": "^0.10.14", 87 88 "serve-handler": "^6.1.6", 89 + "sharp": "^0.33.5", 88 90 "shiki": "^1.22.2", 89 91 "source-map-support": "^0.5.21", 90 92 "to-vfile": "^8.0.0",
+1
quartz.config.ts
··· 19 19 baseUrl: "quartz.jzhao.xyz", 20 20 ignorePatterns: ["private", "templates", ".obsidian"], 21 21 defaultDateType: "created", 22 + generateSocialImages: false, 22 23 theme: { 23 24 fontOrigin: "googleFonts", 24 25 cdnCaching: true,
+6 -1
quartz/cfg.ts
··· 2 2 import { QuartzComponent } from "./components/types" 3 3 import { ValidLocale } from "./i18n" 4 4 import { PluginTypes } from "./plugins/types" 5 + import { SocialImageOptions } from "./util/og" 5 6 import { Theme } from "./util/theme" 6 7 7 8 export type Analytics = ··· 60 61 * Quartz will avoid using this as much as possible and use relative URLs most of the time 61 62 */ 62 63 baseUrl?: string 64 + /** 65 + * Whether to generate social images (Open Graph and Twitter standard) for link previews 66 + */ 67 + generateSocialImages: boolean | Partial<SocialImageOptions> 63 68 theme: Theme 64 69 /** 65 70 * Allow to translate the date in the language of your choice. 66 71 * Also used for UI translation (default: en-US) 67 - * Need to be formated following BCP 47: https://en.wikipedia.org/wiki/IETF_language_tag 72 + * Need to be formatted following BCP 47: https://en.wikipedia.org/wiki/IETF_language_tag 68 73 * The first part is the language (en) and the second part is the script/region (US) 69 74 * Language Codes: https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes 70 75 * Region Codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
+9
quartz/cli/handlers.js
··· 356 356 source: "**/*.*", 357 357 headers: [{ key: "Content-Disposition", value: "inline" }], 358 358 }, 359 + { 360 + source: "**/*.webp", 361 + headers: [{ key: "Content-Type", value: "image/webp" }], 362 + }, 363 + // fixes bug where avif images are displayed as text instead of images (future proof) 364 + { 365 + source: "**/*.avif", 366 + headers: [{ key: "Content-Type", value: "image/avif" }], 367 + }, 359 368 ], 360 369 }) 361 370 const status = res.statusCode
+165 -7
quartz/components/Head.tsx
··· 3 3 import { CSSResourceToStyleElement, JSResourceToScriptElement } from "../util/resources" 4 4 import { googleFontHref } from "../util/theme" 5 5 import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" 6 + import satori, { SatoriOptions } from "satori" 7 + import fs from "fs" 8 + import sharp from "sharp" 9 + import { ImageOptions, SocialImageOptions, getSatoriFont, defaultImage } from "../util/og" 10 + import { unescapeHTML } from "../util/escape" 11 + 12 + /** 13 + * Generates social image (OG/twitter standard) and saves it as `.webp` inside the public folder 14 + * @param opts options for generating image 15 + */ 16 + async function generateSocialImage( 17 + { cfg, description, fileName, fontsPromise, title, fileData }: ImageOptions, 18 + userOpts: SocialImageOptions, 19 + imageDir: string, 20 + ) { 21 + const fonts = await fontsPromise 22 + const { width, height } = userOpts 23 + 24 + // JSX that will be used to generate satori svg 25 + const imageComponent = userOpts.imageStructure(cfg, userOpts, title, description, fonts, fileData) 26 + 27 + const svg = await satori(imageComponent, { width, height, fonts }) 28 + 29 + // Convert svg directly to webp (with additional compression) 30 + const compressed = await sharp(Buffer.from(svg)).webp({ quality: 40 }).toBuffer() 31 + 32 + // Write to file system 33 + const filePath = joinSegments(imageDir, `${fileName}.${extension}`) 34 + fs.writeFileSync(filePath, compressed) 35 + } 36 + 37 + const extension = "webp" 38 + 39 + const defaultOptions: SocialImageOptions = { 40 + colorScheme: "lightMode", 41 + width: 1200, 42 + height: 630, 43 + imageStructure: defaultImage, 44 + excludeRoot: false, 45 + } 6 46 7 47 export default (() => { 8 - const Head: QuartzComponent = ({ cfg, fileData, externalResources }: QuartzComponentProps) => { 48 + let fontsPromise: Promise<SatoriOptions["fonts"]> 49 + 50 + let fullOptions: SocialImageOptions 51 + const Head: QuartzComponent = ({ 52 + cfg, 53 + fileData, 54 + externalResources, 55 + ctx, 56 + }: QuartzComponentProps) => { 57 + // Initialize options if not set 58 + if (!fullOptions) { 59 + if (typeof cfg.generateSocialImages !== "boolean") { 60 + fullOptions = { ...defaultOptions, ...cfg.generateSocialImages } 61 + } else { 62 + fullOptions = defaultOptions 63 + } 64 + } 65 + 66 + // Memoize google fonts 67 + if (!fontsPromise && cfg.generateSocialImages) { 68 + fontsPromise = getSatoriFont(cfg.theme.typography.header, cfg.theme.typography.body) 69 + } 70 + 71 + const slug = fileData.filePath 72 + // since "/" is not a valid character in file names, replace with "-" 73 + const fileName = slug?.replaceAll("/", "-") 74 + 75 + // Get file description (priority: frontmatter > fileData > default) 76 + const fdDescription = 77 + fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description 9 78 const titleSuffix = cfg.pageTitleSuffix ?? "" 10 79 const title = 11 80 (fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title) + titleSuffix 12 - const description = 13 - fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description 81 + let description = "" 82 + if (fdDescription) { 83 + description = unescapeHTML(fdDescription) 84 + } 85 + 86 + if (fileData.frontmatter?.socialDescription) { 87 + description = fileData.frontmatter?.socialDescription as string 88 + } else if (fileData.frontmatter?.description) { 89 + description = fileData.frontmatter?.description 90 + } 91 + 92 + const fileDir = joinSegments(ctx.argv.output, "static", "social-images") 93 + if (cfg.generateSocialImages) { 94 + // Generate folders for social images (if they dont exist yet) 95 + if (!fs.existsSync(fileDir)) { 96 + fs.mkdirSync(fileDir, { recursive: true }) 97 + } 98 + 99 + if (fileName) { 100 + // Generate social image (happens async) 101 + generateSocialImage( 102 + { 103 + title, 104 + description, 105 + fileName, 106 + fileDir, 107 + fileExt: extension, 108 + fontsPromise, 109 + cfg, 110 + fileData, 111 + }, 112 + fullOptions, 113 + fileDir, 114 + ) 115 + } 116 + } 117 + 14 118 const { css, js } = externalResources 15 119 16 120 const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) ··· 18 122 const baseDir = fileData.slug === "404" ? path : pathToRoot(fileData.slug!) 19 123 20 124 const iconPath = joinSegments(baseDir, "static/icon.png") 21 - const ogImagePath = `https://${cfg.baseUrl}/static/og-image.png` 125 + 126 + const ogImageDefaultPath = `https://${cfg.baseUrl}/static/og-image.png` 127 + // "static/social-images/slug-filename.md.webp" 128 + const ogImageGeneratedPath = `https://${cfg.baseUrl}/${fileDir.replace( 129 + `${ctx.argv.output}/`, 130 + "", 131 + )}/${fileName}.${extension}` 132 + 133 + // Use default og image if filePath doesnt exist (for autogenerated paths with no .md file) 134 + const useDefaultOgImage = fileName === undefined || !cfg.generateSocialImages 135 + 136 + // Path to og/social image (priority: frontmatter > generated image (if enabled) > default image) 137 + let ogImagePath = useDefaultOgImage ? ogImageDefaultPath : ogImageGeneratedPath 138 + 139 + // TODO: could be improved to support external images in the future 140 + // Aliases for image and cover handled in `frontmatter.ts` 141 + const frontmatterImgUrl = fileData.frontmatter?.socialImage 142 + 143 + // Override with default og image if config option is set 144 + if (fileData.slug === "index") { 145 + ogImagePath = ogImageDefaultPath 146 + } 147 + 148 + // Override with frontmatter url if existing 149 + if (frontmatterImgUrl) { 150 + ogImagePath = `https://${cfg.baseUrl}/static/${frontmatterImgUrl}` 151 + } 152 + 153 + // Url of current page 154 + const socialUrl = 155 + fileData.slug === "404" ? url.toString() : joinSegments(url.toString(), fileData.slug!) 22 156 23 157 return ( 24 158 <head> ··· 32 166 </> 33 167 )} 34 168 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 169 + {/* OG/Twitter meta tags */} 170 + <meta name="og:site_name" content={cfg.pageTitle}></meta> 35 171 <meta property="og:title" content={title} /> 172 + <meta property="og:type" content="website" /> 173 + <meta name="twitter:card" content="summary_large_image" /> 174 + <meta name="twitter:title" content={title} /> 175 + <meta name="twitter:description" content={description} /> 36 176 <meta property="og:description" content={description} /> 37 - {cfg.baseUrl && <meta property="og:image" content={ogImagePath} />} 38 - <meta property="og:width" content="1200" /> 39 - <meta property="og:height" content="675" /> 177 + <meta property="og:image:type" content={`image/${extension}`} /> 178 + <meta property="og:image:alt" content={description} /> 179 + {/* Dont set width and height if unknown (when using custom frontmatter image) */} 180 + {!frontmatterImgUrl && ( 181 + <> 182 + <meta property="og:image:width" content={fullOptions.width.toString()} /> 183 + <meta property="og:image:height" content={fullOptions.height.toString()} /> 184 + <meta property="og:width" content={fullOptions.width.toString()} /> 185 + <meta property="og:height" content={fullOptions.height.toString()} /> 186 + </> 187 + )} 188 + <meta property="og:image:url" content={ogImagePath} /> 189 + {cfg.baseUrl && ( 190 + <> 191 + <meta name="twitter:image" content={ogImagePath} /> 192 + <meta property="og:image" content={ogImagePath} /> 193 + <meta property="twitter:domain" content={cfg.baseUrl}></meta> 194 + <meta property="og:url" content={socialUrl}></meta> 195 + <meta property="twitter:url" content={socialUrl}></meta> 196 + </> 197 + )} 40 198 <link rel="icon" href={iconPath} /> 41 199 <meta name="description" content={description} /> 42 200 <meta name="generator" content="Quartz" />
+5
quartz/plugins/transformers/frontmatter.ts
··· 71 71 const cssclasses = coerceToArray(coalesceAliases(data, ["cssclasses", "cssclass"])) 72 72 if (cssclasses) data.cssclasses = cssclasses 73 73 74 + const socialImage = coalesceAliases(data, ["socialImage", "image", "cover"]) 75 + 76 + if (socialImage) data.socialImage = socialImage 77 + 74 78 // fill in frontmatter 75 79 file.data.frontmatter = data as QuartzPluginData["frontmatter"] 76 80 } ··· 93 97 lang: string 94 98 enableToc: string 95 99 cssclasses: string[] 100 + socialImage: string 96 101 comments: boolean | string 97 102 }> 98 103 }
+9
quartz/util/escape.ts
··· 6 6 .replaceAll('"', "&quot;") 7 7 .replaceAll("'", "&#039;") 8 8 } 9 + 10 + export const unescapeHTML = (html: string) => { 11 + return html 12 + .replaceAll("&amp;", "&") 13 + .replaceAll("&lt;", "<") 14 + .replaceAll("&gt;", ">") 15 + .replaceAll("&quot;", '"') 16 + .replaceAll("&#039;", "'") 17 + }
+200
quartz/util/og.tsx
··· 1 + import { FontWeight, SatoriOptions } from "satori/wasm" 2 + import { GlobalConfiguration } from "../cfg" 3 + import { QuartzPluginData } from "../plugins/vfile" 4 + import { JSXInternal } from "preact/src/jsx" 5 + import { ThemeKey } from "./theme" 6 + 7 + /** 8 + * Get an array of `FontOptions` (for satori) given google font names 9 + * @param headerFontName name of google font used for header 10 + * @param bodyFontName name of google font used for body 11 + * @returns FontOptions for header and body 12 + */ 13 + export async function getSatoriFont(headerFontName: string, bodyFontName: string) { 14 + const headerWeight = 700 as FontWeight 15 + const bodyWeight = 400 as FontWeight 16 + 17 + // Fetch fonts 18 + const headerFont = await fetchTtf(headerFontName, headerWeight) 19 + const bodyFont = await fetchTtf(bodyFontName, bodyWeight) 20 + 21 + // Convert fonts to satori font format and return 22 + const fonts: SatoriOptions["fonts"] = [ 23 + { name: headerFontName, data: headerFont, weight: headerWeight, style: "normal" }, 24 + { name: bodyFontName, data: bodyFont, weight: bodyWeight, style: "normal" }, 25 + ] 26 + return fonts 27 + } 28 + 29 + /** 30 + * Get the `.ttf` file of a google font 31 + * @param fontName name of google font 32 + * @param weight what font weight to fetch font 33 + * @returns `.ttf` file of google font 34 + */ 35 + async function fetchTtf(fontName: string, weight: FontWeight): Promise<ArrayBuffer> { 36 + try { 37 + // Get css file from google fonts 38 + const cssResponse = await fetch(`https://fonts.googleapis.com/css?family=${fontName}:${weight}`) 39 + const css = await cssResponse.text() 40 + 41 + // Extract .ttf url from css file 42 + const urlRegex = /url\((https:\/\/fonts.gstatic.com\/s\/.*?.ttf)\)/g 43 + const match = urlRegex.exec(css) 44 + 45 + if (!match) { 46 + throw new Error("Could not fetch font") 47 + } 48 + 49 + // Retrieve font data as ArrayBuffer 50 + const fontResponse = await fetch(match[1]) 51 + 52 + // fontData is an ArrayBuffer containing the .ttf file data (get match[1] due to google fonts response format, always contains link twice, but second entry is the "raw" link) 53 + const fontData = await fontResponse.arrayBuffer() 54 + 55 + return fontData 56 + } catch (error) { 57 + throw new Error(`Error fetching font: ${error}`) 58 + } 59 + } 60 + 61 + export type SocialImageOptions = { 62 + /** 63 + * What color scheme to use for image generation (uses colors from config theme) 64 + */ 65 + colorScheme: ThemeKey 66 + /** 67 + * Height to generate image with in pixels (should be around 630px) 68 + */ 69 + height: number 70 + /** 71 + * Width to generate image with in pixels (should be around 1200px) 72 + */ 73 + width: number 74 + /** 75 + * Whether to use the auto generated image for the root path ("/", when set to false) or the default og image (when set to true). 76 + */ 77 + excludeRoot: boolean 78 + /** 79 + * JSX to use for generating image. See satori docs for more info (https://github.com/vercel/satori) 80 + * @param cfg global quartz config 81 + * @param userOpts options that can be set by user 82 + * @param title title of current page 83 + * @param description description of current page 84 + * @param fonts global font that can be used for styling 85 + * @param fileData full fileData of current page 86 + * @returns prepared jsx to be used for generating image 87 + */ 88 + imageStructure: ( 89 + cfg: GlobalConfiguration, 90 + userOpts: UserOpts, 91 + title: string, 92 + description: string, 93 + fonts: SatoriOptions["fonts"], 94 + fileData: QuartzPluginData, 95 + ) => JSXInternal.Element 96 + } 97 + 98 + export type UserOpts = Omit<SocialImageOptions, "imageStructure"> 99 + 100 + export type ImageOptions = { 101 + /** 102 + * what title to use as header in image 103 + */ 104 + title: string 105 + /** 106 + * what description to use as body in image 107 + */ 108 + description: string 109 + /** 110 + * what fileName to use when writing to disk 111 + */ 112 + fileName: string 113 + /** 114 + * what directory to store image in 115 + */ 116 + fileDir: string 117 + /** 118 + * what file extension to use (should be `webp` unless you also change sharp conversion) 119 + */ 120 + fileExt: string 121 + /** 122 + * header + body font to be used when generating satori image (as promise to work around sync in component) 123 + */ 124 + fontsPromise: Promise<SatoriOptions["fonts"]> 125 + /** 126 + * `GlobalConfiguration` of quartz (used for theme/typography) 127 + */ 128 + cfg: GlobalConfiguration 129 + /** 130 + * full file data of current page 131 + */ 132 + fileData: QuartzPluginData 133 + } 134 + 135 + // This is the default template for generated social image. 136 + export const defaultImage: SocialImageOptions["imageStructure"] = ( 137 + cfg: GlobalConfiguration, 138 + { colorScheme }: UserOpts, 139 + title: string, 140 + description: string, 141 + fonts: SatoriOptions["fonts"], 142 + _fileData: QuartzPluginData, 143 + ) => { 144 + // How many characters are allowed before switching to smaller font 145 + const fontBreakPoint = 22 146 + const useSmallerFont = title.length > fontBreakPoint 147 + 148 + // Setup to access image 149 + const iconPath = `https://${cfg.baseUrl}/static/icon.png` 150 + return ( 151 + <div 152 + style={{ 153 + display: "flex", 154 + flexDirection: "column", 155 + justifyContent: "center", 156 + alignItems: "center", 157 + height: "100%", 158 + width: "100%", 159 + backgroundColor: cfg.theme.colors[colorScheme].light, 160 + gap: "2rem", 161 + paddingTop: "1.5rem", 162 + paddingBottom: "1.5rem", 163 + paddingLeft: "5rem", 164 + paddingRight: "5rem", 165 + }} 166 + > 167 + <div 168 + style={{ 169 + display: "flex", 170 + alignItems: "center", 171 + justifyContent: "flex-start", 172 + width: "100%", 173 + flexDirection: "row", 174 + gap: "2.5rem", 175 + }} 176 + > 177 + <img src={iconPath} width={135} height={135} /> 178 + <p 179 + style={{ 180 + color: cfg.theme.colors[colorScheme].dark, 181 + fontSize: useSmallerFont ? 70 : 82, 182 + fontFamily: fonts[0].name, 183 + }} 184 + > 185 + {title} 186 + </p> 187 + </div> 188 + <p 189 + style={{ 190 + color: cfg.theme.colors[colorScheme].dark, 191 + fontSize: 44, 192 + lineClamp: 3, 193 + fontFamily: fonts[1].name, 194 + }} 195 + > 196 + {description} 197 + </p> 198 + </div> 199 + ) 200 + }