Hiroaki Satou profile image Hiroaki Satou

Ghostで日本語と英語ページを作る方法まとめ

本記事では、Ghostブログで日本語と英語の多言語表示を実現するための具体的な設定方法をまとめました。routes.yamlによる言語別ルーティング、Code InjectionによるSEO対応、JavaScriptでのユーザー言語に応じた自動リダイレクト、言語ごとのメタタグ切り替え、日本語Webフォントの適用方法までを網羅しています。テーマの限界やカスタマイズの留意点についても触れていますので、Ghostブログで多言語運用を検討している方はぜひご一読ください。

Ghostで日本語と英語ページを作る方法まとめ
Photo by Pawel Czerwinski / Unsplash

1. まずはroutesファイルでページを分けて表示するタグをjaかenか分ける。

私のブログのデフォルトはjaなので以下のようなroutes.yamalファイルになります

routes:

collections:
  /:
    permalink: /{slug}/          # 日本語記事の URL
    filter: "tag:[hash-ja]"         # #jaの記事だけ

  /en/:
    permalink: /en/{slug}/          # 英語記事の URL
    filter: "tag:[hash-en]"         # #en の記事だけ

taxonomies:
  tag: /tag/{slug}/                 # 必要なら言語別に分けてもOK
  author: /author/{slug}/

この機能はカスタマイズのlabsから、routesファイルをダウンロードして書き換えるだけです。

2. #jaと#enのタグをブログ記事毎につけていきます。

3. CodeInjectionにSEO対策のリンクを追加

<link rel="alternate" hreflang="en" href="https://www.functional-visual.design/en/" />
<link rel="alternate" hreflang="ja" href="https://www.functional-visual.design/" />
<link rel="alternate" hreflang="x-default" href="https://www.functional-visual.design/" />

4. 同じくCodeInjectionにJavaScriptで言語によりdispatchする機能を追加

(私のような日本語ユーザー英語ページを表示したい時にはじかれないようにuserSelectedLanguageをsessionStorageに追加しています)

