<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
<channel>
<title><![CDATA[ RailsGrowth ]]></title>
<description><![CDATA[ Growth &amp; Design for Ruby on Rails ]]></description>
<link>https://railsgrowth.com</link>
<image>
    <url>https://railsgrowth.com/favicon.png</url>
    <title>RailsGrowth</title>
    <link>https://railsgrowth.com</link>
</image>
<lastBuildDate>Fri, 01 May 2026 09:14:05 +0000</lastBuildDate>
<atom:link href="https://railsgrowth.com" rel="self" type="application/rss+xml"/>
<ttl>60</ttl>

    <item>
        <title><![CDATA[ Growth Engineering Glossary ]]></title>
        <description><![CDATA[
            <p>I'm in the process of preparing for a new engineering role on a Growth team. I've worked heavily in digital marketing and growth projects, and I've worked in Rails engineering roles, but this is my first engineering role 100% focused on growth team.</p><p>So, to help me acclimate to the role, I decided to analyze a few hundred top ranking webpages on <code>growth engineering</code> topics and extract out the most commonly used phrases into a Glossary format.</p><p>Below is a curated version of the output and terms I found helpful, and a helpful outline/mental structure for thinking about grouping these topics.</p><h2 id="table-of-contents">Table of Contents</h2><ul><li><a>Methodology and Process Terms</a></li><li><a>Role Titles and Team Structures</a></li><li><a>A/B Testing and Experimentation Vocabulary</a></li><li><a>Feature Flags and Rollout Terminology</a></li><li><a>Growth Loops and Growth Model Terminology</a></li><li><a>Network Effects Terminology</a></li><li><a>Viral Mechanics and Referral Terminology</a></li><li><a>PLG (Product-Led Growth) Terminology</a></li><li><a>Activation, Retention, and Engagement Metrics</a></li><li><a>Churn Terminology</a></li><li><a>Funnel Optimization Language</a></li><li><a>AARRR Pirate Metrics Framework</a></li><li><a>Onboarding and User Journey Phrases</a></li><li><a>Technical Growth Infrastructure Terms</a></li><li><a>Attribution and Analytics Terminology</a></li><li><a>Mobile Attribution Terms</a></li><li><a>Identity and User Identification Terms</a></li><li><a>Monetization and Conversion Optimization Language</a></li><li><a>Pricing Optimization Terms</a></li><li><a>North Star Metrics and Goal-Setting Vocabulary</a></li><li><a>SaaS and Revenue Metrics</a></li><li><a>Key benchmarks reference</a></li><li><a>Sources consulted</a></li><li><a>Methodology</a></li></ul><hr><h2 id="methodology-and-process-terms">Methodology and Process Terms</h2><p><strong>Growth Engineering</strong>&nbsp;— The systematic, technical approach to growth using data-driven experiments rather than gut-driven decisions. Often described as "writing code to make a company money."&nbsp;<a href="https://www.ehfeng.com/what-is-growth-engineering/?ref=railsgrowth.com">[Eric Feng]</a>&nbsp;<a href="https://newsletter.pragmaticengineer.com/p/what-is-growth-engineering?ref=railsgrowth.com">[Pragmatic Engineer]</a></p><p><strong>Build to Learn</strong>&nbsp;— Growth engineering philosophy where code is shipped primarily to learn and validate hypotheses, not to build lasting features. Contrasts with product engineering's "build to last" approach.&nbsp;<a href="https://alexeymk.com/2025/03/25/why-growth-engineering-practices-dont-transfer-to-product-engineering?ref=railsgrowth.com">[Alexey MK]</a></p><p><strong>Kill Your Darlings</strong>&nbsp;— Growth engineering mindset of willingness to throw away failed experiments without attachment, essential for rapid iteration.&nbsp;<a href="https://alexeymk.com/2025/03/25/why-growth-engineering-practices-dont-transfer-to-product-engineering?ref=railsgrowth.com">[Alexey MK]</a></p><p><strong>Hypothesis-Driven Development</strong>&nbsp;— Approach where features and experiments are designed around testable hypotheses with clear success criteria.&nbsp;<a href="https://www.atlassian.com/blog/atlassian-engineering/what-does-a-product-growth-engineer-work-on?ref=railsgrowth.com">[Atlassian]</a></p><p><strong>Experiment Roadmap</strong>&nbsp;— Prioritized backlog of growth experiments separate from the product roadmap.&nbsp;<a href="https://www.productboard.com/blog/the-role-of-growth-engineering-at-productboard-key-skills-responsibilities-methodologies/?ref=railsgrowth.com">[Productboard]</a></p><p><strong>RICE Framework</strong>&nbsp;— Prioritization framework: Reach × Impact × Confidence ÷ Effort. Used to prioritize growth experiments.&nbsp;<a href="https://productschool.com/blog/leadership/growth-team?ref=railsgrowth.com">[Product School]</a></p><p><strong>ICE Framework</strong>&nbsp;— Simplified prioritization: Impact × Confidence × Ease, scored 1-10 for each factor.&nbsp;<a href="https://cxl.com/blog/growth-team-structure/?ref=railsgrowth.com">[CXL]</a></p><p><strong>Fake Door Test</strong>&nbsp;— Offering a not-yet-available feature to gauge customer interest before investing in full development.&nbsp;<a href="https://www.prodpad.com/glossary/ab-testing/?ref=railsgrowth.com">[ProdPad]</a></p><p><strong>Tent vs Skyscraper</strong>&nbsp;— Metaphor for growth vs product engineering approaches. Tents (growth) optimize for speed of setup/teardown; skyscrapers (product) optimize for durability and permanence.&nbsp;<a href="https://alexeymk.com/2025/03/25/why-growth-engineering-practices-dont-transfer-to-product-engineering?ref=railsgrowth.com">[Alexey MK]</a></p><p><strong>Growth Engineering vs Product Engineering</strong>&nbsp;— Product engineers build core product features for longevity; growth engineers optimize business metrics through rapid experimentation.&nbsp;<a href="https://newsletter.pragmaticengineer.com/p/what-is-growth-engineering?ref=railsgrowth.com">[Pragmatic Engineer]</a></p><p><strong>Growth Engineering vs Marketing Engineering</strong>&nbsp;— Marketing engineers serve internal teams and focus on productivity tools. Growth engineers focus on business metrics and customer-facing optimization.&nbsp;<a href="https://newsletter.pragmaticengineer.com/p/what-is-growth-engineering?ref=railsgrowth.com">[Pragmatic Engineer]</a></p><h2 id="role-titles-and-team-structures">Role Titles and Team Structures</h2><p><strong>Growth Engineer</strong>&nbsp;— A software engineer who combines technical expertise with data-driven strategies to drive user acquisition, engagement, and revenue growth. Unlike product engineers who build features for longevity, growth engineers optimize business metrics through rapid experimentation. Origins trace to Facebook in 2007 under Chamath Palihapitiya.&nbsp;<a href="https://newsletter.pragmaticengineer.com/p/what-is-growth-engineering?ref=railsgrowth.com">[Pragmatic Engineer]</a>&nbsp;<a href="https://userguiding.com/blog/growth-engineering?ref=railsgrowth.com">[UserGuiding]</a></p><p><strong>Growth PM (Growth Product Manager)</strong>&nbsp;— A product manager responsible for improving specific business metrics rather than owning a specific product area. Focuses on short-term experiments across the entire user funnel—from acquisition through monetization.&nbsp;<a href="https://www.productled.org/foundations/the-rise-of-the-growth-product-manager?ref=railsgrowth.com">[ProductLed]</a>&nbsp;<a href="https://userpilot.com/blog/product-growth-manager/?ref=railsgrowth.com">[Userpilot]</a></p><p><strong>Growth Hacker</strong>&nbsp;— Marketing-oriented role focused on creative, low-cost strategies for customer acquisition. Coined by Sean Ellis in 2010. Relies on non-technical solutions and shortcuts, unlike growth engineers who use systematic, technical approaches.&nbsp;<a href="https://medium.com/teamtimelapse/growth-engineering-when-growth-hackers-grow-up-da332aa7471?ref=railsgrowth.com">[Medium]</a></p><p><strong>Growth Designer</strong>&nbsp;— Designer within growth teams who creates user interfaces optimized for engagement and conversion. Works closely with growth engineers on A/B test variations and rapid experimentation.&nbsp;<a href="https://www.productboard.com/blog/the-role-of-growth-engineering-at-productboard-key-skills-responsibilities-methodologies/?ref=railsgrowth.com">[Productboard]</a></p><p><strong>Growth Data Analyst / Product Analyst</strong>&nbsp;— Data specialists who dive deep into user data, extract insights, decode behavior patterns, monitor KPIs, and provide actionable recommendations to inform growth strategy.&nbsp;<a href="https://www.productboard.com/blog/the-role-of-growth-engineering-at-productboard-key-skills-responsibilities-methodologies/?ref=railsgrowth.com">[Productboard]</a></p><p><strong>Technical Growth Marketer</strong>&nbsp;— Marketer who understands backend development, APIs, and script building. Bridges the gap between marketing and engineering.&nbsp;<a href="https://newsletter.pragmaticengineer.com/p/what-is-growth-engineering?ref=railsgrowth.com">[Pragmatic Engineer]</a></p><p><strong>VP of Growth / Head of Growth</strong>&nbsp;— Executive responsible for overall growth strategy, team structure, and organizational alignment.&nbsp;<a href="https://andrewchen.com/how-to-build-a-growth-team/?ref=railsgrowth.com">[Andrew Chen]</a></p><p><strong>Chief Growth Officer (CGO)</strong>&nbsp;— Executive position leading growth teams; responsible for setting strategy, goals, and tasks across all growth initiatives.&nbsp;<a href="https://mixpanel.com/blog/growth-team/?ref=railsgrowth.com">[Mixpanel]</a></p><p><strong>Growth Team</strong>&nbsp;— Cross-functional team dedicated to driving user acquisition, engagement, and retention. Typically includes engineers, PMs, designers, data analysts, and sometimes marketers.&nbsp;<a href="https://productschool.com/blog/leadership/growth-team?ref=railsgrowth.com">[Product School]</a>&nbsp;<a href="https://cxl.com/blog/growth-team-structure/?ref=railsgrowth.com">[CXL]</a></p><p><strong>Independent Growth Team</strong>&nbsp;— Standalone team with its own PM, engineers, designers, and analysts functioning as a "mini startup" within the company.&nbsp;<a href="https://andrewchen.com/how-to-build-a-growth-team/?ref=railsgrowth.com">[Andrew Chen]</a></p><p><strong>Embedded Growth Function</strong>&nbsp;— Growth specialists who sit within core product teams and own specific metrics.&nbsp;<a href="https://cxl.com/blog/growth-team-structure/?ref=railsgrowth.com">[CXL]</a></p><p><strong>Hybrid Growth Model</strong>&nbsp;— Combination of independent and embedded approaches; adapts to company needs and scales with organizational growth.&nbsp;<a href="https://productschool.com/blog/leadership/growth-team?ref=railsgrowth.com">[Product School]</a></p><p><strong>Growth Pod</strong>&nbsp;— Small, focused team carved out to work full-time on key growth initiatives.&nbsp;<a href="https://mixpanel.com/blog/growth-team/?ref=railsgrowth.com">[Mixpanel]</a></p><p><strong>Owner Model</strong>&nbsp;— Growth team structure where growth engineering operates independently with full ownership of their domain and metrics.&nbsp;<a href="https://newsletter.pragmaticengineer.com/p/what-is-growth-engineering?ref=railsgrowth.com">[Pragmatic Engineer]</a></p><p><strong>Hitchhiker Model</strong>&nbsp;— Growth engineers who work alongside other product teams, contributing growth expertise while those teams retain primary ownership.&nbsp;<a href="https://newsletter.pragmaticengineer.com/p/what-is-growth-engineering?ref=railsgrowth.com">[Pragmatic Engineer]</a></p><h2 id="ab-testing-and-experimentation-vocabulary">A/B Testing and Experimentation Vocabulary</h2><p><strong>A/B Testing (Split Testing)</strong>&nbsp;— Core growth engineering methodology comparing two versions (control vs. variant) to measure which performs better on specific metrics.&nbsp;<a href="https://www.prodpad.com/glossary/ab-testing/?ref=railsgrowth.com">[ProdPad]</a>&nbsp;<a href="https://netflixtechblog.com/its-all-a-bout-testing-the-netflix-experimentation-platform-4e1ca458c15?ref=railsgrowth.com">[Netflix Tech Blog]</a></p><p><strong>Control Group</strong>&nbsp;— The group of users that sees the original, unchanged version of the experience. Serves as the baseline for comparison.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>Treatment (Variant)</strong>&nbsp;— The new or modified version of the experience being tested against the control.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>A/B/n Test</strong>&nbsp;— A variation of A/B testing where multiple variants (n) are tested against a control simultaneously.&nbsp;<a href="https://vwo.com/glossary/multi-armed-bandit-testing/?ref=railsgrowth.com">[VWO]</a></p><p><strong>A/A Test</strong>&nbsp;— An experiment where identical versions are tested against each other to validate the testing framework and ensure there are no systemic biases.&nbsp;<a href="https://www.bigdatawire.com/2022/06/22/a-b-test-like-youre-airbnb/?ref=railsgrowth.com">[BigDATAwire]</a></p><p><strong>Multivariate Test (MVT)</strong>&nbsp;— An experiment that tests multiple variables and their combinations simultaneously to determine which combination produces the best outcome.&nbsp;<a href="https://www.braze.com/resources/articles/from-multivariant-test-to-ai-decisioning?ref=railsgrowth.com">[Braze]</a></p><p><strong>Holdout Group</strong>&nbsp;— A subset of users deliberately excluded from receiving new features to serve as a long-term baseline for measuring cumulative impact.&nbsp;<a href="https://www.slideshare.net/SteveUrban/experimentation-platform-at-netflix?ref=railsgrowth.com">[Netflix/SlideShare]</a></p><p><strong>Statistical Significance</strong>&nbsp;— An indicator that the observed difference between control and treatment groups is unlikely to have occurred by random chance alone. Typically measured at 95% confidence level (α = 0.05).&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>P-value</strong>&nbsp;— The probability of observing results as extreme or more extreme than the observed results, assuming the null hypothesis is true.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>Confidence Level</strong>&nbsp;— The level of certainty (e.g., 95%) that the true effect lies within the confidence interval.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>Confidence Interval</strong>&nbsp;— The range of values within which the true metric value is likely to fall if the experiment were repeated many times.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>Minimum Detectable Effect (MDE)</strong>&nbsp;— The smallest true effect size that an A/B test can reliably detect with specified statistical power.&nbsp;<a href="https://www.bigdatawire.com/2022/06/22/a-b-test-like-youre-airbnb/?ref=railsgrowth.com">[BigDATAwire]</a></p><p><strong>Statistical Power</strong>&nbsp;— The probability that an experiment will correctly detect a real effect as statistically significant when it exists. Convention is 80% power.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>Type I Error (False Positive)</strong>&nbsp;— Incorrectly rejecting the null hypothesis when there is actually no real effect.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>Type II Error (False Negative)</strong>&nbsp;— Failing to reject the null hypothesis when a real effect actually exists.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>Null Hypothesis</strong>&nbsp;— The assumption that there is no significant difference between the control and treatment groups.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>Sample Size</strong>&nbsp;— The number of visitors or users included in an experiment. Larger samples increase power and enable detection of smaller effects.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>Lift</strong>&nbsp;— The percentage improvement in a metric between the treatment and control groups. Calculated as (Treatment - Control) / Control × 100%.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>Primary Metric (Success Metric)</strong>&nbsp;— The central metric that the experiment aims to optimize, directly tied to business goals.&nbsp;<a href="https://vwo.com/stats-blog/three-kinds-of-metrics-the-success-the-guardrail-and-the-diagnostic/?ref=railsgrowth.com">[VWO]</a></p><p><strong>Guardrail Metric</strong>&nbsp;— Secondary metrics monitored during experiments to ensure changes don't have unintended negative consequences.&nbsp;<a href="https://www.geteppo.com/blog/what-are-guardrail-metrics-with-examples?ref=railsgrowth.com">[Eppo]</a>&nbsp;<a href="https://vwo.com/stats-blog/three-kinds-of-metrics-the-success-the-guardrail-and-the-diagnostic/?ref=railsgrowth.com">[VWO]</a></p><p><strong>Diagnostic Metric</strong>&nbsp;— Metrics providing deeper insight into how an experiment affected user behavior beyond the primary success metric.&nbsp;<a href="https://vwo.com/stats-blog/three-kinds-of-metrics-the-success-the-guardrail-and-the-diagnostic/?ref=railsgrowth.com">[VWO]</a></p><p><strong>CUPED (Controlled-experiment Using Pre-Experiment Data)</strong>&nbsp;— A variance reduction technique using pre-experiment data to reduce noise and accelerate experiment results by 20-65%.&nbsp;<a href="https://www.bigdatawire.com/2022/06/22/a-b-test-like-youre-airbnb/?ref=railsgrowth.com">[BigDATAwire]</a></p><p><strong>Sequential Testing</strong>&nbsp;— A method allowing data analysis at multiple points during an experiment while maintaining statistical validity.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>Bayesian Inference</strong>&nbsp;— A statistical approach using prior beliefs updated with observed data to calculate probability distributions of effects.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>Frequentist Statistics</strong>&nbsp;— The traditional statistical framework using fixed hypothesis testing, p-values, and confidence intervals.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>Bonferroni Correction</strong>&nbsp;— A statistical adjustment reducing false-positive rates when conducting multiple hypothesis tests.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>Multi-Armed Bandit (MAB)</strong>&nbsp;— An adaptive algorithm that dynamically allocates traffic to better-performing variants in real-time.&nbsp;<a href="https://vwo.com/glossary/multi-armed-bandit-testing/?ref=railsgrowth.com">[VWO]</a>&nbsp;<a href="https://en.wikipedia.org/wiki/Multi-armed_bandit?ref=railsgrowth.com">[Wikipedia]</a></p><p><strong>Thompson Sampling</strong>&nbsp;— A Bayesian bandit algorithm that samples from posterior distributions to determine which variant to show.&nbsp;<a href="https://www.braze.com/resources/articles/from-multivariant-test-to-ai-decisioning?ref=railsgrowth.com">[Braze]</a></p><p><strong>Contextual Bandit</strong>&nbsp;— An advanced MAB that uses context about users, variants, and environment to make personalized allocation decisions.&nbsp;<a href="https://www.braze.com/resources/articles/from-multivariant-test-to-ai-decisioning?ref=railsgrowth.com">[Braze]</a></p><p><strong>Sample Ratio Mismatch (SRM)</strong>&nbsp;— A diagnostic check detecting when actual traffic split differs significantly from intended allocation.&nbsp;<a href="https://www.bigdatawire.com/2022/06/22/a-b-test-like-youre-airbnb/?ref=railsgrowth.com">[BigDATAwire]</a></p><p><strong>Stratified Sampling</strong>&nbsp;— Random distribution with enforcement of sample proportions across segments to ensure representative allocation.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>Novelty Effect</strong>&nbsp;— A temporary change in user behavior due to the newness of a feature, which may fade over time.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>Primacy Effect</strong>&nbsp;— Users preferring original experiences due to familiarity, potentially biasing results against new variants.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>Winner's Curse</strong>&nbsp;— The phenomenon where winning variants' effects appear inflated when measured individually.&nbsp;<a href="https://www.mida.so/ab-testing-terms?ref=railsgrowth.com">[Mida]</a></p><p><strong>Experiment Velocity</strong>&nbsp;— The rate at which an organization can run experiments, often measured in experiments per week or month.&nbsp;<a href="https://newsletter.pragmaticengineer.com/p/what-is-growth-engineering?ref=railsgrowth.com">[Pragmatic Engineer]</a></p><p><strong>Overall Evaluation Criterion (OEC)</strong>&nbsp;— A composite metric combining multiple signals into a single measure for experiment decision-making.&nbsp;<a href="https://netflixtechblog.com/its-all-a-bout-testing-the-netflix-experimentation-platform-4e1ca458c15?ref=railsgrowth.com">[Netflix Tech Blog]</a></p><h2 id="feature-flags-and-rollout-terminology">Feature Flags and Rollout Terminology</h2><p><strong>Feature Flag</strong>&nbsp;— A software development technique that enables or disables features without code deployment, allowing controlled feature releases and experiments.&nbsp;<a href="https://newsletter.pragmaticengineer.com/p/what-is-growth-engineering?ref=railsgrowth.com">[Pragmatic Engineer]</a></p><p><strong>Progressive Rollout (Percentage Rollout)</strong>&nbsp;— Gradually increasing the percentage of users exposed to a feature over time (e.g., 10% → 25% → 50% → 100%).</p><p><strong>Kill Switch</strong>&nbsp;— An emergency feature flag that can instantly disable a feature if problems are detected.</p><p><strong>Targeting Rules</strong>&nbsp;— Logic determining which users see which flag variation based on attributes like user properties, device type, or location.</p><p><strong>Fallthrough (Default Rule)</strong>&nbsp;— The rule applied when no specific targeting conditions match.</p><p><strong>Release Flag (Rollout Flag)</strong>&nbsp;— A temporary boolean flag used to control incremental feature releases.</p><p><strong>Experiment Flag</strong>&nbsp;— A feature flag specifically configured for A/B testing, often with multiple variations and associated metrics.</p><p><strong>Canary Release (Canary Test)</strong>&nbsp;— Releasing a change to a small subset of users before broader rollout to detect issues early.</p><p><strong>Dark Launch</strong>&nbsp;— Deploying code to production without exposing it to users, enabling testing of infrastructure before feature activation.</p><p><strong>Bucketing</strong>&nbsp;— The process of assigning users to experiment variants, typically using consistent hashing of user identifiers.</p><p><strong>Exposure</strong>&nbsp;— The moment when a user is shown a variant and their assignment is logged for analysis.</p><h2 id="growth-loops-and-growth-model-terminology">Growth Loops and Growth Model Terminology</h2><p><strong>Growth Loop</strong>&nbsp;— A closed system where inputs go through a series of pre-defined steps to generate outputs, which are then reinvested as new inputs. Unlike linear funnels, loops compound over time. Reforge popularized this framework.&nbsp;<a href="https://ortto.com/learn/growth-loops/?ref=railsgrowth.com">[Ortto]</a></p><p><strong>Viral Loop</strong>&nbsp;— A mechanism where users refer others to a product, and those referrals become referrers through the same mechanism, creating a self-reinforcing cycle.&nbsp;<a href="https://ortto.com/learn/growth-loops/?ref=railsgrowth.com">[Ortto]</a></p><p><strong>Content Loop</strong>&nbsp;— A growth loop where content creation and distribution attracts users who then create more content. Can be user-generated (Pinterest) or company-generated (HubSpot).&nbsp;<a href="https://ortto.com/learn/growth-loops/?ref=railsgrowth.com">[Ortto]</a></p><p><strong>Paid Loop</strong>&nbsp;— A growth loop where paid advertising attracts users/customers, generating revenue that can be reinvested into more paid advertising.&nbsp;<a href="https://ortto.com/learn/growth-loops/?ref=railsgrowth.com">[Ortto]</a></p><p><strong>Sales Loop</strong>&nbsp;— A loop where a sales force acquires customers, generating revenue that funds hiring more salespeople.&nbsp;<a href="https://ortto.com/learn/growth-loops/?ref=railsgrowth.com">[Ortto]</a></p><p><strong>Acquisition Loop</strong>&nbsp;— Any loop focused specifically on bringing new users into the product.</p><p><strong>Loop Velocity</strong>&nbsp;— The speed at which a growth loop completes one full cycle. Faster velocity means faster compounding growth.</p><p><strong>Loop Efficiency</strong>&nbsp;— A measure of how much output a loop generates relative to its inputs.</p><p><strong>K-Factor (Viral Coefficient)</strong>&nbsp;— The number of new users each existing user generates through invitations or referrals. Formula: K = i (invites sent per user) × c (conversion rate of invites). A K-factor &gt;1 indicates exponential growth.&nbsp;<a href="https://www.yotpo.com/blog/referral-program-meaning/?ref=railsgrowth.com">[Yotpo]</a></p><p><strong>Viral Cycle Time</strong>&nbsp;— The time from a user joining to successfully referring a new user who joins. Shorter cycle times accelerate viral growth.&nbsp;<a href="https://www.yotpo.com/blog/referral-program-meaning/?ref=railsgrowth.com">[Yotpo]</a></p><p><strong>Compounding Growth</strong>&nbsp;— Growth that builds upon itself over time, creating exponential rather than linear growth curves.</p><p><strong>Critical Mass</strong>&nbsp;— The minimum number of users required for a network or viral effect to become self-sustaining.</p><p><strong>Growth Flywheel</strong>&nbsp;— A framework visualizing growth as a circular, self-reinforcing system rather than a linear funnel.&nbsp;<a href="https://www.chameleon.io/blog/plg-flywheel?ref=railsgrowth.com">[Chameleon]</a></p><p><strong>Flywheel Friction</strong>&nbsp;— Anything that slows down the flywheel's momentum—poor onboarding, confusing pricing, bad UX.&nbsp;<a href="https://www.chameleon.io/blog/plg-flywheel?ref=railsgrowth.com">[Chameleon]</a></p><h2 id="network-effects-terminology">Network Effects Terminology</h2><p><strong>Network Effect</strong>&nbsp;— When a product becomes more valuable as more people use it. Responsible for 70% of value created by tech companies since 1994.</p><p><strong>Direct Network Effect</strong>&nbsp;— When increased usage directly increases value to users (e.g., telephone networks—more users = more people to call).</p><p><strong>Indirect Network Effect</strong>&nbsp;— When increased usage of one side increases value for a different side (e.g., more iOS users = more apps developed).</p><p><strong>Two-Sided Network Effect</strong>&nbsp;— Networks with two distinct user groups (supply and demand) that provide complementary value to each other.</p><p><strong>Data Network Effect</strong>&nbsp;— When product value increases with more data, and usage generates more useful data.</p><p><strong>Metcalfe's Law</strong>&nbsp;— The value of a network is proportional to the square of the number of users (N²).</p><p><strong>Reed's Law</strong>&nbsp;— Network value increases exponentially (2^N) as sub-groups can form within the network.</p><p><strong>Marketplace Network Effect</strong>&nbsp;— Two-sided effect where buyers attract sellers and sellers attract buyers.</p><p><strong>Platform Network Effect</strong>&nbsp;— Supply side engineers products specifically for the platform (e.g., iOS app developers).</p><p><strong>Asymptotic Network Effect</strong>&nbsp;— When initial supply quickly adds value but additional supply yields diminishing returns.</p><p><strong>Multi-tenanting</strong>&nbsp;— When users can participate in multiple competing networks simultaneously, weakening network lock-in.</p><p><strong>Market Tipping</strong>&nbsp;— When one network gains enough advantage that it "pulls away" from competitors.</p><p><strong>Bandwagon Effect</strong>&nbsp;— Social pressure to join a network to avoid being "left out."</p><p><strong>Network Density</strong>&nbsp;— The number of connections between nodes in a network. Higher density = more valuable network.</p><h2 id="viral-mechanics-and-referral-terminology">Viral Mechanics and Referral Terminology</h2><p><strong>Referrer/Advocate</strong>&nbsp;— The existing customer who recommends a product to others and earns rewards for successful referrals.&nbsp;<a href="https://www.yotpo.com/blog/referral-program-meaning/?ref=railsgrowth.com">[Yotpo]</a></p><p><strong>Referee/Referred Friend</strong>&nbsp;— The new customer who discovers a brand based on an advocate's recommendation.&nbsp;<a href="https://www.yotpo.com/blog/referral-program-meaning/?ref=railsgrowth.com">[Yotpo]</a></p><p><strong>Double-Sided Incentive (Give X, Get Y)</strong>&nbsp;— A referral structure where both the referrer and the referred friend receive rewards.&nbsp;<a href="https://www.yotpo.com/blog/referral-program-meaning/?ref=railsgrowth.com">[Yotpo]</a></p><p><strong>Referral Link/Code</strong>&nbsp;— A unique identifier assigned to each referrer for tracking successful referrals and attributing rewards.&nbsp;<a href="https://www.yotpo.com/blog/referral-program-meaning/?ref=railsgrowth.com">[Yotpo]</a></p><p><strong>Referral Loop</strong>&nbsp;— The complete cycle: advocate makes purchase → joins program → shares link → friend converts → both get rewards → friend becomes advocate.&nbsp;<a href="https://www.yotpo.com/blog/referral-program-meaning/?ref=railsgrowth.com">[Yotpo]</a></p><p><strong>Invite Mechanics</strong>&nbsp;— The system design for how users invite others—including invite prompts, sharing channels, friction reduction, and timing.</p><p><strong>Referral Rate</strong>&nbsp;— The percentage of users who successfully refer at least one new user within a given time period.</p><p><strong>Share Rate</strong>&nbsp;— The percentage of users who share referral links, regardless of conversion outcome.</p><p><strong>Tiered Referral Program</strong>&nbsp;— A system offering increasingly valuable rewards as advocates refer more customers.&nbsp;<a href="https://www.yotpo.com/blog/referral-program-meaning/?ref=railsgrowth.com">[Yotpo]</a></p><p><strong>Inherent Virality</strong>&nbsp;— When the product's core use case naturally involves sharing (e.g., Calendly sends invites by its nature) versus engineered virality through incentives.</p><p><strong>Word-of-Mouth (WOM)</strong>&nbsp;— Organic sharing and recommendations between people.</p><p><strong>Social Proof</strong>&nbsp;— The psychological phenomenon where people look to others' actions to determine their own.</p><h2 id="plg-product-led-growth-terminology">PLG (Product-Led Growth) Terminology</h2><p><strong>Product-Led Growth (PLG)</strong>&nbsp;— A business methodology where user acquisition, expansion, conversion, and retention are all driven primarily by the product itself rather than sales or marketing. Coined by OpenView's Blake Bartlett in 2016.&nbsp;<a href="https://www.chameleon.io/blog/plg-flywheel?ref=railsgrowth.com">[Chameleon]</a></p><p><strong>Product Qualified Lead (PQL)</strong>&nbsp;— A lead who has experienced the product's value firsthand (via free trial or freemium) and demonstrated buying intent through product usage behaviors.&nbsp;<a href="https://www.chameleon.io/blog/plg-flywheel?ref=railsgrowth.com">[Chameleon]</a></p><p><strong>Product Qualified Account (PQA)</strong>&nbsp;— The account-level equivalent of a PQL for B2B, identifying high-value sales opportunities based on collective usage patterns.</p><p><strong>Time to Value (TTV)</strong>&nbsp;— The time it takes for a user to realize meaningful value from the product after first use.&nbsp;<a href="https://productled.com/blog/activation-velocity?ref=railsgrowth.com">[ProductLed]</a></p><p><strong>Time to First Value (TTFV)</strong>&nbsp;— Specifically measures the time until a user experiences the initial benefit.</p><p><strong>Self-Serve</strong>&nbsp;— A go-to-market model where users can discover, evaluate, adopt, and purchase the product without requiring sales assistance.</p><p><strong>Freemium</strong>&nbsp;— A pricing model offering free access to basic features, with paid upgrades for premium functionality.</p><p><strong>Free Trial</strong>&nbsp;— A time-limited full access model that lets users experience the complete product before committing to purchase.</p><p><strong>Reverse Trial</strong>&nbsp;— A model where users start with full premium features, then downgrade to free tier after the trial ends.</p><p><strong>PQL Rate</strong>&nbsp;— The percentage of new signups reaching PQL status.</p><p><strong>PQL to Paid Conversion Rate</strong>&nbsp;— The percentage of PQLs who convert to paying customers.</p><p><strong>Time to PQL</strong>&nbsp;— How long it takes a user to reach PQL status.</p><p><strong>Product-Led Sales (PLS)</strong>&nbsp;— A hybrid approach combining PLG's bottom-up motion with traditional top-down sales, using product usage data to identify sales opportunities.</p><p><strong>Value Metric</strong>&nbsp;— The unit of measure that aligns pricing with customer value (e.g., number of users, messages sent, storage used).</p><p><strong>Bottom-Up Adoption</strong>&nbsp;— When end users adopt a product independently, then drive organizational adoption.</p><p><strong>Land and Expand</strong>&nbsp;— Strategy of starting small within an organization (land) then growing usage and revenue within that account over time (expand).</p><h3 id="plg-flywheel-stages">PLG Flywheel Stages</h3><p><strong>Evaluator</strong>&nbsp;— First stage user exploring whether the product might solve their problem.&nbsp;<a href="https://www.chameleon.io/blog/plg-flywheel?ref=railsgrowth.com">[Chameleon]</a></p><p><strong>Beginner</strong>&nbsp;— User who has started using the product but hasn't activated.&nbsp;<a href="https://www.chameleon.io/blog/plg-flywheel?ref=railsgrowth.com">[Chameleon]</a></p><p><strong>Regular</strong>&nbsp;— Activated user who has adopted the product into their workflow.&nbsp;<a href="https://www.chameleon.io/blog/plg-flywheel?ref=railsgrowth.com">[Chameleon]</a></p><p><strong>Champion</strong>&nbsp;— Highly engaged user invested in the product's success who becomes an advocate.&nbsp;<a href="https://www.chameleon.io/blog/plg-flywheel?ref=railsgrowth.com">[Chameleon]</a></p><p><strong>Activate → Adopt → Adore → Advocate</strong>&nbsp;— The PLG Flywheel action sequence connecting user stages.&nbsp;<a href="https://www.chameleon.io/blog/plg-flywheel?ref=railsgrowth.com">[Chameleon]</a></p><h2 id="activation-retention-and-engagement-metrics">Activation, Retention, and Engagement Metrics</h2><p><strong>Aha Moment</strong>&nbsp;— The pivotal point when a user suddenly realizes and experiences the core value of a product. Examples: Facebook's 7 friends in 10 days, Twitter's following 30 people, Slack's 2,000 messages exchanged.&nbsp;<a href="https://amplitude.com/blog/product-north-star-metric?ref=railsgrowth.com">[Amplitude]</a></p><p><strong>Activation</strong>&nbsp;— The process of turning new sign-ups into engaged users who clearly understand and experience product value.&nbsp;<a href="https://productled.com/blog/activation-velocity?ref=railsgrowth.com">[ProductLed]</a></p><p><strong>Activation Rate</strong>&nbsp;— The percentage of users who complete a defined activation milestone. Industry average for SaaS is approximately 37.5%.</p><p><strong>Activation Event</strong>&nbsp;— A specific, measurable action that signifies a user has experienced product value.</p><p><strong>Activation Velocity</strong>&nbsp;— A metric measuring how quickly a cohort of users activates over time.&nbsp;<a href="https://productled.com/blog/activation-velocity?ref=railsgrowth.com">[ProductLed]</a></p><p><strong>Setup Moment</strong>&nbsp;— The point when users complete essential configuration steps that enable them to derive value.</p><p><strong>Daily Active Users (DAU)</strong>&nbsp;— The number of unique users who engage with a product within a 24-hour period.&nbsp;<a href="https://www.geckoboard.com/best-practice/kpi-examples/dau-mau-ratio/?ref=railsgrowth.com">[Geckoboard]</a></p><p><strong>Weekly Active Users (WAU)</strong>&nbsp;— The number of unique users who engage with a product over a 7-day period.</p><p><strong>Monthly Active Users (MAU)</strong>&nbsp;— The number of unique users who engage with a product over a 30-day period.&nbsp;<a href="https://www.geckoboard.com/best-practice/kpi-examples/dau-mau-ratio/?ref=railsgrowth.com">[Geckoboard]</a></p><p><strong>DAU/MAU Ratio (Stickiness)</strong>&nbsp;— The proportion of monthly active users who engage daily. Standard benchmark is 10-25%; top apps like WhatsApp exceed 50%.&nbsp;<a href="https://www.geckoboard.com/best-practice/kpi-examples/dau-mau-ratio/?ref=railsgrowth.com">[Geckoboard]</a>&nbsp;<a href="https://www.gainsight.com/essential-guide/product-management-metrics/dau-mau/?ref=railsgrowth.com">[Gainsight]</a></p><p><strong>Stickiness</strong>&nbsp;— How often users return to engage with a product. A sticky product becomes part of users' daily routines.&nbsp;<a href="https://www.wallstreetprep.com/knowledge/product-stickiness/?ref=railsgrowth.com">[Wall Street Prep]</a></p><p><strong>Retention Rate</strong>&nbsp;— The percentage of users who continue engaging with a product over a specific time period.&nbsp;<a href="https://amplitude.com/blog/cohorts-to-improve-your-retention?ref=railsgrowth.com">[Amplitude]</a></p><p><strong>Retention Curve</strong>&nbsp;— A visual representation showing how user retention changes over time, typically declining before reaching an asymptote.&nbsp;<a href="https://amplitude.com/blog/cohorts-to-improve-your-retention?ref=railsgrowth.com">[Amplitude]</a></p><p><strong>Retention Asymptote</strong>&nbsp;— The point where a cohort's retention curve flattens, indicating the percentage of users expected to remain long-term.</p><p><strong>N-Day Retention</strong>&nbsp;— Retention measured at specific intervals after sign-up (Day 1, Day 7, Day 30, etc.).&nbsp;<a href="https://www.adjust.com/blog/demystifying-cohort-retention-session-kpis/?ref=railsgrowth.com">[Adjust]</a></p><p><strong>Rolling Retention</strong>&nbsp;— Measures users who return on or after a specific day, rather than on that exact day.</p><p><strong>Cohort</strong>&nbsp;— A group of users who share a common characteristic tracked together over time.&nbsp;<a href="https://deepdive.headline.com/learn/resources/cohorts-retention-101-for-startups?ref=railsgrowth.com">[Headline]</a></p><p><strong>Cohort Analysis</strong>&nbsp;— A method of grouping users by shared characteristics and tracking their behavior over time.&nbsp;<a href="https://medium.com/@anton.paragraph/cohort-analysis-vs-funnel-analysis-choosing-right-approaches-for-business-insight-69b25092b7f3?ref=railsgrowth.com">[Medium]</a></p><p><strong>Acquisition Cohort</strong>&nbsp;— Users grouped by when they were acquired (e.g., all January sign-ups).&nbsp;<a href="https://amplitude.com/blog/cohorts-to-improve-your-retention?ref=railsgrowth.com">[Amplitude]</a></p><p><strong>Behavioral Cohort</strong>&nbsp;— Users grouped by actions they've taken within the product.&nbsp;<a href="https://amplitude.com/blog/cohorts-to-improve-your-retention?ref=railsgrowth.com">[Amplitude]</a></p><p><strong>Power Users</strong>&nbsp;— The most engaged segment of users who use the product frequently and deeply.</p><h2 id="churn-terminology">Churn Terminology</h2><p><strong>Churn Rate</strong>&nbsp;— The percentage of customers who stop using a product or cancel subscriptions during a given period.&nbsp;<a href="https://churnzero.com/churnopedia/churn-rate/?ref=railsgrowth.com">[ChurnZero]</a></p><p><strong>Customer Churn (Logo Churn)</strong>&nbsp;— The number/percentage of customers lost, regardless of their revenue value.&nbsp;<a href="https://churnzero.com/churnopedia/churn-rate/?ref=railsgrowth.com">[ChurnZero]</a></p><p><strong>Revenue Churn (MRR/ARR Churn)</strong>&nbsp;— The amount of recurring revenue lost due to cancellations and downgrades.&nbsp;<a href="https://mercury.com/blog/types-of-saas-churn?ref=railsgrowth.com">[Mercury]</a></p><p><strong>Gross Churn</strong>&nbsp;— Revenue lost from churned customers without accounting for expansion revenue.&nbsp;<a href="https://mercury.com/blog/types-of-saas-churn?ref=railsgrowth.com">[Mercury]</a></p><p><strong>Net Churn</strong>&nbsp;— Revenue lost minus revenue gained from existing customer expansions. Negative net churn is positive.&nbsp;<a href="https://mercury.com/blog/types-of-saas-churn?ref=railsgrowth.com">[Mercury]</a></p><p><strong>Voluntary Churn</strong>&nbsp;— When customers intentionally choose to cancel or not renew.&nbsp;<a href="https://www.younium.com/blog/churn-analysis?ref=railsgrowth.com">[Younium]</a></p><p><strong>Involuntary Churn</strong>&nbsp;— Customer loss due to payment failures or other non-intentional reasons.&nbsp;<a href="https://www.saasacademy.com/blog/saas-churn-rate?ref=railsgrowth.com">[SaaS Academy]</a></p><p><strong>Churn Cohort Analysis</strong>&nbsp;— Grouping customers by acquisition time to analyze churn behavior patterns.</p><h2 id="funnel-optimization-language">Funnel Optimization Language</h2><p><strong>Conversion Funnel (Sales Funnel/Marketing Funnel)</strong>&nbsp;— A visualization of the steps users take from awareness to completing a desired action.</p><p><strong>TOFU (Top of Funnel)</strong>&nbsp;— The awareness stage where prospects first discover a product.</p><p><strong>MOFU (Middle of Funnel)</strong>&nbsp;— The consideration stage where prospects evaluate whether a product fits their needs.</p><p><strong>BOFU (Bottom of Funnel)</strong>&nbsp;— The decision stage where prospects are ready to convert/purchase.</p><p><strong>AIDA Model</strong>&nbsp;— Attention, Interest, Desire, Action—a classic marketing framework describing customer journey stages.</p><p><strong>Conversion Rate</strong>&nbsp;— The percentage of users who complete a desired action out of total users at a funnel stage.</p><p><strong>Drop-off Rate</strong>&nbsp;— The percentage of users who abandon the funnel at a specific step.</p><p><strong>Funnel Friction</strong>&nbsp;— Any obstacle, confusion, or unnecessary step that causes users to abandon the funnel.</p><p><strong>Friction Point</strong>&nbsp;— A specific moment in the user journey where users struggle or abandon the process.</p><p><strong>Micro-Conversion</strong>&nbsp;— Small, interim actions users take on the way to a macro-conversion (e.g., adding to cart before purchase).</p><p><strong>Macro-Conversion</strong>&nbsp;— The primary goal action (e.g., purchase, subscription sign-up).</p><p><strong>Funnel Analysis</strong>&nbsp;— Tracking user progression through sequential steps toward a goal, identifying drop-off points.&nbsp;<a href="https://medium.com/@anton.paragraph/cohort-analysis-vs-funnel-analysis-choosing-right-approaches-for-business-insight-69b25092b7f3?ref=railsgrowth.com">[Medium]</a></p><h2 id="aarrr-pirate-metrics-framework">AARRR Pirate Metrics Framework</h2><p><strong>AARRR (Pirate Metrics)</strong>&nbsp;— Dave McClure's 2007 framework tracking five user-behavior metrics: Acquisition, Activation, Retention, Referral, Revenue. Called "pirate metrics" because the acronym sounds like "Arrr!"&nbsp;<a href="https://builtin.com/articles/aarrr?ref=railsgrowth.com">[Built In]</a></p><p><strong>Acquisition</strong>&nbsp;— First stage; how users discover the product.&nbsp;<a href="https://builtin.com/articles/aarrr?ref=railsgrowth.com">[Built In]</a></p><p><strong>Activation</strong>&nbsp;— Second stage; user's first valuable experience with product.&nbsp;<a href="https://builtin.com/articles/aarrr?ref=railsgrowth.com">[Built In]</a></p><p><strong>Retention</strong>&nbsp;— Third stage; keeping users engaged and returning.&nbsp;<a href="https://builtin.com/articles/aarrr?ref=railsgrowth.com">[Built In]</a></p><p><strong>Referral</strong>&nbsp;— Fourth stage; users recommending product to others.&nbsp;<a href="https://builtin.com/articles/aarrr?ref=railsgrowth.com">[Built In]</a></p><p><strong>Revenue</strong>&nbsp;— Fifth stage; monetizing user engagement.&nbsp;<a href="https://builtin.com/articles/aarrr?ref=railsgrowth.com">[Built In]</a></p><p><strong>RARRA Framework</strong>&nbsp;— Alternative framework prioritizing Retention first: Retention, Activation, Referral, Revenue, Acquisition.</p><h2 id="onboarding-and-user-journey-phrases">Onboarding and User Journey Phrases</h2><p><strong>User Onboarding</strong>&nbsp;— The process of acquainting new users with a product, guiding them from sign-up to activation.&nbsp;<a href="https://userpilot.com/blog/onboarding-user-flow-examples/?ref=railsgrowth.com">[Userpilot]</a></p><p><strong>Onboarding Flow</strong>&nbsp;— A step-by-step sequence introducing users to a product's features and value.&nbsp;<a href="https://userpilot.com/blog/onboarding-user-flow-examples/?ref=railsgrowth.com">[Userpilot]</a></p><p><strong>Primary Onboarding</strong>&nbsp;— Initial onboarding focused on core features and getting users to their first aha moment.&nbsp;<a href="https://www.theuserflow.co/blog/the-ultimate-user-onboarding-guide-driving-product-growth-and-success?ref=railsgrowth.com">[The User Flow]</a></p><p><strong>Secondary Onboarding</strong>&nbsp;— Introducing users to advanced features building on core value.&nbsp;<a href="https://www.theuserflow.co/blog/the-ultimate-user-onboarding-guide-driving-product-growth-and-success?ref=railsgrowth.com">[The User Flow]</a></p><p><strong>Tertiary Onboarding</strong>&nbsp;— Account expansion focused on upselling additional features.</p><p><strong>Product Tour</strong>&nbsp;— A guided walkthrough introducing users to product features and interface.&nbsp;<a href="https://www.appcues.com/blog/the-10-best-user-onboarding-experiences?ref=railsgrowth.com">[Appcues]</a></p><p><strong>Interactive Walkthrough</strong>&nbsp;— Hands-on guidance where users learn by performing actions within the product.</p><p><strong>Onboarding Checklist</strong>&nbsp;— A list of tasks guiding users through setup and initial product engagement.&nbsp;<a href="https://userpilot.com/blog/onboarding-user-flow-examples/?ref=railsgrowth.com">[Userpilot]</a></p><p><strong>Progress Bar</strong>&nbsp;— A visual indicator showing users how far they've progressed through onboarding.</p><p><strong>Empty State</strong>&nbsp;— The initial state of a product interface before users add data, often used to provide guidance.</p><p><strong>Welcome Survey</strong>&nbsp;— A questionnaire during sign-up collecting user preferences to personalize onboarding.</p><p><strong>Persona-Based Onboarding</strong>&nbsp;— Customizing onboarding flows based on user type, role, or goals.</p><p><strong>Progressive Disclosure</strong>&nbsp;— Gradually revealing product features to avoid overwhelming new users.</p><p><strong>Deferred Account Creation</strong>&nbsp;— Postponing registration until users have experienced product value.</p><p><strong>Gradual Engagement</strong>&nbsp;— Allowing users to experience value before requiring commitment.</p><p><strong>Onboarding Completion Rate</strong>&nbsp;— Percentage of users who finish the onboarding process.</p><p><strong>Tooltip</strong>&nbsp;— A contextual hint appearing when users hover over or click UI elements.</p><p><strong>Hotspot</strong>&nbsp;— A pulsing indicator drawing attention to specific features or actions.</p><p><strong>Modal</strong>&nbsp;— A pop-up window requiring user attention or action.</p><p><strong>Slideout</strong>&nbsp;— A panel that slides in from the screen edge with information or prompts.</p><p><strong>Auth vs Unauth</strong>&nbsp;— Authenticated versus unauthenticated user experiences; different optimization strategies apply to logged-in users versus anonymous visitors.&nbsp;<a href="https://newsletter.pragmaticengineer.com/p/what-is-growth-engineering?ref=railsgrowth.com">[Pragmatic Engineer]</a></p><h2 id="technical-growth-infrastructure-terms">Technical Growth Infrastructure Terms</h2><p><strong>Growth Stack</strong>&nbsp;— Collection of tools and technologies used by growth teams for analytics, experimentation, automation, and optimization.</p><p><strong>Experiment Platform</strong>&nbsp;— Infrastructure for standardizing A/B test setup, user bucketing, and statistical methodology.&nbsp;<a href="https://netflixtechblog.com/its-all-a-bout-testing-the-netflix-experimentation-platform-4e1ca458c15?ref=railsgrowth.com">[Netflix Tech Blog]</a></p><p><strong>MarTech (Marketing Technology)</strong>&nbsp;— Tools enabling marketers to work without engineering involvement: landing page builders, email platforms, analytics tools.</p><p><strong>Customer Data Platform (CDP)</strong>&nbsp;— Software that collects, unifies, and activates customer data from multiple sources to create persistent, unified customer profiles. Examples: Segment, mParticle, Rudderstack.</p><p><strong>Data Management Platform (DMP)</strong>&nbsp;— A platform focused on third-party data for advertising audience targeting.</p><p><strong>Event</strong>&nbsp;— A discrete user action or system occurrence tracked in analytics (e.g., page_view, button_click, purchase).</p><p><strong>Event Properties</strong>&nbsp;— Metadata attached to events providing additional context.</p><p><strong>User Traits</strong>&nbsp;— Attributes stored on user profiles that persist across sessions.</p><p><strong>Event Streaming</strong>&nbsp;— Real-time continuous transmission of event data as it occurs.</p><p><strong>Event Broker (Event Bus)</strong>&nbsp;— Infrastructure (e.g., Apache Kafka) that receives, stores, and distributes event streams.</p><p><strong>ETL (Extract, Transform, Load)</strong>&nbsp;— Traditional data pipeline pattern for data movement and transformation.</p><p><strong>ELT (Extract, Load, Transform)</strong>&nbsp;— Modern pattern where raw data is loaded directly into a data warehouse, then transformed.</p><p><strong>Data Pipeline</strong>&nbsp;— Automated system for moving data from source systems through processing stages to destinations.</p><p><strong>Reverse ETL</strong>&nbsp;— Moving data from a data warehouse back to operational tools for activation.</p><p><strong>Data Warehouse</strong>&nbsp;— Centralized repository optimized for analytical queries on historical data.</p><p><strong>Change Data Capture (CDC)</strong>&nbsp;— Technology that detects and captures changes in source databases for real-time synchronization.</p><p><strong>Instrumentation</strong>&nbsp;— The process of adding tracking code to applications to capture user behavior events.</p><p><strong>Tracking Plan</strong>&nbsp;— Documentation specifying which events and properties to track, their definitions, and implementation requirements.</p><p><strong>Event Taxonomy</strong>&nbsp;— The naming conventions, hierarchy, and structure for organizing tracked events consistently.</p><p><strong>Auto-Capture (Auto-Track)</strong>&nbsp;— Automated collection of user interactions without manual event instrumentation.</p><p><strong>Device Mode (Client-Side)</strong>&nbsp;— Analytics implementation where tracking libraries load directly on user devices.</p><p><strong>Cloud Mode (Server-Side)</strong>&nbsp;— Analytics implementation where data is sent to your servers first, then forwarded to destinations.</p><h2 id="attribution-and-analytics-terminology">Attribution and Analytics Terminology</h2><p><strong>Multi-Touch Attribution (MTA)</strong>&nbsp;— A measurement technique that assigns fractional credit to multiple touchpoints along the customer journey.</p><p><strong>Last-Touch Attribution</strong>&nbsp;— An attribution model that assigns 100% credit to the final touchpoint before conversion.</p><p><strong>First-Touch Attribution</strong>&nbsp;— An attribution model giving 100% credit to the first marketing touchpoint.</p><p><strong>Linear Attribution</strong>&nbsp;— A multi-touch model that distributes equal credit across all touchpoints.</p><p><strong>Time Decay Attribution</strong>&nbsp;— A model assigning progressively more credit to touchpoints occurring closer to conversion.</p><p><strong>U-Shaped Attribution (Position-Based)</strong>&nbsp;— Assigns 40% credit each to first and last touchpoints, with 20% distributed across middle interactions.</p><p><strong>W-Shaped Attribution</strong>&nbsp;— Gives 30% credit each to first touch, lead creation, and opportunity creation touchpoints.</p><p><strong>Data-Driven Attribution (Algorithmic)</strong>&nbsp;— Uses machine learning to dynamically assign credit based on actual conversion path data.</p><p><strong>View-Through Attribution</strong>&nbsp;— Credits conversions to ad impressions that were viewed but not clicked.</p><p><strong>Click-Through Attribution</strong>&nbsp;— Credits conversions only to ads that were actually clicked.</p><p><strong>Attribution Window (Lookback Window)</strong>&nbsp;— The time period during which touchpoints are considered eligible for attribution credit.</p><p><strong>UTM Parameters (Urchin Tracking Module)</strong>&nbsp;— URL tags added to links to track marketing campaign performance.</p><p><strong>utm_source</strong>&nbsp;— Identifies the traffic source (e.g., google, facebook, newsletter).</p><p><strong>utm_medium</strong>&nbsp;— Identifies the marketing medium/channel type (e.g., cpc, email, social).</p><p><strong>utm_campaign</strong>&nbsp;— Identifies the specific campaign name or promotion.</p><p><strong>utm_content</strong>&nbsp;— Differentiates between similar content or links within the same campaign.</p><p><strong>utm_term</strong>&nbsp;— Identifies paid search keywords driving traffic.</p><p><strong>Tracking Pixel</strong>&nbsp;— A 1x1 transparent image embedded in web pages or emails that fires an HTTP request when loaded.</p><p><strong>Incrementality Testing</strong>&nbsp;— Controlled experiments measuring the true causal impact of marketing.</p><p><strong>Marketing Mix Modeling (MMM)</strong>&nbsp;— Statistical analysis evaluating the aggregate impact of marketing spend across channels.</p><h2 id="mobile-attribution-terms">Mobile Attribution Terms</h2><p><strong>Mobile Measurement Partner (MMP)</strong>&nbsp;— A third-party platform that attributes app installs and in-app events to marketing sources. Examples: AppsFlyer, Adjust, Branch, Singular.</p><p><strong>IDFA (Identifier for Advertisers)</strong>&nbsp;— Apple's device-level identifier for tracking and attribution on iOS.</p><p><strong>GAID (Google Advertising ID)</strong>&nbsp;— Google's device-level identifier for Android devices.</p><p><strong>SKAdNetwork (SKAN)</strong>&nbsp;— Apple's privacy-preserving attribution framework providing aggregated, anonymized conversion data.</p><p><strong>Deterministic Attribution</strong>&nbsp;— Attribution method using exact identifier matches for high-confidence attribution.</p><p><strong>Probabilistic Attribution (Fingerprinting)</strong>&nbsp;— Statistical method using device characteristics and behavioral patterns for attribution.</p><p><strong>Deep Linking</strong>&nbsp;— Technology that routes users to specific in-app content rather than just opening an app.</p><p><strong>Deferred Deep Linking</strong>&nbsp;— Deep linking that works even when the app isn't installed.</p><p><strong>Self-Reporting Network (SRN)</strong>&nbsp;— Major ad platforms (Meta, Google, TikTok) that perform their own attribution.</p><p><strong>Install Referrer</strong>&nbsp;— Data passed from app stores containing attribution information about where an app install originated.</p><h2 id="identity-and-user-identification-terms">Identity and User Identification Terms</h2><p><strong>Anonymous ID (anonymousId)</strong>&nbsp;— A UUID automatically generated for unknown visitors before they authenticate.</p><p><strong>User ID (userId)</strong>&nbsp;— A persistent, unique identifier assigned to users after they authenticate.</p><p><strong>Device ID</strong>&nbsp;— A unique identifier tied to a specific device.</p><p><strong>Client ID</strong>&nbsp;— Browser-specific identifier generated by analytics tools to identify unique browser sessions.</p><p><strong>Identity Resolution</strong>&nbsp;— The process of connecting multiple identifiers to create unified user profiles across devices and sessions.</p><p><strong>Identity Graph</strong>&nbsp;— A data structure that maps relationships between different user identifiers.</p><p><strong>Identity Stitching</strong>&nbsp;— The process of merging previously anonymous user sessions with identified user profiles.</p><p><strong>Cross-Device Tracking</strong>&nbsp;— The ability to recognize and track the same user across multiple devices.</p><h2 id="monetization-and-conversion-optimization-language">Monetization and Conversion Optimization Language</h2><p><strong>CTA (Call-to-Action)</strong>&nbsp;— A button, link, or prompt that encourages users to take a specific action.</p><p><strong>Friction</strong>&nbsp;— Any element that creates resistance or hesitation in the user journey, reducing conversion likelihood.</p><p><strong>Bounce Rate</strong>&nbsp;— The percentage of visitors who leave a site after viewing only one page without taking action.</p><p><strong>Heatmap</strong>&nbsp;— A visual representation of user behavior showing where visitors click, scroll, and focus attention.</p><p><strong>Session Replay</strong>&nbsp;— Recordings of individual user sessions showing exactly how visitors navigate and interact.</p><p><strong>Above the Fold</strong>&nbsp;— Content visible on a webpage without scrolling.</p><p><strong>Social Proof</strong>&nbsp;— Evidence that others have used/endorsed a product (testimonials, reviews, logos, user counts).</p><p><strong>Paywall</strong>&nbsp;— A barrier restricting content/feature access until users pay.</p><p><strong>Hard Paywall</strong>&nbsp;— All features locked behind payment/subscription after optional free trial.</p><p><strong>Soft Paywall</strong>&nbsp;— Some features available free while premium features require payment.</p><p><strong>Metered Paywall</strong>&nbsp;— Users get limited free access (e.g., 3 articles/month) before hitting the paywall.</p><p><strong>Dynamic Paywall</strong>&nbsp;— Adjusts access based on user behavior, engagement level, or segment in real-time.</p><p><strong>Feature Gating</strong>&nbsp;— Restricting specific features to paid tiers to drive upgrade behavior.</p><p><strong>Usage-Based Pricing</strong>&nbsp;— Pricing tied to consumption/usage metrics rather than flat subscriptions.</p><p><strong>Tiered Pricing</strong>&nbsp;— Multiple pricing levels targeting different user segments and willingness to pay.</p><h2 id="pricing-optimization-terms">Pricing Optimization Terms</h2><p><strong>Price Elasticity</strong>&nbsp;— How demand changes in response to price changes. Elastic demand (&gt;1) = highly responsive; Inelastic (&lt;1) = low sensitivity.</p><p><strong>Willingness to Pay (WTP)</strong>&nbsp;— The maximum price a customer will pay for a product/feature.</p><p><strong>Van Westendorp Price Sensitivity Meter</strong>&nbsp;— Survey technique asking four questions about price thresholds to identify optimal price range.</p><p><strong>Gabor-Granger Method</strong>&nbsp;— Price research technique presenting different price points to measure purchase likelihood.</p><p><strong>Conjoint Analysis</strong>&nbsp;— Research method presenting product configurations at varying prices to reveal how features impact willingness to pay.</p><p><strong>Value-Based Pricing</strong>&nbsp;— Setting prices based on perceived customer value rather than cost-plus or competitor pricing.</p><p><strong>Price Anchoring</strong>&nbsp;— Psychological technique showing expensive options first to make lower tiers appear more reasonable.</p><p><strong>Annual Discount</strong>&nbsp;— Offering reduced rates for annual commitment (typically 20-30% off monthly equivalent).</p><h2 id="north-star-metrics-and-goal-setting-vocabulary">North Star Metrics and Goal-Setting Vocabulary</h2><p><strong>North Star Metric (NSM)</strong>&nbsp;— A single, company-wide metric that best captures the core value delivered to customers. Examples: Airbnb's "Nights Booked," Spotify's "Time Spent Listening."&nbsp;<a href="https://amplitude.com/blog/product-north-star-metric?ref=railsgrowth.com">[Amplitude]</a></p><p><strong>Input Metrics</strong>&nbsp;— The contributing factors that influence a North Star Metric. Common dimensions: breadth (reach), depth (engagement), frequency (cadence), efficiency.&nbsp;<a href="https://amplitude.com/blog/product-north-star-metric?ref=railsgrowth.com">[Amplitude]</a></p><p><strong>Output Metrics</strong>&nbsp;— Business outcomes that result from North Star Metric improvements (revenue, profit, market share).</p><p><strong>OKRs (Objectives and Key Results)</strong>&nbsp;— A goal-setting framework where Objectives are qualitative goals, and Key Results are specific, measurable outcomes.</p><p><strong>Committed OKRs</strong>&nbsp;— Goals the organization must achieve; expected to reach 100% completion.</p><p><strong>Aspirational OKRs</strong>&nbsp;— Stretch goals aimed at significant progress; 70% achievement is considered successful.</p><p><strong>One Metric That Matters (OMTM)</strong>&nbsp;— A team-specific focus metric that changes quarterly based on current priorities.</p><p><strong>KPIs (Key Performance Indicators)</strong>&nbsp;— Quantitative metrics pegged to specific targets used to measure success.</p><p><strong>Leading Indicators</strong>&nbsp;— Metrics that predict future outcomes and provide early warning signals.</p><p><strong>Lagging Indicators</strong>&nbsp;— Metrics that measure past performance/outcomes.</p><h2 id="saas-and-revenue-metrics">SaaS and Revenue Metrics</h2><p><strong>MRR (Monthly Recurring Revenue)</strong>&nbsp;— Predictable monthly revenue from subscriptions.</p><p><strong>ARR (Annual Recurring Revenue)</strong>&nbsp;— MRR × 12; the key metric for company valuation.</p><p><strong>New MRR</strong>&nbsp;— Recurring revenue from newly acquired customers in a period.</p><p><strong>Expansion MRR</strong>&nbsp;— Additional revenue from existing customers through upgrades or add-ons.</p><p><strong>Churned MRR</strong>&nbsp;— Revenue lost from customers who cancelled during a period.</p><p><strong>Contraction MRR</strong>&nbsp;— Revenue reduction from existing customers downgrading plans.</p><p><strong>Net Revenue Retention (NRR/NDR)</strong>&nbsp;— Revenue retained from existing customers including expansion minus churn/contraction. Over 100% indicates net negative churn.</p><p><strong>Gross Revenue Retention (GRR)</strong>&nbsp;— Revenue retention excluding expansion; measures pure retention capability.</p><p><strong>LTV (Customer Lifetime Value)</strong>&nbsp;— Total revenue expected from a customer over their entire relationship.</p><p><strong>CAC (Customer Acquisition Cost)</strong>&nbsp;— Total cost to acquire one new customer, including marketing and sales expenses.</p><p><strong>LTV:CAC Ratio</strong>&nbsp;— Compares customer lifetime value to acquisition cost. Benchmark: 3:1.</p><p><strong>CAC Payback Period</strong>&nbsp;— Months required to recover customer acquisition cost.</p><p><strong>ARPU/ARPA (Average Revenue Per User/Account)</strong>&nbsp;— Total MRR divided by total customers.</p><p><strong>ACV (Annual Contract Value)</strong>&nbsp;— Annual value of a customer's subscription revenue.</p><p><strong>Trial-to-Paid Conversion</strong>&nbsp;— Percentage of free trial users who convert to paying customers.</p><p><strong>Quick Ratio (SaaS)</strong>&nbsp;— Measures growth efficiency: (New MRR + Expansion MRR) ÷ (Churned MRR + Contraction MRR). Benchmark: 4:1.</p><p><strong>Unit Economics</strong>&nbsp;— The direct revenues and costs associated with a single customer.</p><p><strong>NPS (Net Promoter Score)</strong>&nbsp;— Customer satisfaction measure based on likelihood to recommend (-100 to +100 scale).</p><p><strong>Feature Adoption Rate</strong>&nbsp;— Percentage of users who adopt and use a specific feature.</p><p><strong>Product Adoption Rate</strong>&nbsp;— Percentage of new active users relative to sign-ups over a specific period.</p><hr><h2 id="key-benchmarks-reference">Key benchmarks reference</h2>
<!--kg-card-begin: html-->
<table>
<thead>
<tr>
<th>Metric</th>
<th>Healthy Benchmark</th>
</tr>
</thead>
<tbody>
<tr>
<td>LTV:CAC Ratio</td>
<td>≥3:1</td>
</tr>
<tr>
<td>Quick Ratio (SaaS)</td>
<td>≥4:1</td>
</tr>
<tr>
<td>Net Revenue Retention</td>
<td>≥100% (Enterprise: 120%+)</td>
</tr>
<tr>
<td>Freemium Conversion</td>
<td>2-5%</td>
</tr>
<tr>
<td>OKR Achievement</td>
<td>60-70%</td>
</tr>
<tr>
<td>Monthly Churn</td>
<td>&lt;0.83% (≈10% annual)</td>
</tr>
<tr>
<td>CAC Payback</td>
<td>&lt;12 months</td>
</tr>
<tr>
<td>DAU/MAU Stickiness</td>
<td>10-25% (best-in-class: 50%+)</td>
</tr>
<tr>
<td>Activation Rate (SaaS)</td>
<td>~37.5% average</td>
</tr>
</tbody>
</table>
<!--kg-card-end: html-->
<hr><h2 id="sources-consulted">Sources consulted</h2><p>This glossary draws terminology from authoritative sources including:</p><ul><li>Engineering blogs from&nbsp;<strong>Airbnb, Netflix, Uber, Pinterest, and Dropbox</strong></li><li>Growth methodology content from&nbsp;<a href="https://www.reforge.com/?ref=railsgrowth.com">Reforge</a>,&nbsp;<a href="https://openviewpartners.com/?ref=railsgrowth.com">OpenView</a>, and&nbsp;<a href="https://productled.com/?ref=railsgrowth.com">ProductLed</a></li><li>Analytics platforms including&nbsp;<a href="https://amplitude.com/?ref=railsgrowth.com">Amplitude</a>,&nbsp;<a href="https://mixpanel.com/?ref=railsgrowth.com">Mixpanel</a>,&nbsp;<a href="https://segment.com/?ref=railsgrowth.com">Segment</a>, and&nbsp;<a href="https://heap.io/?ref=railsgrowth.com">Heap</a></li><li>Experimentation platforms such as&nbsp;<a href="https://statsig.com/?ref=railsgrowth.com">Statsig</a>,&nbsp;<a href="https://launchdarkly.com/?ref=railsgrowth.com">LaunchDarkly</a>,&nbsp;<a href="https://www.optimizely.com/?ref=railsgrowth.com">Optimizely</a>, and&nbsp;<a href="https://www.growthbook.io/?ref=railsgrowth.com">GrowthBook</a></li><li>Attribution platforms including&nbsp;<a href="https://www.appsflyer.com/?ref=railsgrowth.com">AppsFlyer</a>,&nbsp;<a href="https://www.adjust.com/?ref=railsgrowth.com">Adjust</a>, and&nbsp;<a href="https://www.branch.io/?ref=railsgrowth.com">Branch</a></li><li>SaaS metrics resources from&nbsp;<a href="https://chartmogul.com/?ref=railsgrowth.com">ChartMogul</a>,&nbsp;<a href="https://www.profitwell.com/?ref=railsgrowth.com">ProfitWell</a>, and&nbsp;<a href="https://baremetrics.com/?ref=railsgrowth.com">Baremetrics</a></li><li>Growth-focused publications including&nbsp;<a href="https://www.lennysnewsletter.com/?ref=railsgrowth.com">Lenny's Newsletter</a>,&nbsp;<a href="https://review.firstround.com/?ref=railsgrowth.com">First Round Review</a>, and&nbsp;<a href="https://cxl.com/?ref=railsgrowth.com">CXL</a></li></ul><h2 id="methodology">Methodology</h2><p>I used Claude in deep research mode to build the initial framework for this page in 2025 and have expanded and updated it manually since. Here's a sample of the starting prompt used if you'd like to replicate this for another topic area.</p><div class="kg-card kg-callout-card kg-callout-card-white"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text"><b><strong style="white-space: pre-wrap;">Note:</strong></b> I also did this for my new company's target audience as well to start the process of learning the customer voice - an awesome way to get to know a new topic space.</div></div><pre><code class="language-md">Generate a long list of phrases used in articles and webpages talking about "growth engineer" or "growth engineering" and output it as a glossary type of format please.

Eg "auth vs unauth experience"

Review as many URLs as you can and do a query fan out on those keywords to dive into various sub topics.</code></pre>
            <br/><br/>
            This article was originally published at <a href="https://railsgrowth.com/resources/growth-engineering-glossary/">Growth Engineering Glossary</a>
        ]]></description>
        <link>https://railsgrowth.com/resources/growth-engineering-glossary/</link>
        <guid isPermaLink="false">694058ee266a0fc053493dab</guid>
        <dc:creator><![CDATA[ Kane Jamison ]]></dc:creator>
        <pubDate>Mon, 15 Dec 2025 18:58:26 +0000</pubDate>
    </item>
    <item>
        <title><![CDATA[ Cleaning Up Stale Git Branches Locally &amp; On Github ]]></title>
        <description><![CDATA[
            <p>I've built up tons of stale branches and wanted to clean them up locally and on origin. These commands take care of that.</p><div class="kg-card kg-callout-card kg-callout-card-white"><div class="kg-callout-emoji">⚠️</div><div class="kg-callout-text">Obviously, if you are working in a repo that might have historical / legacy reasons for keeping all of these branches, maybe don't run these commands without checking the branches you'll be deleting.</div></div><h2 id="how-to-delete-local-branches-that-have-been-deleted-from-github-origin">How to Delete Local Branches That Have Been Deleted From Github / Origin</h2><p>Nowadays in my repos I configure Github to "Automatically delete head branches" after we squash and merge. But I didn't used to do that before the feature existed, so we have a lot of branches hanging around on Github that could be deleted.</p><p>But as far as I can tell, those branches still stick around on my local device.</p><p>So this first command deletes all local branches that no longer exist on origin, except for my current branch:</p><pre><code class="language-bash">git fetch -p &amp;&amp; git branch -vv | grep ': gone]' | awk '{print $1}' | xargs -I {} git branch -D {}</code></pre><p>It <strong>shouldn't</strong> delete:</p><ol><li><strong>Local-only branches</strong> — branches you created but never pushed</li><li><strong>Branches still on origin</strong> — even if you don't care about them locally, if they exist remotely, they're kept</li><li><strong>Your current branch</strong> — git won't let you delete the branch you're on</li></ol><p>Since this is one I'll run periodically moving forward, I've also set it up as a zsh alias in my .zshrc file.</p><pre><code class="language-zsh"># git helpers
alias cleanbranches="git fetch -p &amp;&amp; git branch -vv | grep ': gone]' | awk '{print $1}' | xargs -I {} git branch -D {}"</code></pre><h2 id="how-to-delete-github-origin-branches-that-arent-attached-to-prs">How to Delete Github / Origin Branches That Aren't Attached to PRs</h2><p>Now - we used to not automatically delete branches on squash and merge. So that means we also have a ton of historical branches on Github (130+) but we only have ~40 live Pull Requests, meaning there's a good 70-90 branches on Github that we probably don't need any more.</p><p>So, this command checks what branches exist on github that don't have a PR attached to them:</p><pre><code class="language-bash">gh api repos/:owner/:repo/branches --paginate --jq '.[].name' | while read branch; do
  if [ "$branch" != "main" ] &amp;&amp; [ "$branch" != "master" ]; then
    pr_count=$(gh pr list --head "$branch" --state all --json number --jq 'length')
    if [ "$pr_count" -eq 0 ]; then
      echo "$branch"
    fi
  fi
done</code></pre><p>Then this version of the command changes the echo to a delete command to actually delete them:</p><pre><code class="language-bash">gh api repos/:owner/:repo/branches --paginate --jq '.[].name' | while read branch; do
  if [ "$branch" != "main" ] &amp;&amp; [ "$branch" != "master" ]; then
    pr_count=$(gh pr list --head "$branch" --state all --json number --jq 'length')
    if [ "$pr_count" -eq 0 ]; then
      git push origin --delete "$branch"
    fi
  fi
done</code></pre><p><strong>What this does:</strong></p><ol><li>Lists all branches on the remote</li><li>Skips main/master</li><li>Checks if any PR (open, closed, or merged) exists with that branch as the head</li><li>Deletes branches with zero associated PRs</li></ol><p>This command is a little slow since it does an API call for every branch, but still only took a minute or so.</p><h2 id="how-to-delete-github-origin-branches-that-are-attached-to-closed-merged-prs">How to Delete Github / Origin Branches That Are Attached to Closed / Merged PRs</h2><p>That last command does not take care of our old branches attached to closed PRs, though. This command will list those branches that are attached to PRs which were closed or merged:</p><pre><code class="language-bash">gh api repos/:owner/:repo/branches --paginate --jq '.[].name' | while read branch; do
  if [ "$branch" != "main" ] &amp;&amp; [ "$branch" != "master" ]; then
    open_pr_count=$(gh pr list --head "$branch" --state open --json number --jq 'length')
    if [ "$open_pr_count" -eq 0 ]; then
      echo "$branch"
    fi
  fi
done</code></pre><p>And again, this next command changes echo to delete and actually deletes them:</p><pre><code class="language-bash">gh api repos/:owner/:repo/branches --paginate --jq '.[].name' | while read branch; do
  if [ "$branch" != "main" ] &amp;&amp; [ "$branch" != "master" ]; then
    open_pr_count=$(gh pr list --head "$branch" --state open --json number --jq 'length')
    if [ "$open_pr_count" -eq 0 ]; then
      git push origin --delete "$branch"
    fi
  fi
done</code></pre><p>Again, this command is a little slow since it does an API call for every branch, but still only took a minute or so.</p><figure class="kg-card kg-gallery-card kg-width-wide kg-card-hascaption"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://railsgrowth.com/content/images/2025/11/before-deleting-branches.png" width="433" height="294" loading="lazy" alt=""></div><div class="kg-gallery-image"><img src="https://railsgrowth.com/content/images/2025/11/after-deleting-branches.png" width="441" height="307" loading="lazy" alt=""></div></div></div><figcaption><p><span style="white-space: pre-wrap;">Before and After Deleting All The Stale Stuff</span></p></figcaption></figure>
            <br/><br/>
            This article was originally published at <a href="https://railsgrowth.com/articles/cleaning-up-stale-git-branches/">Cleaning Up Stale Git Branches Locally &amp; On Github</a>
        ]]></description>
        <link>https://railsgrowth.com/articles/cleaning-up-stale-git-branches/</link>
        <guid isPermaLink="false">692b710c266a0fc053493d3b</guid>
        <dc:creator><![CDATA[ Kane Jamison ]]></dc:creator>
        <pubDate>Sat, 29 Nov 2025 22:40:12 +0000</pubDate>
    </item>
    <item>
        <title><![CDATA[ Archiving Rails Database Migrations by Year ]]></title>
        <description><![CDATA[
            <p>We are approaching 400 Rails database migrations in Content Harmony and they are getting a little unwieldy in my editor sidebar. </p><p><a href="https://bsky.app/profile/did:plc:ama6ppmotzg57qlxdmv4z5ur?ref=railsgrowth.com">Simon Chiu</a> has <a href="https://codewithrails.com/clean-up-db-migrations?ref=railsgrowth.com">a nice rake task he posted</a> that helps archive old database migration, and his post explains some of the reasons for doing so aside from being annoyed by scrolling.</p><figure class="kg-card kg-image-card"><img src="https://railsgrowth.com/content/images/2025/08/image-2.png" class="kg-image" alt="" loading="lazy" width="912" height="903" srcset="https://railsgrowth.com/content/images/size/w600/2025/08/image-2.png 600w, https://railsgrowth.com/content/images/2025/08/image-2.png 912w" sizes="(min-width: 720px) 720px"></figure><p>His version pushes all files into a single archive, and I wanted a variation I could run that would organize these by year. I also didn't want to archive migrations from the current year, nor did I want this rake task to ever get run in production just in case, so here is a slight update on his rake task that takes those requirements into account:</p><pre><code class="language-ruby">namespace :db do
  namespace :migrate do
    desc "Archive migration files from past years into yearly subdirectories"
    task :archive do
      if Rails.env.production?
        puts "ERROR: This task should not be run in production environment."
        return
      end

      current_year = Date.current.year
      migrations_path = Rails.root.join("db/migrate")
      archives_path = Rails.root.join("db/migrate/archives")

      puts "Archiving migration files (excluding #{current_year})..."

      # Create archives directory if it doesn't exist
      FileUtils.mkdir_p(archives_path)

      # Get all migration files
      migration_files = Dir.glob(File.join(migrations_path, "*.rb"))

      archived_count = 0

      migration_files.each do |file|
        filename = File.basename(file)

        # Extract timestamp from filename (first 14 characters: YYYYMMDDHHMMSS)
        timestamp = filename[0, 14]

        # Skip if timestamp is invalid
        next unless timestamp.match?(/^\d{14}$/)

        # Extract year from timestamp
        file_year = timestamp[0, 4].to_i

        # Skip files from current year
        next if file_year &gt;= current_year

        # Create year-specific archive directory
        year_archive_path = File.join(archives_path, file_year.to_s)
        FileUtils.mkdir_p(year_archive_path)

        # Move the file
        destination = File.join(year_archive_path, filename)
        FileUtils.mv(file, destination)

        puts "Archived #{filename} to archives/#{file_year}/"
        archived_count += 1
      end

      if archived_count &gt; 0
        puts "Successfully archived #{archived_count} migration file(s)."
        puts "Current migration files remain in db/migrate/"
      else
        puts "No migration files to archive (all files are from #{current_year} or later)."
      end
    end
  end
end
</code></pre><p>Here is what you'll see in console upon running this:</p><figure class="kg-card kg-image-card"><img src="https://railsgrowth.com/content/images/2025/08/image.png" class="kg-image" alt="" loading="lazy" width="1069" height="725" srcset="https://railsgrowth.com/content/images/size/w600/2025/08/image.png 600w, https://railsgrowth.com/content/images/size/w1000/2025/08/image.png 1000w, https://railsgrowth.com/content/images/2025/08/image.png 1069w" sizes="(min-width: 720px) 720px"></figure><p>And here is what you'll see if you run this twice in the same year:</p><figure class="kg-card kg-image-card"><img src="https://railsgrowth.com/content/images/2025/08/image-1.png" class="kg-image" alt="" loading="lazy" width="980" height="203" srcset="https://railsgrowth.com/content/images/size/w600/2025/08/image-1.png 600w, https://railsgrowth.com/content/images/2025/08/image-1.png 980w" sizes="(min-width: 720px) 720px"></figure><p>If you're running Standard or Rubocop you'll likely need to make a change like this as well to ignore older migrations that don't pass linting:</p><pre><code class="language-yml">ignore:
  # ignore old database migrations
  - "db/migrate/archives/**/*"</code></pre><figure class="kg-card kg-image-card"><img src="https://railsgrowth.com/content/images/2025/08/image-3.png" class="kg-image" alt="" loading="lazy" width="904" height="362" srcset="https://railsgrowth.com/content/images/size/w600/2025/08/image-3.png 600w, https://railsgrowth.com/content/images/2025/08/image-3.png 904w" sizes="(min-width: 720px) 720px"></figure><h3 id="cant-you-just-drag-and-drop-these-files-manually"><em>"Can't you just drag and drop these files manually?"</em></h3><p>Yeah absolutely. But this way is more fun.</p>
            <br/><br/>
            This article was originally published at <a href="https://railsgrowth.com/articles/archiving-rails-database-migrations-by-year/">Archiving Rails Database Migrations by Year</a>
        ]]></description>
        <link>https://railsgrowth.com/articles/archiving-rails-database-migrations-by-year/</link>
        <guid isPermaLink="false">68adfd88266a0fc053493cb8</guid>
        <dc:creator><![CDATA[ Kane Jamison ]]></dc:creator>
        <pubDate>Tue, 26 Aug 2025 18:38:07 +0000</pubDate>
    </item>
    <item>
        <title><![CDATA[ How To Organize ViewComponents in Rails ]]></title>
        <description><![CDATA[
            <p>When you first start adding components like ViewComponents into an application, I think it's normal to start running basic commands like <code>rails generate component Button</code> as an easy starting point.</p><p>Very quickly, you'll find that /app/components/ fills up with dozens or hundreds of files, and they rarely organize themselves nicely.</p><p>I prefer to namespace components early on - usually in a shallow single folder structure.</p><h2 id="common-viewcomponent-namespaces">Common ViewComponent Namespaces</h2><p>Here are some examples of common folders I'll use. All of these will shift depending on the size of the app and what type of design system we're working on. Is it a CMS? SaaS app? eCommerce site? All of those will demand different setups.</p><h3 id="ui-elements-controls">UI Elements &amp; Controls</h3><ul><li><code>/buttons/</code> - Primary, secondary, icon buttons, button groups</li><li><code>/forms/</code> or <code>/fields/</code>- Form controls, input groups, field wrappers</li><li><code>/inputs/</code> - Text fields, selects, checkboxes, radio buttons. Sometimes I'll keep these in <code>/fields/</code></li><li><code>/tables/</code> - Data tables, sortable headers, pagination</li><li><code>/navigation/</code> - Navbars, breadcrumbs, tabs, sidebar menus</li><li><code>/modals/</code> - Dialog boxes, confirmations, overlays</li><li><code>/dropdowns/</code> - Select menus, action menus, context menus</li><li><code>/alerts/</code> - Success, error, warning, info notifications</li><li><code>/badges/</code> - Status indicators, counts, labels</li><li><code>/icons/</code> - SVG icons, icon wrappers, icon buttons</li></ul><p>Depending on the number of these types of components, many of them also fit nicely into a <code>/utilities/</code> namespace, eg <code>Utility::AlertComponent</code>.</p><h3 id="layout-structure-components">Layout &amp; Structure Components</h3><p>Depending on the amount of layout components I'll start with a <code>/layout/</code> folder, but for commonly used items like Cards or Sidebars I'll break out into separate folders.</p><ul><li><code>/layouts/</code> - Page layouts, grid systems, containers</li><li><code>/cards/</code> - Content cards, product cards, user cards</li><li><code>/panels/</code> - Collapsible panels, accordion sections</li><li><code>/sections/</code> - Page sections, content blocks</li><li><code>/headers/</code> - Page headers, section headers</li><li><code>/footers/</code> - Page footers, section footers</li><li><code>/sidebars/</code> - Navigation sidebars, filter sidebars</li><li><code>/wrappers/</code> - Generic container components</li></ul><h3 id="content-display-components">Content Display Components</h3><p>Most of these fit nicely into an Elements namespace, eg <code>Elements::IconListComponent</code>, but can get broken out as they increase in complexity.</p><ul><li><code>/lists/</code> - Ordered, unordered, definition lists</li><li><code>/media/</code> - Images, videos, galleries, carousels</li><li><code>/text/</code> - Typography, headings, paragraphs, quotes</li><li><code>/avatars/</code> - User avatars, profile pictures</li><li><code>/thumbnails/</code> - Image thumbnails, preview cards</li><li><code>/tags/</code> - Content tags, category labels</li><li><code>/progress/</code> - Progress bars, loading indicators</li><li><code>/charts/</code> - Data visualization components</li></ul><h3 id="interactive-elements-components">Interactive Elements Components</h3><p>These sort of exist in between Layout and Utility and Elements namespaces and might deserve their own folders.</p><ul><li><code>/tabs/</code> - Tab navigation, tab content</li><li><code>/accordions/</code> - Expandable content sections</li><li><code>/toggles/</code> - Switch toggles, show/hide controls</li><li><code>/tooltips/</code> - Hover tooltips, help text</li><li><code>/popovers/</code> - Click-triggered content overlays</li><li><code>/pagination/</code> - Page navigation, item counters</li><li><code>/search/</code> - Search bars, filters, results</li><li><code>/sorting/</code> - Sort controls, order toggles</li></ul><h3 id="application-specific-components">Application-Specific Components</h3><p>I reserve these for design patterns that really don't apply outside of the model/product area that we're working on.</p><ul><li><code>/dashboard/</code> - Dashboard widgets, metrics cards</li><li><code>/admin/</code> - Admin-specific components</li><li><code>/auth/</code> - Login forms, registration, password reset</li><li><code>/profile/</code> - User profile components</li><li><code>/settings/</code> - Configuration forms, preference toggles</li><li><code>/comments/</code> - Comment threads, reply forms</li><li><code>/notifications/</code> - System notifications, alerts</li><li><code>/messages/</code> - Chat bubbles, message threads</li></ul><h3 id="utility-system-components">Utility &amp; System Components</h3><p>These all fit well into a Utility namespace as well for simpler sites.</p><ul><li><code>/loading/</code> - Spinners, skeleton screens, placeholders</li><li><code>/empty/</code> - Empty state messages, call-to-actions</li><li><code>/errors/</code> - Error messages, 404 pages</li><li><code>/confirmations/</code> - Confirmation dialogs, delete warnings</li><li><code>/skeletons/</code> - Loading placeholders for content</li><li><code>/overlays/</code> - Background overlays, dimming layers</li></ul><h2 id="lookbook-organization">Lookbook Organization</h2><p>Similar to our ViewComponents, I like to use namespaces in Lookbook previews as well.</p><p>Lookbook will automatically handle namespaced folders for objects like <code>class Sidebars::SidebarContainerComponentPreview &lt; ViewComponent::Preview</code></p><p>You can see this on the <a href="https://lookbook.build/?ref=railsgrowth.com" rel="noreferrer">Lookbook homepage</a> preview image:</p><figure class="kg-card kg-image-card"><img src="https://railsgrowth.com/content/images/2025/07/image-2.png" class="kg-image" alt="" loading="lazy" width="1434" height="1176" srcset="https://railsgrowth.com/content/images/size/w600/2025/07/image-2.png 600w, https://railsgrowth.com/content/images/size/w1000/2025/07/image-2.png 1000w, https://railsgrowth.com/content/images/2025/07/image-2.png 1434w" sizes="(min-width: 720px) 720px"></figure>
            <br/><br/>
            This article was originally published at <a href="https://railsgrowth.com/articles/how-to-organize-viewcomponents/">How To Organize ViewComponents in Rails</a>
        ]]></description>
        <link>https://railsgrowth.com/articles/how-to-organize-viewcomponents/</link>
        <guid isPermaLink="false">687ffb7e266a0fc053493c15</guid>
        <dc:creator><![CDATA[ Kane Jamison ]]></dc:creator>
        <pubDate>Tue, 22 Jul 2025 21:16:57 +0000</pubDate>
    </item>
    <item>
        <title><![CDATA[ A Custom Outdated Gems Script to Speed Up Rails Dependency Upgrades [+ Video] ]]></title>
        <description><![CDATA[
            <p>I spent much of 2024 working on modernizing Content Harmony's Rails stack. During that time, one of the bigger backlogs that had accumulated was out-of-date gem dependencies.</p><p>We were on Rails 6.0.0_rc1 while Rails was working on development for Rails 8.</p><p>We were on Ruby 2.5 shortly after 3.3 had been released.</p><p>And to top it off, we had over 90 gems in our Gemfile that were pinned haphazardly without clear logic or notes.</p><p>Getting everything up to date was a team effort, but one of the problems that needed to be solved was that we needed to do incremental updates on nearly every single dependency, upgrading them one by one and dealing with breaking changes and lack of test coverage on most of them.</p><p>When you're at the starting line and have to handle dozens or hundreds of dependency upgrades, out-of-the-box tools like <code>bundle outdated</code> aren't always sufficient for understanding the current state of your gemfile.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://railsgrowth.com/content/images/2025/07/image.png" class="kg-image" alt="" loading="lazy" width="1013" height="1154" srcset="https://railsgrowth.com/content/images/size/w600/2025/07/image.png 600w, https://railsgrowth.com/content/images/size/w1000/2025/07/image.png 1000w, https://railsgrowth.com/content/images/2025/07/image.png 1013w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">At a glance, I can't tell which gems are critical vs dev/test only, how out of date each gem is, what my oldest gems are, etc.</span></figcaption></figure><p>So, I began the process of writing a custom script that I could run to gather more data via the <a href="https://guides.rubygems.org/rubygems-org-api/?ref=railsgrowth.com">Ruby Gems API</a>.</p><h2 id="what-my-script-does-differently">What My Script Does Differently</h2><p>Instead of just showing me what's outdated, my script pulls together the context that actually matters:</p><ul><li><strong>Release dates</strong> - I can see I'm running a gem from May 2020 versus March 2025, which tells me way more than just version numbers</li><li><strong>Number of versions available</strong> - 55 available versions might sound scary, but if AWS pumps out updates weekly, that's different than a gem that hasn't been touched in years</li><li><strong>Explicit vs implicit requirements</strong> - I only care about gems I explicitly added to my Gemfile, not the dozens of downstream dependencies that will update automatically</li><li><strong>Metadata</strong> - Direct links to changelogs and homepages so I can quickly assess what's changed</li><li><strong>Formatting</strong> - One other aspect of <code>bundle outdated</code> - when you copy/paste it out of the terminal it doesn't parse well into a spreadsheet. So instead of outputting a fancy format version, we just output everything to <code>pipe|separated|data</code>.</li><li><strong>Move Terminal Output to Google Sheets</strong> - Copies everything to user clipboard then opens up a new Google Sheet to set up a pivot table for analysis.</li></ul><p>The script outputs everything, copies it to my clipboard, and automatically opens a new Google Sheet.</p><p>From there, I split the data into columns and build a pivot table that lets me filter and sort however I need.</p><h2 id="my-outdated-gems-script">My Outdated Gems Script</h2><p>Here is the latest version which lives at <code>bin/lib/outdated_gems_audit.rb</code> in my app:</p><pre><code class="language-ruby"># From the app root folder, run the script with this command:
# &gt;    ruby bin/lib/outdated_gems_audit.rb
# Then the script will copy the output to your clipboard and open a new spreadsheet.
# Paste into the spreadsheet and split Text To Columns by | to analyze the results.
# Add alias outdatedgems="ruby lib/scripts/outdated_gems_audit.rb" to ~/.bashrc or ~/.zshrc

require "bundler"
require "net/http"
require "json"
require "uri"
require "launchy"
require "date"

# Determine the OS to allow copy to clipboard to work
def os
  @os ||= (
    host_os = RUBY_PLATFORM.downcase
    case host_os
    when /linux/
      "linux"
    when /darwin/
      "mac"
    when /mswin|mingw|cygwin/
      "windows"
    else
      raise "unknown os: #{host_os.inspect}"
    end
  )
end

# Copy the output to the clipboard
def copy_to_clipboard(text)
  case os
  when "mac"
    IO.popen('pbcopy', 'w') { |f| f &lt;&lt; text }
    puts "Output copied to clipboard!"
  when "linux"
    # Attempt to use xclip, but fall back to xsel if xclip is not available
    if system("command -v xclip &gt; /dev/null")
      IO.popen('xclip -selection clipboard', 'w') { |f| f &lt;&lt; text }
    elsif system("command -v xsel &gt; /dev/null")
      IO.popen('xsel --clipboard --input', 'w') { |f| f &lt;&lt; text }
    else
      raise "Clipboard utilities xclip or xsel are not installed."
    end
    puts "Output copied to clipboard!"
  when "windows"
    IO.popen('clip', 'w') { |f| f &lt;&lt; text }
    puts "Output copied to clipboard!"
  else
    raise "Clipboard not supported on this OS"
  end
end


# Parse the Gemfile and Gemfile.lock
definition = Bundler.definition
lockfile = Bundler::LockfileParser.new(Bundler.read_file(Bundler.default_lockfile))

def fetch_url_content(url)
  uri = URI.parse(url)
  response = Net::HTTP.get_response(uri)

  case response
  when Net::HTTPSuccess then
    JSON.parse(response.body)
  else
    raise "Could not fetch URL: #{url} - Error: #{response.message}"
  end
rescue URI::InvalidURIError
  raise "Invalid URL: #{url}"
end

def fetch_gem_release_date_and_versions(gem_name, current_version)
  versions_url = "https://rubygems.org/api/v1/versions/#{gem_name}.json"
  versions_data = fetch_url_content(versions_url)

  # Extract all version numbers from the fetched data
  all_versions = versions_data.map { |v| v["number"] }
  
  # Check if a version is a prerelease (contains alpha, beta, rc, pre patterns)
  def prerelease_version?(version)
    version.match?(/\.(alpha|beta|rc|pre|dev)/i) || version.match?(/-(alpha|beta|rc|pre|dev)/i)
  end
  
  # Separate stable and prerelease versions
  versions = all_versions.reject { |v| prerelease_version?(v) }
  prerelease_versions = all_versions.select { |v| prerelease_version?(v) }

  # Calculate the number of versions newer than the current version
  current_version_index = versions.find_index(current_version)

  # Find the release date of the current version, if it exists
  current_version_data = versions_data.find { |data| data["number"] == current_version }

  # Initialize defaults
  release_date = "Not Found"
  changelog_url = ""
  homepage_url = ""

  if current_version_data
      if current_version_data["built_at"]
        # Parse the date and format it as YYYY-MM-DD
        release_date = Date.parse(current_version_data["built_at"]).strftime("%Y-%m-%d")
      end

      # Extract metadata for changelog and homepage URLs if available
      if current_version_data["metadata"]
        changelog_url = current_version_data["metadata"]["changelog_uri"] || ""
        homepage_url = current_version_data["metadata"]["homepage_uri"] || ""
      end
    end

    # Generate versions string excluding the current version (only newer versions)
    versions_available_array = current_version_index.nil? ? [] : versions[0...current_version_index]
    versions_string = versions_available_array.join(",")
    
    # Generate prerelease versions string (only newer prerelease versions)
    current_prerelease_index = prerelease_versions.find_index(current_version)
    if current_prerelease_index.nil?
      # Current version is not a prerelease, so include all newer prerelease versions
      prerelease_available_array = prerelease_versions
    else
      # Current version is a prerelease, exclude it from the list
      prerelease_available_array = prerelease_versions[0...current_prerelease_index]
    end
    prerelease_string = prerelease_available_array.join(",")

    # If current_version_index is nil, it means the current version was not found in the list
    # which should ideally not happen but could indicate an issue with version naming or data retrieval
    if current_version_index.nil?
      versions_available = "Nil"
    else
      # Versions newer than the current version
      versions_available = versions_available_array.length
    end

    [release_date, versions_available, changelog_url, homepage_url, versions_string, prerelease_string]
rescue =&gt; e
    puts "Error fetching data for #{gem_name} #{current_version}: #{e}"
    ["Release date not found", 0, "", "", "", ""]
end

def open_new_google_sheet
  Launchy.open("http://sheets.new")
rescue =&gt; e
  puts "Failed to open browser: #{e}"
end

# Store all output so we can copy it to the clipboard
headers = "Gem|Explicitly Required|Pinned At|Groups|Current|Release Date|Versions Available|Changelog URL|Homepage|Versions|Prerelease Versions"
output = "#{headers}\n"
puts headers

lockfile.specs.each do |spec|
  # Determine if the gem is explicitly required in the Gemfile
  explicitly_required = definition.dependencies.any? { |dep| dep.name == spec.name }

  # Find the version requirement and groups for the gem from the Gemfile
  dep = definition.dependencies.find { |d| d.name == spec.name }
  pinned_version = dep &amp;&amp; dep.requirement.to_s
  groups = dep ? dep.groups.join(", ") : "default"

  # Fetch additional gem version information
  release_date, versions_available, changelog_url, homepage_url, versions_string, prerelease_string = fetch_gem_release_date_and_versions(spec.name, spec.version.to_s)

  line = "#{spec.name}|#{explicitly_required}|#{pinned_version}|#{groups}|#{spec.version}|#{release_date}|#{versions_available}|#{changelog_url}|#{homepage_url}|#{versions_string}|#{prerelease_string}"

  output += "#{line}\n"
  puts line
end

# Copy the generated output to the clipboard
copy_to_clipboard(output)

# Open a new Google Sheet for pasting the data
open_new_google_sheet
</code></pre><h2 id="walkthrough-video">Walkthrough Video</h2><p>Here's a walkthrough video on how I use the script to quickly triage all of the outdated gem dependencies in a Rails app, and how I handle ongoing maintenance updates every month or every few months. (Note: I ran this with an old branch to have more sample data, so this is not an accurate reflection of the gems currently used on that project 😄)</p>
<!--kg-card-begin: html-->
<div style="position: relative; padding-bottom: 50.847457627118644%; height: 0;"><iframe src="https://www.loom.com/embed/b1ffce184d64469ca92a3d9c9e1db008?sid=15ce4147-f943-4e65-8649-e360bcdffb9f" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe></div>
<!--kg-card-end: html-->
<h2 id="my-actual-gem-update-workflow">My Actual Gem Update Workflow</h2><p>Here's how I use this in practice:</p><ol><li>Run the script: <code>ruby bin/lib/outdated_gems_audit.rb</code></li><li>Paste into Google Sheets and set up pivot table</li><li>Filter to only explicitly required gems</li><li>Sort by release date to find my oldest dependencies</li><li>Color code based on priority:</li></ol><div class="kg-card kg-callout-card kg-callout-card-green"><div class="kg-callout-emoji">✅</div><div class="kg-callout-text">Green: completed updates</div></div><div class="kg-card kg-callout-card kg-callout-card-red"><div class="kg-callout-emoji">🛑</div><div class="kg-callout-text">Red: known blockers that need separate attention</div></div><div class="kg-card kg-callout-card kg-callout-card-yellow"><div class="kg-callout-emoji">⚠️</div><div class="kg-callout-text">Yellow: major releases that need their own PR</div></div><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">ℹ️</div><div class="kg-callout-text">Other labels ad hoc when I need to tag them as something else.</div></div><p>The actual pivot table ends up looking like this:</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://railsgrowth.com/content/images/2025/07/outdated-gems-audit-example-1.png" class="kg-image" alt="" loading="lazy" width="1219" height="876" srcset="https://railsgrowth.com/content/images/size/w600/2025/07/outdated-gems-audit-example-1.png 600w, https://railsgrowth.com/content/images/size/w1000/2025/07/outdated-gems-audit-example-1.png 1000w, https://railsgrowth.com/content/images/2025/07/outdated-gems-audit-example-1.png 1219w" sizes="(min-width: 1200px) 1200px"><figcaption><span style="white-space: pre-wrap;">Sample outdatedgems audit on a Rails 7.1 app example.</span></figcaption></figure><p>I typically tackle <code>dev</code>/<code>test</code> gems first since they're lower risk, then work through production dependencies. </p><p>Each gem gets its own commit in a single "gem updates" PR so I have clean CI logs if something breaks.</p><p>On larger updates that require code changes I will break off a standalone PR.</p><p>I prefix my Dependency PRs with the ⬆️ up arrow emoji to make them easier to glance through in Github history:</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://railsgrowth.com/content/images/2025/07/image-1.png" class="kg-image" alt="" loading="lazy" width="1469" height="922" srcset="https://railsgrowth.com/content/images/size/w600/2025/07/image-1.png 600w, https://railsgrowth.com/content/images/size/w1000/2025/07/image-1.png 1000w, https://railsgrowth.com/content/images/2025/07/image-1.png 1469w" sizes="(min-width: 1200px) 1200px"><figcaption><span style="white-space: pre-wrap;">Some recent dependency update and upgrade PRs in a Rails app.</span></figcaption></figure><p>The biggest win is being able to prioritize more quickly and see which gems might require more review work. That context makes all the difference when you're trying to methodically work through a dependency backlog without breaking your app (or an app that's new to you).</p><p>Another perk is accumulating some historical artifacts after you complete this process a number of times. I can reference back on update history here (though it's stored in GitHub history as well)</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://railsgrowth.com/content/images/2025/06/image-8.png" class="kg-image" alt="" loading="lazy" width="1192" height="950" srcset="https://railsgrowth.com/content/images/size/w600/2025/06/image-8.png 600w, https://railsgrowth.com/content/images/size/w1000/2025/06/image-8.png 1000w, https://railsgrowth.com/content/images/2025/06/image-8.png 1192w"><figcaption><span style="white-space: pre-wrap;">My numerous outdated gem audit pivot tables that have accumulated since I started using this process.</span></figcaption></figure><p>If you're dealing with a large list of outdated dependencies, you might want to build something similar. The script is straightforward - it's just hitting the RubyGems API and formatting the response in a way that's actually useful for decision-making.</p><hr><p>Acknowledgements: Many thanks to <a href="https://bsky.app/profile/jeremysmith.co?ref=railsgrowth.com">Jeremy Smith</a> for pushing me to improve on my original quickly scripted version that just scraped the Ruby Gems website and instead dive into the Ruby Gems API and build a proper integration.</p>
            <br/><br/>
            This article was originally published at <a href="https://railsgrowth.com/articles/custom-outdated-gems-script/">A Custom Outdated Gems Script to Speed Up Rails Dependency Upgrades [+ Video]</a>
        ]]></description>
        <link>https://railsgrowth.com/articles/custom-outdated-gems-script/</link>
        <guid isPermaLink="false">687ea4d7266a0fc053493bb9</guid>
        <dc:creator><![CDATA[ Kane Jamison ]]></dc:creator>
        <pubDate>Mon, 21 Jul 2025 22:32:35 +0000</pubDate>
    </item>
    <item>
        <title><![CDATA[ The Ideal CMS Structure For Rails ]]></title>
        <description><![CDATA[
            <p>On all publishing websites I have worked on, and many Rails sites, I've needed to publish static pages or collections of editorial content.</p><p>Sometimes these are basic app pages, like <u>/contact/</u> or <u>/support/</u>, in which case it's easy enough to make a <code>PagesController</code>, route each static page to a view file like <code>contact.html.erb</code>, and move on with your day.</p><p>But oftentimes these are more than basic pages. These could be a collection of help docs at <u>/docs/</u>. Or if we're serving the public website from our main Rails app, it might include <u>/blog/</u>, <u>/case-studies/</u>, <u>/resources/</u>, <u>/integrations/</u>, <u>/partners/</u>, and dozens of other content collections.</p><p>In the case of these collections, the most common practice is to start out creating a standard blog implementation, and then copy and pasting that build structure each time you need a new content collection.</p><p>But, that leads to tons of code duplication, and now each time we make an upgrade related to those content types, we have to extrapolate it over every model. And when the marketing or content team wants a new content directory, it's cumbersome to launch and requires database migrations and probably some refactoring.</p><p>Need to add a <code>featured_image</code>? You have to add it to every model.</p><p>Need a new field so you can set custom OpenGraph metadata? Add it to every model.</p><p>These are not the hardest problems to solve. You can share partials or build components, you can copy/paste code to a few places, but eventually it will probably lead to differences in how each of the models are handled and make it harder to manage.</p><p>I use what I think is a better approach: one shared posts model and many routed content collections:</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://railsgrowth.com/content/images/2025/06/ideal-rails-cms-structure-1.png" class="kg-image" alt="" loading="lazy" width="960" height="540" srcset="https://railsgrowth.com/content/images/size/w600/2025/06/ideal-rails-cms-structure-1.png 600w, https://railsgrowth.com/content/images/2025/06/ideal-rails-cms-structure-1.png 960w"><figcaption><span style="white-space: pre-wrap;">Simple overview of the role of the Posts model, and simplified version of how it gets routed across many collections of content.</span></figcaption></figure><h2 id="creating-a-shared-posts-model-serving-it-across-many-content-collections">Creating A Shared Posts Model &amp; Serving It Across Many Content Collections</h2><p>There is another pattern used by CMSs like WordPress and Ghost that I prefer however, and I'm going to walk through how I've built this in Rails.</p><div class="kg-card kg-callout-card kg-callout-card-white"><div class="kg-callout-emoji">🤫</div><div class="kg-callout-text">Some of the code samples below are mix and matched from a few repos so please forgive me for any variations you spot. There are a lot of strings that could be enums or relationships, and other smaller code improvements, so don't get too caught up on smaller specifics. I want to focus on the bigger pattern.</div></div><h3 id="everything-is-a-post">Everything is a <code>Post</code></h3><p>Every Post has a <code>post_type</code>, which might default to something like <code>blog</code>.</p><p>Posts also typically have a few other key columns, with an initial migration that looks like this:</p><pre><code class="language-ruby">create_table :posts do |t|
      t.string :title
      t.string :slug
      t.enum :status, null: false, enum_type: "post_status"
      t.json :content # assuming we're storing JSON content, depends on editor
      t.text :excerpt
      t.string :meta_title
      t.text :meta_description
      t.datetime :published_at
      t.integer :word_count
      t.string :post_type

      t.timestamps
    end</code></pre><p>This is a simple starting point. Depending on our site requirements, at any time we want to simplify our posts model, we can always break some of these columns off to separate tables. For example:</p><ul><li>We could move our meta_tag values to <code>post_metadata</code>,</li><li>We can store data specific to a single post_type in <code>custom_fields</code>, etc.</li><li>If we use ActionText we'd skip <code>t.json :content</code> and use <code>has_rich_text :content</code> in our model instead.</li><li>If we want to store content with revisions, we might set this up as a has_many on a post_content model instead, along with some logic for tracking the latest version. I can't find the original post, but I'm pretty sure this is roughly the model used by Basecamp when a user makes edits to an existing message or post</li></ul><h3 id="we-dont-publicly-display-anything-as-posts"><strong>We don't publicly display anything as <u>/posts/</u></strong></h3><p>While all of our content is stored as Posts, the Post model is entirely restricted to authorized users across Index, Show, Edit, etc.</p><p>This makes the routing simple, and the index at <u>/posts/</u> effectively becomes the dashboard for our CMS. That will typically look like this in our Routes:</p><figure class="kg-card kg-code-card"><pre><code class="language-ruby"># Admin-only Post editing routes
  authenticated :user, -&gt;(user) { user.admin? } do
    resources :posts
  end</code></pre><figcaption><p><span style="white-space: pre-wrap;">You can imagine another version where we check user.post_author? permissions instead of full admin permissions.</span></p></figcaption></figure><h3 id="posts-can-be-displayed-through-any-routed-collection-we-want">Posts can be displayed through any routed Collection we want</h3><p>What makes this all work is that Posts can be displayed through as many public content collections as we want.</p><p>This is similar to patterns like Custom Post Types for WordPress which store in wp_posts table, or Collections in Webflow. But I'm borrowing most heavily from Ghost, where collections are just Posts which are routed through a tagging system rather than a post_type column.</p><p>We do this by registering a controller, routes, and 2 view files. For example:</p><p><strong>Routes Example</strong></p><pre><code class="language-ruby"># Public Blog and Case Study routes (read-only)

resources :blog, controller: 'blog', path: 'blog', param: :slug, only: %i[index show]

resources :case_studies, controller: 'case_studies', path: 'case-studies', param: :slug, only: %i[index show]</code></pre><p><strong>Controller Example</strong></p><figure class="kg-card kg-code-card"><pre><code class="language-ruby">class BlogController &lt; ApplicationController
  def index
    @posts = filtered_posts.published
  end

  private

  def post_type
    'blog'
  end

  def filtered_posts
    Post.where(post_type: 'blog').where(status: 'published).where(indexable: true)
  end
end
</code></pre><figcaption><p><span style="white-space: pre-wrap;">We could also descend from PostsController in order to share some methods like filtered_posts or add a controller concern.</span></p></figcaption></figure><p> I've replicated <code>filtered_posts</code> here without pretty scope names in order to get a glimpse of what it's doing. In reality, we'd be setting up scopes in the Post model to make it simpler to call these:</p><pre><code class="language-ruby">scope :published, -&gt; { where(status: 'published') }
scope :indexable, -&gt; { where(indexable: true) }
scope :blog_posts, -&gt; { where(post_type: 'blog') }
scope :case_studies, -&gt; { where(post_type: 'case_study') }</code></pre><p><strong>View Files - Index &amp; Show</strong></p><p>Finally, we have 2 core view files for index and show.</p><p>For this example each page serves up basic page metadata and then passes all posts into <code>Posts::IndexComponent</code> or <code>Posts::ShowComponent</code> ViewComponents, which handle the actual formatting of posts.</p><figure class="kg-card kg-code-card"><pre><code class="language-erb">&lt;%= page_title("Blog") %&gt;
&lt;%= meta_tag :title, "Blog" %&gt;
&lt;%= meta_tag :description, "Here is my blog index meta description" %&gt;
&lt;%= meta_tag :url, blog_index_url %&gt;
&lt;%= canonical_url(blog_index_url) %&gt;

&lt;%= render Posts::IndexComponent.new(
  posts: @posts, 
  admin_links: current_user&amp;.admin?,
  title: "Blog",
  new_post_url: new_post_path(post_type: "blog"),
  post_type: "blog"
) %&gt;</code></pre><figcaption><p><u><span class="underline" style="white-space: pre-wrap;">/app/views/blog/index.html.erb</span></u></p></figcaption></figure><figure class="kg-card kg-code-card"><pre><code class="language-erb">&lt;%= page_title(@post.meta_title.presence || @post.title) %&gt;
&lt;%= meta_tag :title, @post.meta_title.presence || @post.title %&gt;
&lt;%= meta_tag :description, @post.meta_description.presence || @post.excerpt.presence || "Read #{@post.title} on the Site Name blog." %&gt;
&lt;%= meta_tag :url, blog_url(@post.slug) %&gt;
&lt;%= canonical_url(blog_url(@post.slug)) %&gt;
&lt;% if @post.featured_image.attached? %&gt;
  &lt;%= meta_tag :image, url_for(@post.featured_image) %&gt;
  &lt;%= meta_tag :image_alt, @post.title %&gt;
&lt;% end %&gt;

&lt;%= render Posts::ShowComponent.new(
  post: @post,
  admin_links: current_user&amp;.admin?,
  back_url: blog_index_path,
  back_label: "← Back to blog"
) %&gt;</code></pre><figcaption><p><u><span class="underline" style="white-space: pre-wrap;">app/views/blog/show.html.erb</span></u></p></figcaption></figure><h2 id="benefits">Benefits</h2><h3 id="centralized-feature-development">Centralized Feature Development</h3><p>This is the big one.</p><p>When you need to add a new feature like featured images, OpenGraph metadata, or reading time estimates, you add it once to the Posts model and immediately every content collection gets it. No more copying the same migration across Blog, CaseStudy, HelpDoc, and Resource models. No more forgetting to update one collection and having inconsistent functionality across your site.</p><p>Need to do a fancy callback to calculate word_count after the post is updated? Just do it once.</p><p>Need to generate url slug if blank upon creation but then validate it each time the user saves? Just do it once.</p><p>Need to do special handling for ActiveStorage attachments on Trix that are intended to be published publicly and need to serve cleaner URLs? Just do it once.</p><h3 id="consistent-seo-metadata-management">Consistent SEO &amp; Metadata Management</h3><p>Every post gets the same <code>meta_title</code>, <code>meta_description</code>, and SEO handling regardless of whether it's a blog post or a case study.</p><p>Your <a href="https://railsgrowth.com/articles/generate-nicer-sitemaps-with-sitemap_generator/" rel="noreferrer">sitemap generation</a> becomes trivial—just iterate individually through each collection of posts.</p><p>Structured data markup can be standardized with a single component that adapts based on <code>post_type</code>. You'll never have that awkward moment where your blog has perfect SEO but your case studies are missing Open Graph tags.</p><h3 id="simplified-content-administration">Simplified Content Administration</h3><p>Your CMS interface becomes incredibly clean. Instead of having separate admin sections for blog posts, case studies, help docs, and resources, you have one unified posts dashboard. You can filter by <code>post_type</code>, but all the core functionality—editing, publishing, scheduling—works the same way everywhere. Your content team learns one interface instead of four.</p><p>Here's an example I built recently for Posts#index that includes param filters on top of the page. These were part of a quick spike that took 3 hours for the entire Posts + collections build out, so design isn't perfected, but it looks way better than your average internal admin screen in regards to editing content:</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://railsgrowth.com/content/images/2025/06/image-4.png" class="kg-image" alt="" loading="lazy" width="1312" height="693" srcset="https://railsgrowth.com/content/images/size/w600/2025/06/image-4.png 600w, https://railsgrowth.com/content/images/size/w1000/2025/06/image-4.png 1000w, https://railsgrowth.com/content/images/2025/06/image-4.png 1312w" sizes="(min-width: 1200px) 1200px"><figcaption><span style="white-space: pre-wrap;">Simple Posts table which is filterable by Post Type and Published status.</span></figcaption></figure><p>And here's the Post#edit view:</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://railsgrowth.com/content/images/2025/06/image-5.png" class="kg-image" alt="" loading="lazy" width="1293" height="995" srcset="https://railsgrowth.com/content/images/size/w600/2025/06/image-5.png 600w, https://railsgrowth.com/content/images/size/w1000/2025/06/image-5.png 1000w, https://railsgrowth.com/content/images/2025/06/image-5.png 1293w" sizes="(min-width: 1200px) 1200px"><figcaption><span style="white-space: pre-wrap;">Simple Trix / ActionText content implementation and basic Tailwind formatting on Post#edit.</span></figcaption></figure><h3 id="reduced-code-duplication">Reduced Code Duplication</h3><p>Personally I think this is one area where this pattern shines. If I didn't do this approach, I would be racking my brain trying to figure out the best way to break up a lot of this stuff into concerns.</p><p>But overall this approach should cut down on a lot of find/replace type of commits.</p><h3 id="add-new-content-collection-in-15-minutes-with-no-migrations">Add New Content Collection in 15 Minutes With No Migrations</h3><p>Need to add a new content collection like <u>/testimonials/</u> or <u>/press-releases/</u>? It's literally just creating a new controller that inherits from PostsController, adding a route, and copy/pasting two view files to build off of an existing page template.</p><p>You can spin up a new content type in 15 minutes instead of spending a day building out new models, refactoring controller and model concerns, updating your admin config to match existing content models, etc.</p><h3 id="easy-search-and-filtering">Easy Search and Filtering</h3><p>Want to add site-wide search?</p><p>You're searching one table instead of trying to union across multiple models.</p><p>Want to show "related content" that might pull from different collections?</p><p>Easy—just query posts with different post_types.</p><p>Need to show recent activity across all content types in your admin dashboard? </p><p>☝️ One query.</p><hr><h2 id="common-concerns">Common Concerns</h2><p>Here are some common variations or concerns that show up and how I'd solve them:</p><h3 id="what-if-i-want-to-display-the-content-differently-on-each-posttype">What if I want to display the content differently on each post_type?</h3><p>This is a common one! What if we don't want to display author name on one of the posts types? What if we want to show table of contents on one post_type but not all?</p><p>Most of the time, this is easily solved by just altering the index or show view templates. Those views can share as much or as little styling as we want, so it's easy to create different layouts by post_type.</p><p>It's also very easy to create different page layouts, eg a layout column which is set to <code>default</code> but can be changed to <code>full_width</code> vs <code>slim</code>.</p><h3 id="what-if-i-need-to-store-different-data-types-for-a-posttype">What if I need to store different data types for a <code>post_type</code>?</h3><p>There are a lot of patterns to solve this and it opens up discussions about delegated types and other stuff that honestly, I'm not the most qualified Rails developer to weigh in on.</p><p>However the pattern that I would reach that is similar to WordPress is simply to create a pattern like <code>has_many custom_fields</code>, or perhaps a tag system. Then my custom_fields table would simply store a key value pattern for custom_fields, or for simpler labels I would use a tag.</p><p><strong>You want case studies to have an industry category that allows you to filter sub-collections on the case studies index page?</strong></p><p>Add a new <code>tag</code>, then add special routes for those tags or serve them up via url param, eg <code>/case-studies/?industry=saas</code>.</p><p><strong>You want case studies to store customer quotes that can be displayed dynamically in your case_study#show template?</strong></p><p>Add a <code>custom_field</code>.</p><p><strong>You want to store and entire section of custom HTML for a page that displays above the regular Post.content section?</strong></p><p>Same deal - format it with a special partial or ViewComponent, and put the dynamic stuff in a <code>custom_field</code>.</p><p>We could get into a lot of theoretical discussions about whether this is over-using a custom_field relationship and whether it makes it hard to validate and monitor the data being stored in custom_field, but how strict you are with that stuff is really up to you. If this is to support a few hundred pages of content on one app then all of this stuff will scale just fine.</p><h3 id="what-about-url-structure-and-seo-implications">What about URL structure and SEO implications?</h3><p>This is one of the best benefits in my opinion.</p><p>All of your URLs need to have custom slugs, which this system handles well. If you want to validate uniqueness by post_type so that you have have /blog/special-slug and /case-studies/special-slug you can do that just fine. The routing handles the collection-specific paths while the underlying data structure remains unified.</p><p>For our other settings, you get consistent metadata handling, easier sitemap generation, and unified canonicalization logic. The main thing to watch out for is if you ever need to change a post's post_type—you'll want to set up proper redirects from the old URL to the new collection.</p><h3 id="how-do-you-handle-different-editorial-workflows">How do you handle different editorial workflows?</h3><p>Not every content type needs the same publishing workflow. Your blog posts might go straight to published, but case studies might need approval from a sales manager first.</p><p>IMO that's usually better managed outside of your app in most small to mid-sized organizations. If you can manage a single repeatable content approval process across all content types, then you should be able to build that into this model the same as you would if all of the Posts were separate models.</p><h3 id="what-about-site-performance-with-large-datasets">What about site performance with large datasets?</h3><p>Once you're dealing with thousands of posts across multiple types, you might want to optimize this structure a little. I haven't reached this point on Rails but I definitely have on some of the WordPress publishing sites I manage.</p><p>Claude suggests:</p><ul><li>think about indexing, such as composite indexes on (post_type, status, published_at) and (post_type, slug) since those are your most common query patterns.</li><li>for really large datasets, you might consider moving to separate models and using concerns instead.</li><li>implement some basic caching—eg fragment caching for your index pages and Russian doll caching for individual posts.</li></ul><h3 id="how-do-you-manage-different-content-schemas">How do you manage different content schemas?</h3><p>Different post types often need different structured data markup. Blog posts need Article schema, case studies might need Service schema, and help docs might need HowTo schema.</p><p>I would handle this by adding schema logic to my ShowComponent that switches based on post_type. Most of the Schema is going to be generated off of Post Title, meta description, featured image, etc. Anything else can be pulled from custom fields.</p><h3 id="how-do-security-and-permissions-work-across-post-types">How do security and permissions work across post types?</h3><p>That's up to you - I always just piggyback whatever methods we have already implemented with Devise, CanCanCan, etc to check for (A) whether user is logged in and (B) whether they have admin privileges. In a more complex environment I would probably add a set of author privileges that is similar to admin but allows post editing access instead of admin privileges.</p><h3 id="what-happens-when-you-need-drastically-different-data-models">What happens when you need drastically different data models?</h3><p>There's definitely a breaking point where the custom_fields approach becomes unwieldy. If one of your post types needs 15 custom fields with complex validation rules, or if you need heavy relational data that doesn't fit the key-value pattern, it might be time to extract that post_type to its own model.</p><p>The nice thing is that you can do this gradually. Keep the unified model for most content types and extract the complex ones as needed. You're not locked into the pattern forever.</p>
            <br/><br/>
            This article was originally published at <a href="https://railsgrowth.com/articles/ideal-cms-structure-for-rails/">The Ideal CMS Structure For Rails</a>
        ]]></description>
        <link>https://railsgrowth.com/articles/ideal-cms-structure-for-rails/</link>
        <guid isPermaLink="false">6854661b266a0fc053493a70</guid>
        <dc:creator><![CDATA[ Kane Jamison ]]></dc:creator>
        <pubDate>Thu, 19 Jun 2025 21:49:29 +0000</pubDate>
    </item>
    <item>
        <title><![CDATA[ How to Generate a Custom Heroku Maintenance Mode Page ]]></title>
        <description><![CDATA[
            <p>I recently had to put our Content Harmony Heroku app into maintenance mode, and discovered <a href="https://devcenter.heroku.com/articles/maintenance-mode?ref=railsgrowth.com#customizing-your-maintenance-page">it is possible to set a custom URL as your Maintenance Mode</a> experience if users visit while the site is down.</p><p>This is a pretty straightforward feature, but delivers a nicer branded experience when you need to put a Heroku app into maintenance mode. We recently did this on Content Harmony both for Postgres upgrades, as well as while migrating our database to a new provider.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://railsgrowth.com/content/images/2025/06/image-3.png" class="kg-image" alt="" loading="lazy" width="1250" height="689" srcset="https://railsgrowth.com/content/images/size/w600/2025/06/image-3.png 600w, https://railsgrowth.com/content/images/size/w1000/2025/06/image-3.png 1000w, https://railsgrowth.com/content/images/2025/06/image-3.png 1250w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">The default Heroku Maintenance Mode page experience looks more like a site outage than scheduled downtime.</span></figcaption></figure><p>If you read the docs, you can set <em>any URL</em> as the page that is shown during maintenance mode, rather than this Heroku branded page.</p><p>So, we created a dedicated page on our marketing site (which runs on Ghost, not Rails and Heroku, so isn't affected by maintenance mode).</p><p>The page lives at <code>/maintenance-mode</code>, and we also added <code>&lt;meta name="<strong>robots</strong>" content="noindex"&gt;</code> to make it clear that the page should not be indexed by Google, etc.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://railsgrowth.com/content/images/2025/06/image.png" class="kg-image" alt="" loading="lazy" width="1667" height="1286" srcset="https://railsgrowth.com/content/images/size/w600/2025/06/image.png 600w, https://railsgrowth.com/content/images/size/w1000/2025/06/image.png 1000w, https://railsgrowth.com/content/images/size/w1600/2025/06/image.png 1600w, https://railsgrowth.com/content/images/2025/06/image.png 1667w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Our custom branded Maintenance Mode page confirms that the App is shut down on purpose and will be running again shortly.</span></figcaption></figure><p>From there, you just list that URL as a Config Var for <code>MAINTENANCE_PAGE_URL</code> in your Heroku settings for your app:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://railsgrowth.com/content/images/2025/06/image-1.png" class="kg-image" alt="" loading="lazy" width="1067" height="439" srcset="https://railsgrowth.com/content/images/size/w600/2025/06/image-1.png 600w, https://railsgrowth.com/content/images/size/w1000/2025/06/image-1.png 1000w, https://railsgrowth.com/content/images/2025/06/image-1.png 1067w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">ENV VAR settings for Heroku Maintenance Mode</span></figcaption></figure><p>The page will now appear any time you run or toggle the Maintenance Mode in your App's settings:</p><figure class="kg-card kg-image-card"><img src="https://railsgrowth.com/content/images/2025/06/image-2.png" class="kg-image" alt="" loading="lazy" width="883" height="165" srcset="https://railsgrowth.com/content/images/size/w600/2025/06/image-2.png 600w, https://railsgrowth.com/content/images/2025/06/image-2.png 883w" sizes="(min-width: 720px) 720px"></figure>
            <br/><br/>
            This article was originally published at <a href="https://railsgrowth.com/articles/custom-heroku-maintenance-mode-page/">How to Generate a Custom Heroku Maintenance Mode Page</a>
        ]]></description>
        <link>https://railsgrowth.com/articles/custom-heroku-maintenance-mode-page/</link>
        <guid isPermaLink="false">685458e5266a0fc053493a39</guid>
        <dc:creator><![CDATA[ Kane Jamison ]]></dc:creator>
        <pubDate>Thu, 19 Jun 2025 18:48:52 +0000</pubDate>
    </item>
    <item>
        <title><![CDATA[ How to generate nicer sitemaps with the sitemap_generator gem ]]></title>
        <description><![CDATA[
            <p>If you've ever set up a sitemap in Rails, you might have used <a href="https://rubygems.org/gems/sitemap_generator/?ref=railsgrowth.com">the sitemap_generator gem</a> (docs: <a href="https://github.com/kjvarga/sitemap_generator?ref=railsgrowth.com">/kjvarga/sitemap_generator</a>) to automatically generate an XML sitemap.</p><figure class="kg-card kg-image-card"><a href="https://rubygems.org/gems/sitemap_generator/?ref=railsgrowth.com"><img src="https://railsgrowth.com/content/images/2025/04/image-13.png" class="kg-image" alt="" loading="lazy" width="993" height="476" srcset="https://railsgrowth.com/content/images/size/w600/2025/04/image-13.png 600w, https://railsgrowth.com/content/images/2025/04/image-13.png 993w" sizes="(min-width: 720px) 720px"></a></figure><p>On a recent client project we were generating a single sitemap for a variety of content types, including static pages, blog posts, and 2-3 custom content directories.</p><p>You can imagine a basic <code>config/sitemap.rb</code> setup like this from the <code>sitemap_generator</code> Readme:</p><pre><code class="language-ruby">SitemapGenerator::Sitemap.default_host = 'http://example.com'

SitemapGenerator::Sitemap.create do
  add '/about', :changefreq =&gt; 'daily', :priority =&gt; 0.9
  add '/contact_us', :changefreq =&gt; 'weekly'

  Article.published.each do |article|
    add article_path(article), priority: 0.64, changefreq: 'weekly'
  end
end

SitemapGenerator::Sitemap.ping_search_engines # Not needed if you use the rake tasks</code></pre><p>I want to walk through a few changes I made to improve this sitemap and why.</p><h2 id="break-into-sitemap-index-multiple-sitemaps">Break Into Sitemap Index + Multiple Sitemaps</h2><p>I prefer to generate multiple individual sitemaps for different content directories.</p><p>That's because when you submit those sitemaps to Google Search Console, it's much easier to analyze URL performance by content type:</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://railsgrowth.com/content/images/2025/04/image-18.png" class="kg-image" alt="" loading="lazy" width="890" height="289" srcset="https://railsgrowth.com/content/images/size/w600/2025/04/image-18.png 600w, https://railsgrowth.com/content/images/2025/04/image-18.png 890w"><figcaption><span style="white-space: pre-wrap;">An example of a Sitemap Index file with 3 additional submitted sitemaps for Posts, Pages, and Tags (which are important index pages for this domain)</span></figcaption></figure><p>Now, after submitting each of those sitemaps into Search Console, I can get more granular breakdowns of which content is being indexed and which isn't, which helps me make decisions around internal linking, identify why pages aren't ranking, etc.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://railsgrowth.com/content/images/2025/04/image-12.png" class="kg-image" alt="" loading="lazy" width="1133" height="1092" srcset="https://railsgrowth.com/content/images/size/w600/2025/04/image-12.png 600w, https://railsgrowth.com/content/images/size/w1000/2025/04/image-12.png 1000w, https://railsgrowth.com/content/images/2025/04/image-12.png 1133w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">An example of the Page Indexing report specifically for my /sitemap-posts.xml file, which allows me to dive deeper into pages that are "Crawled - currently not indexed" and other vague Google index statuses.</span></figcaption></figure><h3 id="break-sitemap-into-separate-files-using-groupfilename-xyz">Break Sitemap into separate files using <code>group(filename: xyz)</code></h3><p>Sitemap_generator has a nice built-in group feature to handle this:</p><pre><code class="language-ruby"># Set the root domain
SitemapGenerator::Sitemap.default_host = "https://www.example.com"

SitemapGenerator::Sitemap.create do
  group(filename: :sitemap_pages) do
    add '/about', :changefreq =&gt; 'daily', :priority =&gt; 0.9
    add '/contact_us', :changefreq =&gt; 'weekly'
  end
  
  group(filename: :sitemap_articles) do
    Article.published.each do |article|
      add article_path(article), priority: 0.64, changefreq: 'weekly'
    end
  end
end
</code></pre><h3 id="add-to-root-back-to-the-pages-group-using-into-separate-files-using-includeroot-true">Add to root back to the pages group using into separate files using <code>include_root: true</code></h3><p>When you use the group feature, <code>sitemap_generator</code> stops adding your homepage to a sitemap. You can add it back using <code>include_root: true</code> in the pages group we established above:</p><pre><code class="language-ruby">group(filename: :pages, include_root: true) do
    add '/about', :changefreq =&gt; 'daily', :priority =&gt; 0.9
    add '/contact_us', :changefreq =&gt; 'weekly'
  end</code></pre><h3 id="set-createindex-true-to-generate-a-sitemapindex-file">Set <code>create_index</code> = <code>true</code> to generate a sitemap_index file</h3><p>Our next step is to tell <code>sitemap_generator</code> to build a sitemap_index file. A sitemap_index file is simply an index of all of your grouped sitemaps.</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://railsgrowth.com/content/images/2025/04/image-11-1.png" class="kg-image" alt="" loading="lazy" width="909" height="401" srcset="https://railsgrowth.com/content/images/size/w600/2025/04/image-11-1.png 600w, https://railsgrowth.com/content/images/2025/04/image-11-1.png 909w"><figcaption><span style="white-space: pre-wrap;">Here's the Ghost-generated current sitemap.xml index file for RailsGrowth, broken apart by pages, posts, authors, and tags.</span></figcaption></figure><p>It is a nice feature because we can add a single sitemap reference to robots.txt and bots will find all of the sitemaps we might generate in the future:</p><pre><code class="language-txt"># robots.txt

Sitemap: https://www.example.com/sitemaps/sitemap_index.xml.gz</code></pre><p>No matter how many new groups we add to our config/sitemap.rb, they'll automatically be referenced by this robots.txt entry. For big sites that is huge because it will automatically include sitemaps that get paginated or broken up after exceeding the maximum URL count per sitemap file.</p><p>We can generate that using the following before our create command:</p><pre><code class="language-ruby"># Create a sitemap index which points to all sitemap files
SitemapGenerator::Sitemap.create_index = true</code></pre><p>Many CMSs or SEO plugins will name this file sitemap_index.xml by default. Sitemap_generator doesn't do that, it just names the file sitemap.xml, so if we'd like, we can override the index filename in the create command to make the filename more explicit.</p><figure class="kg-card kg-code-card"><pre><code class="language-ruby"># Set the root domain
SitemapGenerator::Sitemap.default_host = "https://www.example.com"

# Create a sitemap index which points to all sitemap files
SitemapGenerator::Sitemap.create_index = true

SitemapGenerator::Sitemap.create(filename: 'sitemap_index') do
  group(filename: :sitemap_pages, include_root: true) do
    add '/about', :changefreq =&gt; 'daily', :priority =&gt; 0.9
    add '/contact_us', :changefreq =&gt; 'weekly'
  end
  
  group(filename: :sitemap_articles) do
    Article.published.each do |article|
      add article_path(article), priority: 0.64, changefreq: 'weekly'
    end
  end
end
</code></pre><figcaption><p><span style="white-space: pre-wrap;">Our current sitemap, which outputs a renamed sitemap_index.xml.gz file, sitemap_pages.xml.gz, and sitemap_articles.xml.gz</span></p></figcaption></figure><h2 id="generate-both-xml-and-xmlgz-sitemap-versions">Generate both .xml and .xml.gz sitemap versions</h2><p>By default, <code>sitemap_generator</code> will generate compressed sitemaps, meaning a gzipped .xml.gz file.</p><p>Those are smaller in filesize and better for serving to bots as well as for submitting to Google Search Console.</p><p>But - they don't render in the browser, they download when you load them. And I don't know about you, but Microsoft Word is really excited to be the default app on my Mac when I unzip them and try to load the .xml file locally.</p><figure class="kg-card kg-gallery-card kg-width-wide kg-card-hascaption"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://railsgrowth.com/content/images/2025/04/image-16-1.png" width="302" height="329" loading="lazy" alt=""></div><div class="kg-gallery-image"><img src="https://railsgrowth.com/content/images/2025/04/image-15-2.png" width="299" height="277" loading="lazy" alt=""></div><div class="kg-gallery-image"><img src="https://railsgrowth.com/content/images/2025/04/image-14-1.png" width="894" height="577" loading="lazy" alt="" srcset="https://railsgrowth.com/content/images/size/w600/2025/04/image-14-1.png 600w, https://railsgrowth.com/content/images/2025/04/image-14-1.png 894w" sizes="(min-width: 720px) 720px"></div></div></div><figcaption><p><span style="white-space: pre-wrap;">The fun journey every single time Word opens up an XML file. I should probably set a different default app for those files.</span></p></figcaption></figure><p>So instead, it's nice to generate an uncompressed .xml file as well which can be viewed in the browser. This makes it much easier for developers to load the .xml version in local, staging, and production to see which how URLs are being loaded in each sitemap file.</p><p>Thankfully, we can do this pretty easily by calling the <code>:compress</code> option, and running <code>SitemapGenerator::Sitemap.create</code> twice:</p><figure class="kg-card kg-code-card"><pre><code class="language-ruby"># Set the root domain
SitemapGenerator::Sitemap.default_host = "https://www.example.com"

# Create a sitemap index which points to all sitemap files
SitemapGenerator::Sitemap.create_index = true

# Generate both compressed and uncompressed versions
[true, false].each do |compress_value|
  SitemapGenerator::Sitemap.compress = compress_value

  SitemapGenerator::Sitemap.create(filename: 'sitemap_index') do
    group(filename: :sitemap_pages, include_root: true) do
      add '/about', :changefreq =&gt; 'daily', :priority =&gt; 0.9
      add '/contact_us', :changefreq =&gt; 'weekly'
    end
    
    group(filename: :sitemap_articles) do
      Article.published.each do |article|
        add article_path(article), priority: 0.64, changefreq: 'weekly'
      end
    end
  end
end</code></pre><figcaption><p><span style="white-space: pre-wrap;">This change allows us to generate both .xml and .xml.gz sitemaps.</span></p></figcaption></figure><p>In this example we're iterating through the <code>[true, false].each do |compress_value|</code> array, using it to set <code>SitemapGenerator::Sitemap.compress = compress_value</code> before running <code>.create</code>, which ends up generating .xml and .xml.gz copies of every sitemap file.</p><p>Here's an example of what our output looks like for a site with 4 groups, a sitemap index, and both .xml and .xml.gz output:</p><pre><code class="language-bash">In '/Users/kanejamison/Github/ltbweb/public/':
+ sitemap_pages.xml.gz                   26 links / 591 Bytes
+ sitemap_blog.xml.gz                    22 links / 911 Bytes
+ sitemap_resources.xml.gz             2713 links /   40.2 KB
+ sitemap_services.xml.gz                19 links / 453 Bytes
+ sitemap_sitemap_index.xml.gz         4 sitemaps / 263 Bytes

Sitemap stats: 2,780 links / 4 sitemaps / 0m00s

In '/Users/kanejamison/Github/ltbweb/public/':
+ sitemap_pages.xml                      26 links /   4.71 KB
+ sitemap_blog.xml                       22 links /   5.14 KB
+ sitemap_resources.xml                2713 links /    510 KB
+ sitemap_services.xml                   19 links /   3.97 KB
+ sitemap_sitemap_index.xml            4 sitemaps / 776 Bytes

Sitemap stats: 2,780 links / 4 sitemaps / 0m00s</code></pre><p>Take note at the filesize difference on the gzipped versions. It's certainly nice to point bots to those versions, especially with how much AI bot activity is happening nowadays.</p><h2 id="if-youre-on-heroku-youre-not-done">If you're on Heroku, you're not done.</h2><p>This part gets a little confusing.</p><p>If you're on a host that allows you to build static xml files dynamically on the server, you might be good to go. You'll need to set up a cron job for rake sitemap:refresh, after which, you'll have a fresh sitemap however often you like.</p><p>But if you're on Heroku, you can't build static assets on the server and keep them there. If I understand correctly they'll let you build the files in /tmp/, but you can't just drop them in /public/, you'll need to set up a storage adapter, which is heavily documented on sitemap_generator's docs.</p><blockquote>Sometimes it is desirable to host your sitemap files on a remote server, and point robots and search engines to the remote files. For example, if you are using a host like Heroku, which doesn't allow writing to the local filesystem. You still require&nbsp;some&nbsp;write access, because the sitemap files need to be written out before uploading. So generally a host will give you write access to a temporary directory. On Heroku this is&nbsp;tmp/&nbsp;within your application directory.</blockquote><p>When you switch to using a storage adapter, then a lot of our setup above becomes a problem.</p><p>As sitemap_generator explains in their section on storage adapters:</p><blockquote>Note that SitemapGenerator will automatically turn off&nbsp;include_index&nbsp;in this case because the&nbsp;sitemaps_host&nbsp;does not match the&nbsp;default_host. The link to the sitemap index file that would otherwise be included would point to a different host than the rest of the links in the sitemap, something that the sitemap rules forbid.</blockquote><p>So, that's a problem - if we can't store our sitemap on the server, and we can't generate a sitemap index, then we're left with a couple of options.</p><p>One is that we can hardcode our sitemap file references in robots.txt. On a smaller site that might be feasible, but if we're creating multiple sitemaps of a certain type, eg sitemap_posts.xml, sitemap_posts2.xml, then it would require us to manually keep track of how many sitemaps are getting created and update that robots.txt entry manually.</p><p><em>[post in progress until this note is gone, still finishing this end section]</em></p><p><strong>STILL TO COVER:</strong></p><ul><li>REVERSE PROXY / NGINX to serve files from our domain URLs.</li><li>Or, whether redirects will work.</li><li>Whether we can override the sitemap_index disabled feature if we're willing to handle the URL controls in one of those ways.</li></ul>
            <br/><br/>
            This article was originally published at <a href="https://railsgrowth.com/articles/generate-nicer-sitemaps-with-sitemap_generator/">How to generate nicer sitemaps with the sitemap_generator gem</a>
        ]]></description>
        <link>https://railsgrowth.com/articles/generate-nicer-sitemaps-with-sitemap_generator/</link>
        <guid isPermaLink="false">67f0554899bd5118f37990b5</guid>
        <dc:creator><![CDATA[ Kane Jamison ]]></dc:creator>
        <pubDate>Thu, 03 Apr 2025 19:21:38 +0000</pubDate>
    </item>
    <item>
        <title><![CDATA[ How to Test SEO Meta Tag Requirements in RSpec ]]></title>
        <description><![CDATA[
            <p>Recently I have been testing various SEO requirements in a Rails app that uses Rspec.</p><p>After copy and pasting <code>expect(page).to have_css "meta[name='robots'][content='noindex']", visible: :hidden</code> for the 5th time it was clear I needed to build out some helper commands that were simpler and more clear.</p><p>I came up with the following SEO Helper that lives at <code>spec/support/seo_helper.rb</code>.</p><p>These tests are primary checking for meta tags. So if you want to verify other requirements like presence in sitemaps that isn't currently covered by this helper.</p><p>If you use this in your own app, you'll need to possibly modify values. For instance when we set meta robots to noindex, if you only set a value of <code>noindex</code> as opposed to <code>noindex, follow</code>, you might need to change that value.</p><pre><code class="language-ruby"># spec/support/seo_helper.rb

module SEOHelper
  # Check if the page has indexable meta robots tag
  def expect_that_page_is_indexable
    expect(page).to have_css "meta[name='robots'][content='index, follow']", visible: :hidden
  end

  # Check if the page has non-indexable meta robots tag
  def expect_that_page_is_noindexed
    expect(page).to have_css "meta[name='robots'][content='noindex, follow']", visible: :hidden
  end

  # Check that canonical URL path exactly matches the expected path
  # Include the full path after domain
  # eg expect_canonical_url_path_matches("/about-us")
  def expect_canonical_url_path_matches(expected_path)
    # Get the actual canonical URL from the page
    canonical_element = find("link[rel='canonical']", visible: :hidden)
    actual_url = canonical_element[:href]

    # Parse the URL to get just the path
    actual_uri = URI.parse(actual_url)
    actual_path = actual_uri.path

    # Ensure expected_path starts with a slash
    expected_path = "/#{expected_path}" unless expected_path.start_with?('/')

    # Check if the path exactly matches the expected path
    expect(actual_path).to eq(expected_path),
                           "Expected canonical path to be '#{expected_path}', but got '#{actual_path}'"
  end

  # Check for specific meta description
  def expect_meta_description(expected_content = nil)
    if expected_content
      expect(page).to have_css "meta[name='description'][content='#{expected_content}']", visible: :hidden
    else
      expect(page).to have_css "meta[name='description']", visible: :hidden
    end
  end

  # Check for exact page title
  def expect_page_title_matches(expected_title)
    expect(page).to have_title expected_title
  end

  # Check that page title contains the expected text
  def expect_page_title_contains(expected_text)
    expect(page.title).to include(expected_text),
                          "Expected page title to contain '#{expected_text}', but got '#{page.title}'"
  end
end

RSpec.configure do |config|
  config.include SEOHelper
end
</code></pre><p>Here's an example of how we use this:</p><pre><code class="language-ruby">require 'rails_helper'

# Note - most tests in this file are confirming that landing pages on the website are renderable, indexable, and that the canonical tag is generated as expected.
# If you need to test specific functionality on these pages it probably deserves a separate spec.
# Pages are sorted alphabetical by URL.

RSpec.describe 'Visit Core Pages', type: :feature do
  it "renders the home page as expected" do
    visit "/"
    expect(page).to have_text("Here is a paragraph that would only appear on the Home page and definitely not in our footer or other pages of the site that would cause false positives.")
    expect_that_page_is_indexable
    expect_canonical_url_path_matches("/")
  end

  it "renders the /about-us page as expected" do
    visit "/about-us"
    expect(page).to have_text("Here is a paragraph that would only appear on the About Us page and definitely not in our footer or other pages of the site that would cause false positives.")
    expect_that_page_is_indexable
    expect_canonical_url_path_matches("/about-us")
  end

   # etc for other critical pages

end
</code></pre><p>I don't need to check the following types of values currently, but here are some likely ways to expand this helper in the future:</p><ol><li>Testing for Open Graph meta tags (eg og:url matches canonical tag)</li><li>Verifying redirect paths are 301 and "single hop"</li><li>Verifying JSON-LD structured data presence and validity</li><li>Checking hreflang tags for multilingual sites</li><li>Validating meta viewport settings</li><li>Testing for proper heading hierarchy (H1, H2, etc.)</li><li>Checking for rel="next" and rel="prev" pagination tags</li><li>Verifying image alt attributes on critical images</li><li>Checking for sitemap.xml references in robots.txt</li><li>Validating proper URL trailing slash consistency (this is partially inherent in our canonical path checker, however you might need to also render the page with a trailing slash and confirmed canonical does not include it)</li><li>Checking for rel="nofollow" on specific links</li><li>Testing breadcrumb markup</li><li>Verifying schema.org markup for specific page types</li><li>Testing for proper canonical links on taxonomy pages, paginated pages, pages with parameters, etc.</li><li>Checking for proper sitemap formatting</li><li>Testing for valid RSS feeds</li></ol>
            <br/><br/>
            This article was originally published at <a href="https://railsgrowth.com/articles/testing-seo-meta-tags-in-rspec/">How to Test SEO Meta Tag Requirements in RSpec</a>
        ]]></description>
        <link>https://railsgrowth.com/articles/testing-seo-meta-tags-in-rspec/</link>
        <guid isPermaLink="false">67f0554899bd5118f37990af</guid>
        <dc:creator><![CDATA[ Kane Jamison ]]></dc:creator>
        <pubDate>Thu, 03 Apr 2025 05:04:11 +0000</pubDate>
    </item>

</channel>
</rss>
