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

DTMセール情報まとめサイト(React + Vite)に、ガイド記事・構造化データ・独自ドメイン移行を入れた話

1
Posted at

はじめに

DTMプラグインやソフト音源のセール情報を、海外のDTM情報サイトから集めて日本語でまとめて表示する DTM PICKS を個人で作っています。

前回までに、RSSの横断取得・検索・自動翻訳、ページごとの title / description / OGP / canonical、カテゴリ別ページ、sitemap.xml の自動生成までは作っていました。

ただ、中身の大半は「他サイトの情報の要約と機械翻訳」です。検索エンジンから見ると独自の価値が薄く、広告(AdSense)の審査でも不利になりやすい状態でした。そこで Claude Code と一緒に、次のことを進めました。

  1. 検索から人が来る入口になる ガイド記事 を追加(Article の構造化データ付き)
  2. vercel.app から 独自ドメイン へ移行(転送・ads.txt・Search Console の登録し直し)
  3. アフィリエイトと AdSense の状態を点検
  4. 記事の下書きを毎朝自動で作り、人が読んでから公開 する流れを作った

この記事は、その記録です。つまずいた点も書いておきます。

構成

役割 使っているもの
フロントエンド 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 の数字を見ながら改善していきます。

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