0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AstroのブログでSvelteを使って目次を作る

0
Posted at

Astroのブログでサイドバーに見出しを付けたいなと思って、色々読み漁ったけど自家製コンポーネントが一番良いという結論に至った。

環境

package.jsonから

  • @astrojs/svelte: ^9.1.0
  • astro: ^7.4.0
  • svelte: ^5.57.2

コンポーネントを作る

目次の本体はSvelteで書く。他のnpmパッケージとかに依存していないので、たぶん、AIとかに投げればVueとかReactでも使えると思う。

今回はIntersectionObserver APIで見出しの移動を検知して、要素に.currentを付与する。

ここでPropsとして受け取ってるheadingsはrender(CollectionEntry): RenderResultの戻り値。{ text: string, slug: string, depth: number }

toc.svelte
<script lang="ts">
  import type { MarkdownHeading } from 'astro'
  import { onMount } from 'svelte'

  interface Props {
    headings: MarkdownHeading[]
  }

  let { headings }: Props = $props()

  let last = $state<Element | null>(null)

  // 俺がnoobすぎたのでこれ書き忘れて全然動かなくて死ぬほど悩んだ。良い子はonMountの外に書かないようにしよう。
  onMount(() => {
    const headingElements = document.querySelectorAll('article h2, article h3')
    last = headingElements[0]

    const observer = new IntersectionObserver((entries) =>
      entries.forEach((entry) => {
        if (entry.isIntersecting) last = entry.target
      }),
    )

    headingElements.forEach((heading) => {
      observer.observe(heading)
    })
  })
</script>

<ul class="toc">
  {#each filteredHeadings as { depth, slug, text } (slug)}
    <li class={`${last?.id === slug ? 'current' : ''}`}>
      <a href={`#${slug}`}>{text}</a>
    </li>
  {/each}
</ul>

気が向いたらちゃんとdepthを反映しようとは思えど、今のところ不要だから私は書かない。<ーそもそもこの記事には関係ない

呼び出してみる

Astroの記事から呼び出す。

[...slug].astro
---
import { type CollectionEntry, getCollection } from 'astro:content'
import ToC from './path/to/toc.svelte'

export async function getStaticPaths() {
  const posts = await getCollection('blog')
  return posts.map((post) => ({
    params: { slug: post.id },
    props: post,
  }))
}
type Props = CollectionEntry<'blog'>

const post = Astro.props
const { title } = post.data
const { Content, headings } = await render(post)
---

<div>
  <main>
    <h1>{ title }</h1>
    <Content />
  </main>

  <aside>
    <span>目次</span>
    <ToC headings={headings} client:load />
  </aside>
</div>

おまけ:スタイリングした

スクリーンショット 2026-10-23 011741.png

どうでも良いこと:昔ってCSSネストできないから.scssに書いてた気がする

ul.toc {
  list-style: none;
  padding: 0;

  display: flex;
  flex-direction: column;

  li {
    border-left: 0.25rem solid #999;
    padding: 0.25rem 0 0.25rem 0.5rem;
    transition:
      0.1s border-left-width,
      0.1s padding-left;

    &.toc-depth-3 {
      padding-left: 2rem;
    }

    &.current {
      border-left-color: #185dcc;
      border-left-width: 0.5rem;
    }

    a {
      color: inherit;
      text-decoration: none;

      &:hover {
        text-decoration: underline;
      }
    }
  }
}

以上

私は両技術、経験が浅いのでもっといい方法あるかもしれません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?