#7 sitemap 추가하기

런타임 시에 사이트맵 생성하기


Table Of Contents


들어가기


검색엔진이 사이트를 크롤링하려면 sitemap이 필요하다고 알고 있다.

Next.js로 블로그를 만들었을 때는 next-sitemap(링크)라는 라이브러리를 이용해서 사이트맵을 만들었는데, 다행히 Remix에도 sitemap을 만들어주는 라이브러리가 존재했다.

remix-sitemap


GitHub 링크

npm 링크

해당 라이브러리는 런타임 또는 빌드 타임에 사이트맵을 생성해준다! (옵션으로 설정가능)

또한 사이트맵이 길 경우, 분할하는 것도 지원해준다.

사용하는 데 조금 시간을 들이게 되었지만, 다른 사람들은 이 글을 보고 조금이라도 도움이 되었으면 좋겠다...!

Remix의 환경변수


Remix는 환경변수를 loader 함수에서만 불러올 수 있다.

클라이언트 함수에서 환경변수를 사용하려면 서버로부터 값을 받아와야 한다는 뜻이다. 이 과정에서 값이 노출되기 때문에, 중요한 데이터 처리는 서버에서 이루어져야 한다.

블로그에서는 Supabase의 url과 api key를 환경 변수에 두고 사용하고 있다. 서버에서 해당 key를 이용해서 포스트를 로드한 다음, 클라이언트로는 로드된 포스트들만 넘겨줌으로써 supabase key를 노출하지 않을 수 있다.

remix-sitemap에서 사이트맵을 생성하려면 app/routes/posts.$slug.tsx처럼 생성을 원하는 라우트에서

export const sitemap: SitemapFunction = ({ config, request }) => { const posts = await getPosts(); ... }

처럼 사용해야 한다.

그런데 getPosts 함수에서 supabase와 connection을 만들려면 supabase url, key가 필요하다. 이들을 어디에서 넘겨받아야 하는가...?

remix-sitemap의 사용법

이걸 알아보기 전에 공식 GitHub에서 사용법을 먼저 알아보자. 런타임 시 생성하는 방법과 빌드 시에 생성하는 방법, 2가지가 있다. 나는 포스트를 supabase 서버에서 받아오기 때문에, 빌드 시에 생성하면 sitemap과 현재 블로그 사이에 차이가 조금 있을 거라고 생각했다. 그래서 런타임 시에 sitemap을 사용하는 방법만 알아보았다.

설치를 하려면 쉘에

npm i remix-sitemap

처럼 입력해준다. 나는 npm을 이용했다.

그리고 entry.server.tsx 파일을 열고,

import { createSitemapGenerator } from 'remix-sitemap';

처럼 import를 해 준다.

다음으로,

