😺

HonoXにワクワクしたので、Markdown式のWikiっぽいものを作ってみた

2024/02/29に公開2

はじめに

こんにちは!
犬専用の音楽アプリ オトとりっぷでエンジニアしています、足立です!

https://www.oto-trip.com/

この記事では、HonoX を使って簡単な Markdown 式の Blog っぽいものを作ってみようと思います。
HonoX ってどんな感じか気になっておられる方に参考になれば幸いです。

そもそも HonoX って?

製作者さん本人の記事が非常に分かりやすいので、ぜひそちらをご覧ください!

https://zenn.dev/yusukebe/articles/724940fa3f2450

一部引用すると、

HonoX とは一言で言うと「Hono と Vite を組み合わせたメタフレームワーク」です。

とのことです。

HonoX は、先日 Hono v4 がリリースされたことに合わせて公開されました。
今回は、製作者さん本人の Example レポジトリを参考にして進めていきます。

https://github.com/yusukebe/honox-examples/

Starter Template

最初にやるとことは、以下 3 点です。

  1. npm create hono@latestコマンドで Starter Template のファイル群を生成する
  2. Target directoryに移動して、開発サーバーを起動する
  3. http://localhost:5173/にアクセスし、スターター画面が表示されるのを確認する

簡単ですね。
今の時点でプロジェクトは以下の通りになっています。
(公式のProject Structureとは一致しないのでご注意ください。)

root
├── app
│   ├── global.d.ts
│   ├── islands
│   │   └── counter.tsx
│   ├── routes
│   │   ├── _renderer.tsx
│   │   └── index.tsx
│   └── server.ts
├── package.json
├── tsconfig.json
└── vite.config.ts

特徴的なディレクトリを簡単に説明すると、以下の通りです。

  • routes : このディレクトリ以下のファイルがファイルベースルーティングのエントリーファイルとなります
  • islands : このディレクトリ以下のファイルを import するとクライアントサイドに配信されuseStateなどが利用可能になります

mdx の導入

HonoX 公式ドキュメントの通り進めていきます。
まず、必要なライブラリをインストールします

$ npm install @mdx-js/rollup remark-frontmatter remark-mdx-frontmatter

次に、vite.config.ts を以下の通り修正します。

vite.config.ts
import pages from '@hono/vite-cloudflare-pages';
+import mdx from '@mdx-js/rollup';
import honox from 'honox/vite';
import client from 'honox/vite/client';
+import remarkFrontmatter from 'remark-frontmatter';
+import remarkMdxFrontmatter from 'remark-mdx-frontmatter';
import { defineConfig } from 'vite';

export default defineConfig(({ mode }) => {
  if (mode === 'client') {
    return {
      plugins: [client()],
    };
  } else {
    return {
+      build: {
+        emptyOutDir: false,
+      },
      plugins: [
        honox(),
        pages(),
+        mdx({
+          jsxImportSource: 'hono/jsx',
+          remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter],
+        }),
      ],
    };
  }
});

これで、mdx ファイルを読み込めるようになりました。
app/routes/posts/以下にhello.mdxファイルを作成します。

app/routes/posts/hello.mdx
---
title: Hello World
---

## Hollo World

This is a test

http://localhost:5173/posts/helloにアクセスしてると、

無事に mdx で書いた内容が変換されて表示されました。

次に、せっかくなので root page に記事一覧が表示されるようにします。
まずはapp/routes/以下にtypes.tsファイルを作成します。

app/routes/types.ts
export type Meta = {
  title: string;
};

次にapp/routes/index.tsxファイルを書き直します。
(一部方エラーが発生するので、強引に ts-ignore で誤魔化します)

app/routes/index.tsx
import type { Meta } from '../types';

export default function Top() {
  // @ts-ignore
  const posts = import.meta.glob<{ frontmatter: Meta }>('./posts/*.mdx', {
    eager: true,
  });
  return (
    <div>
      <h2>Posts</h2>
      <ul >
        {Object.entries(posts).map(([id, module]) => {
          // @ts-ignore
          if (module.frontmatter) {
            return (
              <li>
                <a href={`${id.replace(/\.mdx$/, '')}`}>
                  {/* @ts-ignore */}
                  {module.frontmatter.title}
                </a>
              </li>
            );
          }
        })}
      </ul>
    </div>
  );
}

