İ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.
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.
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:
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.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).
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ı.
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.
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).
/: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#show → Category.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.
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.
/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.
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.
Claude'a tek bir Rails kod tabanından iki marka sitesi çalıştırtmak — tam kontrol listesi:
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).as adlarıyla her iki rota bloğuna bağlanır./pricing gibi belirli sayfaları /:slug catch-all'dan önce açıkça bildir, yoksa yutulurlar.smart_article_path / on_dev_domain? helper'ları view'ları host-agnostik tutar. Template'lerin içinde if request.host.include?(...) yok./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ı.