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

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

Gatsby.jsでTypescriptのコンポーネントを作ってみた

サークルサイトの再構築をするにあたり、Typescriptを導入してみました。

「最近の記事」コンポーネント

GraphQLで取得したデータに型を指定する

interface StaticQueryProps {
  allMarkdownRemark: {
    edges: Edge[]
  }
}

interface Edge {
  node: {
    frontmatter: {
      date: string
      title: string
    }
    id: string
  }
}

const recentPost: React.FC = () => (
  <StaticQuery
    query={graphql`
      query RecentPostQuery {
        allMarkdownRemark(limit: 4, sort: { fields: frontmatter___date, order: DESC }) {
          edges {
            node {
              frontmatter {
                date(formatString: "YYYY/MM/DD")
                title
              }
              id
            }
          }
        }
      }
    `}
    render={(data: StaticQueryProps) =>
      data.allMarkdownRemark.edges.map(({ node }) => (
        <div key={node.id}>{node.frontmatter.title}</div>
        //  component
      ))
    }
  />
)

メリット

  • Propsとして下位コンポーネントに引数を渡す際、「あれ?このProps何入ってたっけ」といちいち上位コンポーネントのコード読む必要がなくなる(このコンポーネントだとそうなってない)(PropsTypesでもいい)
  • MarkdownRemarkあたりのデータ構造、適当にえいやで動くように書いていたものの構造をしっかり理解してコードを書けるので手戻りが少ない

なぜJAMstackを採用するのか?

はじめに

この記事はJAMstack Advent Calender20日目の記事です。

qiita.com

フレームワーク類の実装や特定のCMSが~~といった内容はこの記事には皆無です。
個人開発、企業のWEBサイト・ブログでそれぞれJAMstackを採用するメリット・デメリットについて考察します。

なお、筆者のJAMstack経験は趣味の自転車に関するブログ及び、同人サークルサイトをそれぞれGatsby.js, Hugoで運用しており、所属企業のコーポレートサイトのアーキテクチャ刷新に検討している程度の経験です。

個人開発(自分)編

当初、個人ブログはBloggerを使っていたり、GCE上にWordpressを立てて運用していたりしましたが、どうしてもデザイン自由度や広告問題、アクセス性能問題がついてまわるため、Hugoに行き着き現在ではGatsby.jsで運用されています。

運用(費用・人的)コスト

WEBサイト運営で重視したい点は(個人的に)下記三つです。

非常に残念なことに、これらをブログサービスや、ペライチさんのようなSaaSで満たそうとすると大抵有料になります。当然ですね。

しかし、Gatsby.jsやHugoで作ると上記の要件を満たすために必要なサービスは以下の2つです。

  • Github無料
  • Netlify… 無料

なんと!完全無料で思いのままにWEBサイトが作れる!

しかも、WordPressなどのCMSをセルフホストして使った場合はアップデートを随時行っていないとあっという間に悪い大人の餌食になってしまう一方、JAMstackは静的配信なので脆弱性対応は最小限で済みます。

ただし、本当に労力を割ききれない場合は(もしくは力を入れたいプロダクト・仕事が別にある場合)、はてなブログやnoteのような完全なマネージドサービスを使ったほうがよいでしょう。
自分も、フルタイムで別の仕事をしつつ、運用するサイトは2つが限界だと判断したため、この技術ブログははてなブログを使っています。

コンテンツ移植性

WordPress <=> Blogger など、サービス間のマイグレーションも、基本的にMarkdown形式で記事が残っているため、遥かに簡単に別のフレームワークに変更できます。
自分も、今度サークルサイトをVue.jsのGatsby.jsことGridsomeに変更してみる予定です。コンテンツは特に手に入れなくとも、ビルド環境とテーマを組み直せば完了です。

移行ツールなどをわざわざ用意してデータフォーマットを変更する必要はありません。

エンタープライズ

さて、企業においてJAMstackのメリットを活かせるユースケースを考えてみます。

CMS上でコンテンツを編集し、IaaS+LB+CDNで公開する王道な構成と、Gitリポジトリ+CI+静的ホスティングのJAMStack構成を比較してみます。

セキュリティ

前述の通り、静的ホスティングサービスを利用することで意識しなければならない点がだいぶ減ります。最終的にReactアプリを配信する場合などはアプリレイヤーのみ、企業側でコントロールすればよくなります。