const { isSitemapUrl, sitemap } = createSitemapGenerator({ siteUrl: 'https://example.com', generateRobotsTxt: true // configure other things here })

처럼 createSitemapGenerator 함수를 호출해 isSitemapUrl 변수와 sitemap 함수를 받아온다. 여기에 옵션들을 추가해 줄 수도 있다. siteUrl은 실제로 사이트를 배포하는 주소로 넣어주자.

그리고 handleRequest 함수 안에

if (isSitemapUrl(request)) return await sitemap(request, remixContext);

이렇게 써준다. 만약 요청받은 url이 sitemap에 해당하는 url이라면(아마 /sitemap.xml 경로일 것이다) sitemap을 만들어 반환하게 된다.

이제 추가로 sitemap을 생성하고자 하는 라우트에서 SitemapFunction을 호출해 sitemap을 생성하고, 이를 export 해 주면 된다.

공식 문서의 예시 코드를 보자면 이렇다.

// app/routes/posts.$slug.tsx import type { SitemapFunction } from 'remix-sitemap'; export const sitemap: SitemapFunction = ({ config, request }) => { const posts = await getPosts(); return posts.map(post => ({ loc: `/posts/${post.slug}`, lastmod: post.updatedAt, exclude: post.isDraft, // exclude this entry // acts only in this loc alternateRefs: [ { href: `${config.siteUrl}/en/posts/${post.slug}`, absolute: true, hreflang: 'en' }, { href: `${config.siteUrl}/es`, hreflang: 'es' } ] })); };

적용해보기

entry.server.tsx 파일에서 설정을 마치고, 나는 $subBlogId.tsx 파일에서 sitemap을 생성하면 될 것 같아서 해당 파일에

export const sitemap: SitemapFunction = async ({ config, request })) => { const posts = await getAllPosts(); return posts.map((post) => ({ loc: `/${post.sub_blog}/${post.id}`, lastmod: post.last_edited_at, })); };

처럼 써줬다. 아직 getAllPosts 함수의 구현은 되지 않은 상태이다.

이 함수를 구현하려면, 어떻게든 이 함수 안으로 supabase url과 key를 가져와야 한다!

이 함수로 넘어오는 인자는 우선 configrequest가 있으므로 이 둘이 어떤 내용을 가지고 있는지 먼저 알아보자. 아래처럼 콘솔에 출력을 해봤다.

config, request 출력 결과

오... request는 모르겠고, config를 보니 siteUrl, generateRobotsTxt는 아까 내가 entry.server.tsx에서 설정을 한 그대로가 아닌가?

그렇다면 entry.server.tsx에서 config에 임의의 옵션을 넘겨준다면 여기에서 확인할 수 있을까?

전달됨

임의로 변수를 만들어 값을 넣으니 정말 전달이 된다!

supabase key도 이렇게 넘기고, getAllPost 함수를 완성해서 아래와 같이 코드를 작성했다.

export const sitemap: SitemapFunction = async ({ config }) => { const supabase = createClient<Database>( config.SUPABASE_URL, config.SUPABASE_KEY ); const posts = await getAllPosts({ supabase }); return posts.map((post) => ({ loc: `/${post.sub_blog}/${post.id}`, lastmod: post.last_edited_at, })); };

이 다음, 프로젝트를 실행하고 /sitemap.xml 경로로 들어가면 아래와 같이 생성된 사이트맵을 볼 수 있다!!🥳🥳🥳

생성된 사이트맵

여담


사실 처음에는 이렇게 key를 넘길 수 있을지 몰라서, 정말 원시적인 방법으로 구현을 했었다.

수동으로 생성하기

바로 sitemap 파일을 생성하는 js 파일을 코딩하는 거였다. 🤣🤣🤣 대충 이렇게 생겼었고 혹시 몰라 기념으로 남겨둔다...!

import fs from "fs"; import dotenv from "dotenv"; dotenv.config(); import { createClient } from "@supabase/supabase-js"; const FILE_PATH = "public/sitemap/sitemap.xml"; const BASE_URL = "https://blog.psst54.me/"; async function main() { const urls = [BASE_URL]; const supabase = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_KEY ); const { data: supabaseData, error: supabaseError } = await supabase .from("posts") .select("id, title, parent_id, type, sub_blog") .order("created_at"); if (supabaseError) throw Error(); supabaseData.forEach((datum) => urls.push({ id: datum.id, sub_blog: datum.sub_blog }) ); const xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>'; let xmlSitemap = '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'; for (const url of urls) { xmlSitemap += ` <url> <loc>${BASE_URL}/${url.sub_blog}/${url.id}</loc> <lastmod>${new Date().toISOString()}</lastmod> <changefreq>weekly</changefreq> <priority>0.8</priority> </url> `; } xmlSitemap += "</urlset>"; const content = xmlHeader + xmlSitemap; fs.writeFile(FILE_PATH, content, (err) => { if (err) { console.error("Error writing to the file:", err); } else { console.log(`Content has been written to ${FILE_PATH}`); } }); } main();

Google Search Console


구글 서치 콘솔

구글 서치 콘솔에 사이트맵 주소까지 제출했다! 결과도 성공으로 나와서 이제 크롤링되기를 기다리면 될 것 같다!!