0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブルースクレイ・ジャパンが自社でSEO Chrome拡張を開発した理由

0
Posted at

※この記事はAIを使用して翻訳されています。
 表現に不自然な点がありましたら申し訳ありません。
※ページ下部に元の英語版も掲載しています。

手動でのSEOタグ確認に疲れ果てた話

作業を爆速にするために、ブラウザ拡張機能を自作しました。

Webデベロッパーやデザイナー、特にSEOスペシャリストの方なら、お決まりの流れをご存知でしょう。新しいランディングページやブログ記事を公開したら、そこから「監査」が始まります。

  • 右クリック -> ページのソースを表示して、<title><meta name="description" content="..."> を確認。
  • DevToolsを開く -> Elementsパネルで canonical URL が正しいか検証。
  • 見出しタグ、メタディスクリプション、各タグを追いかけ、すべてが整合しているかチェック。
  • 構造化データ(JSON-LD)を検証するために、全く別のツールに切り替える。
  • URLを別のサービスにコピペして、OGPのプレビュー画像を確認。

退屈で、悪夢のような作業です。

「ソースを表示」問題

さらに悪いことに、「ページのソースを表示」はますます信頼できなくなっています。Svelte、SolidJS、Vue、Reactなどのモダンなフレームワークの台頭により、サーバーから送られる生のHTMLは、JavaScript実行後にユーザー(およびGooglebot)が見るものと一致しないことがよくあります。

私は、HTMLソースとレンダリングされたDOMの不一致が原因で、クライアントのデバッグに追われる自分(やチームメンバー)の姿を何度も目にしました。

私が必要としていたのは単なるチェックリストではなく、SEOのための「ライブデバッガー」でした。

必要なツールの条件はこうです:

  • クリックすると消えてしまうポップアップではなく、Chrome DevTools内に直接常駐すること。
  • サーバーの初期レスポンスだけでなく、現在のDOM状態を反映すること。
  • Google構造化データのような複雑なデータに対して、シンタックスハイライトを提供すること。

ブルースクレイ・ジャパンでの私のエンジニアリングワークフローに合うツールが見つからなかったので、自分で作ることにしました。

それが SEOdin Page Analyzer です(そして、ブラウザ拡張機能開発という地獄の始まりでもありました)。

技術スタック: Svelte 5, Vite (rolldown), Comlink

静的なスキャナーではなく「ライブデバッガー」のように感じられるツールを作るには、最小限のオーバーヘッドで細かいリアクティビティを処理できるスタックが必要でした。

数年前にSolidJSを使い始めてとても気に入っていましたが、最終的に以下の構成に落ち着きました:

  • Svelte 5(新しいRunes APIを使用)
  • Vite(現在はrolldownを使用)、Svelteコンポーネントのコンパイル用
  • Comlink(RPCラッパー)、各拡張コンテキスト間の通信用

なぜ Svelte 5 なのか?

SEO監査の状態管理は驚くほど複雑です。1つのページに、インデックス可能性、タイトル長、canonicalリンク、hreflangリンク、見出し構造、alt属性、アクセシビリティなど、同時にチェックされる何百もの「ルール」が存在する場合があります。そしてウェブサイトがSPA(シングルページアプリケーション)である場合、これらの要素はいつでも変更される可能性があります。

ノードが変わるたびに監査全体を再実行したくはありませんでした。きめ細やかなリアクティビティが必要だったのです。

ここで Svelte 5 Runes が威力を発揮します。標準のstoreの代わりに、分析状態エンジンを実装しました。

以下は、コアの分析ロジック (seodin.svelte.ts) のスニペットです:

// src/lib/seodin.svelte.ts

export const enum AnalysisStatus {
	IGNORE = "ignore",
	INFO = "info",
	SUCCESS = "success",
	WARNING = "warning",
	ERROR = "error",
}

export interface Analysis {
	status: AnalysisStatus;
	detail: string;
}

