初歩からの無職

GatsbyのGraphQLでformatStringは使うべきではない

  • GatsbyJS
  • Contentful
  • JavaScript

以下はGatsbyのversion2.30.1の話。ちなみにタイトル詐欺で結局自分は使う方向にしている。

経緯

  • このブログはContentfulのエントリで例えば2021-01-01T06:00+09:00のような形でpublishDateを保存している
  • GraphQL側で publishDate(formatString: "YYYY年MM月DD日") のようにフォーマットしてフロントに渡している
  • するとフロントでは2020年12月31日と表示される(内部的には2020-12-31T21:00+00:00になっており、アーカイブでは2021年1月に正しく分類される)

わかったこと

  • GatsbyのGraphQLでformatStirngはmoment.jsの .format() を使っているという説明だがその前に .utc() でutcモードにされている
  • 強制的にutcモードにされるため、utc offset情報が含まれるものはoffset分のずれが発生する
  • .utcOffset() を使えばこれは回避されるが、そういうのはフロントでやればいいじゃんという姿勢っぽい

考えられる対策

  • GraphQLのformatStringは使わない(上記の回答を見るとイギリスなどの一部の国以外の人にとって意図しない表示が増える弊害のほうが大きいと思われるので、そもそも廃止したほうがGatsbyの利用体験は改善されるだろう)
  • 日付にutc offset情報を含めずに、すべて現地時間として扱う(個人ブログで基本的にすべて日本時間という前提があるため、今回はこちらを利用した)

参考

以下、やったこと等の駄文

振る舞いの理解

内部的にはmoment.jsが使われているようなので、該当部分のソースコードをみて挙動を確認してみた。

const moment = require('moment');

const time = "2021-01-01T06:00+09:00"; // contentful側に保存されている形式

// gatsby/packages/gatsby/src/schema/types/date.ts 
const ISO_8601_FORMAT = [
  `YYYY`,
  `YYYY-MM`,
  `YYYY-MM-DD`,
  `YYYYMMDD`,

  // Local Time
  `YYYY-MM-DDTHH`,
  `YYYY-MM-DDTHH:mm`,
  `YYYY-MM-DDTHHmm`,
  `YYYY-MM-DDTHH:mm:ss`,
  `YYYY-MM-DDTHHmmss`,
  `YYYY-MM-DDTHH:mm:ss.SSS`,
  `YYYY-MM-DDTHHmmss.SSS`,
  `YYYY-MM-DDTHH:mm:ss.SSSSSS`,
  `YYYY-MM-DDTHHmmss.SSSSSS`,
  // `YYYY-MM-DDTHH:mm:ss.SSSSSSSSS`,
  // `YYYY-MM-DDTHHmmss.SSSSSSSSS`,

  // Local Time (Omit T)
  `YYYY-MM-DD HH`,
  `YYYY-MM-DD HH:mm`,
  `YYYY-MM-DD HHmm`,
  `YYYY-MM-DD HH:mm:ss`,
  `YYYY-MM-DD HHmmss`,
  `YYYY-MM-DD HH:mm:ss.SSS`,
  `YYYY-MM-DD HHmmss.SSS`,
  `YYYY-MM-DD HH:mm:ss.SSSSSS`,
  `YYYY-MM-DD HHmmss.SSSSSS`,
  // `YYYY-MM-DD HH:mm:ss.SSSSSSSSS`,
  // `YYYY-MM-DD HHmmss.SSSSSSSSS`,

  // Coordinated Universal Time (UTC)
  `YYYY-MM-DDTHHZ`,
  `YYYY-MM-DDTHH:mmZ`,
  `YYYY-MM-DDTHHmmZ`,
  `YYYY-MM-DDTHH:mm:ssZ`,
  `YYYY-MM-DDTHHmmssZ`,
  `YYYY-MM-DDTHH:mm:ss.SSSZ`,
  `YYYY-MM-DDTHHmmss.SSSZ`,
  `YYYY-MM-DDTHH:mm:ss.SSSSSSZ`,
  `YYYY-MM-DDTHHmmss.SSSSSSZ`,
  // `YYYY-MM-DDTHH:mm:ss.SSSSSSSSSZ`,
  // `YYYY-MM-DDTHHmmss.SSSSSSSSSZ`,

  // Coordinated Universal Time (UTC) (Omit T)
  `YYYY-MM-DD HHZ`,
  `YYYY-MM-DD HH:mmZ`,
  `YYYY-MM-DD HHmmZ`,
  `YYYY-MM-DD HH:mm:ssZ`,
  `YYYY-MM-DD HHmmssZ`,
  `YYYY-MM-DD HH:mm:ss.SSSZ`,
  `YYYY-MM-DD HHmmss.SSSZ`,
  `YYYY-MM-DD HH:mm:ss.SSSSSSZ`,
  `YYYY-MM-DD HHmmss.SSSSSSZ`,
  // `YYYY-MM-DD HH:mm:ss.SSSSSSSSSZ`,
  // `YYYY-MM-DD HHmmss.SSSSSSSSSZ`,

  // Coordinated Universal Time (UTC) (Omit T, Extra Space before Z)
  `YYYY-MM-DD HH Z`,
  `YYYY-MM-DD HH:mm Z`,
  `YYYY-MM-DD HHmm Z`,
  `YYYY-MM-DD HH:mm:ss Z`,
  `YYYY-MM-DD HHmmss Z`,
  `YYYY-MM-DD HH:mm:ss.SSS Z`,
  `YYYY-MM-DD HHmmss.SSS Z`,
  `YYYY-MM-DD HH:mm:ss.SSSSSS Z`,
  `YYYY-MM-DD HHmmss.SSSSSS Z`,

  `YYYY-[W]WW`,
  `YYYY[W]WW`,
  `YYYY-[W]WW-E`,
  `YYYY[W]WWE`,
  `YYYY-DDDD`,
  `YYYYDDDD`,
]

