Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] OG Image 추가 #886

Merged
merged 5 commits into from
Jan 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading