diff --git a/astro.config.mjs b/astro.config.mjs index dc2641f..7f133c4 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -7,9 +7,11 @@ import cloudflare from "@astrojs/cloudflare"; import mdx from "@astrojs/mdx"; +import react from "@astrojs/react"; + // https://astro.build/config export default defineConfig({ - integrations: [tailwind(), mdx()], + integrations: [tailwind(), mdx(), react()], site: "https://yukky-sandbox.dev/", vite: { optimizeDeps: { @@ -28,4 +30,4 @@ export default defineConfig({ defaultStrategy: "viewport", prefetchAll: false, }, -}); \ No newline at end of file +}); diff --git a/bun.lockb b/bun.lockb index 379503f..ce41e5a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index def4f1e..d7c7f23 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,18 @@ "dependencies": { "@astrojs/cloudflare": "11.0.4", "@astrojs/mdx": "^3.1.6", + "@astrojs/react": "^3.6.2", "@astrojs/rss": "^4.0.7", "@astrojs/tailwind": "^5.1.0", "@formkit/tempo": "^0.1.2", + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", "astro": "4.15.6", "microcms-js-sdk": "^3.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "satori": "^0.11.0", + "sharp": "^0.33.5", "tailwindcss": "^3.3.3", "typescript-eslint": "^7.2.0" }, @@ -26,6 +33,7 @@ "@playwright/test": "^1.36.2", "@tailwindcss/typography": "^0.5.9", "@types/bun": "^1.0.8", + "@types/sharp": "^0.32.0", "eslint": "^8.46.0", "eslint-config-prettier": "^9.1.0", "eslint-config-standard-with-typescript": "^43.0.1", diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index dde5277..eccce16 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -6,9 +6,10 @@ export interface Props { title: string; description: string; ogType?: string; + ogImageUrl?: string; } -const { title, description, ogType } = Astro.props; +const { title, description, ogType, ogImageUrl } = Astro.props; const canonicalURL = new URL(Astro.url.pathname, Astro.site); --- @@ -23,17 +24,26 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site); - + + { ogImageUrl !== undefined ? : '' } + { ogImageUrl !== undefined ? : '' } + { ogImageUrl !== undefined ? : '' } + + + + { ogImageUrl !== undefined ? : } + { ogImageUrl !== undefined ? : '' } + diff --git a/src/layouts/PostLayout.astro b/src/layouts/PostLayout.astro index 9e8e3b3..1be27c3 100644 --- a/src/layouts/PostLayout.astro +++ b/src/layouts/PostLayout.astro @@ -5,13 +5,14 @@ export interface Props { title: string; description: string; createdAt: string; + slug: string; } -const { title, description, createdAt } = Astro.props; +const { title, description, createdAt, slug } = Astro.props; const OG_TYPE = "article"; const dateTypeCreatedAt = new Date(createdAt); --- - +
diff --git a/src/pages/og/[...slug].png.ts b/src/pages/og/[...slug].png.ts new file mode 100644 index 0000000..94a1a41 --- /dev/null +++ b/src/pages/og/[...slug].png.ts @@ -0,0 +1,24 @@ +import type { APIContext, APIRoute } from "astro"; +import { getEntry } from "astro:content"; +import { getOgImage } from "./_OgImage"; + +export const prerender = false; + +export async function GET({ params, redirect }: APIContext) { + const { slug } = params; + if (slug === undefined) { + return new Response(null, { + status: 500, + statusText: "No slug provided", + }); + } + const post = await getEntry("post", slug); + if (post === undefined) { + return new Response(null, { + status: 404, + statusText: "Not found", + }); + } + const body = await getOgImage(post?.data.title ?? "No title"); + return new Response(body); +} diff --git a/src/pages/og/_OgImage.tsx b/src/pages/og/_OgImage.tsx new file mode 100644 index 0000000..d4780c5 --- /dev/null +++ b/src/pages/og/_OgImage.tsx @@ -0,0 +1,130 @@ +import satori from "satori"; +import sharp from "sharp"; + +interface OgImageProps { + text: string; +} + +const OgImage = ({ text }: OgImageProps): JSX.Element => { + return ( +
+
+

{text}

+
+

+ + + + + + ゆっきー +

+

+ ゆっきーの砂場 +

+
+
+
+ ); +}; +export async function getOgImage(text: string): Promise { + const notoSansJpUrl = `https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@600`; + const reggeaeOneUlr = + "https://fonts.googleapis.com/css2?family=Reggae+One&display=swap&text=ゆっきーの砂場"; + const notoSansJpFontData = await getFontData(notoSansJpUrl); + const reggeaeOneFontData = await getFontData(reggeaeOneUlr); + const svg = await satori(, { + width: 800, + height: 400, + fonts: [ + { + name: "Noto Sans JP", + data: notoSansJpFontData, + style: "normal", + }, + { + name: "Reggae One", + data: reggeaeOneFontData, + style: "normal", + }, + ], + }); + + return await sharp(Buffer.from(svg)).png().toBuffer(); +} + +const getFontData = async (url: string): Promise => { + const css = await ( + await fetch(url, { + headers: { + "User-Agent": + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1", + }, + }) + ).text(); + + const resource = css.match( + /src: url\((.+)\) format\('(opentype|truetype)'\)/ + ); + + if (resource === null) { + throw new Error("Font resource not found"); + } + + return await fetch(resource[1]).then(async (res) => await res.arrayBuffer()); +}; diff --git a/src/pages/post/[...slug].astro b/src/pages/post/[...slug].astro index 9c7e817..6b4dec5 100644 --- a/src/pages/post/[...slug].astro +++ b/src/pages/post/[...slug].astro @@ -10,7 +10,7 @@ export async function getStaticPaths() { const { entry } = Astro.props; const { Content } = await entry.render(); --- - + diff --git a/tsconfig.json b/tsconfig.json index 8a703f3..aa69280 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,10 @@ { "extends": "astro/tsconfigs/strict", "compilerOptions": { - "types": ["bun-types"] + "types": [ + "bun-types" + ], + "jsx": "react-jsx", + "jsxImportSource": "react" } -} +} \ No newline at end of file