mirror of
https://github.com/KazooTTT/kazoottt-blog.git
synced 2025-06-16 23:41:21 +08:00
feat: add og image
This commit is contained in:
@ -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 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
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
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
15556
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -27,6 +27,7 @@
|
|||||||
"@astrojs/sitemap": "^3.1.1",
|
"@astrojs/sitemap": "^3.1.1",
|
||||||
"@astrojs/tailwind": "^5.1.0",
|
"@astrojs/tailwind": "^5.1.0",
|
||||||
"@giscus/react": "^3.0.0",
|
"@giscus/react": "^3.0.0",
|
||||||
|
"@resvg/resvg-js": "^2.6.2",
|
||||||
"@types/react": "^18.3.11",
|
"@types/react": "^18.3.11",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"astro": "^4.4.15",
|
"astro": "^4.4.15",
|
||||||
@ -46,6 +47,8 @@
|
|||||||
"rehype-external-links": "^3.0.0",
|
"rehype-external-links": "^3.0.0",
|
||||||
"remark-unwrap-images": "^4.0.0",
|
"remark-unwrap-images": "^4.0.0",
|
||||||
"sanitize-html": "^2.13.1",
|
"sanitize-html": "^2.13.1",
|
||||||
|
"satori": "^0.12.1",
|
||||||
|
"satori-html": "^0.3.2",
|
||||||
"sharp": "^0.33.2",
|
"sharp": "^0.33.2",
|
||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
|
BIN
src/assets/NotoSansHans-Regular-2.ttf
Normal file
BIN
src/assets/NotoSansHans-Regular-2.ttf
Normal file
Binary file not shown.
BIN
src/assets/roboto-mono-700.ttf
Normal file
BIN
src/assets/roboto-mono-700.ttf
Normal file
Binary file not shown.
BIN
src/assets/roboto-mono-regular.ttf
Normal file
BIN
src/assets/roboto-mono-regular.ttf
Normal file
Binary file not shown.
@ -3,29 +3,13 @@ import type { HTMLAttributes } from 'astro/types'
|
|||||||
|
|
||||||
interface Props extends HTMLAttributes<'time'> {
|
interface Props extends HTMLAttributes<'time'> {
|
||||||
date: Date
|
date: Date
|
||||||
coverImage: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { date, coverImage: coverImage, ...attrs } = Astro.props
|
const { date, ...attrs } = Astro.props
|
||||||
console.log('coverImage', coverImage)
|
|
||||||
|
|
||||||
const formattedDate = date.toISOString().slice(0, 10).replace(/-/g, '')
|
const formattedDate = date.toISOString().slice(0, 10).replace(/-/g, '')
|
||||||
---
|
---
|
||||||
|
|
||||||
{
|
|
||||||
coverImage ? (
|
|
||||||
<div class='relative'>
|
|
||||||
<time datetime={date.toISOString()} {...attrs}>
|
<time datetime={date.toISOString()} {...attrs}>
|
||||||
{formattedDate}
|
{formattedDate}
|
||||||
</time>
|
</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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
@ -8,25 +8,22 @@ import Label from '../Label.astro'
|
|||||||
interface Props {
|
interface Props {
|
||||||
content: CollectionEntry<'post'>
|
content: CollectionEntry<'post'>
|
||||||
simple?: boolean
|
simple?: boolean
|
||||||
|
socialImage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
content: { data, render },
|
content: { data, render },
|
||||||
simple = false
|
simple = false,
|
||||||
|
socialImage
|
||||||
} = Astro.props
|
} = Astro.props
|
||||||
|
|
||||||
const { remarkPluginFrontmatter } = await render()
|
const { remarkPluginFrontmatter } = await render()
|
||||||
|
|
||||||
const dateTimeOptions: Intl.DateTimeFormatOptions = {
|
|
||||||
month: 'long'
|
|
||||||
}
|
|
||||||
---
|
---
|
||||||
|
|
||||||
{
|
{
|
||||||
data.coverImage && (
|
socialImage && (
|
||||||
<div class='aspect-h-9 aspect-w-16 mb-6'>
|
<div class='mb-6'>
|
||||||
<img
|
<img
|
||||||
src={data.coverImage}
|
src={socialImage}
|
||||||
class='rounded-2xl object-cover'
|
class='rounded-2xl object-cover'
|
||||||
fetchpriority='high'
|
fetchpriority='high'
|
||||||
loading='eager'
|
loading='eager'
|
||||||
@ -34,11 +31,12 @@ const dateTimeOptions: Intl.DateTimeFormatOptions = {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
{data.draft ? <span class='text-red-500'>(Draft)</span> : null}
|
{data.draft ? <span class='text-red-500'>(Draft)</span> : null}
|
||||||
|
|
||||||
<div class='flex flex-wrap items-center gap-x-3 gap-y-2'>
|
<div class='flex flex-wrap items-center gap-x-3 gap-y-2'>
|
||||||
<p class='text-xs'>
|
<p class='text-xs'>
|
||||||
<FormattedDate date={data.date} dateTimeOptions={dateTimeOptions} /> /{' '}
|
<FormattedDate date={data.date} /> /{' '}
|
||||||
{remarkPluginFrontmatter.minutesRead}
|
{remarkPluginFrontmatter.minutesRead}
|
||||||
</p>
|
</p>
|
||||||
{
|
{
|
||||||
@ -106,12 +104,3 @@ const dateTimeOptions: Intl.DateTimeFormatOptions = {
|
|||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- {
|
|
||||||
data.date && (
|
|
||||||
<p class='mt-6 text-base'>
|
|
||||||
Last Updated:
|
|
||||||
<FormattedDate class='ms-1' date={data.date} dateTimeOptions={dateTimeOptions} />
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
} -->
|
|
||||||
|
@ -19,7 +19,7 @@ const coverImage = post.data.coverImage
|
|||||||
|
|
||||||
{showYear && <h2 class='my-2 text-xl font-semibold'>{year}</h2>}
|
{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'>
|
<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>
|
<Tag>
|
||||||
{post.data.draft && <span class='text-red-500'>(Draft) </span>}
|
{post.data.draft && <span class='text-red-500'>(Draft) </span>}
|
||||||
|
@ -15,10 +15,11 @@ interface Props {
|
|||||||
|
|
||||||
const { post, simple = false, backHref = '/blog' } = Astro.props
|
const { post, simple = false, backHref = '/blog' } = Astro.props
|
||||||
const {
|
const {
|
||||||
data: { description, ogImage, title, date }
|
data: { description, ogImage, title, date },
|
||||||
|
slug
|
||||||
} = post
|
} = post
|
||||||
|
|
||||||
const socialImage = ogImage
|
const socialImage = ogImage ? ogImage : `/og-image/${slug}.png`
|
||||||
const articleDate = date?.toISOString()
|
const articleDate = date?.toISOString()
|
||||||
const { headings } = await post.render()
|
const { headings } = await post.render()
|
||||||
---
|
---
|
||||||
@ -51,7 +52,7 @@ const { headings } = await post.render()
|
|||||||
{!!headings.length && <TOC headings={headings} />}
|
{!!headings.length && <TOC headings={headings} />}
|
||||||
<article class='flex-1 flex-grow break-words' data-pagefind-body>
|
<article class='flex-1 flex-grow break-words' data-pagefind-body>
|
||||||
<div id='blog-hero'>
|
<div id='blog-hero'>
|
||||||
<BlogHero content={post} simple={simple} />
|
<BlogHero content={post} simple={simple} socialImage={socialImage} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
id='blog-gallery'
|
id='blog-gallery'
|
||||||
|
88
src/pages/og-image/[...slug].png.ts
Normal file
88
src/pages/og-image/[...slug].png.ts
Normal 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,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
@ -60,7 +60,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: [...fontFamily.sans],
|
sans: [...fontFamily.sans],
|
||||||
satoshi: ['Satoshi', 'sans']
|
satoshi: ['Satoshi', 'sans', 'Noto Sans SC']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user