Goでsitemap.xmlを生成する

こんにちは、spotチームの名嘉眞です。spotチームはキャンプ場検索サービス(hinata spot)を開発しております。私はspotチームのバックエンド担当として日々Goを書いてます。

hinata spot

今回はsitemap.xmlをGoで生成する方法についてまとめてみました。標準パッケージで割と簡単に出来るかなと思います。

始めに

hinata spotでは、sitemap.xmlの生成もGoで書いています。ちなみにsitemap.xmlとは、ウェブサイト内の各ページのURLや優先度、最終更新日、更新頻度などを記述したXML形式のファイルのことです。

railsだとsitemap_generatorというgemを使って生成したりするかもしれません。Goにもそのようなライブラリがあるかもしれないですが、標準packageのencoding/xmlで十分実装できると考えました。

処理の流れ

私の担当するサービスの場合、sitemap.xmlに記載されるurlを構成する要素はDBから取得する必要があります。そのため下記のような処理の流れで生成します。

  • DBから対象のデータ取得
  • URLの組み立て
  • sitemap.xmlの生成
  • sitemap.xmlgzip
  • GCSへアップロード

この記事では主に、sitemap.xmlの生成部分について紹介していきます。

実装部分

sitemap.xmlは以下のようなファイルです。sitemap固有のタグなど生成ルールが決まっています。

<?xml version="1.0" encoding="UTF-8"?>
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
      <url>
          <loc>https://hinata-spot.me/spots/taito-beach</loc>
          <changefreq>2020-10-20T14:56:46+09:00</changefreq>
      </url>
      <url>
          <loc>https://hinata-spot.me/spots/tanpopomura</loc>
          <changefreq>2020-10-20T14:56:46+09:00</changefreq>
      </url>
  </urlset>

まず、sitemapxmlをGoのstructで表現するために、structを以下のように定義しています。 encoding/xmlの場合、structの各フィールドにxmlタグと付与したいメタ情報を適用すると、適用したメタ情報をもとにxml生成時にタグで値を囲います。 attr と記載すると上位のタグの中にattrで指定した値を埋め込みます。

実際に生成したいsitemap.xml(上の例)と以下のstructでマッチする部分としては、 XMLName、Version、Xhtmlフィールドが実際に生成したいsitemap.xmlのurlsetタグの部分になります。 SiteListフィールドがsitemap.xmlに設定したいサイトのページのURLになります。

package models

import "encoding/xml"

type SiteMapXML struct {
    XMLName  xml.Name `xml:"urlset"`
    Version  string   `xml:"xmlns,attr"`
    Xhtml    string   `xml:"xmlns:xhtml,attr"`
    SiteList []*Site  `xml:"url"`
}

type Site struct {
    URL       string `xml:"loc"`
    UpdatedAt string `xml:"changefreq"`
}

サイトマップ生成の関数が実行されると、DBから対象のデータを取得し上記のSite structを生成するようにします。 以下のような関数を使ってsitemapに設定したいURLを生成しています。 最終的に、[]*Site型 を生成します。

func CreateSite(path, updatedAt string) *models.Site {
    u := url.URL{Scheme: "https", Host: spotHost, Path: path}
    site := &models.Site{
        URL:       u.String(),
        UpdatedAt: updatedAt,
    }

    return site
}

// dbResultはDBから取得したデータとする
siteList := make([]*models.Site, len(dbResult))
var index int

for i := range dbResult {
  path := "spots/" +  dbResult[i]
  site := CreateSite(path, dbResult[i].UpdatedAt)

  siteList[index] = site
  index++
}

sitemap.xml生成するパッケージは以下のように定義しています。このパッケージに定義したCreateXMLgzip 関数の引数に、上の例で生成した[]*models.Siteを渡すことで、sitemap.xmlの生成とgzip化を行います。 sitemap.xml固有の値は定数にて定義しています。

package sitemap

import (
  // 省略
)

const (
    version         = "http://www.sitemaps.org/schemas/sitemap/0.9"
    xhtml           = "http://www.w3.org/1999/xhtml"
    spotHost        = "hinata-spot.me"
)

// CreateXMLgzip creates site map xml from struct.
func CreateXMLgzip(siteList []*models.Site) (io.ReadWriter, error) {
    ss := &models.SiteMapXML{
        Version:  version,
        Xhtml:    xhtml,
        SiteList: siteList,
    }

    data, err := xml.MarshalIndent(ss, "  ", "    ")
    if err != nil {
        return nil, err
    }

    // xml.Header は、encoding/xmlパッケージで以下のようにconstで宣言されています。
    // <?xml version="1.0" encoding="UTF-8"?>
    bss := [][]byte{[]byte(xml.Header), data}
    bs := (bytes.Join(bss, []byte("")))

    var result bytes.Buffer
    zw := gzip.NewWriter(&result)

    _, err = zw.Write(bs)
    if err != nil {
        return nil, err
    }

    if err := zw.Close(); err != nil {
        return nil, err
    }

    return &result, nil
}

CreateXMLgzip 関数は、戻り値をインターフェース io.ReadWriter型にしています。理由はGCSにアップロードするオブジェクトをインターフェースのio.Reader型で受け取るようにしているからです。 io.Reader で受け取れるようにすることで、ストレージへのアップロードなど共通で使うような関数を使いやすくしています。

結構簡単に実装できたと感じるのではないでしょうか。サービスの内容によるかもしれないですが、SEOを考えるとXMLサイトマップを生成し管理することがあると思いますので、その際に役に立つことができたら嬉しいです。