← Home

Lshoot documentation

Dashboard →

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-app

Then:

cd my-app
pnpm dev

Open http://localhost:3000 — you land on your own dashboard listing the projects in projects/.

What the CLI does

  1. Clones https://github.com/RDH36/Lshoot.git into the directory you name.
  2. Wipes the original git history so you start fresh — your clone is yours.
  3. Replaces app/page.tsx with a project-dashboard landing (lists your projects, links to /dashboard and /docs) — not the public marketing page.
  4. Removes the landing-protection files (.landing-lock, scripts/check-landing.mjs, .husky/pre-commit, the prepare script) since you own this fork now.
  5. 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 version

Source on GitHub · package on npm.

Install 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 lshoot

2. Install dependencies

pnpm install

This 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 puppeteer

4. Start the dev server

pnpm dev

Open 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 start

Only 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.md

The 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

  1. mkdir -p projects/my-app/screenshots projects/my-app/assets
  2. Create projects/my-app/config.json (see below)
  3. Upload captures to /projects/my-app/assets/
  4. Write screenshots NN-name.tsx in screenshots/
  5. Preview at localhost:3000/projects/my-app
  6. 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

FieldRequiredDescription
nameYesCommercial name shown in the dashboard
bundleIdYesReverse-DNS ([a-zA-Z0-9._-]+)
defaultDeviceFrameNoiphone-15-pro / iphone-15 / ipad-13 / android-phone
languagesNoArray of codes (e.g. ["en", "fr"]). Enables switcher + multi-lang export
protectedNoWhen true, export requires a developer code. Used for private/reference projects.
appStoreIdNoReference only
playStoreIdNoReference 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/font in app/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:

ComponentMain props
DeviceFramevariant (iphone-15-pro, iphone-15, ipad-13, android-phone)
AppMockupsrc, device, fit (cover/contain)
Headlinesize (xl → 6xl), color, align
Subheadlinesize (sm → xl), color, align
GradientBackgroundfrom, to, via?, direction
SolidBackgroundcolor
PatternBackgroundpattern (dots/grid/waves), color, size, opacity
CenteredLayoutheadline, subheadline, mockup, padding, textPosition
SplitLayouttext, 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 languages is 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:

  1. Visits /preview/{slug}/{screenshot}?format={id}&lang={lang}
  2. Viewport at 2x DPR
  3. Waits for fonts.ready + all images loaded + double rAF
  4. Captures via boundingBox of [data-screenshot-canvas]
  5. 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 puppeteer

Port 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.