Skip to content

タイムラインの作成

前回、サイトを作成するために重要な Renderer を作成しました。 今回は、この Renderer を使って、SNS でよく見るタイムラインの表示を作ってみます。

Xのタイムライン

投稿の型を定義する

まずは、投稿がどんなものかを考えましょう。 ユーザーに関しては認証を作る必要があって、それは次回以降に行うので、今回は考えません。 投稿には、投稿の内容、投稿の日時が必要そうです。

これを src/types.ts に定義しておきましょう。

ts
export type Post = {
  content: string; // 投稿の内容
  createdAt: Date; // 投稿の日時
};

投稿の内容は文字列で、投稿の日時は JavaScript の Date 型で表現できそうです。 このように、先に抽象的な型を定義しておくことで、後々の実装が楽になります。

また、exporttype 宣言の前につけることで、この型を他のファイルからも import して使えるようにしておきます。

ダミーの投稿を作る

さて投稿の型ができたので、ダミーの投稿を作って実際に投稿されてるものとしてサイトに表示してみたいと思います。 (実際にデータベースなどを使って投稿するシステムはのちに作るので、「投稿できないじゃん」というツッコミは無しでお願いします)

src/dummy.ts を作って、以下のようにしてください。

ts
import { Post } from "./types";

export const posts: Post[] = [
  {
    content: "おなかすいた",
    createdAt: new Date(),
  },
  {
    content: "おはよう、今起きました",
    createdAt: new Date(),
  },
  {
    content: "おやすみなさい",
    createdAt: new Date(),
  },
];

こちらも export をつけて、他のファイルからも import して使えるようにしておきます。 src/types.ts から先ほど定義した Post 型を import して、Post 型の配列を posts という名前で export しています。

投稿を表示する

さて、ここまでで投稿の型ができたので、実際に投稿を表示してみましょう。 まずは投稿をトップページに表示したいので、src/index.tsx を以下のように編集してください。

tsx
import { posts } from "./dummy"; // ファイルの一番上でimport

// ...

app.get("/", (c) => {
  return c.render(
    <main>
      <h1>Hello Hono!</h1>
      <h1>Hono SNS</h1>
      <p>これはHonoのトップページです。</p>
      {posts.map((post) => ( 
        <div>
          <p>{post.content}</p>
          <p>{post.createdAt.toLocaleString()}</p>
        </div> 
      ))}
    </main>
  );
});

これがうまくいくと、以下のように投稿一覧が表示されるはずです。

投稿一覧が表示されている画面

実際に dummy.ts で定義した投稿が表示されていることが確認できると思います。

内容を変えてみたり、投稿を増やしてみたりして、表示が変わることを確認してみてください。 (内容が変わった場合ブラウザのリロードが必要です)

ロゴを表示させる

ここで一旦投稿ロジックから外れて、次はロゴを表示させさせるようにすることで画像を配信してみます。

まずは project 直下に static ディレクトリを作成します。

txt
.
├── package-lock.json
├── package.json
├── static
├── src
│  ├── dummy.ts
│  ├── index.tsx
│  └── types.ts
└── tsconfig.json

次に static ディレクトリに hono-sns-logo.svg を配置します。

以下の画像をダウンロードして、static ディレクトリに配置してください。

ロゴ

Honoのロゴを組み合わせて鳥を作ってみました、多方面から怒られそうです。

次に static ディレクトリにあるファイルを静的アセットとして配信するように設定します。

src/index.tsx に以下のように追記してください。

tsx
import { serveStatic } from "@hono/node-server/serve-static";

app.use("/static/*", serveStatic({ root: "./" }));

この設定をすることで、/static 以下の URL にアクセスすると、static ディレクトリの中身がそのまま配信されるようになります。

最後にこのロゴを表示させるために、src/index.tsx を以下のように編集してください。

tsx
app.get("/", (c) => {
  return c.render(
    <main class={mainStyle}>
      <img src="/static/hono-sns-logo.svg" alt="Hono SNS" width={48} height={48} />
      <h1>Hono SNS</h1>
      <div>
        {posts.map((post) => (
          <div class={timelineCardStyle}>
            <p>{post.content}</p>
            <p>{post.createdAt.toLocaleString()}</p>
          </div>
        ))}
      </div>
    </main>,
    { title: "トップページ" }
  );
});

<img> タグの src 属性に /static/hono-sns-logo.svg を指定することで、ロゴが表示されるようになります。

デザインする

ここまでで投稿が表示されるようになりましたが、デザインがないので、ちょっと寂しいですね。

ここではサイトをデザインするためのCSSを書いていきます。

