読者です 読者をやめる 読者になる 読者になる

GoogleAppsScriptで作成したRSS2.0フィードをW3CのFeedValidationServiceに対応してみる

GoogleAppsScript

前回はGoogleAppsScriptでRSS2.0フィードを作成・出力してみました。


そのフィードが問題ないかをチェックするW3CのFeed Validation Serviceを通してみたところ、いくつかRecommendationsが出てきたので対応してみることにしました。
W3C Feed Validation Service, for Atom and RSS


当初のRecommendations

item要素の不足
line 13, column 4: item should contain a guid element (2 occurrences)
    </item>
    ^


guid要素が不足していたようなので、設定するようにしました。



channel要素の不足
line 20, column 2: Missing atom:link with rel="self"
  </channel>
  ^


タグが不足しているようだったので、XmlService.createElement('atom:link') としてみたところ、GASにて以下のようなエラーが発生しました。

サーバーエラーが発生しました。しばらくしてからもう一度試してください。


XmlService.createElement('atom:link') では問題なく動作したことから、コロンをうまく扱ってくれていないようでした。

他の方法として

を試してみてもうまくいかなかったため、仕方なく最後に文字列化したところでreplaceをするようにしました。



次のRecommendations

上記の対応後に再度チェックしてみたところ、Recommendationsが変わっていました。

line 4, column 4: XML parsing error: <unknown>:4:4: unbound prefix
    <atom:link href="http://example.com" rel="self" type="application/rss+xm ...
    ^


xmlのネームスペースの定義をしていなかったようなので、追加しました。
参考:PubSubHubBubのpubに対応する : ryo.com



最後のRecommendations

再度チェックしてみたところ、またRecommendationsが変わっていました。

line 4, column 81: Self reference doesn't match document location
... rel="self" type="application/rss+xml" />


以下のstackoverflowの対応もしてみましたが、Recommendationsは変わりませんでした。
参考:rss2 - RSS atom:link - Self reference doesn't match document location - Stack Overflow


Feed Validation Serviceのヘルプも見ましたが、「This may not be a problem」との記載もあったため、今回はこれ以上追求するのをやめておくことにしました。
参考:Self reference doesn't match document location



その他

前回のコードでは、addTitle()するとすぐにRSSのタグを生成していましたが、後からタグの内容を修正するのが面倒そうだったので、最後で生成するように変更しました。


また、addItem()の引数が多かったのですが、引数の意味や順番が分かりにくかったため、名前付き引数へと変更しました。



ソース

Gistもアップデートしておきました。
GoogleAppsScriptでRSS2.0フィードを出力する例 (使う時は、.jsを.gsにする)


example.gs

function doGet() {
  var rss = makeRss();
  
  rss.setTitle('RSS 2.0 test');
  rss.setLink('http://example.com');
  rss.setDescription('RSS 2.0のテスト');
  rss.setLanguage('ja');
  rss.setAtomlink('http://example.com/rss');
  
  for (var i = 1; i < 3; i++){
    rss.addItem({title: 'ページ:' + i,
                 link: 'http://example.com/' + i,
                 description: i + 'つ目のページ',
                 pubDate: new Date()
                })
  }
  
  return ContentService.createTextOutput(rss.toString())
         .setMimeType(ContentService.MimeType.RSS);
}


rss20feed.gs

var makeRss = function(){
  var channel = XmlService.createElement('channel');
  var root = XmlService.createElement('rss')
                       .setAttribute('version', '2.0')
                       .setAttribute('xmlnsatom', "http://www.w3.org/2005/Atom")
                       .addContent(channel);
  
  var title = '';
  var link = '';
  var description = '';
  var language = '';
  var atomlink = '';
  var items = {};
  
  var createElement = function(element, text){
    return XmlService.createElement(element).setText(text);
  };

  
  return {
    setTitle: function(value){ title = value; },
    setLink: function(value){ link = value; },
    setDescription: function(value){ description = value; },
    setLanguage: function(value){ language = value; },
    setAtomlink: function(value){ atomlink = value; },
    
    addItem: function(args){
      if (typeof args.title === 'undefined') { args.title = ''; }
      if (typeof args.link === 'undefined') { args.link = ''; }
      if (typeof args.description === 'undefined') { args.description = ''; }
      if (!(args.pubDate instanceof Date)) { throw 'pubDateは日付型'; }
      if (typeof args.timezone === 'undefined') { args.timezone = "JST"; }
      if (typeof args.guid === 'undefined' && typeof args.link === 'undefined') { throw 'GUIDが設定できません'; }
      
      
      var item = {
        title: args.title,
        link: args.link,
        description: args.description,
        pubDate: Utilities.formatDate(args.pubDate, args.timezone, "EEE, dd MMM yyyy HH:mm:ss Z"),
        guid: args.guid === 'undefined' ? args.link : args.link
      }
      
      items[item.guid] = item;
    },
    
    toString: function(){
      // createElementではコロンの指定ができないため、あとでreplaceする。
      // 誤って他のデータをreplaceしないよう、先頭に変換対象のatomlinkを持ってきておく
      channel.addContent(XmlService.createElement("atomlink")
                         .setAttribute('href', atomlink)
                         .setAttribute('rel', 'self')
                         .setAttribute('type', 'application/rss+xml')
                        );

      channel.addContent(createElement('title', title));
      channel.addContent(createElement('link', link));
      channel.addContent(createElement('description', description));
      channel.addContent(createElement('language', language));
      

      for (var i in items)
      {
        channel.addContent(
          XmlService
          .createElement('item')
          .addContent(createElement('title', items[i].title))
          .addContent(createElement('link', items[i].link))
          .addContent(createElement('description', items[i].description))
          .addContent(createElement('pubDate', items[i].pubDate))
          .addContent(createElement('guid', items[i].guid))
        );
      }
      
      var document = XmlService.createDocument(root);
      var xml = XmlService.getPrettyFormat().format(document)
      
      
      // コロンが必要なタグの変換作業は、xml化できたところで行う
      var result = xml.replace('xmlnsatom', 'xmlns:atom')
                      .replace('<atomlink href=','<atom:link href=');
      
      return result;
    }
  };
};