feat: add og image

This commit is contained in:
KazooTTT
2025-01-30 20:26:14 +08:00
parent 5fcbb79ee4
commit f742a42d72
12 changed files with 144 additions and 15612 deletions

View File

@ -1,15 +1,15 @@
import { defineConfig } from 'astro/config'
import mdx from '@astrojs/mdx'
import tailwind from '@astrojs/tailwind'
import sitemap from '@astrojs/sitemap'
import { remarkReadingTime } from './src/utils/remarkReadingTime.ts'
import remarkUnwrapImages from 'remark-unwrap-images'
import rehypeExternalLinks from 'rehype-external-links'
import expressiveCode from 'astro-expressive-code'
import { expressiveCodeOptions } from './src/site.config'
import icon from 'astro-icon'
import react from '@astrojs/react'
import cloudflare from '@astrojs/cloudflare'
import mdx from '@astrojs/mdx'
import react from '@astrojs/react'
import sitemap from '@astrojs/sitemap'
import tailwind from '@astrojs/tailwind'
import expressiveCode from 'astro-expressive-code'
import icon from 'astro-icon'
import { defineConfig } from 'astro/config'
import rehypeExternalLinks from 'rehype-external-links'
import remarkUnwrapImages from 'remark-unwrap-images'
import { expressiveCodeOptions } from './src/site.config'
import { remarkReadingTime } from './src/utils/remarkReadingTime.ts'
// https://astro.build/config
export default defineConfig({
@ -79,5 +79,28 @@ export default defineConfig({
}
}
},
prefetch: true
prefetch: true,
vite: {
optimizeDeps: {
exclude: ['@resvg/resvg-js']
},
plugins: [tailwind(), rawFonts([".ttf", ".woff"])],
},
})
function rawFonts(ext) {
return {
name: "vite-plugin-raw-fonts",
// @ts-expect-error:next-line
transform(_, id) {
if (ext.some((e) => id.endsWith(e))) {
const buffer = fs.readFileSync(id);
return {
code: `export default ${JSON.stringify(buffer)}`,
map: null,
};
}
},
};
}

15556
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,7 @@
"@astrojs/sitemap": "^3.1.1",
"@astrojs/tailwind": "^5.1.0",
"@giscus/react": "^3.0.0",
"@resvg/resvg-js": "^2.6.2",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0",
"astro": "^4.4.15",
@ -46,6 +47,8 @@
"rehype-external-links": "^3.0.0",
"remark-unwrap-images": "^4.0.0",
"sanitize-html": "^2.13.1",
"satori": "^0.12.1",
"satori-html": "^0.3.2",
"sharp": "^0.33.2",
"tailwind-merge": "^2.2.1",
"tailwindcss": "^3.4.1",

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -3,29 +3,13 @@ import type { HTMLAttributes } from 'astro/types'
interface Props extends HTMLAttributes<'time'> {
date: Date
coverImage: string
}
const { date, coverImage: coverImage, ...attrs } = Astro.props
console.log('coverImage', coverImage)
const { date, ...attrs } = Astro.props
const formattedDate = date.toISOString().slice(0, 10).replace(/-/g, '')
---
{
coverImage ? (
<div class='relative'>
<time datetime={date.toISOString()} {...attrs}>
{formattedDate}
</time>
<img
src={coverImage}
class='absolute left-0 top-0 -z-10 h-full w-full object-cover opacity-10'
/>
</div>
) : (
<time datetime={date.toISOString()} {...attrs}>
{formattedDate}
</time>
)
}
<time datetime={date.toISOString()} {...attrs}>
{formattedDate}
</time>

View File

