Why I Built This
A friend kept manually copy-pasting "sorry 🙏" over and over to send dramatic apology messages on WhatsApp. I watched him do it for about 30 seconds before thinking — this is a solved problem.
A few days later, TextRepeater was live.
It lets you type any text, set how many times to repeat it (up to 1000), pick a separator, add emoji scene modes, and apply 15 visual effects — all in the browser, no account, no backend.
The Stack
Deliberately simple:
-
Next.js 14 with
output: 'export'(fully static) - Tailwind CSS for styling
- Cloudflare Pages for hosting
- GitHub Actions for CI/CD
No database. No API routes. No auth. Everything runs client-side.
Deployment: GitHub Actions + Cloudflare Pages Direct Upload
The interesting part was automating deployment. Cloudflare Pages has a GitHub App integration, but connecting it via API throws a cryptic error:
error code: 8000011 — There is an internal issue with your Cloudflare Pages Git installation.
This happens because the GitHub App OAuth flow requires browser authorization — it can't be done purely through the API.
The workaround: use Direct Upload mode instead of GitHub integration.
Step 1 — Create the Pages project via API (no source field)
curl -X POST "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/pages/projects" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
--data '{
"name": "text-repeater",
"production_branch": "main"
}'
Step 2 — GitHub Actions workflow
name: Deploy to Cloudflare Pages
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy out --project-name=text-repeater --branch=main
Every push to main triggers a full build and deploy. Zero manual steps.
Next.js Config for Static Export
For Cloudflare Pages to serve routes correctly:
// next.config.mjs
const nextConfig = {
output: 'export',
trailingSlash: true, // important for Cloudflare routing
images: {
unoptimized: true, // required for static export
},
}
export default nextConfig
trailingSlash: true is the one that trips people up — without it, direct URL navigation breaks on Cloudflare Pages.
Custom Domain via Cloudflare API
After purchasing the domain, instead of clicking through the dashboard, I wired everything via API:
# 1. Bind domain to Pages project
curl -X POST "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/pages/projects/text-repeater/domains" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
--data '{"name": "textrepeater.top"}'
# 2. Add CNAME record (proxied = orange cloud enabled)
curl -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
--data '{
"type": "CNAME",
"name": "textrepeater.top",
"content": "text-repeater-9lr.pages.dev",
"proxied": true
}'
SSL certificate issued automatically within ~2 minutes.
SEO for a Static Next.js Site
Launching a new domain means starting from zero authority. Here's what I set up on day one:
1. Canonical URLs (every page)
export const metadata: Metadata = {
alternates: {
canonical: 'https://textrepeater.top/apology-mode/',
},
}
Without this, search engines might treat http vs https or trailing slash variants as duplicate pages, splitting your ranking signal.
2. robots.txt
User-agent: *
Allow: /
Sitemap: https://textrepeater.top/sitemap.xml
Obvious but easy to forget on a static site.
3. sitemap.xml with lastmod
<url>
<loc>https://textrepeater.top/</loc>
<lastmod>2026-03-30</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
4. Open Graph + Twitter Card
openGraph: {
type: 'website',
siteName: 'TextRepeater',
url: 'https://textrepeater.top',
title: 'Text Repeater Online — Repeat Any Text Instantly',
description: '...',
},
twitter: {
card: 'summary',
title: 'Text Repeater Online — Repeat Any Text Instantly',
description: '...',
},
5. JSON-LD Structured Data
For the homepage:
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'TextRepeater',
url: 'https://textrepeater.top',
}),
}}
/>
For tool pages, use WebApplication with offers.price: '0' — this can show a free badge in rich results.
The "Scene Mode" Idea
The differentiator I'm most proud of is Scene Modes — instead of just repeating text, you pick a context (apology, birthday, anger) and the tool automatically inserts themed emoji patterns throughout your message.
Input: "I'm really sorry"
Output: "🙏 I'm really sorry 😔 I'm really sorry 💦 I'm really sorry 🥺"
It maps to specific long-tail keywords like "apology text generator" and "sorry message generator for WhatsApp" — much lower competition than "text repeater" alone.
Each Scene Mode is its own page with targeted metadata, giving the site multiple entry points for organic traffic.
What's Next
- Completing the remaining Scene Modes (Heart, Anger, Birthday, Like)
- Adding more WhatsApp-specific templates
- Tracking which visual effects actually get used
Try It
textrepeater.top — free, no sign-up, works on mobile.
If you're building something similar (static Next.js + Cloudflare Pages), the Direct Upload + GitHub Actions approach is the smoothest path I've found. Happy to answer questions in the comments.
Top comments (0)