[Tips] React で highlight.js を適用する方法

2020年12月

はじめに

Reacthighlight.js を組み込もうとしたのですが、若干躓いてしまったので対処法についてメモっておきます。

また、React は既にプロジェクトにインストール済みと仮定します。

# 一応 React をインストールするためのコマンドは ↓
npm i --save react react-dom

React に highlight.js を組み込む

まずは highlight.js を NPM or Yarn でインストールします。

# NPM で highlight.js をインストールする
npm i --save highlight.js
yarn add highlight.js

その後、React ソースコードに highlight.js を組み込みます。
ソースコードの該当部分のみを載せると下記の感じになります。

import Head from 'next/head'
import styles from '../styles/Home.module.css'
import React, { useState, useEffect } from 'react';
/**
highlight.js を import する
*/
import hljs from 'highlight.js/lib/core';
/**
シンタックスハイライトしたい言語のみ import として登録する
今回は html をハイライトしたかったので xml を import した
デザインは highlight.js/styles/~ を変更することで調整可能
https://highlightjs.org/ のトップページから各種デザインについては確認可能
(コード右下にある style の右側リンククリックで各種デザインのプレビューが可能)
*/
import xml from 'highlight.js/lib/languages/xml';
import 'highlight.js/styles/github.css';
hljs.registerLanguage('xml', xml);
let inputChecker = null;
export default function Home() {
  const [user, setUser] = useState('nikaera');
  const [previewUser, setPreviewUser] = useState('nikaera');
  const [badgeCode, setBadgeCode] = useState('nikaera');
  const [style, setStyle] = useState('plastic')
  /**
  useEffect のタイミングで hightlight.js の初期化を行う。
  called プロパティを false にすることで highlight.js で、
  コードが変更された場合でも常にシンタックスハイライトすることが可能
  */
  useEffect(() => {
    hljs.initHighlighting();
    hljs.initHighlighting.called = false;
  });
  useEffect(() => {
    /**
    シンタックスハイライトしたいコード input フォームへの入力内容に応じて動的に変わる
    */
    setBadgeCode(`  <!-- highlight.js でハイライトする -->
  <div>Hello ${user}!</div>
  }`, [user, style]);
  });
  const handleChange = (event) => {
    if (inputChecker)
      clearTimeout(inputChecker);
    inputChecker = setTimeout(() => {
      clearTimeout(inputChecker);
      inputChecker = null;
      setPreviewUser(event.target.value);
    }, 1 * 1000); // 1 seconds
    setUser(event.target.value);
  };
  return (
    <div className={styles.container}>
      <Head>
        <title>Highlight</title>
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>
          Highlight sample
        </h1>

        <input type="text" value={user} onChange={handleChange} />

        <div>
          { /* pre -> code タグ内に highlight.js で
            シンタックスハイライトしたい内容を出力する */ }
          <pre style={{ width: '80vw' }}>
            <code className="xml">
              {badgeCode}
            </code>
          </pre>
        </div>
      </main>

      <footer className={styles.footer}>
      </footer>
    </div>
  )
}

おわりに

React に highlight.js を組み込むやり方は多々見つけたものの、中々上手くできなかったのでメモっておきました。同様のケースで悩んでいる方の助けになれれば幸いです:pray:

参考リンク

[Tips] Hugo で外部リンクを target=”_blank” で開く方法

2020年12月

はじめに

Hugo で設定した外部リンクを開くときは別ウィンドウで開けるようにしたかったので、Hugo のテーマファイルをオーバーライドして対応しました。外部リンクが設定されているときのみ a タグに target="_blank" rel="noopener noreferrer" が追加されるようにしました。

テンプレートファイルは GO の HTML テンプレートで書かれているので、その書式にしたがって a タグの属性を書き換えることで、外部リンクの場合は target="_blank" rel="noopener noreferrer" を追加します。

a タグに外部リンクが設定されていた場合は target="_blank" を付与する

下記は hugo-PaperMod で、メインメニューの a タグに target="_blank" rel="noopener noreferrer" を追加するときのサンプルになります。