<script>
  (function() {
    // ユーザーが明示的に言語を選択したかチェック
    const userChoseLang = sessionStorage.getItem('userSelectedLanguage');
    
    // 現在のパスが言語プレフィックスを持っているかチェック
    const hasLangPrefix = location.pathname.match(/^\/(en|ja)\//);
    
    // 明示的に選択していない場合のみ自動リダイレクト
    if (!userChoseLang && !hasLangPrefix) {
      const lang = navigator.language || navigator.userLanguage;
      // 英語ユーザーのみ /en/ にリダイレクト
      // 日本語ユーザーはリダイレクトしない(日本語がデフォルト)
      if (lang.startsWith('en')) {
        // 同じページにリダイレクトしないよう確認
        const newPath = '/en' + (location.pathname === '/' ? '/' : location.pathname);
        if (location.pathname !== newPath) {
          location.replace(newPath);
        }
      }
    }
    
    // 言語リンクがクリックされたときの処理を追加
    document.addEventListener('DOMContentLoaded', function() {
      const langLinks = document.querySelectorAll('a[hreflang]');
      langLinks.forEach(link => {
        link.addEventListener('click', function() {
          // ユーザーが言語を選択したことを記録
          sessionStorage.setItem('userSelectedLanguage', true);
        });
      });
    });
  })();
</script>
  1. SEO対策のメタタグの切り替え機能を追加
<script>
  (function() {
    // ① 各言語のディスクリプションをここで定義
    var metaTexts = {
      en: {
        title:       'Functional Visual Design',
        description: 'As a hobbyist designer living with a disability, I pour my passion into design and coding. Here I showcase my projects and share the insights gained along my creative journey.',
        ogLocale:    'en_US'
      },
      ja: {
        title:       'Functional Visual Design',
        description: '障がいを抱える趣味のデザイナーとして、デザインとコーディングに情熱を注いでいます。ここではプロジェクトを公開し、創造の過程で得た洞察を共有します。',
        ogLocale:    'ja_JP'
      }
    };
    
    // ② 現在のパスが /en/ から始まるか判定
    var lang = location.pathname.startsWith('/en/') ? 'en' : 'ja';
    var cfg  = metaTexts[lang];
    
    // ③ <title> を書き換え
    document.title = cfg.title;
    
    // ④ メタタグを上書きまたは追加する関数
    function upsertMeta(attr, value, content) {
      var selector = 'meta[' + attr + '="' + value + '"]';
      var el = document.querySelector(selector);
      if (el) {
        el.setAttribute('content', content);
      } else {
        var m = document.createElement('meta');
        m.setAttribute(attr, value);
        m.setAttribute('content', content);
        document.head.appendChild(m);
      }
    }
    
    // メタタグの更新
    upsertMeta('name', 'description', cfg.description);
    upsertMeta('property', 'og:description', cfg.description);
    upsertMeta('property', 'og:locale', cfg.ogLocale);
    
    // (必要なら canonical も言語別に切り替え)
    // var canonicalUrl = location.origin + location.pathname;
    // var canonicalEl = document.querySelector('link[rel="canonical"]');
    // if (canonicalEl) {
    //   canonicalEl.setAttribute('href', canonicalUrl);
    // } else {
    //   var link = document.createElement('link');
    //   link.setAttribute('rel', 'canonical');
    //   link.setAttribute('href', canonicalUrl);
    //   document.head.appendChild(link);
    // }
  })();
</script>

6. Themeが日本語対応してないので日本語ウェブフォントとカスタムCSS

これもcode injectionで実現(このCSSは現在変更したThemeの変数ではないので後で書き換え予定)を/en/を含まないページに適用します

<script>
  (function() {
    // /ja/ で始まるパスならフォントスクリプトとスタイルを追加
    if (location.pathname.startsWith('/en/')) {
      // do nothing
    } else {
      // ① FontPlus スクリプトを動的ロード
      var fp = document.createElement('script');
      fp.src = 'https://webfont.fontplus.jp/accessor/script/fontplus.js?something-inside-the-api-key';
      document.head.appendChild(fp);
      // ② 日本語フォント用の CSS 変数&グローバル設定
      var style = document.createElement('style');
      style.textContent = `
        html[lang="ja"] {
          --gh-font-heading: TazuganeGothicStdN-Bold;
          --gh-font-body: TazuganeGothicStdN-Regular;
          --logo-author-font-weight: 700;
          --logo-author-letter-spacing: 0.005em;
          --logo-auhtor-line-height: 150%;
          --font-weight-titles: 700;
          --big-author-name-font-size: 16rem;
          --big-author-name-font-weight: 700;
          --big-author-name-line-height: 150%;
          --big-author-name-letter-spacing: 0.005em;
          --full-width-post-font-size: 4.8rem;
          --full-width-post-line-height: 125%;
          --full-width-post-letter-spacing: 0.005em;
          --h1-font-size: 4.8rem;
          --h1-line-height: 150%;
          --h1-letter-spacing: 0.005em;
          --h2-font-size: 3.2rem;
          --h2-line-height: 150%;
          --h2-letter-spacing: 0.005em;
          --h3-font-size: 2.4rem;
          --h3-line-height: 150%;
          --h3-letter-spacing: 0.005em;
          --h4-font-size: 2rem;
          --h4-line-height: 150%;
          --h4-letter-spacing: 0.005em;
          --text-L-semibold-font-size: 1.8rem;
          --text-L-semibold-font-weight: 600;
          --text-L-semibold-line-height: 150%;
          --text-L-semibold-letter-spacing: 0.005em;
          --text-L-medium-font-size: 1.8rem;
          --text-L-medium-font-weight: 500;
          --text-L-medium-line-height: 150%;
          --text-M-semibold-font-size: 1.6rem;
          --text-M-semibold-font-weight: 600;
          --text-M-semibold-line-height: 175%;
          --text-M-semibold-letter-spacing: 0.005em;
          --text-M-medium-font-size: 1.4rem;
          --text-M-medium-font-weight: 500;
          --text-M-medium-line-height: 175%;
          --text-S-bold-font-size: 1.4rem;
          --text-S-bold-font-weight: 700;
          --text-S-bold-line-height: 150%;
          --text-S-bold-letter-spacing: 0.005em;
          --text-S-medium-font-size: 1.4rem;
          --text-S-medium-font-weight: 500;
          --text-S-medium-line-height: 150%;
          --text-XS-bold-font-size: 1.2rem;
          --text-XS-bold-font-weight: 700;
          --text-XS-bold-line-height: 175%;
          --text-XS-bold-letter-spacing: 0.005em;
        }
        /* フォント変数だけじゃなくグローバルスタイルも同時に */
        html[lang="ja"], html[lang="ja"] body {
          font-size: 17px;
          letter-spacing: 0.5%;
        }
      `;
      document.head.appendChild(style);
    }
  })();
</script>

7. Nivigationに言語を追加

日本語の項目を作ってホームのURLを貼り付ける、英語の項目を作ってホームURL+\en\の項目をつけるようにします(私はデフォルトを日本語にしているので。

残された課題

私のthemeにはshow-favorite-postsなどのtheme固有のセクション毎の表示項目をオンにして表示をする機能がありますが、これは使えません。themeに複数のクエリを設定して表示を変えるには、theme自体が発行するクエリを変えないといけないためです。つまり普通にshow-favorite-postsでthemeのデフォルトのクエリを発行すると、#jaや#enに関係なく英語ページに日本語のfavorite-postsが混ざることになります。色々と考えたのですが、thema内部をカスタマイズすると、themeのアップデートの度に変更の手間がかかるので諦めました。favorite-posts機能は今のところ私にとって凄く必要な機能じゃなかったからです。

最後に私の利用しているghostホスティングサービスの紹介

私は極度の面倒くさがりなので、自分で日本のサーバーにghostをインスートールしてbackup用のスクリプトやghostを書くのが面倒でした。そこで日本でサーバーを借りる変わりにヨーロッパのサーバーを利用してghostのインストールから毎日のバックアップ、ghostの代行までを提供してもらえるところと契約しています。年間21,355円するので安くはありませんし、私はSNSをやらないのでサーチエンジンから到達して見てくれる人は少ないです。そこで見にきてくれた貴重な人も読める記事を追加すべく英語記事をmediumからghostに移しました。多くのghostホスティングサービスと同様にこのホストもCDNを使っているので静的ページのデータ日本の近くにあるので表示はそれほど重くないはずです。問題はバックエンドは動的に生成されるのでヨーロッパのサーバーだと少し重いことですね。ただし、楽したいなら多くの簡単に契約できるghostホスティングサービスがヨーロッパやアメリカにサーバーを持つのも事実です。

Magic Pages
Get your Ghost CMS publication up and running in no time with Magic Pages’ Ghost CMS web hosting – starting at $6/month!