http://localhost:5173/にアクセスしてると、

無事に posts 以下に配置した mdx ファイルを読み込んで一覧表示してくれました。

CSS 装飾

まっさらな html だけだと味気ないので、装飾を施します。
まず、app/components/以下にlayout.tsxファイルを作成します。

layout.tsx(長いので折りたたんでいます)
app/components/layout.tsx
import { css } from 'hono/css';

const styles = {
  main: css`
    display: grid;
    grid-template-columns: auto 980px auto;
    grid-template-rows: 90px 1fr 90px;
  `,
  header: css`
    margin-left: 12px;
    margin-right: 12px;
    grid-column-start: 2;
    grid-column-end: 3;
    grid-row-start: 1;
    grid-row-end: 2;
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
  `,
  headerBorder: css`
    grid-column-start: 1;
    grid-column-end: 4;
    grid-row-start: 1;
    grid-row-end: 1;
    border-bottom: 1px solid #000;
  `,
  headerLink: css`
    cursor: pointer;
    margin-left: 12px;
  `,
  section: css`
    margin-left: 12px;
    margin-right: 12px;
    grid-column-start: 2;
    grid-column-end: 3;
    grid-row-start: 2;
    grid-row-end: 3;
  `,
};

export default function Layout({
  children,
  title = 'My App',
}: {
  children: any;
  title?: string;
}) {
  return (
    <main class={styles.main}>
      <div class={styles.headerBorder}></div>
      <header class={styles.header}>
        <h1>{title}</h1>
        <div>
          <a href='/' class={styles.headerLink}>
            Home
          </a>
        </div>
      </header>
      <section class={styles.section}>{children}</section>
    </main>
  );
}

hono には CSS モジュールが存在しているので、そちらを利用しました。
Tailwind CSS を利用したい場合は、公式ドキュメントをご覧ください。

次に、mdx ファイルから layout を読み込みます。

app/routes/posts/hello.mdx
---
title: Hello World
---

-## Hollo World

This is a test

+import BaseLayout from '../../components/layout';
+export default ({ children }) => (
+  <BaseLayout title='Hello World'>{children}</BaseLayout>
+);

無事に Layout が適応されました。
ついでに root ファイルにも Layout を適応させておきます。

app/routes/index.tsx
+import BaseLayout from '../components/layout';
import type { Meta } from '../types';

export default function Top() {
  // @ts-ignore
  const posts = import.meta.glob<{ frontmatter: Meta }>('./posts/*.mdx', {
    eager: true,
  });
  return (
-    <div>
-      <h2>Posts</h2>
+    <BaseLayout title='posts'>
      <ul >
        ...
      </ul>
-    </div>
+    </BaseLayout>
  );
}

無事にこちらにも Layout が適応されました。

デプロイ

それでは、ここまでの内容を Cloudflare Pages にデプロイしてみたいと思います。
詳細はcloudflare 公式ドキュメントをご覧ください。

デプロイ設定は、以下の通りnpm run buildのみです。

そしたら、たった 24 秒でデプロイ完了してしまいました!

https://d73cc5fe.ototrip-honox-sample.pages.dev/

Workers AI 追加

ここまでで「mdx wiki が非常に簡単に作成、デプロイできる」という点はお伝えできたかと思います。

せっかくなので、Cloudflare っぽい機能追加してみようと思います。
Cloudflare には Workers AI という CDN 上で AI 推論するためのサービスがあります。

https://developers.cloudflare.com/workers-ai/

これを追加して、記事検索機能を追加していきます。
(ちなみに text embedding などという高尚な技術は使いませんので精度はご愛嬌です)

まずは Workers AI を使うためのもろもろ設定をしていきます。
必要なライブラリを追加します。

$ npm install @cloudflare/ai

次に root 以下にwrangler.tomlファイルを作成します。

wrangler.toml
[ai]
binding = "AI"

