7rikazhexde’s tech log

技術的な興味関心、備忘録、アウトプットなどを書いています。

Pinterest APIの申請方法と使用例について

経緯

ローカルに保存した画像をWebにも投稿したいと思い、画像や動画投稿に対応したPinterestに投稿することを考えました。

ピン投稿はPinterestアプリから実行できますが、画像が多く、titleやdescriptionも指定したかったので公式のPinterest APIを利用することにしました。

本記事では、実際にAPIを申請して動作確認をしたので、その内容をまとめた記事になります。

Pinterest APIの申請方法

2024年4月現在、Pinterest API(REST API)はAPI v5が最新です。また、Pinterest APIを利用するためには、Pinterestのビジネスアカウントを作成し、App登録(申請)を行う必要があります。 https://developers.pinterest.com/account-setup/

これはPinterest APIの仕様として、OAuth 2.0 認証フレームワークでアプリに紐付くApp idとApp Secret Keyを使用してAcccess Tokenを生成し、Bearer認証方式でエンドポイントURLに対してGET/POSTリクエストを送信することで、ボードやPinの作成/管理を行う仕組みであるためです。

詳細は下記に記載されています。 https://developers.pinterest.com/docs/getting-started/set-up-app/

また、App idとApp Secret Keyを取得するためにはappsページから指定のフォームに従い必要事項を記入してAppを登録する必要があります。

  • アプリアイコン(png)
  • アプリ名
  • 会社名
  • 会社のURL/アプリのURL
  • プライバシーポリシーのURL
  • アプリの利用目的

アプリ申請ページ(キャプチャ)

Connect App

私の場合、申請ではアプリのURLを入力するために、サイトをDocusaurusで作成しました。
サイトはsocial cardをデフォルトでOGPも設定していない必要最低限の構成ですが、申請は通りました。

7rikazhexde.github.io

APIの使用例

Appを登録(申請)するとPinterestからアプリ構築リクエスト/トライアルアクセス承認のメールが届きます。
早ければ翌日には承認メールが届きます。アクセスが承認されるとApp idとApp Secret Keyが表示されます。

API申請メール受信画面

そして、APIに関する情報として以下の情報が記載されています。

アクセストークンに関する情報はドキュメント(Generating an access token)にも記載されていますが、ここではGitHub API Quickstartget_access_token.pyを使用した例を紹介します。

access_tokenの取得方法

※本コードはコミットハッシュ(6304e40)の情報です。

アクセストークンを使用してコードを実行する場合はScope(権限)を設定する必要があります。以下では全権限を設定する例を示します。

なお、事前にapi-quickstart?の手順を実行して、環境変数(PINTEREST_APP_ID,PINTEREST_APP_SECRET)を設定する必要があります。詳細は上記リンクを確認してください。

Scopeはコマンドライン引数(-s/--scopes)で指定しますが、ScopeはEnumapi-quickstart/python/src/oauth_scope.pyに定義されています。

scope定義: oauth_scope.py
class Scope(Enum):
    READ_ADS = "ads:read"
    WRITE_ADS = "ads:write"
    READ_BOARDS = "boards:read"
    WRITE_BOARDS = "boards:write"
    READ_CATALOGS = "catalogs:read"
    WRITE_CATALOGS = "catalogs:write"
    READ_PINS = "pins:read"
    WRITE_PINS = "pins:write"
    READ_USERS = "user_accounts:read"
    READ_SECRET_BOARDS = "boards:read_secret"
    WRITE_SECRET_BOARDS = "boards:write_secret"
    READ_SECRET_PINS = "pins:read_secret"
    WRITE_SECRET_PINS = "pins:write_secret"
    READ_ADVERTISERS = "ads:read"

# 以降省略

コマンドの使用方法は下記の通りです。

$ ./scripts/get_access_token.py --help

usage: get_access_token.py [-h] [-w] [-ct] [-s SCOPES] [-a ACCESS_TOKEN]
                           [-l LOG_LEVEL]

Get Pinterest OAuth token

options:
  -h, --help            show this help message and exit
  -w, --write           write access token to file
  -ct, --cleartext      print the token in clear text
  -s SCOPES, --scopes SCOPES
                        comma separated list of scopes or 'help'
  -a ACCESS_TOKEN, --access-token ACCESS_TOKEN
                        access token name
  -l LOG_LEVEL, --log-level LOG_LEVEL
                        level of logging verbosity

引数の説明は上記記載の通りですが、まとめると下記です。

  • -s:スコープ(権限)の設定
  • -ct:アクセストークン情報を表示
  • -w:アクセストークン情報をapi-quickstart/common/oauth_tokensに保存する
    • -aでアクセストークン名を指定した場合は、アクセストークン名.jsonが保存される
    • -aを指定しない場合は、access_token.jsonが保存される

注意事項

調べたところ、上記コードでスコープに書き込み権限を付与できますが、実際にAPIを実行すると書き込み処理でエラーになります。

これは推測ですが、申請後のステータスはtrialであり、機能が制限されている可能性があります。書き込み機能を使用したい場合はヘルプセンターに問い合わせして有効にしてもらう対応が必要という認識です。

ただし、sandbox指定のエンドポイントURL指定であれば実行可能です。詳細は後述します。

全権限を付与する例: get_access_token.py

% ./scripts/get_access_token.py -w -s ads:read,ads:write,boards:read,boards:read_secret,boards:write,boards:write_secret,catalogs:read,catalogs:write,pins:read,pins:read_secret,pins:write,pins:write_secret,user_accounts:read -ct

Using application ID and secret from PINTEREST_APP_ID and PINTEREST_APP_SECRET.
getting auth_code...
// ここでリダイレクトURLが起動されるため、Give Accessを押下します。

exchanging auth_code for access_token...
POST https://api.pinterest.com/v5/oauth/token
<Response [200]>
scope: ads:read ads:write boards:read boards:read_secret boards:write boards:write_secret catalogs:read catalogs:write pins:read pins:read_secret pins:write pins:write_secret user_accounts:read
Please keep clear text tokens secure!
clear text access token: [pina_hogehoge] //ここがアクセストークン
clear text refresh token: [pinr_hogehoge]
hashed refresh token: [hogehoge]
writing access token
GET https://api.pinterest.com/v5/user_account
<Response [200]>
--- User Summary ---
Username: [user_hogehoge]
Account Type: BUSINESS
Profile Image: [hogehoge.jpg]
Website URL: 
--------------------

