Qiitaのユーザー+タグの検索画面リンク集のMarkdownを作る

# はしがき

Qiitaの特定ユーザーの記事検索を強化して欲しいという声を見かけてはや5年以上。とくに機能追加は見られぬまま、いくつかの拡張機能やWebサービスが現れたり自作したりしてきました。
直近では2020年になっても同様の悩みからQiigleというサービスなどが生まれ続けています。
しかし今もって私が知る限り、自分や特定個人の記事を検索するには検索クエリーに`user:`を手打ちする必要があり[^1]特定ユーザーから記事を調べることは億劫な作業です。自分の記事ですら。

一部の方はピックアップ記事に自分の記事のサイトマップや分類してまとめたようなものを作成してくれていますが、稀ですしメンテが要ります。

不便を感じ続けていますが、自力で非拡張で利便性を上げるには検索画面での`user:user tag:tag`への到達を手軽にするぐらいしか今のところ思いつきません。

Qiita内でクリックのみでひとまず`user:user tag:tag`に到達するにはそういったリンクへの記事があればよいのではないかと考え、とりあえずそういうmdを出力するものを書きました。

# 作ったもの

https://khsk.github.io/Qiita-create-tag-links-markdown/

![md.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62354/43cb28f5-34aa-4a83-ccbd-87f6ddae820d.jpeg)


APIを使い記事を取得してタグをまとめてmarkdownのリンクに変換します。
それをぺろっとコピペでQiitaに(限定共有)投稿すればポチポチクリックだけでリンク先に飛べます。

記事が多い方の取得結果ではタグも多すぎて冗長なため、一度しか出なかったタグを無視したりselectボックスで手動で取捨選択するオプションをつけました。
基本は自分用ですが、万一他人のリンクを取得したい場合は欲しいのはmdではなく機能するリンクなのでパーサーにかけて下部に表示する変換ボタンもつけました。