// moment()はローカルモード
const localTime = moment(time);
console.log(localTime); // Moment<2021-01-01T06:00:00+09:00>

// moment.utc()はutcモード
const utcTime = moment.utc(time);
console.log(utcTime); // Moment<2020-12-31T21:00:00Z>

// GatsbyのGraphQLのformatString("YYYY-MM-DDTHH:mmZ")でやってること
console.log(moment.utc(time, ISO_8601_FORMAT, true).format("YYYY-MM-DDTHH:mmZ")) // 2020-12-31T21:00+00:00

// 以下のようにutcOffsetを使えばタイムゾーンにも対応できるが、それはしない方向らしい
const utcOffset = moment(time).format('Z');
console.log(moment.utc(time, ISO_8601_FORMAT, true).utcOffset(utcOffset).format("YYYY-MM-DDTHH:mmZ")) // 2021-01-01T06:00+09:00

ContentfulのContent modelからタイムゾーン設定をなくす

日付にUTC offset情報が入らないようにContent modelを修正した。エントリの日付情報を扱うfieldのSettingsからAppearanceのFormatのプルダウンを開いて、Date and time without timezoneを選択。

contentful-publishdate-format

これでpost画面からUTC選択メニューがなくなった。

Contentfulの全エントリーの日付情報からUTC offset情報を削除する

過去に投稿した記事には相変わらずUTC offsetが残っているのでこれを修正する必要がある。これはContent Management APIでサクっと。

contentful/remove-timezone.js
require("dotenv").config({ path: `.env.development` });
const env = process.env;

process.on("unhandledRejection", console.dir);

const contentful = require("contentful-management");
const client = contentful.createClient({
  accessToken: env.CONTENTFUL_PERSONAL_ACCESS_TOKEN,
});

const convertNoUtcOffset = async (entries) => {
  for (let item of entries.items) {
    try {
      if (item.fields.publishDate.ja.split('+')[1]) {
        const convertedTime = await item.fields.publishDate.ja.split('+')[0];
        console.log(`converted: ${item.fields.publishDate.ja} --> ${convertedTime}`);
        item.fields.publishDate.ja = convertedTime;
        const updatedEntry = await item.update();
        await updatedEntry.publish();
      }
    } catch (err) {
      console.log('skipped for some reason');
    }
  };
}

const checkPublishDate = (entries) => {
  for (let item of entries.items) {
    try {
      if (item.fields.publishDate.ja.split('+')[1]) {
        console.log(`warning: ${item.fields.title.ja}`);
        console.log(item.fields.publishDate.ja);
      } else {
        console.log(item.fields.publishDate.ja)
      }
    } catch (err) {
      throw err;
    }
  }
}

(async () => {
  const space = await client.getSpace(env.CONTENTFUL_SPACE_ID);
  const environment = await space.getEnvironment("master");
  const entries = await environment.getEntries({
    content_type: "blogPost",
    limit: 200,
    order: "sys.createdAt",
  });
  convertNoUtcOffset(entries);
  checkPublishDate(entries);
})();

感想

  • タイムゾーン問題はハマりどころだなーって思った
  • GraphQLのformatでタイムゾーンを考慮するようにして欲しい。でなければ、そもそもformatできないようにしたほうがよい
  • GraphQL側で整形できるとやはり便利なのでcontentful側の情報を純粋に削る判断をしたがとても気持ち悪い
  • こういうのパパっとPR飛ばせる人になりたい人生だった