npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cosense-toolbox/astro

v0.1.0-beta.9

Published

Astro integration for cosense-x: use .csn / .csnx (Cosense notation) as pages and content collections.

Readme

@cosense-toolbox/astro

Cosense (旧 Scrapbox) の記法で書いた .csn / .csnx を、Astro のページや content collection で使うための統合です。 コンパイルには @cosense-toolbox/cosense-x を使います。

ドキュメント → https://cosense-toolbox.qaynam.dev/astro/

beta:公開 API はまだ変わりうる。

動く例は examples/astro-blog にある。

設定

// astro.config.mjs
import svelte from "@astrojs/svelte"
import cosense from "@cosense-toolbox/astro"
import { defineConfig } from "astro/config"

export default defineConfig({
  integrations: [
    svelte(),
    cosense({
      components: "./src/components/cosense.ts",
      pageUrl: (page) => `/posts/${encodeURIComponent(page.slug)}/`,
      tagUrl: (tag) => `/tags/${encodeURIComponent(tag)}/`,
      lint: { unresolvedLinks: "error" },
    }),
  ],
})

| オプション | 内容 | | :-------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | components | すべてのページに渡すコンポーネントを default export するモジュールの、プロジェクトのルートからのパス | | pageUrl | リンク先のページの URL。{ id, title, slug } を受け取る。id はプロジェクトのルートからのパス | | tagUrl projectUrl | compile の同名のオプションと同じ | | unresolvedLinks | サイトに無いページへのリンクの出し方。'text' (既定) はテキスト、'link' はタイトルから作った URL へのリンクにする | | lint | ビルドの前にリンク切れを調べる。{ unresolvedLinks?, frontmatter? }。省略すると調べない。下を参照 | | publicMedia | サイトに置いたファイルを [:/images/a.png] で画像・動画・音声として読む (parser の publicMedia)。パスの前に base が付く。false で無効。既定は有効 | | parseOptions | パースの設定。parser の parse のオプション (extensions など) がそのまま渡る | | renderOptions | 描画の設定。parser の toHast のオプション (extensions handlers classNames showPads iconImageUrl) と title がそのまま渡る。色付けは syntaxHighlight で決める | | rehypePlugins | compile の同名のオプションと同じ | | syntaxHighlight | コードブロックの色付け。既定の 'astro' は markdown.shikiConfig に従う。false で無効、関数で自前の色付け。下を参照 | | assets | Cosense 上の画像とファイルを、ビルド時に取ってきてサイトの中に置く。{ pat?, origin?, links? }、または false で無効。既定は有効 | | gyazoVideo | 拡張子の無い Gyazo の URL が動画だったときの出し方。既定の 'gif' は通信せず /raw の画像 (録画は動く gif)。'video' / 'embed' はビルド時に oEmbed で聞き、動画なら <video> / Gyazo のプレーヤーの埋め込みにする |

リンク切れを調べる

lint を渡すと、ビルドの前と、開発サーバーの起動時・ページを変えたときに、srcDir の下の .csn / .csnx を読み、サイトに無いページへの [リンク] を調べる。開発中は 'error' でも止めず、ログに出すだけにする。 判定はエディタの診断 (@cosense-toolbox/lsp) と csn-lsp check と同じ関数で、パースにはこの統合の parseOptions を使う。エディタで警告されるものと、ビルドで止まるものが一致する。

| lint のオプション | 内容 | 既定 | | :------------------ | :--------------------------------------------------------------------------------------------------------------- | :---------- | | unresolvedLinks | 'off' / 'hint' / 'information' / 'warning' / 'error'。'error' ならビルドを止め、それ以外はログに出す | 'warning' | | frontmatter | 1 行目の --- を frontmatter (YAML) として飛ばすか | true |

ページの題名はファイルの 1 行目で、大文字小文字と、空白と _ の違いは無視して比べる。#タグ と [/別プロジェクト/ページ] は調べない。

サイトに置いたファイル

public/ に置いた画像・動画・音声は、[:/…] とサイトの根元からのパスで書く。種類は拡張子で決まる。

[:/images/screenshot.png]
[[:/images/hero.png]]
[:/movies/demo.mp4]
[:/audio/bgm.mp3]

base: "/docs" のサイトなら、[:/images/a.png] は /docs/images/a.png を指す。 メディアでないファイル ([:/files/a.pdf]) は、そのファイルへのリンクになる。 Cosense Web では [:/images/a.png] もページへのリンクなので、Cosense から持ってきたページの読み方は変わらない。

Cosense 上の画像とファイル

