feat: completed the production of open-graph-image for pages and articles using the astro-og-canvas library

This commit is contained in:
Guoqi Sun 2024-12-21 13:50:01 +08:00
parent 388d2c2ba7
commit 18abe2f3d9
21 changed files with 138 additions and 99 deletions

View file

@ -5,7 +5,7 @@ import { defineConfig } from "astro/config"
// https://astro.build/config
export default defineConfig({
site: "https://localhost:4321/",
site: "https://astro-air.netlify.app",
vite: {
worker: {
plugins: () => [],

View file

@ -20,6 +20,8 @@
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"astro": "^5.1.0",
"astro-og-canvas": "^0.5.5",
"canvaskit-wasm": "^0.39.1",
"lucide-react": "^0.468.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",

31
pnpm-lock.yaml generated
View file

@ -35,6 +35,12 @@ importers:
astro:
specifier: ^5.1.0
version: 5.1.0(@types/node@22.10.2)(jiti@2.4.2)(rollup@4.27.4)(typescript@5.7.2)(yaml@2.6.1)
astro-og-canvas:
specifier: ^0.5.5
version: 0.5.5(astro@5.1.0(@types/node@22.10.2)(jiti@2.4.2)(rollup@4.27.4)(typescript@5.7.2)(yaml@2.6.1))
canvaskit-wasm:
specifier: ^0.39.1
version: 0.39.1
lucide-react:
specifier: ^0.468.0
version: 0.468.0(react@18.3.1)
@ -1151,6 +1157,9 @@ packages:
'@vscode/l10n@0.0.18':
resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==}
'@webgpu/types@0.1.21':
resolution: {integrity: sha512-pUrWq3V5PiSGFLeLxoGqReTZmiiXwY3jRkIG5sLLKjyqNxrwm/04b4nw7LSmGWJcKk59XOM/YRTUwOzo4MMlow==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@ -1248,6 +1257,12 @@ packages:
resolution: {integrity: sha512-F6NW1RJo5pp2kPnnM97M5Ohw8zAGjv83MpxHqfAochH68n/kiXN57+hYaNUCA7XkScoVNr6yzvly3hsY34TGfQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
astro-og-canvas@0.5.5:
resolution: {integrity: sha512-dbZ7voJAmvy8Zyv5zFsgENs9G4uhcCJ4nVuUPb7ymu8+Vp/jr/fdQDKPrLYUG2TSi/1HejYRnzkXcSv6ntlDTA==}
engines: {node: '>=18.14.1'}
peerDependencies:
astro: ^3.0.0 || ^4.0.0 || ^5.0.0
astro@5.1.0:
resolution: {integrity: sha512-g/cqwGK84Ozp5jyW45c3+2KQ4BeJtigbfwO8EA3lr7AC+XjE6/5dMvX4/bBaWf3gJVghd0L6cdqwlWikq+/Rrw==}
engines: {node: ^18.17.1 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'}
@ -1329,6 +1344,9 @@ packages:
caniuse-lite@1.0.30001684:
resolution: {integrity: sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==}
canvaskit-wasm@0.39.1:
resolution: {integrity: sha512-Gy3lCmhUdKq+8bvDrs9t8+qf7RvcjuQn+we7vTVVyqgOVO1UVfHpsnBxkTZw+R4ApEJ3D5fKySl9TU11hmjl/A==}
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@ -4743,6 +4761,8 @@ snapshots:
'@vscode/l10n@0.0.18': {}
'@webgpu/types@0.1.21': {}
acorn-jsx@5.3.2(acorn@8.14.0):
dependencies:
acorn: 8.14.0
@ -4864,6 +4884,13 @@ snapshots:
- supports-color
- typescript
astro-og-canvas@0.5.5(astro@5.1.0(@types/node@22.10.2)(jiti@2.4.2)(rollup@4.27.4)(typescript@5.7.2)(yaml@2.6.1)):
dependencies:
astro: 5.1.0(@types/node@22.10.2)(jiti@2.4.2)(rollup@4.27.4)(typescript@5.7.2)(yaml@2.6.1)
canvaskit-wasm: 0.39.1
deterministic-object-hash: 2.0.2
entities: 4.5.0
astro@5.1.0(@types/node@22.10.2)(jiti@2.4.2)(rollup@4.27.4)(typescript@5.7.2)(yaml@2.6.1):
dependencies:
'@astrojs/compiler': 2.10.3
@ -5039,6 +5066,10 @@ snapshots:
caniuse-lite@1.0.30001684: {}
canvaskit-wasm@0.39.1:
dependencies:
'@webgpu/types': 0.1.21
ccount@2.0.1: {}
chalk@4.1.2:

BIN
public/avatar.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

Before

Width:  |  Height:  |  Size: 749 B

BIN
public/fonts/hwmc.otf Normal file

Binary file not shown.

View file

@ -1,14 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48">
<path fill="#17191E"
d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z" />
<path fill="url(#a)"
d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z" />
<path fill="#17191E"
d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z" />
<defs>
<linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse">
<stop stop-color="#D83333" />
<stop offset="1" stop-color="#F041FF" />
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -1,14 +0,0 @@
---
import { en, zh } from "~/config"
import { getLangFromUrl } from "~/i18n/utils"
const lang = getLangFromUrl(Astro.url)
const config = lang === "zh" ? zh.meta : en.meta
---
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href={config.favicon} />
<meta name="generator" content={Astro.generator} />
<title>{config.title} - {config.slogan}</title>
<meta name="description" content={config.description} />

View file

@ -1,6 +1,6 @@
---
import { getLangFromUrl, useTranslations } from "~/i18n/utils"
import HomeLayout from "~/layouts/home.astro"
import MainLayout from "~/layouts/main.astro"
import { formatDate } from "~/utils"
interface Props {
@ -16,7 +16,7 @@ const t = useTranslations(lang)
const filteredPosts = posts.filter((post: any) => post.data.tags?.includes(tag))
---
<HomeLayout>
<MainLayout>
<div class="mb-4 mt-2 text-2xl">
{tag}
</div>
@ -43,4 +43,4 @@ const filteredPosts = posts.filter((post: any) => post.data.tags?.includes(tag))
<p class="text-gray-500">{t("tag.no_posts")}</p>
)
}
</HomeLayout>
</MainLayout>

View file

@ -6,7 +6,7 @@ export const latestPosts = 8
const common = {
meta: {
favicon: "/favicon.svg",
favicon: "/avatar.png",
url: "https://blog.sunguoqi.com",
},
social: [

View file

@ -7,6 +7,7 @@ const postSchema = z.object({
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
ogImage: z.string().optional(),
tags: z.array(z.string()).optional(),
})

View file

@ -2,6 +2,7 @@
title: "My First Blog Post"
pubDate: 2020-07-01
description: "This is the first post of my new Astro blog."
ogImage: "https://sunguoqi.com/me.png"
author: "Astro Learner"
image:
url: "https://docs.astro.build/assets/rose.webp"

View file

@ -3,15 +3,51 @@ import "~/styles/globals.css"
import { NoiseBackground } from "~/components/react/noise-background"
import { ClientRouter } from "astro:transitions"
import { getLangFromUrl } from "~/i18n/utils"
import Head from "~/components/astro/head/root.astro"
import { en, zh } from "~/config"
const lang = getLangFromUrl(Astro.url)
const { title, description, ogImage } = Astro.props
const ogImageURL = new URL(ogImage, Astro.site).href
const permalink = new URL(Astro.url.pathname, Astro.site).href
const config = lang === "zh" ? zh.meta : en.meta
---
<!doctype html>
<html lang={lang}>
<head>
<Head />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href={config.favicon} />
<title>
{
!title
? `${config.title} - ${config.slogan}`
: `${config.title} - ${title}`
}
</title>
<meta name="generator" content={Astro.generator} />
<meta
name="description"
content={!description ? config.description : description}
/>
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={permalink} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={ogImageURL} />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={permalink} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={ogImageURL} />
<ClientRouter />
<script is:inline>
const setTheme = () => {

View file

@ -2,9 +2,13 @@
import Header from "~/components/astro/header.astro"
import Navigation from "~/components/astro/nav.astro"
import BaseLayout from "~/layouts/base.astro"
const { title, description, ogImage } = Astro.props
const filename = Astro.url.pathname.split("/").filter(Boolean).pop() ?? ""
const openGraphImage = !ogImage ? `/og/${filename}.png` : ogImage
---
<BaseLayout>
<BaseLayout title={title} description={description} ogImage={openGraphImage}>
<main class="max-auto mb-10 w-full max-w-3xl">
<Header />
<Navigation />

View file

@ -1,38 +0,0 @@
---
import HomeLayout from "~/layouts/home.astro"
import "~/styles/post.css"
import { formatDate } from "~/utils"
const { title, description, pubDate, updatedDate } = Astro.props
---
<HomeLayout>
<head slot="head">
<title>{title}</title>
{description && <meta name="description" content={description} />}
</head>
{
title && (
<div class="mb-4 flex flex-col gap-3">
<h1 class="text-2xl font-bold">{title}</h1>
{pubDate && (
<p class="text-sm text-gray-500 dark:text-gray-400">
{formatDate(pubDate)}
</p>
)}
</div>
)
}
<article class="prose dark:prose-invert">
<slot />
</article>
{
updatedDate && (
<div class="mt-10">
<p class="text-sm text-gray-500 dark:text-gray-400">
更新时间:{formatDate(updatedDate)}
</p>
</div>
)
}
</HomeLayout>

View file

@ -2,7 +2,7 @@
import AboutContentEn from "~/config/en/about.mdx"
import AboutContentZh from "~/config/zh/about.mdx"
import { getLangFromUrl } from "~/i18n/utils"
import HomeLayout from "~/layouts/home.astro"
import MainLayout from "~/layouts/main.astro"
import { getLanguagePaths } from "~/utils/langs"
export function getStaticPaths() {
@ -11,10 +11,12 @@ export function getStaticPaths() {
const lang = getLangFromUrl(Astro.url)
const AboutContent = lang === "zh" ? AboutContentZh : AboutContentEn
const ogImage = "https://sunguoqi.com/me.png"
---
<HomeLayout>
<MainLayout title="about" description="test" ogImage={ogImage}>
<div class="prose dark:prose-invert">
<AboutContent />
</div>
</HomeLayout>
</MainLayout>

View file

@ -1,6 +1,6 @@
---
import { getLangFromUrl } from "~/i18n/utils"
import HomeLayout from "~/layouts/home.astro"
import MainLayout from "~/layouts/main.astro"
import { formatDate, getPostsByLocale } from "~/utils"
import { getLanguagePaths } from "~/utils/langs"
@ -27,7 +27,7 @@ const postsByYear = posts.reduce(
const years = Object.keys(postsByYear).sort((a, b) => Number(b) - Number(a))
---
<HomeLayout>
<MainLayout>
<div class="space-y-8">
{
years.map((year) => (
@ -49,4 +49,4 @@ const years = Object.keys(postsByYear).sort((a, b) => Number(b) - Number(a))
))
}
</div>
</HomeLayout>
</MainLayout>

View file

@ -2,7 +2,7 @@
import Blog from "~/components/astro/blog.astro"
import Footer from "~/components/astro/footer.astro"
import Intro from "~/components/astro/intro.astro"
import HomeLayout from "~/layouts/home.astro"
import MainLayout from "~/layouts/main.astro"
import { getLanguagePaths } from "~/utils/langs"
export function getStaticPaths() {
@ -10,8 +10,8 @@ export function getStaticPaths() {
}
---
<HomeLayout>
<MainLayout>
<Intro />
<Blog />
<Footer />
</HomeLayout>
</MainLayout>

View file

@ -1,6 +1,7 @@
---
import { langs } from "~/i18n/ui"
import PostLayout from "~/layouts/post.astro"
import MainLayout from "~/layouts/main.astro"
import "~/styles/post.css"
import { getPostsByLocale } from "~/utils"
export async function getStaticPaths() {
@ -21,6 +22,8 @@ export async function getStaticPaths() {
const { post } = Astro.props
---
<PostLayout {...post.data}>
<MainLayout {...post.data}>
<article class="prose dark:prose-invert">
<div set:html={post.rendered.html} />
</PostLayout>
</article>
</MainLayout>

View file

@ -1,6 +1,6 @@
---
import { getLangFromUrl } from "~/i18n/utils"
import HomeLayout from "~/layouts/home.astro"
import MainLayout from "~/layouts/main.astro"
import { getPostsByLocale } from "~/utils"
import { getLanguagePaths } from "~/utils/langs"
@ -13,7 +13,7 @@ const posts = await getPostsByLocale(lang)
const tags = [...new Set(posts.map((post: any) => post.data.tags).flat())]
---
<HomeLayout>
<MainLayout>
<div>
{
tags.map((tag) => (
@ -23,4 +23,4 @@ const tags = [...new Set(posts.map((post: any) => post.data.tags).flat())]
))
}
</div>
</HomeLayout>
</MainLayout>

View file

@ -0,0 +1,34 @@
import { OGImageRoute } from "astro-og-canvas"
import { defaultLanguage } from "~/config"
import { getPostsByLocale } from "~/utils"
const posts = await getPostsByLocale(defaultLanguage)
// Transform the collection into an object
// @ts-ignore
const pages = Object.fromEntries(posts.map(({ id, data }) => [id, { data }]))
export const { getStaticPaths, GET } = OGImageRoute({
// The name of your dynamic route segment.
// In this case its `route`, because the file is named `[...route].ts`.
param: "route",
// A collection of pages to generate images for.
pages,
// For each page, this callback will be used to customize the OG image.
getImageOptions: async (_, { data }: (typeof pages)[string]) => {
return {
title: data.title,
description: data.description,
bgGradient: [
[6, 38, 45],
[8, 3, 2],
],
logo: {
path: "./public/avatar.png",
size: [100],
},
fonts: ["./public/fonts/hwmc.otf"],
}
},
})