次に、app/以下のglobal.d.tsの型を修正します。

app/global.d.ts
-    Bindings: {};
+    Bindings: {
+      AI: any;
+    };

最後に Cloudflare Pages の設定で Workers AI を使えるように設定します。
詳細はcloudflare 公式ドキュメントをご覧ください。


次に、検索するためのページを作成します。
app/routes/以下にsearch/index.tsxファイルを作成します。

search/index.tsx(長いので折りたたんでいます)
app/routes/search/index.tsx
import { Ai } from '@cloudflare/ai';
import { css } from 'hono/css';
import type { FC } from 'hono/jsx';
import { createRoute } from 'honox/factory';

import BaseLayout from '../../components/layout';

type Data = {
  text?: string;
};

const pageStyles = {
  form: css`
    display: flex;
    flex-direction: column;
    gap: 1rem;
  `,
  input: css`
    padding: 0.5rem;
  `,
};

const Page: FC<Data> = ({ text }) => {
  return (
    <BaseLayout title='search'>
      <p>関連キーワードを入力してください。postsの中から検索します。</p>
      <form method='POST' class={pageStyles.form}>
        <input
          type='text'
          class={pageStyles.input}
          name='content'
          placeholder='キーワード'
        />
        <button type='submit'>検索</button>
      </form>
      <p>{text}</p>
    </BaseLayout>
  );
};

type LlamaAnswer = {
  response: string;
};

type M2mAnswer = {
  translated_text: string;
};

export const POST = createRoute(async (c) => {
  const { content } = await c.req.parseBody<{ content: string }>();
  const ai = new Ai(c.env.AI);

  // @ts-ignore
  const posts = import.meta.glob<{ frontmatter: Meta }>('../posts/*.mdx', {
    eager: true,
  });
  const contents = Object.entries(posts).map(([id, module]) => {
    // @ts-ignore
    return {
      // @ts-ignore
      title: module.frontmatter.title,
      // @ts-ignore
      description: module.frontmatter.description,
    };
  });
  const descriptions = contents
    .map((item) => `${item.title}: ${item.description}`)
    .join(', ');

  const preAnswer: LlamaAnswer = await ai.run('@cf/meta/llama-2-7b-chat-int8', {
    messages: [
      {
        role: 'user',
        content: `\
        There are ${contents.length} arrays here. Let's play a game to guess which array has the closest contents to ${content}.\
        What is the closest content to ${content} in the ${descriptions}?\
        `,
      },
    ],
  });
  const answer: LlamaAnswer = await ai.run('@cf/meta/llama-2-7b-chat-int8', {
    messages: [
      {
        role: 'user',
        content: `\
        There are ${contents.length} arrays here. Let's play a game to guess which array has the closest contents to ${content}.\
        What is the closest content to ${content} in the ${descriptions}?\
        `,
      },
      {
        role: 'assistant',
        content: preAnswer.response,
      },
      {
        role: 'user',
        content:
          'Output only what you think is the best answer in the original text and title.',
      },
    ],
  });

  const response: M2mAnswer = await ai.run('@cf/meta/m2m100-1.2b', {
    text: answer.response,
    source_lang: 'english',
    target_lang: 'japanese',
  });

  return c.render(<Page text={response.translated_text} />);
});

export default createRoute((c) => {
  return c.render(<Page />);
});

中身を簡単に説明すると、mdx の description に記載された内容に基づいて、記事のタイトルを表示するように指示しています。

また、検索用に Python と JavaScript の簡単な説明記事を追加しておきます。

python.mdx
app/routes/posts/python.mdx
---
title: 初心者でもわかるPython
description: Python is a versatile programming language known for its simplicity and readability, making it great for beginners. It's widely used for web development, data analysis, artificial intelligence, and more, due to its extensive libraries and frameworks.
---

**以下の文章は ChatGPT によって生成された文章です。**

## 概要

Python は、初心者からプロフェッショナルまで幅広く使われているプログラミング言語です。その読みやすい構文と、強力なライブラリのサポートにより、Web 開発、データサイエンス、人工知能など、多岐にわたる分野で活躍しています。