OAuth_Authentication

以上でアクセストークンを取得できました。

Boardの情報を取得する

以下のコードを使用してボードの情報を取得します。

get_user_boards.py

オプションの指定は下記の通りです。

% ./scripts/get_user_boards.py -h                    
usage: get_user_boards.py [-h] [-ps PAGE_SIZE] [--include-empty] [--no-include-empty] [--include-archived] [--no-include-archived] [-a ACCESS_TOKEN] [-l LOG_LEVEL]

Get A User's Boards

options:
  -h, --help            show this help message and exit
  -ps PAGE_SIZE, --page-size PAGE_SIZE
                        Boards per page
  --include-empty       Include empty boards?
  --no-include-empty
  --include-archived    Include archived boards?
  --no-include-archived
  -a ACCESS_TOKEN, --access-token ACCESS_TOKEN
                        access token name
  -l LOG_LEVEL, --log-level LOG_LEVEL
                        level of logging verbosity

オプション指定なしで取得してみます。アカウント作成されたボードの情報を1度に25件分取得できるようです。26件以降は取得許可(yes)を入力すると取得できました。

% ./scripts/get_user_boards.py   
Using application ID and secret from PINTEREST_APP_ID and PINTEREST_APP_SECRET.
reading access_token from environment failed, trying read
read access_token from $HOME/develop/pinterest_dev/api-quickstart/common/scripts/../oauth_tokens/access_token.json
GET https://api.pinterest.com/v5/user_account
<Response [200]>
GET https://api.pinterest.com/v5/boards?page_size=25
<Response [200]>
[1] --- Board Summary ---
Board ID: [BOARD_ID-1]
Name: Animals 動物
Description: 
Privacy: PUBLIC
--------------------
//
--------------------
[16] --- Board Summary ---
Board ID: [BOARD_ID-16]
Name: Nature 自然
Description: 
Privacy: PUBLIC
--------------------
//
[26] --- Board Summary ---
//
--------------------
get_board.py

指定のボードに関する情報を取得します。コマンドの使用方法は下記の通りです。

% ./scripts/get_board.py -h                          
usage: get_board.py [-h] -b BOARD_ID [--pins] [-a ACCESS_TOKEN] [-l LOG_LEVEL]

Get a Board

options:
  -h, --help            show this help message and exit
  -b BOARD_ID, --board-id BOARD_ID
                        board identifier
  --pins                Get the Pins for the Board
  -a ACCESS_TOKEN, --access-token ACCESS_TOKEN
                        access token name
  -l LOG_LEVEL, --log-level LOG_LEVEL
                        level of logging verbosity

以下のオプションで[BOARD_ID-1]指定で取得してみます。

  • -b: 取得したいボード(BOARD_IDで指定)
  • --pins: ボードのピンを取得
% ./scripts/get_board.py -b [BOARD_ID-1] --pins
Using application ID and secret from PINTEREST_APP_ID and PINTEREST_APP_SECRET.
reading access_token from environment failed, trying read
read access_token from $HOME/develop/pinterest_dev/api-quickstart/common/scripts/../oauth_tokens/access_token.json
GET https://api.pinterest.com/v5/boards/[BOARD_ID-1]
<Response [200]>
--- Board Summary ---
Board ID: [BOARD_ID-1]
Name: Animals 動物
Description: 
Privacy: PUBLIC
--------------------
GET https://api.pinterest.com/v5/boards/[BOARD_ID-1]/pins
<Response [200]>
--- Pin Summary ---
Pin ID: [PIN_ID-1]
Title: Register - Login
Description: Tumblr. Pure effervescent enrichment. Old internet energy. Home of the Reblogs. All the art you never knew you needed. All the fandoms you could wish for. Enough memes to knock out a moderately-sized mammal. Add to it or simply scroll through and soak it up.
Link: http://www.tumblr.com/dashboard
Section ID: None
Domain: None
--------------------
--- Pin Summary ---
Pin ID: [PIN-ID_2]
Title: 【64枚】ほのぼの動物画像の時間だコラァ! : あじゃじゃしたー
Description: 1: 以下、\(^o^)/でVIPがお送りします 2016/11/09(水) 19:58:34.044 ID:t+KJjYdV0 オラァ!
Link: http://blog.livedoor.jp/chihhylove/archives/9410795.html
Section ID: None
Domain: None
--------------------
//省略
GET https://api.pinterest.com/v5/boards/[BOARD_ID-1]/sections
<Response [200]>
参考:get_board_ids.py

get_board.pyではボードの情報が見づらいので以下のコードでGETリクエストを送信するコードも自作しました。

import requests

username = "user_hogehoge"
access_token = "pina_hogehoge"

url = f"https://api.pinterest.com/v5/boards/?owner={username}"
headers = {"Authorization": f"Bearer {access_token}"}
response = requests.get(url, headers=headers)

if response.status_code == 200:
    data = response.json()
    for board in data["items"]:
        print(f"Board ID: {board['id']}, Board Name: {board['name']}")
else:
    print("Failed to retrieve boards.")
    print(f"Status code: {response.status_code}")
    print(f"Error message: {response.text}")

実行すると以下のように取得できます。

% python scripts/get_board_id.py
Board ID: [BOARD_ID-1], Board Name: Animals 動物
//
Board ID: [BOARD_ID-2], Board Name: Nature 自然

curlでのピン投稿

ピン投稿ではpins:writeboards:writeが必要ですが、
api-quickstartではPin投稿用のコードはありません。

一方でAPI referenceにはPython SDKのサンプルが記載されています。

以下のコードをpins:writeboards:writeのscopeを設定したアクセストークンをget_access_token.pyで取得し、書き込み用のコードを作成して実行しましたが、invalidエラーになりました。

これは前述した通り、Appはtrial対象であり、何らかの制限が設けられている可能性があります。 

# Follow this link for initial setup: https://github.com/pinterest/pinterest-python-sdk#getting-started

from pinterest.organic.pins import Pin
# Board information can be fetched from profile page or from create/list board method here:
# https://developers.pinterest.com/docs/api/v5/#operation/boards/list
BOARD_ID="<Add your board id here>"

