terryds/memegenscript

Free meme generator on Cloudflare Workers: a drag-and-drop editor for 200+ templates plus a URL-based meme API (every meme is just a link).

TypeScript

0

30 commits

updated Sep 19, 2026

See the code
api
canvas
cloudflare-workers
meme
meme-generator
typescript

See what people are saying (1)

README

Memegenscript

A free meme generator that runs entirely on Cloudflare Workers: an in-browser editor for 450+ templates, plus a URL-based API where every meme is just a link.

  • Editor pages: one page per template with draggable, individually styled text boxes, your own image layers, custom backgrounds, undo/redo, and PNG/JPG export. No signup, no server round-trips while you edit.
  • Meme API: GET /images/{template}/{top}/{bottom}.png renders a meme on the fly, with fonts, colors, styles, overlays, custom backgrounds, and animated GIF/WebP output. Interactive docs live at /docs.
  • Built for search: every template page is server-rendered with a description of the meme, alternate names, tags, structured data, social cards, and a sitemap.
  • Serverless: TypeScript Worker, WASM image codecs, static assets at the edge. No origin server, no image storage, nothing to babysit.
GET  /images/buzz/memes/memes_everywhere.png
GET  /images/ds/small_file/high_quality.jpg?style=maga&width=800
GET  /images/oprah/you_get/animated_text.gif
POST /images  {"template_id": "fry", "text": ["not sure if", "or just"]}

Meme editor pages

Every template has a human-facing, SEO-friendly editor page rendered by src/views/editor.ts:

