Free

Claude'a tek bir Rails kod tabanından iki marka sitesi çalıştırtmak (how2claude.com + how2claude.dev)

İki marka, tek Rails uygulaması — routes.rb'de host constraints + dev namespace + akıllı helper'lar + üç gerçek tuzak (catch-all slug, nav kayması, sign-out eksik).


how2claude'un iki sitesi var:

  • how2claude.com — genel site, Claude kullanımının tüm kategorilerini kapsar (getting-started / prompting / use-cases / tools / comparisons / claude-code)
  • how2claude.dev — geliştirici odaklı, sadece claude-code kategorisini yüzeyler, ayrı marka, düz URL'ler, koyu terminal teması

İçeriğin %95'i paylaşılır (aynı makale verisi), sunumun %20'si dallanmak zorunda (marka, nav, URL yapısı, tema). İki Rails deposu açıkça değmez — makale sistemi, ödemeler, hesaplar, OAuth, i18n, hepsi bakımda iki katına çıkar. Ama tek kod tabanı, iki domain, çizilecek bir yığın sınıra sahip: rotalar nasıl bölünür, controller'lar yeniden kullanılır mı, view hangi domain'de çalıştığını nasıl bilir, layout nerede dallanır.

Claude'a bunu inşa ettirdim. Aşağıda dallanma çizgilerinin nerede durduğunun tam kaydı var — 5 gün önceki feature çalışması, her iki site de şimdi prodüksiyonda.


Ana soru: ne paylaşılır, ne dallanır

Paylaşılan vs. dallanan'ı baştan haritala:

Paylaşılan (tek kod, her iki domain de kullanır):
- Makale modeli, Category, Series
- OAuth girişi, Stripe aboneliği, x402 ödemeler
- Hesap sayfası /accounts (aynı özellik, aynı UI)
- Fiyatlandırma sayfası /pricing (aynı veri, aynı Stimulus kontroller)
- i18n locale mekanizması, 19 dil dosyası

