DEV Community

Cover image for 28% of My Users Are on Mobile. My Conversion Page Was Broken for All of Them.
Kazutaka Sugiyama
Kazutaka Sugiyama

Posted on

28% of My Users Are on Mobile. My Conversion Page Was Broken for All of Them.

I had analytics data for weeks showing mobile underperformance. I never noticed because I was looking at the wrong number.

I run RepoClip, a SaaS that generates promo videos from GitHub repos. The pipeline analyzes code with Gemini, generates images/video clips, adds narration, and renders a video. The previous articles covered image model migration and video clip integration.

This time, the problem wasn't in the AI pipeline. It was in a CSS grid.

The Aggregate Lie

My GA4 dashboard showed an overall bounce rate that looked fine. Mobile users were 28% of traffic — a meaningful chunk. But the aggregate number hid the real story.

When I broke it down by page using BigQuery, the conversion funnel on /dashboard/new (the video creation page) told a different story:

29 page views → 5 video generations
Enter fullscreen mode Exit fullscreen mode

That's a 17% conversion rate on the page where users actually create videos. Desktop was converting at roughly 3x that rate.

What Was Actually Broken

I opened Chrome DevTools, switched to iPhone SE (375px), and immediately saw the problems.

Problem 1: Grids That Don't Fit

The content mode selector uses a 3-column grid. On 375px, each button was ~105px wide — barely enough for the emoji, label, and "100 credits" text. The visual style selector was worse: 4 columns on a phone screen.

// Before: cramped on mobile
<div className="grid grid-cols-3 gap-3">
  {CONTENT_MODES.map((cm) => (
    <button className="flex flex-col items-center gap-1.5 p-3 ...">
      <span className="text-xl">{cm.emoji}</span>
      <span className="text-xs font-medium">{cm.label}</span>
      <span className="text-[10px]">{cm.description}</span>
      <span className="text-[10px]">{cm.credits} credits</span>
    </button>
  ))}
</div>
Enter fullscreen mode Exit fullscreen mode

The fix was one class change per grid:

- <div className="grid grid-cols-3 gap-3">
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-3">

- <div className="grid grid-cols-4 gap-3">
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
Enter fullscreen mode Exit fullscreen mode

Two columns on mobile, full grid on desktop. The buttons went from cramped and barely tappable to comfortable with clear labels.

Problem 2: The URL Input Squeeze

The URL input and "Validate" button sat side by side in a flex row. On a phone, the input was too narrow to show the full URL — users couldn't see what they'd typed:

- <div className="flex gap-3">
+ <div className="flex flex-col sm:flex-row gap-2 sm:gap-3">
Enter fullscreen mode Exit fullscreen mode

Stacking vertically on mobile gave the input full width. Simple, obvious in hindsight.

Problem 3: The Real Friction — Typing a URL on a Phone

This was the insight that went beyond CSS.

On desktop, users copy a GitHub URL from their browser and paste it. One action. On mobile, users have to:

  1. Switch to their browser
  2. Navigate to GitHub
  3. Find the repo
  4. Copy the URL
  5. Switch back to RepoClip
  6. Paste it
  7. Tap "Validate"

That's 7 steps before they even start configuring their video. No amount of responsive CSS fixes this.

The One-Tap Solution

RepoClip already has users' GitHub OAuth tokens (for private repo access). Those tokens can fetch their recent repositories:

// New GET handler in /api/github
export async function GET() {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) return NextResponse.json({ repos: [] });

  const { data: profile } = await supabase
    .from("profiles")
    .select("github_access_token")
    .eq("id", user.id)
    .single();

  const token = profile?.github_access_token;
  if (!token) return NextResponse.json({ repos: [] });

  const octokit = new Octokit({ auth: token });
  const { data: repos } = await octokit.repos.listForAuthenticatedUser({
    sort: "updated",
    per_page: 5,
  });

  return NextResponse.json({
    repos: repos.map((r) => ({
      owner: r.owner.login,
      name: r.name,
      description: r.description,
      stars: r.stargazers_count,
      language: r.language,
      isPrivate: r.private,
    })),
  });
}
Enter fullscreen mode Exit fullscreen mode

On the frontend, when the URL input is empty, the user sees their 5 most recent repos as tappable chips:

{!githubUrl && !repoInfo && recentRepos.length > 0 && (
  <div>
    <p className="text-xs text-slate-500 mb-2">Recent repositories</p>
    <div className="flex flex-wrap gap-2">
      {recentRepos.map((repo) => (
        <button
          key={`${repo.owner}/${repo.name}`}
          onClick={() => {
            const url = `https://github.com/${repo.owner}/${repo.name}`;
            setGithubUrl(url);
            // Auto-validate immediately
            validateRepo(url);
          }}
          className="px-3 py-1.5 text-xs font-medium rounded-full border ..."
        >
          {repo.owner}/{repo.name}
        </button>
      ))}
    </div>
  </div>
)}
Enter fullscreen mode Exit fullscreen mode

Tap a chip, auto-validate, start configuring. Three taps instead of 7 steps. On desktop, it's a nice convenience. On mobile, it eliminates the primary friction point.

