<?xml version="1.0" encoding="UTF-8"?><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" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Beck Moulton on Medium]]></title>
        <description><![CDATA[Stories by Beck Moulton on Medium]]></description>
        <link>https://medium.com/@beckmoulton?source=rss-f6287c0ef80d------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*CWM5HgNdb9zxH5CXjoXgfw.jpeg</url>
            <title>Stories by Beck Moulton on Medium</title>
            <link>https://medium.com/@beckmoulton?source=rss-f6287c0ef80d------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 08:47:55 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@beckmoulton/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Image Optimization for Modern Web Applications: Formats, CDNs, Automation]]></title>
            <link>https://beckmoulton.medium.com/image-optimization-for-modern-web-applications-formats-cdns-automation-1d690b8fb2ba?source=rss-f6287c0ef80d------2</link>
            <guid isPermaLink="false">https://medium.com/p/1d690b8fb2ba</guid>
            <category><![CDATA[saas]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[cdn]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Beck Moulton]]></dc:creator>
            <pubDate>Wed, 10 Jun 2026 04:22:17 GMT</pubDate>
            <atom:updated>2026-06-10T04:22:17.837Z</atom:updated>
            <content:encoded><![CDATA[<p>Images account for 50–70% of a typical web page’s weight. Optimizing them properly — choosing the right format, generating responsive sizes, automating compression, and serving from a CDN — is the single highest-impact performance optimization available. This guide covers the complete image optimization pipeline used at <a href="https://tanstackship.com/">tanstackship.com</a>, from development workflow to production delivery.</p><h3>Image Format Comparison</h3><p>FormatCompressionTransparencyAnimationBrowser SupportFile Size vs PNGJPEGLossy❌❌100%50–70% smallerPNGLossless✅❌100%Baseline<strong>WebPLossy + Lossless✅✅97%25–35% smallerAVIFLossy + Lossless✅✅93%50–60% smaller</strong>JPEG XLLossy + Lossless✅❌Growing40–50% smaller</p><p><strong>Recommendation in 2026</strong>: Serve AVIF as the primary format with WebP fallback. Most modern browsers (93%) support AVIF, and WebP covers the remaining.</p><h3>Generating Optimized Images</h3><h3>Automated Build Pipeline</h3><pre>// scripts/optimize-images.ts<br>// Run during build process<br>import sharp from &quot;sharp&quot;<br>import { readdir, readFile, writeFile } from &quot;fs/promises&quot;<br>import path from &quot;path&quot;</pre><pre>interface ImageConfig {<br>  source: string       // Original image path<br>  outputDir: string    // Base output directory<br>  formats: string[]    // [&#39;avif&#39;, &#39;webp&#39;, &#39;jpeg&#39;]<br>  sizes: number[]      // [640, 1200, 1920]<br>  quality: number      // 80<br>}</pre><pre>async function optimizeImage(config: ImageConfig) {<br>  const inputBuffer = await readFile(config.source)<br>  const filename = path.parse(config.source).name</pre><pre>  for (const format of config.formats) {<br>    for (const width of config.sizes) {<br>      const outputPath = path.join(<br>        config.outputDir,<br>        format,<br>        `${filename}-${width}w.${format}`<br>      )</pre><pre>      await sharp(inputBuffer)<br>        .resize(width, undefined, { withoutEnlargement: true })<br>        .toFormat(format as any, { quality: config.quality })<br>        .toFile(outputPath)</pre><pre>      console.log(`Created: ${outputPath}`)<br>    }<br>  }<br>}</pre><h3>Responsive Images with srcset</h3><pre>// components/OptimizedImage.tsx<br>interface OptimizedImageProps {<br>  src: string  // e.g., &quot;/images/dashboard&quot;<br>  alt: string<br>  widths?: number[]<br>  sizes?: string<br>  priority?: boolean<br>}</pre><pre>function OptimizedImage({<br>  src,<br>  alt,<br>  widths = [640, 1200, 1920],<br>  sizes = &quot;(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw&quot;,<br>  priority = false,<br>}: OptimizedImageProps) {<br>  const formats = [&quot;avif&quot;, &quot;webp&quot;, &quot;jpeg&quot;]<br>  const suffix = priority ? &quot;fetchPriority=&#39;high&#39;&quot; : &quot;loading=&#39;lazy&#39;&quot;</pre><pre>  return (<br>    &lt;picture&gt;<br>      {/* Modern format first */}<br>      {formats.map((format) =&gt; (<br>        &lt;source<br>          key={format}<br>          type={`image/${format === &quot;jpeg&quot; ? &quot;jpeg&quot; : format}`}<br>          srcSet={widths<br>            .map((w) =&gt; `${src}-${w}w.${format} ${w}w`)<br>            .join(&quot;, &quot;)<br>          }<br>          sizes={sizes}<br>        /&gt;<br>      ))}<br>      {/* Fallback */}<br>      &lt;img<br>        src={`${src}-1200w.jpeg`}<br>        srcSet={widths<br>          .map((w) =&gt; `${src}-${w}w.jpeg ${w}w`)<br>          .join(&quot;, &quot;)<br>        }<br>        alt={alt}<br>        width={1200}<br>        height={675}<br>        decoding=&quot;async&quot;<br>        {...(priority ? { fetchPriority: &quot;high&quot; as const } : { loading: &quot;lazy&quot; as const })}<br>      /&gt;<br>    &lt;/picture&gt;<br>  )<br>}</pre><h3>CDN-Based Image Optimization</h3><p>Using Cloudflare Image Resizing eliminates the need for build-time generation:</p><pre>// Skip build-time optimization — transform on the fly<br>function CdnImage({ src, width, alt, priority = false }: CdnImageProps) {<br>  const baseUrl = &quot;https://tanstackship.com/cdn-cgi/image&quot;</pre><pre>  // Generate transform URLs<br>  const formats = [<br>    { format: &quot;avif&quot;, url: `${baseUrl}/width=${width},format=avif,quality=80/${src}` },<br>    { format: &quot;webp&quot;, url: `${baseUrl}/width=${width},format=webp,quality=80/${src}` },<br>    { format: &quot;jpeg&quot;, url: `${baseUrl}/width=${width},format=jpeg,quality=85/${src}` },<br>  ]</pre><pre>  return (<br>    &lt;picture&gt;<br>      {formats.map(({ format, url }) =&gt; (<br>        &lt;source key={format} type={`image/${format}`} srcSet={url} /&gt;<br>      ))}<br>      &lt;img<br>        src={`${baseUrl}/width=${width},format=jpeg,quality=85/${src}`}<br>        alt={alt}<br>        width={width}<br>        loading={priority ? &quot;eager&quot; : &quot;lazy&quot;}<br>        fetchPriority={priority ? &quot;high&quot; : &quot;auto&quot;}<br>      /&gt;<br>    &lt;/picture&gt;<br>  )<br>}</pre><h3>Image Resizing Options</h3><pre>// Available CDN parameters — build helper<br>interface ImageTransformOptions {<br>  width?: number       // 1-4096<br>  height?: number      // 1-4096<br>  fit?: &quot;scale-down&quot; | &quot;contain&quot; | &quot;cover&quot; | &quot;crop&quot; | &quot;pad&quot;<br>  quality?: number     // 1-100<br>  format?: &quot;webp&quot; | &quot;avif&quot; | &quot;jpeg&quot; | &quot;png&quot;<br>  sharpen?: number     // 0-10<br>  blur?: number        // 0-250<br>  brightness?: number  // -100 to 100<br>  contrast?: number    // -100 to 100<br>  gamma?: number       // 0.1-9.99<br>}</pre><pre>function buildImageUrl(basePath: string, opts: ImageTransformOptions): string {<br>  const params = Object.entries(opts)<br>    .filter(([, v]) =&gt; v !== undefined)<br>    .map(([k, v]) =&gt; `${k}=${v}`)<br>    .join(&quot;,&quot;)</pre><pre>  return `/cdn-cgi/image/${params}/${basePath}`<br>}</pre><h3>Lazy Loading Strategy</h3><pre>// Intelligent lazy loading based on viewport position<br>function LazyImage({ src, alt, threshold = 0.1 }: LazyImageProps) {<br>  const imgRef = useRef&lt;HTMLImageElement&gt;(null)<br>  const [loaded, setLoaded] = useState(false)<br>  const [inView, setInView] = useState(false)</pre><pre>  useEffect(() =&gt; {<br>    const observer = new IntersectionObserver(<br>      ([entry]) =&gt; {<br>        if (entry.isIntersecting) {<br>          setInView(true)<br>          observer.disconnect()<br>        }<br>      },<br>      { rootMargin: &quot;200px&quot;, threshold }<br>    )</pre><pre>    if (imgRef.current) observer.observe(imgRef.current)<br>    return () =&gt; observer.disconnect()<br>  }, [threshold])</pre><pre>  return (<br>    &lt;div ref={imgRef} className=&quot;image-wrapper&quot; style={{ minHeight: &quot;200px&quot; }}&gt;<br>      {inView ? (<br>        &lt;img<br>          src={src}<br>          alt={alt}<br>          onLoad={() =&gt; setLoaded(true)}<br>          className={loaded ? &quot;loaded&quot; : &quot;loading&quot;}<br>          loading=&quot;lazy&quot;<br>        /&gt;<br>      ) : (<br>        &lt;div className=&quot;image-skeleton&quot; /&gt;<br>      )}<br>    &lt;/div&gt;<br>  )<br>}</pre><p>Lazy Loading MethodProsConsNative loading=&quot;lazy&quot;Zero JS, browser-nativeLess control over thresholdIntersection ObserverPrecise controlRequires JSBelow-fold onlySimpleManual per-imagePriority hintsWorks with native lazyLimited browser support</p><h3>Automated Image Pipeline</h3><pre>// server/images/pipeline.ts<br>export const uploadImage = createServerFn({ method: &quot;POST&quot; }).handler(<br>  async ({ request, context }) =&gt; {<br>    const formData = await request.formData()<br>    const file = formData.get(&quot;image&quot;) as File</pre><pre>    // Validation<br>    if (!file.type.startsWith(&quot;image/&quot;)) throw new Error(&quot;Invalid file type&quot;)<br>    if (file.size &gt; 10 * 1024 * 1024) throw new Error(&quot;File too large&quot;)</pre><pre>    const buffer = await file.arrayBuffer()<br>    const id = crypto.randomUUID()<br>    const key = `images/${id}`</pre><pre>    // Store original<br>    await context.env.MY_BUCKET.put(`${key}/original`, buffer, {<br>      httpMetadata: { contentType: file.type },<br>    })</pre><pre>    // Generate thumbnails via Image Resizing<br>    const sizes = [<br>      { width: 150, suffix: &quot;thumb&quot; },<br>      { width: 400, suffix: &quot;small&quot; },<br>      { width: 800, suffix: &quot;medium&quot; },<br>      { width: 1600, suffix: &quot;large&quot; },<br>    ]</pre><pre>    for (const { width, suffix } of sizes) {<br>      // Store reference — actual resize happens on CDN<br>      await context.env.MY_BUCKET.put(`${key}/${suffix}.webp`, buffer)<br>    }</pre><pre>    return {<br>      id,<br>      urls: {<br>        original: `https://images.tanstackship.com/${key}/original`,<br>        thumb: `https://images.tanstackship.com/cdn-cgi/image/width=150/${key}/original`,<br>        small: `https://images.tanstackship.com/cdn-cgi/image/width=400/${key}/original`,<br>        medium: `https://images.tanstackship.com/cdn-cgi/image/width=800/${key}/original`,<br>        large: `https://images.tanstackship.com/cdn-cgi/image/width=1600/${key}/original`,<br>      },<br>    }<br>  }<br>)</pre><h3>Performance Impact</h3><p>Image TypeBeforeAfter (AVIF + Responsive)SavingsHero image (1920px)1.2 MB (JPEG)180 KB (AVIF)85%Blog thumbnail (400px)180 KB (JPEG)35 KB (AVIF)81%Product screenshot (1200px)850 KB (PNG)120 KB (WebP)86%Icon set (32px SVG)25 KB25 KB (already optimized) — <strong>Total page weight2.5 MB380 KB85% reduction</strong></p><h3>Image CDN Cost Comparison</h3><p>ProviderBandwidthTransformationsStorageCloudflare ImagesIncluded with Workers$0.50/1000 unique$5/1000 imagesCloudflare Image ResizingFree with Pro Bizper-requestN/A (via R2)imgix$0.10/GBIncluded$0.20/GBCloudinaryFree tier: 25GB CDN25GB transformations25GB storageAWS CloudFront + Lambda@Edge$0.085/GBLambda costS3 pricing</p><h3>Image Optimization Checklist</h3><ul><li>[ ] AVIF used as primary format, WebP as fallback</li><li>[ ] Responsive images with srcset and sizes attributes</li><li>[ ] Lazy loading for all images below the fold</li><li>[ ] Explicit width and height to prevent CLS</li><li>[ ] CDN-based image transformation for dynamic sizing</li><li>[ ] Automated build pipeline for static images</li><li>[ ] Image quality set to 80–85 (diminishing returns after this)</li><li>[ ] SVG used for icons and illustrations where possible</li><li>[ ] Preload LCP images with fetchpriority=”high”</li><li>[ ] Monitor Lighthouse image optimization score weekly</li></ul><h3>Conclusion</h3><p>Image optimization is not optional — it is the single highest-impact performance optimization for most web pages. The modern approach combines three strategies:</p><ol><li><strong>Format selection</strong>: AVIF for quality, WebP for compatibility</li><li><strong>Responsive delivery</strong>: Different sizes for different viewports</li><li><strong>CDN optimization</strong>: Transform on the edge, cache at the edge</li></ol><p>With Cloudflare Image Resizing, you can skip the build-time optimization pipeline entirely and let the CDN handle format selection, resizing, and compression dynamically. This simplifies the development workflow while delivering optimal images to every browser.</p><p>For a production site with complete image optimization, see <a href="https://tanstackship.com/">tanstackship.com</a>.</p><h3>Related Resources</h3><ul><li><a href="https://tanstackship.com/blog/cloudflare-r2-object-storage-guide">Cloudflare R2: Object Storage for Edge Applications</a></li><li><a href="https://tanstackship.com/blog/core-web-vitals-optimization-guide">Core Web Vitals Optimization Guide</a></li><li><a href="https://tanstackship.com/blog/edge-caching-strategies-guide">Edge Caching Strategies: From CDN to Workers Cache</a></li><li><a href="https://tanstackship.com/blog/lighthouse-optimization-saas">Lighthouse 95+ Optimization Tips for SaaS</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1d690b8fb2ba" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Holiday and Seasonal Promotions: A SaaS Discount Strategy That Works]]></title>
            <link>https://beckmoulton.medium.com/holiday-and-seasonal-promotions-a-saas-discount-strategy-that-works-526c29f41e04?source=rss-f6287c0ef80d------2</link>
            <guid isPermaLink="false">https://medium.com/p/526c29f41e04</guid>
            <dc:creator><![CDATA[Beck Moulton]]></dc:creator>
            <pubDate>Tue, 09 Jun 2026 15:55:59 GMT</pubDate>
            <atom:updated>2026-06-09T15:55:59.551Z</atom:updated>
            <content:encoded><![CDATA[<p>Discounting is a double-edged sword for SaaS. Done well, holiday promotions can accelerate annual plan adoption, reactivate churned users, and drive seasonal spikes. Done poorly, they train users to wait for discounts and devalue your product. This guide covers a structured approach to holiday promotions — which holidays to target, what discount structures work, how to manage campaign timing, and the database infrastructure you need to run them. See production campaign management at <a href="https://tanstackship.com/">tanstackship.com</a>.</p><h3>The SaaS Discounting Dilemma</h3><p>Discount StrategyRiskRewardBest For<strong>20–30% off annual plans</strong>Low (annual commitment offsets discount)HighBlack Friday, New Year<strong>Free month</strong>Medium (user may cancel after)MediumTrial conversion push<strong>Lifetime discount (early adopter)</strong>High (permanent revenue reduction)Very highLaunch<strong>2-for-1 annual</strong>MediumHighEnd-of-year push<strong>First 3 months 50% off</strong>Medium (churn after discount)MediumQ1 slumps</p><p><strong>Key principle</strong>: Discount annual plans, not monthly. An annual discount locks in revenue for 12 months. A monthly discount creates churn risk when the promotional period ends.</p><h3>Holiday Calendar by Market</h3><h3>Major SaaS Promotion Windows</h3><p>HolidayDateMarketSaaS Opportunity<strong>New Year / Q1 Push</strong>Jan 1–31GlobalAnnual plan adoption<strong>Valentine’s Day</strong>Feb 14US, EU”Love your business” campaign<strong>Spring Sale</strong>March-AprilGlobalNew year, new stack<strong>Tax Season</strong>AprilUSSpending mindset<strong>Summer Slump</strong>June-AugustGlobal”Get ahead before fall”<strong>Back to School</strong>SeptUS, EUProductivity push<strong>Singles’ Day (11.11)</strong>Nov 11ChinaBiggest shopping day globally<strong>Black Friday / Cyber Monday</strong>Nov-DecGlobalThe biggest SaaS promo window<strong>Christmas / Year-End</strong>Dec 15–31GlobalLast-chance annual deals</p><h3>Campaign Structure</h3><h3>Campaign Database Schema</h3><pre>// D1 schema for campaign management<br>export const campaigns = sqliteTable(&quot;campaigns&quot;, {<br>  id: text(&quot;id&quot;).primaryKey(),<br>  name: text(&quot;name&quot;).notNull(),<br>  slug: text(&quot;slug&quot;).notNull().unique(),<br>  type: text(&quot;type&quot;, {<br>    enum: [&quot;holiday&quot;, &quot;seasonal&quot;, &quot;launch&quot;, &quot;retention&quot;, &quot;reactivation&quot;],<br>  }).notNull(),<br>  discountPercent: real(&quot;discount_percent&quot;).notNull(),<br>  discountType: text(&quot;discount_type&quot;, {<br>    enum: [&quot;percentage&quot;, &quot;fixed&quot;, &quot;free_month&quot;, &quot;tiered&quot;],<br>  }).notNull(),<br>  appliesTo: text(&quot;applies_to&quot;, {<br>    enum: [&quot;annual&quot;, &quot;monthly&quot;, &quot;all_plans&quot;, &quot;specific_plans&quot;],<br>  }).notNull(),<br>  specificPlans: text(&quot;specific_plans&quot;), // JSON array of plan IDs<br>  startDate: integer(&quot;start_date&quot;, { mode: &quot;timestamp&quot; }).notNull(),<br>  endDate: integer(&quot;end_date&quot;, { mode: &quot;timestamp&quot; }).notNull(),<br>  maxRedemptions: integer(&quot;max_redemptions&quot;),<br>  currentRedemptions: integer(&quot;current_redemptions&quot;).default(0),<br>  isActive: integer(&quot;is_active&quot;, { mode: &quot;boolean&quot; }).default(true),<br>  couponCode: text(&quot;coupon_code&quot;),<br>  metadata: text(&quot;metadata&quot;), // JSON for campaign assets<br>  createdAt: integer(&quot;created_at&quot;, { mode: &quot;timestamp&quot; }),<br>})</pre><h3>Creating a Campaign</h3><pre>// server/campaigns.ts<br>export const createCampaign = createServerFn({ method: &quot;POST&quot; }).handler(<br>  async ({ data, context }: { data: CreateCampaignInput }) =&gt; {<br>    const id = crypto.randomUUID()</pre><pre>    // Create Stripe coupon<br>    const coupon = await stripe.coupons.create({<br>      percent_off: data.discountPercent,<br>      duration: &quot;once&quot;,<br>      max_redemptions: data.maxRedemptions,<br>      applies_to: {<br>        products: data.specificPlans.length &gt; 0<br>          ? data.specificPlans<br>          : undefined,<br>      },<br>    })</pre><pre>    await context.env.DB.prepare(`<br>      INSERT INTO campaigns<br>        (id, name, slug, type, discount_percent, discount_type,<br>         applies_to, start_date, end_date, max_redemptions, coupon_code)<br>      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)<br>    `).bind(<br>      id, data.name, data.slug, data.type,<br>      data.discountPercent, data.discountType,<br>      data.appliesTo, data.startDate.getTime(),<br>      data.endDate.getTime(), data.maxRedemptions,<br>      coupon.id<br>    ).run()</pre><pre>    return { id, couponId: coupon.id }<br>  }<br>)</pre><h3>Black Friday Campaign Example</h3><h3>Pre-Launch (2 Weeks Before)</h3><pre>// Prepare campaign assets and notifications<br>export const prepareCampaign = createServerFn({ method: &quot;GET&quot; }).handler(<br>  async ({}, { context }) =&gt; {<br>    const upcoming = await context.env.DB.prepare(`<br>      SELECT * FROM campaigns<br>      WHERE start_date &gt; ? AND start_date &lt; ?<br>      AND is_active = 1<br>    `).bind(Date.now(), Date.now() + 14 * 24 * 60 * 60 * 1000).all()</pre><pre>    for (const campaign of upcoming.results) {<br>      // Send announcement emails to subscribers<br>      await sendCampaignAnnouncement(campaign)</pre><pre>      // Update pricing page to show upcoming sale<br>      // Update in KV for zero-downtime<br>      await context.env.KV.put(&quot;campaign:upcoming&quot;, JSON.stringify({<br>        name: campaign.name,<br>        discountPercent: campaign.discount_percent,<br>        startDate: campaign.start_date,<br>        slug: campaign.slug,<br>      }))<br>    }</pre><pre>    return { prepared: true, count: upcoming.results.length }<br>  }<br>)</pre><h3>Live Campaign</h3><pre>// Apply discount during checkout<br>export const applyCampaignDiscount = createServerFn({ method: &quot;POST&quot; }).handler(<br>  async ({ data, context }: {<br>    data: { campaignSlug: string; priceId: string }<br>  }) =&gt; {<br>    const campaign = await context.env.DB.prepare(`<br>      SELECT * FROM campaigns<br>      WHERE slug = ? AND is_active = 1<br>      AND start_date &lt;= ? AND end_date &gt;= ?<br>    `).bind(data.campaignSlug, Date.now(), Date.now()).first()</pre><pre>    if (!campaign) {<br>      return { valid: false, error: &quot;Campaign not active&quot; }<br>    }</pre><pre>    // Check redemption limit<br>    if (campaign.max_redemptions &amp;&amp; campaign.current_redemptions &gt;= campaign.max_redemptions) {<br>      return { valid: false, error: &quot;Campaign fully redeemed&quot; }<br>    }</pre><pre>    // Increment counter<br>    await context.env.DB.prepare(`<br>      UPDATE campaigns SET current_redemptions = current_redemptions + 1<br>      WHERE id = ?<br>    `).bind(campaign.id).run()</pre><pre>    return {<br>      valid: true,<br>      couponCode: campaign.coupon_code,<br>      discountPercent: campaign.discount_percent,<br>    }<br>  }<br>)</pre><h3>Promotion Types: Which to Use</h3><p>TypeConversion RateRevenue ImpactBest Timing<strong>% off annual</strong>+30–50%High (annual lock-in)Black Friday, New Year<strong>Free months</strong>+20–30%MediumLaunch, reactivation<strong>Tiered discount</strong>+25–40%Medium-HighExtended periods<strong>Limited time</strong>+40–60%High (urgent)24–48 hour flash sales<strong>BOGO (buy org, get 1 free)</strong>+15–20%MediumTeam/Org plans<strong>Bundle discount</strong>+20–25%MediumFeature add-ons</p><h3>Percentage Off vs Free Months</h3><pre>Percentage off annual plan:<br>  Normal: $29/mo × 12 = $348/year<br>  With 30% off: $243.60/year (saves $104.40)<br>  ✅ User commits for 12 months<br>  ✅ You get cash upfront<br>  ✅ Lower churn risk</pre><pre>Free months on monthly plan:<br>  Normal: $29/mo × 12 = $348/year<br>  With 2 months free: $29/mo × 10 = $290/year (saves $58)<br>  ❌ User can cancel any time<br>  ❌ Revenue is deferred<br>  ❌ Higher churn risk</pre><pre>Winner: Percentage off annual plan</pre><h3>Analytics and Attribution</h3><pre>// Track campaign performance<br>export const getCampaignAnalytics = createServerFn({ method: &quot;GET&quot; }).handler(<br>  async ({ data, context }: { data: { campaignId: string } }) =&gt; {<br>    const [campaign, conversions, revenue] = await Promise.all([<br>      context.env.DB.prepare(&quot;SELECT * FROM campaigns WHERE id = ?&quot;)<br>        .bind(data.campaignId).first(),<br>      context.env.DB.prepare(`<br>        SELECT COUNT(*) as conversions, SUM(mrr) as total_mrr<br>        FROM subscriptions<br>        WHERE coupon_id = (SELECT coupon_code FROM campaigns WHERE id = ?)<br>        AND created_at BETWEEN<br>          (SELECT start_date FROM campaigns WHERE id = ?) AND<br>          (SELECT end_date FROM campaigns WHERE id = ?)<br>      `).bind(data.campaignId, data.campaignId, data.campaignId).first(),<br>      context.env.DB.prepare(`<br>        SELECT SUM(amount) as total_revenue<br>        FROM invoices<br>        WHERE subscription_id IN (<br>          SELECT id FROM subscriptions<br>          WHERE coupon_id = (SELECT coupon_code FROM campaigns WHERE id = ?)<br>        )<br>      `).bind(data.campaignId).first(),<br>    ])</pre><pre>    return { campaign, conversions, revenue }<br>  }<br>)</pre><h3>Anti-Discounting Patterns</h3><p>BehaviorProblemSolutionAlways running a promotionUsers wait for discountsCreate clear promotion windowsDiscounting monthly plansHigh churn after promotionDiscount annual only50%+ discountsDevalues the productCap at 30–40%No expirationNo urgencyLimited time onlyDiscounting core featuresTrains users to value features lessDiscount commitment, not features</p><h3>Campaign Calendar Template</h3><pre>Q1 (Jan-Mar): New Year Annual Plan Push<br>  - &quot;New Year, New Stack&quot; — 25% off annual<br>  - Target: Free users on monthly, upgrade to annual</pre><pre>Q2 (Apr-Jun): Spring Refresh<br>  - &quot;Spring Cleaning&quot; — 20% off annual<br>  - Target: Inactive users reactivation</pre><pre>Q3 (Jul-Sep): Back to Business<br>  - &quot;Back to School for Your Business&quot; — 20% off team plans<br>  - Target: Team/org plan upgrades</pre><pre>Q4 (Oct-Dec): Black Friday + Year-End<br>  - Pre-BF: &quot;Early Black Friday&quot; — 30% off annual (limited quantity)<br>  - BF/CM: &quot;Black Friday Deal&quot; — 35% off annual<br>  - Year-End: &quot;Last Chance 2026&quot; — 25% off annual<br>  - Target: All segments, maximum conversion</pre><h3>Promotion Management Checklist</h3><ul><li>[ ] Campaign start/end dates set with proper timezone handling (UTC)</li><li>[ ] Stripe coupon created with redemption limits</li><li>[ ] Pricing page dynamically shows/hides promotional pricing</li><li>[ ] Email sequences ready: announcement, reminder, last chance, expired</li><li>[ ] UTM parameters set for all campaign links</li><li>[ ] Analytics dashboard tracking: impressions, clicks, conversions, revenue</li><li>[ ] Post-campaign retention analysis (do promo users retain at normal rates?)</li><li>[ ] Expired campaigns automatically hidden from checkout</li><li>[ ] Customer support briefed on campaign details</li><li>[ ] Refund policy adjusted for promotional purchases</li></ul><h3>Conclusion</h3><p>Holiday promotions are a powerful growth lever for SaaS — when executed strategically. The key principles are:</p><ol><li><strong>Discount annual plans, not monthly</strong> — annual commitment protects your revenue</li><li><strong>Cap discounts at 30–35%</strong> — higher discounts devalue the product</li><li><strong>Create urgency</strong> — limited-time offers convert better than permanent discounts</li><li><strong>Track everything</strong> — UTM, coupon codes, conversion analytics, retention rates</li><li><strong>Plan a calendar</strong> — know your promotion windows months in advance</li></ol><p>The campaign management infrastructure — database schema, Stripe integration, analytics tracking — should be built once and reused for every promotion. With the right infrastructure, a holiday campaign is a matter of configuration, not engineering.</p><p>For a SaaS with built-in campaign management, UTM tracking, and promotion infrastructure, see <a href="https://tanstackship.com/">tanstackship.com</a>.</p><h3>Related Resources</h3><ul><li><a href="https://tanstackship.com/blog/saas-growth-framework-first-100-customers">SaaS Growth Framework: First 100 Paying Customers</a></li><li><a href="https://tanstackship.com/blog/email-marketing-automation-saas">Email Marketing Automation: From Signup to Paid</a></li><li><a href="https://tanstackship.com/blog/saas-pricing-psychology">SaaS Pricing Psychology: Anchoring, Scarcity, Reciprocity</a></li><li><a href="https://tanstackship.com/blog/utm-attribution-complete-guide">UTM Attribution Complete Guide</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=526c29f41e04" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[In today’s data-driven world, access to up-to-date charts and rankings is essential for developers…]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-snippet">In this article, we&#x2019;ll explore the latest free and public charts APIs in 2025, covering music, mobile apps, streaming platforms, and&#x2026;</p><p class="medium-feed-link"><a href="https://beckmoulton.medium.com/in-todays-data-driven-world-access-to-up-to-date-charts-and-rankings-is-essential-for-developers-805262324b9f?source=rss-f6287c0ef80d------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://beckmoulton.medium.com/in-todays-data-driven-world-access-to-up-to-date-charts-and-rankings-is-essential-for-developers-805262324b9f?source=rss-f6287c0ef80d------2</link>
            <guid isPermaLink="false">https://medium.com/p/805262324b9f</guid>
            <dc:creator><![CDATA[Beck Moulton]]></dc:creator>
            <pubDate>Sat, 07 Feb 2026 13:47:00 GMT</pubDate>
            <atom:updated>2026-02-07T13:47:00.993Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[DeepSeek Makes a Strong Comeback: Open-Sources IMO Gold Medal-Level Math Model]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://beckmoulton.medium.com/deepseek-makes-a-strong-comeback-open-sources-imo-gold-medal-level-math-model-889507d17121?source=rss-f6287c0ef80d------2"><img src="https://cdn-images-1.medium.com/max/600/0*pbLngrr9SHpZkjx3" width="600"></a></p><p class="medium-feed-snippet">A breakthrough reasoning model is here&#x200A;&#x2014;&#x200A;DeepSeek opens up the direction of self-verifiable mathematical reasoning.</p><p class="medium-feed-link"><a href="https://beckmoulton.medium.com/deepseek-makes-a-strong-comeback-open-sources-imo-gold-medal-level-math-model-889507d17121?source=rss-f6287c0ef80d------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://beckmoulton.medium.com/deepseek-makes-a-strong-comeback-open-sources-imo-gold-medal-level-math-model-889507d17121?source=rss-f6287c0ef80d------2</link>
            <guid isPermaLink="false">https://medium.com/p/889507d17121</guid>
            <category><![CDATA[development]]></category>
            <category><![CDATA[tutorial]]></category>
            <category><![CDATA[developer]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Beck Moulton]]></dc:creator>
            <pubDate>Thu, 05 Feb 2026 13:41:01 GMT</pubDate>
            <atom:updated>2026-02-05T13:41:01.001Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Will ICLR 2026 Recover?]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://beckmoulton.medium.com/will-iclr-2026-recover-51ea977e9db2?source=rss-f6287c0ef80d------2"><img src="https://cdn-images-1.medium.com/max/600/0*4OKhhFYuFy9CsNaQ" width="600"></a></p><p class="medium-feed-snippet">The troubles for this year&#x2019;s ICLR aren&#x2019;t over yet.</p><p class="medium-feed-link"><a href="https://beckmoulton.medium.com/will-iclr-2026-recover-51ea977e9db2?source=rss-f6287c0ef80d------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://beckmoulton.medium.com/will-iclr-2026-recover-51ea977e9db2?source=rss-f6287c0ef80d------2</link>
            <guid isPermaLink="false">https://medium.com/p/51ea977e9db2</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[tutorial]]></category>
            <category><![CDATA[development]]></category>
            <category><![CDATA[developer]]></category>
            <dc:creator><![CDATA[Beck Moulton]]></dc:creator>
            <pubDate>Wed, 04 Feb 2026 13:41:00 GMT</pubDate>
            <atom:updated>2026-02-04T13:41:00.731Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[When Recommendation Systems Truly “Understand You”: New Achievement TagCF Proposed by Kuaishou Team…]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://beckmoulton.medium.com/when-recommendation-systems-truly-understand-you-new-achievement-tagcf-proposed-by-kuaishou-team-2a0d6a444283?source=rss-f6287c0ef80d------2"><img src="https://cdn-images-1.medium.com/max/600/0*LXQIujg5kiaHU7Ee" width="600"></a></p><p class="medium-feed-snippet">Every day, recommendation systems are capturing our interests and preferences. From the videos we scroll through to the live streaming&#x2026;</p><p class="medium-feed-link"><a href="https://beckmoulton.medium.com/when-recommendation-systems-truly-understand-you-new-achievement-tagcf-proposed-by-kuaishou-team-2a0d6a444283?source=rss-f6287c0ef80d------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://beckmoulton.medium.com/when-recommendation-systems-truly-understand-you-new-achievement-tagcf-proposed-by-kuaishou-team-2a0d6a444283?source=rss-f6287c0ef80d------2</link>
            <guid isPermaLink="false">https://medium.com/p/2a0d6a444283</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[tutorial]]></category>
            <category><![CDATA[development]]></category>
            <category><![CDATA[developer]]></category>
            <dc:creator><![CDATA[Beck Moulton]]></dc:creator>
            <pubDate>Mon, 02 Feb 2026 13:36:01 GMT</pubDate>
            <atom:updated>2026-02-02T13:36:01.397Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Introduction to Linear Algebra: Detailed Explanation and Application of Column Space and Zero Space]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-link"><a href="https://beckmoulton.medium.com/introduction-to-linear-algebra-detailed-explanation-and-application-of-column-space-and-zero-space-244b209fc3d1?source=rss-f6287c0ef80d------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://beckmoulton.medium.com/introduction-to-linear-algebra-detailed-explanation-and-application-of-column-space-and-zero-space-244b209fc3d1?source=rss-f6287c0ef80d------2</link>
            <guid isPermaLink="false">https://medium.com/p/244b209fc3d1</guid>
            <category><![CDATA[developer]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[tutorial]]></category>
            <category><![CDATA[development]]></category>
            <dc:creator><![CDATA[Beck Moulton]]></dc:creator>
            <pubDate>Sun, 01 Feb 2026 13:36:01 GMT</pubDate>
            <atom:updated>2026-02-01T13:36:01.403Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[The 2026 Strangler Fig: Smart Micro-frontends for UI Decoupling]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://beckmoulton.medium.com/the-2026-strangler-fig-smart-micro-frontends-for-ui-decoupling-4a955eff3a85?source=rss-f6287c0ef80d------2"><img src="https://cdn-images-1.medium.com/max/600/1*8l_SkNIy7NzhbjS1qOJEnA.png" width="600"></a></p><p class="medium-feed-link"><a href="https://beckmoulton.medium.com/the-2026-strangler-fig-smart-micro-frontends-for-ui-decoupling-4a955eff3a85?source=rss-f6287c0ef80d------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://beckmoulton.medium.com/the-2026-strangler-fig-smart-micro-frontends-for-ui-decoupling-4a955eff3a85?source=rss-f6287c0ef80d------2</link>
            <guid isPermaLink="false">https://medium.com/p/4a955eff3a85</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[productivity]]></category>
            <category><![CDATA[developer]]></category>
            <dc:creator><![CDATA[Beck Moulton]]></dc:creator>
            <pubDate>Fri, 30 Jan 2026 13:26:01 GMT</pubDate>
            <atom:updated>2026-01-30T13:26:01.738Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Developer Experience in Monolith Refactoring: A 2026 Priority]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://beckmoulton.medium.com/developer-experience-in-monolith-refactoring-a-2026-priority-f3776d7a8f8c?source=rss-f6287c0ef80d------2"><img src="https://cdn-images-1.medium.com/max/600/1*wM5DktPw-Umggcb2rURyIA.png" width="600"></a></p><p class="medium-feed-link"><a href="https://beckmoulton.medium.com/developer-experience-in-monolith-refactoring-a-2026-priority-f3776d7a8f8c?source=rss-f6287c0ef80d------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://beckmoulton.medium.com/developer-experience-in-monolith-refactoring-a-2026-priority-f3776d7a8f8c?source=rss-f6287c0ef80d------2</link>
            <guid isPermaLink="false">https://medium.com/p/f3776d7a8f8c</guid>
            <category><![CDATA[productivity]]></category>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[developer]]></category>
            <dc:creator><![CDATA[Beck Moulton]]></dc:creator>
            <pubDate>Wed, 28 Jan 2026 13:26:01 GMT</pubDate>
            <atom:updated>2026-01-28T13:26:01.937Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Beyond the Hype: My Team’s Five-Year Journey After Monolith Decommissioning]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://beckmoulton.medium.com/beyond-the-hype-my-teams-five-year-journey-after-monolith-decommissioning-79b878592e6d?source=rss-f6287c0ef80d------2"><img src="https://cdn-images-1.medium.com/max/600/1*weGtmZCosgymT1fN57rIvg.png" width="600"></a></p><p class="medium-feed-link"><a href="https://beckmoulton.medium.com/beyond-the-hype-my-teams-five-year-journey-after-monolith-decommissioning-79b878592e6d?source=rss-f6287c0ef80d------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://beckmoulton.medium.com/beyond-the-hype-my-teams-five-year-journey-after-monolith-decommissioning-79b878592e6d?source=rss-f6287c0ef80d------2</link>
            <guid isPermaLink="false">https://medium.com/p/79b878592e6d</guid>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[developer]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[productivity]]></category>
            <dc:creator><![CDATA[Beck Moulton]]></dc:creator>
            <pubDate>Tue, 27 Jan 2026 13:27:04 GMT</pubDate>
            <atom:updated>2026-01-27T13:27:04.769Z</atom:updated>
        </item>
    </channel>
</rss>