現代網頁開發已變得非常強大,但也越來越複雜。

許多專案在撰寫單一頁面之前,就必須先安裝數百 MB 的依賴套件。框架、打包工具、路由器、樣板系統、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

頁面是普通的 HTML,可選擇性加入 Rino 語法:

<!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 的 import:

<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 import、跳過循環 import、跳過 URL import,並壓縮 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

若 bundle 外的程式碼需要呼叫函式,請將其附加到 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 版是一個重要的里程碑,但這只是開始。未來版本還有許多想法,我期待根據實際使用情況持續改進這個專案。