loading...

RSS/Atom and Site Map for Svelte/Sapper Blog - Part 3

cleverguy25 profile image Cleve Littlefield Originally published at cleverdev.codes ・3 min read

Yet another bootstrapping my blog series (3 Part Series)

1) Svelte, Sapper, and Squidex Headless CMS, Part 1 2) Exporting Squidex Assets for Svelte/Sapper blog - Part 2 3) RSS/Atom and Site Map for Svelte/Sapper Blog - Part 3

I feel like a boomer when I tell people I still use an RSS reader. I still find it is the best way to keep up with interesting people writing about technology. I often share my blog roll with other programmers when they ask and point them to my reader of choice, RSS Owl. So yes, I expect my blog to support RSS even in this day and age.

When I started with a template Svelte/Sapper Blog project, I was slightly surprised that it does not have a way to generate RSS/Atom out of the box. It took a bit of searching to see how others were implementing RSS and found this on David Lacourt's blog, which references a solution from the Svelte man Rich Harris himself..

Pretty straight forward, with a regular server-side route, just returning XML over JSON. I have to admit coming other technologies that I assumed this needed to be more complicated with a model and XML translation middleware. I worked with "frameworks" so much that a straightforward solution eluded me for the moment. I also blame working predominantly with Kubernetes and infrastructure for the last few years.

Since I static gen my site with sapper export and that works by traversing all links, I have a link to my feeds from one my pages, I used the privacy policy page.

While RSS would do the trick on its own, let's also do ATOM to be thorough. The real secret here is ATOM expects the date to be in RFC3339 format pulled from here.


import { getPosts } from './_posts.js';
import { siteUrl } from '../../stores/_config.js';

function toRFC3339(date) {

  function pad(n) {
      return n < 10 ? "0" + n : n;
  }

  function timezoneOffset(offset) {
      var sign;
      if (offset === 0) {
          return "Z";
      }
      sign = (offset > 0) ? "-" : "+";
      offset = Math.abs(offset);
      return sign + pad(Math.floor(offset / 60)) + ":" + pad(offset % 60);
  }

  return date.getFullYear() + "-" +
      pad(date.getMonth() + 1) + "-" +
      pad(date.getDate()) + "T" +
      pad(date.getHours()) + ":" +
      pad(date.getMinutes()) + ":" +
      pad(date.getSeconds()) + 
      timezoneOffset(date.getTimezoneOffset());
}

function renderXmlAtomFeed(posts) {
  return `<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

  <title>Clever Dev Codes</title>
  <link href="${siteUrl}"/>
  <link rel="self" href="${siteUrl}/blog/atom" />
  <updated>${toRFC3339(new Date())}</updated>
  <author>
    <name>Cleve Littlefield</name>
  </author>
  <id>urn:uuid:DBEF5693-FACC-47DB-9063-3AFF0FD30733</id>
  ${posts.map(post => `
  <entry>
    <title>${post.title}</title>
    <link href="${siteUrl}/blog/${post.slug}"/>
    <id>urn:uuid:${post.id}</id>
    <updated>${toRFC3339(new Date(post.publishedDate))}</updated>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
        ${post.html}
      </div>
    </content>
    <summary type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
        ${post.excerpt}
      </div>
    </summary>
  </entry>`).join('\n')}

</feed>`;
}

export async function get(req, res) {

  res.writeHead(200, {
    'Cache-Control': `max-age=0, s-max-age=${600}`, // 10 minutes
    'Content-Type': 'application/atom+xml'
  });

  const posts = await getPosts();
  const feed = renderXmlAtomFeed(posts);
  res.end(feed);

}

How about a site map? Similar concept:


import { getPosts } from "./blog/_posts.js";
import { siteUrl } from "../stores/_config.js";

function renderSitemapXml(slugs) {
  return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
${slugs
  .map(
    slug => `
  <url>
    <loc>${siteUrl}/${slug}</loc>
    <changefreq>daily</changefreq>
    <priority>0.7</priority>
  </url>
`
  )
  .join("\n")}
</urlset>`;
}

export async function get(req, res) {
  res.writeHead(200, {
    "Cache-Control": `public, max-age=0, must-revalidate`,
    "Content-Type": "application/xml"
  });

  const posts = await getPosts();
  const slugs = posts.map(post => `blog/${post.slug}`);
  const feed = renderSitemapXml([...slugs, "", "/blog"]);
  res.end(feed);
}

Yet another bootstrapping my blog series (3 Part Series)

1) Svelte, Sapper, and Squidex Headless CMS, Part 1 2) Exporting Squidex Assets for Svelte/Sapper blog - Part 2 3) RSS/Atom and Site Map for Svelte/Sapper Blog - Part 3

Discussion

markdown guide