pin_create = Pin.create(
  board_id=BOARD_ID,
  title="My Pin",
  description="Pin Description",
  media_source={
      "source_type": "image_url",
      "content_type": "image/jpeg",
      "data": "string",
      'url':'https://i.pinimg.com/564x/28/75/e9/2875e94f8055227e72d514b837adb271.jpg'
      }
  )
print("Pin Id: %s, Pin Title:%s" %(pin_create.id, pin_create.title))

他の方法がないか調べたところ、Pinterest APIではSandbox環境が提供されており、Sandboxではトライアル、スタンダード、アドバンスのすべてのアクセス階層で利用可能であることが記載されていました。 https://developers.pinterest.com/docs/dev-tools/sandbox/

  • Sandbox is available in all access tiers – Trial, Standard or Advanced. You can make successful API calls to the Sandbox environment regardless of which access tier your app is in.

そこで、API reference記載のcurl(Sandbox)のコマンドを実行してみました。

% curl --location --request POST 'https://api-sandbox.pinterest.com/v5/pins' \
  --header 'Authorization: Bearer [pina_xxxx]' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "title": "My Pin",
    "description": "Pin Description",
    "board_id": "[boad_id]",
    "media_source": {
      "source_type": "image_url",
      "url": "https://i.pinimg.com/564x/28/75/e9/2875e94f8055227e72d514b837adb271.jpg"
    }
  }'

結果、指定のボードに投稿することができました。

{"note":"","product_tags":[],"board_owner":{"username":"[username]"},"board_section_id":null,"creative_type":"REGULAR","board_id":"[board_id]","is_owner":true,"parent_pin_id":null,"pin_metrics":null,"is_standard":true,"created_at":"2024-04-10T14:20:01","has_been_promoted":false,"description":"Pin Description","dominant_color":"#ffffff","alt_text":null,"link":null,"title":"My Pin","media":{"media_type":"image","images":{"150x150":{"width":150,"height":150,"url":"https://i.pinimg.com/150x150/64/ed/fc/64edfcd9fe0bf9a47d440b8bf7ee5047.jpg"},"400x300":{"width":400,"height":300,"url":"https://i.pinimg.com/400x300/64/ed/fc/64edfcd9fe0bf9a47d440b8bf7ee5047.jpg"},"600x":{"width":564,"height":317,"url":"https://i.pinimg.com/564x/64/ed/fc/64edfcd9fe0bf9a47d440b8bf7ee5047.jpg"},"1200x":{"width":564,"height":317,"url":"https://i.pinimg.com/1200x/64/ed/fc/64edfcd9fe0bf9a47d440b8bf7ee5047.jpg"}}},"id":"[id]"}

API_pins_test

上記ではsource_typeimage_urlを指定しましたが、image_base64も指定であるためローカル保存された画像に対しても投稿できる可能性はあります。こちらは未確認ですが、動作確認ができた場合は結果を追記します。

まとめ

  • Pinterest APIの申請方法を紹介しました。
  • 読み込みと書き込みの使用例を紹介しました。
  • 最終的にはアプリに組み込むことを検討しているため進展があれば別記事で紹介します。
  • 最後に、本記事が参考になりましたら、「はてなブックマーク」と「はてなスター」をお願いします。

以上です。

【GA4】プロパティ作成とストリームIDを確認する方法

毎回忘れるので備忘録としてまとめます。

注意事項

本記事は2024/03/31時点の情報です。最新の情報はGA4の公式ページを確認してください。

手順

プロパティの作成

管理 > 作成 > プロパティ

GA4_プロパティの作成

プロパティの詳細を入力

以下を入力/選択する。

  • プロパティの作成
  • お店やサービスの詳細
  • ビジネス目標
  • データの収集

GA4_プロパティの詳細を入力

データストリームの設定

以下を入力する。

  • ウェブサイトのURL(イベント送信対象のURL)
  • ストリーム名(GA4のプロパティ名に相当)

GA4_データストリームの設定

ストリームIDの確認方法

  • 登録したストリーム(プロパティ)を選択 > 管理 > データの収集と設定(データストリームを選択)

以上です。

CanvaとInkscapeを使用してPNGファイルからSVGファイルを作成する方法

経緯

Docusaurusを使用して静的サイト(SSG)を作成する際にロゴSVGで作成する必要がありました。

アイコン自体はCanvaで作成することができますが、SVGファイルでダウンロードする場合は有料版を使用する必要があります。
有料版は便利な機能がありますが、とりあえずSVGファイル作りたいという状況だったため、無料版で対応できないか考えていました。

調査したところ、無料版を使用してPNGファイル指定でDLし、InkscapePNGファイルからSVGファイルをDLすることで対応できることがわかりました。
本記事ではその内容について紹介する記事になります。

やり方

Canvaでアイコンを作成してPNGファイルを保存する

Canvaでカスタムサイズを指定し、フォント、テキストスタイルで指定のデザインを作成し、ダウンロードでPNGを指定してファイルを保存します。

Canva_create_icon

保存したPNGファイルは以下の通りです。

logo_png_detail

Inkscapeを使用してPNGファイルからSVGファイルを保存する

  1. icon.pngInkscapeで読み込みます。(読み込み時の指定はデフォルト)
  2. Plain SVGを指定してエクスポートする

Inkscape_create_svg

保存したSVGファイルの詳細は以下の通りです。

補足: InkscapeのDL方法

下記記事で紹介しています。
7rikazhexde-techlog.hatenablog.com

補足: SVGファイルのサイズ指定

SVGのサイズを変更する場合は下記青枠の200の部分を変更すれば指定サイズに変更することができます。

logo_svg_detail

サイト起動例

DocusaurusでWEBサイトをローカルで起動した結果は下記の通りです。 ページの画像はデフォルトのものをそのまま使用していますが、Home横の画像が作成したSVGファイルで表示できていることが確認できると思います。

Docusaurus_local_web_site

補足: Python、Node.jsでの作成方法

他の方法として、Python、Node.jsを使用して、PNGファイルをバイナリデータに変換してBase64エンコードして、SVGのimgタグに埋め込む方法も可能でしたが、低解像度で不完全でした。

調べるとSVGではパス、ベジェ曲線、プリミティブなどのベクター要素を使用する必要があり、単純な変換は難しいようです。

Inkscapeではこれらベクター情報を考慮してSVGを生成し、編集することに対応しているため、Inkscapeを利用することが良いと個人的には思います。