// Runesを使用してリアクティブな分析オブジェクトを作成
export function create_analysis(
	status = AnalysisStatus.IGNORE,
	detail = "",
): Analysis {
	const analysis_state = $state<Analysis>({ status, detail });
	return analysis_state;
}

$derived を使用することで、要素の「最も重要なステータス」を自動的に計算できます。ユーザーが不足している alt タグを修正すると、その特定の部分の状態だけが更新され、UIに即座に反映されます。それがホットリロードによるものであれ、DevTools Workspace を使用してブラウザで直接編集したものであれ関係ありません。

// 下位のレポートが変わると、ステータスは自動的に更新されます
const most_critical_status = $derived(
	get_most_critical_status(report(), reveal_priority),
);

Comlink によるコンテキストを跨いだ状態管理

Chrome拡張機能開発における最大の苦痛の1つは、コンテキストスクリプト、サービスワーカー、DevToolsパネルなどの間で行われる非同期メッセージングです。標準の chrome.runtime.sendMessage APIは、しばしば「コールバック地獄」や乱雑なコードを招きます。

これを解決するために、Chrome拡張のポートの特性に合わせて調整したComlinkを使用しました。これにより、サービスワーカーをローカルモジュールのように扱い、RPC(リモートプロシージャコール)経由で直接関数を呼び出すことができます。

service_worker/rpc.ts では、単にAPIを公開するだけです:

// src/service_worker/rpc.ts

import * as Comlink from "$lib/comlink";
import { PortName } from "$lib/Port";
import * as http from "./rpc/http";
import * as robots_txt from "./rpc/robots_txt";
import * as text from "./rpc/text_content";

const service_worker_api = {
	...http,
	...robots_txt,
    ...text,
} as const;

// 他のパーツに対してAPIを公開
chrome.runtime.onConnect.addListener((port) => {
	if (port.name !== PortName.SERVICE_WORKER) return;
	Comlink.expose(service_worker_api, port);
});

この抽象化により、ブラウザ拡張機能の複雑なアーキテクチャを1つのまとまったアプリケーションとして扱い、コードベースをクリーンでメンテナンス可能な状態に保つことができます。

「見えない」課題:Shadow DOM と 厳格なスキーマ

ユーザーのページ内に表示されるツールチップを追加すると、巨大な継承問題が発生します。そう、CSSです。

もし私の拡張機能が .container.button のような汎用的なクラスを使用すると、ホストページのスタイルを継承して表示が壊れたり、逆にホストページのスタイルを上書きしてサイトを壊したりする可能性があります。もちろん、light-DOMだけで回避する方法もありますが、常に何らかの欠点がありました。

1. Shadow DOM によるスタイル隔離

これを最適に解決するため、SEOdinは注入するUI全体をShadow Rootでラップしています。

src/components/Root.svelte では、手動でコンテナ要素にShadow Rootをアタッチしています。これにより、拡張機能とホストページの間に「スタイルの防火壁」が作られます。

// ...

function get_mount_target_shadow_root() {
	mount_target = document.getElementById(seodin_root_id);
	if (!mount_target) {
		console.log("Creating SEOdin mount root...");
		mount_target = document.createElement("seodin-ext");
		mount_target.id = seodin_root_id;
		//
		// フォントのスタイルシートは、https://issues.chromium.org/issues/41085401
		// が修正されるまで、シャドウDOMの外側にも*同様に*追加される必要があります。
		//
		const font_link = document.createElement("link");
		font_link.rel = "stylesheet";
		font_link.href = font_css_url_resolved;
		mount_target.append(font_link);
		document.body.append(mount_target);
	}
	let mount_target_shadow_root = mount_target.shadowRoot;
	if (!mount_target_shadow_root) {
		console.log("Creating SEOdin mount target...");
		mount_target_shadow_root = mount_target.attachShadow({
			mode: "open",
		});
	}
	return mount_target_shadow_root;
}

// ...

