한국어 | English | 日本語
Webアプリケーションエンジニア (経験8.8年)
技術・開発
engineering
ウェブフロントエンドと バックエンド開発を扱います

Hexo、Icarusの新しいバージョンへの移行とカスタマイズ

本稿では、Node.jsのバージョン衝突によるブログ公開エラーを解決するために実施したHexo 5.0およびIcarus 4.0への移行プロセスについて解説します。単なるバージョンアップにとどまらず、JSXベースに変わったテーマ構造を分析し、ブログの可読性を高めるためにコンポーネント単位でコードを修正した実践的なカスタマイズ記録をまとめています。
HexoとIcarusテーマのメジャーバージョンアップ過程で発生したライブラリ衝突の解決策を共有します。既存のEJS方式からJSX(Inferno.js)方式に変更されたテーマ構造に合わせて、ナビゲーションバーのロゴ、プロフィールウィジェット、投稿の幅、そして特定のページ(About/Resume)のフィルタリングロジックを直接実装した事例を、コードと共に詳細に説明します。

21年最初のブログ記事公開時のエラー

仕事を通して本当に多くのことを経験し、学びますが、時間を割いて整理しどこかに書いておかないと、すぐに忘れてしまいます。社内wikiをうまく活用していましたが、より多くの方と情報や意見を共有したいと思い、ブログに記事を再度アップしようとしました。先日経験した課題に関する記事をHexoで作成し、hexo g -dで公開しようとしたところ、突然エラーが発生しました。

TypeError [ERR_INVALID_ARG_TYPE]: The "mode" argument must be integer.
Received an instance of Object at copyFile

Googleで検索してみると、Node.jsのバージョンをダウングレードする必要があり、Hexo 5.0.0以降のバージョンで修正されたというスレッドを見つけました。そういえば、昨年末に開発学習のためにライブラリやフレームワークのバージョンを一括でアップデートしていました。その中にNode.jsがバージョン15にアップデートされていたのです。Node.jsの最新バージョンをグローバルにアップデートしたため、Hexoでnpmが動作する際に衝突が発生しているようでした。

Hexo最新バージョンへの移行

使用していたバージョンはHexo 3.8.0 / Icarus 2.3.0でしたが、確認した最新バージョンはHexo 5.3.0 / Icarus 4.1.1でした。まずHexoをバージョンアップするために、package.jsonから既存のパッケージをすべて削除し、Hexoのバージョンを5.3.0に変更しました。npm installを実行した後、壊れたパッケージを一つずつ追加していくのが面倒だったので、新しいディレクトリでhexo initを実行して自動生成されたpackage.jsonを参照しました。

Hexo's package.json Version Change Logs

最新のHexoと古いIcarusの衝突

Hexoのバージョンアップ後、hexo serverでローカルにページを表示してみたところ、Icarusテーマファイルで特定の変数が見つからないなどのエラーが頻繁に発生しました。Googleで検索すると、これもバージョン問題であることが判明しました。そこで、既存のIcarusテーマのカスタマイズ設定である_config.ymlだけをバックアップしたまま、すべてを削除し、Icarusを最新バージョンで(1) npm installではなく(2) git submoduleを通じて(カスタマイズのために)インストールしました。再度実行すると、JSXコードだけがそのまま露出する問題が発生しましたが、JSXだったのでReact.jsだと思っていたらInferno.jsで開発されていたため、該当ライブラリをインストールして解決しました。

Icarus JSXカスタマイズ

どんなテーマでも修正したくなる几帳面な性格のせいで、以前のIcarusテーマはEJSコードを分析して個別にカスタマイズしていました。以前はEJSベースだったIcarusが、今はJSXベースになったため、以前のカスタマイズコードをそのまま使用することはできず、再度分析して修正する必要がありました。 フロントエンド開発をずっとReact.jsで行ってきており、Icarusの開発フレームワークであるInferno.jsもReact-likeを標榜しているため、デバッグさえできればそれほど難しくはないだろうと思っていましたし、実際その通りでした。ただし、IcarusプロジェクトがJSXに変換されたことで、過去の乱雑だったEJS構造からコンポーネント単位でモジュール化がしっかり行われたため、ページごとにレンダリングを変える必要がある部分は、共通コンポーネントに例外条件を加える形で処理する必要がありました。 以下のカスタマイズコードをご覧いただければご理解いただけるでしょう。

VSCodeを使ったHexoのデバッグ