[Google検索はその方が便利な場面もあるだろうということで。](https://qiita.com/tadsan/items/882b23505858d7de25a8)

Qiita上で右側の見出しリストからジャンプできるように各行は見出し`#`で書いてますが、大きすぎたり下線が邪魔かもしれません。`###`の方がいいかも。

# ソースコード

<details>
<div>

```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Qiitaユーザーのタグ検索画面へのリンク集を作成します</title>
    <style type="text/css">
        div {
            margin-left: 3%;
        }
        #markdown {
            width: 100%;
        }
    </style>
    <script src="markdown-it.js"></script>
</head>
<body>
<div>
    <p><a href="https://qiita.com/">Qiita</a>ユーザーの直近100件の投稿タグを利用した検索画面への個人用リンク集を作成します</p>
    <p>ユーザーIDを入力して出力ボタンを押してください</p>
</div>
<div>
    <input type="text" id="userid" pattern="[\w-]{3,}" placeholder="ユーザーID(ex:qiita)" required autofocus>
    <input type="checkbox" id="ignore">
    <label for="ignore">1件のタグを無視する</label>

</div>
<div>
    <span>タグの並び:</span>
    <input type="radio" name="tagsort" id='pop' checked="true">
    <label for="pop">記事数順</label>
    <input type="radio" name="tagsort" id='asc'>
    <label for="asc">名前順</label>
</div>
<div>
    <span>検索画面の並び:</span>
    <input type="radio" name="searchsort" id='created' checked="true">
    <label for="created">新着順</label>
    <input type="radio" name="searchsort" id='rel'>
    <label for="rel">関連順</label>
    <input type="radio" name="searchsort" id='stock'>
    <label for="stock">ストック数順</label>
    <input type="radio" name="searchsort" id='like'>
    <label for="like">LGTM数順</label>
</div>
<div>
    <button id="getlist" type="button">タグを手動で選択する</button>
    <select id="list" multiple="true"></select>
</div>
<div>
    <button id="output" type="button">Markdownを出力</button>
    
</div>
<hr>
<div>
    <button id="copy">クリップボードにコピー</button>

</div>
<div>
    <textarea id="markdown" rows="15" placeholder="# [khskの記事](https://qiita.com/search?sort=like&q=user:khsk)
# [C#](https://qiita.com/search?sort=like&q=user:khsk+tag:C%23)
# [C++](https://qiita.com/search?sort=like&q=user:khsk+tag:C%2B%2B)
# [HTML](https://qiita.com/search?sort=like&q=user:khsk+tag:HTML)
# [JavaScript](https://qiita.com/search?sort=like&q=user:khsk+tag:JavaScript)
# [Node.js](https://qiita.com/search?sort=like&q=user:khsk+tag:Node.js)
# [PHP](https://qiita.com/search?sort=like&q=user:khsk+tag:PHP)
# [Ruby](https://qiita.com/search?sort=like&q=user:khsk+tag:Ruby)
# (外部検索)[Google検索](https://www.google.com/search?q=site:qiita.com/khsk)"></textarea>
</div>
<div>
    <button id="html">HTMLに変換</button>
</div>
<div id="markup"></div>


<script type="text/javascript">

    function validateUserId() {
        return document.querySelector('#userid').reportValidity()
    }

    async function fetchItems(){
        if (!validateUserId()) {
            return
        }
        const url = 'https://qiita.com/api/v2/items?per_page=100&query=user:'
        const res = await fetch(url + document.querySelector('#userid').value)
        return res.json()
    }

    function extractTags(json) {
        const tags = json.reduce((currentTags, item) => {
            item.tags.forEach(tag => {
                if(currentTags.hasOwnProperty(tag.name)) {
                    currentTags[tag.name].count += 1
                } else {
                    currentTags[tag.name] = {
                        name: tag.name,
                        count: 1,
                    }
                }
            })
            return currentTags
        }, {})
        return tags
    }

    function popSort(a, b) {
        return b[1].count - a[1].count
    }

    function nameSort(a, b) {
        return a[1].name.localeCompare(b[1].name)
    }

    function getSortedTags(tags) {
        tags = Object.entries(tags)
        // 同数のタグの並びをよくするため、文字列ソートは必ず通しておく
        tags = tags.sort(nameSort)
        if (document.querySelector('#pop').checked) {
            tags = tags.sort(popSort)
        }
        return tags
    }

    function deleteSingleTags(tags) {
        for (p in tags) {
            if (tags[p].count == 1) {
                delete tags[p]
            }
        }
        return tags
    }

    document.querySelector('#getlist').addEventListener('click', () => {
        if (!validateUserId()) {
            return
        }
        const getList = document.querySelector('#getlist')
        const tmpButtonText = getList.textContent
        getList.textContent = '取得中…'
        getList.disabled = 'true'
        document.querySelector('#output').disabled = 'true'
        fetchItems().then(items => {
            let tags = extractTags(items)
            if (document.querySelector('#ignore').checked) {
                deleteSingleTags(tags)
            }
            tags = getSortedTags(tags)
            const list = document.querySelector('#list')
            list.innerHTML = ''
            list.size = 5
            const option = document.createElement('option')
            tags.forEach(tag => {
                const option = document.createElement('option')
                option.textContent = tag[0]
                option.value = tag[0]
                list.appendChild(option)
            })
            getList.textContent = tmpButtonText
            getList.disabled = ''
            document.querySelector('#output').disabled = ''
        })
    })

    document.querySelector('#output').addEventListener('click',async () => {
        if (!validateUserId()) {
            return
        }
        const mdButton = document.querySelector('#output')
        const tmpButtonText = mdButton.textContent
        mdButton.textContent = '処理…'
        mdButton.disabled = 'true'
        document.querySelector('#getlist').disabled = 'true'

        const options = document.querySelectorAll('option:checked')
        let tags
        if (options.length) {
            tags = Array.from(options).map((option) => option.value)
        } else {
            const items = await fetchItems()
            tags = extractTags(items)
            if (document.querySelector('#ignore').checked) {
                tags = deleteSingleTags(tags)
            }
            tags = getSortedTags(tags).map(tag => tag[0])
        }
        const userid = document.querySelector('#userid').value
        const baseUrl = 'https://qiita.com/search?sort=' + document.querySelector('input[name="searchsort"]:checked').id + '&q=user:' + encodeURIComponent(userid)
        let mdText = ''

        mdText += '# [' + userid + 'の記事](' + baseUrl + ')\n'
        tags.forEach(tag => {
            mdText += '# [' + tag + '](' + baseUrl + '+tag:' + encodeURIComponent(tag)  + ')\n'
        })
        mdText += '# (外部検索)[Google検索](https://www.google.com/search?q=site:qiita.com/' + encodeURIComponent(userid) + ')'

        document.querySelector('#markdown').value = mdText

        mdButton.textContent = tmpButtonText
        mdButton.disabled = ''
        document.querySelector('#getlist').disabled = ''
        document.querySelector('#copy').textContent = 'クリップボードにコピー'
    })

    document.querySelector('#copy').addEventListener('click',() => {
        navigator.clipboard.writeText(document.querySelector('#markdown').value).then(() => {
            document.querySelector('#copy').textContent = 'コピーしました'
        })
    })

    document.querySelector('#html').addEventListener('click',() => {
        document.querySelector('#markup').innerHTML = (new markdownit).render(document.querySelector('#markdown').value)
    })
</script>
</body>
</html>
```
</div>

</details>


# Qiitaのユーザー名制限の記録

Qiitaのユーザー名で使える文字種は何だったかしらんと`/users`でユーザー名を見ようと思いましたが、[そういえば一覧は廃止されていた](https://blog.qiita.com/close-user-list-page/)ので新規登録ページで見ました。

![qiitaname.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62354/34295918-69a5-40ac-8179-f22942197a01.jpeg)

patternでは困らないであろう`[\w-]{3,}`にしました。

# 備忘録

## [批判を受けたQiita運営のアップデート「50万人のコミュニティ」といかに対話するか | i:Engineer(アイエンジニア)｜パーソルテクノロジースタッフのエンジニア派遣](https://persol-tech-s.co.jp/i-engineer/product/ateamqiita)

>
最近、特に感心したのが、Qiitaの記事検索ができる「Qiigle（キーグル）」という仕組みを実際に作ったユーザーさんの記事でした。
>
▼【Qiigle】というQiitaの記事を検索するサービスを作りました
>
Qiitaのユーザーさんって、開発チームに新しい機能要望を出すだけじゃなくって、実際に作って提案してくれるんですよね。「こういうのがあったらいいな」を、本当に作って持ち込んでくれる。それはプログラマーのコミュニティならではなのかな、と。

うーん…2020でこの反応は公式の機能強化はまだまだなさそうかな？

# 検索強化関連記事(古いものもアイデアとして)

* [Qiita 記事や公式ドキュメントを検索するのに便利な TIPS【検索に Google エンジンを使う】 - Qiita](https://qiita.com/KEINOS/items/63c5f9b1c1f70f1ff531)
* [Qiitaのユーザーページにタグ検索のリンクを追加するChrome拡張機能「Qiitag Link」を作った - Qiita](https://qiita.com/sola-msr/items/c16f8045678a9d060088)  
<div>

>
ユーザーごとにどんな記事を投稿しているのかはユーザーページを見ればグラフで表示されているので一発でわかるのですが、
じゃあそのタグの記事を見たいとなると、ユーザーページから絞れないし、検索の時にオプション指定する必要があったりでめんどくさい。
</div>

* [Chrome拡張 | soku_qiita - 即聞いた | Qiita のユーザー・タグ指定検索をマウスを使わずに素早く実行する #chrome_extension - Qiita](https://qiita.com/tbpgr/items/c643533b39e5aeeb15b9)
* [Qiita ブックマークレット プロンプトで入力した特定キーワードでQiita内の自分の記事を検索する - Qiita](https://qiita.com/koara-local/items/591208ab70fcc440089a)

# 自分の昔の

* [記事に「user:user tag:記事のタグ」への検索リンクを追加するユーザースクリプト - Qiita](https://qiita.com/khsk/items/4ce78f87ebc6082e72b7)
* [Qiitaのユーザー画面からそのユーザー記事のフリーワード検索へ移動する - Qiita](https://qiita.com/khsk/items/4a6ffc6fc1b98d058da4)

[^1]: 検索オプションをクリックすれば自分の記事を絞り込めるオプションになりますが、そもそも空の検索キーワードで検索画面に飛べる導線が見つからず、適当に「a」と入力して検索画面へ飛ぶ無情感があります。