let mounted_component: object | null = null;
async function mount_seodin_component() {
	await unmount_seodin_component();
	console.log("Mounting SEOdin...");
	try {
		mounted_component = mount(Main, { target: get_mount_target_shadow_root() });
		mover.observe(document.body, { childList: true });
	} catch (e) {
		console.error("Failed to mount SEOdin!", e);
	}
}

// ...

これにより、ホストページのCSSがいかに乱雑であっても、ページ上のツールチップは一貫した表示を維持できます。

2. JSON-LD のカオス

ほとんどのSEOツールは、構造化データを完全に無視するか(その潔さが羨ましいですが)、JSON-LDが有効なJSONであるかどうかをチェックする程度です。外部ツールを使わずに検証を試みる拡張機能は、私の知る限り他にありません。

たとえば、Product スキーマには offersreview といった特定のプロパティが必要です。Googleのリッチリザルトテストは素晴らしいですが、速度が遅く、画面を切り替える必要があります。

私はその検証を直接DevToolsパネルに持ち込むことにしました。

そのために、Google構造化データの型に対するバリデーターの巨大なコレクションを実装しました。

  • src/ ... /schema/types/Product.ts
  • src/ ... /schema/types/Recipe.ts
  • src/ ... /schema/types/Organization.ts

これにより、拡張機能はリアルタイムでデータの構造を分析できます。

// 特定のSchemaタイプを分析する例
import type { Product } from "./schema/types/Product";

export async function* analyzeProduct(data: Product) {
	if (!data.offers && !data.review) {
		yield {
			status: AnalysisStatus.WARNING,
			message: "Productにoffersまたはreviewが不足しています",
		};
	}
	// ... 深層分析のためのロジック
}

Svelte 5のリアクティビティとCodeMirrorのエディタを組み合わせることで、拡張機能は(VS Codeのような)シンプルなインターフェースを即座に提供し、ユーザーが変更を加えると同時にスキーマをリアクティブに分析します。

今後のロードマップ

SEOdin Page Analyzerは現在ブルースクレイ・ジャパンで活発に開発されており、まだ始まったばかりです。

私のゴールは、単なる「SEOチェッカー」を作ることではなく、モダンなフロントエンドエンジニアリングとテクニカルSEOの間のギャップを埋めるツールを構築することです。

正直なところ、やりたいことはいくらでもありますが、他のタスクやプロジェクトを優先しなければならないことも多いです。いくつかのアイデアを挙げます:

  • ページ上のツールチップをDevToolsパネル内の表示に置き換え、分析された要素が重なって巨大なツールチップが生成される稀なケースを回避する。
  • 構造化データのチェック項目を増やす(あまり一般的でないオブジェクトはまだ分析されていません)。
  • より多くのデータ(GSC、GA4など)のためにサードパーティ連携を追加する。
  • Chromeの内蔵AI機能を活用する。

試してみて

退屈なSEOチェックに疲れている方、あるいはJSON-LDをよりクリーンにデバッグする方法を探している方は、ぜひこの拡張機能を試してみてください。完全に無料で、今後も無料です。

チャットしましょう

フィードバックを直接いただければ幸いです。バグを見つけた、機能のリクエストがある、あるいは単に話をしたいという方でも、Google Chat スペースにチャットしましょう。

読んでいただきありがとうございました!

( 英語 )

I was tired of manually reviewing HTML tags for SEO

So, I created a browser extension to speed things along.

If you're a web developer, web designer, or especially an SEO specialist, you probably know the drill. You launch a new landing page or a blog post, and then the "audit" begins.

  • Right-click -> View Page Source to check the <title> and <meta name="description" content="...">.
  • Open DevTools -> Elements to verify the canonical URL is correct.
  • Hunt down title tags, meta descriptions, and heading tags to ensure everything correlates well.
  • Switch to a completely different tool to validate your Structured Data (JSON-LD).
  • Copy-paste the URL into another service to check Open Graph preview images.

It's a tedious nightmare.

The "View Source" Problem

