Post

Astroのサイドバーを多重化する

Astroのサイドバーを多重化する

サイドバーの多重化

公式サイトではサイドバーが多重化(タブ化)している。これと似たような構造を実現する。

tabbed-sidebar

厳密な動作は異なるが、シンプルな実現方法でサイドバーを多重化する。

docs以下のディレクトリ毎に分割する

src/content/docs 以下のトップディレクトリごとに分割する。以下の例では guidesreference 以下のコンテンツがそれぞれ独立したサイドバーで表示されるようにする。

1
2
3
4
5
src/content/docs
├── guides
│   └── example.md
└── reference
    └── example.md

サイドバーの上書き

通常のサイドバーではカスタマイズが難しいため、通常のサイドバーの上部にリンクボタンを設置した、カスタムのサイドバーを用意する。コンポーネントの上書きやカラーテーマのドキュメントを参照する。

GitHub PagesなどでURLにリポジトリ名(BASE_URL)を含む場合は、BASE_URL の処理に注意する。

src/components/Sidebar.astro 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
---
import Default from "@astrojs/starlight/components/Sidebar.astro";

// GitHub Pagesの場合はリポジトリ名(BASE_URL)も含める
const links = [
  { label: "Guides", href: "/guides/getting-started/" },
  { label: "Reference", href: "/reference/example/" },
];

const current = Astro.url.pathname;
const isActive = (href) => {
  if (href === "/") {
    return current === "/";
  }

  const group = href.substring(0, href.indexOf('/', 1));
  return current.startsWith(group);

  // BASE_URLがある場合は2要素目がグループ名
  // const group = href.split('/')[2];
  // return current.split('/')[2] === group;
};
---

<nav class="my-sidebar">
  <!-- サイドバー切り替えボタン -->
  <div class="my-fixed-links">
    <div class="my-fixed-links-grid">
      {links.map((item) => (
        <a
          class={`my-fixed-link ${isActive(item.href) ? "active" : ""}`}
          href={item.href}
        >
          {item.label}
        </a>
      ))}
    </div>
  </div>

  <!-- Default Sidebar -->
  <Default><slot /></Default>
</nav>

<style>
  .my-sidebar {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
  }

  .my-fixed-links {
    padding: 0.75rem;
    border: 1px solid var(--sl-color-gray-5);
    border-radius: 0.75rem;
    background: var(--sl-color-black);
  }

  .my-fixed-links-title {
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--sl-color-gray-2);
    margin-bottom: 0.5rem;
  }

  .my-fixed-links-grid {
    display: grid;
    gap: 0.5rem;
  }

  .my-fixed-link {
    display: block;
    padding: 0.5rem 0.6rem;
    border-radius: 0.6rem;
    text-decoration: none;
    color: var(--sl-color-white);
    background: var(--sl-color-gray-6);
  }

  .my-fixed-link:hover {
    background: var(--sl-color-gray-5);
  }

  .my-fixed-link.active {
    outline: 2px solid var(--sl-color-accent-high);
  }
</style>

作成したサイドバーを使用するように astro.config.mjs に記述する。

astro.config.mjs:

1
2
3
4
5
6
7
8
9
10
11
export default defineConfig({
  integrations: [
    starlight({
      ...
      components: {
        // Override the default Sidebar
        Sidebar: './src/components/Sidebar.astro',
      },
    }),
  ],
});

サイドバーのリンクをフィルタする

リンクボタンは作成できたが、このままではサイドバーには全てのページが表示されているので、これを必要なもののみにフィルタする。

Starlight Examplesに例があるので、基本はそちらから拝借。リファレンスはRoute Dataを参照。

ただし、単純に流用するとGitHub Pagesのリポジトリ名を含むURL(BASE_URL)に対応できない。また、トップページでサイドバーを表示する際にもフィルタが効かないため、少し調整する。

src/routeMiddleware.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { defineRouteMiddleware } from '@astrojs/starlight/route-data';

export const onRequest = defineRouteMiddleware((context) => {
  // ドキュメントのパス名を分解
  const pathComps = context.url.pathname.split('/');
  const currentBase = (() => {
    if (import.meta.env.BASE_URL && pathComps.length >= 3) {
      // baseが指定されている場合は["", base, topGroup]の最低3要素を要求
      return pathComps.slice(0, 3).join("/") + "/";
    } else if (!import.meta.env.BASE_URL && pathComps.length >= 2) {
      // baseがない場合は2要素
      return pathComps.slice(0, 2).join("/") + "/";
    } else {
      // 要素が足りない場合(indexページ)は空
      return "";
    }
  })();

  const { pagination } = context.locals.starlightRoute;
  if (currentBase) {
    // サイドバーから同一階層内にないドキュメントを取り除く
    context.locals.starlightRoute.sidebar = context.locals.starlightRoute.sidebar.filter(
      (entry) =>
        entry.type === 'group' &&
        entry.entries.some(
          (subEntry) => subEntry.type === 'link' && subEntry.href.startsWith(currentBase)
        )
    );

    // カテゴリを跨ぐページネーションリンクは作成しない
    if (pagination.prev && !pagination.prev.href.startsWith(currentBase)) {
      pagination.prev = undefined;
    }
    if (pagination.next && !pagination.next.href.startsWith(currentBase)) {
      pagination.next = undefined;
    }
  } else {
    // currentBaseが空なら全て空にする
    context.locals.starlightRoute.sidebar = [];
    pagination.next = undefined;
    pagination.prev = undefined;
  }
});

作成したミドルウェアを使用するように astro.config.mjs で指定する。

astro.config.mjs:

1
2
3
4
5
6
7
8
export default defineConfig({
  integrations: [
    starlight({
      ...
      routeMiddleware: `./src/routeMiddleware.ts`
    }),
  ],
});

サイドバーの表示コンテンツを1階層下げる

このままでも概ね良いが、トップディレクトリがサイドバーに表示されていて冗長なので指定階層を1階層下げる。この構成に合わせて、 Sidebar.astro で定義したリンクも調整する。

astro.config.mjs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default defineConfig({
  integrations: [
    starlight({
      sidebar: [
        {
          label: 'Guides',
          autogenerate: { directory: 'guides' },
        },
        // Reference
        {
          label: 'Sub1',
          autogenerate: { directory: 'reference/sub1' },
        },
        {
          label: 'Sub2',
          autogenerate: { directory: 'reference/sub2' },
        },
      ],
    }),
  ],
});

GitHub

実際のコードは以下にある。

  1. シンタックスハイライトは jsx としたが、実際は .astro ファイルである。このほか mjs ファイルも js としている。 ↩︎

This post is licensed under CC BY 4.0 by the author.