そのまま CSS を書いてもいいのですが、Hono は hono/css というパッケージで JS ファイルの中に CSS を書くことができるので、それを使っていきます。

もし Hono CSS を使わず素の CSS を書きたい場合

レンダラーの head に <link> タグを追加して CSS を静的ファイルとして読み込むことでノーマルな CSS を使っていくこともできます。

src/index.tsx を以下のように編集してください。

tsx
app.use(
  "*",
  jsxRenderer(
    ({ children }) => {
      return (
        <html>
          <head>
            <title>Hono SNS</title>
            <meta charset="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <link rel="sylesheet" href="/static/style.css" />
          </head>
          <body>{children}</body>
        </html>
      )
    },
    { stream: true, docType: true }
  )
)

次に static ディレクトリに style.css を作成してください。

css
body {
  color: red;
}

画面をリロードしてください。style.css が読み込まれて、画面の文字が赤くなっていれば成功です。

ノーマルな CSS を使ったバージョンのコードはこちらにあります。

今後は Hono CSS を使っていく予定なので適宜読み替えながら進めてください。

まずは src/index.tsx に以下のように追記してください。

tsx
import { Style, css } from "hono/css"; 

app.use(
  "*",
  jsxRenderer(
    ({ children }) => {
      return (
        <html>
          <head>
            <title>Hono SNS</title>
            <meta charset="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <link rel="sylesheet" href="/static/style.css" />
            <Style />
          </head>
          <body>{children}</body>
        </html>
      )
    },
    { stream: true, docType: true }
  )
)

<Style /> を追加することで、Hono CSS を使うことができるようになります。

次に src/index.tsx に以下のように追記してください。

tsx
const bodyStyle = css`
  margin: 0;
  padding: 0;
  font-family: sans-serif;
`;

この css`...`; のような書き方は「タグ付きテンプレートリテラル」というものですが、ライブラリを使う時以外ではあまり使わないので、見慣れないかもしれません。 もし VSCode を使っている場合は、拡張機能でvscode-styled-componentsを入れると、この CSS 関数内の文字列が CSS としてハイライトされるようになります。ぜひ入れてみてください。

この CSS 関数は定義すると戻り値として CSS のクラス名が文字列で返ってくるので、それをそのまま使いたい要素の class 属性に指定することで、その要素に CSS が適用されるようになります。

そしてこの bodyStyle<body> タグに適用してください。

tsx
app.use(
  "*",
  jsxRenderer(
    ({ children }) => {
      return (
        <html>
          <head>
            <title>Hono SNS</title>
            <meta charset="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <link rel="sylesheet" href="/static/style.css" />
            <Style />
          </head>
          <body>{children}</body>
          <body class={bodyStyle}>{children}</body>
        </html>
      )
    },
    { stream: true, docType: true }
  )
)

これで bodyStyle が適用されるようになります。

実際にフォントが sans-serif に変わっていれば成功です。

さらにタイムラインにもデザインを適用していきます。

以下のような Hono CSS を書いて、src/index.tsx に追記してください。

tsx
const mainStyle = css`
  max-width: 600px;
  margin: 0 auto;
  padding: 16px;
`;

const headerStyle = css`
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 16px;

  * {
    margin: 0;
  }
`;

const timelineCardStyle = css`
  padding: 16px;
  border-top: 1px solid #ccc;
`;
tsx
app.get("/", (c) => {
  return c.render(
    <main>
    <main class={mainStyle}>
      <header class={headerStyle}>
        <img src="/static/hono-sns-logo.svg" alt="Hono SNS" width={48} height={48} />
        <h1>Hono SNS</h1>
      </header>
      <div>
        {posts.map((post) => (
          <div>
          <div class={timelineCardStyle}>
            <p>{post.content}</p>
            <p>{post.createdAt.toLocaleString()}</p>
          </div>
        ))}
      </div>
    </main>,
    { title: "トップページ" }
  );
});

これでタイムラインの投稿がこのようにデザインされるようになります。

デザインされたタイムライン

mainStyle では、max-width で幅を制限し、margin: 0 auto で中央寄せにすることで、中央に大きすぎない幅で表示されるようになります。

headerStyle では、display: flex で中身を横並びにし、align-items: center で img と h1 を上下中央寄せにし、gap: 8px で中身の間隔を開けています。 そして * { margin: 0; }h1 にデフォルトでついてくるマージンを消しています。

timelineCardStyle では、padding で中身の間隔を開け、border-top で上に線を引いています。 これで各投稿が線で区切られているようになります。

これでタイムラインのデザインが完成しました。

ここまでのコードはこちらにあります。