Tech系サービスやガジェットの使い心地、自分の作業環境、資産運用について気が向いたときに記録を残しています。

記事内のAmazonアソシエイト適格販売及び、Google Adsenseでお小遣いを得ています。

Nest Cam(Battery)にソーラーパネルを追加して電源不要の監視カメラを実現する

Nest Cam(バッテリー)

store.google.com

Nest CamはGoogleストアで販売しているスマート監視カメラ。 「壁とマウント」「マウントと本体」がマグネットで吸着するため、鉄製の壁なら加工なし、通常の住宅の壁でも強力な下地なしに鉄板さえ追加で張り付ければどこにでも、無段階で角度をつけて取り付けができます。

この無段階というのがミソで、一般的な屋外監視カメラでは、首振りの角度で設置場所の制限が発生するところ、Nest Camはほぼどこにでも設置できて好みの画角に設定できるというメリットがあります。

AnkerやRingのカメラは、上下チルトや左右の振りに制限があり、追加でマウントを買わないと自由度が確保できません。

Pixelシリーズを買った時に大量に付与されるクレジットの使い道として購入して使っていました。デフォルトでは過去3時間のイベントごと録画しか記録できないため、Nest Awareはほぼ必須でしょう。

800円/月に値上がりしたのは痛いですが、顔認識や動画保存はNest Camのメリットを享受するために必須(というよりこれらがなければAnkerやRingでよい)です。

高度な自動化

Google Homeアプリでの自動化にカメラトリガーは実装されていませんが、ベータ版のスクリプトエディタからカメラの人物認識をトリガーにアクションを実行できます。

HomeのスクリプトエディタはWeb版のみアクセス可能です。これで帰宅したら自室の明かりがONになるように設定しています。

home.google.com

スピーカーから任意の音声が流れるようにしても面白いですね。

充電、取り外し、エリア再設定

Nest Awareを契約していると、人物以外にもカメラの物体検知がトリガーされた際にスマートフォンアプリで通知を受けられるのですが、大抵の場合は自宅の敷地外もカメラの映像範囲内に入っていると思います。

そこはどのメーカーでもあると思うのですが、カメラの撮影範囲内の特定のエリアを指定して、通知の有無を設定できます。

これで、自宅敷地内に人が映った場合のみ通知するという、一般的な監視カメラに求められる機能が実現できます。

通知用の検知エリアを設定できるが、充電すると画角が変わるので再設定必須

ただ、Nest Camは本体とバッテリーが一体化しているので、充電の際は本体を取り外してバッテリーを室内で充電する必要があります。

常時電源用のケーブルはあるのですが、AC側が無駄に大きく、一般的な日本家屋の防水コンセントには装着できません。ちゃんとフィードバック受けてるのか?

store.google.com

しかも、充電のたびに取り外して、充電後に再度マグネット装着をすると画角が変わってしまいます。そうすると、前述のエリア設定も座標がずれてしまうので再設定が必要に…

大体2~3か月でバッテリーがゼロになるので、そのたびに再設定です。めんどい。

ソーラーパネルで充電から解放

store.google.com

さすがに半年ほど運用して面倒になったので、ソーラーパネルで充電頻度を下げる作戦に出ました。

代償として、1日中日陰の位置には設置できなくなりますが、幸い我が家では排水パイプに装着できそうでした。

パイプは樹脂なので、防犯カメラブラケットを装着してそこにソーラーパネルとNest Camを装着しています。

ブラケットが鉄製なので、Nest Camはぺたっと貼り付けるだけでOKなのがうれしい。

夏場はカメラに高熱アラートが出ていましたが、ソーラーパネルで隠すことで夏場の熱問題の解決も狙ってます。付属ケーブルが無駄に長いですが、日差しから隠れるように巻いてます。

ソーラーパネルによるバッテリー寿命無限大へ

ソーラーパネルを接続するとバッテリーレベルが見えなくなる謎仕様

設置場所は南西側で、南側には隣家があるため冬場の直射日光は午後の数時間しか当たらないという環境。

それでも、10月~2月の間でバッテリーが一度も0にならないという快挙を達成!

寒気が強い日は低温アラートが来ることもありましたが、録画は常に継続できています。夏場はこれからですが、熱による問題を除けば冬より発電量は多いはずなので、年間無充電を計画しています。

世界最速でHonoをAmplify Hostingで動かしてみた

前置き

筆者はAWSジャパンのソリューションアーキテクトです。本記事は個人的な興味関心による検証記事となります。

普段は常体で書いているこのブログですが、本記事は書きやすさから敬体で書いていきます。

AmplifyにおけるNext.js"以外"のSSRサポート

これまで、Next.jsのSSRのみをサポートしていましたが、Nuxt.jsのSSRサポートと同時に、node.jsで実行できる任意のフレームワークSSRアプリケーションを実行できるようになりました。