### Python の基本

Python のコードは非常に読みやすく、まるで英語を読んでいるかのような感覚を持つことができます。以下は、Python での「Hello, World!」プログラムの例です。

```python
print("Hello, World!")

変数とデータ型

Python では、変数を宣言する際に型を指定する必要はありません。Python が自動的に型を判断します。

message = "Hello, Python!"
number = 42
pi_value = 3.14

リストと辞書

データを集めて管理するためのリスト(配列)や辞書(キーと値のペア)も、Python では簡単に扱うことができます。

# リスト
my_list = [1, 2, 3]

# 辞書
my_dict = {"apple": "red", "banana": "yellow"}

関数

Python では、defキーワードを使って関数を定義します。関数は、コードの再利用を促進し、プログラムをモジュール化するのに役立ちます。

def greet(name):
    return "Hello, " + name + "!"

print(greet("Python"))

この短い例からもわかるように、Python は非常に直感的で、学習しやすい言語です。興味を持ったら、ぜひとも Python の世界へ飛び込んでみてください!

import BaseLayout from '../../components/layout';
export default ({ children }) => (
<BaseLayout title='初心者でもわかるPython'>{children}</BaseLayout>
);


javascript.mdx
app/routes/posts/javascript.mdx
---
title: 初心者でもわかるJavaScript
description: JavaScript is a programming language used to create interactive and dynamic content on websites. It allows you to implement complex features, such as animations, forms, and responses to user actions, enhancing the user experience on the web.
---

**以下の文章は ChatGPT によって生成された文章です。**

## 概要

JavaScript は、ウェブページを動的にするためのプログラミング言語です。HTMLCSS と組み合わせて使われることが多く、ユーザーインターフェイスの改善、API からのデータ取得、ウェブページの動的な更新などに利用されます。

### JavaScript の基本

JavaScript を使用するには、HTML ドキュメント内に`<script>`タグを使ってコードを記述します。以下は、簡単なアラートを表示する JavaScript の例です。

```html
<script>
  alert('Hello, JavaScript!');
</script>

変数とデータ型

JavaScript では、var, let, constを使って変数を宣言します。データ型には、数値、文字列、ブーリアンなどがあります。

let message = 'Hello, JavaScript!';
const pi_value = 3.14;
var isLearningFun = true;

配列とオブジェクト

JavaScript でデータを扱う基本的な構造には配列とオブジェクトがあります。

// 配列
let colors = ['red', 'green', 'blue'];

// オブジェクト
let person = {
  firstName: 'John',
  lastName: 'Doe',
  age: 30,
};

関数

関数は、コードのブロックをグループ化し、それを何度でも実行できるようにします。関数はfunctionキーワードを使って定義します。

function greet(name) {
  return 'Hello, ' + name + '!';
}

console.log(greet('World'));

イベントハンドラ

JavaScript は、ユーザーのアクションに応じてコードを実行できます。例えば、ボタンがクリックされたときに特定の動作をするように設定できます。

<button onclick="alert('Hello World!')">Click Me!</button>

このように、JavaScript はウェブ開発において非常に強力なツールです。基本から始めて、徐々に複雑なプロジェクトへとスキルを広げていくことが可能です。

import BaseLayout from '../../components/layout';
export default ({ children }) => (
<BaseLayout title='初心者でもわかるJavaScript'>{children}</BaseLayout>
);


これらの修正をGithubにPushし、Pagesをデプロイし直します。
/searchにアクセスして、data analysisの関連検索をしてみると、

と、なんとかPythonについて答えようとしているのが垣間見えます。
でも精度は微妙ですね。。。

皆様もぜひお確かめください!

https://ototrip-honox-sample.pages.dev

また利用したコードはこちらで公開していますので、よろしければどうぞ。

https://github.com/ototrip-lab/ototrip-honox-sample

最後に

ここまで読んでいただきありがとうございました。
とても軽量で、そしてとても早いフレームワークであることが体験できました!

また、犬専用の音楽アプリに興味を持っていただけたら、ぜひダウンロードしてみてください!

https://www.oto-trip.com/

Discussion