The Analytics Blind Spot

While investigating the mobile conversion gap, I tried to answer a natural question: do mobile users choose portrait (9:16) video and desktop users choose landscape (16:9)?

I couldn't answer it. The video_generate_start GA4 event didn't include aspect_ratio:

// Before: missing aspect_ratio and visual_style
event("video_generate_start", {
  repo_owner: repoInfo?.owner ?? "",
  repo_name: repoInfo?.name ?? "",
  content_mode: contentMode,
  bgm_enabled: bgmEnabled ? "true" : "false",
});
Enter fullscreen mode Exit fullscreen mode

I could see aspect ratio distribution in Supabase (82% landscape, 13% portrait, 5% square) and device breakdown in GA4 (83% desktop, 17% mobile for generations). But I couldn't correlate them — different systems, different user IDs.

The fix was two lines:

  event("video_generate_start", {
    repo_owner: repoInfo?.owner ?? "",
    repo_name: repoInfo?.name ?? "",
    content_mode: contentMode,
+   aspect_ratio: aspectRatio,
+   visual_style: visualStyle,
    bgm_enabled: bgmEnabled ? "true" : "false",
  });
Enter fullscreen mode Exit fullscreen mode

Now I can run this BigQuery query:

SELECT
  device.category,
  (SELECT value.string_value
   FROM UNNEST(event_params)
   WHERE key = 'aspect_ratio') AS aspect_ratio,
  COUNT(*) AS generates
FROM `analytics.events_*`
WHERE event_name = 'video_generate_start'
GROUP BY device.category, aspect_ratio
ORDER BY generates DESC
Enter fullscreen mode Exit fullscreen mode

The data won't be available until the next deployment collects new events, but the hypothesis is testable now. If mobile users do prefer portrait videos, I should default to 9:16 when navigator.userAgent indicates a mobile device.

The Landing Page Badge Overflow

One more mobile issue, unrelated to conversions but visible to every visitor: the third-party badges (TAAFT, BetaList, Futurepedia) on the landing page had hard-coded pixel widths.

// Before: fixed widths overflow on 375px screens
<div className="flex justify-center items-center gap-6 py-8">
  <img width="300" src="..." />
  <img width="156" height="54" style={{ width: 156, height: 54 }} />
  <img width={250} height={54} style={{ width: 250, height: 54 }} />
</div>
Enter fullscreen mode Exit fullscreen mode

300 + 156 + 250 + gaps = 730px minimum. iPhone SE is 375px. The badges just overflowed.

// After: responsive widths with flex-wrap
<div className="flex flex-wrap justify-center items-center gap-4 sm:gap-6 px-4 py-8">
  <img src="..." className="w-48 sm:w-[300px]" />
  <img src="..." className="h-10 sm:h-[54px] w-auto" />
  <img src="..." className="h-10 sm:h-[54px] w-auto" />
</div>
Enter fullscreen mode Exit fullscreen mode

flex-wrap lets badges flow to a second row on small screens. Responsive height classes scale them down proportionally.

What I Learned

1. Aggregate metrics hide page-level problems. "Mobile bounce rate" told me nothing. Breaking conversion down by page and device category revealed that one specific page was broken. If you have GA4 + BigQuery, query by page path — not just by device.

2. The biggest mobile friction isn't layout — it's input. Responsive grids are table stakes. The real question is: what actions require typing on mobile that could be replaced with tapping? For RepoClip, the answer was URL input. For your app, it might be search, filters, or form fields.

3. Track what you'll want to correlate later. I added content_mode to the GA4 event at launch but forgot aspect_ratio and visual_style. When I needed to correlate device type with aspect ratio choice, the data wasn't there. Think about the questions you'll ask in 3 months and instrument for them now.

4. Fixed pixel widths are mobile time bombs. Every width="300" and style={{ width: 250 }} is a mobile overflow waiting to happen. Use responsive classes from the start, even if your primary audience is desktop.

The Checklist

If you're building a SaaS and haven't checked your mobile conversion funnel recently, here's my quick audit list:

  • [ ] Query GA4 by page path + device category, not just aggregate
  • [ ] Open every conversion-critical page on a 375px viewport
  • [ ] Check every grid: does it still make sense at half the width?
  • [ ] Find every input that requires typing — can it be replaced with selection?
  • [ ] Search your codebase for hard-coded width= attributes
  • [ ] Verify your analytics events include all dimensions you'll want to slice by

Try It

The mobile improvements are live now: repoclip.io

If you have a GitHub repo, try the one-tap flow: log in, go to "Create New Video," and your recent repos should appear as chips. On a phone, it's noticeably faster than typing a URL.

Questions for the community:

  • What's the worst mobile UX bug you've found hiding in aggregate analytics?
  • How do you decide when to optimize for mobile vs. accepting that some workflows are desktop-first?

Drop a comment or find me on GitHub.


Four CSS changes, one API endpoint, and two GA4 parameters. Sometimes the highest-impact work isn't in the AI pipeline — it's in the form that feeds it.

Top comments (0)