Dallanan (domain'ler arasında farklı):
- Ana sayfa: .com çoklu-kategori gezinme vs. .dev dev-özel tasarım (hero $ claude --master)
- URL yapısı: .com /:category/:slug vs. .dev /:slug (düz)
- Görünür içerik: .dev sadece claude-code kategorisi makalelerini gösterir
- Tema: .com beyaz arka plan + orange aksan vs. .dev koyu monospace emerald
- Navigasyon: .com kategori linkleri var vs. .dev Pricing + Sign in'e sadeleştirilmiş

Ana prensip: veri katmanına dokunma, gerektiğinde controller'ları dalla, view / helper'ları host-duyarlı yap.

Mimari: routes.rb'deki host-constraint bloğu

Tüm forking config/routes.rb'den akar:

# 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

İki not:

  1. Host regex'i kesin olmalıhow2claude\.dev önekini \A ile sabitle, opsiyonel portları (:\d+)? ile eşle. how2claude.test yerel geliştirme domain'i (dnsmasq veya 127.0.0.1'e yönlendirilen /etc/hosts ile); pattern'e eklemek dev rotalarını yerel olarak da çalıştırır.
  2. Locale scope host constraint içinde iç içe — her iki rota bloğu da scope "(:locale)" ile sarılır, /zh, /ja gibi URL önekleri her iki sitede de yeniden yazmadan çalışır.

Aynı PagesController#pricing her iki bloğa da bağlanır — bir controller metodu, farklı route helper'lar: .com pricing_path kullanır, .dev dev_pricing_path kullanır. Bu alias hilesi "helper ismi çakışmasını" önler (iki domain'in /pricing helper'ını pricing olarak adlandırması sonra gelen tanımın önceki tanımı ezmesine yol açar).

Dev namespace controller'ları: sadece dallanma gerektiğinde

Claude'a bunu söylersen ilk içgüdüsü her sayfa için Dev::XxxController yapmaktır. Yanlış — çoğu controller her iki domain arasında paylaşılır. Sadece davranış gerçekten ayrıldığında namespace kullan.

Pratikte sadece iki tane:

# 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") olarak kilitli — bu .dev domain'i için veri katmanı kısıtı. /:slug makale slug'ı ile doğrudan arar; .com'un aksine /:category_slug/:slug gerekmez.

ContentGate paywall concern'i, her iki site arasında paylaşılır — ödeme kuralları aynı, sadece giriş yolları farklı.

Akıllı helper'lar: view'ları host'u bilmekten uzak tut

View şablonları if request.host.include?("how2claude.dev") içermemeli — çirkin, DRY değil. Helper'lara çıkar:

# 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

Sonra her paylaşılan view (articles/_row.html.erb, articles/show.html.erb, breadcrumb'lar, sidebar) article_path(...) yerine smart_article_path(article, category) kullanır.

smart_category_path .dev'de dev_root_path döndürür — çünkü .dev'in ayrı bir kategori sayfası yok (tüm site tek kategori), kategori linkleri home'a işaret etmeli.

Bu helper'lar tek bir view şablonunun her iki domain'de doğru davranmasını sağlar, view'ı dallamadan.

Layout dallanması: tema + nav, ama disiplinli kal

app/views/layouts/dev.html.erb ve application.html.erb dallanmıştır — koyu vs. açık, monospace vs. sans, emerald vs. orange. Ama her işlevsel link ana layout ile hizalı kalır:

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

Görsel dallanır, ama Pricing / Sign in / Account / locale switcher — bu işlevsel linklerin hiçbiri kaybolmaz. Bu bana çarptı (aşağıda).

Tuzak #1: /:slug catch-all'u /pricing'i yuttu (commit 23163cc)

Dev rotaları başlangıçta şöyle görünüyordu:

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 ziyareti → /:slug ile eşleşir → Dev::ArticlesController#showCategory.find_by!(slug: "claude-code").articles.find_by!(slug: "pricing")ActiveRecord::RecordNotFound → 404.

Fix: /pricing'i /:slug'dan önce açıkça bildir:

 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 rotaları bildirim sırasına göre eşleşir/pricing eşleşmeyi önce kapmak için /:slug'dan önce gelmeli.

Bonus tuzak: alias dev_pricing olmalı, pricing değil — çünkü .com bloğunda zaten as: :pricing var. Rails hata vermez; sonraki tanım önceki tanımı sessizce ezer, yani .com'da pricing_path .dev bloğuna bağlı URL üretmeye başlar. Claude bu snippet'i ilk yazdığında as: :pricing kullandı; sadece .com'da /pricing'e bastığımda ve bir şeyin ters olduğunu fark ettiğimde yakaladım — URL string /pricing doğru görünüyordu, ama altta yatan route hedefi değişmişti (ve her iki rota da PagesController#pricing'e vurduğundan, bug sessizdi).

Kural: aynı action birden çok rota bloğuna bağlandığında, her bloğun benzersiz bir as adına ihtiyacı var.

Tuzak #2: dev nav Pricing + Sign in'i kaçırdı (commit eac4b2f)

Claude'a dev.html.erb'yi ilk inşa ettirdiğimde sadeleştirilmiş bir versiyon yazdı — marka logosu + locale switcher sadece. Gerekçe: "dev site içerik sitesi, nav temiz olmalı."

Gönderdim. .dev'de yeni hesap kaydettim — Sign in linki yok. /session/new'u elle yazmak zorunda kaldım. Sonra ücretli kullanıcılar geldi — Pricing linki yok. /pricing'i elle yazmak zorunda.

Bu temiz tasarım değil — eksik özellikler. eac4b2f .com nav çekirdeğini kopyaladı, koyu tema için yeniden stillendirdi.

Kural: görseller dallanabilir (koyu/açık, monospace/sans) ama işlevsellik hizalı kalmalı. Bir sitenin sahip olduğu her çekirdek giriş noktası (Pricing, Sign in, Account, locale) diğerinin de ihtiyacı, açık bir ürün nedeni yoksa.

Claude'dan dallanmış bir layout istediğinde, açıkça söyle: "tüm işlevsel girişleri koru, sadece görseli değiştir." Varsayılanı "sadeleştirmek," ve kullanıcı tarafında "sadeleştirmek" "özellik eksik" olarak okunur.

Tuzak #3: /accounts Sign-out butonu yoktu (commit 92b34a8)

Doğrudan çoklu domain'le bağlı değil, ama aynı dönemde yüzeye çıktı.

Rails 8'in session template'i DELETE /session rotası üretir ama butonu hiçbir yerde göstermez. Kullanıcıların logout için tek yolu:

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

Açıkça kabul edilemez. 92b34a8 /accounts'un altına kaza tıklamalarını önlemek için turbo_confirm ile muted bir "Sign out" button_to ekledi.

Kural: özellik eksiksizliğini denetlerken, sadece ileri yolu izleme. Kayıt, giriş, ödeme, kullanma — bunları Claude kapsamlı şekilde örter. Ters yollar (çıkış, abonelik iptal, hesap silme) kaçırılır. Claude'a "kullanıcının her durum geçişi"ni saydırmak "hesap sayfasını yap" demenin verdiğinden daha geniş kapsam verir.

Yerel geliştirme

Prodüksiyon host'ları how2claude.com ve how2claude.dev; yerel olarak her ikisini de simüle etmemiz gerek. /etc/hosts:

127.0.0.1 how2claude.test
127.0.0.1 dev.how2claude.test

Sonra rota constraint'i .test'i .dev'in yerel alias'ı olarak ekler:

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

(Yukarıdakinde, how2claude.test etkin olarak .dev için yerel alias. Yerel .com trafiği varsayılan localhost veya 127.0.0.1'e vurur — hiçbir host constraint eşleşmez, varsayılan rota bloğuna düşer.)

Pratikte: bin/rails s -b 127.0.0.1 -p 3000 çalışırken, tarayıcıda .dev rotalarını test etmek için how2claude.test:3000'e, .com için localhost:3000'e vur. İki domain, bir Rails süreci, kod değiştir ve ikisi de yeniden yüklenir.

Kontrol listesi

Claude'a tek bir Rails kod tabanından iki marka sitesi çalıştırtmak — tam kontrol listesi:

  1. Paylaşılan vs. dallanan'ı önce haritala — hangi modüller eşleşir, hangileri eşleşmez. Baştan namespace açma.
  2. routes.rb'de dev bloğunu constraints(host:) ile sar, varsayılan bloğun öncesine yerleştir (Rails bildirim sırasına göre eşleşir).
  3. Dev namespace controller'ları sadece davranış ayrıldığında. Paylaşılabilir controller'lar (PagesController, AccountsController) çakışmayı önlemek için benzersiz as adlarıyla her iki rota bloğuna bağlanır.
  4. /pricing gibi belirli sayfaları /:slug catch-all'dan önce açıkça bildir, yoksa yutulurlar.
  5. smart_article_path / on_dev_domain? helper'ları view'ları host-agnostik tutar. Template'lerin içinde if request.host.include?(...) yok.
  6. Layout'ta görselleri dalla ama işlevsel girişleri hizalı tut. Claude'a "tüm işlevselliği koru, sadece görseli değiştir" de — varsayılanı sadeleştirmek, ve bu özellik kaybı olarak okunur.
  7. Ters yolları ayrıca denetle. Sign out, iptal, silme kaçırılır; Claude'a "kullanıcının her durum geçişi"ni saydırmak "her sayfayı yap" demekten daha fazla kapsar.
  8. Yerelde /etc/hosts + .test domain'i kullan dev domain'ini simüle etmek için. Rota constraint'ine .test'i dev alias'ı olarak ekle.

Tek bir Rails kod tabanından iki marka sitesi çalıştırmanın teknik zorluğu yüksek değil — constraints(host:) artı helper soyutlamaları seni oraya götürür. Gerçekten dikkatini gerektiren şey dallanmanın disiplini: ne paylaşılır, ne dallanır, ne zaman dallanılır, ne zaman birleşilir. Claude kodu doğru yazabilir, ama iki sitenin aynı görünüp görünmemesi gerektiğine senin adına karar vermez. O ürün yargısı.