Introduction
Lshoot generates ASO (App Store / Play Store) screenshots for a mobile app from React components. You describe each screen in JSX (headline, mockup, background) and the app produces the PNGs in every format Apple + Google require, in every language you declare.
Two ways to use it: with Claude Code (the AI writes screenshots from an ASO brief) or manually (you write the JSX yourself).
Applicable to any type of app: games, wellness, productivity, SaaS, finance, e-commerce, dev tools, social, health, education. Each app has its own brand — adapt the templates to the tone.
Quickstart with CLI
The fastest way to bootstrap your own Lshoot instance is the lshoot CLI (zero dependency, ~9 KB). It clones the repo, swaps the public marketing landing for your personal project dashboard, and offers to install dependencies.
One command
npx lshoot my-app
# or
pnpm dlx lshoot my-appThen:
cd my-app
pnpm devOpen http://localhost:3000 — you land on your own dashboard listing the projects in projects/.
What the CLI does
- Clones
https://github.com/RDH36/Lshoot.gitinto the directory you name. - Wipes the original git history so you start fresh — your clone is yours.
- Replaces
app/page.tsxwith a project-dashboard landing (lists your projects, links to/dashboardand/docs) — not the public marketing page. - Removes the landing-protection files (
.landing-lock,scripts/check-landing.mjs,.husky/pre-commit, thepreparescript) since you own this fork now. - Prompts you to run
pnpm install(if pnpm is available).
Requirements
- Node.js 20+
- Git
- pnpm (optional during install, required to run the dev server — install via
corepack enable)
Other commands
lshoot --help # show usage
lshoot --version # show the CLI versionInstall manually
Prefer to clone yourself? Or already cloned and want to set things up by hand? Follow the steps below. (If you used npx lshoot above, skip this section — everything is already done.)
Lshoot is a local-first Next.js app. Everything runs on your machine.
Prerequisites
- Node.js 20+ (check with
node --version) - pnpm 10+ (install with
npm install -g pnpm) - Git
1. Clone the repo
git clone <your-fork-or-origin-url> lshoot
cd lshoot2. Install dependencies
pnpm installThis installs Next.js, Puppeteer (with Chromium), Sharp, Zod, shadcn/ui, Husky (git hooks), and the Google fonts. Total download is around ~400 MB (most of it is Chromium for Puppeteer).
3. Chromium download (if Puppeteer skipped it)
pnpm 10 blocks install scripts by default. The package.json whitelists Puppeteer and Sharp via pnpm.onlyBuiltDependencies. If Chromium is still missing:
pnpm rebuild puppeteer4. Start the dev server
pnpm devOpen http://localhost:3000. You will land on the home page.
Structure of the repo
lshoot/
├── app/ Next.js App Router (landing, dashboard, API, preview, /docs)
├── components/aso/ Screenshot component library (DeviceFrame, AppMockup, ...)
├── components/ui/ shadcn components for the dashboard
├── projects/{slug}/ Your projects (config.json + screenshots + assets)
├── lib/ Formats spec, Puppeteer pipeline, Sharp export, schemas
├── exports/ Generated PNGs (gitignored)
├── .claude/skills/ Claude Code skill for automated project creation
└── scripts/ Internal scripts (landing page protection, etc.)Build for production (optional)
pnpm build
pnpm startOnly useful if you want to serve Lshoot on a machine other than your own dev laptop. For day-to-day use, pnpm dev is fine.
First run
After pnpm dev, the dashboard lists projects found in projects/. A demo project (example-app) and a full reference project (flipia) are provided.
Open the dashboard
Click Get Started in the nav (top-right) then go to Dashboard, or jump directly to localhost:3000/dashboard.
Preview a screenshot
From the dashboard, click any project card, then any thumbnail. You land on the preview route /preview/{slug}/{screenshot} that renders the screenshot at its target viewport size (1290×2796 by default).
Export a PNG
On the project page, click Export. A popover lets you pick which screenshots, formats, and languages to export. Files land in /exports/{slug}/{lang}/{store}/{format}/.
With Claude Code (AI)
Automated workflow via the new-aso-project skill.
1. Prepare the brief
Write an ASO brief (freeform or structured Markdown) describing your app: name, bundleId, value prop, audience, features, brand colors, tone, target languages. If you have the app's source code, mention the path — the agent will extract the exact palette and fonts.
Briefs typically live in /aso-project/{slug}/aso.md(gitignored). You can also paste the brief directly in the chat.
2. Invoke the skill
/new-aso-project aso-project/my-app/aso.mdThe agent reads the brief, asks for missing details, scaffolds the project in /projects/{slug}/, writes config.json + 6-8 JSX screenshots, creates an i18n dictionary if multilingual, and asks you to upload assets.
3. Upload app captures
From the dashboard → project page → Assets button → drag and drop your Xcode simulator / Android emulator captures (PNG, JPG, WebP, 10 MB max).
4. Preview
The project page shows a live grid of thumbnails (scaled iframes). Click any thumbnail to open the full-size preview. If you declared languages, an FR/EN/… switcher appears.
5. Export
Click Export → popover with checkboxes (screenshots, formats, languages). Pick what you want and click Export N PNGs. Files land in /exports/{slug}/{lang}/{store}/{format}/.
Manually
If you prefer writing JSX yourself (no AI), you can scaffold the project by hand and create files directly. The sections below detail each step.
Steps
mkdir -p projects/my-app/screenshots projects/my-app/assets- Create
projects/my-app/config.json(see below) - Upload captures to
/projects/my-app/assets/ - Write screenshots
NN-name.tsxinscreenshots/ - Preview at
localhost:3000/projects/my-app - Click Export and select formats/languages
Project config
Each project = a folder in /projects/ with a minimal config.json:
{
"name": "My App",
"bundleId": "com.company.myapp",
"defaultDeviceFrame": "iphone-15-pro",
"languages": ["en", "fr"]
}Fields
| Field | Required | Description |
|---|---|---|
name | Yes | Commercial name shown in the dashboard |
bundleId | Yes | Reverse-DNS ([a-zA-Z0-9._-]+) |
defaultDeviceFrame | No | iphone-15-pro / iphone-15 / ipad-13 / android-phone |
languages | No | Array of codes (e.g. ["en", "fr"]). Enables switcher + multi-lang export |
protected | No | When true, export requires a developer code. Used for private/reference projects. |
appStoreId | No | Reference only |
playStoreId | No | Reference only |
Writing a screenshot
A screenshot = a file NN-name.tsx in projects/{slug}/screenshots/. The NN prefix sets the order in the store.
Rules
- Single
export default - Component fills
w-full h-full - Don't wrap in
<ScreenshotCanvas>— the preview route does it - Imports: only
@/components/aso(+ local project components). No shadcn, no lucide - No
"use client", no React state - No responsive classes (
sm:,md:) - Web fonts: via
next/fontinapp/layout.tsx
Minimal example
import { GradientBackground, Headline, Subheadline } from "@/components/aso";
export default function Hero() {
return (
<div className="w-full h-full">
<GradientBackground from="#6366f1" to="#ec4899" direction="to-br" />
<div className="relative w-full h-full flex flex-col items-center justify-center px-[8%] text-center">
<Headline size="6xl" color="#ffffff">
Build habits<br />that stick
</Headline>
<Subheadline size="xl" color="#f5f3ff">
5 minutes a day
</Subheadline>
</div>
</div>
);
}Available components
All exported from @/components/aso:
| Component | Main props |
|---|---|
DeviceFrame | variant (iphone-15-pro, iphone-15, ipad-13, android-phone) |
AppMockup | src, device, fit (cover/contain) |
Headline | size (xl → 6xl), color, align |
Subheadline | size (sm → xl), color, align |
GradientBackground | from, to, via?, direction |
SolidBackground | color |
PatternBackground | pattern (dots/grid/waves), color, size, opacity |
CenteredLayout | headline, subheadline, mockup, padding, textPosition |
SplitLayout | text, mockup, direction, reverse |
8 reference templates
projects/example-app/screenshots/ contains 8 copy-paste ASO patterns: Hero + Typography, Device Center, Split Layout, Tilted 3D, Minimalist, Floating Callouts, Dark SaaS, Call to Action.
Uploading assets
Assets are raw captures of your app (Xcode simulator or Android emulator).
- Accepted formats: PNG, JPG, WebP (max 10 MB)
- From the dashboard: Assets (N) button on the project page → drag & drop
- Or directly: copy files into
/projects/{slug}/assets/
Reference in a screenshot via the API route:
<AppMockup src="/api/assets/my-app/home.png" device="iphone-15-pro" />Never reference/projects/...directly — the/api/assets/...route is secured against path traversal.
Multi-language
To support multiple languages in a single project:
1. Create the dictionary
Create projects/{slug}/i18n.tsx:
import type { ReactNode } from "react";
export const LANGUAGES = ["en", "fr"] as const;
type ScreenT = {
headline: (accent: string) => ReactNode;
sub: string;
};
const EN = {
duel: {
headline: (c: string) => <>Memory just got<br /><span style={{ color: c }}>competitive</span></>,
sub: "Face real players online",
},
};
const FR = {
duel: {
headline: (c: string) => <>Le memory<br />devient un <span style={{ color: c }}>duel</span></>,
sub: "Affronte de vrais joueurs en ligne",
},
};
export function useT(lang?: string) {
return lang === "fr" ? FR : EN;
}2. Use in screenshots
import { useT } from "../i18n";
const ACCENT = "#A2340A";
export default function Duel({ lang }: { lang?: string }) {
const t = useT(lang);
return (
<MyLayout
headline={<h1>{t.duel.headline(ACCENT)}</h1>}
subheadline={<p>{t.duel.sub}</p>}
/>
);
}3. Declare in config.json
{ "languages": ["en", "fr"] }Custom fonts
To match a specific app's identity, add its fonts in app/layout.tsx via next/font/google:
import { Fredoka, Nunito } from "next/font/google";
const fredoka = Fredoka({
variable: "--font-fredoka",
subsets: ["latin"],
weight: ["500", "600", "700"],
});
// In <html className={...}> append:
// ${fredoka.variable}Then in screenshots:
<div style={{ fontFamily: "var(--font-fredoka), sans-serif" }}>
{/* all children inherit */}
</div>Advanced HTML mockup
For apps where you want a faithful and localizable inner render, write the mockup in React inside projects/{slug}/components/ instead of uploading a PNG.
When to use it
- The app has an iconic UI to showcase (game, dashboard, map)
- Color emojis must stay color (Sharp/SVG renders them black — only Chromium renders them in color)
- Content needs to be localizable
- Frequent iteration on the look
See projects/flipia/components/GameMockup.tsx as a reference.
Exporting
The Export button on the project page opens a popover where you pick:
- Screenshots to export (all by default)
- Formats: App Store + Play Store (required only by default)
- Languages: if
languagesis in config
Render pipeline
Puppeteer launches Chromium with 26 Remotion-style flags (--force-color-profile=srgb, --font-render-hinting=none, background processes disabled…). For each screenshot × format × language:
- Visits
/preview/{slug}/{screenshot}?format={id}&lang={lang} - Viewport at 2x DPR
- Waits for fonts.ready + all images loaded + double rAF
- Captures via
boundingBoxof[data-screenshot-canvas] - Sharp downsamples in Lanczos3, PNG compressed
Output structure
- With languages:
/exports/{slug}/{lang}/{store}/{format-id}/{screenshot}.png - Without languages:
/exports/{slug}/{store}/{format-id}/{screenshot}.png
Via curl (CLI)
curl -N -X POST http://localhost:3000/api/export \
-H "Content-Type: application/json" \
-d '{"project":"my-app","langs":["en","fr"]}'Protected projects
If a project's config.json has "protected": true, the export endpoint requires a developer code. In the UI, a prompt asks for the code before running. Via curl, pass it in the body:
curl -X POST http://localhost:3000/api/export \
-H "Content-Type: application/json" \
-d '{"project":"flipia","devCode":"<code>"}'Troubleshooting
Puppeteer doesn't download Chromium
pnpm rebuild puppeteerPort 3000 busy
pkill -f "next-server"Device frame appears black / empty
Ensure AppMockup has a parent container with defined height (flex container).
"N" Next.js badge in exports
next.config.ts must contain devIndicators: false.
Emojis appear black in an uploaded PNG
Sharp/SVG doesn't render color emojis. Use an HTML mockup in components/ (rendered by Chromium).
/preview shows a dynamic import error
Check that the file exists and its name only contains [a-zA-Z0-9._-].
Export is refused on a project
The project is protected ("protected": true in config.json). Enter the developer code in the prompt, or pass devCode in the API body.