aws.amazon.com

ドキュメントには、Amplify Hosting用のディレクトリ構成が記載されており、ビルド成果物をこのディレクトリ構成に合わせて調整し、 deploy-manifest.json に設定を記載してやることで任意のエントリポイントからアプリケーションを起動できます。

※2023/11/21時点ではドキュメントは英語版のみ更新されています

docs.aws.amazon.com

Next.js および Nuxt.js 以外は公式アダプタが提供されておらず、この記事を書いている時点では自分でこれらの設定を記述する必要があるので、勉強がてら自分でアダプターを設定して作ってみました。

直近で一番利用頻度が高いフレームワークと言うことで、Astro SSRを試してみたかったところですが、エムスリーさんに先を越されてしまったため、別のフレームワークで試してみることにしました。

そこで、軽量かつ、エッジランタイムで爆速なWebフレームワーク、HonoをAmplify Hostingにデプロイしていきます。

github.com

なお、本記事で利用しているコードは以下のリポジトリで公開しています。

github.com

Hono Node.js Adapter

Honoといえば、専ら話題になるのはCloudflare Workers, Deno, Bunなどのエッジランタイムでの動作ですが、LambdaやLambda@Edgeをはじめとする一般的なnode環境でも使うことができます。

Amplify HostingのSSRはnode.jsで動いているように読めるので、今回はこのアダプターを使ってHonoプロジェクトを作成していきます。

ドキュメントのQuickStartに従ってプロジェクトを作成します。

npm create hono@latest
>nodejs

SSRであることがわかりやすくなるよう、ヘッダからUser-Agentを表示するロジックを追加して、localhostで動作確認します。

//src/index.ts
import { serve } from "@hono/node-server"
import { Hono } from "hono"

const app = new Hono()
app.get("/", c => {
  const userAgent = c.req.raw.headers.get("User-Agent")
  return c.text(`Hello Hono!\nUA: ${userAgent}`)
})

serve(app)

開発サーバーを起動した様子

うまくいきました。飾り気がないですが動作確認には十分なのでこのまま進めていきます。

Amplify Hosting用の設定ファイルを追加する

ドキュメントにExpressサーバー用のサンプルが記載されているので、これを参考にしながら設定していきます。

docs.aws.amazon.com

Amplify Hostingの設定については、ほぼエムスリーさんのブログの通りなので省略します。

www.m3tech.blog

たまたまローカルのnodeバージョンが18になっていたので、私は「ライブパッケージの更新」で Node.js version に18を設定しました。

tsxでentrypointを叩けるのか?

create hono で作成したサンプルはtsxを使って内部的にesbuildを使ったトランスパイルをしてTypescriptを実行しており、プロジェクトディレクトリに明示的にトランスパイル後のファイルが出てきません。

これは、感覚としてはTypeScriptをそのまま実行できており、開発者体験もよいので、「tsxでの実行をそのままAmplify Hostingに持ち込めたらいいなぁ~~!」くらいの気持ちで、ソースをそのまま.amplify-hostingディレクトリにコピーしてentrypointにindex.tsを指定してみました。

npm serveを自動的に認識していい感じに動いてくれればもうけものです。

//deploy-manifest.json
{
  "version": 1,
  "framework": { "name": "hono", "version": "3.10.2" },
  "imageSettings": {
    "sizes": [100, 200, 1920],
    "domains": [],
    "remotePatterns": [],
    "formats": [],
    "minimumCacheTTL": 60,
    "dangerouslyAllowSVG": false
  },
  "routes": [
    {
      "path": "/_amplify/image",
      "target": {
        "kind": "ImageOptimization",
        "cacheControl": "public, max-age=3600, immutable"
      }
    },
    {
      "path": "/*.*",
      "target": {
        "kind": "Static",
        "cacheControl": "public, max-age=2"
      },
      "fallback": {
        "kind": "Compute",
        "src": "default"
      }
    },
    {
      "path": "/*",
      "target": {
        "kind": "Compute",
        "src": "default"
      }
    }
  ],
  "computeResources": [
    {
      "name": "default",
      "runtime": "nodejs18.x",
      "entrypoint": "index.ts" ←
    }
  ]
}

ドキュメント通りにpostbuild.shも作りますが、ビルドせずにソースをそのままコピーしてみます。

# postbuild.sh
#!/bin/bash

rm -rf ./.amplify-hosting

mkdir -p ./.amplify-hosting/compute

# 改変
cp -r ./src ./.amplify-hosting/compute/default 
cp -r ./node_modules ./.amplify-hosting/compute/default/node_modules

cp -r public ./.amplify-hosting/static

cp deploy-manifest.json ./.amplify-hosting/deploy-manifest.json

ビルドは成功したのですが、動きませんでした。そこまで都合よくはいきません…