layouts/partials/header.html
<ul class="menu" id="menu" onscroll="menu_on_scroll()">
    <!-- `.Site.Menues.main` の要素数 (メニュー数) ループします -->
    {{- range .Site.Menus.main }}
    <!--
        要素内の .URL にアクセスすることで設定されているリンクにアクセスする。
        設定されている URL のプレフィクスが https:// or http:// であれば、
        絶対リンクが設定されているはずなため、外部リンクが設定されているとみなす。
    -->
    {{- $is_abs_url := or (strings.HasPrefix .URL "https://") (strings.HasPrefix .URL "http://") }}
    <!--
        もし外部リンクが設定されていれば、そのまま .URL の内容を出力する。
        そうでなければ、内部リンクを language プレフィクスを付与した形で出力する。
    -->
    {{- $menu_item_url := (cond $is_abs_url .URL (printf "%s/" .URL) ) | absLangURL }}
    <li>
        <!--
            外部リンクが設定されていたら ($is_abs_url が true なら)
            a タグに target="_blank" rel="noopener noreferrer" を設定する
        -->
        <a href="{{ $menu_item_url }}" {{- if $is_abs_url }} target="_blank" rel="noopener noreferrer" {{- end}}>
            <span {{- if eq $menu_item_url $page_url }} class="active" {{- end }}>
                {{ .Name }}
            </span>
        </a>
    </li>
    {{- end -}}
</ul>

おわりに

Hugo でページを自分の思い通りカスタマイズするためには、ある程度は Go の HTML テンプレートの知識も必要になりそうです... :boy: :writing_hand:

参考リンク

MediaPackage 用の CloudFront ディストリビューションを AWS SDK で作成する

2020年12月

はじめに

とある事情で MediaPackage のエンドポイント用の CloudFront ディストリビューションを AWS SDK で作成する機会がありました。その際得た知見をソースコードを交えながら備忘録として記事に残しておきます。

本記事内容で紹介しているソースコードは Gist にも同じ内容でアップしてあります。

ちなみに MediaLive + MediaPackage + CloudFront の構成でインフラ構築したい場合は、CloudFormation が MediaPackage にも対応したので CloudFormation の利用を推奨します。

本記事内容はあくまでも何らかの事情で、後から CloudFront ディストリビューションを MediaPackage エンドポイントに紐づけたいケース等で参考になると思われます。

実装内容

作成したソースコードの内容は下記になります。
最下部の createDistributionForMediaPackage が本記事タイトルに該当する関数です。

