dalfuss.net/docs/features/social images.md
Ben Schlegel 137d55eb1b
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 fa69c2a565
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 8eb1554b13
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 dcdeae4e7b
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 48452231d5
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 16d33fb771
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 b029eeadab
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 6a9e6352e8
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 d6301fae90.

commit 70e029d151
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 52a172d1a4.

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

    docs: document enableToc

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

    docs: wording changes for offline support

commit d6301fae90
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 27a6087dd5
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 1bf7e3d8b3
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 cc31a40b0c
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 0d3cf29226
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 7164857f6e
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 47024022e8
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 b98e4be665
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 8be51a0504
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 92cc23dc45
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 097abc3cda
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 a00324ddfd
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 9fff6d7d0d
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 0f5a9d7b66
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 b4236e5142
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 6e0c102970
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 94a54698ab
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 2e9a0c21db
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 b30a200bd4
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 6d59aa8201
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>
2024-11-12 07:33:35 -05:00

15 KiB

title
Social Media Preview Cards

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. 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.

Showcase

After enabling generateSocialImages in quartz.config.ts, the social media link preview for authoring content looks like this:

Light Dark
!social-image-preview-light.png !social-image-preview-dark.png

For testing, it is recommended to use opengraph.xyz to see what the link to your page will look like on various platforms (more info under social images#local testing).

Customization

You can customize how images will be generated in the quartz config.

For example, here's what the default configuration looks like if you set generateSocialImages: true:

generateSocialImages: {
  colorScheme: "lightMode", // what colors to use for generating image, same as theme colors from config, valid values are "darkMode" and "lightMode"
  width: 1200, // width to generate with (in pixels)
  height: 630, // height to generate with (in pixels)
  excludeRoot: false, // wether to exclude "/" index path to be excluded from auto generated images (false = use auto, true = use default og image)
}

Frontmatter Properties

[!tip] Hint

Overriding social media preview properties via frontmatter still works even if generateSocialImages is disabled.

The following properties can be used to customize your link previews:

Property Alias Summary
socialDescription description Description to be used for preview.
socialImage image, cover Link to preview image.

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".

[!info] Info

The priority for what image will be used for the cover image looks like the following: frontmatter property > generated image (if enabled) > default image.

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.


Fully customized image generation

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. Vercel provides an online playground that can be used to preview how your html/css looks like as a picture. This is ideal for prototyping your custom design.

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().

[!tip] Hint

Satori only supports a subset of all valid CSS properties. All supported properties can be found in their documentation.

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:

imageStructure: (
  cfg: GlobalConfiguration, // global Quartz config (useful for getting theme colors and other info)
  userOpts: UserOpts, // options passed to `generateSocialImage`
  title: string, // title of current page
  description: string, // description of current page
  fonts: SatoriOptions["fonts"], // header + body font
) => JSXInternal.Element

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.

[!example] Examples

Here are some examples for markup you may need to get started:

  • Get a theme color

    cfg.theme.colors[colorScheme].<colorName>, where <colorName> corresponds to a key in ColorScheme (defined at the top of quartz/util/theme.ts)

  • Use the page title/description

    <p>{title}</p>/<p>{description}</p>

  • Use a font family

    Detailed in the Fonts chapter below


Fonts

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. To use them in CSS, use the .name property (e.g. fontFamily: fonts[1].name to use the "body" font family).

An example of a component using the header font could look like this:

export const myImage: SocialImageOptions["imageStructure"] = (...) => {
  return <p style={{ fontFamily: fonts[0].name }}>Cool Header!</p>
}

[!example]- Local fonts

For cases where you use a local fonts under static folder, make sure to set the correct @font-face in custom.scss

@font-face {
  font-family: "Newsreader";
  font-style: normal;
  font-weight: normal;
  font-display: swap;
  src: url("/static/Newsreader.woff2") format("woff2");
}

Then in quartz/util/og.tsx, you can load the satori fonts like so:

const headerFont = joinSegments("static", "Newsreader.woff2")
const bodyFont = joinSegments("static", "Newsreader.woff2")

export async function getSatoriFont(cfg: GlobalConfiguration): Promise<SatoriOptions["fonts"]> {
  const headerWeight: FontWeight = 700
  const bodyWeight: FontWeight = 400

  const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)

  const [header, body] = await Promise.all(
    [headerFont, bodyFont].map((font) =>
      fetch(`${url.toString()}/${font}`).then((res) => res.arrayBuffer()),
    ),
  )

  return [
    { name: cfg.theme.typography.header, data: header, weight: headerWeight, style: "normal" },
    { name: cfg.theme.typography.body, data: body, weight: bodyWeight, style: "normal" },
  ]
}

This font then can be used with your custom structure

Local testing

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 (make sure to set Visibility to public if testing on external tools like opengraph.xyz).

If you have generateSocialImages enabled, you can check out all generated images under public/static/social-images.

Technical info

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.

When using images, the appropriate Open Graph and Twitter meta tags will be set to ensure they work and look as expected.

Examples

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).

After adding that file, you can update quartz.config.ts to use your image generation template as follows:

// Import component at start of file
import { customImage } from "./quartz/util/customSocialImage.tsx"

// In main config
const config: QuartzConfig = {
  ...
  generateSocialImages: {
    ...
    imageStructure: customImage, // tells quartz to use your component when generating images
  },
}