jsファイルにトランスパイルする処理を追加していきます。

Honoのtsファイルをトランスパイルする

//tsconfig.json
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}
//package.json
{
  "type": "commonjs",
  "scripts": {
    "start": "tsx watch src/index.ts",
    "build": "tsc",
    "serve": "tsx watch src/index.ts",
    "postbuild": "chmod +x bin/postbuild.sh && ./bin/postbuild.sh"
  },
  "dependencies": {
    "@hono/node-server": "^1.2.3",
    "hono": "^3.10.2"
  },
  "devDependencies": {
    "@types/node": "^20.9.3",
    "tsx": "^3.12.2",
    "typescript": "^5.3.2"
  }
}

ビルドコマンドにtscを指定しつつ、ESmoduleとして認識されないように"type":"commonjs"を追加します。

tsconfigは、ドキュメントのものをそのままコピペして、postbuild.shdeploy-manifest.json もサンプル通りに修正しまして、無事デプロイが通りUser-Agentが表示されたことを確認出来ました。

オリジンの前にAmplifyがCloudfrontを挟んでアクセスさせているため、UAもCloudFrontと表示されています。

Astro MDX integrationで画像をレスポンシブ対応にする方法

自転車ブログをGatsbyからAstroに書き換えた。

blog.gensobunya.net

新興フレームワークだけあり、Gatsbyのように豊富なプラグインによる便利な設定というものはないので、殆どの機能を自分で実装する必要がある。

Head APIがなく、レイアウトコンポーネントから必要な要素を全てPropsで渡してheadタグに記述したり、クライアント処理をAstroでやるのはつらいのでReactを使ったりと、Gatsbyに慣れているといろいろ面倒なところが多く感じた。

その中でも、ナレッジが少なく(というかundocumented)でデフォルト処理してくれない、MDX内に相対パスで記述した画像をレスポンシブ化する部分だけ抜粋してメモ。

背景

AstroにはContent Collectionという機能があり、srcディレクトリ内に配置された画像を自動的にフォーマット変換・レスポンシブ対応で自動的に複数サイズの画像をsharpレンダリングする機能がある。

.astroコンポーネントの場合は、組み込みの<Image>, <Picture>タグを通じてこの最適化をコントロールできる。

docs.astro.build

一方で、このContent Collectionは.md, .mdxファイル内の画像リンクも最適化してくれるのだが、この最適化は最適化と名ばかりのオリジナルサイズのwebpを吐き出すだけである(しかもフォールバックもない)。

Content Collectionには(ドキュメントを読む限り)グローバル設定が存在しないうえ、gatsby-plugin-image のようなキャプション設定機能も存在していないため、自前でこれらを実装する必要がある。

Astroの書き味はNext.jsに似ていて、シンプルなコア機能やコンポーネントを提供する方針のようだ。

前提

  • Astro.3.x
  • Content Collection
  • @astro/mdx integration >1.1.0

実装

実は、@astro/mdx インテグレーションにはCustom Componentのマッピングimgを指定した場合、ContentCollectionによって処理された画像に限り AstroのImageMetadataタイプがPropsとして渡される仕様になっている。

github.com

これを知っていると非常にシンプルに記述できる。この情報にたどり着くまでは自分でrehype, remarkプラグインを書かねばならないのかと絶望していたところ。

---
//MdxPicture.astro
import type { ImageMetadata } from "astro"
import { Picture } from "astro:assets"
type Props = {
  src: string | ImageMetadata
  alt: string
}
const { src, alt } = Astro.props
---

{
  typeof src === "string" ? (
    <figure>
      <img class="mx-auto" src={src} alt={alt} />
      <figcaption class="not-prose text-center text-xs text-secondary md:text-sm">
        {alt}
      </figcaption>
    </figure>
  ) : (
    <figure>
      <Picture
        formats={["webp"]}
        fallbackFormat="jpg"
        widths={[360, 752]}
        class="mx-auto"
        {src}
        {alt}
      />
      <figcaption class="not-prose text-center text-xs text-secondary md:text-sm">
        {alt}
      </figcaption>
    </figure>
  )
}
//[...slug].astro
---
~
~
import { MdxPicture } from "~~~"
~
~
const { Content } = await post.render()
---

  <Content
    components={{
      ~
      ~
      img: MdxPicture
    }}
  />


componentsとして渡すときに特段Propsとして明示的に渡す必要がないところがポイント。

ドキュメントに書く価値のある情報のはずなのだが…

デスクトップPCを Flexispot CPUスタンド CH1 で自作昇降デスクに吊る

PCを吊ることで、昇降時のケーブル長やルンバ衝突などの懸念を解消

プライベート用のPCをTsukumoのBTO PC(G-GEAR)に変更。