RoutePurpose
/Index of all templates with search (/?q= also works server-side)
/memes/{slug}Canvas editor for one template (name-based; old ids and slugs redirect)
/agents, /llms.txtGuide for AI agents, as a page and as raw markdown (src/docs/agents.ts)
/privacyWhat the site collects (src/views/privacy.ts); update it when that changes
/sitemap.xmlLists the index and every editor page
/robots.txtAllows crawling of the pages, disallows the JSON API paths, links the sitemap
/static/*Stylesheet and scripts (assets/static/, cache-busted with ?v=<version>)
/assets/templates/{id}/{f}Raw template images used by the editor canvas
/assets/fonts/{file}Font files loaded by the editor with the FontFace API
/proxy/image?url=Fetches a remote image (10 MB max, images only) so the canvas can use it

The editor (assets/static/editor.js) runs entirely in the browser, imgflip-style:

  • Text boxes start where the template defines them; drag to move, corner handles to resize, top handle to rotate (Shift snaps to 15°), arrow keys to nudge.
  • Every text box has its own font, size (automatic fit or manual), color, outline color and width, alignment, vertical alignment, case, opacity and rotation.
  • Add as many text boxes as you like, add your own images as layers (upload or URL, with flip and opacity), reorder and duplicate layers, and replace the background with an uploaded image or a blank canvas of any size. Alternate template styles are selectable.
  • Undo/redo (Ctrl+Z / Ctrl+Y), delete key, double-click a box to edit its text.
  • Export as PNG or JPG at full resolution, copy the image to the clipboard, or copy an editor link that restores the text layers (the state lives in the URL hash).

Each page is server-rendered with a unique title, meta description, canonical URL, Open Graph and Twitter Card tags (the example meme is the social image), JSON-LD (WebSite with SearchAction, WebPage, ImageObject, BreadcrumbList), an <h1>, descriptive copy, the template's source link, keyword links, and related templates. Without JavaScript a <noscript> form posts to POST /images instead.

The site name in titles and social cards comes from the SITE_NAME variable.

Progressive web app

The site is installable: /manifest.webmanifest (icons generated by npx tsx scripts/build-icons.ts into assets/static/icons/), a service worker at /sw.js (assets/static/sw.js, its cache name bound to the asset content hash so every deploy refreshes caches), and an /offline fallback page. The worker serves pages network-first with an offline fallback, and caches static assets, fonts, template images, and the last 80 rendered memes cache-first. On phones, assets/static/pwa.js shows a closeable "Install" banner: Chrome/Android use the native prompt, iOS gets "Share → Add to Home Screen" instructions; dismissal is remembered for 14 days and the banner never shows once the app is installed.

Every template page explains what the meme is, where it comes from, its alternate names, and related tags, so people can find a template by describing it rather than knowing its name. The search on / (and ?q=) matches names, aliases, keywords, tags, example text, and the description; multi-word queries require every word to match.

  • data/descriptions.json is fetched from each template's Know Your Meme entry by npm run fetch:descriptions (short About/Origin excerpts, tags, alternate names). It only refetches missing or failed entries; pass --force to refresh everything.
  • data/descriptions.manual.json holds hand-written entries for templates without a Know Your Meme source; these override fetched data. Add an entry here for any new template.
  • scripts/build-templates.ts merges both into the manifest and warns about templates that still lack a description.

Know Your Meme excerpts are short, attributed, and linked; write original copy in the manual file when you want fully unique page text.

Getting started

Requirements: Node 20+, a Cloudflare account for deployment.

npm install
npm run dev          # http://localhost:8787 (runs the template build first)
npm test             # vitest inside the Workers runtime
npm run typecheck
npm run deploy       # wrangler deploy (uploads the Worker + ~90 MB of template assets)

Cloudflare's free plan limits Workers to 10 ms of CPU per request, which is not enough to render images. Deploy on the Workers Paid plan; wrangler.jsonc sets a 30 s CPU limit.

Configuration

Variables are defined in wrangler.jsonc (vars). Secrets go through wrangler secret put or a local .dev.vars file (see .dev.vars.example).

VariablePurpose
SITE_NAMEName used on the web pages and social cards (Memegenscript)
DEBUG"true" draws text/overlay boxes, enables /test, disables caching
DOMAINHost used in absolute URLs (defaults to the request's own origin)
DEFAULT_STATIC_EXTENSIONExtension used when none is requested (png)
DEFAULT_ANIMATED_EXTENSIONExtension for animated templates (gif)
CACHE_TTLSeconds to keep rendered images in the edge cache (0 disables)
REMOTE_TRACKING_URLOptional memecomplete backend for API keys, tokens, search, tracking
REMOTE_TRACKING_ERRORS_LIMITErrors before request tracking turns itself off (10)

How it works

The API began as a TypeScript port of memegen (Python), whose template library it still uses; the editor, pages, search, and design are original. The table shows what replaced each Python-era dependency:

ConcernOriginally (memegen, Python)Memegenscript
HTTPSanicWorker fetch handler + ordered regex router (src/router.ts)
Template metadatatemplates/*/config.yml via datafilesSame YAML, compiled to src/generated/templates.json at build
Template images, fontsLocal filesystemWorkers Static Assets (assets/, read via the ASSETS binding)
Text measurement & glyphsPillow + FreeTypeopentype.js (src/images/layout.ts)
Rasterizing text/overlaysPillow ImageDrawSVG built in src/images/layer.ts, rendered by resvg (wasm)
Resize, blur, compositingPillowPure TypeScript (src/images/raster.ts)
PNG / JPEG / WebP codecsPillow, webpjSquash wasm codecs
GIFPillowgifuct-js (decode) + gifenc (encode)
Animated WebPwebp packagePer-frame encode + hand-written ANMF muxer (src/images/webp.ts)
Emojiemoji + pilmoji (Twemoji)emojilib aliases + Twemoji images (src/utils/emoji.ts)
style: mock textspongemock (seeded random)MT19937 port with CPython seeding (src/utils/mt19937.ts)
Rendered image cacheimages/ directory on diskCache API (caches.default), keyed by request URL
Custom backgrounds/overlaysDownloaded to templates/_custom-<sha1>/Downloaded on demand, cached with the Cache API

Request flow for GET /images/{template}/{text}.{ext}:

  1. src/views/images.ts normalizes the slug and handles redirects (style, watermark, tokens).
  2. src/views/helpers.ts#renderImageResponse resolves the template, validates every parameter and picks the status code exactly like the Python view.
  3. src/images/render.ts decodes the background, resizes it, renders the foreground layer (overlays + text) once per distinct animation state, pads/watermarks, and encodes.

Adding a template

Drop a directory into assets/templates/<id>/ with a config.yml and a default.png (or .jpg/.gif), exactly as in the Python project. Extra images in the directory become style= options. The manifest is rebuilt automatically by npm run dev, npm test and npm run deploy (or run npm run build:templates).

API

