モダンなウェブ開発は非常に強力になりました。しかし同時に複雑さも増しています。

多くのプロジェクトでは、1ページを書く前に数百メガバイトの依存関係をインストールすることから始まります。フレームワーク、バンドラー、ルーター、テンプレートシステム、CSSツール、ランタイムライブラリはすべて重要な問題を解決しますが、追加の複雑さももたらします。

私は別の方法を求めました。

私は、プレーンなHTMLから始まりながら、今日の開発者が期待する機能を提供するウェブサイトを構築したいと考えました:

  • 再利用可能なコンポーネント
  • Markdownサポート
  • TypeScript
  • CSSおよびJavaScriptのバンドル
  • 国際化(i18n)
  • コンテンツコレクション
  • RSS/Atomフィード
  • サイトマップ生成
  • 高速な開発ビルド

そのアイデアがRino.jsになりました。

Rino.jsとは?

Rino.jsは、静的ウェブサイト、ドキュメント、ブログ、ポートフォリオ、企業サイト、その他のコンテンツ駆動型プロジェクトを構築するためのHTMLファーストのウェブサイトコンパイラです。

Rino.jsは、カスタムテンプレート言語を導入したり、フロントエンドフレームワークを必要としたりする代わりに、HTMLを主要な言語として扱います。ページは有効なHTMLのまま、ビルド時の少数の規約を通じて追加機能が追加されます。

目標はシンプルです:

HTMLを書く。最適化された静的ウェブサイトを生成する。

なぜHTMLファーストなのか?

HTMLは何十年も存在していますが、モダンなウェブ開発ではしばしば別の言語によって生成されるものとして扱われます。

Rino.jsは逆のアプローチを取ります。

JSXや別のテンプレート言語でコンポーネントを書く代わりに、コンポーネントは単なるHTMLファイルです。

<component rino-import="header"></component>

Enter fullscreen mode Exit fullscreen mode

これだけです。

コンパイラはビルド中にコンポーネントを置き換え、ランタイム依存関係のないプレーンな静的HTMLを生成します。

Rino.jsを始める

Rino.jsには、デフォルトのプロジェクトを提供するために設計されたコマンドがあります。

npm create rino@latest

Enter fullscreen mode Exit fullscreen mode

プロジェクトの構造

Rino.jsプロジェクトは通常このようになります:

my-site/
  rino-config.js
  dev.js
  generate.js
  feed.js
  sitemap.js
  backoffice.js
  pages/
    index.html
    about.html
  components/
    header.html
    footer.html
  public/
    images/
      photo.webp
  scripts/
    export/
      app.js
      dashboard.ts
  styles/
    export/
      site.css
  mds/
    intro.md
  contents/
    en/
      blog/
        1-first-post.md
  content-theme/
    en/
      content.html
      content-list.html
  i18n/
    en/
      index.json
    ko/
      index.json

Enter fullscreen mode Exit fullscreen mode

必要なフォルダだけが必要です。小さな静的サイトの場合、pages/components/public/scripts/export/styles/export/で十分かもしれません。

設定

プロジェクトの設定はrino-config.jsにあります。

export default {
  dist: "./dist",
  port: 3000,
  site: {
    url: "https://example.com",
  },
  sitemap: ["https://example.com/custom-page"],
  i18n: {
    defaultLocale: "en",
    locales: ["en", "ko"],
  },
};

Enter fullscreen mode Exit fullscreen mode

distは出力フォルダを制御し、portは開発サーバーを制御し、site.urlはサイトマップとフィードに使用され、sitemapは追加のURLを追加し、i18nはローカライズされたページ生成を制御します。

ページ

pages/内のファイルは出力でHTMLファイルになります。

pages/index.html        -> dist/index.html
pages/about.html        -> dist/about.html
pages/blog/index.html   -> dist/blog/index.html

Enter fullscreen mode Exit fullscreen mode

ページはオプションのRino構文を含む通常のHTMLです:

<!doctype html>
<html>
  <head>
    <title><lang>site.title</lang></title>
    <link rel="stylesheet" href="/styles/site.css" />
    <script src="/scripts/app.js"></script>
  </head>
  <body>
    <component rino-import="header"></component>

    <main>
      <h1><lang>home.heading</lang></h1>
      <script rino-type="md" rino-import="intro" rino-tag="section"></script>
    </main>

    <component rino-import="footer" rino-tag="footer"></component>
  </body>
