Skip to content

Commit

Permalink
[FEAT] OG Image 추가 (#886)
Browse files Browse the repository at this point in the history
* chore: @vercel/og 설치

* chore: default og image 추가

* feat: default og image 적용

* feat: 공유 페이지에서 동적인 og 이미지 생성

* ci/cd: 빌드 시 추가된 환경변수 적용
  • Loading branch information
dmdgpdi authored Jan 1, 2025
1 parent 7210f9a commit 71bf6cc
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 4 deletions.
1 change: 1 addition & 0 deletions .github/workflows/baguni.test.client.deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs:
echo "NEXT_PUBLIC_DOMAIN=${{ secrets.FRONT_NEXT_PUBLIC_DOMAIN }}" >> .env.production
echo "NEXT_PUBLIC_REDIRECT_URL=${{secrets.FRONT_NEXT_PUBLIC_REDIRECT_URL}}" >> .env.production
echo "NEXT_PUBLIC_MIXPANEL_TOKEN=${{secrets.FRONT_NEXT_PUBLIC_MIXPANEL_TOKEN}}" >> .env.production
echo "NEXT_NEXT_PUBLIC_IMAGE_URL=${{secrets.FRONT_NEXT_PUBLIC_IMAGE_URL}}" >> .env.production
- # .env.sentry-build-plugin 설정
name: Create .env.sentry-build-plugin
Expand Down
3 changes: 2 additions & 1 deletion frontend/techpick/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
NEXT_PUBLIC_DOMAIN='localhost를 실행할 도메인'
NEXT_PUBLIC_API='API 주소 요청 주소'
NEXT_PUBLIC_REDIRECT_URL=로그인 이후, 리다이렉트할 URL
NEXT_PUBLIC_MIXPANEL_TOKEN=믹스패널에 사용하는 토큰
NEXT_PUBLIC_MIXPANEL_TOKEN=믹스패널에 사용하는 토큰
NEXT_PUBLIC_IMAGE_URL=OG IMAGE URL을 가져오는 주소.
1 change: 1 addition & 0 deletions frontend/techpick/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@sentry/nextjs": "8",
"@tanstack/react-query": "^5.59.0",
"@vanilla-extract/css": "^1.15.5",
"@vercel/og": "^0.6.4",
"cmdk": "^1.0.0",
"dompurify": "^3.1.7",
"embla-carousel-react": "^8.5.1",
Expand Down
Binary file added frontend/techpick/public/image/og_image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 40 additions & 1 deletion frontend/techpick/src/app/(unsigned)/share/[uuid]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
loginButtonStyle,
} from './page.css';
import { SignUpLinkButton } from './SignUpLinkButton';

import type { Metadata, ResolvingMetadata } from 'next';
const EmptyPickRecordImage = dynamic(
() =>
import('@/components/EmptyPickRecordImage').then(
Expand All @@ -34,6 +34,45 @@ const EmptyPickRecordImage = dynamic(
}
);

export async function generateMetadata(
{
params,
}: {
params: { uuid: string };
},
parent: ResolvingMetadata
): Promise<Metadata> {
const { uuid } = params;
const sharedFolder = await getShareFolderById(uuid);
const { pickList } = sharedFolder;

const imageUrls = pickList
.map((pick) => pick.linkInfo.imageUrl)
.filter((url) => url && url !== '')
.slice(0, 16); // 최대 16개까지 허용

let ogImageUrl: string;

if (imageUrls.length === 0) {
ogImageUrl = `${process.env.NEXT_PUBLIC_IMAGE_URL}/image/og_image.png`;
} else {
const apiUrl = new URL(
`${process.env.NEXT_PUBLIC_IMAGE_URL}/api/generate-og-image`
);
apiUrl.searchParams.set('imageUrls', JSON.stringify(imageUrls));
ogImageUrl = apiUrl.toString();
}

const previousImages = (await parent).openGraph?.images || [];
return {
title: `${sharedFolder.folderName} 폴더 공유 페이지`,
description: `${pickList.length}개의 북마크가 공유되었습니다.`,
openGraph: {
images: [ogImageUrl, ...previousImages],
},
};
}

export default async function Page({ params }: { params: { uuid: string } }) {
const { uuid } = params;
const sharedFolder = await getShareFolderById(uuid);
Expand Down
70 changes: 70 additions & 0 deletions frontend/techpick/src/app/api/generate-og-image/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* eslint-disable jsx-a11y/alt-text */
import { NextRequest } from 'next/server';
import { ImageResponse } from '@vercel/og';

export const runtime = 'edge';

const styles = {
1: { width: '1200px', height: '630px' },
2: { width: '600px', height: '630px' },
4: { width: '600px', height: '315px' },
8: { width: '300px', height: '315px' },
16: { width: '300px', height: '157.5px' },
};

const getImageStyle = (index: number) => {
return styles[index as keyof typeof styles] || styles[16];
};

export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const imageUrls: string[] = JSON.parse(searchParams.get('imageUrls') || '[]');

const width = 1200;
const height = 630;

// 이미지 개수를 1, 2, 4, 8, 16 중 가장 가까운 수로 조정
const adjustedCount = [1, 2, 4, 8, 16].reduce((prev, curr) =>
Math.abs(curr - imageUrls.length) < Math.abs(prev - imageUrls.length)
? curr
: prev
);

const images = await Promise.all(
imageUrls.slice(0, adjustedCount).map(async (url: string) => {
try {
const res = await fetch(url);
if (!res.ok) throw new Error('Failed to fetch image');
return url;
} catch {
return '/image/og_image.png';
}
})
);
const imageCount = images.length;

return new ImageResponse(
(
<div
style={{
display: 'flex',
flexWrap: 'wrap',
width: '1200px',
height: '630px',
}}
>
{images.map((url: string, index: number) => (
<img
key={index}
src={url}
style={{
...getImageStyle(imageCount),
objectFit: 'cover',
}}
/>
))}
</div>
),
{ width, height }
);
}
3 changes: 3 additions & 0 deletions frontend/techpick/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export const metadata: Metadata = {
icons: {
icon: '/favicon.ico',
},
openGraph: {
images: `${process.env.NEXT_PUBLIC_IMAGE_URL}/image/og_image.png`,
},
};

export default function RootLayout({
Expand Down
Loading

0 comments on commit 71bf6cc

Please sign in to comment.