jaoafa / jaoweb4

🌏 jao Minecraft Server Website
https://jaoafa.com
5 stars 4 forks source link

サイト内検索機能 #55

Open book000 opened 1 year ago

book000 commented 1 year ago

ルールとか探すときにワード検索できたら楽ですよねってやつです 検索画面とか作るのがなかなかめんどくさそうですが…。

Hiratake commented 1 year ago

SSGだから外部サービスに頼るしかないのではなどと思ってますが

book000 commented 1 year ago

たしかに…

Hiratake commented 1 year ago

https://www.algolia.com/ Algoliaとかよく見るけど、おいくら万円くらいかかるのかわからん

book000 commented 1 year ago

10,000 search + 10,000 recommend requests/mo and 10,000 records/mo

Hiratake commented 1 year ago

26 と重複?こっちは記事の絞り込み的なやつかな

book000 commented 1 year ago

クレカ登録不要ならまあ上限いったらごめんなさいにすればいいかしらねえ

26 はブログ記事に限っての絞り込みなのでちょっと違います。こっちはブログ以外に焦点を当てた検索って感じ。例えば「投票について書かれてたルールページどこだっけ?」とか

Hiratake commented 1 year ago

10,000 search + 10,000 recommend requests/mo and 10,000 records/mo

無料である程度使えるなら、とりあえず使ってみるのはありかねえ

26 はブログ記事に限っての絞り込みなのでちょっと違います。

issueの件名変えたほうが良きかねえ。ブログ記事の絞り込み機能とか。こっちは ContentList.vue で一覧データ取得はやってるから、カテゴリ(まだカテゴリ自体存在しないけど)とか著者での絞り込みとかならできそう。しらんけど。

book000 commented 1 year ago

issueの件名変えたほうが良きかねえ。

かえました

Hiratake commented 1 year ago

:igyo:

book000 commented 1 year ago

ドキュメント更新時にドキュメントを Algolia に投げるコードを書く感じかなあ

https://sunday-morning.app/posts/2021-03-27-generate-contentful-algolia-index

book000 commented 1 year ago

検索結果の表示と表示結果からの遷移を考えると、アンカーが付けられる各タイトル部分とそれ以下のドキュメントでインデックスつけたほうがよさそう。

book000 commented 1 year ago

https://vivliostyle.github.io/vivliostyle_doc/ja/vivliostyle-user-group-vol2/spring-raining/index.html

book000 commented 1 year ago

とりあえずざっとMarkdownからASTつくってheadingとそれ以下のコンテンツに分ける処理

/* eslint-disable no-console */
import { fromMarkdown } from 'mdast-util-from-markdown'
import { gfmTable } from 'micromark-extension-gfm-table'
import { gfmTableFromMarkdown } from 'mdast-util-gfm-table'
import fs from 'node:fs'
import { toString } from 'mdast-util-to-string'
import { Content } from 'mdast-util-from-markdown/lib'
import { Logger } from '@book000/node-utils'
import matter from 'gray-matter'

const htmlCommentRegex = /<!--[\S\s]*?-->/g

async function parse(content: string) {
  const ast = fromMarkdown(content, {
    extensions: [gfmTable],
    mdastExtensions: [gfmTableFromMarkdown],
  })

  // headingとそれ以下の内容を取得
  const results: {
    title: string | null
    asts: Content[]
    content: string | null
  }[] = []
  for (const node of ast.children) {
    if (node.type === 'heading') {
      // 前項目のコンテンツ処理
      const last = results[results.length - 1]
      if (last) {
        last.content = toString(last.asts).replace(htmlCommentRegex, '').trim()
      }

      // 新しい項目追加
      const title = toString(node)
      results.push({
        title,
        asts: [],
        content: null,
      })
      continue
    }

    // headingでない場合は前項目に追加
    const last = results[results.length - 1]
    if (last) {
      last.asts.push(node)
    } else {
      results.push({
        title: null,
        asts: [node],
        content: null,
      })
    }
  }

  return results
}

function getFiles(directory: string): string[] {
  const files = fs.readdirSync(directory)
  const results: string[] = []
  for (const file of files) {
    const path = `${directory}/${file}`
    if (fs.statSync(path).isDirectory()) {
      results.push(...getFiles(path))
    } else {
      results.push(path)
    }
  }
  return results
}

async function main() {
  const logger = Logger.configure('main')
  const directory = './files'
  const files = getFiles(directory)
  const results: {
    pageTitle: string
    path: string
    heading: string | null
    content: string
  }[] = []
  for (const path of files) {
    if (!path.endsWith('.md')) {
      continue
    }
    logger.info(`Parsing ${path}...`)
    const frontMatter = matter(fs.readFileSync(path, 'utf8'))
    const parsed = await parse(frontMatter.content)

    for (const p of parsed) {
      if (!p.content) {
        continue
      }

      results.push({
        pageTitle: frontMatter.data.title,
        path: path.replace(directory, '').replace('.md', ''),
        heading: p.title,
        content: p.content,
      })
    }
  }

  fs.writeFileSync('./results.json', JSON.stringify(results, null, 2), 'utf8')
}

;(async () => {
  await main()
})()
book000 commented 1 year ago

generateしたら /search ルートが消えました(どこからもリンクされていないので)。静的ルート書かなきゃならなそう… クライアントサイドでalgoliaにリクエストを出していいのか、よくわかりません

動かすためには .envALGOLIA_API_KEYALGOLIA_APPLICATION_ID を設定しなければなりません。値は https://www.algolia.com/account/api-keys/all?applicationId=RF6OC1MM2P にて閲覧可能。

https://github.com/book000/jaoweb4/tree/feat/algolia-search

book000 commented 1 year ago

DocSearch とやらを公開したら使うのがよさそう? https://docsearch.algolia.com/

Hiratake commented 1 year ago

そっちのが一般的なんかねえ。 とりあえず、検索が絶対なきゃならんってわけでもないと思うので、公開後に両方試してみて比較してからでもいいと思う。