Sales-oriented marketing site for Robopipe (industrial machine vision by KOALA42).
Stack: Next.js 16 (App Router) + Payload CMS 3 embedded in one app · PostgreSQL · Tailwind v4 · next-intl (cs/en) · Resend · Google Cloud Storage media. Deployed to Coolify as a Docker image.
docker compose up -d # Postgres 17 on :5432
cp .env.example .env # defaults work out of the box
pnpm install
pnpm payload migrate # apply DB migrations
pnpm seed # demo content in cs+en, admin user
pnpm dev- Site: http://localhost:3000 (redirects to
/csor/enby browser language) - Admin: http://localhost:3000/admin — seeded login
admin@robopipe.io/admin
Optional env: RESEND_API_KEY (lead notification emails; logged to console when unset), GCS_BUCKET + GCS_CREDENTIALS (media storage; local ./media when unset).
- Localization — content is field-localized in Payload (
csdefault,en), URLs are prefixed (/cs/*,/en/*), and slugs are localized (/cs/cenik↔/en/pricing). Section segments (/blog,/case-studies) are constant. hreflang alternates are emitted per document; the language switcher reads them from the head to land on the translated slug. - Blog visibility — posts/case studies appear only in locales where they have a slug (queries run with
fallbackLocale: false); marketing pages are expected to be fully translated. - Pages are block-based (
src/blocks/*): hero, feature grid, content, media, stats, logo cloud, testimonials, use-case cards, pricing table, FAQ accordion, case-study grid, blog teaser, CTA banner, contact form. - Leads — the contact form posts a server action (
src/actions/submitLead.ts): zod validation, honeypot + per-IP rate limit, stored in theleadscollection (public REST create is closed), notification via Resend tosite-settings.leadNotificationEmail. - Revalidation — any published change triggers coarse site-wide ISR revalidation (
src/hooks/revalidate.ts); pages also refresh every 10 min. - SEO — localized sitemap with hreflang, robots, per-locale RSS (
/{locale}/blog/rss.xml), Article JSON-LD, 301 map from the old Webflow URLs innext.config.tsplus an editor-managedredirectscollection. - Draft preview — Payload live-preview +
/next/preview?secret=…(draft mode), exit via/next/exit-preview.
Dev uses migrations too — after changing collections run:
pnpm payload migrate:create <name>
pnpm payload migrate
pnpm generate:typesCommit the generated migration and src/payload-types.ts.
Editors queue article ideas in Blog → Blog topics (short description + optional per-article instructions). A nightly Coolify scheduled task (03:00 UTC) hits /api/cron/run-jobs (authorized via CRON_SECRET), which runs the generate-blog-post workflow (src/jobs/):
- Claude (
claude-fable-5, web search enabled, server-side refusal fallback to Opus 4.8) writes the English article as structured JSON, picking a category. - A second call adapts it into Czech (not a literal translation).
- A hero photo is fetched from Pexels and uploaded to Media with localized alt text.
- A bilingual draft post is assembled (markdown → Lexical via
convertMarkdownToLexical); the topic's target date prefillspublishedAt; a "draft ready" email goes out via Resend.
Publishing is always manual — review the draft and hit Publish. Regenerate by setting the topic's status back to Queued (content is overwritten; old text stays in version history), or skip the nightly wait entirely with the Generate now button in the topic sidebar (POST /api/blog-topics/:id/generate — responds immediately and runs the job via after(); the button polls until the draft is ready). The base writing prompt lives in the Blog AI global, editable without a deploy. Steps are resumable inline tasks, so a failed/timed-out run resumes where it stopped on the next cron hit.
Required env vars: ANTHROPIC_API_KEY (the Anthropic org must allow ≥30-day data retention — Fable requirement), PEXELS_API_KEY, CRON_SECRET. To run the queue locally: create a topic, then curl -H "Authorization: Bearer $CRON_SECRET" "http://localhost:3000/api/payload-jobs/run?limit=10".
The app ships as a Docker image (Dockerfile, Next.js standalone output) deployed with Coolify's Dockerfile build pack. The build runs pnpm payload migrate && pnpm build against the production database, so each deployment migrates the DB before next build prerenders against it.
-
Database — a PostgreSQL resource from Coolify's template on the same server/destination as the app. Use its internal URL for
DATABASE_URL: Coolify builds with--add-hostentries for the destination network's containers, so the internal hostname also resolves during the image build. -
Media — a Google Cloud Storage bucket with public read (
allUsers→ Storage Object Viewer) and a service account with Storage Object Admin on it. Media URLs point straight athttps://storage.googleapis.com/<bucket>/…. -
Application — new resource → Git repository → build pack Dockerfile, port
3000, domainshttps://robopipe.ioandhttps://shop.robopipe.io(the old Shopify host is redirected bynext.config.ts). Under Advanced → Build, prefer Build secrets → Docker BuildKit secrets so build-time values don't end up in image metadata; the Dockerfile accepts both secrets and plain build args. -
Environment variables
Variable Build Runtime Notes DATABASE_URL✓ ✓ internal URL of the Postgres resource NEXT_PUBLIC_SERVER_URL✓ ✓ https://robopipe.io, inlined into the client bundleGCS_BUCKET✓ ✓ media URLs are baked into prerendered pages PAYLOAD_SECRET✓ the build uses a placeholder when it's not available GCS_CREDENTIALS✓ service-account key JSON, on one line PREVIEW_SECRET,CRON_SECRET,RESEND_API_KEY,ANTHROPIC_API_KEY,PEXELS_API_KEY✓ Changing a build-time value needs a redeploy; runtime-only values just a restart.
-
Scheduled task (nightly blog generation) — on the application, Scheduled Tasks → add: frequency
0 3 * * *, timeout900s (one article takes ~3–5 min), command:wget -qO- --header="Authorization: Bearer $CRON_SECRET" http://localhost:3000/api/cron/run-jobs
- Build memory —
pnpm buildraises the Node heap to 8 GB; the Coolify server needs that much free RAM during builds (or lower--max-old-space-sizeinpackage.json). - Single instance — the lead/newsletter rate limits are in-memory and ISR caches on the container filesystem; run one replica.
- Analytics —
<Analytics />/<SpeedInsights />in the frontend layout are Vercel-only and collect nothing on Coolify; replace them (e.g. self-hosted Plausible/Umami) or remove them. - Geo — the locale-suggestion feature reads Vercel's
x-vercel-ip-countryheader insrc/proxy.ts, so it stays dormant on Coolify unless that header is provided (e.g. mapped from Cloudflare'scf-ipcountry). - Seeding prod (one-time): run
pnpm seedlocally with the productionDATABASE_URL(expose the Postgres resource publicly for the duration) and the GCS variables exported, then redeploy so prerendered pages pick up the content.