Building a City Boy Meme Generator with Next.js and Fabric.js (No Watermarks!)

Published: (January 17, 2026 at 05:11 AM EST)
5 min read
Source: Dev.to

Source: Dev.to

I recently built a free online meme generator for the “City Boy” meme format, and I want to share the technical journey – from choosing the right canvas library to solving tricky React re‑rendering issues.

🔗 Live Demo: https://cityboymeme.com

The Problem with Most Meme Generators

  • Annoying watermarks on downloaded images
  • Forced registration before downloading
  • Clunky, non‑responsive interfaces
  • Ad‑heavy experiences

I wanted to build something better: fast, free, and with zero compromises.

ItemChoice
FrameworkNext.js 16 (App Router)
UI LibraryReact 19
LanguageTypeScript
Canvas ManipulationFabric.js 5.3
StylingTailwind CSS 4
IconsReact Icons
✅ No registration required

Why Fabric.js?

I initially considered the plain Canvas API or html2canvas, but I went with Fabric.js for several reasons:

// With Fabric.js, object manipulation is incredibly simple
const text = new fabric.Text('City Boys Be Like', {
  left: 300,
  top: 300,
  fontSize: 48,
  fill: '#FFFFFF',
  stroke: '#000000',
  strokeWidth: 3,
});

canvas.add(text);
canvas.setActiveObject(text); // Instantly interactive!

Benefits

  • Built‑in object manipulation (drag, resize, rotate)
  • Better text‑rendering quality
  • Event handling out of the box
  • Easier state management

The Most Frustrating Bug

Every time a user clicked anywhere or updated text, the entire canvas would flicker and reset.

❌ Bad implementation (re‑initializes canvas on every render)

const handleTextSelect = (id: string) => {
  setSelectedTextId(id);
};

useEffect(() => {
  // Canvas gets destroyed and recreated!
  const canvas = new fabric.Canvas(canvasRef.current);
  return () => canvas.dispose();
}, [onTextSelect]); // This dependency changes every render!

✅ Good implementation (stable callbacks)

// Stable function reference
const handleTextSelect = useCallback((id: string | null) => {
  setSelectedTextId(id);
}, []);

const handleCanvasReady = useCallback(() => {
  setCanvasReady(true);
}, []);

// Only initialize canvas once
useEffect(() => {
  const canvas = new fabric.Canvas(canvasRef.current);
  // ...setup code
  return () => canvas.dispose();
}, []); // Empty dependencies – runs once!

Key Lesson: In React, when passing callbacks to components (especially canvas libraries), always use useCallback to prevent unnecessary re‑renders.

Fixing Persistent Text Styles

When switching between text styles (outlined → filled → bold), the previous style properties weren’t being cleared properly.

❌ Bad: Old styles persist

if (updates.style !== undefined) {
  const style = getTextStyle(updatedElement);
  textObj.set(style);
}

✅ Good: Clear old styles first

if (updates.style !== undefined) {
  // Clear all style‑related properties first
  textObj.set({
    stroke: undefined,
    strokeWidth: 0,
    backgroundColor: '',
    padding: 0,
  });

  const style = getTextStyle(updatedElement as TextElement);
  const { originX, originY, ...styleOnly } = style;
  textObj.set(styleOnly);
}

SEO & Discoverability

// app/layout.tsx
export const metadata: Metadata = {
  title: {
    default: "City Boy Meme Generator - Free Online Meme Maker",
    template: "%s | City Boy Meme",
  },
  description: "Create hilarious City Boy memes instantly...",
  alternates: {
    canonical: "https://cityboymeme.com/",
  },
  openGraph: {
    type: "website",
    locale: "en_US",
    url: "https://cityboymeme.com",
    siteName: "City Boy Meme Generator",
    images: [
      {
        url: "https://cityboymeme.com/logo.png",
        width: 1200,
        height: 630,
      },
    ],
  },
};
// app/sitemap.ts
export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: "https://cityboymeme.com",
      lastModified: new Date(),
      changeFrequency: "daily",
      priority: 1,
    },
  ];
}
# public/robots.txt
User-agent: *
Allow: /

Sitemap: https://cityboymeme.com/sitemap.xml

Generating Favicons (macOS sips)

sips -z 16 16 logo.png --out favicon-16.png
sips -z 32 32 logo.png --out favicon-32.png
sips -z 180 180 logo.png --out apple-icon.png
sips -z 192 192 logo.png --out icon-192.png
sips -z 512 512 logo.png --out icon-512.png