定期的にパブリッククラウドの監査、ミドルウェアのバージョン確認、セキュリティパッチの適用作業などをこなしていく必要はなくなります。

コンテンツベンダーのいる業務運用

Qiita的にはあまり聞かない話題ですね

そこそこ大きい企業では部門ごとにコンテンツの発注先が違うことはよくあることかと思います。
CMS上でコンテンツ作成するタイプのCMSを使っている場合、コンテンツの作成お作法や運用ルールなどを新規ベンダー追加の度に伝える必要があり、情シスは死にます。

さらに、プロモーションページなど継続的に取引が発生しないコンテンツに関して1回だけのためにCMS運用教育を行うことは最高に非効率です。

代替案として、特定ディレクトリ下で機能することを前提にした静的なHTML+CSS+JSを納品してもらい、最も取引の大きいコンテンツベンダーに依頼してCMSで使えるコンテンツ形式に変換してもらうことも考えますが、責任の所在が曖昧になって…よくないことになります。

JAMStackのフレームワークの場合、納品された静的ファイルを/publicに配置してCIを回すだけで新しいコンテンツを追加できます。

プルリクエスト単位でステージングサイトを作ることができ、Gitリポジトリでコンフリクトを管理できることも大きいです。特定企業の中でしか通用しないCMSの運用ルールを覚えてもらうより、Gitの使い方に沿ってもらったほうが生産的です。

部門間

CIのHTTP hookさえキックできるのであれば、部門ごとにHeadless CMSを用意して完全にコンテンツ作成を分離しつつ、最終的に一つのWEBサイトとして公開することも可能になると妄想中です。

製品ページはContentful、プロモーションページは静的ファイルで作成、コーポレート関連の情報は今までの試算を活かすためにDrupalをHeadless化してビルド時にコンテンツ取得…なんてこともできます。

まとめ

ステージングサイトのアクセス制限をどうする?結局Gatsbyなどのフレームワークでテーマを作る部門に要望が集中するのでは?などの課題もありますが、大規模にも使える可能性を秘めているアーキテクチャだと考えています。

もっと事例が増えて、一般的に使われる技術に成長してほしいですね!

Google Tag Managerでパスごとに読み込むscriptを変更してパフォーマンス劣化を防ぐ+Adsenseタグの配信方法

eBayのスマートタグを記事ページのみ配信してみる

eBayパートナーネットワークのスマートリンク機能は、Scriptタグを配置することでページ内のebayリンクを全てアフィリンクに置き換えてくれるスグレモノ。ただ、Lighthouseのスコアを悪化させるので不要なページには置きたくない。

そこでタグマネージャーを使って記事ページのみにこのスクリプトを配信する。

f:id:gensobunya:20191109112329p:plain
trigger

トリガー設定でPage Pathを/postに指定することで、トップページやタグ記事一覧のページを対象外にして配信できる。 これまでなら面倒なJavaScriptを書かなければならなかったことを考えると大変にかんたん。素晴らしい。

2019/10からのAdsenseタグと相性悪くてクソ

<head>タグ内に配置することが必須なタグはGTMで配信しづらい。

代表例はアドセンスの自動広告タグ(2019/10最新版)。こいつは<head>内配置指定されている。

<script data-ad-client="/*your adsense id*/" async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>

回避策は古いタグを使って「HTMLタグ」を使って配信すること。機能に変化はない。

<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
  (adsbygoogle = window.adsbygoogle || []).push({
    google_ad_client: "/*your adsense id*/",
    enable_page_level_ads: true
  });
</script>

自転車ブログをHugoからGatsby.jsで書き直した

Gatsbyに変更した理由

Hugoは元々サイトビルドが爆速という理由で採用しました。事実、ローカルサーバを作る場合でも300ページ近いブログも一瞬(0.2秒程度)でビルドが終わりlocalhostで接続できるのでこの点については全く不満なし。

ただし、主に実際にWEBサイトとして運用するにあたって下記の点が課題になっていました。

  • レスポンシブな画像配信がテーマ任せで、デバイスごとに最適な配信がしづらい
  • 上記の影響で結局shortcodeというHugo特有の記法をMarkdownに書かねばならず、可搬性が落ちている
  • 画像サイズが最適でない影響でSEO悪化
  • 初期のバージョンで構築したので、git submoduleを使ったテーマのアップデートができなかった(そもそもテーマも結構いじってしまったので単純アップデートができない)
  • テーマをいじるのにGolangでフロントエンドを書くという苦行をしなければならない