</html>

Enter fullscreen mode Exit fullscreen mode

コンポーネント

コンポーネントはcomponents/にあります。<component>を使って、ページや別のコンポーネント内にレンダリングします。

<component rino-import="header"></component>

Enter fullscreen mode Exit fullscreen mode

これにより以下が読み込まれます:

components/header.html

Enter fullscreen mode Exit fullscreen mode

ネストされたコンポーネントパスがサポートされています:

<component rino-import="layout/sidebar"></component>

Enter fullscreen mode Exit fullscreen mode

これにより以下が読み込まれます:

components/layout/sidebar.html

Enter fullscreen mode Exit fullscreen mode

コンポーネントの出力を要素でラップし、そのラッパーに属性を渡したい場合はrino-tagを使用します。

<component
  rino-import="button"
  rino-tag="button"
  type="button"
  class="button"
  onclick="alert('Hello')"
></component>

Enter fullscreen mode Exit fullscreen mode

components/button.htmlに以下が含まれている場合:

Click me

Enter fullscreen mode Exit fullscreen mode

生成されるHTMLは以下のようになります:

<button type="button" class="button" onclick="alert('Hello')">Click me</button>

Enter fullscreen mode Exit fullscreen mode

バージョン3では、rino-importを使用してください。古いrino-path構文はサポートされていません。

Markdown

Rino.jsはmds/からMarkdownスニペットをレンダリングできます。

mds/intro.md

Enter fullscreen mode Exit fullscreen mode

<script rino-type="md" rino-import="intro" rino-tag="section" class="intro"></script>

Enter fullscreen mode Exit fullscreen mode

これによりmds/intro.md<section>内にレンダリングされます。

Markdownをインラインで書くこともできます:

<script rino-type="markdown" rino-tag="article" type="text/markdown">
  ## Hello from Markdown

  This content is rendered during the build.
</script>

Enter fullscreen mode Exit fullscreen mode

サポートされているMarkdownタイプはmdmarkdownです。

テンプレートスクリプト

テンプレートスクリプトは生成中に実行されます。console.logで生成されたページにHTMLを出力します。

JavaScriptテンプレートスクリプト:

<script rino-type="js" type="text/javascript">
  console.log("<ul>");
  for (const item of ["Home", "Blog", "About"]) {
    console.log(`<li>${item}</li>`);
  }
  console.log("</ul>");
</script>

Enter fullscreen mode Exit fullscreen mode

TypeScriptテンプレートスクリプト:

<script rino-type="ts" type="text/typescript">
  const message: string = "Generated with TypeScript";
  console.log(`<p>${message}</p>`);
</script>

Enter fullscreen mode Exit fullscreen mode

テンプレートスクリプトはNode.jsのインポートを使用できます:

<script rino-type="js" type="text/javascript">
  import os from "os";
  console.log(`<p>Built on ${os.type()}</p>`);
</script>

Enter fullscreen mode Exit fullscreen mode

バージョン3では、Rino.jsがすべてのテンプレートスクリプトタグごとに新しいプロセスを作成する代わりに、独立した子プロセスランナーを再利用するため、テンプレートの実行が高速化されています。

ブラウザスクリプト

ブラウザスクリプトは通常のブラウザJavaScriptです。ソースファイルをscripts/export/に置きます。

scripts/export/app.js -> dist/scripts/app.js
scripts/export/app.ts -> dist/scripts/app.js

Enter fullscreen mode Exit fullscreen mode

生成されたファイルをHTMLから参照します:

<script src="/scripts/app.js"></script>

Enter fullscreen mode Exit fullscreen mode

JavaScriptの例:

document.querySelector("button")?.addEventListener("click", () => {
  console.log("Clicked");
});

Enter fullscreen mode Exit fullscreen mode

TypeScriptの例:

const button = document.querySelector<HTMLButtonElement>("button");

button?.addEventListener("click", () => {
  console.log("Clicked with TypeScript");
});

Enter fullscreen mode Exit fullscreen mode