To make matters worse, "View Page Source" is becoming increasingly unreliable. With the rise of modern frameworks like Svelte, SolidJS, Vue, React etc., the raw HTML sent from the server often doesn't match what the user (and Googlebot) sees after the JavaScript executes.

I constantly found myself (and other team members) debugging issues for clients caused by a mismatch between the HTML source and the rendered DOM.

I didn't just need a checklist; I needed a live debugger for SEO.

I needed a tool that:

  1. Lives directly in Chrome DevTools, not a popup that disappears when I click away.
  2. Reflects the current DOM state, not just the initial server response.
  3. Provides syntax-highlighting for complex data like Google Structured Data.

Since I couldn't find a tool that fit my specific engineering workflow at Bruce Clay Japan, I decided to build one.

Enter SEOdin Page Analyzer (and the hell that is browser extension development).

Technical Architecture: Svelte 5, Vite (rolldown), and Comlink

To build a tool that feels like a "Live Debugger" rather than a static scanner, I needed a tech stack that could handle fine-grained reactivity with minimal overhead.

I started with SolidJS years ago, and enjoyed it a lot, but eventually settled on the following:

  • Svelte 5 for the (with the new Runes API)
  • Vite (now with rolldown), to compile the Svelte components, and
  • Comlink (RPC wrapper), to communicate between each extension context

Why Svelte 5?

Handling the state of an SEO audit is surprisingly complex. A single page might have hundreds of "rules" being checked simultaneously for things like indexability, title length, canonical links, hreflang links, heading structures, alt attributes, accessibility, and more. And when a website is a Single Page Application (SPA), these elements can change at any time.

I didn't want to re-run the entire audit every time a node changed. I needed fine-grained reactivity.

This is where Svelte 5 Runes shine. Instead of standard stores, I implemented an analysis state engine.

Here is a snippet from my core analysis logic (seodin.svelte.ts):

// src/lib/seodin.svelte.ts

export const enum AnalysisStatus {
	IGNORE = "ignore",
	INFO = "info",
	SUCCESS = "success",
	WARNING = "warning",
	ERROR = "error",
}

export interface Analysis {
	status: AnalysisStatus;
	detail: string;
}

// Using Runes to create a reactive analysis object
export function create_analysis(
	status = AnalysisStatus.IGNORE,
	detail = "",
): Analysis {
	const analysis_state = $state<Analysis>({ status, detail });
	return analysis_state;
}

By using $derived, I can automatically calculate the "most critical status" of an element. If a user fixes a missing alt tag, only that specific part of the state updates, and the UI reflects it instantly, whether it was updated through hot-reloading or editing directly in the browser using a DevTools Workspace.

// The status automatically updates when the underlying report changes
const most_critical_status = $derived(
	get_most_critical_status(report(), reveal_priority),
);

Tracking Cross-Context State with Comlink

One of the biggest pain points in Chrome Extension development is the asynchronous messaging between the Context Scripts, the Service Worker, the DevTools Panel, and more. The standard chrome.runtime.sendMessage API often leads to "callback hell" and messy code.

To solve this, I used a modified version of Comlink. Modified in order to work with the peculiarities of Chrome's extension ports. This allows me to treat the Service Worker almost like a local module, calling functions directly via RPC (Remote Procedure Call).

In service_worker/rpc.ts, I simply expose the API:

// src/service_worker/rpc.ts
import * as Comlink from "$lib/comlink";

const service_worker_api = {
	...text,
	...robots_txt,
	...http,
} as const;

// Expose the API to other parts of the extension
chrome.runtime.onConnect.addListener((port) => {
	if (port.name !== PortName.SERVICE_WORKER) return;
	Comlink.expose(service_worker_api, port);
});

This abstraction allows me to keep the codebase clean and maintainable, treating the complex architecture of a browser extension as a cohesive application.

The "Invisible" Challenges: Shadow DOM & Strict Schema

Adding tooltips that live inside a user's page comes with a massive inheritance problem... CSS.