CloudFrontClientForMediaPackage.ts
import { CloudFront } from "aws-sdk";
import * as url from "url";
import {
    CreateDistributionWithTagsResult,
    GetDistributionResult,
    UpdateDistributionResult
} from "aws-sdk/clients/cloudfront";
export class CloudFrontClientForMediaPackage {
    private cloudFront: CloudFront;
  constructor() {
    this.cloudFront = new CloudFront({
        region: "ap-northeast-1",
        apiVersion: '2020-05-31',
    });
}
/**
 * CloudFront ディストリビューションの情報を取得するために利用する
 * @param id CloudFront ディストリビューションの ID
 * @return ディストリビューションの情報を取得する
 */
  async getDistribution(id: string): Promise<GetDistributionResult> {
      const distribution = await this.cloudFront.getDistribution({
          Id: id
      }).promise()
      return distribution;
  }
  /**
   * CloudFront ディストリビューションの設定内容を取得するために利用する
   * @param id CloudFront ディストリビューションの ID
   * @return ディストリビューションの設定内容を取得する
   */
  async getDistributionConfig(id: string): Promise<CloudFront.DistributionConfig> {
      const config = await this.cloudFront.getDistributionConfig({
          Id: id
      }).promise()
      return config.DistributionConfig;
  }
  /**
   * CloudFront ディストリビューションを削除する
   * @param id 削除したい CloudFront ディストリビューションの ID
   */
  async deleteDistribution(id: string) {
    const distribution = await this.getDistribution(id);
    await this.cloudFront.deleteDistribution({
        Id: id, IfMatch: distribution.ETag
    }).promise()
  }
  /**
   * CloudFront ディストリビューションを無効化する
   * @param id 無効化したい CloudFront ディストリビューションの ID
   * @return 無効化した CloudFront ディストリビューションの情報
   */
  async disableDistribution(id: string): Promise<UpdateDistributionResult> {
      const distribution = await this.getDistribution(id);
      const config = distribution.Distribution.DistributionConfig;
      config.Enabled = false;
      return await this.cloudFront.updateDistribution({
        Id: id,
        IfMatch: distribution.ETag,
        DistributionConfig: config
      }).promise();
  }
  /**
   * MediaPackage のエンドポイント用の CloudFront ディストリビューションを作成する
   * @param id CloudFront ディストリビューションを判別するための ID
   * @param mediaPackageArn MediaPackage チャンネルの ARN
   * @param mediaPackageUrl MediaPackage エンドポイントの URL
   */
  async createDistributionForMediaPackage(
      id: string,
      mediaPackageArn: string,
      mediaPackageUrl: string
    ): Promise<CreateDistributionWithTagsResult> {
    // 1. url モジュールを用いて URL 文字列をパースする
    const mediaPackageEndpoint = url.parse(mediaPackageUrl);
    /**
    2. MediaPackage のエンドポイント URL から FQDN を取得する。
    後述する CloudFront ディストリビューションのオリジンのドメイン名としても利用する
    */
    const mediaPackageHostname = mediaPackageEndpoint.hostname;
    /**
    3. MediaPackage のエンドポイント URL のフォーマットは
    https://<AccountID>.mediapackage.<Region>.amazonaws.com/**** となっているので、
    FQDN の先頭部分を文字列分割で取り出すとアカウント ID が取得できる
    */
    const accountId = mediaPackageHostname.split('.')[0];
    // 4. 後述する CloudFront ディストリビューションのオリジン ID として、アカウント ID を利用する
    const targetOriginId = `MP-${accountId}`
    /**
    5. createDistribution ではなく、createDistributionWithTags 関数で、
    CloudFront ディストリビューションを作成する。MediaPackage との紐付けにタグを利用するため。
    */
    return await this.cloudFront.createDistributionWithTags({
        DistributionConfigWithTags: {
            Tags: {
                Items: [
                    /**
                    !!!!!重要!!!!!
                    6. CloudFront ディストリビューションに紐付けたい
                    MediaPackage エンドポイントのチャンネル ARN を
                    mediapackage:cloudfront_assoc で定義する。
                    mediapackage:cloudfront_assoc を定義することで、
                    CloudFront ディストリビューションと
                    MediaPackage チャンネルを紐付けることが可能となる。
                    */
                    {
                        Key: 'mediapackage:cloudfront_assoc',
                        Value: mediaPackageArn
                    },
                    {
                        Key: 'Id',
                        Value: id
                    },
                    {
                        Key: 'Product',
                        Value: 'product'
                    },
                    {
                        Key: 'Stage',
                        Value: 'dev'
                    }
                ]
            },
            DistributionConfig: {
                CallerReference: new Date().toISOString(),
                Comment: `Managed by MediaPackage - ${id}`,
                Enabled: true,
                /**
                7. CloudFront ディストリビューションのオリジンには 2つ設定します。
                1つが MediaPackage のエンドポイントに対するものと、
                もう 1つが MediaPacakge サービスに対するものです。
                基本的には MediaPackage のエンドポイントに対するオリジンを利用します。
                例外時に向けるオリジンが MediaPacakge サービスに対するものになります。
                */
                Origins: {
                    Quantity: 2,
                    Items: [
                        {
                            DomainName: mediaPackageHostname,
                            Id: targetOriginId,
                            CustomOriginConfig: {
                                HTTPPort: 80,
                                HTTPSPort: 443,
                                OriginProtocolPolicy: 'match-viewer'
                            }
                        },
                        {
                            DomainName: 'mediapackage.amazonaws.com',
                            Id: "TEMP_ORIGIN_ID/channel",
                            CustomOriginConfig: {
                                HTTPPort: 80,
                                HTTPSPort: 443,
                                OriginProtocolPolicy: 'match-viewer'
                            }
                        }
                    ]
                },
                /**
                8. CacheBehaviors のいずれにも当てはまらなかった場合の
                キャッシュの振る舞いを定義します。
                MediaPackage は タイムシフト表示機能を使用する際等で、クエリ文字列に start, m, end を利用しています。
                そのため、それらの文字列は WhitelistedNames に含め QueryString には true を指定しておきます。
                DefaultCacheBehavior に引っかかる挙動は例外的扱いなので、
                使用するオリジンは MediaPackage サービスのものを設定します。
                */
                DefaultCacheBehavior: {
                    ForwardedValues: {
                        Cookies: {
                            Forward: 'whitelist',
                            WhitelistedNames: {
                                Quantity: 3,
                                Items: [
                                    'end', 'm', 'start'
                                ]
                            }
                        },
                        QueryString: true,
                        Headers: {
                            Quantity: 0
                        },
                        QueryStringCacheKeys: {
                            Quantity: 0
                        }
                    },
                    MinTTL: 6,
                    TargetOriginId: "TEMP_ORIGIN_ID/channel",
                    TrustedSigners: {
                        Enabled: false,
                        Quantity: 0
                    },
                    ViewerProtocolPolicy: 'redirect-to-https',
                    AllowedMethods: {
                        Items: [
                            'GET', 'HEAD'
                        ],
                        Quantity: 2,
                    },
                    MaxTTL: 60
                },
                /**
                9. CloudFront のエラーコード全ての TTL に 1sec を設定します。
                MediaPackage のエラーのキャッシュが長時間持続してしまうと、
                その間は MediaPackage で正常に配信できているとしても、
                復旧できない状態となるからです。
                */
                CustomErrorResponses: {
                    Quantity: 10,
                    Items: [
                    {
                        ErrorCode: 400,
                        ErrorCachingMinTTL: 1
                    }, {
                        ErrorCode: 403,
                        ErrorCachingMinTTL: 1
                    }, {
                        ErrorCode: 404,
                        ErrorCachingMinTTL: 1
                    }, {
                        ErrorCode: 405,
                        ErrorCachingMinTTL: 1
                    }, {
                        ErrorCode: 414,
                        ErrorCachingMinTTL: 1
                    }, {
                        ErrorCode: 416,
                        ErrorCachingMinTTL: 1
                    }, {
                        ErrorCode: 500,
                        ErrorCachingMinTTL: 1
                    }, {
                        ErrorCode: 501,
                        ErrorCachingMinTTL: 1
                    }, {
                        ErrorCode: 502,
                        ErrorCachingMinTTL: 1
                    }, {
                        ErrorCode: 503,
                        ErrorCachingMinTTL: 1
                    }
                    ]
                },
                /**
                10. CloudFront ディストリビューションのキャッシュの振る舞いを 2つ定義します。
                それぞれの設定内容は基本的に DefaultCacheBehavior で定義したものと同様です。
                しかし、利用するオリジンは MediaPackage エンドポイントに向けたものを利用します。
                1つは Microsoft Smooth Streaming での配信時に利用する
                index.ism に対するもので Smooth Streaming を true に設定しています。
                もう 1つは上記 Microsoft Smooth Streaming 以外の
                全てに当てはまるストリーミングに適用されるものになります。
                */
                CacheBehaviors: {
                    Quantity: 2,
                    Items: [{
                        MinTTL: 6,
                        PathPattern: 'index.ism/*',
                        TargetOriginId: targetOriginId,
                        ViewerProtocolPolicy: 'redirect-to-https',
                        AllowedMethods: {
                            Items: [
                                'GET', 'HEAD'
                            ],
                            Quantity: 2,
                        },
                        ForwardedValues: {
                            Cookies: {
                                Forward: 'whitelist',
                                WhitelistedNames: {
                                    Quantity: 3,
                                    Items: [
                                        'end', 'm', 'start'
                                    ]
                                }
                            },
                            QueryString: true,
                            Headers: {
                                Quantity: 0
                            },
                            QueryStringCacheKeys: {
                                Quantity: 0
                            },
                        },
                        SmoothStreaming: true
                    }, {
                        MinTTL: 6,
                        PathPattern: '*',
                        TargetOriginId: targetOriginId,
                        ViewerProtocolPolicy: 'redirect-to-https',
                        AllowedMethods: {
                            Items: [
                                'GET', 'HEAD'
                            ],
                            Quantity: 2,
                        },
                        ForwardedValues: {
                            Cookies: {
                                Forward: 'whitelist',
                                WhitelistedNames: {
                                Quantity: 3,
                                Items: [
                                    'end', 'm', 'start'
                                ]
                                }
                            },
                            QueryString: true,
                            Headers: {
                                Quantity: 0
                            },
                            QueryStringCacheKeys: {
                                Quantity: 0
                            },
                        }
                    }]
                },
                PriceClass: 'PriceClass_All'
            }
        }
    }).promise()
  }
}