テーマを修正するためには、まずHexo、IcarusのJSXがページにどのようにレンダリングされるかを知る必要があります。ローカルでHexoのテストのために実行するhexo serverコマンドは、hexo-cliに定義されているnpmスクリプトを実行したものです。テーマ設定を変更したり、記事を修正したりするとすぐにローカルページに適用されますが、これはnpmを通じて動的にレンダリングされているためです。私はIcarusテーマがどのように動作するのかを理解し、修正した自分のコードが正しく動作するかを確認するために、VSCodeを通じてデバッグを行いました。VSCodeでのデバッグ方法は、本ブログに公開している「VSCodeでHexoをデバッグする方法」という記事で詳しく説明していますので、参考にしてください。

ナビゲーションバーのロゴ

最上部のナビゲーションバーには、デフォルトでロゴ画像を配置するように設定されていますが、私はブログを表現できる言葉に置き換え、フォントサイズを設定しました。

Named Logo Navigation Bar

_config.icarus.yml
# Path or URL to the website's logo
logo:
    text: Crucian Carp
include/style/navbar.styl
.navbar-logo
    img
        max-height: $logo-height
    font-size: 1.4rem

左ウィジェット - プロフィール再設定

本ブログでは、ウェブ検索の利便性を犠牲にしても記事の構成を最もシンプルにしたかったため、タグは一切使用していません。左ウィジェットのプロフィールで、投稿数とカテゴリ数のみを表示するように、タグ数は表示しないようにコードを削除しました。

Tag Removed Profile

layout/widget/profile.jsx
<div class="level-item has-text-centered is-marginless">
    <div>
        <p class="heading">{counter.tag.title}</p>
        <a href={counter.tag.url}>
            <p class="title">{counter.tag.count}</p>
        </a>
    </div>
</div>
layout/widget/profile.jsx
tag: {
    count: tagCount,
    title: _p('common.tag', tagCount),
    url: url_for('/tags')
}

投稿上部の時間フォーマット変更

Icarusテーマはデフォルトで、投稿の上部に記事が最初に作成されてからどのくらい経過したか、更新されてからどのくらい経過したかを表示しますが、個人的には昔のNaverのようなWYSIWYGブログで表示される投稿の初回作成日を日付形式で見るのを好むため、これも変更しました。

Erased Elapsed Time

layout/common/article.jsx
<div class="level-left">
    {/* Creation Date */}
    {page.date && <span class="level-item" dangerouslySetInnerHTML={{
        __html: _p(`<div>${date(page.date)}</div>`)
    }}></span>}
    {/* author */}
    {page.author ? <span class="level-item"> {page.author} </span> : null}

フォント変更

Hexoブログを始めたとき、すべての記事をハングルで書く予定だったので、字間がごくわずかに開いている方が可読性が良いと判断し、Hexoを使い始めて以来使用しているNanum Gothicフォントを継続して使用することにしました。

include/style/base.styl
$family-sans-serif ?= 'Ubuntu', 'Nanum Gothic', sans-serif
$family-code ?= 'Source Code Pro', monospace, 'Microsoft YaHei'

ウィジェットと投稿の幅の再設定

Icarusが提供するウィジェットには、(1)右と(2)左がありますが、両方を使用すると中央の投稿の幅が短くなり、可読性を損なう可能性があると判断しました。左ウィジェットのみを使用した場合でも、ウィジェットの幅が投稿の幅に比べて長いと感じたため、調整を行いました。

Previous Widget Width

Newest Widget Width

Icarusの幅の配分は、Bulma CSSの12セルルールを使用しています。以前は以下の通りでした。

本ブログでは、記事の可読性を高めるために左ウィジェットのみを使用するため、

と、投稿の幅を8から9に、ウィジェットの幅を4から3に再定義しました。

layout/common/widgets.jsx
function getColumnSizeClass(columnCount) {
    switch (columnCount) {
        case 2:
            return 'is-3-tablet is-3-desktop is-3-widescreen';
        case 3:
            return 'is-3-tablet is-3-desktop is-2-widescreen';
    }
    return '';
}
layout/layout.js
<div class="columns">
    <div class={classname({
        column: true,
        'order-2': true,
        'column-main': true,
        'is-12': columnCount === 1,
        'is-9-tablet is-9-desktop is-9-widescreen': columnCount === 2,
        'is-9-tablet is-9-desktop is-8-widescreen': columnCount === 3
    })} dangerouslySetInnerHTML={{ __html: body }}></div>
    <Widgets site={site} config={config} helper={helper} page={page} position={'left'} />
    <Widgets site={site} config={config} helper={helper} page={page} position={'right'} />
</div>

Aboutページでのウィジェット、プラグインの削除

上部のナビゲーションバーから「About」をクリックすると、私に関する概要情報を確認できます。また、個人の履歴書ページも別途作成しており、Google DocsやLinkedInにアクセスしなくても、私の経歴を一目で確認できるようにしています。

