はじめに
DTMプラグインやソフト音源のセール情報を、海外のDTM情報サイトから集めて日本語でまとめて表示する DTM PICKS を個人で作っています。
- 公開URL:https://dtmpicks.com
前回までに、RSSの横断取得・検索・自動翻訳、ページごとの title / description / OGP / canonical、カテゴリ別ページ、sitemap.xml の自動生成までは作っていました。
ただ、中身の大半は「他サイトの情報の要約と機械翻訳」です。検索エンジンから見ると独自の価値が薄く、広告(AdSense)の審査でも不利になりやすい状態でした。そこで Claude Code と一緒に、次のことを進めました。
- 検索から人が来る入口になる ガイド記事 を追加(Article の構造化データ付き)
-
vercel.appから 独自ドメイン へ移行(転送・ads.txt・Search Console の登録し直し) - アフィリエイトと AdSense の状態を点検
- 記事の下書きを毎朝自動で作り、人が読んでから公開 する流れを作った
この記事は、その記録です。つまずいた点も書いておきます。
構成
| 役割 | 使っているもの |
|---|---|
| フロントエンド | React 19 + Vite(ルーターなし。URL ごとに静的 HTML を出し分け) |
| データ取得 | Vercel Serverless Functions(/api/deals で RSS を取得) |
| 翻訳 | Google Cloud Translation API |
| ホスティング | Vercel |
1. ガイド記事を「静的 HTML まるごと」で出す
もともとの仕組み
ページの定義を src/seo-pages.js に1か所でまとめ、ビルド時に Vite のプラグインがページごとの HTML を出力する作りにしていました。React アプリと、ビルド時の HTML 生成の両方が同じ定義を読むので、検索エンジンが読む内容と画面の内容がずれません。
// vite.config.js(抜粋)
function seoPagesPlugin(siteUrl) {
const [homePage, ...subPages] = seoPages
return {
name: 'seo-pages',
enforce: 'post',
generateBundle(_options, bundle) {
const html = String(bundle['index.html'].source)
for (const page of subPages) {
this.emitFile({
type: 'asset',
fileName: `${page.path.slice(1)}.html`, // category/synth.html, guide/xxx.html
source: renderPageHtml(html, page, siteUrl),
})
}
// robots.txt と sitemap.xml もここで出力
},
}
}
ガイド記事を足す
この仕組みを広げて、/guide/<slug> を追加しました。記事はデータとして定義します。
// src/seo-pages.js(抜粋)
export const guidePages = [
{
slug: 'black-friday-2026',
name: 'ブラックフライデー2026の狙い目',
title: 'ブラックフライデー2026 DTMプラグインセールの狙い目と準備ガイド - DTM PICKS',
description: '…',
h1: '…',
datePublished: '2026-10-24',
dateModified: '2026-10-24',
intro: ['…'],
sections: [{ heading: '2026年の日程', paragraphs: ['…'], list: ['…'] }],
sources: [{ label: '…', url: 'https://…' }],
},
// 年間セールカレンダー、初心者向け無料プラグインの選び方
]
カテゴリページは一覧を JavaScript で読み込むので、静的 HTML には見出しと導入文だけを出しています。一方、ガイド記事は中身が固定の文章なので、本文をまるごと静的 HTML に出す ようにしました。JavaScript を実行しないクローラーにも本文が読めます。
記事には Article の構造化データと、パンくずリスト(BreadcrumbList)を付けています。
// lib/seo-html.js(抜粋)
{
'@context': 'https://schema.org',
'@type': 'Article',
headline: page.h1,
description: page.description,
url: pageUrl,
mainEntityOfPage: pageUrl,
datePublished: page.datePublished,
dateModified: page.dateModified,
inLanguage: 'ja',
author: { '@type': 'Organization', name: SITE_NAME },
publisher: { '@type': 'Organization', name: SITE_NAME },
}
つまずいた点:トップページ用の構造化データが記事にも出ていた
最初のビルドで、記事ページに WebSite(トップページ用)の構造化データが出ていました。もとのコードが「カテゴリでなければトップページ」という分岐になっていたためです。
- if (!page.category) {
+ if (!page.category && !page.guide) {
ビルド後に、各ページの JSON-LD の @type を Node で取り出して確認するようにしたら、すぐ気づけました。
node -e "
const h = require('fs').readFileSync('dist/guide/sale-calendar.html', 'utf8');
for (const m of h.matchAll(/ld\+json\">(.*?)<\/script>/g)) console.log(JSON.parse(m[1])['@type'])"
Vercel 側の設定
/guide/xxx で guide/xxx.html を返すように、rewrites を足しています。
"rewrites": [
{ "source": "/category/:slug", "destination": "/category/:slug.html" },
{ "source": "/guide/:slug", "destination": "/guide/:slug.html" }
]
記事の中身で気をつけたこと
- 事実は根拠のあるものだけ:セールの時期などは、過去に実際にあったセールの情報(メーカーのプレスリリースや海外のまとめ記事)をもとに書き、記事の最後に出典を載せました。公式発表でない情報は、本文でもそう書いています。
- 値引き率の予想はしない:毎年変わるので、確かなことが言えないためです。
- トップとカテゴリページから記事へリンク:記事が孤立しないようにしました。
2. vercel.app から独自ドメインへ移行
なぜ移行したか
AdSense の審査を受けようとしたところ、xxx.vercel.app のままでは難しそうだと分かりました。
- AdSense は、自分が持っているドメインでサイトを登録する前提になっている
-
ads.txtはドメインのいちばん上に置く必要があるが、vercel.appのいちばん上は Vercel のもの
ドメインは Vercel の画面から買いました。買った時点で DNS が Vercel につながるので、設定が楽でした。
Vercel CLI でつなぎ込み
npx vercel domains add dtmpicks.com reactproject
npx vercel domains add www.dtmpicks.com reactproject
npx vercel domains verify dtmpicks.com
printf 'https://dtmpicks.com' | npx vercel env add SITE_URL production
SITE_URL は、canonical・sitemap.xml・構造化データの URL を組み立てるのに使っています。
旧アドレスからの転送
古い vercel.app のアドレスと www を、新ドメインの同じページへ 301 系の転送(Vercel では 308)で送ります。
"redirects": [
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "reactproject-nine-pi.vercel.app" }],
"destination": "https://dtmpicks.com/:path",
"permanent": true
},
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "www.dtmpicks.com" }],
"destination": "https://dtmpicks.com/:path",
"permanent": true
}
]
つまずいた点:/:path* ではトップページが転送されなかった
最初は "source": "/:path*" と書いていました。デプロイ後に curl で確認すると、/category/synth などは転送されるのに、トップページ / だけ 200 のままでした。/:path(.*) に変えたら、トップページも転送されるようになりました。
curl -s -o /dev/null -w "%{http_code} -> %{redirect_url}\n" https://reactproject-nine-pi.vercel.app/
# 308 -> https://dtmpicks.com/
転送設定は、新ドメインがつながる前にデプロイすると、サイトが開けなくなります。つなぎ込みが終わってからデプロイしました。
ads.txt
public/ads.txt に1行置くだけです(pub- 以降は自分のサイト運営者ID)。
google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0
Search Console の登録し直し
独自ドメインでは「ドメイン」タイプのプロパティで登録しました。確認用の TXT レコードは、Vercel CLI で追加できます。
npx vercel dns add dtmpicks.com '' TXT 'google-site-verification=xxxxxxxx'
dig +short TXT dtmpicks.com @8.8.8.8 # 外から見えるか確認
つまずいた点:サイトマップの送信が「無効」になった
URLプレフィックス型のプロパティでは sitemap.xml だけで送れました。ドメイン型では、https://dtmpicks.com/sitemap.xml のように 完全な URL で入力する必要がありました。
3. アフィリエイトと AdSense の点検
「アフィリエイトできてる?」と聞いたら、できていませんでした。
アフィリエイト ID を環境変数で渡す作りにしていたのですが、その環境変数が Vercel に登録されていませんでした。ビルド後の JavaScript を見ると、ID が void 0(未定義)のままでした。
curl -s https://dtmpicks.com/assets/index-xxxx.js | grep -o '.\{60\}a_aid.\{80\}'
# …param:`a_aid`,id:void 0}…
Vite の import.meta.env.VITE_* はビルド時に値が埋め込まれます。なので、Vercel の環境変数に入れてから 再デプロイ しないと反映されません。アフィリエイトプログラムは申し込んで審査待ちなので、承認されたら設定する予定です。
AdSense の広告枠の ID も未設定だったので、審査に通って広告ユニットを作ってから設定します。
4. 記事の下書きを毎朝自動で作る
独自の記事を増やしたいのですが、毎日自分で書くのは大変です。そこで、Claude Code の定期実行(スケジュールタスク)で、毎朝7時に記事の下書きを1本作るようにしました。
決めたルールは次のとおりです。
- 既存の記事やこれまでの下書きと テーマが重ならない ようにする
- 事実は Web で根拠を確認できたものだけ書き、出典を必ず付ける
- よいテーマがない日は 無理に書かない
-
公開はしない。下書きは
drafts/に Markdown で置き、自分が読んで OK したものだけサイトに入れる
AI が確認なしで記事を大量に公開すると、Google の「大量生成コンテンツの不正使用」に当たるおそれがあります。サイト全体の評価や AdSense の審査にも響きかねないので、最後は人が読む形にしました。自分の体験談を後から足せるように、目印も入れてもらっています。
おわりに
やってみて分かったことです。
- 中身が「他サイトの要約」だけだと、検索でも広告の審査でも弱い。独自の記事は少しずつでも足していく
- 静的 HTML を出す仕組みを最初に作っておくと、記事ページの追加は「データを足すだけ」で済む
- デプロイ後は、ブラウザで見るだけでなく curl で ステータスコードと転送先 を確認すると、思わぬ漏れに気づける
検索で上位に出るかどうかはまだ分かりません。これから Search Console の数字を見ながら改善していきます。