createDistributionForMediaPackage で作成したディストリビューションは、公式ページに記載された手順 で作成した CloudFront ディストリビューションと同等のものになります。

詳細な説明はインラインコメントにて書きましたが、一応補足説明を少し付け加えておきます。

随所に出てくる Quantity について

Quantity には Items で指定する項目の数を入力します。 例えば HeadersQueryStringCacheKeys には Items に何も指定していないため、Quantity0 を指定します。

しかし、AllowedMethodsWhitelistedNames には Items に指定した項目数である 23Quantity に入力しています。Quantity の数と Items の項目数が合わないと、エラーが発生するため、注意が必要です。

mediapackage:cloudfront_assoc を定義する意味

CloudFront ディストリビューションのタグに mediapackage:cloudfront_assoc で紐付ける MediaPackage のチャンネル ARN を指定することで、MediaPackage コンソールから紐付けられた CloudFront ディストリビューション情報を参照できるようになります。

試しに紐づけられた MediaPackage のチャンネルのエンドポイント詳細ページに遷移すると、
下記のような画面が確認できるはずです。

mediapackage:cloudfront_assoc で紐付いた CloudFront ディストリビューションが確認できる
mediapackage:cloudfront_assoc で紐付いた CloudFront ディストリビューションが確認できる

なお、本記事内のソースコードでは他にも Id, Product, Stage といったタグを定義していますが、MediaPackage とは関係無いものなので削除して問題ありません。

