AI-assisted landing page generator that uses Gemini CLI to create static marketing pages and deploy them to Cloudflare Pages under subdomains of a domain you own.
- Python
>=3.11andpip - Git (for cloning) and a POSIX shell (examples assume bash/zsh; PowerShell works with equivalent commands)
- A domain you own, already moved to Cloudflare (nameservers pointing at Cloudflare)
- Cloudflare account with:
- Account ID (
CLOUDFLARE_ACCOUNT_ID) - API token with Pages:Edit and DNS:Edit permissions for that account (
CLOUDFLARE_API_TOKEN)
- Account ID (
- Cloudflare Wrangler CLI installed locally via npm (used for deploying to Pages and the contact form Worker)
- Gemini CLI installed and configured (see https://geminicli.com/docs/get-started/deployment/). Text prompts run via the CLI using its own auth (login recommended); set
GEMINI_ALLOW_CLI_API_KEY=1only if you want the CLI to use your API key as well. - Billing-enabled
GEMINI_API_KEYonly for image generation (Python client). This key is not passed to the CLI by default so text requests stay on the non-billed flow. - Optional: a preferred Gemini code and image model name (see
.env.exampledefaults)
- Logging in with
gemini loginkeeps text prompts on the Google AI Studio free tier. Rate limits on the free tier apply (see https://ai.google.dev/gemini-api/docs/rate-limits under those models). - Leave
GEMINI_ALLOW_CLI_API_KEY=0or unset to avoid accidentally switching the CLI onto your paid API key; use the API key only for image calls where billing is required.
| Limit Type | Personal / Free User | Pro / Enterprise User |
|---|---|---|
| Requests per minute (RPM) | 120 | 120 |
| Requests per day (RPD) | 1,000 | 1,500 |
| Context window | 1,000,000 tokens | 1,000,000 tokens |
Updated info can be found at: https://docs.cloud.google.com/gemini/docs/quotas
Note
- When using the CLI in Agent Mode, a single user prompt may trigger multiple background model requests, all of which count toward these limits.
- Rate limits are enforced per project (not per API key) and are checked independently across RPM (requests per minute), TPM ((input) tokens per minute), and RPD (requests per day). Exceeding any dimension triggers a rate limit error; RPD resets at midnight Pacific.
- Limits vary by model and some limits are model-specific.
| Model | Requests per Minute (RPM) | Requests per Day (RPD) | Tokens per Minute (TPM) |
|---|---|---|---|
| Gemini 2.5 Flash | 5 | 20 | 250,000 |
| Gemini 2.5 Flash-Lite | 10 | 20 | 250,000 |
| Gemini 3 Flash | 5 | 20 | 250,000 |
You can check your current per project limits at: https://aistudio.google.com/usage
- Google’s free Gemini tier only covers text models (plus very limited embeddings). Image models such as
gemini-2.5-flash-image,gemini-2.0-flash-image, and earlierpreview-imagevariants have 0 free daily/minute requests and 0 free input tokens. - Calls to those image models on the free tier return
RESOURCE_EXHAUSTEDwithlimit: 0because no quota is allocated. - To generate images you must have a billing-enabled Gemini API key (pay-as-you-go) or remaining GCP free-trial credits. Without billing, image generation is blocked even if text requests work.
git clone <your-repo-url> landing-genie
cd landing-genie
python -m venv .venv
source .venv/bin/activate # Windows: .venv\\Scripts\\activate
pip install -e .cp .env.example .envEdit .env (see Environment variables below for meanings and defaults).
nvm install 20.19.4
source ~/.nvm/nvm.sh
nvm use 20.19.4
Node version auto-switching
The repo pins a version of Node in.nvmrc. For bash shells, append this helper to~/.bashrc:cat <<'EOF' >> ~/.bashrc load-nvmrc() { local nvmrc="$PWD/.nvmrc" if [ -f "$nvmrc" ]; then nvm use --silent >/dev/null 2>&1 || nvm install fi } export PROMPT_COMMAND="load-nvmrc${PROMPT_COMMAND:+;$PROMPT_COMMAND}" load-nvmrc EOF
Local install via npm (includes Wrangler and Vitest)
npm installWrangler is installed locally, so use npx wrangler (as the deploy command does).
landing-genie init
landing-genie new --prompt "Landing page for an AI habit tracking app" --suggested-subdomain "habitlab"
# Review locally, give feedback if needed, then deploy:
landing-genie deploy habitlab
# Skip image generation for a run:
landing-genie new --prompt "Landing page for an AI habit tracking app" --suggested-subdomain "habitlab" --no-images
# Skip clarifying questions:
landing-genie new --prompt "Landing page for an AI habit tracking app" --suggested-subdomain "habitlab" --no-follow-upsDuring landing-genie new, Gemini proposes a few clarifying questions (audience, product stage, tone, CTA). Answer what matters and press Enter to skip others; your responses are threaded into the first draft.
Pass --no-follow-ups if you want to skip asking Gemini for clarifications entirely.
With GEMINI_API_KEY set, landing-genie new will also render images for any assets/*.png placeholders it finds. Skip with --no-images or regenerate later with landing-genie images <slug> (add --overwrite to replace existing files).
landing-genie images dronehit --overwrite --prompt "Landing page for an AI habit tracking app"# Run all tests
pytest
# Run just the CLI smoke test (uses Gemini CLI with a tiny prompt)
pytest -s tests/test_gemini_cli.py
# Run just the Cloudflare Pages helper tests (fully mocked; no network)
pytest tests/test_cloudflare_api.py
# Run just the image smoke test (uses GEMINI_API_KEY once)
pytest -s tests/test_image_generation.pyNotes:
- Tests load
.envviaConfig.load(), so run from the repo root with your env set. - The CLI test respects
GEMINI_ALLOW_CLI_API_KEY; by default it stripsGEMINI_API_KEYfrom the CLI env. - The image test skips if
GEMINI_API_KEYis unset; it writes the generated file under pytest’s temp dir (e.g.,/tmp/pytest-of-<user>/.../sites/image-smoke/assets/test.png).
The contact form worker is written in JavaScript and tested with Vitest using Cloudflare's Workers test pool.
Run tests:
npm test
-
landing-genie init
Bootstrap config, validate environment, and ensuresites/exists. -
landing-genie new
Generate a new landing using Gemini CLI, gather quick clarifications, serve locally for review, and allow iterative refinements. If--promptis omitted, the CLI will ask for the product description interactively. Pass--no-imagesto skip the post-generation image step. Add--debugto print the full prompt sent to Gemini CLI for this run.- Add
--no-follow-upsto skip asking Gemini for clarifying questions (handy for quick iterations or scripted runs). This also skips image follow-ups; re-enable them explicitly with--image-follow-upsif needed, or disable only image follow-ups with--no-image-follow-ups.
- Add
-
landing-genie deploy <slug>
Deploy an existing landing undersites/<slug>to Cloudflare Pages, creating a dedicated Pages project namedlp-<slug>-<rootdomain>(e.g.,lp-smart-forget-ailablife) and attaching the custom subdomain on your root domain. -
landing-genie images <slug>
Generate images for an existing landing using yourGEMINI_API_KEY(skips existing files unless--overwriteis passed). -
landing-genie list
List generated landings undersites/.
Generated landings include a contact form that posts to /api/contact. This repo deploys a
single Cloudflare Worker during landing-genie deploy that handles that endpoint for all
landing subdomains and forwards submissions to your inbox using Cloudflare Email Routing.
What you need:
- Enable Cloudflare Email Routing for your root domain.
- Set
LEAD_TO_EMAILin your.envto the inbox that should receive submissions.
What the Worker does:
- From:
leads@<ROOT_DOMAIN>with a display name that includes the subdomain. - Subject:
[Landing Lead] - New contact form submission - Body: all submitted fields (no database/storage).
The contact form Worker sends using Cloudflare Email Routing with:
- Sender (From):
leads@<ROOT_DOMAIN>(this is fixed by the deploy code) - Destination (To):
LEAD_TO_EMAILfrom your.env
You must configure Email Routing so that both of these are permitted.
landing-genie deploy <slug> writes .wrangler/contact-form-worker.toml with the Worker’s destination_address (your LEAD_TO_EMAIL) and allowed_sender_addresses (the derived leads@<ROOT_DOMAIN>). If you change LEAD_TO_EMAIL, redeploy to update the Worker config.
Cloudflare Dashboard → Email → Email Routing
Click Get started.
Create one custom address that exactly matches the sender used by the Worker:
leads@<YOUR_ROOT_DOMAIN>
Example:
leads@example.com
Action:
- Choose Send to an email
- Destination: An inbox (i.e. email address) you control (the same value set as
LEAD_TO_EMAIL)
Note: Cloudflare treats destination addresses as case-sensitive exact strings. The address used by your Worker (
LEAD_TO_EMAIL) must exactly match the verified destination address, including letter case.
Cloudflare will send a verification email to the destination inbox.
You must:
- Open the email
- Click Verify email address
After verification:
Email → Email Routing → Overview
If you see:
Email Routing is currently disabled
Click Enable Email Routing.
This step is not automatic and is required even after verification.
When prompted, click Add records and enable.
Cloudflare will add:
- MX records (route mail to Cloudflare)
- SPF TXT record (authorize Cloudflare to send)
- DKIM TXT record (sign outgoing mail)
These records are mandatory for sending email from Workers.
If you skip this step, email sending will fail.
Email → Email Routing → Settings
You must see:
- Routing status: Enabled
- Email DNS records: Configured
- MX records: Locked
Only this state supports sending email from Cloudflare Workers.
- When the local preview opens, hover or click any text-heavy section to launch the Refine section popup.
- Describe the tweak you want (e.g., “make it longer,” “add slight humour,” “focus on compliance leaders”) and landing-genie will regenerate that section with Gemini.
- The preview reloads automatically after the refinement finishes. The editing overlay appears only in local preview, never in deployed builds.
- The generated sites live under
sites/<slug>as static assets. You can edit them manually before deploy. - With
GEMINI_API_KEYset, image files undersites/<slug>/assets/are generated via Gemini's image model after the page is created. Skip with--no-imagesor regenerate later withlanding-genie images <slug>(add--overwriteto replace existing files). GEMINI_API_KEYis stripped from Gemini CLI subprocesses by default so text prompts use your CLI login. SetGEMINI_ALLOW_CLI_API_KEY=1if you intentionally want the CLI to use that key too.- Prompts used for generation live under
prompts/; adjust them to steer tone and structure. .gemini/holds Gemini CLI configs; keep it in sync with your model choices and auth method.- Cloudflare must manage the DNS for your
ROOT_DOMAIN; if nameservers are not pointed to Cloudflare, custom subdomains will not resolve. - Deploys create a new Cloudflare Pages project per subdomain automatically; no need to pre-create one.
All configuration is read from .env (sample in .env.example). Defaults apply when a variable is unset or blank.
| Variable | Required? / Default | Purpose |
|---|---|---|
ROOT_DOMAIN |
required | Root domain you own (pointed to Cloudflare) for generated subdomains. |
CLOUDFLARE_ACCOUNT_ID |
required | Cloudflare account ID used for Pages and DNS. |
CLOUDFLARE_API_TOKEN |
required | API token with Pages:Edit and DNS:Edit for the account. |
LEAD_TO_EMAIL |
required for deploy | Inbox that receives contact form submissions (used by the shared contact form Worker). |
GEMINI_CODE_MODEL |
default gemini-2.5-pro |
Text/code model for page generation via Gemini CLI. |
GEMINI_IMAGE_MODEL |
default gemini-2.5-flash-image |
Image model for rendering assets. |
GEMINI_IMAGE_OUTPUT_COST_PER_1K_TOKENS |
optional | USD cost per 1k tokens for your image model (for cost reporting). |
GEMINI_CLI_COMMAND |
default gemini |
Executable name/path for Gemini CLI. |
GEMINI_API_KEY |
optional | Billing-enabled API key used by Python image generation (not passed to CLI unless allowed). |
GEMINI_ALLOW_CLI_API_KEY |
default unset/0 |
Set to 1 if you want Gemini CLI text calls to use GEMINI_API_KEY instead of CLI login. |
GEMINI_TELEMETRY_OTLP_ENDPOINT |
optional | OTLP collector endpoint; forwarded to CLI runs initiated by landing-genie. Leave blank to rely on the CLI settings file for interactive use. |
LANDING_GENIE_PROMPT_LOG_PATH |
default .log/ |
Path or directory for prompt logs. If a directory or ends with /, logs to <dir>/gemini_prompts.log. |
LANDING_GENIE_PROMPT_LOG_MAX_MB |
default 5 |
Max prompt log size in MB before truncation (respects LANDING_GENIE_PROMPT_LOG_MAX_BYTES if set). |
LANDING_GENIE_MAX_FOLLOW_UP_QUESTIONS |
default 20 |
Max clarifying questions for text prompts. |
LANDING_GENIE_MAX_IMAGE_FOLLOW_UP_QUESTIONS |
default 20 |
Max clarifying questions for image prompts. |
- This project uses
subprocess.runinlanding_genie/cloudflare_api.pyandlanding_genie/gemini_runner.pyto call trusted CLIs (Wrangler and Gemini). All calls useshell=False, pass arguments as a list, and avoid interpolating untrusted user input. Slugs are normalized and validated (only[a-z0-9-]) before being used in paths or commands. - Bandit (
bandit -q -r landing_genie) reports low‑severity warnings (B404/B603) for thesesubprocessusages as a general caution, but there are no medium or high‑severity findings in the package. If you want a noise‑free Bandit run, you can add targeted# nosec B603comments beside those lines with a short justification. - The local preview server (
landing_genie/preview.py) binds to127.0.0.1only, so the/__preview/refineendpoint is not exposed on external interfaces.
The first implementation tried to deploy files directly using Cloudflare’s low-level Pages API. That path looks simple on paper, but in reality it is under-documented, fragile, and extremely easy to break in subtle ways. After multiple failures and inconsistent responses from the API, the deployment flow was replaced with a stable, supported mechanism: invoking Wrangler to handle the upload.
This change made deployments reliable and dramatically reduced the amount of custom logic needed in the codebase.
Cloudflare Pages exposes HTTP endpoints that look like they let you upload static assets directly, but these endpoints are not fully documented and appear to exist primarily for internal use by Wrangler. Because of that:
- Small mistakes in the POST body or multipart formatting cause silent or vague failures.
- The API often accepts a deployment but does not attach the uploaded files, leading to pages that exist in the dashboard but serve 404 or empty responses.
- Error messages are generic and do not point to what is actually wrong.
- The expected multipart field shapes differ from example to example, making debugging guesswork.
This makes it extremely brittle to reimplement the uploader.
-
Incorrect multipart field naming
- The manifest maps file paths to content hashes.
- Cloudflare expects each file upload part to use the hash as the field name, not the file path.
- Our initial implementation sent files under their filenames (
index.html,styles.css), so Cloudflare could not match any assets to the manifest.
-
Undocumented internal endpoints
- Endpoints like
/pages/assets/upload,/pages/assets/upsert-hashes, and/deploymentshave no stable public schema. - Different sources show inconsistent JSON formats.
- Even minor mismatches caused Cloudflare to return “Request body is incorrect” or to create a deployment with no assets.
- Endpoints like
-
Dashboard mismatch
- The dashboard UI showed a successful deployment and listed files.
- But the underlying asset store did not receive the blobs, so visiting the site returned a blank/404 response.
- This makes debugging even harder because the UI suggests everything is fine.
Overall: reimplementing this workflow manually is error-prone and not worth the ongoing maintenance.
Wrangler is Cloudflare’s official CLI tool, and it already implements the entire asset-upload pipeline correctly:
- It calculates hashes exactly as Cloudflare expects.
- It uploads blobs to the correct internal asset store.
- It creates deployments with the correct manifest format.
- It is stable, supported, and kept in sync with Cloudflare’s backend.
- If something breaks, Wrangler gives useful logs and is easy to run manually for debugging.
By shelling out to:
npx wrangler pages deploy <folder> --project-name=<name> --branch=mainthe tool delegates all the fragile low-level upload logic to Cloudflare itself. Our app only needs to:
- Provide the folder path
- Set the correct environment variables
- Configure the custom domain afterward
No reverse-engineering, no guessing multipart structures, no manifest construction by hand.
Using Wrangler:
- Deployments are consistent and predictable.
- Every subdomain correctly serves its unique content.
- The codebase is simpler, smaller, and far easier to maintain.
- Cloudflare handles all the complexity of asset uploading.
This is why the project moved from raw API calls to a Wrangler-based deployment flow.
For up to date information see https://ai.google.dev/gemini-api/docs/pricing.
Google charges for images based on output tokens, not per-image.
The formula is always:
Cost = (Output tokens / 1,000,000) × Model Price
Average token usage per image:
- 1K image (1024×1024): ~1,100–1,300 tokens
- 2K image (2048×2048): ~2,000–2,500 tokens
- 4K image (4096×4096): ~3,500–4,000 tokens
Model prices per 1M output tokens:
- Gemini 3 Pro Image Preview: $120 per 1M tokens
- Gemini Flash Image Preview: $40 per 1M tokens
Effective cost per image:
- Gemini 3 Pro
- 1K/2K image ≈ $0.13–0.14
- 4K image ≈ $0.24–0.48
- Gemini Flash
- 1K/2K image ≈ $0.05
- 4K image ≈ $0.12–0.16
Flash is roughly 3 times cheaper than Pro.
Example for one 1K image with Pro:
Tokens ≈ 1,200
Cost = (1,200 / 1,000,000) × 120 = $0.144
Example for one 1K image with Flash:
Tokens ≈ 1,200
Cost = (1,200 / 1,000,000) × 40 = $0.048
What 1K / 2K / 4K means:
- 1K → roughly 1024×1024 pixels
- 2K → roughly 2048×2048 pixels
- 4K → roughly 4096×4096 pixels
Higher resolution → more output tokens → higher cost.
Current Gemini image models do not allow exact resolution control like width and height parameters.
You cannot explicitly set 1024×1024 or 2048×2048 via API.
You can influence resolution using prompt instructions:
- “Generate a square 4K image”
- “High-resolution portrait”
- “16:9 widescreen landscape”
- “Ultra-high-quality 2048 wide render”
But the model chooses the final size internally.
Only older legacy models allowed explicit resolution fields. New Gemini image models use semantic sizing:
- Ask for “4K” → returns a higher-res image
- Ask for “square” → returns a square
- Ask for “portrait” → returns vertical aspect
For strict resolution control, you must resize after generation.
-
Create a Gemini API key
- Go to https://aistudio.google.com/app/apikey (Google account required).
- Click Create API key, choose or create a GCP project, and copy the key.
- Export it in your shell:
export GEMINI_API_KEY=your_key_here(or add to.env).
-
Enable billing for image generation
- In the same API key dialog, click Manage billing (or open https://console.cloud.google.com/billing).
- Attach a billing account to the project that owns the API key (free tier does not include image quota).
- Confirm the Gemini API is enabled for that project (AI Studio enables it automatically; you can also check in GCP API Library).
- Optional: set budgets/alerts in Cloud Billing to watch spend.
-
Verify access
- Run any simple image call (e.g.,
landing-genie images <slug>) to confirm you don’t getRESOURCE_EXHAUSTEDwithlimit: 0. - If you still see quota errors, ensure billing is active on the same project where the API key was created.
- Run any simple image call (e.g.,
-
Move DNS to Cloudflare
- Sign up/sign in to Cloudflare and add your domain.
- Cloudflare scans existing DNS records; review and confirm (ensure A/AAAA/CNAME/MX/TXT records match your current registrar).
- Cloudflare will show two nameservers (e.g.,
emma.ns.cloudflare.com,ivan.ns.cloudflare.com). - At your registrar (e.g., GoDaddy/Namecheap/Google Domains), replace existing nameservers with the two from Cloudflare.
- Wait for propagation (often minutes, can be up to 24h). Your domain must now be “Active” in Cloudflare for Pages + DNS automation to work.
-
Cloudflare Pages projects are auto-created
- landing-genie creates a new Pages project for each subdomain (pattern
lp-<slug>-<rootdomain>). No manual project setup required.
- landing-genie creates a new Pages project for each subdomain (pattern
-
Create an API token with scoped permissions
- Dashboard:
My Profile→API Tokens→Create Token→Create Custom Token. - Permissions:
Account→Cloudflare Pages→EditAccount→Workers Scripts→EditZone→Workers Routes→EditZone→DNS→EditUser→User Details→Read(required by Wrangler in some environments)
- Account resources: Restrict to the target account (recommended) instead of “All accounts.”
- Zone resources: Restrict to the specific domain (recommended) instead of “All zones.”
- Save the token and set it as
CLOUDFLARE_API_TOKENin.env. Store it securely; you won’t see it again.
- Dashboard:
-
Confirm DNS and Pages readiness
- In Cloudflare DNS, ensure your root domain records are present.
- Custom subdomains created by this tool will be added as DNS records (via API) pointing to the Pages project created for that subdomain.
- If nameservers aren’t pointed to Cloudflare, the automated DNS step will fail.