旧IcarusのEJS時代には、AboutとResumeの各ページはそれぞれ独立したEJSページを持っていたため、そのページだけを修正すればよかったのですが、新しいIcarusのJSXでは、投稿のコンポーネントがAbout、Resumeなどすべてのページの基本コンポーネントとして使用されていました。そこで、特定のページでのみウィジェットとプラグインを表示しないようにフィルタリングするロジックを、静的リストを作成して導入しました。

Previous About Page

Newest About Page

layout/layout.jsx
const isAboutPage = [ "about/index.html", "resume/index.html" ].includes(page.path);
layout/layout.jsx
<Head site={site} config={config} helper={helper} page={page} />
<body class={`is-${columnCount}-column`}>
    <Navbar config={config} helper={helper} page={page} />
    <section class="section">
        <div class="container">
            <div class="columns">
                <div class={classname({
                    column: true,
                    'order-2': true,
                    'column-main': true,
                    'is-12': columnCount === 1,
                    'is-9-tablet is-9-desktop is-9-widescreen': columnCount === 2,
                    'is-9-tablet is-9-desktop is-8-widescreen': columnCount === 3
                })} dangerouslySetInnerHTML={{ __html: body }}></div>
                {!isAboutPage && <Widgets site={site} config={config} helper={helper} page={page} position={'left'} />}
                {!isAboutPage && <Widgets site={site} config={config} helper={helper} page={page} position={'right'} />}
layout/common/article.jsx
const isAboutPage = [ "about/index.html", "resume/index.html" ].includes(page.path);
layout/common/article.jsx
{/* Licensing block */}
{!isAboutPage && !index && article && article.licenses && Object.keys(article.licenses)
    ? <ArticleLicensing.Cacheable page={page} config={config} helper={helper} /> : null}

{/* Tags */}
{!isAboutPage && !index && page.tags && page.tags.length ? <div class="article-tags is-size-7 mb-4">
    <span class="mr-2">#</span>
    {page.tags.map(tag => {
        return <a class="link-muted mr-2" rel="tag" href={url_for(tag.path)}>{tag.name}</a>;
    })}
</div> : null}

{/* "Read more" button */}
{!isAboutPage && index && page.excerpt ? <a class="article-more button is-small is-size-7" href={`${url_for(page.link || page.path)}#more`}>{__('article.more')}</a> : null}

{/* Share button */}
{!isAboutPage && !index ? <Share config={config} page={page} helper={helper} /> : null}

{/* Donate button */}
{!isAboutPage && !index ? <Donates config={config} helper={helper} /> : null}

{/* Post navigation */}
{!isAboutPage && !index && (page.prev || page.next) ? <nav class="post-navigation mt-4 level is-mobile">

{/* Comment */}
{!isAboutPage && !index ? <Comment config={config} page={page} helper={helper} /> : null}

今ご覧いただいているこのブログとこの記事は、(現在のTPOブログの直前、2021年に作成された)ブログとこの記事は、上記の要素をすべてカスタマイズしたIcarusテーマで構成されています。数年前にHexoやIcarusを適用されたことがあり、移行を控えている方や、カスタムで修正したい方に本記事がお役に立てば幸いです。今後、CSSやJSXに細かな修正が入る可能性はありますが、すべてを個別に更新するつもりはありません。

Hexo、Icarusの新しいバージョンへの移行とカスタマイズ
Author
Aaron
Posted on
Licensed Under
CC BY-NC-SA 4.0
CC BY-NC-SA 4.0
同じカテゴリーの関連記事
最新記事
LLMフィルターが奪う会話の筋肉とコミュニケーション様式
会話における無礼さを濾過し、洗練された回答を生成するLLMツールが日常化した現代において、私たちは本当に思慮深い会話をしているのだろうか?リアルタイムのコミュニケーションにおける数多くの失敗を通じて磨かれるべき会話能力が、外部ツールに依存することで退化している現象と、それがもたらす社会的な不安や世代間の行動様式の変化について考察する。
シニア採用における年俸交渉の最適なタイミングと戦略
年俸交渉は単なる数字の交換ではなく、心理的な駆け引きとタイミングが重要です。本稿では、企業側にとって、候補者が計算的な態度を取りがちな最終合格後よりも、採用プロセスの初期段階から段階的に交渉を進めることが、なぜより効率的であり、率直な情報の共有に繋がるのかを考察します。
法治主義の限界と人間の多様性
全ての人間の行為を単一の法体系で規制できるという信念は、傲慢であるかもしれない。この記事は、中世の階層的な統制から脱却し、現代の無限の自由を手に入れた人類が直面する法治主義の逆説と、多様性という名のもとに深化する社会的強制力と他者への悪魔化現象を鋭く分析する。
토스트 예시 메세지