13

I'm trying to render an XML file when pointing to www.example.com/sitemap.xml. Since the project was developed using Next.js, the routing works with the js files stored in the pages directory:

  • example.com/help -> help.js
  • example.com/info -> info.js

So, is there any way to achieve this by avoiding accessing the backend?

CoronelV
  • 357
  • 1
  • 2
  • 14

3 Answers3

41

JS Version

/pages/sitemap.xml.jsx

import React from 'react'

class Sitemap extends React.Component {
  static async getInitialProps({ res }) {
    res.setHeader('Content-Type', 'text/xml')
    res.write(`<?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
      ...
    </urlset>`)
    res.end()
  }
}

export default Sitemap

TS Version

/pages/sitemap.xml.tsx

import { GetServerSideProps } from 'next'
import React from 'react'

const Sitemap: React.FC = () => null

export const getServerSideProps: GetServerSideProps = async ({ res }) => {
  if (res) {
    res.setHeader('Content-Type', 'text/xml')
    res.write(`<?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">

    </urlset>`)
    res.end()
  }
  return {
    props: {},
  }
}

export default Sitemap

API Version

/pages/api/sitemap.xml.tsx

import type { NextApiRequest, NextApiResponse } from 'next'
import { getAllContentPagesQuery, getAllShopProductsQuery } from './utils/requests'

export default async (req: NextApiRequest, res: NextApiResponse<string>) => {
  const pages = await getAllContentPagesQuery()
  const products = await getAllShopProductsQuery()

  const frontUrl = "https://localhost:3000"
  const pagesAndSlugs = [...pages, ...products].map(url => ({
    url: `${frontUrl}${'Variations' in url ? '/product/' : '/'}${url.slug}`, // -> /page1, /product/myproduct...
    updatedAt: url.updatedAt,
  }))

  const urls = pagesAndSlugs.map(
    ({ url, updatedAt }) => `
    <url>
      <loc>${url}</loc>
      <lastmod>${new Date(updatedAt).toISOString()}</lastmod>
    </url>
    `
  )
    .join('')

  res
    .setHeader('Content-Type', 'text/xml')
    .setHeader(
      'Cache-Control',
      'public, s-maxage=10, stale-while-revalidate=59'
    )
    .status(200)
    .send(`<?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
      ${urls}
    </urlset>`)
}

:D

Emanuel
  • 2,603
  • 22
  • 24
  • 5
    Can't believe I didn't even think about just sticking a file called `sitemap.xml.tsx` in the `pages` directory... ‍♂️ Good job! – Svish Feb 25 '21 at 21:45
  • Thanks man, I've added the file `sitemap.xml.tsx` but was searching a way to return *xml* instead of *html* –  Jun 17 '21 at 13:16
  • Hi, I got `TypeError: res.write is not a function` when I've tried to do it with `getInitialProps`. Does anyone have an idea how to solve it? – Elisei Nicolae Jun 29 '22 at 09:09
  • @NicolaeElisei `res` is only available in `getInitialProps` and `getServerSideProps` for technical reasons. – Camilo Jul 05 '22 at 23:21
  • A better solution would be to use API routes (https://nextjs.org/docs/api-routes/introduction) – Jonathan Dsouza Aug 13 '22 at 09:52
  • Is there a way to do it using `next export`, with `getStaticProps`? – gvo Oct 04 '22 at 17:37
  • Can anybody confirm me if `/pages/sitemap.xml.tsx` version still works for Next version `12.x.x` Can't seem to get it work. Always getting 404. – Sisir Dec 28 '22 at 13:33
5

Add a static file called sitemap.xml under public directory

public/sitemap.xml

after build you can access www.yourdomain.com/sitemap.xml

Read more on static files: https://nextjs.org/docs/basic-features/static-file-serving

xIsra
  • 867
  • 10
  • 21
3

You can use nextjs api routes.

path - pages/api/sitemap.ts

content -

import type { NextApiRequest, NextApiResponse } from 'next';
import { SitemapStream, streamToPromise } from 'sitemap';

async function sitemap(req: NextApiRequest, res: NextApiResponse<string>) {
  try {
    const smStream = new SitemapStream({
      hostname: `https://${req.headers.host}`,
    });

    // List of posts
    const posts: string[] = ['hello'];

    // Create each URL row
    posts.forEach(() => {
      smStream.write({
        url: `/post/hello`,
        changefreq: 'daily',
        priority: 0.9,
      });
    });

    // End sitemap stream
    smStream.end();

    // XML sitemap string
    const sitemapOutput = (await streamToPromise(smStream)).toString();

    res.writeHead(200, {
      'Content-Type': 'application/xml',
    });

    // Display output to user
    res.end(sitemapOutput);
  } catch (e) {
    console.log(e);
    res.send(JSON.stringify(e));
  }
}

export default sitemap;

In robots.txt you can specify

Sitemap: https://[SITE_DOMAIN_HERE]/api/sitemap

weivall
  • 917
  • 13
  • 16
  • 2
    In addition to the API route, one could setup a [`rewrites`](https://nextjs.org/docs/api-reference/next.config.js/rewrites) rule to map `/sitemap.xml` to `/api/sitemap` thus making `www.example.com/sitemap.xml` still point to the sitemap file. – juliomalves Jul 05 '22 at 22:58
  • Thanks @juliomalves for pointing this. In my project, I also did redirect with service mesh (istio), but it can be done in many ways. – weivall Jul 06 '22 at 07:18