Rino.jsはスクリプトをバンドルし、出力を最小化します。最小化はファイルサイズを小さくするのに役立ちます。

CSS

CSSファイルをstyles/export/に置きます。

styles/export/site.css -> dist/styles/site.css

Enter fullscreen mode Exit fullscreen mode

@import "../theme.css";

body {
  font-family: system-ui, sans-serif;
}

Enter fullscreen mode Exit fullscreen mode

生成されたCSSを参照します:

<link rel="stylesheet" href="/styles/site.css" />

Enter fullscreen mode Exit fullscreen mode

Rino.jsはローカルCSSインポートを解決し、循環インポートをスキップし、URLインポートをスキップし、CSS出力を最小化します。

パブリックアセット

public/内のファイルは出力ルートにコピーされます。

public/favicon.ico       -> dist/favicon.ico
public/images/photo.webp -> dist/images/photo.webp

Enter fullscreen mode Exit fullscreen mode

ルート相対パスを使用します:

<img src="/images/photo.webp" alt="Photo" />
<link rel="icon" href="/favicon.ico" />

Enter fullscreen mode Exit fullscreen mode

インラインスタイル、JavaScript、TypeScriptのエクスポート

rino-exportを使用すると、ページやコンポーネントは、外部ファイルを生成しながらも関連するCSSとJavaScriptをHTMLの近くに保持できます。

エクスポートされたタグは最終的なHTMLから削除され、生成されたアセットに書き込まれます。

CSSエクスポート

<section class="callout">
  <h2>Hello</h2>
</section>

<style rino-export="/site.css">
  .callout {
    border: 1px solid #ddd;
    padding: 1rem;
  }
</style>

Enter fullscreen mode Exit fullscreen mode

これにより以下が書き込まれます:

dist/styles/site.css

Enter fullscreen mode Exit fullscreen mode

JavaScriptエクスポート

<script rino-export="/site.js">
  console.log("Exported browser script");

  window.openMenu = function () {
    document.body.classList.toggle("menu-open");
  };
</script>

Enter fullscreen mode Exit fullscreen mode

これにより以下が書き込まれます:

dist/scripts/site.js

Enter fullscreen mode Exit fullscreen mode

TypeScriptエクスポート

<script rino-export="/site.ts" type="text/typescript">
  const message: string = "Exported TypeScript";
  console.log(message);
</script>

Enter fullscreen mode Exit fullscreen mode

これによりJavaScriptが書き込まれます:

dist/scripts/site.js

Enter fullscreen mode Exit fullscreen mode

バンドル外のコードが関数を呼び出す必要がある場合は、windowにアタッチします。

<script rino-export="/site.ts" type="text/typescript">
  (window as any).testExportTypescript = function (): void {
    console.log("Called from outside");
  };
</script>

Enter fullscreen mode Exit fullscreen mode

次にHTMLから呼び出します:

<button onclick="window.testExportTypescript()">Run</button>

Enter fullscreen mode Exit fullscreen mode

複数のタグを同じファイルにエクスポートできます。Rino.jsは一意のブロックを追加し、完全に同一のブロックを無視します。

i18n

ページやコンポーネントで<lang>タグを使用します:

<h1><lang>name</lang></h1>
<p><lang>body.hero.copy</lang></p>
<p><lang>items[0].title</lang></p>

Enter fullscreen mode Exit fullscreen mode

i18n/{locale}/に一致するJSONファイルを作成します。

pages/index.html
i18n/en/index.json
i18n/ko/index.json

Enter fullscreen mode Exit fullscreen mode

i18n/en/index.jsonの例:

{
  "name": "Rino.js",
  "body": {
    "hero": {
      "copy": "Simple static websites from HTML."
    }
  },
  "items": [
    {
      "title": "First item"
    }
  ]
}

Enter fullscreen mode Exit fullscreen mode

デフォルトのロケールは通常のページパスで生成されます。他のロケールは、/ko/index.htmlなどのロケールプレフィックスの下に生成されます。

欠落したキーはビルドを中断する代わりにHTMLに残ります。リテラルの言語タグを保持するには、エスケープします:

\<lang>name\</lang>

Enter fullscreen mode Exit fullscreen mode