それにともない、デスク周りの環境を変更した。PC本体を床置きしてしまうと、ルンバと衝突したり、Flexispot の昇降に合わせたケーブル長を確保する必要があったりと、いろいろ不都合がある。

また、仕事用のPCは変わらずノートなので、モニターを含めた周辺機器を共有はマスト。

いろいろ手段を検討したが、シンプルに天板と一体固定してしまうのが早かったので、Flexispot の CH1 を使って天板下に吊るすことにした。

Flexispot CH1 取り付け

メーカー純正の組み合わせだが、実際は直付け式

純正品ということで、取り付けが簡単であることを期待したが、届いた説明書を読んだところ、特にFlexispot本体と共締めするような機構はなく、天板に直接ねじ止めが必要だった。

若干の失望を持ちつつも、天板からPC上部のオフセットは昇降デスク本体のケーブルトレイとは接触しないように設計されていたので、気を取り直して作業していく。

特に難しい点はないのでマーキングして下穴を開ける

作業自体はとても簡単で、2mmのドリルで下穴を開け、本体をねじ止めするだけ。脚の取り付けに比べれば一瞬だ。

デスク下のケーブルカバーや、昇降スイッチとの干渉もないのは流石純正。特にケーブルカバーとのクリアランスは少しだけ余裕を持たせつつ、かなり攻めている。

下準備:デスクトップPCにThunderbolt 4カードを増設

周辺機器共有はノートPCの場合、USB Type-C (10Gbps以上)のポートからドックを介して接続しておけば、ドックの接続先を変更するだけで済む。

自分の場合は、HP Thunderbolt 3 Dock 120W G2をずっと使っていた。

gensobunya-tech.hatenablog.com

ポート数・独自電源・HP製PC向けの電源連動など、スペックもいいし当時は非常に安価だった。(残念ながら今は廃盤)

一方で、タワー型のデスクトップPCにはThunderbolt端子が装備されていることは少ない。ノートPCやミニPCと違って、インターフェースが豊富なので基本的には不要だからだろう。

とはいえ、現在の作業環境はすべてこのドックを介しているので、この良さはそのまま流用したい。そこで、対応マザボを使っているBTOということで、G-GEARに白羽の矢を立てた。

また、ツクモBTO PCは販売している状態から機器を増設しても、組み立て手順に問題がなければ保証が継続するという、流石の老舗サポートだった点も大きい。

購入したPCのマザーボードは「ASRock B660 Pro RS」。BTOオリジナルモデルだったが、変更点はSSDヒートシンクのみ、いわゆる去勢はされていないと確認して購入した。

TB4カード互換性

Thunderboltカードをを増設する場合(ASUSとASRockしか見ていないが)、単純なUSB接続だけでなく、Thunderboltヘッダと呼ばれるものに増設カードを接続する必要がある。

つまり、マザーボード側もThunderboltカードの増設に対応している必要がある。B660にThunderboltヘッダはあるのだが、現行品のThunderbolt 4 AIC R2.0の対応品にB660 Pro RSの表記がない…?

www.asrock.com

サポートを切られたのかと思いきや、公式アカウント曰く更新が間に合っていないだけとのこと。

手順書通り取り付け、ドライバをインストールしたところ、USB機器・ディスプレイ共に問題なくドック経由で認識するようになった

ドックのケーブル交換

HP Thunderbolt 3 Dock 120W G2は、本体からケーブルが生えているように見えて、実はケーブルを交換可能。

先駆者のブログを参考に、底部のカバーを外して交換。はずして分かったのだが、底部ポートからまっすぐケーブルが出ていないので将来的にかならず断線する仕様…自分のケーブルも被覆が破れていた。

www.buzzyvox.com

Thunderbolt4からは、アクティブケーブル・パッシブケーブルをユーザーが意識する必要がなくなったため、遠慮なく2mのThunderbolt4ケーブルを購入して交換した。

ここは大容量のデータが流れるので、ケチらず信頼性の高いメーカーを使う。

まとめ

デスクトップ↔ノートPCでのケーブル付け替えだけで作業環境のスイッチが完了する状態を構成した。

懸念はケーブルの付け替えが面倒で、毎回デスク下にもぐる必要がある点。Thunderbolt 4 スイッチャーや、Thunderbolt ハブがあれば、手元だけでの変更も可能だが、現状2万円もかける価値があるかといわれると微妙…

Notion + Markdown + VivlioStyle で共著の同人誌を作成する

ここ数年、自分の同人サークルである幻想サイクルの同人誌作成では、vivliostyleを使って原稿を作っている。

vivliostyle.org

組版アプリケーションを使っていて「このオプション、もう少し細かく設定できたらなぁ……」と思ったことはありませんか? Vivliostyle を使った CSS 組版では、比較的柔軟かつプログラマブルに原稿のスタイルを設定できます。

