diff --git a/CLAUDE.md b/CLAUDE.md index b14de89..03c7366 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,16 +1,21 @@ # CLAUDE.md — OGCOPS +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + ## What is this? -OGCOPS is a free, open-source OG image generator and social media preview checker. Built with Astro + React Islands, deployed to Vercel. +OGCOPS is a free, open-source OG image generator and social media preview checker. Built with Astro SSR + React Islands, deployed to Vercel at og.codercops.com. GitHub: github.com/codercops/ogcops. MIT licensed. ## Commands ```bash -npm run dev # Start dev server -npm run build # Production build -npm run preview # Preview production build -npm run test # Run vitest -npm run test:watch # Watch mode -npm run check # Type-check (astro check + tsc) +npm run dev # Start dev server (port 4321) +npm run build # Production build +npm run preview # Preview production build +npm run test # Run vitest (single run) +npm run test:watch # Watch mode +npm run test:ui # Vitest UI +npm run check # Type-check (astro check + tsc --noEmit) +npm run lint # Astro linting +npm run generate:favicons # Generate favicon assets ``` ## Architecture @@ -19,20 +24,32 @@ npm run check # Type-check (astro check + tsc) - **Satori** runs client-side for instant SVG preview (zero server calls during editing) - **Satori + resvg-wasm** runs server-side for PNG generation (`/api/og`) - **No database** — state lives in URL query params + client-side useReducer -- **CORS-open API** for developer use +- **CORS-open API** — no API key, no rate limits ## Key Directories -- `src/templates/` — 109 templates across 12 categories. Each template is a `.ts` file exporting a `TemplateDefinition`. -- `src/lib/` — Core engine (og-engine.ts, font-loader.ts, meta-fetcher.ts, meta-analyzer.ts) +- `src/templates/` — 120 templates across 12 categories. Each is a `.ts` file exporting a `TemplateDefinition`. +- `src/lib/` — Core engine (og-engine.ts, font-loader.ts, meta-fetcher.ts, meta-analyzer.ts, api-validation.ts) - `src/components/editor/` — React components for the OG image editor - `src/components/preview/` — React components for the social media preview checker -- `src/pages/api/` — API endpoints (og, preview, templates) +- `src/pages/api/` — API endpoints +- `src/styles/` — CSS files (global.css, editor.css, preview.css, api-docs.css) +- `public/fonts/` — Bundled .woff fonts (Inter, Playfair Display, JetBrains Mono) +- `tests/` — Vitest tests (api/, lib/, templates/) -## Conventions -- CSS custom properties only (no Tailwind). Accent: `#E07A5F`. -- TypeScript strict mode. Path alias `@/*` → `src/*`. -- Fonts bundled as `.woff` in `public/fonts/`. -- Templates follow `TemplateDefinition` interface in `src/templates/types.ts`. +## Pages & API Endpoints + +**Pages:** +- `/` — Homepage +- `/create/` — OG image editor +- `/templates` — Template gallery +- `/preview` — Social media preview checker +- `/api-docs` — API documentation + +**API (all CORS-open, no auth):** +- `GET /api/og?template={id}&...` — Generate PNG (1200x630, 24h cache) +- `GET /api/preview?url={url}` — Fetch and analyze a URL's meta tags +- `GET /api/templates` — List all templates as JSON +- `GET /api/templates/[id]/thumbnail.png` — Template thumbnail (1-week cache) ## Reference Files - Template interface: `src/templates/types.ts` @@ -42,12 +59,34 @@ npm run check # Type-check (astro check + tsc) - API validation schemas: `src/lib/api-validation.ts` - Template registry: `src/templates/registry.ts` -## Template Contribution -1. Create `src/templates/{category}/{id}.ts` -2. Export a `TemplateDefinition` -3. Register in `src/templates/{category}/index.ts` +## Template System + +120 templates across 12 categories: blog, product, saas, github, event, podcast, developer, newsletter, quote, ecommerce, job, tutorial. + +**Adding a template:** +1. Create `src/templates/{category}/{id}.ts` exporting a `TemplateDefinition` +2. Register in `src/templates/{category}/index.ts` +3. Add import + registration in `src/templates/registry.ts` 4. Run `npm run test` to verify +**Template field types:** text, textarea, color, select, number, toggle, image. +**Field groups:** Content, Style, Brand. + +## Conventions +- CSS custom properties only (no Tailwind). Accent: `#E07A5F`. +- TypeScript strict mode. Path alias `@/*` → `src/*`. +- Fonts bundled as `.woff` in `public/fonts/` (Inter Regular/Medium/SemiBold/Bold, Playfair Display Regular/Bold, JetBrains Mono Regular/Bold). +- Node 22 (`.nvmrc`). +- Testing: Vitest with node environment. Tests in `tests/**/*.test.ts`. Globals enabled. + +## CI/CD +- Runs on push to `production`/`dev` and PRs to those branches +- Steps: `npm run check` → `npm run test` → `npm run build` +- **Branch strategy:** `dev` is the default branch. PRs target `dev`. Releases go `dev` → `production`. Direct PRs to `production` are blocked unless from `dev`. + +## Environment Variables (`.env.local`, all optional) +- `UPSTASH_REDIS_REST_URL`, `UPSTASH_REDIS_REST_TOKEN` — Optional visitor counter + ## Gotchas / Constraints - Satori does **not** support CSS grid — only flexbox - Every `div` must have `display: 'flex'` explicitly in its style @@ -55,6 +94,7 @@ npm run check # Type-check (astro check + tsc) - Font files must be listed in `astro.config.mjs` `includeFiles` array for Vercel deployment - WASM imports need `optimizeDeps.exclude` in the Vite config - `renderToPng` returns `ArrayBuffer` (not `Buffer`) for `BodyInit` compatibility +- Canvas is always 1200x630px ## Do NOT - Add Tailwind CSS — the project uses CSS custom properties only diff --git a/src/components/Header.astro b/src/components/Header.astro index 086b61a..7ae7f71 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -36,41 +36,82 @@ const navItems = [ - - - -
-
- {navItems.map((item, i) => ( - {item.label} - ))} - - - Star on GitHub - {starCount !== null && {formatStarCount(starCount)}} - + + -
+ diff --git a/src/components/editor/EditorApp.tsx b/src/components/editor/EditorApp.tsx index e8f9035..b992b98 100644 --- a/src/components/editor/EditorApp.tsx +++ b/src/components/editor/EditorApp.tsx @@ -31,6 +31,17 @@ export function EditorApp({ initialCategory }: EditorAppProps) { const { svg, loading, error, render } = useSatoriRenderer(); const [mobileTab, setMobileTab] = useState<'templates' | 'customize' | 'export'>('customize'); + const [isMobile, setIsMobile] = useState(() => + typeof window !== 'undefined' ? window.innerWidth <= 768 : false + ); + + useEffect(() => { + const mq = window.matchMedia('(max-width: 768px)'); + const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches); + mq.addEventListener('change', handler); + setIsMobile(mq.matches); + return () => mq.removeEventListener('change', handler); + }, []); // Find current template definition (use registry lookup to ensure render function is present) const currentTemplate = useMemo( @@ -59,8 +70,11 @@ export function EditorApp({ initialCategory }: EditorAppProps) { const handleTemplateSelect = useCallback( (template: TemplateDefinition) => { setTemplate(template); + if (isMobile) { + setMobileTab('customize'); + } }, - [setTemplate] + [setTemplate, isMobile] ); const handleReset = useCallback(() => { @@ -114,11 +128,20 @@ export function EditorApp({ initialCategory }: EditorAppProps) { />
- {/* Right: Customize */} + {/* Right: Customize / Export */}
{mobileTab === 'export' ? (
+
+ +
+
) : ( copyToClipboard(apiUrl, 'url')} > - {copied === 'url' ? 'Copied!' : 'Copy URL'} + + {copied === 'url' ? 'Copied!' : 'Copy Image URL'}
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 0cb1229..1d0ce60 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -31,7 +31,7 @@ const fullTitle = title === 'OGCOPS' ? title : `${title} | ${siteName}`; - + {noindex && } @@ -106,7 +106,7 @@ const fullTitle = title === 'OGCOPS' ? title : `${title} | ${siteName}`; -
+
diff --git a/src/layouts/ToolLayout.astro b/src/layouts/ToolLayout.astro index 1c21182..37acc6d 100644 --- a/src/layouts/ToolLayout.astro +++ b/src/layouts/ToolLayout.astro @@ -19,7 +19,7 @@ const fullTitle = `${title} | ${siteName}`; - + @@ -61,7 +61,14 @@ const fullTitle = `${title} | ${siteName}`; body { display: flex; flex-direction: column; + min-height: 100vh; + min-height: 100dvh; height: 100vh; + height: 100dvh; overflow: hidden; + padding-top: env(safe-area-inset-top); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); + padding-bottom: env(safe-area-inset-bottom); } diff --git a/src/pages/api-docs.astro b/src/pages/api-docs.astro index 9c56591..591db5b 100644 --- a/src/pages/api-docs.astro +++ b/src/pages/api-docs.astro @@ -5,24 +5,29 @@ import '@/styles/api-docs.css';
- + +
+

API Documentation

+

+ Generate OG images, check URL meta tags, and list templates with our free REST API. + No API key required. CORS-enabled for browser use. +

+
+ +