The following example will generate images that look as follows:

Light Dark
!custom-social-image-preview-light.png !custom-social-image-preview-dark.png

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).

import { SatoriOptions } from "satori/wasm"
import { GlobalConfiguration } from "../cfg"
import { SocialImageOptions, UserOpts } from "./imageHelper"
import { QuartzPluginData } from "../plugins/vfile"

export const customImage: SocialImageOptions["imageStructure"] = (
  cfg: GlobalConfiguration,
  userOpts: UserOpts,
  title: string,
  description: string,
  fonts: SatoriOptions["fonts"],
  fileData: QuartzPluginData,
) => {
  // How many characters are allowed before switching to smaller font
  const fontBreakPoint = 22
  const useSmallerFont = title.length > fontBreakPoint

  const { colorScheme } = userOpts
  return (
    <div
      style={{
        display: "flex",
        flexDirection: "row",
        justifyContent: "flex-start",
        alignItems: "center",
        height: "100%",
        width: "100%",
      }}
    >
      <div
        style={{
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          height: "100%",
          width: "100%",
          backgroundColor: cfg.theme.colors[colorScheme].light,
          flexDirection: "column",
          gap: "2.5rem",
          paddingTop: "2rem",
          paddingBottom: "2rem",
        }}
      >
        <p
          style={{
            color: cfg.theme.colors[colorScheme].dark,
            fontSize: useSmallerFont ? 70 : 82,
            marginLeft: "4rem",
            textAlign: "center",
            marginRight: "4rem",
            fontFamily: fonts[0].name,
          }}
        >
          {title}
        </p>
        <p
          style={{
            color: cfg.theme.colors[colorScheme].dark,
            fontSize: 44,
            marginLeft: "8rem",
            marginRight: "8rem",
            lineClamp: 3,
            fontFamily: fonts[1].name,
          }}
        >
          {description}
        </p>
      </div>
      <div
        style={{
          height: "100%",
          width: "2vw",
          position: "absolute",
          backgroundColor: cfg.theme.colors[colorScheme].tertiary,
          opacity: 0.85,
        }}
      />
    </div>
  )
}

[!example]- Advanced example

The following example includes a customized social image with a custom background and formatted date.

export const og: SocialImageOptions["Component"] = (
  cfg: GlobalConfiguration,
  fileData: QuartzPluginData,
  { colorScheme }: Options,
  title: string,
  description: string,
  fonts: SatoriOptions["fonts"],
) => {
  let created: string | undefined
  let reading: string | undefined
  if (fileData.dates) {
    created = formatDate(getDate(cfg, fileData)!, cfg.locale)
  }
  const { minutes, text: _timeTaken, words: _words } = readingTime(fileData.text!)
  reading = i18n(cfg.locale).components.contentMeta.readingTime({
    minutes: Math.ceil(minutes),
  })

  const Li = [created, reading]

  return (
    <div
      style={{
        position: "relative",
        display: "flex",
        flexDirection: "row",
        alignItems: "flex-start",
        height: "100%",
        width: "100%",
        backgroundImage: `url("https://${cfg.baseUrl}/static/og-image.jpeg")`,
        backgroundSize: "100% 100%",
      }}
    >
      <div
        style={{
          position: "absolute",
          top: 0,
          left: 0,
          right: 0,
          bottom: 0,
          background: "radial-gradient(circle at center, transparent, rgba(0, 0, 0, 0.4) 70%)",
        }}
      />
      <div
        style={{
          display: "flex",
          height: "100%",
          width: "100%",
          flexDirection: "column",
          justifyContent: "flex-start",
          alignItems: "flex-start",
          gap: "1.5rem",
          paddingTop: "4rem",
          paddingBottom: "4rem",
          marginLeft: "4rem",
        }}
      >
        <img
          src={`"https://${cfg.baseUrl}/static/icon.jpeg"`}
          style={{
            position: "relative",
            backgroundClip: "border-box",
            borderRadius: "6rem",
          }}
          width={80}
        />
        <div
          style={{
            display: "flex",
            flexDirection: "column",
            textAlign: "left",
            fontFamily: fonts[0].name,
          }}
        >
          <h2
            style={{
              color: cfg.theme.colors[colorScheme].light,
              fontSize: "3rem",
              fontWeight: 700,
              marginRight: "4rem",
              fontFamily: fonts[0].name,
            }}
          >
            {title}
          </h2>
          <ul
            style={{
              color: cfg.theme.colors[colorScheme].gray,
              gap: "1rem",
              fontSize: "1.5rem",
              fontFamily: fonts[1].name,
            }}
          >
            {Li.map((item, index) => {
              if (item) {
                return <li key={index}>{item}</li>
              }
            })}
          </ul>
        </div>
        <p
          style={{
            color: cfg.theme.colors[colorScheme].light,
            fontSize: "1.5rem",
            overflow: "hidden",
            marginRight: "8rem",
            textOverflow: "ellipsis",
            display: "-webkit-box",
            WebkitLineClamp: 7,
            WebkitBoxOrient: "vertical",
            lineClamp: 7,
            fontFamily: fonts[1].name,
          }}
        >
          {description}
        </p>
      </div>
    </div>
  )
}