Content Strategy

  • Target keyword density: 3‑5%
  • Total word count: 800+ words
  • Keywords: “City Boy meme”, “meme generator”, “free meme maker”
  • Implementation: Natural language throughout FAQ, features, and about sections

Architecture Overview

The entire app runs in the browser with zero backend.

Advantages

  • Instant deployment (Vercel / Netlify)
  • No server costs
  • No database needed
  • Privacy‑first (no data collection)
  • Faster for users (no API calls)

Trade‑offs

  • Can’t save memes to the cloud
  • No user accounts
  • Limited analytics

For a meme generator, this is the right choice. Users want speed and privacy.

Project Structure

app/
├── layout.tsx          # SEO metadata, fonts
├── page.tsx            # Main entry point
├── globals.css         # Global styles
└── sitemap.ts         # Dynamic sitemap

components/
├── MemeEditor.tsx      # Main editor component
└── FabricCanvas.tsx   # Canvas abstraction

public/
├── logo.png           # Source image
├── favicon.ico        # Multiple sizes
└── robots.txt         # SEO
import Image from 'next/image';
;
import { Inter } from "next/font/google";

const inter = Inter({
  subsets: ["latin"],
  display: "swap", // Prevent layout shift
});
// Export canvas as Data URL for download
export const exportAsDataURL = () => {
  canvas.discardActiveObject();
  canvas.renderAll();
  return canvas.toDataURL({
    format: "png",
    quality: 1,
    multiplier: 2, // 2× resolution for crisp downloads
  });
};

Canvas + React: The Compatibility Challenge

  • React wants to control everything via state.
  • Canvas manipulates the DOM directly.
  • Re‑renders can destroy canvas state.

Solution: Treat the canvas as an “uncontrolled component” and manage it via refs and callbacks.

// Example: initializing Fabric canvas only once
useEffect(() => {
  const canvas = new fabric.Canvas(canvasRef.current);
  // ...setup (add listeners, objects, etc.)
  return () => canvas.dispose();
}, []); // runs once

Fabric.js has TypeScript definitions that make this integration type‑safe and developer‑friendly.

Custom Interfaces

export interface TextElement {
  id: string
  text: string
  color: string
  font: string
  style: 'bold' | 'filled' | 'outlined'
  size: number
}

export interface FabricCanvasRef {
  addText: (element: TextElement) => void
  updateText: (id: string, updates: Partial) => void
  removeText: (id: string) => void
  exportAsDataURL: () => string
}

SEO Optimisation (Even for a SPA)

  • Proper meta tags
  • Structured data (JSON‑LD)
  • Semantic HTML
  • Keyword optimisation
  • Fast loading times

Initial Feature Wishlist

  • Image upload
  • Sticker library
  • Advanced filters
  • User accounts

Realisation: Simplicity is the killer feature. Users just want to add text and download. Done.

Ideas I’m Considering

  • Template Gallery: Add more meme templates
  • Quick Templates: Pre‑made text layouts
  • Color Picker: Custom colours beyond presets
  • Export Formats: JPEG, WebP options
  • History / Undo: Canvas state management
  • PWA: Offline support

Deployment (Vercel – Zero Config)

npm run build   # Build output
# Push to GitHub
# Vercel auto‑deploys

Build output includes:

  • Static HTML / CSS / JS
  • Optimised images
  • Automatic sitemap generation
  • Edge CDN distribution

Lessons Learned

  1. Start simple: Build the core feature first, add complexity later.
  2. Choose the right tools: Fabric.js saved weeks of development.
  3. Test on real devices: Mobile experience matters.
  4. SEO from day one: Don’t treat it as an afterthought.
  5. Performance matters: Users expect instant feedback.
  6. No ads, no BS: Sometimes the best monetisation is none.

🚀 Live Demo: https://cityboymeme.com
The entire project runs in the browser – no registration, no watermarks, completely free.

What I Gained

  • React performance optimisation
  • Canvas manipulation in modern frameworks
  • SEO for single‑page apps
  • The value of simplicity

If you’re building something similar, I hope this helps you avoid the pitfalls I encountered!

Questions? Comments? Drop them below – I’m happy to discuss any aspect of the implementation.

Found this helpful? Give it a ❤️ and share with your fellow developers!

Tags: #nextjs #react #javascript #webdev #typescript #canvas #seo #tutorial

Back to Blog

Related posts

Read more »

Open-Source Developer Portfolio

A clean, production‑ready Next.js portfolio open source that you can use as a reference when building your own developer site. Overview If you’re building a dev...