最大でも年に2回しか同人誌を作成しない身としては、InDesignIllustratorのような高価かつ、同人誌作成でしか利用しないスキルを身につけるのは様々な意味でコストが高い。(サークルカットなどを作るにもInkscapeを使ってAdobe税を回避しているほどだ)

VivlioStyleは、普段から仕事で使っているフロントエンドのスキルセット(主にCSS)を使いPDFレイアウトができるツール。さらに、入稿の元データも馴染みのあるMarkdown構文で記述できる。

テーマに原稿データを流し込み、テンプレートエンジンのような感覚で入稿可能なPDF※を生成できる。画像の配置・改ページ・ノンブルや目次の自動生成など、手作業でやると抜けの出やすい部分をおまかせでき、執筆者陣はコンテンツ作成に集中できるという寸法。

※印刷用の入稿PDFにはフォントのアウトライン化やグレースケール化をはじめとするPDF/X-1aという規格に準拠する必要がある

この夏の新刊はこちら。

www.gensobunya.net

後述するが、印刷物としての制限がないKindleでは写真を全てカラー版に置き換えている。

こういった複数フォーマットに対しても、ビルド時の設定を変更することで、単一の原稿ファイルからPDF/X-1a規格とKindle用のPDFを出し分けられる点が非常に強力だ。

組版ツールでも、リンク先のファイルを置き換えるだけで対応できるといえばできるだろうが、こうした機械的な作業は作業漏れ対策も兼ねて自動化するに限る。

VivlioStyleの強みと、自転車同人誌のミスマッチ

VivlioStyleのプロジェクトはMarkdown + CSS + 設定ファイルという構成になり、テキストベースのファイルだけで構成できるので、Gitを使って管理できる。

ここまでくれば、ITエンジニア諸氏にはGitHubなどのGit WEBサービス上にリモートリポジトリを設定し、Pull Requestベースでお互いの原稿ファイルをレビューしたり、ローカルでは好みのエディタを使ってLinterやフォーマッターを使って原稿の質を確保する…といったメリットがすぐに思い浮かぶだろう。(コードではなく文章を書く場合にもTextlintというものがあり、これが素晴らしい仕事をしてくれる)

github.com

事実、技術書の執筆ではこうしたコラボレーションツールと、Textlintを用いたMarkdown原稿での運用事例が山のように転がっている。(MarkdownでなければRe:viewが多いか)

ただ、幻想サイクルの共著者すくみずさんはどちらかというとハードウェア寄りのスキルの持ち主なので、Gitを設定してVSCode上でFormat on SaveしながらLinterの吐いたWarningを読んで修正…ということを1から身に着けてもらうのは難しい。繰り返しだが、年に2回のイベントでしか使わないスキルを身に着けるというのは無理がある。大抵は次回までに体が忘れている。

そこで、ITエンジニアでなくとも使いやすいツールで共同作業しながら、こうしたソースと成果物を分割できるワークフローを回せないかと試行錯誤している。

GoogleDriveでファイルを共有しながら、.mdファイルをいじる…ということもやってみたのだが、テキストエディタMarkdownをいじるという行為をPrettier無しでやると、画像リンクや改行など"流儀"の差が出てしまう他、校正や見直した部分がわからなくなったり、最終編集時刻を見ずにいじって原稿データのデグレが頻発するなど、気分のいいものではなかった。

ようやくこの記事の本題だが、C102では下記のステップで原稿を作った。手戻り少なく成果物もわかりやすい状態を維持できて、中々いい具合だったので記録に残しておきたい。

  1. 下書きと構成をNotionでコメント入れながら試行錯誤(Notion内)
  2. Markdownエクスポートしたものをリポジトリに入れてTextlintとPrettierで綺麗にする
  3. VivliostyleでPDF作って校正(チャットでやり取り)

1. Notionでの原稿下書き

Notionでの下書きについては特に何も考慮することはない…わけでもない。

エクスポート時に、Notionにおける「ページ」が1つのMarkdownファイルになるため、これを意識したページ構成にする必要がある。また、後々は原稿と画像ファイルの紐付け管理のために、Markdownファイルと、それに伴うアセットファイルがうまく整理されると嬉しい。

Vivliostyleの設定ファイルから、理想的な下書きの吐き出し方を考慮してページ構成を決める必要がある。

//vivliostyle.config.js

module.exports = {
  title: "限界自転車部屋",
  author: "Gen and skmz",
  language: "ja",
  size: "JIS-B5",
  theme: "theme_print.css",
  tocTitle: "目次",
  entry: [  //原稿用Markdownのパス
    { path: "toc.md", rel: "contents", theme: "theme_toc.css" },
    "chapter0/intro.md",
    "chapter1/chapter1.md", 
    "chapter2/chapter2.md",
    "chapter99/outro.md",
  ],
  entryContext: "./contents",
  workspaceDir: ".vivliostyle",
  output: "./output/bike-room-catalog.pdf", 
  toc: true,
};