Cosense にアップロードした画像やファイル (https://scrapbox.io/files/…) とアイコン (/api/pages/…/icon) は、ビルド時に取ってきて {base}/_cosense/ に置き、HTML の URL をそこに差し替える。

  • これらのファイルは別のサイトの <img> からは読めない (Cross-Origin-Resource-Policy: same-origin)
  • リダイレクト先の URL は数分で切れる
  • そのため、静的なサイトで表示するにはサイトの中に置くしかない
cosense({
  // 非公開プロジェクトの画像を取るときは PAT を渡す。Cosense への要求にだけ付ける
  assets: { pat: process.env.COSENSE_PAT },
})
  • .csn / .csnx の中の画像は自動で差し替える
  • ファイル名は {元の URL のハッシュ}.{拡張子}。アイコンは {ハッシュ}_{ユーザー名}.{拡張子}
    • 同じ URL は何度ビルドしても同じ名前になる
    • ハッシュから元の URL は分からないので、非公開プロジェクトのファイル ID は出ない
  • 取ってきたファイルは Astro のキャッシュのディレクトリに残す
    • アップロードしたファイルは中身が変わらないので、次のビルドでは取り直さない
    • アイコンは差し替えられることがあるので、ビルドのたびに取り直す
    • 出力先には、そのビルドで使ったファイルだけを写す
  • 取れなかったファイルは警告を出し、元の URL のまま出す
  • dev サーバーでは、同じパスで配信する
  • 非公開プロジェクトの画像も、公開するサイトに置かれる。公開してよいものだけを書くこと

リンクした Cosense のファイル ([https://scrapbox.io/files/x.zip] など、<a href> になるもの) は、既定では元の URL のまま出す。 公開プロジェクトならクリックして開ける (Cross-Origin-Resource-Policy は画面の遷移には効かない)。 非公開プロジェクトのファイルは見に来た人が開けないので、links: 'download' で画像と同じくサイトに置く。

cosense({
  assets: { pat: process.env.COSENSE_PAT, links: "download" },
})

toHtml などで自分で描画するページは、virtual:cosense-x/assets の localizeCosenseAssets に HTML を通す。 アイコンの URL は cosenseIconUrl で作る。

---
import { cosenseIconUrl, fetchPageText } from "@cosense-toolbox/cosense-x/fetch"
import { parse } from "@cosense-toolbox/parser"
import { toHtml } from "@cosense-toolbox/parser/html"
import { localizeCosenseAssets } from "virtual:cosense-x/assets"

const project = "help-jp"
const text = await fetchPageText(project, "ブラケティング")
const html = await localizeCosenseAssets(
  toHtml(parse(text), { iconImageUrl: (icon) => cosenseIconUrl(project, icon.user) }),
)
---

<article class="cosense" set:html={html} />

差し替えられるのは、ページをビルド時に描画するとき (静的なページと prerender) と dev サーバーだけ。実行時に描画する SSR では元の URL のまま返す。

コードブロックの色付け

.csn / .csnx のコードブロック (code:hello.js) は、.md / .mdx と同じく Astro の markdown.syntaxHighlight と markdown.shikiConfig の設定で shiki が色付けする。 components に登録しなくてよい。

export default defineConfig({
  markdown: { shikiConfig: { theme: "github-light" } },
  integrations: [cosense()],
})
  • 言語はファイル名の拡張子から決める。code:hello.js なら js、code:python なら python
  • shiki が知らない言語と excludeLangs の言語は、色付けせずに出す
  • theme / themes / defaultColor / langs / langAlias / transformers を使う。wrap は使わない。長い行は @cosense-toolbox/style が折り返す
  • markdown.syntaxHighlight が 'prism' のときは色付けしない (相当するものが無い)

syntaxHighlight: false で色付けをやめる。関数を渡すと、shiki の代わりにそれで色付けする。形は compile の renderOptions.highlight と同じ。

cosense({
  syntaxHighlight: (code, language) => myHighlighter(code, language), // hast か null を返す
})

toHtml で自分で描画するページは、toHtml の highlight に shiki を直接渡す。 テーマなどの設定を 1 つのファイルにまとめ、astro.config.mjs とページの両方から読むと、.md / .csn と見た目が揃う。

// src/shiki.ts
import type { HastHighlighter } from "@cosense-toolbox/parser/html"
import type { ShikiConfig } from "astro"
import { createHighlighter } from "shiki"

export const shikiConfig = { theme: "github-light" } satisfies Partial<ShikiConfig>

/** toHtml は highlight を同期で呼ぶので、使う言語は先に読み込んでおく */
export const createCodeHighlight = async (langs: string[]): Promise<HastHighlighter> => {
  const shiki = await createHighlighter({ themes: [shikiConfig.theme], langs })
  // shiki の hast はそのまま返してよい。<pre><code> は剥がされ、テーマの色はコードブロックに移る
  return (code, lang) =>
    shiki.getLoadedLanguages().includes(lang)
      ? shiki.codeToHast(code, { lang, theme: shikiConfig.theme })
      : null // 読み込んでいない言語は色付けしない
}
// astro.config.mjs
import { shikiConfig } from "./src/shiki.ts"

export default defineConfig({
  markdown: { shikiConfig },
  integrations: [cosense()],
})
---
import { parse } from "@cosense-toolbox/parser"
import { toHtml } from "@cosense-toolbox/parser/html"
import { createCodeHighlight } from "../shiki"

const highlight = await createCodeHighlight(["js", "ts"])
const html = toHtml(parse(text), { highlight })
---

<article class="cosense" set:html={html} />

行番号

@cosense-toolbox/parser/html の codeLineNumbers() を描画の拡張 (renderOptions.extensions) に渡すと、コードブロックの本体行に行番号 (data-line) が付く。 番号の表示は @cosense-toolbox/style と @cosense-toolbox/tailwind が持っていて、行の左の余白に出す。本文の位置は変わらず、コピーしたときに番号は入らない。

// astro.config.mjs
import { codeLineNumbers } from "@cosense-toolbox/parser/html"

cosense({ renderOptions: { extensions: [codeLineNumbers()] } })

toHtml で描画するページは、toHtml(page, { extensions: [codeLineNumbers()] }) のように渡す。

  • 番号の色は --cosense-line-number で変えられる
  • エディタと同じく、本文の左に番号の欄を取る。欄の幅はブロックの最後の番号の桁数で決まり、同じブロックの行はそろう
  • shiki で色付けしたブロックも、色付けしないブロックと同じく 1 行ずつの要素になるので番号が付く
  • 行をまたぐ出力を返すハイライタ (highlight.js など) でひと塊になったブロックには付かない

テーブルのセル

セルの中は Cosense Web と同じく、リンクの記法だけを読む。行と同じく記法を読みたいときはパースの拡張 tableCellNotation() を、セルの中の \n のような文字の並びを改行にしたいときは描画の拡張 tableCellLineBreaks() を渡す。

// astro.config.mjs
import { tableCellLineBreaks } from "@cosense-toolbox/parser/html"
import { tableCellNotation } from "@cosense-toolbox/parser/extensions"

cosense({
  parseOptions: { extensions: [tableCellNotation()] },
  renderOptions: { extensions: [tableCellLineBreaks("\\n")] },
})

content collection

// src/content.config.ts
import { defineCollection } from "astro:content"
import { glob } from "astro/loaders"

const posts = defineCollection({
  loader: glob({ pattern: "**/*.{csn,csnx}", base: "./src/content/posts" }),
})

export const collections = { posts }

data には frontmatter に加えて title / slug / description / image / tags / draft が入る。 Cosense では 1 行目がタイトルなので、frontmatter に書かなくても title がある。 slug が entry の id になる。

---
import { render } from "astro:content"
const { Content } = await render(post)
---

<Content />

ページ

src/pages に .csn / .csnx を置くと、そのままページになる。 frontmatter の layout にレイアウトの .astro を指定すると、本文をその default のスロットに入れる。 レイアウトには frontmatter と metadata が props で渡る。

---
layout: ../layouts/Page.astro
---
このサイトについて
本文

リンクグラフ

virtual:cosense-x/graph から、src の下のすべての .csn / .csnx のリンクグラフを読める。 グラフの id は、content collection の entry の filePath と同じ形 (プロジェクトのルートからのパス)。

---
import graph from "virtual:cosense-x/graph"
const backlinks = graph.backlinks[post.filePath]
const twoHop = graph.twoHop[post.filePath]
---

コンポーネント

.csnx の <Name /> の行には、components に指定したモジュールか、<Content components={...} /> で渡したものが使われる。 Astro の中で描画されるので、Svelte などのコンポーネントも渡せる。

ブラウザで動かすための client:* ディレクティブは .astro の中でしか付けられない。 .astro のコンポーネントで包んでから渡す。

---
// CounterIsland.astro
import Counter from "./Counter.svelte"
---

<Counter client:load {...Astro.props} />

仕組み

  • .csn / .csnx を Vite のプラグインで JS にする。jsxImportSource は astro
  • リンクの解決には全ページのタイトルが要る。そのため、ビルドの最初に src の下を全部読んで索引を作る。dev サーバーでは、どれか 1 ページが変わると索引を作り直して再読み込みする
  • 描画には astro:jsx レンダラを使う。@astrojs/mdx と同じものなので、両方入れてもぶつからない
  • ページの拡張子と content collection の形式の登録には、@astrojs/mdx も使っている Astro の非公開のフック (addPageExtension / addContentEntryType) を使っている。Astro の更新で動かなくなる可能性がある

ライセンス

MIT。