2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

はしがき

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

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

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

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

作ったもの

md.JPG

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

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

Google検索はその方が便利な場面もあるだろうということで。

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

ソースコード

<!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>

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

Qiitaのユーザー名で使える文字種は何だったかしらんと/usersでユーザー名を見ようと思いましたが、そういえば一覧は廃止されていたので新規登録ページで見ました。

qiitaname.JPG

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

備忘録

批判を受けたQiita運営のアップデート「50万人のコミュニティ」といかに対話するか | i:Engineer(アイエンジニア)|パーソルテクノロジースタッフのエンジニア派遣

最近、特に感心したのが、Qiitaの記事検索ができる「Qiigle(キーグル)」という仕組みを実際に作ったユーザーさんの記事でした。

▼【Qiigle】というQiitaの記事を検索するサービスを作りました

Qiitaのユーザーさんって、開発チームに新しい機能要望を出すだけじゃなくって、実際に作って提案してくれるんですよね。「こういうのがあったらいいな」を、本当に作って持ち込んでくれる。それはプログラマーのコミュニティならではなのかな、と。

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

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

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

自分の昔の

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

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?