補足: favicon.icoの作成方法

Docusaurusではfaviconの指定も可能です。favicon.icoPNGファイルから作成する方法としてPythonでの例を示します。パス、ファイル名、sizesは対象ファイルの情報で変更してください。

from PIL import Image

img = Image.open('./media_tools/media/input.png')
img.save('./media_tools/media/output.ico', format='ICO', sizes=[(32,32)])

まとめ

DocusaurusでSVGファイルを使用するため、CanvaとInkscapeを使用してPNGファイルからSVGファイルを作成する方法を紹介しました。
もし本記事が参考になれば「はてなスター / はてなブックマーク」をお願いします。

以上です。

Docusaurusのi18n設定について

Docusaurusの多言語化対応(i18n)の設定について情報をまとめます。

注意事項

  • 本記事は公式ドキュメント(Fast Track)を実行済みであることを前提とします。
  • Docusaurusはv1,v2,v3で仕様が異なります。下記記事は特記事項がない限り、v3.1.1をベースにします。
  • 仕様は変更されることがありますので、公式ドキュメントやissuesの確認をお願いします。
  • 情報は適宜更新します。

i18n対応の基本情報

公式ドキュメントに記載されています。
以下を参照して設定すれば基本的な言語切り替え動作は確認できました。

多言語サイトをローカル起動する時の注意点

言語設定でサイトをローカルで起動する方法は下記ドキュメントに記載されている通りです。
https://docusaurus.io/docs/i18n/tutorial#start-your-site

日本語用のサイトであれば、yarn run start --locale jajaで構築したサイトを起動できます。

しかし、以下警告の記載の通り、ja->enの切り替えても、実際にアクセスするとPage Not Foundになります。

言語を切り替えて表示する場合は、yarn build && yarn startでビルドして起動する必要があります。startコマンドはあくまで指定言語の起動用のコマンドであることに注意が必要です。

WARNING

Each locale is a distinct standalone single-page application: it is not possible to start the Docusaurus sites in all locales at the same time.

<補足>yarn startとyarn build && yarn serveの違い

コマンドは公式ドキュメントでも記載されていますが、 https://docusaurus.io/docs/deployment#testing-build-locally

claudeでも確認しました。回答は以下の通りです。

yarn startyarn buildyarn serve の違いは以下の通りです。

yarn start

  • Docusaurusの開発サーバーを起動します
  • ファイルの変更を監視し、変更があれば自動的にリロードされます
  • 開発用の設定が適用されています
  • 本番用のビルドは行われません

yarn build

  • 本番用の静的なウェブサイトをビルドします
  • ソースコードを最適化し、JS/CSSなどのリソースをバンドルします
  • i18n対応の設定が適用されています

yarn serve

  • yarn build で生成された本番用ビルド結果を、ローカルの一時サーバーで確認できます

つまり、yarn start は開発用なので、i18nの多言語設定が適用されません。

一方で、yarn build で本番用にビルドし、yarn serve でその結果を確認すると、i18nの多言語設定が適切に適用されるため、言語切替えと正しいコンテンツの表示ができるようになります。

i18nの設定は、production buildした状態でのみ、適切に反映されることが理由だと考えられます。開発中は yarn start で行い、最終的に yarn build して動作確認することが推奨されているようです。

この挙動は、Docusaurusのドキュメントにも記載があり、production buildでのみi18nが有効になることが説明されています。

言語切り替え用プルダウンの設定

Docusaurusではdocusaurus.config.jsi18nの設定を追加することで、言語変更に対応したプルダウンを表示することができます。i18n/ja,enフォルダ、ファイルを作成すれば、プルダウン選択で切り替えることができます。