特にきついのは画像配信周りとテーマの弄り難さでした。

そんなわけもあってReact.jsでフロント周りを自分で1から書きながらGatsby

採用したフレームワークなど

ほぼ自分でデザインを書くと決めていたので、採用するフレームワークも選び放題なのでなるべくモダンで、かつ最小で済むようにしました。
Gatsby-starter-blogをベースに使わないと思われるプラグインを削除して、下記のサービスやフレームワークに必要なものをインストール。
今はgatsby-starter-blog-mdxで始めるのがいいと思います

www.gatsbyjs.org

npmの文脈で全てが完結するので、新たに勉強しなければならないことが少なく割と楽ちんでした。
プログラミングではないけど、Google Tag Managerはかなり便利だったので別途記事書きます。

特に工夫という工夫はなく、各プラグインフレームワークの記載に従って実装しました。

というよりも、3ヶ月ほどかかってそれなりにハマったはずなのに、逐一メモらなかったために終わったら綺麗サッパリ忘れてしまった。どこかでハマってたどり着いた人はリポジトリ参照してください。

github.com

強いて言えば、Hugo時代の遺産であるdraftフラグを使うためにGraphQLに1行追加しました

....
    allMarkdownRemark(
      sort: { fields: [frontmatter___date], order: DESC }
      skip: $skip
      limit: $limit
      filter: {frontmatter: {draft: {eq: false}}}
...
...

悲しい作業

一番不毛だと感じたのがHugo用に書いたShortcodeと、Markdownのfrontmaterを正規表現と手動Grepで置き換える作業。
Markdownでコンテンツ管理する以上は、標準的な(最低でもGithub flavorな)Markdown記法だけに利用をとどめましょう…

Markdown以外から画像を利用するのはGatsby.jsにおけるクソ実装レベルがかなり高い行為なのでなるべくMarkdownからページ生成したほうがいいということはわかった。

移行結果

f:id:gensobunya:20191109104458p:plain
lighthouse

出来上がったものがこちら

blog.gensobunya.net

記事書いといてLighthouseスコア100じゃないのかよとツッコミたくなりますが、以前のブログは画像サイズのせいで70台などと全体的に低迷していたのでかなり改善されました。Performanceは毎回数字が変わるので飾りです。AccessibilityはもうちょいUIの実装力や色の知識を上げれば90後半になりそう。

テストに関して

ホスティングに使っているNetlifyのDeploy-preview機能を利用しました。

リンクしているGithubリポジトリのプルリクから自動的にステージングサイトを生成してくれるので、CI/CDをほぼ書かなくても実装できます。
今回、本番サイトはhugoでビルドしているため、ビルドコマンドが変更となりますがNetlifyはこんな要件にも対応しています。

プロジェクトrootのnetlify.tomlに下記のように環境毎のビルドコマンドと公開ディレクトリを指定できるので本番と違うコマンドで自動ビルド可能です。

[context.production]
  command = "npm run build"
  publish = "/public"

[context.deploy-preview]
  command = "npm run build"
  publish = "/public"

[context.branch-deploy]
  command = "npm run build"
  publish = "/public"

JavaScript同士ならnpm run buildの中身を書き換えておけばいいんですが言語ごと変わるとそれもできないのでありがたい。 管理画面でBranch deployをONにすると特定のブランチが更新されるたびにステージングサイトを作ることもできます。Netlifyマジですごい、神。

Withingsの体組成計を買って、GASでSlackの減量チャンネルにポストする

f:id:gensobunya:20191108222621j:plain
withings

ものぐさレコーディングダイエット

減量できたことないけど、いろんな事を記録して可視化するのはいいこと。
そして、体重の崩壊を防ぐ仲間内Slackでは、このようにpostした体重と体脂肪率をグラフ化してくれるスクリプトがすでに動いています。

f:id:gensobunya:20191108222638p:plain
weight graph

f:id:gensobunya:20191108222655p:plain
fatrate graph

postしたusernameを自動的に取得して、それぞれの人の記録をトレンド含め表示してくれています。
今までは、愛用していたTANITAの体組成計から手動でメモしていたのですが、いかんせん朝にメモ作業するのは結構面倒。目も疲れるし。

そこで、Wi-Fi対応の体組成計を購入して、乗ったら自動的にSlackに体重と体脂肪率を飛ばすシステムを作りました。
白羽の矢がたった機種がこちら。

体重の計測は200g刻みと、21世紀の体重計とは思えない精度ですが計測結果が100g程度変動していたところで一喜一憂するものでもなし、インターネット直結という点を重視しました。

とりあえずIFTTT

計測した内容はWithings Health MateというWEBサービスにアップロードされ、モバイルアプリでも閲覧できます。
本体の設定はすべてモバイルアプリ経由なので楽ちん。このサービス自体がIFTTTに対応しているので、まずは直接IFTTTを使ってSlackに連携してみます。

f:id:gensobunya:20191108222807p:plain
ifttt to slack

骨量や筋肉量、水分量も計測できるのですがAPIで取得できるのは「体重」「前回体重差」「体脂肪量」「前回体脂肪量差」「体脂肪率」「計測日時」の6点のみでした。
こちらのIFTTTアプレットの起動結果がこちら。

f:id:gensobunya:20191108222822p:plain
slack post

連携はできましたが、一つ思い出してください。

postしたusernameを自動的に取得して、それぞれの人の記録をトレンド含め表示してくれています。

IFTTTのbotアカウントがpostするだけだとグラフ化の方のbotが正常に動いてくれません。自分のアカウントがpostしたことにしないと…
ちなみにコードを書かなくてもMyfitnessPal経由でGARMIN CONNECTやSTRAVAに体重を連携できるので単体でも結構便利です。

Slack APIを直接叩こう

Slack APIの仕様によると、chat.postMessageas_userオプションをtrueにしてPOSTすればいいようです
IFTTTのオプションにはないので直接何かしらのスクリプトを起動することにします。

f:id:gensobunya:20191108222834p:plain
overview

どこで実行するかが問題ですが、データの受け皿を用意しつつ無料でスクリプトを動かせるとなると、やっぱりGoogle SpreadSheetとApps Scriptがシンプルで簡単です。 tokenの取得やアプリの登録に関しては割愛します。

一回、IFTTTでスプレッドシートにデータを飛ばしてカラムを確認した後に以下のスクリプトを、編集をトリガーに設定して実行します。 Date,Weight,Weight w/o fat,Fat weight,Fat Percentの順に4カラムIFTTTで入力しています。

function getLatestWeightData(){
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const lastRawNum = sheet.getLastRow();
  const dataRange = sheet.getRange(lastRawNum, 1, 1, 5) //Date,Weight,Weight w/o fat,Fat weight,Fat Percent
  const postData = {
    "weightKg": dataRange.getValues()[0][1], 
    "fatPercent": dataRange.getValues()[0][4]
  }
  Logger.log(postData)
  return postData
}


function postMeasureDataToSlack() {
  //Get latest row data
  const data = getLatestWeightData();
  const postMessage = data.weightKg+"kg "+data.fatPercent+"%";
    
  //POST to Slack channel
  const baseURL = "https://slack.com/api/chat.postMessage";
  const postData = {
    "channel": "<your channel name>",
    "as_user": true,
    "text": postMessage
  };
  const options = {
    "method": "post",
    "contentType": "application/json; charset=utf-8",
    "headers":{
      "Authorization": "Bearer <!!your access token!!>"
    },
    "payload": JSON.stringify(postData)
  }
  
  const response = UrlFetchApp.fetch(baseURL, options);
  Logger.log(response)
  return
}

Slackグループの設定がありませんが、トークンがグループごとに発行されるのでそこで認識されています。
"as_user": trueを設定したので、あたかも自分が発言したかのようにチャンネルにポストされます。

f:id:gensobunya:20191108222850p:plain
users post

仲間内で牽制し合うコミュニティではIFTTTよりこちらのほうがより便利かと思います。
他にも自動で有給連絡したり、as_userオプションの使い道は結構ありそうですね。

アプリ連携体重計だとその後の広がりがあまり良くないのですが、WEBサービスだと連携も簡単です。タニタオムロンも、WEBサービス連携する体重計は結構高いので1万円程度で買えるWithingsはかなりコスパ良い買い物でした。

Vue.js + firebase でローカル開発時だけvue devtoolsをfirebase serveと共存させる

Firebase + Vue.jsとかで開発するとき

前提

  • firebaseのバックエンドとVue.jsを同じディレクトリ内で開発している
  • functionsへのアクセスをfirebase.jsonrewriteして使っている
  • Vue-cliなどを使ってプロジェクト作成を丸投げしていた(人)

Vue devtoolsはとても便利です。ただしVueをdevelopモードでビルドしないと発動しません。

バックエンドをfirebase functions等で同じプロジェクト内で開発している場合、大抵の場合firebaseのrewriteを使いたいので、firebase serveでローカル環境にデプロイするんですが、Hostingのディレクトリにビルドしてからfirebaseのローカル環境を立ち上げるとデフォルト設定だとproductionモードでビルドされているのでVue側のデバッグが非常に面倒。

vueとfirebaseでそれぞれserveする選択肢もあるんですが、CORS周りで死ぬ。

ググって5秒で見つけた解決策

Vue.config.devtools = true;をapp.jsとかのエントリーポイントに書く。

デプロイ前だけコメントアウトでもするのか知らないけど、とても事故りそう…

イケてる解決→npmスクリプトの方を触る

npm run serveの中身を書き換えて、developモードでビルドしてそのままfirebaseのローカル環境を立ち上げることにした。

Vue-cli3でプロジェクト作った素の状態がこちら

  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },

