免費

讓 Claude 給一套 Rails 跑兩個品牌站(how2claude.com + how2claude.dev)

兩個品牌站共用一套 Rails 程式——routes.rb host constraints + dev namespace + 智慧 helper + 三個真實踩坑(catch-all 吞路由、導航走樣、漏 sign-out)。


how2claude 有兩個站:

  • how2claude.com — 通用站,涵蓋 Claude 用法全部類別(getting-started / prompting / use-cases / tools / comparisons / claude-code)
  • how2claude.dev — 開發者專屬,只展示 claude-code 分類,獨立品牌,扁平 URL,深色終端主題

內容的 95% 是共享的(同一套文章資料),20% 的展示需要分叉(品牌、導航、URL 結構、主題)。開兩個 Rails 倉庫顯然不值——文章系統、支付、帳戶、OAuth、i18n 那套都要維護兩份。但一套程式跑兩個域名有一堆邊界要劃:路由怎麼分、controller 要不要複用、view 怎麼感知自己在哪個域名上跑、layout 分叉在哪。

讓 Claude 把這事做完了。本文是分叉邊界的完整實錄——5 天前的 feature 工作,現在兩個站都在生產跑。


核心問題:共享什麼、分叉什麼

先把共享和分叉列出來:

共享(一份程式,兩個域名都用):
- 文章資料模型、Category、Series
- OAuth 登入、Stripe 訂閱、x402 支付
- 帳戶頁 /accounts(同樣的功能,同樣的 UI)
- 定價頁 /pricing(同樣的資料,同樣的 Stimulus 控件)
- i18n locale 機制、19 種語言檔案

分叉(兩個域名不一樣):
- 首頁:.com 多分類瀏覽 vs .dev 開發者專屬設計($ claude --master hero)
- URL 結構:.com /:category/:slug vs .dev /:slug(扁平)
- 可見內容:.dev 只顯示 claude-code 分類的文章
- 主題:.com 白底 orange 點綴 vs .dev 深色 monospace emerald
- 導航:.com 有分類連結 vs .dev 簡化只有 Pricing + Sign in

核心原則:資料層不動,controller 層按需分叉,view / helper 層做宿主感知。

架構:routes.rb 的 host constraint 塊

所有分叉的入口是 config/routes.rb

# how2claude.dev — developer site (Claude Code only, flat URLs)
constraints(host: /\A(how2claude\.dev|how2claude\.test)(:\d+)?\z/) do
  scope "(:locale)", locale: /zh|zh-TW|ja|ko|.../ do
    root "dev/home#index", as: :dev_root
    get "/pricing", to: "pages#pricing", as: :dev_pricing
    get "/:slug", to: "dev/articles#show", as: :dev_article
  end
end

# Public routes — default locale (en) has no prefix, others use /:locale
scope "(:locale)", locale: /zh|zh-TW|ja|ko|.../ do
  root "pages#home"
  get "/pricing", to: "pages#pricing", as: :pricing
  get "/:category_slug",       to: "categories#show", as: :category
  get "/:category_slug/:slug", to: "articles#show",   as: :article
end

兩點注意:

  1. host 正則要精確——how2claude\.dev 前綴要錨定 \A,埠要用 (:\d+)? 可選匹配。how2claude.test 是本地開發域名(透過 dnsmasq 或 /etc/hosts 指向 127.0.0.1),規則裡加進去 dev 的路由本地也能生效。
  2. locale scope 嵌套在 host constraint 裡——兩套路由都走 scope "(:locale)",URL 前綴 /zh/ja 這套機制兩個站共用,不重複寫。

同一個 PagesController#pricing 在兩個塊裡都掛了,同一個控制器方法透過不同的 route helper 被呼叫.compricing_path.devdev_pricing_path。這個 alias 技巧避免了「helper 名稱衝突」(兩個域名的 /pricing 同名 helper 會被 Rails 後定義覆蓋前定義)。

Dev 命名空間 controller:只在需要分叉時建

一上來讓 Claude 做這件事,它想給每個頁面都建 Dev::XxxController。不對——大多數 controller 兩個域名共用。只有行為不同時才需要 namespace。

實際只建了兩個:

# app/controllers/dev/home_controller.rb
class Dev::HomeController < ApplicationController
  allow_unauthenticated_access
  layout "dev"

  def index
    @category = Category.find_by!(slug: "claude-code")
    @series = @category.series.order(:position)
    @standalone_articles = @category.articles
                                    .where(series: nil)
                                    .published
                                    .order(:position)

    first_free = @category.articles.published.where(free: true).order(:position).first
    @cta_article = first_free || @category.articles.published.order(:position).first
  end