If my extension uses a generic class like .container or .button, it might inherit styles from the host page (making the extension look broken) or, worse, override the host page's styles (breaking the user's site). Of course, there are ways around this using only the light-DOM, but there was always some drawback.

1. Style Isolation with Shadow DOM

To best solve this, SEOdin wraps its entire injected UI in a Shadow Root.

In src/components/Root.svelte, I manually attach a shadow root to a container element. This creates a "style firewall" between the extension and the host page.

// ...

function get_mount_target_shadow_root() {
	mount_target = document.getElementById(seodin_root_id);
	if (!mount_target) {
		console.log("Creating SEOdin mount root...");
		mount_target = document.createElement("seodin-ext");
		mount_target.id = seodin_root_id;
		//
		// The font stylesheet needs to *also* be appended outside the
		// shadow DOM until https://issues.chromium.org/issues/41085401
		// is fixed.
		//
		const font_link = document.createElement("link");
		font_link.rel = "stylesheet";
		font_link.href = font_css_url_resolved;
		mount_target.append(font_link);
		document.body.append(mount_target);
	}
	let mount_target_shadow_root = mount_target.shadowRoot;
	if (!mount_target_shadow_root) {
		console.log("Creating SEOdin mount target...");
		mount_target_shadow_root = mount_target.attachShadow({
			mode: "open", // must be "open" in order to access any existing shadowRoot
		});
	}
	return mount_target_shadow_root;
}

// ...

let mounted_component: object | null = null;
async function mount_seodin_component() {
	await unmount_seodin_component();
	console.log("Mounting SEOdin...");
	try {
		mounted_component = mount(Main, { target: get_mount_target_shadow_root() });
		mover.observe(document.body, { childList: true });
	} catch (e) {
		console.error("Failed to mount SEOdin!", e);
	}
}

// ...

This ensures that no matter how messy the host page's CSS is, the on-page tooltip remains consistent.

2. The JSON-LD Chaos

Most SEO tools either ignore Structure Data outright (I envy that approach) or check if your JSON-LD is valid JSON. I'm not aware of any extensions that attempt to validate it without using external tools.

For example, a Product schema requires specific properties like offers or review. Google's Rich Results test is great, but it's slow and requires a context switch.

I decided to bring that validation directly into the DevTools panel.

To do this, I implemented a massive collection of validators for Google Structured Data types.

  • src/ ... /schema/types/Product.ts
  • src/ ... /schema/types/Recipe.ts
  • src/ ... /schema/types/Organization.ts

This allows the extension to analyze the structure of the data in real-time.

// Example of how we analyze specific Schema types
import type { Product } from "./schema/types/Product";

export async function* analyzeProduct(data: Product) {
	if (!data.offers && !data.review) {
		yield {
			status: AnalysisStatus.WARNING,
			message: "Product is missing offers or reviews",
		};
	}
	// ... specific logic for deep analysis
}

Combining Svelte 5's reactivity and CodeMirror's code editor, the extension can instantly provide a simple interface (similar to VS Code) and reactively analyze the schema as a user alters it.

Future Roadmap

SEOdin Page Analyzer is currently in active development at Bruce Clay Japan, and we're just getting started.

My goal isn't just to build another "SEO Checker," but to build a tool that bridges the gap between modern frontend engineering and technical SEO.

Honestly, I could add any number of things, but I often have other tasks and projects that take priority. A few ideas are:

  • Replace the on-page tooltip with a tooltip displayed in the DevTools panel to avoid the rare case in which many analyzed elements overlap and generate a massive tooltip...
  • Add more Structured Data checks (many less common objects are not yet analyzed).
  • Add third-party integrations for more data (GSC, GA4, etc.).
  • Utilize the built-in AI capabilities of Chrome.

Try It Out

If you're tired of the tedious SEO checks, or if you just want a cleaner way to debug JSON-LD, give the extension a try. It's completely free and will remain so.

Let's Chat!

I'd love to hear your feedback directly. Whether you found a bug, have a feature request, or just want to chat, come say hi in our Google Chat space.

Thank you for reading!

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?