目次

Stencilとは

Stencilとは、Ionic Frameworkチームが開発しているWeb Componentsコンパイラで、Web ComponentsベースのUIライブラリやWebアプリの開発が可能です。TypeScriptやJSXを標準でサポートしていたり、ローカルサーバも付属されているなど、環境構築が楽で柔軟性の高いコンポーネントを作成できるので個人的に気に入っているフレームワークの1つです。

そして、Stencilはv1.13.0からSSG(Static Site Generation)機能が実装され、Next.jsやNuxt.jsのようにSPAだけでなく静的サイトも生成できるようになりました。気になったので試しにこのブログのAPIなどを使って遊んでみてわかったSSGのポイントや所感を書きました。

実際に作業したものはGitHubに上げているので、気になる方はこちらを参考にしてみてください。

Stencilで静的サイトをビルドする方法

Stencilはstencil buildコマンドでプロジェクトをビルドでき、次のようにstencil.config.tsにアプリのビルド設定(type: 'www')があればプロジェクトがSPAとしてビルドされます。ビルド先のディレクトリはデフォルトが/wwwで、dirで任意のパスを指定することもできます。

import { Config } from '@stencil/core';

export const config: Config = {
  outputTargets: [
    {
      type: 'www',
      baseUrl: 'https://example.com/'
    }
  ]
};

そして、静的サイトとしてビルドするには、次のように--prerenderオプションをつけるだけです。--prerenderオプションを指定すると、ビルド時にprerenderingを行い、静的サイトとして出力されます。

npx stencil build --prerender

簡単なサイトであれば、特別複雑な作業を必要とせずに、これだけで静的出力ができるようになるのでとても簡単です。

SSGに関するconfig

静的出力やprerenderingに関する設定は、stencil.config.tsとは別に作成して、次のようにprerenderConfigで読み込みます。ファイル名は任意のもので良さそうですが、ドキュメントのコードサンプルではprerender.config.tsが採用されているので同じようにするのがわかりやすくて良いでしょう。

// stencil.config.ts
import { Config } from '@stencil/core';

export const config: Config = {
  outputTargets: [
    {
      type: 'www',
      baseUrl: 'https://example.com/',
      prerenderConfig: './prerender.config.ts',
    }
  ]
};

prerender.config.tsには次のようにカスタマイズしたい項目を設定します。

// prerender.config.ts
import { PrerenderConfig } from '@stencil/core';

export const config: PrerenderConfig = {
  filePath(url, filePath) {
    // 静的出力されたファイルパスで何かしたい
  },
  ...
};

設定の詳細は「Prerender Config - Stencil」を参照してください。

静的に出力するページを自動で検出してくれる

個人的にすごい便利だと思った機能です。

Stencilは静的出力するときに、対象となるページのURLを自ら検出して自動で選定してくれます。これは、/post/:idのような動的なURLであっても、ページ内でリンクが繋がってさえいれば検出してくれます。

Nuxt.jsでは動的なURLはnuxt.config.jsgenerate.routesで指定する必要がありますが、Stencilの場合はルートページ(デフォルトは/)を起点に、導線が繋がっていれば勝手に対象に含めてくれるのでとても楽です。

ルートページを起点として、リンクが繋がっていないページも静的に出力する場合は、prerender.config.tsentryUrlsを設定して起点となるURLを増やします。またcrawlUrls: falseを指定すると、URLを自動で検出しなくなるので、全て手動で指定したい場合はこの辺りを変更します。

prerendering時だけ特定の処理を実行させたい時

Build.isBrowserを使うことで、prerendering時だけやブラウザだけで実行させたい処理を分岐させることが可能です。ブラウザで実行できない処理などはこれを使って分岐できます。

import { Build } from '@stencil/core';

if (Build.isBrowser) {
  // ブラウザのみで実行
} else {
  // prerendering時のみ実行
}

Headless CMSなどのAPIを叩いてprerenderingする

JAMstackな使い方もできます。Headless CMSなどを使用して、サーバの情報を静的にビルドする場合は、componentWillLoad()を使います。componentWillLoad()はNuxt.jsのasyncData()や、Next.jsのgetInitialProps()相当のものだと考えて大丈夫です。

次のソースは、/post/:idのようなURLで簡易的なブログの記事ページを実装する例です。(記事情報の型定義とか雑ですみません。)

// /src/components/app-post/app-post.tsx
import { Component, Prop, State, h } from '@stencil/core';

@Component({
  tag: 'app-post',
  styleUrl: 'app-post.css',
  shadow: true
})
export class AppPost {
  @Prop() match: any;

  @State() title: string = '';
  @State() content: string = '';

  async componentWillLoad() {
    const postId = this.match.params.id;
    const response = await fetch(`${API_URL}/post/${postId}`);
    const post = await response.json();

    this.title = post.title;
    this.content = post.content;
  }

  render() {
    return (
      <div class="app-post">
        <h1 class="title">{this.title}</h1>
        <div class="content" innerHTML={this.content}></div>
      </div>
    );
  }
}

metaタグを動的に指定する

metaタグを動的に指定するには、@stencil/helmetを使います。react-helmetと同じように書けるので、React使いには簡単ですね。

先ほどの記事ページの例に追加すると次のようになります。

// /src/components/app-post/app-post.tsx
import { Component, Prop, State, h } from '@stencil/core';
import Helmet from '@stencil/helmet';

export class AppProfile {
  @Prop() match: any;

  @State() title: string = '';
  @State() description: string = '';
  @State() content: string = '';

  async componentWillLoad() {
    const postId = this.match.params.id;
    const response = await fetch(`${API_URL}/post/${postId}`);
    const post = await response.json();

    this.title = post.title;
    this.content = post.content;
    this.description = post.description;
  }

  render() {
    return (
      <div class="app-post">
        <Helmet>
          <title>{this.title} - Site Name</title>
          <meta name="description" content={this.description} />
          { /* その他OGPとか */ }
        </Helmet>

        <h1 class="title">{this.title}</h1>
        <div class="content" innerHTML={this.content}></div>
      </div>
    );
  }
}

実際に使う場合は、各ページにmeta情報を全部書くのは面倒なのでcomponent化するのが良さそう。

まとめ

  • stencil build--prerenderをつけるだけで静的ビルドできる
  • 静的出力を自動で検出してくれるのすごい便利
  • componentWillLoad()@stencil/helmetを使ってJAMstackもできる

個人的に、業務で静的サイトを作成する際の有力候補かと言われるとそうではないですが、コンポーネントをWeb Componentsで作るので再利用生の高いサイトを作ることはできます。また、StencilでUI Componentを作成した時のドキュメントサイトやサンプルページはSPAではなく静的出力することで表示パフォーマンスも向上するのでやる価値はあると思います。デザインなどをちゃんと組んでいない状態で雑にLighthouseで計測した結果も、特に問題もなくいい感じでした。

このブログを近いうちにNuxt.jsからGatsbyJSにリプレイスしようと思っていたのですが、もう少しいろいろ試してみて問題なければStencilで作ってみるのもありだなーと思いました。