コンテンツコレクション

Rino.jsは、ブログなどのMarkdownコンテンツコレクションを構築できます。

contents/en/blog/1-first-post.md

Enter fullscreen mode Exit fullscreen mode

Markdownコンテンツは、HTMLコメント内のJSONメタデータで始めることができます:

<!--
{
  "title": "Welcome to the Blog",
  "time": "2026-07-29T00:00:00.000Z",
  "description": "The first post on our Rino.js blog."
}
-->

# Hello World

This is the first post.

Enter fullscreen mode Exit fullscreen mode

これにより以下が生成されます:

dist/contents/en/blog/1-first-post.html

Enter fullscreen mode Exit fullscreen mode

一致するテンプレートは以下にあります:

content-theme/en/content.html

Enter fullscreen mode Exit fullscreen mode

コンテンツテンプレートの例:

<!doctype html>
<html lang="en">
  <head>
    <title>{{ content.title }}</title>
  </head>
  <body>
    <main>
      <h1>{{ content.title }}</h1>
      <p>{{ content.time }}</p>
      <article>{{ content.body }}</article>
    </main>
  </body>
</html>

Enter fullscreen mode Exit fullscreen mode

メタデータと生成されたフィールドにアクセスできます:

{{ content.title }}
{{ content.description }}
{{ content.body }}
{{ content.urlPath }}
{{ content.nearby[0].title }}

Enter fullscreen mode Exit fullscreen mode

ループの場合は、テンプレートスクリプトを使用します:

<script rino-type="js" type="text/javascript">
  const args = process.argv;
  const content = JSON.parse(args[args.length - 1]);

  console.log("<ul>");
  for (const post of content.nearby || []) {
    console.log(`<li><a href="${post.link}">${post.title}</a></li>`);
  }
  console.log("</ul>");
</script>

Enter fullscreen mode Exit fullscreen mode

コンテンツリスト

コンテンツリストテンプレートは、コンテンツテンプレートの隣にあります:

content-theme/en/content-list.html

Enter fullscreen mode Exit fullscreen mode

Rino.jsは各カテゴリに対してページ分割されたリストページを作成します:

dist/contents-list/en/blog/blog-1.html

Enter fullscreen mode Exit fullscreen mode

例:

<ol>
  <script rino-type="js" type="text/javascript">
    const args = process.argv;
    const data = JSON.parse(args[args.length - 1]);

    for (const item of data.contentList) {
      console.log(`<li><a href="${item.link}">${item.title}</a></li>`);
    }
  </script>
</ol>

Enter fullscreen mode Exit fullscreen mode

利用可能なデータには以下が含まれます:

{{ contentList[0].title }}
{{ pagination.prevLink }}
{{ pagination.nextLink }}

Enter fullscreen mode Exit fullscreen mode

通常、配列をレンダリングするにはテンプレートスクリプトの方が良い選択肢です。

サイトマップとフィード

Rino.jsは、ページ、ローカライズされたページ、コンテンツページ、追加の設定URLからサイトマップを生成できます。

import { generateProjectSitemapFile } from "rinojs";
import config from "./rino-config.js";

await generateProjectSitemapFile(process.cwd(), config);

Enter fullscreen mode Exit fullscreen mode

また、contents/からRSSとAtomフィードを生成することもできます。

import { generateProjectFeedFiles } from "rinojs";
import config from "./rino-config.js";

await generateProjectFeedFiles(process.cwd(), config);

Enter fullscreen mode Exit fullscreen mode

典型的な出力:

dist/sitemap.xml
dist/rss.xml
dist/atom.xml
dist/rss-en.xml
dist/atom-en.xml

Enter fullscreen mode Exit fullscreen mode

私の目標

別のフロントエンドフレームワークを構築するのではなく、軽量なコンパイラでプレーンなHTMLがどこまでいけるかを探求したいと考えました。

その結果、HTMLを中心に据えながら、必要に応じて実用的なビルド時機能を追加するワークフローができました。

バージョン3は大きなマイルストーンですが、始まりに過ぎません。将来のリリースには多くのアイデアがあり、実世界の使用に基づいてプロジェクトを改善し続けることを楽しみにしています。