(個々人の好みはあるにせよ)チャプターごとにディレクトリを分けておきたいところ。

Vivliostyleは章ごとにファイルを指定するので、Notionページも章ごとに分けておく。(これは粒度が大きすぎるので、もっと細かくファイルを分けたいところだがVivliostyleの設定を見る限りこれ以下の粒度にはできないっぽい)

サブページを出力し、フォルダ分けするオプションをONにすると都合のいい出力となる

それっぽいオプションをONにして出力すると、いい具合にページごとのディレクトリが作成される。

ページ名+ハッシュがアセットのディレクトリ名となり、日本語名は文字化けするので英字必須。この変換・出力は5分ほどかかる点に注意。

後処理の必要性があるファイル名とディレクトリ名だが、後ほど処理する

Google Docsライクな共同作業能力

ページの編集自体は何の変哲もなく、Googleドキュメントのようなオンライン同時編集と、履歴管理・コメント管理ができる。これでレビューや章立ての議論・見直しを行いながら本文を書きあげていく。

Gitでバージョン管理して複数案検討したり、それぞれの案で現物出力したいなどの欲は出るが、この方法でも必要十分な議論はできる。

2. エクスポートした原稿を整形し、改ページなどをいじる

ここからはITエンジニアの作業。

エクスポートしたファイルたちを取り込むvivliostyle.config.jsを書き、フォーマッター・Linterを用いて原稿を綺麗にする。そのうえで、Vivliostyleのプレビュー機能を使いながら改ページや段落の分割などを行っていく。

NotionエクスポートしたMarkdownの下処理

Lintの表記を修正したくなるところだが、キャプション付き画像のキャプションがMarkdown Image記法と本文で2重に出力されたり、キャプションnullの画像はファイル名が挿入されるなどの都合があるため、これらを一括修正する必要がある。

ファイル名はともかく、段落にもキャプションが入ってしまうのは仕様としてイケていないので何とかしてほしい…

キャプション未設定だとキャプションにファイル名が入る

なぜかキャプションが二重に…

これらを正規表現で修正。

また、ページ名とハッシュの間にもスペースがありLinuxでは困るという点については、ディレクトリとファイル名のスペースを削除してMarkdown内は置換でなんとかする。

まとめると、下記の手順となる。

  1. アセットディレクトリのスペースを削除し、Markdown内の相対リンクも半角スペースを消したディレクトリ名で置換
  2. キャプション被りを正規表現置換で修正する
    1. ^!\[(.*)\]\((.*)\)\n\n\1 to ![$1]($2)
  3. ファイル名キャプションの画像を修正する
    1. ^!\[(.*.[png|jpg|JPG|PNG|webp|WEBP])\] to ![ ]

Markdown文法上は、キャプションの中身を0文字にしても問題ないが、Vivliostyle側でエラーになってしまうので半角スペースを挿入している。

校正

Prettierやmarkdownlintで自動整形が走った後に、textlintのサジェストを一つ一つ確認しながら、ルールを無効にするか、文章を修正するか判断して機械的に判定できる点の構成を行っていく。

プリインのルールや、技術書向けのルールを適用している

利用している校正ルールの一覧

これだけのルールを人力で見つけるのは非現実的なので、機械的に検出することで漏れをなくし、時間も劇的に節約できる。

改ページ処理

実際の出来上がりをプレビューしながら、自動改ページを手直しする

Vivliostyleが画像や段落に応じて自動的に改ページを入れてくれるのだが、そのままにしておくと「写真が1枚載っているだけ」というページや、「改ページ直後にその後の本文と無関係な写真が入る」という現象が起きる

これらを改ページ用の不可視区切り<hr class="page-wrap" />を挿入して改ページを増やしたり、段落分割を行ったりして見た目にダサい本文にならないように修正。

3. PDFプレビュー

一通りの処理が終わったら、PCで読めるPDFファイルに出力して最終校正を行う。

グレイスケール化や、フォントのアウトライン化を行わないことで軽量なPDFを出力して読み合わせし、温かみのある課題表を手作りして対応。

// package.json
  "scripts": {
    "lint": "textlint README.md 'contents/**/**.md'",
    "lint:fix": "textlint --fix 'contents/**/*.md'",
    "build": "run-p build:scss build:vivliostyle",
    "build-ebook": "run-p build:scss build:ebook",
    "build:scss": "sass theme:.",
    "build:vivliostyle": "vivliostyle build --press-ready --preflight-option gray-scale",
    "build:ebook": "vivliostyle build -o ./output/ebook.pdf",
    "dev": "run-p preview watch:scss",
    "preview": "vivliostyle preview",
    "preview-ebook": "vivliostyle preview -c vivliostyle.ebook.config.js",
    "preview-build": "npm run build:scss && vivliostyle build -o ./output/preview.pdf",
    "validate": "vivliostyle-theme-scripts validate",
    "watch:scss": "sass --watch theme:."
  },

