diff --git a/src/app/book/sitemap.ts b/src/app/book/sitemap.ts new file mode 100644 index 00000000..04c60d8e --- /dev/null +++ b/src/app/book/sitemap.ts @@ -0,0 +1,47 @@ +import type { MetadataRoute } from 'next'; +import type { APIRecommendedBookshelf } from '@/types/bookshelf'; +import type { APIBook } from '@/types/book'; + +const options = { + headers: { + 'Content-Type': 'application/json', + }, + next: { revalidate: 60 * 60 * 24 }, +}; + +export async function booksSitemap() { + try { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/suggestions/bookshelves/default`, + options + ); + + if (!res.ok) { + return Promise.reject(); + } + + const data: APIRecommendedBookshelf = await res.json(); + + const books = new Set(); + + data.bookshelfResponses.forEach(bookshelf => + bookshelf.books.forEach(book => books.add(book.bookId)) + ); + + const filteredBooks = Array.from(books); + + return filteredBooks; + } catch { + return []; + } +} + +export default async function sitemap(): Promise { + const booksId = await booksSitemap(); + const sitemap = ['search', ...booksId]; + + return sitemap.map(value => ({ + url: `${process.env.NEXT_PUBLIC_HOST}/book/${value}`, + lastModified: new Date(), + })); +} diff --git a/src/app/bookshelf/sitemap.ts b/src/app/bookshelf/sitemap.ts new file mode 100644 index 00000000..35d7daf7 --- /dev/null +++ b/src/app/bookshelf/sitemap.ts @@ -0,0 +1,40 @@ +import type { MetadataRoute } from 'next'; +import type { APIRecommendedBookshelf } from '@/types/bookshelf'; + +const options = { + headers: { + 'Content-Type': 'application/json', + }, + next: { revalidate: 60 * 60 * 24 }, +}; + +export async function bookshelvesSitemap() { + try { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/suggestions/bookshelves/default`, + options + ); + + if (!res.ok) { + return Promise.reject(); + } + + const data: APIRecommendedBookshelf = await res.json(); + const bookshelves = data.bookshelfResponses.map(({ bookshelfId }) => ({ + bookshelfId, + })); + + return bookshelves; + } catch { + return []; + } +} + +export default async function sitemap(): Promise { + const bookshelves = await bookshelvesSitemap(); + + return bookshelves.map(({ bookshelfId }) => ({ + url: `${process.env.NEXT_PUBLIC_HOST}/bookshelf/${bookshelfId}`, + lastModified: new Date(), + })); +} diff --git a/src/app/group/sitemap.ts b/src/app/group/sitemap.ts new file mode 100644 index 00000000..e963145b --- /dev/null +++ b/src/app/group/sitemap.ts @@ -0,0 +1,38 @@ +import type { MetadataRoute } from 'next'; +import type { APIGroupPagination } from '@/types/group'; + +const options = { + headers: { + 'Content-Type': 'application/json', + }, + next: { revalidate: 60 * 60 * 24 }, +}; + +export const bookGroupSitemap = async () => { + try { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/book-groups?pageSize=100`, + options + ); + + if (!res.ok) { + return Promise.reject(); + } + + const data: APIGroupPagination = await res.json(); + const bookGroups = data.bookGroups.map(group => group.bookGroupId); + + return bookGroups; + } catch { + return []; + } +}; + +export default async function sitemap(): Promise { + const bookGroups = await bookGroupSitemap(); + + return bookGroups.map(bookGroupId => ({ + url: `${process.env.NEXT_HOST}/group/${bookGroupId}`, + lastModified: new Date(), + })); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 8e19e35d..5c597d81 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,3 +1,5 @@ +import { Metadata } from 'next'; + import ContextProvider from '@/components/ContextProvider'; import AuthFailedErrorBoundary from '@/components/AuthFailedErrorBoundary'; import Layout from '@/v1/layout/Layout'; @@ -5,15 +7,41 @@ import Layout from '@/v1/layout/Layout'; import { LineSeedKR } from '@/styles/font'; import '@/styles/global.css'; +export const metadata: Metadata = { + metadataBase: new URL(`${process.env.NEXT_HOST}`), + title: { + template: '%s | 다독다독', + default: '다독다독', + }, + description: '책에 대한 인사이트를 공유하고 소통하는 독서 소셜 플랫폼', + keywords: [ + '다독다독', + 'dadok', + 'dadokdadok', + '책장', + '책추천', + '도서검색', + '독서모임', + '책', + '독서', + ], + verification: { + google: '72kN3MWyQHuvSb8V67dVkfPUPMrw102Tm6BsvTvfKmg', + other: { + 'naver-site-verification': '9046af5eda448309a92e2e923a45cb874df986a0', + }, + }, +}; + const RootLayout = ({ children }: { children: React.ReactNode }) => { return ( - 다독다독 + {/* @todo Chakra 제거시 app-layout 프로퍼티 제거. */} diff --git a/src/app/opengraph-image.jpg b/src/app/opengraph-image.jpg new file mode 100644 index 00000000..60da537d Binary files /dev/null and b/src/app/opengraph-image.jpg differ diff --git a/src/app/robots.ts b/src/app/robots.ts new file mode 100644 index 00000000..90f25b69 --- /dev/null +++ b/src/app/robots.ts @@ -0,0 +1,12 @@ +import type { MetadataRoute } from 'next'; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: '*', + allow: '/', + }, + sitemap: `${process.env.NEXT_HOST}/sitemap.xml`, + host: `${process.env.NEXT_HOST}`, + }; +} diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts new file mode 100644 index 00000000..3c01196e --- /dev/null +++ b/src/app/sitemap.ts @@ -0,0 +1,25 @@ +import type { MetadataRoute } from 'next'; + +import { default as bookSitemap } from './book/sitemap'; +import { default as bookshelfSitemap } from './bookshelf/sitemap'; +import { default as bookGroupSitemap } from './group/sitemap'; + +export default async function sitemap(): Promise { + return [ + { + url: `${process.env.NEXT_HOST}/bookarchive`, + lastModified: new Date(), + }, + { + url: `${process.env.NEXT_HOST}/group`, + lastModified: new Date(), + }, + { + url: `${process.env.NEXT_HOST}/profile/me`, + lastModified: new Date(), + }, + ...(await bookSitemap()), + ...(await bookshelfSitemap()), + ...(await bookGroupSitemap()), + ]; +}