Skip to content

Commit 74a0c98

Browse files
committed
feat(meta): OG image + PWA manifest (#37)
1 parent a9ed8b6 commit 74a0c98

10 files changed

Lines changed: 503 additions & 2 deletions

File tree

app/apple-icon.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ImageResponse } from "next/og";
2+
3+
// iOS 홈 화면 추가용 아이콘 (180x180).
4+
export const size = { width: 180, height: 180 };
5+
export const contentType = "image/png";
6+
7+
export default function AppleIcon() {
8+
return new ImageResponse(
9+
(
10+
<div
11+
style={{
12+
fontSize: 130,
13+
background: "#0f172a",
14+
color: "#ffffff",
15+
width: "100%",
16+
height: "100%",
17+
display: "flex",
18+
alignItems: "center",
19+
justifyContent: "center",
20+
fontWeight: 700,
21+
letterSpacing: -4,
22+
}}
23+
>
24+
M
25+
</div>
26+
),
27+
{ ...size },
28+
);
29+
}

app/icon.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { ImageResponse } from "next/og";
2+
3+
// Favicon (32x32). Latin "M" — 한국어 폰트 fetch 없이도 안정적으로 렌더.
4+
export const size = { width: 32, height: 32 };
5+
export const contentType = "image/png";
6+
7+
export default function Icon() {
8+
return new ImageResponse(
9+
(
10+
<div
11+
style={{
12+
fontSize: 22,
13+
background: "#0f172a",
14+
color: "#ffffff",
15+
width: "100%",
16+
height: "100%",
17+
display: "flex",
18+
alignItems: "center",
19+
justifyContent: "center",
20+
fontWeight: 700,
21+
letterSpacing: -1,
22+
borderRadius: 6,
23+
}}
24+
>
25+
M
26+
</div>
27+
),
28+
{ ...size },
29+
);
30+
}

app/icons/[size]/route.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { ImageResponse } from "next/og";
2+
3+
// PWA manifest 가 참조하는 동적 아이콘 라우트. `/icons/192`, `/icons/512` 만 허용.
4+
// Korean 폰트 없이 안정적으로 렌더되도록 Latin "M" 사용.
5+
6+
export const runtime = "nodejs";
7+
8+
const ALLOWED_SIZES = new Set([192, 512]);
9+
10+
type Params = { size: string };
11+
12+
export async function GET(
13+
_req: Request,
14+
context: { params: Promise<Params> },
15+
) {
16+
const { size: raw } = await context.params;
17+
const n = Number.parseInt(raw, 10);
18+
if (!ALLOWED_SIZES.has(n)) {
19+
return new Response("Not Found", { status: 404 });
20+
}
21+
return new ImageResponse(
22+
(
23+
<div
24+
style={{
25+
fontSize: Math.round(n * 0.72),
26+
background: "#0f172a",
27+
color: "#ffffff",
28+
width: "100%",
29+
height: "100%",
30+
display: "flex",
31+
alignItems: "center",
32+
justifyContent: "center",
33+
fontWeight: 700,
34+
letterSpacing: -Math.round(n * 0.02),
35+
}}
36+
>
37+
M
38+
</div>
39+
),
40+
{ width: n, height: n },
41+
);
42+
}

app/layout.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,24 @@ const appUrl = process.env.NEXT_PUBLIC_APP_URL?.trim();
77

88
export const metadata: Metadata = {
99
metadataBase: appUrl ? new URL(appUrl) : undefined,
10-
title: "뮤런",
10+
title: {
11+
default: "뮤런",
12+
template: "%s · 뮤런",
13+
},
1114
description: "애니뮤 러닝 소모임의 정기 운동 아카이브",
15+
applicationName: "뮤런",
16+
openGraph: {
17+
type: "website",
18+
locale: "ko_KR",
19+
siteName: "뮤런",
20+
title: "뮤런",
21+
description: "애니뮤 러닝 소모임의 정기 운동 아카이브",
22+
},
23+
twitter: {
24+
card: "summary_large_image",
25+
title: "뮤런",
26+
description: "애니뮤 러닝 소모임의 정기 운동 아카이브",
27+
},
1228
};
1329

1430
export const viewport: Viewport = {

app/manifest.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { MetadataRoute } from "next";
2+
3+
// PWA 매니페스트. iOS / Android 모두 홈 화면 추가 가능.
4+
// 아이콘은 동적 라우트(`app/icons/[size]/route.tsx`)가 생성.
5+
export default function manifest(): MetadataRoute.Manifest {
6+
return {
7+
name: "뮤런",
8+
short_name: "뮤런",
9+
description: "애니뮤 러닝 소모임의 정기 운동 아카이브",
10+
start_url: "/",
11+
display: "standalone",
12+
orientation: "portrait",
13+
background_color: "#ffffff",
14+
theme_color: "#0f172a",
15+
icons: [
16+
{
17+
src: "/icons/192",
18+
sizes: "192x192",
19+
type: "image/png",
20+
purpose: "any",
21+
},
22+
{
23+
src: "/icons/512",
24+
sizes: "512x512",
25+
type: "image/png",
26+
purpose: "any",
27+
},
28+
{
29+
src: "/icons/192",
30+
sizes: "192x192",
31+
type: "image/png",
32+
purpose: "maskable",
33+
},
34+
{
35+
src: "/icons/512",
36+
sizes: "512x512",
37+
type: "image/png",
38+
purpose: "maskable",
39+
},
40+
],
41+
};
42+
}

app/opengraph-image.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { ImageResponse } from "next/og";
2+
3+
import { getOgFonts } from "@/lib/og-font";
4+
5+
// 기본 사이트 OG (홈/그 외 페이지에서 더 구체적인 게 없을 때). 1200x630.
6+
export const runtime = "nodejs";
7+
export const dynamic = "force-dynamic";
8+
export const alt = "뮤런 — 애니뮤 러닝 소모임 아카이브";
9+
export const size = { width: 1200, height: 630 };
10+
export const contentType = "image/png";
11+
12+
export default async function DefaultOg() {
13+
let fonts: Awaited<ReturnType<typeof getOgFonts>> | undefined;
14+
try {
15+
fonts = await getOgFonts();
16+
} catch {
17+
fonts = undefined;
18+
}
19+
20+
return new ImageResponse(
21+
(
22+
<div
23+
style={{
24+
width: "100%",
25+
height: "100%",
26+
background: "linear-gradient(135deg, #0f172a 0%, #1e293b 100%)",
27+
color: "#ffffff",
28+
display: "flex",
29+
flexDirection: "column",
30+
padding: 80,
31+
fontFamily: fonts ? "Noto Sans KR" : "sans-serif",
32+
}}
33+
>
34+
<div
35+
style={{
36+
fontSize: 28,
37+
opacity: 0.7,
38+
letterSpacing: 4,
39+
}}
40+
>
41+
MURUN
42+
</div>
43+
<div
44+
style={{
45+
marginTop: "auto",
46+
display: "flex",
47+
flexDirection: "column",
48+
gap: 16,
49+
}}
50+
>
51+
<div style={{ fontSize: 96, fontWeight: 700, lineHeight: 1.1 }}>
52+
뮤런
53+
</div>
54+
<div style={{ fontSize: 36, opacity: 0.85, fontWeight: 400 }}>
55+
애니뮤 러닝 소모임의 정기 운동 아카이브
56+
</div>
57+
</div>
58+
</div>
59+
),
60+
{ ...size, fonts },
61+
);
62+
}

0 commit comments

Comments
 (0)