必要なコマンドを作っているだけでnpmスクリプトが肥大化するが、必要なので仕方ない。

ここばかりは人力が必要。有料版Acrobatを持っていたりすると、直接赤を書き込めて捗る。

まとめ

GUIで共同作業できるNotionと、IDEのパワーを使うことで、入稿期限数時間前まで変更をかけても原稿ファイルが破綻しない。実際にC102ではデスマーチ対応実績も積むことができ、期せずして信頼性の高いワークフローに仕上がったと思っている。

その他:テーマについて

本のテーマは、公式で配布しているvivliostyle-techbookを自分用に手直ししたものだが、飾り気に欠けるのが改善点と思いながら着手できていない。

ひとえに、CSS力が足りない他、Vivliostyleはmarkdown→HTML→PDFという内部変換を行っている影響も大きい。手になじんでいるNext.jsやGatsbyでHTMLを出力すればMDXと合わせてリッチな原稿を作れる可能性を考えたのだが、クライアントサイドのJavascriptCSS-in-JSライブラリがVivliostyleのPDF化ライフサイクルのどこで評価されるか分かりづらいこともあって手を出せなかった。

最近使いだしたAstroは、ゼロランタイムのCSSライブラリと合わせて完全に静的なHTML+CSSを出力できるため、こうしたフレームワークを使って冬コミ原稿にチャレンジしたいところ。

2023年版 地方税(自動車税・固定資産税)と国税(所得税など)を手数料ゼロでクレジットカード納付する

今年も税金の季節になってきました。

自動車税や固定資産税、例にもれず我が家にも届いたのですが、当然コンビニに赴いて現金で納付…なんてことはせずに絶対にクレジットカードで支払いたい。しかも手数料は払いたくない。

色々仕様を調べたらいいルートがあったため記録。

前置き

筆者はポイ活自体は好きだが、手間をかけたポイ活はしたくなくプラチナプリファード一本化で「最小限のアクションでそれなりの還元」を追及している。

同様にチャージ式の決済サービスも嫌っているので、基本的に残高が必要な決済サービスは無視です

地方税自動車税・固定資産税)

三井住友カードをLINE Payのチャージアンドペイに登録して、請求書払いをする。これだけ。

チャージアンドペイは、LINE Payクレジットカードか、三井住友カードだけで利用できるクレカ直結型の支払い手段。(いわゆるクレカプロキシ)

www.smbc-card.com

linepay.officialblog.jp

今年から運用されているeL-QRからf-regiのクレジットカード払いを選択すると、支払手数料がかかるが、LINE Pay経由なら手数料なし

Vポイントも、通常のショッピングと同じ割合で付与される。楽天PayやファミPayもポイント利用や還元で候補になりえるが、チャージが嫌だし普段使っていない経済圏なので使わず。

国税

今回の件とは別だが、オマケで記録。

国税の支払いサイトはAmazon Payを基本的に使う。なぜならギフトカード支払いをするとプライム会員なら1%Amazonギフト券で還元されるから。ギフト券を購入したクレジットカードが1%還元なら合計2%還元。

pay.amazon.co.jp

そして結構な頻度でキャンペーンもやっているので、実質還元率が上がることも多い。もはやAmazonギフト券Amazonで使ってはいけないレベル。

ふるさと納税やその他の通販サイトでもAmazon Payを使えるところは意外と多いので、意識してみると結構お得になる。

Amazonそのもので買い物する際は毎回ギフトカード残高のチェックを外さないといけないので、面倒だが…

Gatsby CloudとCloudflareを利用して日本向けに効率よく配信する

TL; DR

  • Firebase Hostingのデプロイに時間がかかる
  • VSCodeでの執筆はやめたくない
  • 日本にエッジがないホスティングでもCloudflare経由でいい感じに配信できた

これまでの構成と課題

これまでは、Gatsby CloudでビルドしたアセットをFirebase Hostingを使ってホスティングしていた。

Gatsby Cloudに日本のCDN Edgeが無いため、TTBFなどの指標が悪く、定性的にも回線品質が良好な自宅のNW環境でもわかりやすくレスポンスが悪かったので、メインコンテンツをホスティングするには不適だと判断。ブログは画像も多いので、基本無料で従量課金もリーズナブルということでFirebase Hostingを使っていた。

ユーザー向けの体験は問題ないが、記事が多くなるにしたがってデプロイに時間がかかりすぎるというデメリットが大きくなってきた。

ビルド時間よりデプロイ時間の方が長い有様

