diff --git a/app/admin/AdminLayoutClient.tsx b/app/admin/AdminLayoutClient.tsx
new file mode 100644
index 0000000..ddb161d
--- /dev/null
+++ b/app/admin/AdminLayoutClient.tsx
@@ -0,0 +1,22 @@
+'use client';
+
+import { SessionProvider } from 'next-auth/react';
+import { Suspense } from 'react';
+import ProtectedRoute from '@/app/entities/common/Layout/ProtectedRoute';
+import SVGLoadingSpinner from '@/app/entities/common/Loading/SVGLoadingSpinner';
+
+interface AdminLayoutClientProps {
+ children: React.ReactNode;
+}
+
+const AdminLayoutClient = ({ children }: AdminLayoutClientProps) => {
+ return (
+
+
+ }>{children}
+
+
+ );
+};
+
+export default AdminLayoutClient;
diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx
index d3236e2..8fea74c 100644
--- a/app/admin/layout.tsx
+++ b/app/admin/layout.tsx
@@ -1,21 +1,21 @@
-'use client';
-import { SessionProvider } from 'next-auth/react';
-import { Suspense } from 'react';
-import ProtectedRoute from '@/app/entities/common/Layout/ProtectedRoute';
-import SVGLoadingSpinner from '@/app/entities/common/Loading/SVGLoadingSpinner';
+import type { Metadata } from 'next';
+import AdminLayoutClient from './AdminLayoutClient';
+
+export const metadata: Metadata = {
+ title: '관리자 | ShipFriend TechBlog',
+ robots: {
+ index: false,
+ follow: false,
+ googleBot: { index: false, follow: false },
+ },
+};
interface AdminPageLayoutProps {
children: React.ReactNode;
}
const AdminPageLayout = ({ children }: AdminPageLayoutProps) => {
- return (
-
-
- }>{children}
-
-
- );
+ return {children};
};
export default AdminPageLayout;
diff --git a/app/api/portfolio/data.ts b/app/api/portfolio/data.ts
index 4ab18fa..5ff74fc 100644
--- a/app/api/portfolio/data.ts
+++ b/app/api/portfolio/data.ts
@@ -39,7 +39,7 @@ const shipfriend: PortfolioItem = {
url: '/posts/%EB%8B%A4%ED%81%AC%EB%AA%A8%EB%93%9C-fouc-%EA%B9%9C%EB%B9%A1%EC%9E%84-%ED%98%84%EC%83%81-%EC%88%98%EC%A0%95%ED%95%98%EA%B8%B0',
},
],
- mainImage: '/plane.png',
+ mainImage: '/images/projects/shipfriend/main.webp',
images: [
'/images/projects/shipfriend/bloglist.webp',
'/images/projects/shipfriend/blog-detail.webp',
diff --git a/app/atelier/layout.tsx b/app/atelier/layout.tsx
index c7b99e4..63af642 100644
--- a/app/atelier/layout.tsx
+++ b/app/atelier/layout.tsx
@@ -1,4 +1,5 @@
import { Metadata } from 'next';
+import { SITE_URL } from '@/app/lib/site';
import AtelierClientLayout from './AtelierClientLayout';
export const metadata: Metadata = {
@@ -7,12 +8,12 @@ export const metadata: Metadata = {
openGraph: {
title: 'Atelier - ShipFriend TechBlog',
description: '생각들을 던져두는 곳, Atelier 페이지입니다.',
- url: 'https://shipfriend.dev/atelier',
+ url: `${SITE_URL}/atelier`,
siteName: 'ShipFriend TechBlog',
type: 'website',
images: [
{
- url: 'https://shipfriend.dev/images/atelier/atelier-og-thumbnail.webp',
+ url: `${SITE_URL}/images/atelier/atelier-og-thumbnail.webp`,
width: 1200,
height: 630,
alt: 'Atelier',
@@ -23,7 +24,10 @@ export const metadata: Metadata = {
card: 'summary_large_image',
title: 'Atelier - ShipFriend TechBlog',
description: '생각들을 던져두는 곳, Atelier 페이지입니다.',
- images: ['https://shipfriend.dev/images/atelier/atelier-og-thumbnail.webp'],
+ images: [`${SITE_URL}/images/atelier/atelier-og-thumbnail.webp`],
+ },
+ alternates: {
+ canonical: `${SITE_URL}/atelier`,
},
};
diff --git a/app/entities/common/NavBar.tsx b/app/entities/common/NavBar.tsx
index fac6331..84322ae 100644
--- a/app/entities/common/NavBar.tsx
+++ b/app/entities/common/NavBar.tsx
@@ -52,11 +52,12 @@ const NavBar = () => {
className={`${fixedStyle} fixed h-16 top-0 px-4 w-screen inline-flex items-center justify-center z-40 backdrop-blur-sm`}
>
-
+
+
ShipFriend TechBlog
) {
- const siteSchema = {
- '@context': 'https://schema.org',
- '@graph': [
- {
- '@type': 'WebSite',
- '@id': `${SITE_URL}/#website`,
- url: SITE_URL,
- name: SITE_NAME,
- description: SITE_DESCRIPTION,
- inLanguage: 'ko-KR',
- publisher: {
- '@id': `${SITE_URL}/#organization`,
- },
- },
- {
- '@type': 'Organization',
- '@id': `${SITE_URL}/#organization`,
- name: SITE_NAME,
- url: SITE_URL,
- logo: {
- '@type': 'ImageObject',
- url: `${SITE_URL}/assets/apple-touch-icon.png`,
- },
- sameAs: ['https://github.com/ShipFriend0516'],
- },
- ],
- };
-
return (
@@ -186,12 +162,6 @@ export default function RootLayout({
__html: preventFOUC,
}}
/>
-
{
feed.addItem({
title: post.title,
- id: `${site_url}/posts/${post.slug}`,
- link: `${site_url}/posts/${post.slug}`,
+ id: `${SITE_URL}/posts/${encodeURIComponent(post.slug)}`,
+ link: `${SITE_URL}/posts/${encodeURIComponent(post.slug)}`,
description: post.subTitle,
content: post.content,
author: [
{
name: '개발자 서정우',
email: 'sjw4371@naver.com',
- link: site_url,
+ link: `${SITE_URL}/`,
},
],
date: new Date(post.date),
diff --git a/app/lib/site.ts b/app/lib/site.ts
new file mode 100644
index 0000000..6215e45
--- /dev/null
+++ b/app/lib/site.ts
@@ -0,0 +1,14 @@
+export const SITE_NAME = 'ShipFriend TechBlog';
+export const SITE_DESCRIPTION =
+ '문제 해결 경험과 개발 지식을 공유하는 개발 블로그입니다.';
+
+export const SITE_URL = (
+ process.env.NEXT_PUBLIC_DEPLOYMENT_URL || 'https://shipfriend.dev'
+).replace(/\/$/, '');
+
+export const DEFAULT_SOCIAL_IMAGE = `${SITE_URL}/images/profile/profile-banner.png`;
+
+export function absoluteUrl(pathOrUrl: string): string {
+ if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
+ return new URL(pathOrUrl, `${SITE_URL}/`).toString();
+}
diff --git a/app/not-found.tsx b/app/not-found.tsx
index a68a5ba..79ed29d 100644
--- a/app/not-found.tsx
+++ b/app/not-found.tsx
@@ -1,5 +1,12 @@
+import type { Metadata } from 'next';
import LottiePlayer from '@/app/entities/common/Animation/LottiePlayer';
import notfoundAnimation from '@/app/public/assets/notfound.json';
+
+export const metadata: Metadata = {
+ title: '페이지를 찾을 수 없음 | ShipFriend TechBlog',
+ robots: { index: false, follow: false },
+};
+
const NotFound = () => {
return (
diff --git a/app/page.tsx b/app/page.tsx
index ce1f59d..cf1edd0 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,4 +1,10 @@
+import type { Metadata } from 'next';
import dbConnect from '@/app/lib/dbConnect';
+import {
+ SITE_DESCRIPTION,
+ SITE_NAME,
+ SITE_URL,
+} from '@/app/lib/site';
import Post from '@/app/models/Post';
import { Post as PostType } from '@/app/types/Post';
import AboutMe from './entities/profile/AboutMe';
@@ -7,6 +13,40 @@ import HeroBanner from './entities/profile/HeroBanner';
import LatestArticles from './entities/profile/LatestArticles';
import WelcomeClient from './entities/profile/WelcomeClient';
+export const metadata: Metadata = {
+ title: SITE_NAME,
+ description: SITE_DESCRIPTION,
+ alternates: { canonical: `${SITE_URL}/` },
+};
+
+const siteSchema = {
+ '@context': 'https://schema.org',
+ '@graph': [
+ {
+ '@type': 'WebSite',
+ '@id': `${SITE_URL}/#website`,
+ url: `${SITE_URL}/`,
+ name: SITE_NAME,
+ alternateName: ['ShipFriend', 'shipfriend.dev'],
+ description: SITE_DESCRIPTION,
+ inLanguage: 'ko-KR',
+ publisher: { '@id': `${SITE_URL}/#organization` },
+ },
+ {
+ '@type': 'Organization',
+ '@id': `${SITE_URL}/#organization`,
+ name: SITE_NAME,
+ alternateName: 'ShipFriend',
+ url: `${SITE_URL}/`,
+ logo: {
+ '@type': 'ImageObject',
+ url: `${SITE_URL}/assets/apple-touch-icon.png`,
+ },
+ sameAs: ['https://github.com/ShipFriend0516'],
+ },
+ ],
+};
+
const Home = async () => {
await dbConnect();
const publicFilter = {
@@ -25,13 +65,21 @@ const Home = async () => {
const posts = rawPosts as unknown as PostType[];
return (
-
-
-
-
-
-
-
+ <>
+
+
+ >
);
};
diff --git a/app/portfolio/[slug]/page.tsx b/app/portfolio/[slug]/page.tsx
index 3fd3b54..52c39b4 100644
--- a/app/portfolio/[slug]/page.tsx
+++ b/app/portfolio/[slug]/page.tsx
@@ -1,5 +1,6 @@
import { Metadata } from 'next';
import { portfolioData } from '@/app/api/portfolio/data';
+import { absoluteUrl, SITE_NAME, SITE_URL } from '@/app/lib/site';
import PortfolioDetailClient from './PortfolioDetailClient';
interface PortfolioDetailPageProps {
@@ -8,9 +9,6 @@ interface PortfolioDetailPageProps {
}>;
}
-const baseUrl =
- process.env.NEXT_PUBLIC_DEPLOYMENT_URL || 'https://shipfriend.dev';
-
export async function generateMetadata(props: PortfolioDetailPageProps): Promise
{
const params = await props.params;
const portfolio = portfolioData[params.slug as keyof typeof portfolioData];
@@ -27,12 +25,12 @@ export async function generateMetadata(props: PortfolioDetailPageProps): Promise
openGraph: {
title: `${portfolio.title} | ShipFriend TechBlog`,
description: portfolio.description,
- url: `${baseUrl}/portfolio/${params.slug}`,
- siteName: 'ShipFriend TechBlog',
+ url: `${SITE_URL}/portfolio/${params.slug}`,
+ siteName: SITE_NAME,
locale: 'ko_KR',
type: 'website',
images: portfolio.mainImage
- ? [{ url: `${baseUrl}${portfolio.mainImage}`, alt: portfolio.title }]
+ ? [{ url: absoluteUrl(portfolio.mainImage), alt: portfolio.title }]
: undefined,
},
twitter: {
@@ -40,11 +38,11 @@ export async function generateMetadata(props: PortfolioDetailPageProps): Promise
title: `${portfolio.title} | ShipFriend TechBlog`,
description: portfolio.description,
images: portfolio.mainImage
- ? [`${baseUrl}${portfolio.mainImage}`]
+ ? [absoluteUrl(portfolio.mainImage)]
: undefined,
},
alternates: {
- canonical: `${baseUrl}/portfolio/${params.slug}`,
+ canonical: `${SITE_URL}/portfolio/${params.slug}`,
},
};
}
diff --git a/app/portfolio/layout.tsx b/app/portfolio/layout.tsx
index 76b3c9a..5bd4b78 100644
--- a/app/portfolio/layout.tsx
+++ b/app/portfolio/layout.tsx
@@ -1,21 +1,27 @@
import type { Metadata } from 'next';
+import { DEFAULT_SOCIAL_IMAGE, SITE_NAME, SITE_URL } from '@/app/lib/site';
export const metadata: Metadata = {
title: '포트폴리오 | ShipFriend TechBlog',
description: 'ShipFriend의 프로젝트 포트폴리오입니다.',
- robots: {
- index: false,
- follow: false,
- },
+ robots: { index: true, follow: true },
openGraph: {
title: '포트폴리오 | ShipFriend TechBlog',
description: 'ShipFriend의 프로젝트 포트폴리오입니다.',
- url:
- (process.env.NEXT_PUBLIC_DEPLOYMENT_URL || 'https://shipfriend.dev') +
- '/portfolio',
- siteName: 'ShipFriend TechBlog',
+ url: `${SITE_URL}/portfolio`,
+ siteName: SITE_NAME,
locale: 'ko_KR',
type: 'website',
+ images: [DEFAULT_SOCIAL_IMAGE],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: '포트폴리오 | ShipFriend TechBlog',
+ description: 'ShipFriend의 프로젝트 포트폴리오입니다.',
+ images: [DEFAULT_SOCIAL_IMAGE],
+ },
+ alternates: {
+ canonical: `${SITE_URL}/portfolio`,
},
};
diff --git a/app/posts/[slug]/page.tsx b/app/posts/[slug]/page.tsx
index 3cb3779..21a2aed 100644
--- a/app/posts/[slug]/page.tsx
+++ b/app/posts/[slug]/page.tsx
@@ -1,5 +1,5 @@
import { Metadata } from 'next';
-import { permanentRedirect } from 'next/navigation';
+import { notFound, permanentRedirect } from 'next/navigation';
import Comments from '@/app/entities/comment/Comments';
import PostActionSection from '@/app/entities/post/detail/PostActionSection';
import PostBreadcrumbJSONLd from '@/app/entities/post/detail/PostBreadcrumbJSONLd';
@@ -8,15 +8,19 @@ import PostJSONLd from '@/app/entities/post/detail/PostJSONLd';
import PostRecommendation from '@/app/entities/post/detail/PostRecommendation';
import SubscribeToast from '@/app/entities/post/detail/SubscribeToast';
import dbConnect from '@/app/lib/dbConnect';
+import { absoluteUrl, SITE_NAME, SITE_URL } from '@/app/lib/site';
import { stripMarkdown } from '@/app/lib/utils/stripMarkdown';
import Post from '@/app/models/Post';
const defaultThumbnail = '/images/placeholder/thumbnail_example2.webp';
+const publicPostFilter = {
+ $or: [{ isPrivate: false }, { isPrivate: { $exists: false } }],
+};
// 정적 생성할 경로 지정 - SSG
export async function generateStaticParams() {
await dbConnect();
- const posts = await Post.find({}, 'slug legacySlug').lean<
+ const posts = await Post.find(publicPostFilter, 'slug legacySlug').lean<
{ slug: string; legacySlug?: string[] }[]
>();
@@ -37,17 +41,20 @@ async function getPostDetail(slug: string) {
await dbConnect();
const decoded = decodeURIComponent(slug);
- const post = await Post.findOne({ slug: decoded }).lean();
+ const post = await Post.findOne({
+ slug: decoded,
+ ...publicPostFilter,
+ }).lean();
if (!post) {
const legacyPost = await Post.findOne(
- { legacySlug: decoded },
+ { legacySlug: decoded, ...publicPostFilter },
{ slug: 1 }
).lean<{ slug: string }>();
if (legacyPost) {
permanentRedirect(`/posts/${legacyPost.slug}`);
}
- throw new Error('Post not found');
+ notFound();
}
return { post: JSON.parse(JSON.stringify(post)) };
@@ -60,12 +67,11 @@ export const generateMetadata = async (
): Promise => {
const params = await props.params;
const { post } = await getPostDetail(params.slug);
- const baseUrl =
- process.env.NEXT_PUBLIC_DEPLOYMENT_URL || 'https://shipfriend.dev';
- const postUrl = `${baseUrl}/posts/${post.slug}`;
+ const postUrl = `${SITE_URL}/posts/${encodeURIComponent(post.slug)}`;
const description = post.subTitle
? stripMarkdown(post.subTitle, 160)
: stripMarkdown(post.content, 160);
+ const imageUrl = absoluteUrl(post.thumbnailImage || defaultThumbnail);
return {
title: post.title,
@@ -78,22 +84,26 @@ export const generateMetadata = async (
title: post.title,
description,
url: postUrl,
- images: [post.thumbnailImage || defaultThumbnail],
+ siteName: SITE_NAME,
+ images: [imageUrl],
type: 'article',
locale: 'ko_KR',
publishedTime: new Date(post.date).toISOString(),
+ modifiedTime: new Date(post.updatedAt || post.date).toISOString(),
authors: [post.author],
+ tags: post.tags,
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: post.title,
+ description,
+ images: [imageUrl],
},
other: {
- 'application-name': 'ShipFriend TechBlog',
author: post.author,
publish_date: new Date(post.date).toISOString(),
'article:tag':
post.tags?.join(',') || 'technology,programming,web development',
- 'twitter:card': 'summary_large_image',
- 'twitter:title': post.title,
- 'twitter:description': description,
- 'twitter:image': String(post.thumbnailImage || defaultThumbnail),
},
};
};
diff --git a/app/robots.ts b/app/robots.ts
index 56cc8de..51030b9 100644
--- a/app/robots.ts
+++ b/app/robots.ts
@@ -1,13 +1,13 @@
// app/robots.ts
import { MetadataRoute } from 'next';
+import { SITE_URL } from '@/app/lib/site';
export default function robots(): MetadataRoute.Robots {
- const baseURL = process.env.NEXT_PUBLIC_DEPLOYMENT_URL || process.env.NEXTAUTH_URL || 'http://localhost:3000';
return {
rules: {
userAgent: '*',
- disallow: ['/admin/', '/private/', '/api/'],
+ disallow: ['/admin', '/private', '/api'],
},
- sitemap: [`${baseURL}/sitemap.xml`, `${baseURL}/rss.xml`],
+ sitemap: [`${SITE_URL}/sitemap.xml`, `${SITE_URL}/rss.xml`],
};
}
diff --git a/app/series/[slug]/layout.tsx b/app/series/[slug]/layout.tsx
index ff18606..f38cbd9 100644
--- a/app/series/[slug]/layout.tsx
+++ b/app/series/[slug]/layout.tsx
@@ -1,5 +1,11 @@
import type { Metadata } from 'next';
import dbConnect from '@/app/lib/dbConnect';
+import {
+ absoluteUrl,
+ DEFAULT_SOCIAL_IMAGE,
+ SITE_NAME,
+ SITE_URL,
+} from '@/app/lib/site';
import SeriesModel from '@/app/models/Series';
import { Series } from '@/app/types/Series';
@@ -14,9 +20,6 @@ export async function generateMetadata(
}
): Promise {
const params = await props.params;
- const baseUrl =
- process.env.NEXT_PUBLIC_DEPLOYMENT_URL || 'https://shipfriend.dev';
-
try {
await dbConnect();
const slug = decodeURIComponent(params.slug);
@@ -35,15 +38,15 @@ export async function generateMetadata(
const description =
series.description ||
`${series.title} 시리즈의 글 목록입니다. ShipFriend TechBlog에서 연재되는 시리즈입니다.`;
- const url = `${baseUrl}/series/${params.slug}`;
+ const url = `${SITE_URL}/series/${encodeURIComponent(series.slug)}`;
const images = series.thumbnailImage
- ? [{ url: series.thumbnailImage, alt: series.title }]
+ ? [{ url: absoluteUrl(series.thumbnailImage), alt: series.title }]
: [
{
- url: `${baseUrl}/images/profile/profile-banner.png`,
- width: 512,
- height: 512,
+ url: DEFAULT_SOCIAL_IMAGE,
+ width: 1424,
+ height: 752,
alt: 'ShipFriend TechBlog',
},
];
@@ -62,16 +65,22 @@ export async function generateMetadata(
title,
description,
url,
- siteName: 'ShipFriend TechBlog',
+ siteName: SITE_NAME,
images,
locale: 'ko_KR',
type: 'website',
},
+ twitter: {
+ card: 'summary_large_image',
+ title,
+ description,
+ images: images.map((image) => image.url),
+ },
alternates: {
- canonical: `/series/${params.slug}`,
+ canonical: url,
},
};
- } catch (error) {
+ } catch {
return {
title: '시리즈 | ShipFriend TechBlog',
description: '시리즈 글 목록입니다.',
diff --git a/app/series/[slug]/page.tsx b/app/series/[slug]/page.tsx
index 3d0cbbf..93af39b 100644
--- a/app/series/[slug]/page.tsx
+++ b/app/series/[slug]/page.tsx
@@ -42,7 +42,13 @@ const SeriesDetailPage = async (props: SeriesDetailPageProps) => {
}
const raw = await Series.findOne({ slug })
- .populate({ path: 'posts', options: { sort: { date: 1 } } })
+ .populate({
+ path: 'posts',
+ match: {
+ $or: [{ isPrivate: false }, { isPrivate: { $exists: false } }],
+ },
+ options: { sort: { date: 1 } },
+ })
.lean();
if (!raw) notFound();
diff --git a/app/series/layout.tsx b/app/series/layout.tsx
index f040d73..89cf0ae 100644
--- a/app/series/layout.tsx
+++ b/app/series/layout.tsx
@@ -1,4 +1,5 @@
import type { Metadata } from 'next';
+import { DEFAULT_SOCIAL_IMAGE, SITE_NAME, SITE_URL } from '@/app/lib/site';
export const metadata: Metadata = {
title: '시리즈 | ShipFriend TechBlog',
@@ -15,15 +16,21 @@ export const metadata: Metadata = {
title: '시리즈 | ShipFriend TechBlog',
description:
'주제별로 정리된 시리즈 글 목록입니다. 관심 있는 시리즈를 선택해 순서대로 읽어보세요.',
- url:
- (process.env.NEXT_PUBLIC_DEPLOYMENT_URL || 'https://shipfriend.dev') +
- '/series',
- siteName: 'ShipFriend TechBlog',
+ url: `${SITE_URL}/series`,
+ siteName: SITE_NAME,
locale: 'ko_KR',
type: 'website',
+ images: [DEFAULT_SOCIAL_IMAGE],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: '시리즈 | ShipFriend TechBlog',
+ description:
+ '주제별로 정리된 시리즈 글 목록입니다. 관심 있는 시리즈를 선택해 순서대로 읽어보세요.',
+ images: [DEFAULT_SOCIAL_IMAGE],
},
alternates: {
- canonical: '/series',
+ canonical: `${SITE_URL}/series`,
},
};
diff --git a/app/sitemap.ts b/app/sitemap.ts
index 5db26e2..c7a2ad0 100644
--- a/app/sitemap.ts
+++ b/app/sitemap.ts
@@ -1,5 +1,6 @@
import { MetadataRoute } from 'next';
import dbConnect from '@/app/lib/dbConnect';
+import { SITE_URL } from '@/app/lib/site';
import Post from '@/app/models/Post';
import Series from '@/app/models/Series';
@@ -15,10 +16,7 @@ function maxDateString(dates: (Date | string | undefined)[]): string {
}
export default async function sitemap(): Promise {
- const baseUrl =
- process.env.NEXT_PUBLIC_DEPLOYMENT_URL ||
- process.env.NEXTAUTH_URL ||
- 'http://localhost:3000';
+ const baseUrl = SITE_URL;
if (!process.env.DB_URI) {
console.error('Database URI is not defined');
@@ -28,7 +26,11 @@ export default async function sitemap(): Promise {
await dbConnect(process.env.DB_URI);
// 포스트 URL 생성
- const posts = await Post.find({}).sort({ updatedAt: -1 }).lean();
+ const posts = await Post.find({
+ $or: [{ isPrivate: false }, { isPrivate: { $exists: false } }],
+ })
+ .sort({ updatedAt: -1 })
+ .lean();
const postsLastmod = posts.length
? maxDateString(posts.map((p) => p.updatedAt || p.date))
: toDateString(new Date());
diff --git a/app/subscribe/error/page.tsx b/app/subscribe/error/page.tsx
index 5cf5bbd..33c4066 100644
--- a/app/subscribe/error/page.tsx
+++ b/app/subscribe/error/page.tsx
@@ -1,8 +1,10 @@
import { Metadata } from 'next';
import Link from 'next/link';
+import { SITE_URL } from '@/app/lib/site';
export const metadata: Metadata = {
robots: { index: false, follow: false },
+ alternates: { canonical: `${SITE_URL}/subscribe/error` },
};
const errorMessages: Record = {
diff --git a/app/subscribe/unsubscribed/page.tsx b/app/subscribe/unsubscribed/page.tsx
index 3d57f1a..2fe78a9 100644
--- a/app/subscribe/unsubscribed/page.tsx
+++ b/app/subscribe/unsubscribed/page.tsx
@@ -1,8 +1,10 @@
import { Metadata } from 'next';
import Link from 'next/link';
+import { SITE_URL } from '@/app/lib/site';
export const metadata: Metadata = {
robots: { index: false, follow: false },
+ alternates: { canonical: `${SITE_URL}/subscribe/unsubscribed` },
};
export default async function UnsubscribedPage(
diff --git a/app/subscribe/verified/page.tsx b/app/subscribe/verified/page.tsx
index af17a1b..4987e6f 100644
--- a/app/subscribe/verified/page.tsx
+++ b/app/subscribe/verified/page.tsx
@@ -1,8 +1,10 @@
import { Metadata } from 'next';
import Link from 'next/link';
+import { SITE_URL } from '@/app/lib/site';
export const metadata: Metadata = {
robots: { index: false, follow: false },
+ alternates: { canonical: `${SITE_URL}/subscribe/verified` },
};
export default async function VerifiedPage(
diff --git a/app/tags/layout.tsx b/app/tags/layout.tsx
index fb63332..34adf46 100644
--- a/app/tags/layout.tsx
+++ b/app/tags/layout.tsx
@@ -1,6 +1,5 @@
import type { Metadata } from 'next';
-
-const BASE_URL = process.env.NEXT_PUBLIC_DEPLOYMENT_URL || 'https://shipfriend.dev';
+import { DEFAULT_SOCIAL_IMAGE, SITE_NAME, SITE_URL } from '@/app/lib/site';
export const metadata: Metadata = {
title: '태그 | ShipFriend TechBlog',
@@ -20,13 +19,21 @@ export const metadata: Metadata = {
title: '태그 | ShipFriend TechBlog',
description:
'ShipFriend TechBlog의 태그 목록 페이지입니다. React, TypeScript, Next.js 등 개발 주제별 글을 탐색해보세요.',
- url: `${BASE_URL}/tags`,
- siteName: 'ShipFriend TechBlog',
+ url: `${SITE_URL}/tags`,
+ siteName: SITE_NAME,
locale: 'ko_KR',
type: 'website',
+ images: [DEFAULT_SOCIAL_IMAGE],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: '태그 | ShipFriend TechBlog',
+ description:
+ 'ShipFriend TechBlog의 태그 목록 페이지입니다. React, TypeScript, Next.js 등 개발 주제별 글을 탐색해보세요.',
+ images: [DEFAULT_SOCIAL_IMAGE],
},
alternates: {
- canonical: `${BASE_URL}/tags`,
+ canonical: `${SITE_URL}/tags`,
},
};