1コマンドでvueをdevelopでビルドしてローカルfirebaseでホスティングする

  "scripts": {
    "serve": "vue-cli-service build --mode develop & firebase serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },

npm run serveを実行するだけで、vue devtoolsの効くfirebase hosting & functionsのローカル環境が立ち上がる。(もちろん他のサービスを使っている場合はそちらも)嬉しい!

Markdownでオフセット同人誌を刷るまで

sphinx

2022年現在では、VivlioStyleを用いた同人誌作成をやっています。

すぐ書き出すためのテンプレートリポジトリも作成しました。詳細は下記記事にて。

gensobunya-tech.hatenablog.com

電子書籍も紙書籍もキッチリ作りたい!

幻想サイクルの活動では、完全オリジナルであることを活かして現物の同人誌頒布だけでなく、電子媒体での配信も意欲的に行ってきました。

BoothでのPDF版の頒布を皮切りに、Amazon kindle、BookWalker(現在は停止)で配信をはじめ、今ではKindle Unlimitedでも読めるようになっています。

実績から言うと、人気は「紙媒体>>>KindleKindle Unlimited>>>>>>>>>その他」となっています。思ったよりで電子版の配信が好評で驚いています。

しかし…実は、現在配信している同人誌には文字情報がありません。Illustratorで作成している都合上、文字情報を維持したままepubファイル(電子書籍形式)を作成することが手持ちのソフトでは困難だったためです。Boothで配信しているPDFは文字情報を維持できているので、コンテンツの質としてはKindle版より上なのですが利便性はそれに優るようです。
※PDFでもKindleモバイルアプリで読むことは可能です

実用寄りの本を書いていることもあり、せっかくなら文字検索ができる状態の本を出してもらおうとしたのがC95の新刊「泥輪事情」です

技術書典が公表を博していることもあり、1ソースから印刷用のファイルと電子書籍用のファイルを出し分けることに関するノウハウがいくらか溜まっていることは把握していたので、ITを仕事にしているものの端くれとして興味が昂ぶっていたこともあり、原稿ファイルから複数の入稿ファイルをビルドするスタイルに挑戦しました。

以下、検討から出力までかなりの長文が続きますが、最終的に下記のツールでPDFの出力まで持ち込みました。

conf.pyが含まれたリポジトリは下記のとおりです。ビルド方法はREADME参照のこと。

Github : gentksb/md2doujin_sphinx
https://github.com/gentksb/md2doujin_sphinx

プランニング

入稿要件

印刷用の原稿は、同人活動開始以来お世話になっている、ラック出版様の入稿ファイルを要件として設定します。移動体通信事業者のプランがかわいく見える印刷所の料金体系の中で、非常に明快な印刷メニューと割引メニューを準備している素晴らしい印刷所です!(ダイレクトマーケティング

入稿はPDF形式です。要件はこちらのページに記載されている通り、大きく下記の3点です。

  • フォントは全て埋め込み
  • PDF/X-1a形式
  • ページ設定---原寸

出力時に上記を満たしている必要があります。epubは出力してくれればなんとかなる(中身はHTML)なので対して問題にならないだろうと割愛。
技術系同人誌はA5が多いですが、評論かつ弊サークルの前例に従いB5サイズを作成するものとします。

Wordじゃだめなの?

ドキュメントビルドのフレームワークを使うことで、ノンブルの設定や目次の自動生成などの細かいあれこれから開放されるためです。

Wordでもできる?こまけえこたあいいんだよ!あと自宅PCはずいぶん前にGoogleに入信したためOfficeが一切入っていません。あと出先でChromebookを使って執筆したいのでOfficeは(個人的には)一切使いません。

フレームワーク

今回のメインとなるお題です。パッと見た限り、情報と実績が豊富なのは下記の手段。
ソースは使い慣れているMarkdownをメインで使うことを前提としています。このサイトもブログもMarkdownなので、生産性という名前の慣れは抜群。

  1. Pandoc
  2. Sphinx + RecommonMark
  3. Re:View + md2review

ここで、最終的に「LateXを使ってPDFを出力する」という点は逃れられないことに気が付きます。若干の不安を抱えつつもネイティブにMarkdownを扱えるPandocがいいなとこの時点では考えていました。どうしてもMarkdown記法で解決できない問題が出てきた際に、SphinxはReST、Re:ViewRe:View記法を新たに理解する必要があるからです。

新しい記法を覚えるくらいなら原稿に力を注ぎたいので、まずはPandocの参考になる情報を漁り、下記のページに行き当たります。
情報をあさっている間、並行してMarkdownで構造に気を使いつつ原稿を書いていきます。

Pandoc + LaTeX で markdownからA5・縦書・2段組の小説本のPDFを作成 http://adbird.hatenablog.com/entry/2017/01/15/010459

バッチリドンピシャやんけ!!!!!!!

こちらの記事を参考に、章ごとに分割した.mdファイルを準備してビルド!と思うもよく見るとTeXのテンプレートファイルを使ってそこに流し込んでいる=TeXの知識がないと細かいことができない、という点に気が付き候補から脱落。素のビルド内容でも試してみましたが、後述するSphinxに比べあまりにも味気ないので、作り込みの時間をとっていないこともあって残念ながら却下となります。

Re:viewはかなり版組み向けの言語でしたが、それゆえに原稿の可読性が著しく落ちているのでやめておきました。

Sphinx+RecommonMarkでオフセ本に耐えるPDFを出力する

ようやく本題です。Sphinxは優しいのでsphinx-quickstartを実行するだけでいい感じにディレクトリを作ってくれます。 こちらは脱稿時のリポジトリです。

リポジトリ構成

nn_題名.mdが各章の原稿、index.rstがビルドの開始地点です。残りの設定はすべてconf.pyの中。conf_lua.pypLateXではなくluaLateXで出力しようとして失敗したときの名残です。
本来、index.rstの内部でファイルのインポート順を決定できるため、.mdファイルにプレフィックス番号は不要なのですがPandoc用に原稿を作成した名残です。

Markdown利用

まずはSphinxMarkdownを使えるようにします。
RecommonMarkとAutoStructifyをインポートしてググって出てきた内容を追加します。

下記の内容をconf.pyに追記・編集していきます。

import recommonmark
from recommonmark.parser import CommonMarkParser
from recommonmark.transform import AutoStructify

# recommonmark の拡張利用
github_doc_root = 'https://github.com/rtfd/recommonmark/tree/master/doc/'
def setup(app):
    app.add_config_value('recommonmark_config', {
            'url_resolver': lambda url: github_doc_root + url,
            'auto_toc_tree_section': 'Contents',
            }, True)
    app.add_transform(AutoStructify)

次に、.mdファイルを原稿ファイルと認識させます。

# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
source_suffix = ['.rst', '.md']
# source_suffix = '.rst'

source_parsers = {
    '.md' : 'recommonmark.parser.CommonMarkParser'
}

この時点で、ビルドコマンドを打って.mdファイルが原稿で使えるようになりました。
PowerShellだとディレクトリ内に実行ファイルがあっても明示的にパスを指定する必要があるのでめんどいです。

.\make clean
.\make latexpdf

Goodbye、Markdown Table

ここで原稿を見ているとあることに気が付きます。テーブルが出力されていません

RecommonMarkはCommonMarkという標準Markdownのみをサポートしているため、みんな大好きGithub Flavored Markdownでサポートされている表組は対応していません。
残念ですがReST記法を用いたテーブルに書き直す必要がありました。

前節のAutoStructifyは、.mdファイルの中でReST記法を使えるようにしてくれます。下記のようにeval_rstで囲ったコードブロック内をReSTで解釈してくれます。

```eval_rst
============   ==========
出走内容        昇格内容
============   ==========
6人未満         昇格無し
6人以上で1勝    昇格できる
6人以上で2勝    昇格は必須
============   ==========
```

conf.pyでimportするだけでなく、定義も宣言しないと動作しないので注意。

github_doc_root = 'https://github.com/rtfd/recommonmark/tree/master/doc/'
def setup(app):
    app.add_config_value('recommonmark_config', {
            'url_resolver': lambda url: github_doc_root + url,
            'auto_toc_tree_section': 'Contents',
            }, True)
    app.add_transform(AutoStructify)

原稿の見た目はできた、出力形式が問題だ

残念ながら、ここからLateXの設定と格闘することになります。conf_pylatex_elementsの設定内にメタデータJSONライクに書いていくのですが、大半の設定はpreambleというLateXの設定を直書きできる箇所でレイアウトを指定します。

文字サイズ、開き方向、原稿サイズ以外はほぼLateXです。B5サイズを使っている人がなかなか居なかったのですが、'papersize': 'b5j'を指定すれば、LateXの設定抜きでB5サイズで出力できます。

なお、最新版のSphinxではlatex_documentsmanualを指定した場合自動的にLatexのレイアウトがjsbook形式で出力されるので、インターネッツの海に浮かんでいるmanualとjsbookをマッピングさせる設定は不要です。

'extraclassoptions': 'openany,twoside'は本っぽく中央の余白を大きく取るための設定となります。

# -- Options for LaTeX output ------------------------------------------------
latex_engine = 'platex'

latex_elements = {
    'pointsize': '10pt',
    'papersize': 'b5j',
    'extraclassoptions': 'openany,twoside',
    'babel': r'''
\usepackage[japanese]{babel}
''',
    'tableofcontents': r'''
%normalで目次ページを出力しつつSphinxスタイルをやめさせる
\tableofcontents
    ''',
    'preamble': r'''
%フォント
%TexLive側コマンドで制御kanji-config-updmap-user [kozka|kozuka-pr6n|ipaex|yu-win10|status|他]
%luatexで
%\usepackage[kozuka-pr6n]{luatexja-preset}

%pdf/x使うための設定
%\usepackage[x-1a1]{pdfx}

%レイアウト
\renewcommand{\plainifnotempty}{\thispagestyle{plain}}
\setlength{\textheight}{\paperheight}
\setlength{\topmargin}{-5.4truemm}
\addtolength{\topmargin}{-\headheight}
\addtolength{\topmargin}{-\headsep}
\addtolength{\textheight}{-40truemm}
\setlength{\textwidth}{\paperwidth}
\setlength{\oddsidemargin}{-5.4truemm}
\setlength{\evensidemargin}{-5.4truemm}
\addtolength{\textwidth}{-40truemm}

% ハイパーリンクモノクロ
\hypersetup{colorlinks=false}

% 字下げ
%\setlength\parindent{1zw}

% パラグラフ間空白
%\setlength{\parskip}{0pt}

% タイトル装飾
\usepackage{titlesec}
\usepackage{picture}

% chapter
\titleformat{\chapter}[block]
{}{}{0pt}{
  \fontsize{30pt}{30pt}\selectfont\filleft
}[
  \hrule \Large{\filleft 第 \thechapter 章}
]

% section
\titleformat{\section}[block]
{}{}{0pt}
{
  \hspace{0pt}
  \normalfont \Large\bfseries{ \thesection }
  \hspace{-4pt}
}

% subsection
\titleformat{\subsection}[block]
{}{}{0pt}
{
  \hspace{0pt}
  \normalfont \large\bfseries{ \thesubsection }
  \hspace{-4pt}
}

''',
}

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
#  author, documentclass [howto, manual, or own class]).
latex_documents = [
    ('index', 'c95.tex', '泥輪事情',
    'ゲン', 'manual'),
]

セクションの装飾を入れたところ、目次が「第0章」となってしまいますが消し方がわからないので許容しました。

入稿要件の満たし方

ようやく見た目の整ったPDFが出力されるようになりました。次は入稿ファイルの要件を満たしていきます。

(2020.09) 下記に自分がやったことを記載していますが、今はPDFを変換するツールを公開している方がいるので、楽々変換できます!

github.com

フォント埋め込み

TeX Live2018を使用した場合、デフォルトでPDFにフォントが埋め込まれます。フォント埋め込みに難航している情報が大量に検索に出てきましたが、古い情報です。

IPAフォントをデフォルトで使用していますが、埋め込みフォントはkanji-config-updmap-userというコマンドを利用することで、フォントセットを簡単に切り替えることができます。

これもフォント変更に難航している情報が大量に浮かんでいますが、このコマンドで一発解決します。使えるフォントセットはこちらを参考に。游ゴシック体モリサワAdobeでお馴染みの小塚フォントもインストールされていれば使うことができます。

ページごとのPDF、ノンブル開始番号

実はラック出版では本文全体のPDFではなく、ページごとのPDFを03.pdfの用に単ページごとに入稿する必要があります。が、もちろんSphinxで素直にビルドすると全体のPDFが出てきます。

本文ページは3ページからスタートするため、目次ページのノンブルは3、本文は4ページ目からスタートしますが、Sphinxはデフォルトで目次と本文のノンブルを別物でカウントしていたためこの2つの課題を解決する必要があります。

なぜかconf.pyのpreambleに設定しても動かないため、index.rstに設定を書き込みます。

.. raw:: latex

   %目次は3ページ目から
   \setcounter{page}{3}

.. raw:: latexはこれまたLateX記法をrstに組み込むためのrst記法です。組版を刷る限りLateXからは逃れられないようです。

ページごとのPDF出力には、Sphinxの裏のLateXの裏で動いているdvipdfmxを使います。

そもそも、pLateXによるPDF出力は.tex.dvi.pdfという変換順になっており、副産物としてDVIファイルが生まれています。こいつを横取りして1ページづつビルドし直します。

こんなコマンドをプロジェクトの/_build/latex/で打っていきます。コマンドの大量生成はGoogleスプレッドシートでやりました。

mkdir .\build\latex\indv\
cd .\build\latex\
dvipdfmx -c -V 3 -s 2-2 -o './indv/03.pdf' 'c95.dvi'
dvipdfmx -c -V 3 -s 3-3 -o './indv/04.pdf' 'c95.dvi'

...

これで見た目はフォントが埋め込まれたページごとのPDFが生成されました。

最後の砦、PDF-X形式

入稿にはPDF-X形式が基本です。カラーやらリンクが埋め込まれていないPDFファイルで、印刷の段階で事故が起きないようにするための形式らしい。RGBではなくCMYKになってます。
PDF/X-1a形式で出力するためとりあえずTeX Wikiへ。

dvipdfmx には出力結果が PDF/X 準拠かどうか検証したり,準拠するように適切に処理したりする機能はありません。

……………………………

LuaLatexはdvildfmxを使わずにPDFを出力でき、\usepackage[x-1a1]{pdfx}をプリアンブルに入れることでpdf-x/a1形式を出力できます。(この際に試行錯誤したときの設定ファイルがconf_lua.pyです)

しかし、Sphinxで出力したtexファイルをLualatexに食わせてPDF出力したところ、思いっきり画像周りのレイアウトが崩れました。
しかもフォントの埋め込み方法が変わってしまいます。

この時点でゲームオーバーと判断。最終的にはdvipdfmxで出力したモノクロPDF(埋め込み画像は予めグレースケール化済み)をラック出版さんに確認出してOKもらった上で入稿しました。オンデマンド印刷なら場合によっては問題ありませんし、コピ本ならなおさら問題なく発行できます。

よく考えたらフォントも小塚を使ったのでAdobeの呪縛からも逃れられていませんでした。AdobeさんChrome OSでも使えるようにLinuxイラレを出してくれ。

結論

最も簡単なソリューションはインデザに課金することです。一人で本を作るならビルド環境よりコンテンツに力を注ぎましょう。
でも夏コミまでにかっこよくLuaLatexで出力したいから誰かナイスなPRをください