直近では、ビルド時間が数分にもかかわらず10分以上もデプロイにかかってしまうという事態が頻発しており、ちょっとした更新でも反映が遅くストレスが高くなっていた。

ビルドに関してはGatsby Cloudでログを見ることができるのだが、デプロイコマンドは完全にブラックボックスで行われているため改善策が打てなかった。(おそらくファイル数が多すぎるのだと思う)

システム構成も数年変更を入れていない他、流石に心理的限界が来たのでビルド・デプロイのプロセスを見直すことにした。

検討内容

変更のPushから反映までの時間を短くするという観点で、いくつかの案を検討した。

ビルド時間とファイル数は殆どが画像ファイルの最適化によって生み出されるものなので、画像ファイル処理をどうにかするのが基本線。VSCodeと各種フォーマッタによる執筆体験は失わないようにするのが絶対要件。

  1. 現在ローカルファイルで管理している記事データを画像データをContentfulからの配信に切り替える(画像最適化オフロード+コンテンツ管理の外出し)
  2. 画像のみ画像CDNサービスを利用して配信する(画像最適化オフロード+画像管理のみ外出し)
  3. 二重CDNだけど、日本にエッジサーバーのあるCDNを噛ませてGatsby Cloudから配信する(デプロイ時間への対処)

1や2を基本線に考えており、ContentfulでもローカルMarkdownのように編集するツールがあったので1が最も良い候補に見えたのだが、Gatsby MDX v2プラグインではリモートのMDXを扱えない点が決め手になってしまいコンテンツはGitリポジトリ内という前提が新たに追加された。

3の場合、候補はAmazon CloudfrontとCloudflare。Cloudfrontの方が細かい設定に分があるが、DNSをCloudflareで管理していたこともあり運用対象が増えない点・完全無料である点を重視。まずはCloudflareで検証した後、リダイレクトルールなどを細かく設定したい場合はCloudfrontを使うという気構えで実装面の調査に入った。

検証

計画しているサービス間連携は上記の通り。

ユーザーに最も近い配信場所はCloudflareにするつもりだが、Gatsby CloudもCDNを持っているため、二重CDN構成となる上に、Gatsby CloudのCDNルールはかなりの部分隠蔽されている。不測の挙動があった場合はエスパー力が要求されると思われる。

移行前のテストとして、既存のドメインはFirebaseを向けたままテスト用のサブドメインを切ってGatsby Cloud Hostingに登録し、Cloudflare経由での配信を試みた

移行手順

Gatsby CloudはHostingオプションをONにする。

Firebaseのリダイレクト設定をGatsby Cloudで有効にするため、firebase.json に記載されていた内容をgatsby-node.tscreateRedirect構文で置き換える (gatsby-plugin-gatsby-cloudが必要)

設定画面上で独自ドメインを登録すると、SSL証明書を自動発行するためのCNAMEレコード(GatsbyCloud Hostingで割り当てられるドメイン)を指定するよう指示される。

Cloudflare側は、CDNを有効にするため、DNSでProxiedをONにしてレコードを登録する。

リダイレクトループを避けるため、HTTPS接続のみを利用するようSSL/TLS設定を変更することも忘れずに。

Cloudflare DNS Proxyと各ホスティングサービスの組み合わせ

ここで1つ問題が発生する。

Cloudflare DNSでキャッシングのためのProxyを有効にすると、設定上はCNAMEレコードがあるように見えるが、DNSレコードの実体はAレコードでCloudflareのIPアドレスを指す。

developers.cloudflare.com

つまり、Gatsby Cloud側でレコードが正常に設定されているように見えず、いつまで経ってもSSL証明書が発行されない。気持ち悪いので、試しにカスタムドメインGatsby Cloudに登録しない状態でDNSを向けたところ404となってしまった。

実際のアクセスはCNAMEはGatsby Cloudの所持しているサブドメインに向いているため、こちらでSSL証明書が使えれば問題無くクライアント・CDNからアクセスできるため今回はこのWarningを無視することにした。

※Firebaseはサブドメインに指定のレコードが正常登録されない場合、そもそもHostingが機能しない仕様だったが、Gatsby Cloudはカスタムドメインは登録さえすればHostingできるようだ

結果

レスポンスヘッダからCloudflareを介して配信されていることがわかる

レスポンスヘッダより、Cloudfrontを介して配信されていることがわかる。エッジ→オリジンまでもCloudflareのNWで最短ルートを通るようにしてくれるため、Gatsby Cloudのエッジが日本にないにもかかわらずCFでキャッシュが効き10msそこそこでレスポンスが返ってくる状態になってくれた。

HTMLはGatsbyCloudでキャッシュされているがCFではデフォルト設定でキャッシュされない(キャッシュヘッダを見て決めている)ため、CDNキャッシュ用に設定をいじるともう少しよくなりそう。