updateDistribution を実行する際の注意点

これは今回の記事内容とは直接関係ないのですが、地味にハマったので載せておきます。

CloudFront では createDistribution の時に要求されるパラメータよりも updateDistribution で要求されるパラメータのほうが多いです。 AWS 公式ページの比較表にある通りです。

そのため、updateDistribution で設定を一部更新したいだけなのに、とても多くのパラメータを指定する必要があり非常に面倒です。例えば CloudFront ディストリビューションの Enable/Disable を切り替えるだけでも 30 個近いパラメータを指定する必要あります。

上記の入力の手間を省くのには getDistribution で取得した既存のディストリビューション情報を改変する形で updateDistribution のパラメータを作成すると楽でした。

今回のソースコードの内容を参照すると disableDistribution が該当します。

// 1. getDistribution を実行して CloudFront ディストリビューションの情報を取得する
const distribution = await this.getDistribution(id);
// 2. CloudFront ディストリビューションの設定内容を取得する
const config = distribution.Distribution.DistributionConfig;
// 3. CloudFront ディストリビューションの Enabled/Disabled を切り替えるオプションを改変する
config.Enabled = false;
// 4. 3. で改変した内容を updateDistribution で CloudFront ディストリビューションに反映する
return await this.cloudFront.updateDistribution({
    Id: id,
    IfMatch: distribution.ETag,
    DistributionConfig: config
}).promise();

おわりに

ニッチな内容なので、本記事内容を今後利用するかどうかは分かりませんが、一応得た知見を記事として残しておきました。同様のことを行う必要が出てきた方の参考になれれば幸いです。

参考リンク