@ -8,25 +8,22 @@ import Label from '../Label.astro'
interface Props {
content: CollectionEntry<'post'>
simple?: boolean
socialImage?: string
}
const {
content: { data, render },
simple = false
simple = false,
socialImage
} = Astro.props
const { remarkPluginFrontmatter } = await render()
const dateTimeOptions: Intl.DateTimeFormatOptions = {
month: 'long'
}
---
{
data.coverImage && (
<div class='aspect-h-9 aspect-w-16 mb-6'>
socialImage && (
<div class='mb-6'>
<img
src={data.coverImage}
src={socialImage}
class='rounded-2xl object-cover'
fetchpriority='high'
loading='eager'
@ -34,11 +31,12 @@ const dateTimeOptions: Intl.DateTimeFormatOptions = {
</div>
)
}
{data.draft ? <span class='text-red-500'>(Draft)</span> : null}
<div class='flex flex-wrap items-center gap-x-3 gap-y-2'>
<p class='text-xs'>
<FormattedDate date={data.date} dateTimeOptions={dateTimeOptions} /> /{' '}
<FormattedDate date={data.date} /> /{' '}
{remarkPluginFrontmatter.minutesRead}
</p>
{
@ -106,12 +104,3 @@ const dateTimeOptions: Intl.DateTimeFormatOptions = {
</Card>
)
}
<!-- {
data.date && (
<p class='mt-6 text-base'>
Last Updated:
<FormattedDate class='ms-1' date={data.date} dateTimeOptions={dateTimeOptions} />
</p>
)
} -->

View File

@ -19,7 +19,7 @@ const coverImage = post.data.coverImage
{showYear && <h2 class='my-2 text-xl font-semibold'>{year}</h2>}
<li class='flex flex-col gap-2 sm:flex-row sm:gap-x-4 [&_q]:basis-full'>
<FormattedDate class='min-w-[80px]' date={postDate} coverImage={coverImage} />
<FormattedDate class='min-w-[80px]' date={postDate} />
<Tag>
{post.data.draft && <span class='text-red-500'>(Draft) </span>}

View File

@ -15,10 +15,11 @@ interface Props {
const { post, simple = false, backHref = '/blog' } = Astro.props
const {
data: { description, ogImage, title, date }
data: { description, ogImage, title, date },
slug
} = post
const socialImage = ogImage
const socialImage = ogImage ? ogImage : `/og-image/${slug}.png`
const articleDate = date?.toISOString()
const { headings } = await post.render()
---
@ -51,7 +52,7 @@ const { headings } = await post.render()
{!!headings.length && <TOC headings={headings} />}
<article class='flex-1 flex-grow break-words' data-pagefind-body>
<div id='blog-hero'>
<BlogHero content={post} simple={simple} />
<BlogHero content={post} simple={simple} socialImage={socialImage} />
</div>
<div
id='blog-gallery'

View File

@ -0,0 +1,88 @@
import NotoSansSC from "@/assets/NotoSansHans-Regular-2.ttf";
import RobotoMonoBold from "@/assets/roboto-mono-700.ttf";
import RobotoMono from "@/assets/roboto-mono-regular.ttf";
import { siteConfig } from "@/site-config";
import { Resvg } from "@resvg/resvg-js";
import type { APIContext, InferGetStaticPropsType } from "astro";
import satori, { type SatoriOptions } from "satori";
import { html } from "satori-html";
import { getSortedAllPostsAndDiaries } from "src/utils/post";
const ogOptions: SatoriOptions = {
// debug: true,
fonts: [
{
data: Buffer.from(RobotoMono),
name: "Roboto Mono",
style: "normal",
weight: 400,
},
{
data: Buffer.from(RobotoMonoBold),
name: "Roboto Mono",
style: "normal",
weight: 700,
},
{
data: Buffer.from(NotoSansSC),
name: "Noto Sans SC",
style: "normal",
weight: 400,
},
],
height: 630,
width: 1200,
};
const markup = (title: string, pubDate: string) =>
html`<div tw="flex flex-col w-full h-full bg-[#1d1f21] text-[#c9cacc] font-['Noto Sans SC']">
<div tw="flex flex-col flex-1 w-full p-10 justify-center">
<p tw="text-2xl mb-6">${pubDate}</p>
<h1 tw="text-6xl font-bold leading-snug text-white">${title}</h1>
</div>
<div tw="flex items-center justify-between w-full p-10 border-t border-[#2bbc89] text-xl">
<div tw="flex items-center">
<img tw="w-14 h-14 rounded-full" src="https://pictures.kazoottt.top/2025/01/20250130-121E9E4A-39FB-46DD-8D64-EAA3C77C6503.jpeg" alt="avatar" />
<p tw="ml-3 font-semibold">${siteConfig.title}</p>
</div>
<p>by ${siteConfig.author}</p>
</div>
</div>`;
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
export async function GET(context: APIContext) {
const { pubDate, title } = context.props as Props;
console.log("context.props", context.props)
const postDate = pubDate instanceof Date
? pubDate.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
: pubDate;
const htmlElement = markup(title, postDate);
const svg = await satori(htmlElement, ogOptions);
const png = new Resvg(svg).render().asPng();
return new Response(png, {
headers: {
"Cache-Control": "public, max-age=31536000, immutable",
"Content-Type": "image/png",
},
});
}
export async function getStaticPaths() {
const posts = await getSortedAllPostsAndDiaries();
return posts
.filter(({ data }) => !data.ogImage)
.map((post) => ({
params: { slug: post.id },
props: {
pubDate: post.data?.date,
title: post.data.title,
},
}));
}

View File

@ -60,7 +60,7 @@ const config = {
},
fontFamily: {
sans: [...fontFamily.sans],
satoshi: ['Satoshi', 'sans']
satoshi: ['Satoshi', 'sans', 'Noto Sans SC']
}
}
}