end
# app/controllers/dev/articles_controller.rb
class Dev::ArticlesController < ApplicationController
  include ContentGate
  allow_unauthenticated_access
  layout "dev"

  def show
    @category = Category.find_by!(slug: "claude-code")
    @article = @category.articles.published.find_by!(slug: params[:slug])
    @series = @article.series
    @series_articles = @series.articles.published.order(:position) if @series
    gate_content!(@article)
  end
end

Dev articles 固定 Category.find_by!(slug: "claude-code")——這是 .dev 域名的資料層約束。/:slug 直接按文章 slug 找,不像 .com 需要 /:category_slug/:slug

ContentGate 是付費門的 concern,兩個站複用——付費規則一致,只是入口路徑不同。

智慧 helper:讓 view 不關心宿主

view 模板不能有 if request.host.include?("how2claude.dev") 這種判斷——醜、不 DRY。抽 helper:

# app/helpers/application_helper.rb
def on_dev_domain?
  request.host.include?("how2claude.dev")
end

def smart_article_path(article, category)
  if on_dev_domain?
    dev_article_path(slug: article.slug)
  else
    article_path(category_slug: category.slug, slug: article.slug)
  end
end

def smart_category_path(category)
  if on_dev_domain?
    dev_root_path
  else
    category_path(category_slug: category.slug)
  end
end

然後所有共享 view(articles/_row.html.erbarticles/show.html.erb、麵包屑、側邊欄)都用 smart_article_path(article, category) 代替 article_path(...)

smart_category_path 在 .dev 域名下返回 dev_root_path——因為 .dev 沒有單獨的分類頁(整個站就一個 category),分類連結應該指向首頁。

這套 helper 讓一個 view 模板在兩個域名下表現正確,不需要分叉 view

Layout 分叉:主題 + 導航,但要自律

app/views/layouts/dev.html.erbapplication.html.erb 是分叉的——深色 vs 淺色,monospace 字型 vs sans,emerald 配色 vs orange。但所有功能連結都和主 layout 對齊

<!-- layouts/dev.html.erb -->
<nav class="...">
  <%= link_to "how2claude.dev", dev_root_path,
        class: "font-mono font-bold text-emerald-400..." %>

  <div class="flex items-center gap-4 text-sm">
    <%= link_to t("pricing.page_title"), dev_pricing_path, class: "..." %>

    <% if authenticated? %>
      <%= link_to t("nav.account"), accounts_path, class: "..." %>
    <% else %>
      <%= link_to t("nav.sign_in"), new_session_path, class: "..." %>
    <% end %>

    <!-- locale dropdown -->
  </div>
</nav>

視覺是分叉的,但 Pricing / Sign in / Account / locale 切換這些功能性連結一個不少。這裡踩過坑(下文)。

踩坑 #1:/:slug catch-all 吞了 /pricing(commit 23163cc

dev 路由最初是這樣:

constraints(host: /how2claude\.dev/) do
  scope "(:locale)" do
    root "dev/home#index", as: :dev_root
    get "/:slug", to: "dev/articles#show", as: :dev_article
  end
end

存取 how2claude.dev/pricing → 匹配到 /:slugDev::ArticlesController#showCategory.find_by!(slug: "claude-code").articles.find_by!(slug: "pricing")ActiveRecord::RecordNotFound → 404。

修復:/:slug 之前顯式寫 /pricing

 constraints(host: /how2claude\.dev/) do
   scope "(:locale)" do
     root "dev/home#index", as: :dev_root
+    get "/pricing", to: "pages#pricing", as: :dev_pricing
     get "/:slug", to: "dev/articles#show", as: :dev_article
   end
 end

Rails 路由按宣告順序匹配/pricing/:slug 前面才能搶先命中。