/** @type {import('@docusaurus/types').Config} */
const config = {
  //省略
  i18n: {
    defaultLocale: 'en',
    locales: ['en','ja'],
  },

ユースケース

以降はユースケース毎にi18nの設定方法を記載します。

以下内容では、言語差異がわかりやすいように、テキスト(単語、文章)は英語/日本語で切り替えています。 例えば、GitHubなどの単語は翻訳しなくても良いですが、翻訳が不要であれば後述するjsonファイル(例:code.json)で定義しなければ翻訳はされません。

公開サイト

i18n設定したページ(en/ja)を紹介します。
サイトはGitHub Pagesで公開しています。

英語版(en)

https://7rikazhexde.github.io/test-website-docusaurus/

日本語版(ja)

https://7rikazhexde.github.io/test-website-docusaurus/ja/

キャプチャ

トップページ(en)

docusaurus-top-i18n-en

トップページ(ja)

docusaurus-top-i18n-ja

ブログページ(en)

docusaurus-blog-i18n-en

ブログページ(ja)

docusaurus-blog-i18n-ja

docsページ / チュートリアル(en)

docusaurus-tutolial-i18n-en

docsページ / チュートリアル(ja)

docusaurus-tutolial-i18n-ja

続けてユースケース毎に設定方法を記載します。

ドキュメント(docs)の言語切り替え

Docs i18nの情報を元に対応します。

docsディレクト内のディレクトリ/ファイルをi18n/ja/docusaurus-plugin-content-docs/current以下にコピー&ペーストします。

ドキュメント(docs)の言語切り替えの設定としては以上です。

トップページ(pages)の言語切り替え

トップページはsrc以下のファイルを元に実行されますが、トップページはReactコードで記載されています。

  • src/pages/index.js
  • src/components/HomepageFeatures/index.js

翻訳には翻訳用のAPIを使用します。 https://docusaurus.io/docs/i18n/tutorial#translate-your-site

翻訳用のAPIには以下2つが提供されていますが、ここでは<Translate/>コンポーネントの例を示します。

公式ドキュメントの例では以下を参照してください。 https://docusaurus.io/docs/i18n/tutorial#translate-your-site

翻訳でポイントになるのは翻訳用のテキストを作成することです。 翻訳用のテキストはi18n/en/code.json,i18n/ja/code.jsonで定義します。 上記例ではcode.jsonの例がないので具体的な処理がわかりづらいのですが、ポイントはprosに記載されているidです。

ここで重要になる点はcode.jsonの作成方法ですが、Docuaurusではjsonファイルを書き出すためのスクリプト(CLI)が提供されています。 https://docusaurus.io/docs/cli#docusaurus-write-translations-sitedir https://docusaurus.io/docs/i18n/tutorial#translate-plugin-data

yarn write-translations //en
yarn write-translations --locale ja //ja

上記コマンドを実行するとi18n/ja/code.json含む、翻訳用のjsonファイルが出力されます。(環境により"message"は下記と異なる可能性があります。)

また、翻訳対象のテキストはidに対応する"message"が対象になるため、必要に応じて変更してください。

i18n/ja/code.json
{
  "homepage.title": {
    "message": "テストウェブサイト Docusaurus"
  },
  "homepage.tagline": {
    "message": "私のウェブサイトへようこそ"
  },
  "homepage.features.easy.title": {
    "message": "簡単に使える"
  },
  "homepage.features.easy.description": {
    "message": "{docusaurus}は、簡単にインストールでき、すぐにWebサイトを立ち上げることができるよう設計されています。"
  },
//
}

翻訳自体はこれで対応できますが、上記jsonには{docusaurus}という変数を記載しています。

これは<Translate/>コンポーネントではHTMLタグを含むテキストを含むことはできない仕様があります。つまり、propsはハードコードされた文字列でなければなりません。

例えば<code>docs</code>のようにcodeタグを含むテキストをレンダリングする場合は、テキストとして表示されてしまいます。この場合はTranslateコンポーネントvaluesプロパティを使用することでHTMLタグに置き換える方法で対応可能です。

下記コードの通り、index.js側で<Translate/>コンポーネントvaluesプロパティに、翻訳テキスト中の変数を置き換えるための値を指定することで、HTMLタグに置き換えることができます。

src/components/HomepageFeatures/index.js
  const translatedDescription = (
    <Translate
      id={description}
      values={{
        docusaurus: <strong>Docusaurus</strong>,
        docDir: <code>docs</code>,
      }}
    />
  );

最後に作成したファイルを記載します。

src/pages/index.js
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
import HomepageFeatures from '@site/src/components/HomepageFeatures';

import Heading from '@theme/Heading';
import styles from './index.module.css';
import Translate from '@docusaurus/Translate';

function HomepageHeader() {
  const {siteConfig} = useDocusaurusContext();
  return (
    <header className={clsx('hero hero--primary', styles.heroBanner)}>
      <div className="container">
        <Heading as="h1" className="hero__title">
          <Translate id="homepage.title">{siteConfig.title}</Translate>
        </Heading>
        <p className="hero__subtitle">
          <Translate id="homepage.tagline">{siteConfig.tagline}</Translate>
        </p>
        <div className={styles.buttons}>
          <Link
            className="button button--secondary button--lg"
            to="/docs/tutorial/intro">
            Docusaurus Tutorial - 5min ⏱️
          </Link>
        </div>
      </div>
    </header>
  );
}

export default function Home() {
  const {siteConfig} = useDocusaurusContext();
  return (
    <Layout
      title={`Hello from ${siteConfig.title}`}
      description="Description will go into a meta tag in <head />">
      <HomepageHeader />
      <main>
        <HomepageFeatures />
      </main>
    </Layout>
  );
}
src/components/HomepageFeatures/index.js
import clsx from 'clsx';
import Heading from '@theme/Heading';
import Translate from '@docusaurus/Translate';
import styles from './styles.module.css';

const FeatureList = [
  {
    title: 'homepage.features.easy.title',
    Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
    description: 'homepage.features.easy.description',
  },
  {
    title: 'homepage.features.focus.title',
    Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
    description: 'homepage.features.focus.description',
  },
  {
    title: 'homepage.features.react.title',
    Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
    description: 'homepage.features.react.description',
  },
];

function Feature({ Svg, title, description }) {
  const translatedTitle = <Translate id={title} />;
  const translatedDescription = (
    <Translate
      id={description}
      values={{
        docusaurus: <strong>Docusaurus</strong>,
        docDir: <code>docs</code>,
      }}
    />
  );

  return (
    <div className={clsx('col col--4')}>
      <div className="text--center">
        <Svg className={styles.featureSvg} role="img" />
      </div>
      <div className="text--center padding-horiz--md">
        <Heading as="h3">{translatedTitle}</Heading>
        <p>{translatedDescription}</p>
      </div>
    </div>
  );
}

export default function HomepageFeatures() {
  return (
    <section className={styles.features}>
      <div className="container">
        <div className="row">
          {FeatureList.map((props, idx) => (
            <Feature key={idx} {...props} />
          ))}
        </div>
      </div>
    </section>
  );
}

トップページ(pages)の言語切り替えの設定としては以上です。

ブログ(blog)の言語切り替え

ブログはdocsと同様に記事自体をブログ用のmarkdownファイルと著者ファイルのauthors.ymlを作成することで切り替えることができます。 https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog#translation-files-location

しかし、ブログの設定ではブログ本体とは別にいくつか設定(configuration)があります。
https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog#configuration

この中で、サイドバーのタイトルについて翻訳の設定例を紹介します。

サイドバーのタイトル変更

まず、ブログのi18n設定は以下に情報が記載されています。
https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog#i18n

ここでブログ本文は.mdファイルを作成すれば切り替えることができますが、サイドバーのタイトル(デフォルトではRecent Posts)は変わりません。

ブログの設定は下記に記載されており、サイドバーのタイトル設定は下記です。
https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog#blogSidebarTitle

単純にサイドバーのタイトルを変更したいのであればdocusaurus.config.jsblogSidebarTitleを追加すれば変更できます。

/** @type {import('@docusaurus/types').Config} */
const config = {
  //省略

  presets: [
    [
      'classic',
      /** @type {import('@docusaurus/preset-classic').Options} */
      ({
        docs: {
          // docs以下のindex.mdを表示する場合はrouteBasePathを指定する
          //routeBasePath: '/',
          sidebarPath: './sidebars.js',
          // Please change this to your repo.
          // Remove this to remove the "edit this page" links.
          editUrl:
            'https://github.com/7rikazhexde/test-website-docusaurus/edit/main',
        },
        blog: {
          showReadingTime: true,
          blogSidebarTitle: 'ブログ一覧', // ここを追加
          // Please change this to your repo.
          // Remove this to remove the "edit this page" links.
          editUrl:
            'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
        },
        theme: {
          customCss: './src/css/custom.css',
        },
      }),
    ],
  ],

しかし、これではi18nのプルダウンで言語を変更しても翻訳はされません。 翻訳するためには、options.jsonが必要になります。

options.jsonトップページ(pages)の言語切り替えでも紹介した通り、CLI(write-translations)で作成することができます。

options.jsonについては下記ドキュメントにも記載されています。
https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog#example-file-system-structure

│ # translations for the plugin options that will be rendered

サイドバーのタイトルの設定(一部)は下記の通りです。

i18n/ja/docusaurus-plugin-content-blog/options.json
{
  "sidebar.title": {
    "message": "最近の投稿", 
    "description": "The blog sidebar title"
  }
}
i18n/en/docusaurus-plugin-content-blog/options.json
{
  "sidebar.title": {
    "message": "Recent Posts", 
    "description": "The blog sidebar title"
  }
}

ブログ(blog)の言語切り替えの設定としては以上です。

ページ左上のDocsへのリンクはnavbarやページ下部のSNSへのリンクはfooterで設定できます。 デフォルト言語の表示はdocusaurus.config.jsthemeConfig(footer,navbar)で設定できます。
https://docusaurus.io/docs/api/themes/configuration#navbar https://docusaurus.io/docs/api/themes/configuration#footer-1

一方で、i18n設定ではfooterとnavbarは以下のファイルで設定できます。
https://docusaurus.io/docs/api/themes/configuration#i18n

  • i18n/ja/docusaurus-theme-classic/footer.json
  • i18n/ja/docusaurus-theme-classic/navbar.json

ここで、上記jsonファイルは、トップページ(pages)の言語切り替えでも紹介した通り、CLI(write-translations)で作成することができます。翻訳対象のテキストはid(Theme configurationではtype)に対応する"message"が対象になるため、必要に応じて変更してください。

まとめ

Docusaurusでi18n設定する方法について以下の項目について紹介しました。

  • docs
  • pages
  • blog
  • theme(footer / navbar)

基本的には本記事の内容で基本的な翻訳は対応できると思います。 また、Docusaurusでは他にも機能があるので気になるものがあれば情報としてまとめようと思います。

最後に、本記事参考になれば、
是非「はてなスター / はてなブックマーク」 をお願いします。

以上です。

GitHub ActionsとGitHub Pagesを使用してpytest-htmlで生成されたレポートを公開する方法

要約

以下内容について記載します。

  • プログラムのテストとPytestについて
  • pytest-htmlのプラグインについて
  • GitHub ActionsとGitHub Pagesを使用してpytest-htmlで生成されたレポートを公開する方法について

はじめに

プログラムのテストでは以下の分類があります。

これらは一般的にプログラミングにおいて共通しています。そしてPython ではUT/IT(一部)をテストフレームワークであるPytestを使用することでテストできます。

また、テストはアーキテクチャ、モジュール、関数、メソッドによってテスト設計対象は異なります。

一般的に、UTでは関数、メソッドについて、原則としてすべてのロジックパスを一度は通るようなテストケースを作成し、検証すること(網羅率:100%)が必要ですが、FTはITの分類の一つであり、目的に応じてテスト設計が必要という認識です。

これらの解釈については下記の記事(Integration and Functional Tests)が参考になりますので共有させていただきます。

zenn.dev

また、pytestでは拡張機能としてプラグインも開発されています。
一例は下記の通りです。

  1. pytest-cov: コードのカバレッジを計測し、レポートを生成する。
  2. pytest-html: テスト結果をHTML形式で出力し、視覚的なレポートを生成する。
  3. pytest-xdist: テストを並列で実行し、処理時間を短縮する。
  4. pytest-mock: テスト中にモックを簡単に作成し、モックオブジェクトを利用できる。

この中でテスト結果の確認ではpytest-htmlが有用です。コードと合わせてコミットすることでテスト結果をファイルとして管理/確認することができます。

他にも、カバレッジ率の確認ついては下記記事が参考になりますので、共有させていただきます。実際に私も使用しています。

rcmdnk.com

pytest-htmlについて、レポート作成は有用ではあるのですが、
それはブラウザでHTMLを表示できるためです。

この点について、例えば、GitHubではコミットされたHTMLはブラウザ表示には対応していません。(あくまでコード管理のみ)

これではHTMLを生成しても視覚的に確認できなければプラグインを十分に活用できているとは言えません。

そこで、実現方法について調べたところ、GitHub Pape1を使用することでウェブサイトとして表示できることがわかりました。

前置きが長くなりましたが、本記事では調査した内容を元に、
GitHub ActionsとGitHub Pagesを使用してpytest-htmlで生成されたレポートを公開する方法をまとめたので、紹介します。

作成したもの

コードは四則演算をするシンプルなものですが、これをpytest-htmlを使用してテスト結果のレポートをHTMLで作成し、GitHub Papesへのデプロイします。
これらの処理はワークフローとして作成して実行します。

注意事項として、下記リポジトリは実験用のもので非定期に更新しますので、使用/参照する際はご注意ください。

実例については、別プロジェクトのvideo-grid-mergeリポジトリを参照いただいた方が良いかと思いますが、本記事では以下のリポジトリ例で示します。

github.com

また、公開したレポートはREADME.mdからアクセスできるようにshieldsでバッジを作成してアクセスできるようにしています。

ubuntu_latest macos-12 windows-latest

使い方と詳細

後述するワークフローファイルをコミットすることでテスト結果のHTMLを作成し、GitHub Papesへのデプロイをすることができます。しかし、使用するためにはいくつか設定が必要です。

まずは、GitHub Pages用にブランチを作る必要があります。 ワークフローではブランチ名はghpagesとします。そして、ghpagesブランチをデプロイ先のブランチに指定します。

ghpagesはブランチ元のコードが存在する場合は、.git以外を削除してコミットしてください。

ghpagesブランチをデプロイ先のブランチ指定する方法

次に、pagesから下記項目でデプロイ先のブランチ(ghpages)を指定します。

github-pages_deploy設定

ghpagesへデプロイするためのGitHub Actionの設定

次に、以下のGitHub Action用のワークフローファイル(.yml)をmainブランチの.github/workflowsに格納してください。

以下の例ではOSとしてUbuntu,Mac,Windows(Server)で実行する例を示します。

github.com

ワークフローファイル

ジョブを実行方法として2例を示します。
osには、ubuntu-latest, macos-12, windows-latestを指定して実行します。

ワークフローでは以下のline-notifyのアクションを使用しています。不要であればコメントアウトしてください。

使用する場合は変数を作成して設定する必要がありますので、
Settings > Secrets に LINE Notify で取得したアクセストークンを LINE_ACCESS_TOKEN を設定してください。

github.com

jobs / needs指定による直列実行する例

jobsでジョブを作成し、それをneedsで指定すると、指定したジョブの完了を待って次のジョブを実行するように制御できます。詳細は下記を参照ください。

docs.github.com

strategy / matrix指定による並列実行する例

strategy / matrixを指定することで、ジョブを変数の可能な組み合わせごとに実行するように制御できます。詳細は下記を参照ください。

docs.github.com

実行結果

jobs / needs指定による直列実行の実行結果例は下記の通りです。

github.com

ワークフローでは、strategy / matrixで並列動作もできますが、needs1jobずつ順番に実行したほうが良いと個人的には思います。

理由は、GitHub Actionsでは、デプロイ用のワークフローを設定しない場合、デフォルトの設定では、同じリポジトリに対して複数のデプロイが同時に行われると、一部のデプロイがキャンセルされることがあります。
これは、GitHubがリソースを効率的に管理し、最新のデプロイを優先するための仕組みのためのようです。

strategy / matrix指定による並列実行の実行結果例は下記の通りです。

pages build and deployment workflow runs

pages build and deployment

上記workflow runsの状態を確認すると「!」マークが表示されていますが、中身を確認すると、エラー内容としてpages build and deployment@ghpagesの優先度の高い待ちリクエストが存在するため、キャンセルしますとあります。最新のデプロイを優先するためこのようなエラーが表示されているという理解です。

まとめ

GitHub ActionsとGitHub Pagesを使用してpytest-htmlで生成されたレポートを公開する方法として、

  • テストの概要とpytest-htmlの紹介
  • 実例の紹介(GitHub ActionsとGitHub Pagesの設定と使い方、実行結果)

ワークフローは正直分岐が多く見づらい部分もありますが、HTMLの可視化はできているので興味があればフォークをして活用してもらえればと思います。

最後に本記事が参考になれば「はてなスター」,「はてなブックマーク」をお願いします。


  1. GitHubリポジトリに格納されたHTML、CSSJavaScriptファイルを使用して静的ウェブサイトをホストするための機能であるGitHub Papesを使用してGitHubにコミットされたHTMLファイルをインターネット上で直接表示する機能

Manta Networkでのステーキング方法について

久々に暗号資産の記事を投稿します。
暗号資産について、2024年はModular Blockchain,DePin,AI関連のトークンに注目しています。

日本の取引所では2024/02/23現在で匿名性のトークンは取り扱われていませんが、プライバシーに配慮したトークンにも注目しています。
特にゼロ知識証明(Zero-Knowledge Proof)によるユースケースが増えることに期待しています。

medium.com

また、元々私はPolkadotに関心があり、プライバシーを考慮した仕組みを持つManta Networkにも注目していました。Manta Networkのパラチェーンオークションには参加していませんが、2023年にはL2向けのManta Pacificが開発され、2024年にはバイナンスでもトークンが扱われるようになりました。

本記事では調査したManta Networkについての概要と、ステーキング方法について紹介します。

概要

  • Manta Networkについて
  • Manta Pacific(L2:イーサリアムLayer2ネットワーク/Manta Pacific L2 Rollup)からManta Atlantic(L1:PolkadotパラチェーンのMoonbeam)へのブリッジ
  • Manta Atlanticでのステーキング方法

Manta Networkについて

Manta Networkは、web3のためのモジュラー型エコシステムを提供しており、二つの主要な部分、Manta PacificとManta Atlanticに分かれています。

参考記事: プライバシーを提供するブロックチェーン「Manta Network(MANTA)」の解説

Manta Network

Manta Pacific

Manta PacificはModular Blockchainに分類されるレイヤー2です。詳細は下記リンクを参照ください。

MantaトークンはERC-20, Native1を持つトークンで、
Manta Pacific L2 Rollupネットワークをメタマスクに追加することでManta Pacificチェーンとしてブリッジや後述するステーキング(L1/XCMへのブリッジが必要)をできるようになります。

Manta Atlantic

Manta Atlanticはプライバシーを重視しかつコンプライアンスに対応することが可能な、高速なZKレイヤー1です。詳細は下記リンクを参照ください。

Manta PacificからManta Atlanticへのブリッジ

公式ドキュメントのBridging $MANTA between Pacific and Atlanticに記載されていますが、ブリッジにはいくつかの工程があります。まとめると下記です。

  • トークンはL2(Ethereumネットワーク:Manta Pacific) からL1(Polkadotネットワーク:Manta Atlantic / XCM Bridge / Moonbeam)に移動させるために、ブリッジを使用する。
  • L2ではネイティブトークンとして$MANTAが存在する状態。
  • ブリッジを使用すると、Moonbeamネットワーク2を介してxcMantaトークンが生成される。(L2->L1)
  • xcMantaは、Manta Atlantic上で使用できる形式のトークンとなる。

Manta Atlanticでのステーキング

詳細は公式ドキュメントのルールにまとめられていますが、ポイントは以下です。(2024/02/23現在)

  • StakeにはMinimum Stake以上が必要
  • CollatorへのDelegateは1カウント25件まで
  • 報酬対象はCollatorが上位63位内
  • 報酬対象のDelegatorsは1Collatorにつき上位100位内
  • 報酬はラウンドと呼ばれる6時間間隔で計算/付与される
  • 手動のClaimは不要
  • Delegatorsの解除または縮小には7日間の待機期間が必要

Collator数はSubscan Explorer / Manta / Acountから確認できます。(2024/02/23現在)

Manta Atlantic Subscan Manta AccountRole

Delegatorsのランキングは下記Stakingページのリンクから確認できます。 https://app.manta.network/manta/stake

Manta Atlantic Staking Status

ステーキングの流れ

Manta Atlanticアプリからステーキングする場合

Manta Pacific(L2)からManta Atlantic(L1)にブリッジしてxcMantaトークンを取得する

xcMantaトークンはMoonbeamネットワーク経由で取得するため、GLMRトークンが必要になりますが、自動で付与されるので事前に準備しておく必要はありません。

また、MantaトークンをManta PacificからMoonbeamに送金する際には最大10分の時間が必要になります。転送途中で止めることはできないため、途中ブラウザを閉じるとGOXしますので注意してください。

Bridge from Manta Pacific to Manta Atlantic

補足として、上記画像ではガス代が不足していますが、ETHがManta Pacific L2 Rollupネットワークに必要になります。イーサリアムメインネットからブリッジでは個人的には比較的ガス代が安いLayerswapをおすすめします。

xcManta

コレーターを選択しステーキングする

ステーキングはNova Walletでも指定できますが、2024/02/23現在ではコレーターの追加には対応していないようです。

Manta Atlantic Staking

BIfrostアプリによるステーキング方法

MantaのステーキングはBIfrostアプリからもできます。

私はBifrostアプリからのステーキングは確認していないので詳細は紹介できませんが、Manta Networkの日本コミュニティが公開しているNotionページ(Bifrostにおける$MANTA リキッドステーキング チュートリアル)にて紹介されていますので、こちらを参照してください。BIfrostアプリではswapやfarmingもできるようです。

まとめ

以下内容について紹介しました。

  • Manta Networkについて
  • Manta Pacific(L2:イーサリアムLayer2ネットワーク/Manta Pacific L2 Rollup)からManta Atlantic(L1:PolkadotパラチェーンのMoonbeam)へのブリッジ
  • Manta Atlanticでのステーキング方法

Manta Networkの今後については、ロードマップトークンで記事が公開されていますが、zkEVMに移行して開発環境の整備を進めてMainnetの完了となるようです。

ここは同様にパラチェーンオークションに参加したASTRと共通していますが、Manta PacificはUniversal Circuits(可能汎用ZK回路)をさらにアップグレードして、ガスコストの削減、より多くのユースケースのサポート、さらに優れたユーザーエクスペリエンスの提供を可能にすることを目指しているようです。

より多くのユースケースに見合ったZKアプリケーションが作成されることを期待します。

以上です。


  1. MantaのNativeトークンはBifrost appでswapする方法で取得可能かも知れませんが、動作確認はしていないため結果は保証しません。
  2. Moonbeamは、Ethereumと完全に互換性を持つスマートコントラクトを展開するPolkadot系プロジェクトです。

ObsidianとShortcutsアプリを使用して簡単に日記作成する環境を整えた話

はじめに


2024年が始まりました。
年始早々、令和6年能登半島地震があり、心落ち着かない年の始まりとなりましたが、被害を受けた方には1日でも早く普段の生活に戻れるよう願っています。


昨年、以下の記事で2023年の振り返り記事を投稿し、
その中で、「月次の投稿もしていきたい」ということを記載しました。

ただ、記事を書く際、私の場合は、

  • 何をやったか思い出せない。
  • 明日から日記を書くぞと意気込んでも、3日坊主で気づいたら書かなくなる。

そんな状況が続き、気づくと2023年が終わってしまいました…。

そこで、どうしたら続けられるかなと考えたところ、
ObsidianとShortcutsアプリを使えば続けられそうだ、という環境を作れたので本記事で紹介することにしました。

モチベーション

日記をアナログで考えるとノートとペンがあれば書けますが、それをデジタルに置き換えた場合、それはアプリとキー入力に相当すると考えます。

次に日記を書くまでの工程をアナログとデジタルで考えてみます。

アナログの場合:
1️⃣ノートを出す > 2️⃣日記ページを開く > 3️⃣日記を書く

デジタルの場合:
1️⃣アプリを開く > 2️⃣日記ページを開く > 3️⃣日記を書く(キーを入力する)

工程数は同じですが、アナログでは栞を挟むなどすれば1️⃣と2️⃣を省くこともできます。しかし、デジタルの場合はどうでしょうか?

3️⃣で入力をショートカットキーなどで作成すれば、効率的に日記を書くことはできますが、1️⃣と2️⃣では思っているよりも、手間がかかると感じています。(少なくとも私の性格ではここをクリアしないと書かないです。)

そこで、1️⃣、2️⃣、3️⃣の操作をなるべく短く、簡単に実行できるような方法を考えることにしました。

作成したもの

iPhone/iPad/Mac限定ですが、ShortcutsMarkdownメモアプリであるObsidianを使用するショートカットを作成しました。

ショートカットは以下のiCloudリンクから取得できます。

ObsidianとShortcutsアプリのインストールと後述するvaultの設定、日記保存先のフォルダを設定することでショートカットを1度実行するだけで日記の記入から保存まで実行できます。

www.icloud.com

処理の流れ

3️⃣, 1️⃣, 2️⃣をショートカットで順に実行します。
結果、ショートカットを1度実行するだけで日記の記入から保存まで実行します。

  • その日に初めて日記を書く場合

    • 指定入力(日記内容)をYYYY-MM-dd.txtで保存
    • YYYY-MM-dd.txtをObsidianがメモを保存するローカルファイルシステムのフォルダーであるvaultの日記保存用のフォルダに保存
    • YYYY-MM-dd.txtYYYY-MM-dd.mdに変換1
  • すでに日記(YYYY-MM-dd.md)が存在する場合(例:日記保存用のフォルダに2023-12-17.mdがすでに保存されている)

    • 指定入力(日記内容)とYYYY-MM-dd.mdの中身を取得し、結合
    • YYYY-MM-dd.mdで上書き保存

日記記入(Obsidian)ショートカット

日記記入(Obsidian)

取得結果

以前投稿したものと同じですが、処理の流れで記載した内容で日記を保存できていること確認できると思います。

Community plugins / Daily-Notes-Editorについて

Obsidianで日記(YYYY-MM-dd.md)を作成する場合、以下のDaily-Notes-Editorを追加するとブログ形式で表示できます。
便利な機能ですのでショートカットと合わせて使うことをお勧めします。

github.com

まとめ

ObsidianとShortcutsアプリ使用して簡単に日記を作成する方法について紹介しました。

ショートカットで記入した内容はObsidianに読み込まれるため、タグやボックスの入力も反映されます。

気になればショートカットを追加して使ってみてください。

また、記事が参考になれば、是非「はてなスター」もお願いします!

以上です。


  1. .txtから.mdの変換はYYYY-MM-dd.mdを指定しても.txtで保存されるので変換します。