韓国で「주소모음(アドレス集)」「링크모음(リンク集)」と検索すると、上位に並ぶのはほとんどが違法サイトのアドレス一覧です。一方で、銀行・行政サービス・宅配追跡のように毎日使うサイトの公式URLは、偽サイトや広告と混ざっていて意外と探しにくい。「公式サイトのURLだけを、人が確認して載せる」ディレクトリがあれば役に立つと思い、個人で作ったのが 주소핀(JusoPin) です。
この記事では、何を載せているか、どう作ったか、運用して分かったこと(特にクローラーと検索エンジン周り)をまとめます。
何を載せているか
29分野、約340サイト。銀行、カード、行政・公共機関、ショッピング、動画配信、教育、AIツールなど
サイトごとに 公式URL・公式ドメイン(登録可能ドメイン)・運営主体・確認日・紹介文・FAQ・偽装URLの例
確認は人手で行います。運営機関の公式案内ページ、アプリストアの開発者情報、サイト下部の事業者情報を突き合わせ、確認した日付を表示
公式URLが変わったら変更履歴に記録。確認済みのフィッシングURLはクリックできない形で公開
受け取ったURLを登録済みの公式ドメインと照合する URL確認ツール
公開データ: sites.json(CC BY 4.0)、llms.txt
ギャンブル・アダルト・海賊版サイトは最初から載せません。
技術構成
役割 使ったもの
Web ASP.NET Core 10 / Razor Pages, EF Core
DB PostgreSQL 17
実行環境 Docker Compose、Caddy(TLS・リバースプロキシ)、Cloudflare(CDN・Brotli)
検索エンジン向け sitemap.xml、RSS、JSON-LD(WebSite / CollectionPage / ItemList / FAQPage / BreadcrumbList)、IndexNow
キャッシュ Output Caching(公開ページ5分)
セキュリティ CSP(インラインスクリプトなし)、HSTS
フレームワークに特別なことはしていません。工夫したのは、むしろ「何をしないか」と、韓国語特有の処理です。
実装で工夫した点
- 韓国語ドメイン(IDN)の表示
韓国には 도구.site のようなハングルのドメインがあります。内部では punycode(xn--2e0b83j.site)で持ち、画面では Unicode に戻して表示します。比較は常に punycode 側で行います。
// 表示用: punycode → Unicode。変換できない文字列はそのまま返す
public static string ToUnicodeHost(string host)
{
try { return new IdnMapping().GetUnicode(host); }
catch (ArgumentException) { return host; }
}
2. 検索: DB の LIKE ではなくメモリで
登録数は数百件なので、検索のたびに全件を軽い DTO で読み、メモリで採点します。空白や記号を落として正規化し、名前の完全一致 > 前方一致 > 部分一致 > タグ > 運営主体 > 紹介文の順に重みを付けます。
if (name == nq) score = 100;
else if (name.StartsWith(nq)) score = 80;
else if (name.Contains(nq)) score = 60;
if (score < 55 && tags.Any(t => t == nq)) score = 55;
if (score < 45 && tags.Any(t => t.Contains(nq))) score = 45;
韓国語には「初声検索」(ㄱㄴㄷ…の子音だけで探す)という文化があるので、ハングル音節を初声に分解した文字列でも照合します。結果が0件のときは Levenshtein 距離でタイポ候補を出し、同じ採点ロジックをオートコンプリート API(/api/suggest)でも使っています。
-
外部リンクの rel は「基本 nofollow、サイト単位で解除」
ディレクトリなので外部リンクが大量にあります。基本は rel="nofollow noopener" にし、管理画面のチェック一つで特定サイトだけ解除できるフラグを持たせました。内部リンクは、クロール不要なページ(通報フォームなど、robots.txt で除外しているもの)だけ nofollow にしています。 -
紹介文の中のサイト名を自動リンク
紹介文に登録済みの別サイト名が出てきたら、最初の1回だけそのサイトのページへリンクします。一般名詞と重なる名前(「다음」= 次、「이슈」= 話題、など)は除外リスト、1文あたり最大5件。 ここで一つハマったのが HtmlEncoder.Default です。既定では非 ASCII をすべて 애 のような数値参照にしてしまうので、HtmlEncoder.Create(UnicodeRanges.All) を使って韓国語をそのまま出す必要がありました。 -
「名前が似ているサイト」
名前をトークンに分け、片方のトークンがもう片方に含まれていれば「混同しやすい」と判定します。韓国のポータル「줌(zum.com)」とビデオ会議の「Zoom(zoom.com)」のように、検索で混ざりやすいペアを各ページに表示できます。 -
自動の死活監視を「やめた」
初期は登録サイトに定期的にアクセスして生存確認する機能がありました。ところが、ある金融機関のセキュリティ担当がその繰り返しアクセスを攻撃と判断し、サーバーが止められました。以後この機能はコードから完全に削除し、確認は人手に戻しました。公式サイトを扱うサービスでは「自動でアクセスしない」こと自体が信頼につながると考えています。
運用して分かったこと(クローラーと検索エンジン)
Bing は HTML が約125KBを超えると「too long」と警告し、後半を取り込まない可能性があります。 トップページが139KBあったので、行のマークアップから改行・インデントを消し、トップページだけ外部リンクの矢印ボタンを外して93KBまで減らしました。文章は1文字も削っていません。
Googlebot は新規サイトには1日7〜20リクエスト程度しか来ません。 Search Console からインデックス登録をリクエストした日だけ40回以上になります。アクセスログからクローラーの訪問時刻を一覧する管理画面を作ると、何が起きているか追いやすくなりました。
検索意図のミスマッチ。 「アドレス集」で検索する人の多くは違法サイトを探しているので、一時的に3位まで上がっても滞在が短く、数日で落ちます。これは対策というより、狙うキーワードを「〇〇 公式サイト URL」のようなページ単位に寄せるべきだという学びでした。
Lighthouse の指摘は地味に効きます。オートコンプリートの に aria-expanded を付けるなら role="combobox" が必要、アバターの白文字は色相によってコントラスト2.6:1しかない、など。
おわりに
日本でもフィッシングや偽サイトの問題は同じだと思うので、「公式サイトだけを人手で確認して載せる」という仕組みは日本語圏でも作れそうです。サイトは jusopin.com、データは GitHub と sites.json で公開しています。ご意見・ご指摘があればコメントでお願いします。