還有一個 bonus 坑:alias 必須叫 dev_pricing,不能叫 pricing——因為 .com 塊裡已經有 as: :pricing 了。Rails 不會報錯,只會後面定義的覆蓋前面的,造成 .compricing_path 生成出 .dev 版本的 URL。讓 Claude 寫這段的時候它第一次寫了 as: :pricing,我切回 .com 點 /pricing 才發現哪裡不對——pricing_path 生成的 URL 是 /pricing 沒錯,但指向的 controller 邏輯變了(沒看出區別是因為是同一個 PagesController#pricing)。

規律:同一個 action 掛到多個路由塊裡,each 塊要用唯一的 as 名

踩坑 #2:dev 導航漏了 Pricing + Sign in(commit eac4b2f

我讓 Claude 做 dev.html.erb 時它寫了簡化版——只有品牌 logo + locale 切換器。理由:「dev 站是內容站,導航要乾淨」。

上線後我在 .dev 上註冊新帳戶——發現沒有 Sign in 連結。只能手敲 /session/new。然後付費使用者進來——沒 Pricing 連結。只能手敲 /pricing

這不是設計簡潔,這是漏功能。commit eac4b2f 把 .com 的核心導航抄過來,style 調成深色主題。

規律:視覺可以分叉(深色/淺色、monospace/sans),但功能必須對齊。一個站有的核心入口(Pricing、Sign in、Account、locale),另一個也要有,除非有明確的產品理由不讓。

讓 Claude 做分叉 layout 時,要主動說「保留所有功能入口,只改視覺」。它預設會「簡化」,但「簡化」在使用者端就是功能缺失。

踩坑 #3:/accounts 沒有 Sign out 按鈕(commit 92b34a8

這個跟多域名沒直接關係,但是在同期發現的。

Rails 8 的 session 模板生成了 DELETE /session 路由但沒在任何地方暴露出按鈕。使用者登出的唯一方式是:

curl -X DELETE https://how2claude.com/session -H "Cookie: ..."

顯然不行。commit 92b34a8/accounts 底部加了一個 muted 的 "Sign out" button_to,帶 turbo_confirm 防誤觸。

規律:做 feature 完整性 audit 時不要只看正向 path。註冊、登入、付費、使用——這些 Claude 會蓋全。反向 path(登出、取消訂閱、刪除帳戶)容易漏。讓 Claude 列一遍「使用者的每個狀態轉換」比讓它「做帳戶頁」得到的覆蓋更全。

本地怎麼開發

正式域名是 how2claude.comhow2claude.dev,本地需要模擬兩個域名。/etc/hosts

127.0.0.1 how2claude.test
127.0.0.1 dev.how2claude.test

然後路由 constraint 加上 .test 作為 dev 域名的本地別名:

constraints(host: /\A(how2claude\.dev|how2claude\.test)(:\d+)?\z/) do

(上面例子裡 how2claude.test 其實是作為 .dev 本地別名。.com 本地走預設的 localhost127.0.0.1——沒有 host 約束所以會落到預設路由塊。)

實際開發:bin/rails s -b 127.0.0.1 -p 3000 跑起來,瀏覽器裡用 how2claude.test:3000 測 .dev 路由,用 localhost:3000 測 .com 路由。兩個域名同一個 Rails 程序,改程式兩邊都 reload。

清單

讓 Claude 給一套 Rails 跑兩個品牌站的完整清單:

  1. 先把共享 vs 分叉列清楚——哪些模組兩邊一樣、哪些不一樣。不要上來就開 namespace。
  2. routes.rbconstraints(host:) 包 dev 塊,放在預設塊之前(Rails 按宣告順序匹配)。
  3. dev namespace controller 只在行為分叉時建。能複用的 controller(PagesController、AccountsController)就掛到兩個路由塊裡,用唯一 as 名避撞。
  4. 具體頁面 /pricing 這種要在 /:slug catch-all 前顯式宣告,否則被吞。
  5. smart_article_path / on_dev_domain? helper 讓 view 不感知宿主。不要在 view 裡寫 if request.host.include?(...)
  6. layout 分叉視覺,但功能入口必須對齊。告訴 Claude「保留所有功能,只改視覺」——它預設會簡化導航,那是功能缺失。
  7. 反向 path 單獨 audit。登出、取消、刪除這些容易漏,讓 Claude 列「使用者的每個狀態轉換」比列「每個頁面」更全。
  8. 本地用 /etc/hosts + .test 域名 模擬 dev 域名。路由 constraint 加 .test 作為 dev 別名。

一套 Rails 程式跑兩個品牌站的技術難度不高——Rails 的 constraints(host:) + helper 抽象就夠了。真正需要你盯的是分叉的紀律:哪些共享、哪些分叉、何時分叉、何時合併。Claude 可以把程式寫對,但它不會替你想「這兩個站到底要不要長得一樣」。那是你的產品判斷。