The full guide lives in docs/guide.md and the client notes in docs/clients.md. Everything from the original README applies:

  • Formats: .png, .jpg, .gif, .webp (GIF/WebP animate the text on static backgrounds)
  • width / height (both → padded to exact size), layout=top, font=<id|alias>
  • color=<line1>,<line2> (names or hex, # optional), style=<name> or style=<url>[,<url>]
  • background=<url> with template_id=custom, center, scale, frames, start, stop
  • Special characters in paths: _/- → space, ___, ---, ~q ~a ~p ~h ~s ~b ~l ~g ~n, ''"
  • Emoji as characters or :aliases:

Differences from the original memegen API

  • Text is rasterized from vector outlines instead of FreeType bitmaps, so glyph shapes and antialiasing differ very slightly; layout, wrapping and font-size selection use the same algorithms and produce the same line breaks.
  • GIF output is quantized without dithering (gifenc), so gradients band a little more.
  • Hebrew text is laid out left-to-right (no bidi shaping), as Pillow does without libraqm.
  • EXIF orientation of custom JPEG backgrounds is not applied.
  • Bugsnag error reporting is not wired up; errors go to Workers logs (observability is on).
  • DEBUG mode does not write new template config files to disk (there is no disk).
  • / serves the template index page instead of redirecting to /docs.

Project layout

assets/            templates/, fonts/, static/  (served by Workers Static Assets)
scripts/           build-templates.ts → src/generated/templates.json
src/index.ts       routes + CORS + error handling
src/views/         one module per Sanic blueprint
src/models/        Template, Text, Overlay, Font
src/images/        codecs, raster ops, text layout, SVG layer, render pipeline
src/utils/         slug codec, urls, colors, emoji, remote tracking, sha1, mt19937
src/docs/          OpenAPI document + Swagger UI page
test/              vitest (runs inside workerd via @cloudflare/vitest-pool-workers)

License and credits

MIT. Portions of the API and the template library come from memegen by Jace Browning (MIT); see LICENSE.txt. Template images belong to their respective owners. Font licenses are in assets/fonts/. Meme descriptions quote short, attributed excerpts from Know Your Meme.

Contributors

terryds

23 commits

gideonaibot

7 commits

terryds/memegenscript

Free meme generator on Cloudflare Workers: a drag-and-drop editor for 200+ templates plus a URL-based meme API (every meme is just a link).

TypeScript

0

30 commits

updated Sep 19, 2026

See the code
api
canvas
cloudflare-workers
meme
meme-generator
typescript

See what people are saying (1)

README

Memegenscript

A free meme generator that runs entirely on Cloudflare Workers: an in-browser editor for 450+ templates, plus a URL-based API where every meme is just a link.

  • Editor pages: one page per template with draggable, individually styled text boxes, your own image layers, custom backgrounds, undo/redo, and PNG/JPG export. No signup, no server round-trips while you edit.
  • Meme API: GET /images/{template}/{top}/{bottom}.png renders a meme on the fly, with fonts, colors, styles, overlays, custom backgrounds, and animated GIF/WebP output. Interactive docs live at /docs.
  • Built for search: every template page is server-rendered with a description of the meme, alternate names, tags, structured data, social cards, and a sitemap.
  • Serverless: TypeScript Worker, WASM image codecs, static assets at the edge. No origin server, no image storage, nothing to babysit.
GET  /images/buzz/memes/memes_everywhere.png
GET  /images/ds/small_file/high_quality.jpg?style=maga&width=800
GET  /images/oprah/you_get/animated_text.gif
POST /images  {"template_id": "fry", "text": ["not sure if", "or just"]}

Meme editor pages

Every template has a human-facing, SEO-friendly editor page rendered by src/views/editor.ts:

RoutePurpose
/Index of all templates with search (/?q= also works server-side)
/memes/{slug}Canvas editor for one template (name-based; old ids and slugs redirect)
/agents, /llms.txtGuide for AI agents, as a page and as raw markdown (src/docs/agents.ts)
/privacyWhat the site collects (src/views/privacy.ts); update it when that changes
/sitemap.xmlLists the index and every editor page
/robots.txtAllows crawling of the pages, disallows the JSON API paths, links the sitemap
/static/*Stylesheet and scripts (assets/static/, cache-busted with ?v=<version>)
/assets/templates/{id}/{f}Raw template images used by the editor canvas
/assets/fonts/{file}Font files loaded by the editor with the FontFace API
/proxy/image?url=Fetches a remote image (10 MB max, images only) so the canvas can use it

The editor (assets/static/editor.js) runs entirely in the browser, imgflip-style:

  • Text boxes start where the template defines them; drag to move, corner handles to resize, top handle to rotate (Shift snaps to 15°), arrow keys to nudge.
  • Every text box has its own font, size (automatic fit or manual), color, outline color and width, alignment, vertical alignment, case, opacity and rotation.
  • Add as many text boxes as you like, add your own images as layers (upload or URL, with flip and opacity), reorder and duplicate layers, and replace the background with an uploaded image or a blank canvas of any size. Alternate template styles are selectable.
  • Undo/redo (Ctrl+Z / Ctrl+Y), delete key, double-click a box to edit its text.
  • Export as PNG or JPG at full resolution, copy the image to the clipboard, or copy an editor link that restores the text layers (the state lives in the URL hash).

Each page is server-rendered with a unique title, meta description, canonical URL, Open Graph and Twitter Card tags (the example meme is the social image), JSON-LD (WebSite with SearchAction, WebPage, ImageObject, BreadcrumbList), an <h1>, descriptive copy, the template's source link, keyword links, and related templates. Without JavaScript a <noscript> form posts to POST /images instead.

The site name in titles and social cards comes from the SITE_NAME variable.

Progressive web app

The site is installable: /manifest.webmanifest (icons generated by npx tsx scripts/build-icons.ts into assets/static/icons/), a service worker at /sw.js (assets/static/sw.js, its cache name bound to the asset content hash so every deploy refreshes caches), and an /offline fallback page. The worker serves pages network-first with an offline fallback, and caches static assets, fonts, template images, and the last 80 rendered memes cache-first. On phones, assets/static/pwa.js shows a closeable "Install" banner: Chrome/Android use the native prompt, iOS gets "Share → Add to Home Screen" instructions; dismissal is remembered for 14 days and the banner never shows once the app is installed.

Every template page explains what the meme is, where it comes from, its alternate names, and related tags, so people can find a template by describing it rather than knowing its name. The search on / (and ?q=) matches names, aliases, keywords, tags, example text, and the description; multi-word queries require every word to match.

  • data/descriptions.json is fetched from each template's Know Your Meme entry by npm run fetch:descriptions (short About/Origin excerpts, tags, alternate names). It only refetches missing or failed entries; pass --force to refresh everything.
  • data/descriptions.manual.json holds hand-written entries for templates without a Know Your Meme source; these override fetched data. Add an entry here for any new template.
  • scripts/build-templates.ts merges both into the manifest and warns about templates that still lack a description.

Know Your Meme excerpts are short, attributed, and linked; write original copy in the manual file when you want fully unique page text.

Getting started

Requirements: Node 20+, a Cloudflare account for deployment.

npm install
npm run dev          # http://localhost:8787 (runs the template build first)
npm test             # vitest inside the Workers runtime
npm run typecheck
npm run deploy       # wrangler deploy (uploads the Worker + ~90 MB of template assets)

Cloudflare's free plan limits Workers to 10 ms of CPU per request, which is not enough to render images. Deploy on the Workers Paid plan; wrangler.jsonc sets a 30 s CPU limit.

Configuration

Variables are defined in wrangler.jsonc (vars). Secrets go through wrangler secret put or a local .dev.vars file (see .dev.vars.example).

VariablePurpose
SITE_NAMEName used on the web pages and social cards (Memegenscript)
DEBUG"true" draws text/overlay boxes, enables /test, disables caching
DOMAINHost used in absolute URLs (defaults to the request's own origin)
DEFAULT_STATIC_EXTENSIONExtension used when none is requested (png)
DEFAULT_ANIMATED_EXTENSIONExtension for animated templates (gif)
CACHE_TTLSeconds to keep rendered images in the edge cache (0 disables)
REMOTE_TRACKING_URLOptional memecomplete backend for API keys, tokens, search, tracking
REMOTE_TRACKING_ERRORS_LIMITErrors before request tracking turns itself off (10)

How it works

The API began as a TypeScript port of memegen (Python), whose template library it still uses; the editor, pages, search, and design are original. The table shows what replaced each Python-era dependency:

ConcernOriginally (memegen, Python)Memegenscript
HTTPSanicWorker fetch handler + ordered regex router (src/router.ts)
Template metadatatemplates/*/config.yml via datafilesSame YAML, compiled to src/generated/templates.json at build
Template images, fontsLocal filesystemWorkers Static Assets (assets/, read via the ASSETS binding)
Text measurement & glyphsPillow + FreeTypeopentype.js (src/images/layout.ts)
Rasterizing text/overlaysPillow ImageDrawSVG built in src/images/layer.ts, rendered by resvg (wasm)
Resize, blur, compositingPillowPure TypeScript (src/images/raster.ts)
PNG / JPEG / WebP codecsPillow, webpjSquash wasm codecs
GIFPillowgifuct-js (decode) + gifenc (encode)
Animated WebPwebp packagePer-frame encode + hand-written ANMF muxer (src/images/webp.ts)
Emojiemoji + pilmoji (Twemoji)emojilib aliases + Twemoji images (src/utils/emoji.ts)
style: mock textspongemock (seeded random)MT19937 port with CPython seeding (src/utils/mt19937.ts)
Rendered image cacheimages/ directory on diskCache API (caches.default), keyed by request URL
Custom backgrounds/overlaysDownloaded to templates/_custom-<sha1>/Downloaded on demand, cached with the Cache API

Request flow for GET /images/{template}/{text}.{ext}:

  1. src/views/images.ts normalizes the slug and handles redirects (style, watermark, tokens).
  2. src/views/helpers.ts#renderImageResponse resolves the template, validates every parameter and picks the status code exactly like the Python view.
  3. src/images/render.ts decodes the background, resizes it, renders the foreground layer (overlays + text) once per distinct animation state, pads/watermarks, and encodes.

Adding a template

Drop a directory into assets/templates/<id>/ with a config.yml and a default.png (or .jpg/.gif), exactly as in the Python project. Extra images in the directory become style= options. The manifest is rebuilt automatically by npm run dev, npm test and npm run deploy (or run npm run build:templates).

API

The full guide lives in docs/guide.md and the client notes in docs/clients.md. Everything from the original README applies:

  • Formats: .png, .jpg, .gif, .webp (GIF/WebP animate the text on static backgrounds)
  • width / height (both → padded to exact size), layout=top, font=<id|alias>
  • color=<line1>,<line2> (names or hex, # optional), style=<name> or style=<url>[,<url>]
  • background=<url> with template_id=custom, center, scale, frames, start, stop
  • Special characters in paths: _/- → space, ___, ---, ~q ~a ~p ~h ~s ~b ~l ~g ~n, ''"
  • Emoji as characters or :aliases:

Differences from the original memegen API

  • Text is rasterized from vector outlines instead of FreeType bitmaps, so glyph shapes and antialiasing differ very slightly; layout, wrapping and font-size selection use the same algorithms and produce the same line breaks.
  • GIF output is quantized without dithering (gifenc), so gradients band a little more.
  • Hebrew text is laid out left-to-right (no bidi shaping), as Pillow does without libraqm.
  • EXIF orientation of custom JPEG backgrounds is not applied.
  • Bugsnag error reporting is not wired up; errors go to Workers logs (observability is on).
  • DEBUG mode does not write new template config files to disk (there is no disk).
  • / serves the template index page instead of redirecting to /docs.

Project layout

assets/            templates/, fonts/, static/  (served by Workers Static Assets)
scripts/           build-templates.ts → src/generated/templates.json
src/index.ts       routes + CORS + error handling
src/views/         one module per Sanic blueprint
src/models/        Template, Text, Overlay, Font
src/images/        codecs, raster ops, text layout, SVG layer, render pipeline
src/utils/         slug codec, urls, colors, emoji, remote tracking, sha1, mt19937
src/docs/          OpenAPI document + Swagger UI page
test/              vitest (runs inside workerd via @cloudflare/vitest-pool-workers)

License and credits

MIT. Portions of the API and the template library come from memegen by Jace Browning (MIT); see LICENSE.txt. Template images belong to their respective owners. Font licenses are in assets/fonts/. Meme descriptions quote short, attributed excerpts from Know Your Meme.

Contributors

terryds

23 commits

gideonaibot

7 commits

Languages

TypeScript

84.6%

JavaScript

11.3%

CSS

4.1%