category : remix / 7 min read

Remix SEO 설정을 위한 sitemap, rss, robots

구글 Search Console네이버 서치어드바이저에 내 블로그를 등록하고, 내 블로그에 컨텐츠를 알려주기 위하여 sitemap, rss, robots를 등록 할 필요가 있었다. 해당 기능을 제공해주기 위해 가장 처음으로 고려한 것은 remix-seo, remix-sitemap과 같은 라이브러리의 도입이었다. remix-seo의 경우 3년 전에 업데이트를 마지막으로 더는 관리되고 있는 패키지였기 때문에 선뜻 선택하기 어려웠고, remix-sitemp의 경우 가이드를 확인해보니 굳이 라이브러리를 사용하지 않아도 무관하다고 판단하여 직접 컨텐츠를 제공하기로 결정했다.

기본 IDEA

Remix Routing 규칙 중 Escaping Special Characters를 확인해보면 대괄호([])로 감싼 문자열은 특수문자라 하더라도 URL 규칙 중 일부로 사용할 수 있다. 물론 public폴더에 xml파일과 txt파일을 직접 올려 서빙할 수도 있지만 그러면 컨텐츠를 발행할때마다 xml파일을 수정해야 하기에 이 과정을 자동으로 진행해주기 위해 코드 베이스로 컨텐츠가 업데이트되면 해당 파일이 수정될 수 있도록 tsx 파일로 만들 계획이다.

💡

생성 할 tsx파일의 위치는 각각 다음과 같다.

  • app/routes/sitemap[.]xml.tsx
  • app/routes/rss[.]xml.tsx
  • app/routes/robots[.]txt.tsx

Sitemap

export const toXmlSitemap = (urls: string[]) => {
  const urlsAsXml = urls
    .map((url) => `<url><loc>${siteUrl}/${url}</loc></url>`)
    .join("\n");

  return `<?xml version="1.0" encoding="UTF-8"?>
        <urlset
          xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"
        >
          ${urlsAsXml}
        </urlset>
      `.trim();
};

사이트맵을 생성하는 로직의 아이디어는 위와 같다. sitemap의 기본적인 프로토콜은 이 곳에서 가져욌으며, urlset 표준은 여기서 보충하였다. 현재 내가 관리하고 있는 모든 페이지의 url 정보를 담아 이를 sitemap을 위한 xml 태그로 변환시켜주는 로직이다. 이후 Resource Route 규칙에 따라 파일을 구성하였다.

import { LoaderFunction } from "@remix-run/node";

import blogConfig from "@/blog.config.json";
import { getAllArticles } from "@/api/getArticle";

export const loader: LoaderFunction = async () => {
  try {
    const articles = await getAllArticles("preview");
    const sitemap = toXmlSitemap([
      "articles",
      ...articles.map((path) => `article/${path}`),
    ]);
    return new Response(sitemap, {
      status: 200,
      headers: {
        "Content-Type": "application/xml",
        "X-Content-Type-Options": "nosniff",
        "Cache-Control": "public, max-age=3600",
      },
    });
  } catch (e) {
    throw new Response("Internal Server Error", { status: 500 });
  }
};

export const toXmlSitemap = (urls: string[]) => {
  const urlsAsXml = urls
    .map((url) => `<url><loc>${blogConfig.siteUrl}/${url}</loc></url>`)
    .join("\n");

  return `<?xml version="1.0" encoding="UTF-8"?>
        <urlset
          xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"
        >
          ${urlsAsXml}
        </urlset>
      `.trim();
};

RSS

rss를 서빙하는 기본적인 Idea도 Sitemap과 일맥상통한다. 나는 md파일을 json형태의 포맷으로 변경하여 블로그 컨텐츠를 서빙하고 있고, md파일 내부적으로 메타정보를 따로 작성하기에 큰 어려움이 없었다. rss 포맷을 완성하는 것은 이 블로그 아티클을 참조하여 나의 상황에 맞게 조금 변형하였다.

// @see https://camchenry.com/blog/how-to-add-a-rss-feed-to-a-remix-app
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
dayjs.extend(utc);

import blogConfig from "@/blog.config.json";
import { getAllArticles } from "@/api/getArticle";

import type { LoaderFunction } from "@remix-run/node";

export const loader: LoaderFunction = async () => {
  const articles = await getAllArticles();

  const rss = generateRss({
    title: blogConfig.title,
    description: blogConfig.description,
    link: blogConfig.siteUrl,
    entries: articles.map(({ metadata }) => ({
      title: metadata.title,
      link: `${blogConfig.siteUrl}/article/${metadata.category}/${metadata.path}`,
      pubDate: dayjs(metadata.created_at).utc().format(),
      description: metadata.description,
      author: blogConfig.author,
      guid: `${blogConfig.siteUrl}/article/${metadata.category}/${metadata.path}`,
    })),
  });

  return new Response(rss, {
    headers: {
      "Content-Type": "application/xml",
      "Cache-Control": "public, max-age=2419200",
    },
  });
};

type RssEntry = {
  title: string;
  link: string;
  pubDate: string;
  description: string;
  author: string;
  guid: string;
};

export function generateRss({
  description,
  entries,
  link,
  title,
}: {
  title: string;
  description: string;
  link: string;
  entries: RssEntry[];
}): string {
  return `<?xml version="1.0" encoding="UTF-8"?>
  <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
      <title>${title}</title>
      <description>${description}</description>
      <link>${link}</link>
      <language>en-us</language>
      <ttl>60</ttl>
      <atom:link href="https://YOUR_SITE_HERE.com/rss.xml" rel="self" type="application/rss+xml" />
      ${entries
        .map(
          (entry) => `
        <item>
          <title><![CDATA[${entry.title}]]></title>
          <description><![CDATA[${entry.description}]]></description>
          <pubDate>${entry.pubDate}</pubDate>
          <link>${entry.link}</link>
          ${entry.guid ? `<guid isPermaLink="false">${entry.guid}</guid>` : ""}
        </item>`
        )
        .join("")}
    </channel>
  </rss>`;
}

Robots

robots는 위와 비교하면 더욱 단순하다. 위 과정을 통해 sitemap을 만들었기 때문에 그저 remix의 Resource Routing 규칙만 지켜주면 된다.

import blogConfig from "@/blog.config.json";

export const loader = () => {
  const robotText = ` 
        User-agent: *
        Allow: /
    
        Sitemap: ${blogConfig.siteUrl}/sitemap.xml
        `;

  return new Response(robotText, {
    status: 200,
    headers: {
      "Content-Type": "text/plain",
    },
  });
};

이제 우리의 블로그에서 sitemap, rss, robots를 서빙할 수 있게 되었다.

목록으로
remix 카테고리의 최신글