152 Commits

Author SHA1 Message Date
Fatih Kadir Akın 9498aa82b4 chore(cloudflare): update Dockerfile and .gitignore for Cloudflare deployment
Entire-Checkpoint: 3684d8febba1
2026-03-25 10:29:02 +03:00
Fatih Kadir Akın f724bb7951 style(components/ads): update ezoic ad container styling 2026-03-22 18:27:51 +03:00
gokbeyinac 47f085b0ce Remove prompt: Kickstart Prompt for UX & UI Design 2026-03-22 03:52:31 +00:00
sametheredge85@gmail.com 024307a329 Add prompt: KJV Harmony Companion 2026-03-22 03:52:31 +00:00
c.burke0327@gmail.com d25abace00 Add prompt: Ultimate Stake.us Dice Strategy Builder — All Risk Levels & Bankrolls 2026-03-22 03:52:31 +00:00
tuanductran 84bc3cf9a9 Add prompt: Reflective Companion, Not Advice 2026-03-22 03:52:30 +00:00
gamaleldientarek@gmail.com 900da199cc Add prompt: Illustrator Style Describer Weavy 2026-03-22 03:52:30 +00:00
xiaoyucunx 7ce8778c7d Update prompt: Code Review Specialist 2026-03-22 03:52:30 +00:00
cperalesg@gmail.com 46abdcc528 Update prompt: Prompt Generator 2026-03-22 03:52:29 +00:00
panda667 af75cf7116 Update prompt: Astrologer 2026-03-22 03:52:29 +00:00
joembolinas 46b3d86dbb Update prompt: The Technical Co-Founder: Building Real Products Together 2026-03-22 03:52:29 +00:00
aitank2020@gmail.com 0b61819b2d Update prompt: Echoes of the Rust Age 2026-03-22 03:52:28 +00:00
dragoy18@gmail.com 21eede5c27 Update prompt: Code Review Specialist 2 2026-03-22 03:52:28 +00:00
lior1976@gmail.com 5d284ddb55 Update prompt: Article Summarizer 2026-03-22 03:52:27 +00:00
beresasis@gmail.com 9b3ea0bf7c Update prompt: Interview Preparation Coach 2026-03-22 03:52:27 +00:00
sinansonmez f89656b582 Update prompt: Code Review Assistant 2026-03-22 03:52:27 +00:00
giorgiop 7691c30bd5 Update prompt: Virtual Doctor 2026-03-22 03:52:26 +00:00
Fatih Kadir Akın 622b02bb80 Merge pull request #1094 from Goro2030/fix/docker-supervisord-socket 2026-03-21 21:40:23 +00:00
Mariano Silva 9156cece88 Fix Docker bootstrap: supervisord socket not configured
supervisord.conf was missing [unix_http_server], [supervisorctl], and
[rpcinterface:supervisor] sections, so the Unix socket was never created.
This caused the readiness check to always fail and the container to exit
with "Supervisord failed to start".

- Add missing supervisord socket and RPC configuration sections
- Improve bootstrap readiness check to verify both process and socket
- Add early exit if supervisord process dies unexpectedly
- Simplify supervisorctl calls (config path no longer needed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:38:42 -03:00
gokbeyinac 08e85a3827 Remove prompt: Kickstart Prompt for UX & UI Design 2026-03-21 03:47:00 +00:00
mcyenerr@gmail.com 85969a537b Add prompt: Betting Prediction
Co-authored-by: devisasari <devisasari@users.noreply.github.com>
2026-03-21 03:46:59 +00:00
mmogdeveloper 0e8a742dd9 Add prompt: Stock 2026-03-21 03:46:59 +00:00
fede.gazzelloni@gmail.com e9b8d6c6a5 Update prompt: Article Summarizer 2026-03-21 03:46:59 +00:00
debashis.sarker@gmail.com c103d1d708 Update prompt: The Technical Co-Founder: Building Real Products Together 2026-03-21 03:46:58 +00:00
aymanrabeemac@gmail.com 279890f962 Update prompt: Code Review Specialist 2026-03-21 03:46:58 +00:00
nolanneff f4b1be8330 Update prompt: Code Review Specialist 2 2026-03-21 03:46:57 +00:00
ersinkoc f0f05357ca Update prompt: Echoes of the Rust Age 2026-03-21 03:46:57 +00:00
cnwdy888@gmail.com 2d6a628391 Update prompt: Interview Preparation Coach 2026-03-21 03:46:57 +00:00
guangzhongzhang978@gmail.com cfef0bfc4b Update prompt: Virtual Doctor 2026-03-21 03:46:56 +00:00
f 9943bbe4b7 Update prompt: Code Review Assistant 2026-03-21 03:46:56 +00:00
devisasari 4f26d270ed Update prompt: Astrologer 2026-03-21 03:46:55 +00:00
iuzn 6c66be57a6 Update prompt: Prompt Generator 2026-03-21 03:46:55 +00:00
wkaandemir 7bc643c889 Remove prompt: video-analysis-expert 2026-03-20 03:50:52 +00:00
wkaandemir b77c8ed13d Remove prompt: performance-engineer 2026-03-20 03:50:52 +00:00
wkaandemir 7e6d9ff4a2 Remove prompt: frontend-architect 2026-03-20 03:50:51 +00:00
wkaandemir d237a6a7f3 Remove prompt: security-engineer 2026-03-20 03:50:51 +00:00
wkaandemir ec63c8e3ff Remove prompt: root-cause-analyst 2026-03-20 03:50:51 +00:00
wkaandemir bc1960f1a5 Remove prompt: repo-indexer 2026-03-20 03:50:50 +00:00
wkaandemir b3b2a4a09c Remove prompt: refactoring-expert 2026-03-20 03:50:50 +00:00
wkaandemir 7f02c0fa8a Remove prompt: quality-engineer 2026-03-20 03:50:49 +00:00
wkaandemir 6d0b72ebd4 Remove prompt: devops-architect 2026-03-20 03:50:49 +00:00
wkaandemir 9972e8b2b4 Remove prompt: bug-risk-analysis 2026-03-20 03:50:49 +00:00
wkaandemir 22cef958c6 Remove prompt: deep-research-agent 2026-03-20 03:50:48 +00:00
winningt25-ux c234ed3f38 Add prompt: Grok customize 2026-03-20 03:50:48 +00:00
mmanisaligil 1c810f3e28 Add prompt: Business Risk & Scenario Analyzer 2026-03-20 03:50:47 +00:00
mmanisaligil 6043696ca7 Add prompt: Go-To-Market Execution Planner 2026-03-20 03:50:47 +00:00
mmanisaligil e7f81e24dc Add prompt: Revenue Model & Unit Economics Analyzer 2026-03-20 03:50:47 +00:00
mmanisaligil 28a2806d4b Add prompt: Market Entry Strategy Engine 2026-03-20 03:50:47 +00:00
mmanisaligil 9683bef22e Add prompt: Strategic Business Blueprint Generator 2026-03-20 03:50:46 +00:00
mmanisaligil 3e53eebcf4 Add prompt: High-Stakes Decision Support System 2026-03-20 03:50:46 +00:00
mmanisaligil b406431755 Add prompt: Legal Risk Minimization Tool for Freelancers 2026-03-20 03:50:46 +00:00
mmanisaligil caf297283c Add prompt: Zero to One Solo-Founder Launch System 2026-03-20 03:50:45 +00:00
mmanisaligil 9f7c200e69 Add prompt: Personal Knowledge & Narrative Tool 2026-03-20 03:50:45 +00:00
mmanisaligil 4f26d0e120 Add prompt: Trading & Investing Simulation Platform 2026-03-20 03:50:45 +00:00
mmanisaligil 039a4028a3 Add prompt: Private Group Coaching Infrastructure 2026-03-20 03:50:45 +00:00
mmanisaligil e11a167a55 Add prompt: Personalized Digital Avatar Generator 2026-03-20 03:50:44 +00:00
mmanisaligil 4639de7157 Add prompt: Dating Profile Optimization Suite 2026-03-20 03:50:44 +00:00
mmanisaligil 1462653763 Add prompt: AI-Powered Personal Compliment & Coaching Engine 2026-03-20 03:50:44 +00:00
mmanisaligil ebdabfefb4 Add prompt: Apple-Level UI System Designer (2026 Standard) 2026-03-20 03:50:43 +00:00
mmanisaligil 560dc17883 Add prompt: Design System Consistency Auditor 2026-03-20 03:50:43 +00:00
mmanisaligil ea715bc2b8 Add prompt: AI-First Design Handoff Generator (Dev-Ready Spec) 2026-03-20 03:50:43 +00:00
mmanisaligil 1189277f46 Add prompt: UX Conversion Deconstruction Engine 2026-03-20 03:50:42 +00:00
wkaandemir 73e017bb6f Add prompt: Visual Media Analysis Expert Agent Role 2026-03-20 03:50:42 +00:00
wkaandemir 11e6c3f440 Add prompt: Repository Indexer Agent Role 2026-03-20 03:50:42 +00:00
wkaandemir 50a97c0729 Add prompt: Deep Research Agent Role 2026-03-20 03:50:42 +00:00
wkaandemir b991170aec Add prompt: Bug Risk Analyst Agent Role 2026-03-20 03:50:41 +00:00
wkaandemir 6d1fbd3152 Add prompt: TypeScript Type Expert Agent Role 2026-03-20 03:50:41 +00:00
wkaandemir 07005ec619 Add prompt: Tool Evaluator Agent Role 2026-03-20 03:50:41 +00:00
wkaandemir 818ac2f32e Add prompt: Shell Script Agent Role 2026-03-20 03:50:40 +00:00
wkaandemir 246418367f Add prompt: Refactoring Expert Agent Role 2026-03-20 03:50:40 +00:00
wkaandemir 2b5ff2c672 Add prompt: Root Cause Analysis Agent Role 2026-03-20 03:50:40 +00:00
wkaandemir 1e032ed819 Add prompt: Rapid Prototyper Agent Role 2026-03-20 03:50:40 +00:00
wkaandemir a7112fa127 Add prompt: Product Planner Agent Role 2026-03-20 03:50:39 +00:00
wkaandemir b529c1b275 Add prompt: Post-Implementation Audit Agent Role 2026-03-20 03:50:39 +00:00
wkaandemir 345989463f Add prompt: Error Handler Agent Role 2026-03-20 03:50:39 +00:00
wkaandemir fbf2a69203 Add prompt: Dependency Manager Agent Role 2026-03-20 03:50:38 +00:00
wkaandemir b45918c4c3 Add prompt: Code Reviewer Agent Role 2026-03-20 03:50:38 +00:00
wkaandemir 60410ea046 Add prompt: Code Review Agent Role 2026-03-20 03:50:38 +00:00
wkaandemir 9db18e74b7 Add prompt: Code Formatter Agent Role 2026-03-20 03:50:38 +00:00
wkaandemir 954fbea6ba Add prompt: Test Engineer Agent Role 2026-03-20 03:50:37 +00:00
wkaandemir 402016da36 Add prompt: Test Analyzer Agent Role 2026-03-20 03:50:37 +00:00
wkaandemir 2fac69e1f7 Add prompt: Quality Engineering Agent Role 2026-03-20 03:50:37 +00:00
wkaandemir 05c32f93ab Add prompt: API Tester Agent Role 2026-03-20 03:50:37 +00:00
wkaandemir 0e8d8e2449 Add prompt: Vulnerability Auditor Agent Role 2026-03-20 03:50:36 +00:00
wkaandemir d55748d291 Add prompt: Diff Security Auditor Agent Role 2026-03-20 03:50:36 +00:00
wkaandemir 612114beaf Add prompt: Performance Tuning Agent Role 2026-03-20 03:50:36 +00:00
wkaandemir 6a1d232347 Add prompt: Optimization Auditor Agent Role 2026-03-20 03:50:35 +00:00
wkaandemir 437be3d61d Add prompt: Caching Architect Agent Role 2026-03-20 03:50:35 +00:00
wkaandemir 008698943a Add prompt: Legal Document Generator Agent Role 2026-03-20 03:50:35 +00:00
wkaandemir 62b5767bc9 Add prompt: UI Architect Agent Role 2026-03-20 03:50:35 +00:00
wkaandemir 8496265084 Add prompt: SEO Optimization Agent Role 2026-03-20 03:50:34 +00:00
wkaandemir a3ee86b65b Add prompt: SEO Auditor Agent Role 2026-03-20 03:50:34 +00:00
wkaandemir 2379887c93 Add prompt: Frontend Developer Agent Role 2026-03-20 03:50:34 +00:00
wkaandemir 1e029580c6 Add prompt: Accessibility Auditor Agent Role 2026-03-20 03:50:34 +00:00
wkaandemir e52826f731 Add prompt: Documentation Maintainer Agent Role 2026-03-20 03:50:33 +00:00
wkaandemir b428739a19 Add prompt: Repository Workflow Editor Agent Role 2026-03-20 03:50:33 +00:00
wkaandemir 867147829b Add prompt: Git Workflow Expert Agent Role 2026-03-20 03:50:33 +00:00
wkaandemir 42c6125e94 Add prompt: Environment Configuration Agent Role 2026-03-20 03:50:33 +00:00
wkaandemir 63b0b66739 Add prompt: DevOps Automator Agent Role 2026-03-20 03:50:32 +00:00
wkaandemir cfdb8726d8 Add prompt: Backup & Restore Agent Role 2026-03-20 03:50:32 +00:00
wkaandemir b04295fa90 Add prompt: Mock Data Generator Agent Role 2026-03-20 03:50:32 +00:00
wkaandemir 77799aadcc Add prompt: Data Validator Agent Role 2026-03-20 03:50:32 +00:00
wkaandemir f51e399dcb Add prompt: Database Architect Agent Role 2026-03-20 03:50:31 +00:00
wkaandemir 63b371b678 Add prompt: Backend Architect Agent Role 2026-03-20 03:50:31 +00:00
wkaandemir d26692fbf2 Add prompt: API Design Expert Agent Role 2026-03-20 03:50:31 +00:00
wkaandemir 0034dc39b5 Add prompt: System Architect Agent Role 2026-03-20 03:50:31 +00:00
jovemexausto 36e73e4ad2 Add prompt: Entropy peer reviews 2026-03-20 03:50:30 +00:00
xiaoyucunx 05ed853084 Update prompt: Code Review Specialist 2026-03-20 03:50:30 +00:00
cperalesg@gmail.com 59bf6f13fe Update prompt: Prompt Generator 2026-03-20 03:50:30 +00:00
panda667 dcc5c5cbbf Update prompt: Astrologer 2026-03-20 03:50:29 +00:00
joembolinas 1803c8e297 Update prompt: The Technical Co-Founder: Building Real Products Together 2026-03-20 03:50:29 +00:00
aitank2020@gmail.com 14f7cfadf8 Update prompt: Echoes of the Rust Age 2026-03-20 03:50:29 +00:00
dragoy18@gmail.com 1d93108df9 Update prompt: Code Review Specialist 2 2026-03-20 03:50:28 +00:00
lior1976@gmail.com cde0e0724c Update prompt: Article Summarizer 2026-03-20 03:50:28 +00:00
beresasis@gmail.com ee712a0c8c Update prompt: Interview Preparation Coach 2026-03-20 03:50:28 +00:00
sinansonmez 721da5e226 Update prompt: Code Review Assistant 2026-03-20 03:50:27 +00:00
giorgiop e3c658bd79 Update prompt: Virtual Doctor 2026-03-20 03:50:27 +00:00
Fatih Kadir Akın 3b49211aab refactor(components): update EzoicPlaceholder to optimize DOM handling 2026-03-20 01:40:06 +03:00
Fatih Kadir Akın b991f8fe10 feat(public/sponsors, src/app): add Vercel OSS dark mode sponsor logo 2026-03-20 01:35:41 +03:00
Fatih Kadir Akın 9fbeddf8f4 refactor(components): update Ezoic ad handling and scripts placement
Entire-Checkpoint: afbec17cde9f
2026-03-20 01:23:52 +03:00
Fatih Kadir Akın 252bcb5de3 refactor(components): refactor Ezoic ad and placeholder components
Entire-Checkpoint: afbec17cde9f
2026-03-20 01:12:46 +03:00
Fatih Kadir Akın 4ef6246284 feat(components): add EzoicAd component for displaying ads 2026-03-20 00:53:42 +03:00
Fatih Kadir Akın a253e6524a fix(api): resolve TypeScript _count type error in prompts.json route 2026-03-20 00:44:19 +03:00
Fatih Kadir Akın f34113302f refactor(components): update Ezoic ad rendering logic 2026-03-20 00:34:12 +03:00
Fatih Kadir Akın 1c287d3a0f Merge pull request #1086 from mksglu/fix/vercel-cost-optimization
perf: reduce Vercel costs with 12 targeted optimizations ($6.4K → $800-1.5K/mo)
2026-03-19 21:23:17 +00:00
Fatih Kadir Akın 29064d6ada refactor(ads): improve Ezoic ad rendering logic 2026-03-20 00:20:51 +03:00
Mert Koseoglu c95f80dd75 fix: ReDoS prevention + bound slugify fallback scan
- Validate elicitation keys match variable name format before regex construction
  (prevents ReDoS via user-controlled regex metacharacters — CWE-1333)
- Narrow slugify fallback to only prompts with slug=null, cap at 500 results
- Add TODO to backfill slug column so fallback can be removed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 21:40:18 +03:00
Mert Koseoglu 0e9258c300 fix: parseBody uses byte-accurate size check and returns 413
- Track bytes with Buffer.byteLength instead of string code units
- Return 413 Payload Too Large instead of generic 500
- Custom PayloadTooLargeError class for handler discrimination

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 21:28:17 +03:00
Mert Koseoglu 08a9c26ed2 fix: address CodeRabbit review — backward compat + slugify fallback
- prompts.json: Always include `content` field alongside `contentPreview`
  (Raycast extension reads `item.content` — removing it breaks the extension)
- mcp.ts GetPromptRequestSchema: Add slugify(title) fallback for prompts
  without a stored slug (getPromptName() advertises slugified titles in
  prompts/list, so the resolver must handle them too)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 21:25:08 +03:00
Mert Koseoglu 1dbdb88717 perf: reduce Vercel costs — 12 optimizations across MCP endpoint, prompts.json, and infra
Addresses $6,478/month Vercel bill with 12 targeted fixes based on forensic
bill analysis and 7-expert review. See #1085 for full analysis.

MCP endpoint (src/pages/api/mcp.ts):
- Fix GetPromptRequestSchema P0 full table scan (findMany→findFirst)
- Remove all JSON pretty-printing (13 occurrences, ~30% payload reduction)
- Add Cache-Control headers to GET discovery endpoint
- Set tools.listChanged=false to prevent client polling
- Add 1MB body size limit to parseBody (DoS protection)
- Make elicitation opt-in via fill_variables param (saves 10s CPU/request)
- Fix timer leak in elicitation setTimeout
- Cap search content at 1,000 chars (contentPreview)
- Remove structuredFormat from search results
- Dynamic import for improve_prompt (reduces cold start)
- Optimize search_skills to return fileNames only

prompts.json:
- Add pagination (?page=&limit=) with backward compatibility
- Add ETag support for 304 Not-Modified responses
- Truncate content to 500-char preview by default

Infrastructure:
- Remove Sentry tunnelRoute (was proxying all browser events through edge)
- Add slug + composite database indexes for MCP query patterns

Closes #1085

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 21:04:34 +03:00
Fatih Kadir Akın cd1715e703 chore(env): update EZOIC_ENABLED to NEXT_PUBLIC_EZOIC_ENABLED in environment variables 2026-03-19 18:21:51 +03:00
Fatih Kadir Akın d9fe66299a feat(prompts): enable Ezoic ads based on environment variable 2026-03-19 18:11:52 +03:00
Fatih Kadir Akın e1741b528e feat(prompts): add Ezoic ad placeholder to prompt page 2026-03-19 17:38:11 +03:00
Fatih Kadir Akın 68bf2e9be8 chore(widgets): update maxCount to 20 in ezoicWidget 2026-03-19 17:30:41 +03:00
Fatih Kadir Akın 266ffe9b9e Merge pull request #1082 from ogzgl/fix/reduce-vercel-costs 2026-03-19 12:26:59 +00:00
Oğuz Gül a46ef57a60 fix: reduce vercel costs 2026-03-19 15:16:07 +03:00
Fatih Kadir Akın 6505f7601f fix(page.tsx): update JSON-LD serialization to escape special characters 2026-03-19 10:29:41 +03:00
Fatih Kadir Akın 9684a58dbc fix(seo): update structured data script to escape special characters 2026-03-19 10:28:58 +03:00
gokbeyinac f50c843cb7 Remove prompt: Kickstart Prompt for UX & UI Design 2026-03-19 03:54:46 +00:00
mal4thia@gmail.com 7f34e89107 Add prompt: Professional photo restoration expert 2026-03-19 03:54:46 +00:00
priyalvijay108@gmail.com 4315ccba98 Add prompt: Review the social media content 2026-03-19 03:54:46 +00:00
fede.gazzelloni@gmail.com 6759e17f1c Update prompt: Article Summarizer 2026-03-19 03:54:45 +00:00
debashis.sarker@gmail.com b1d591c946 Update prompt: The Technical Co-Founder: Building Real Products Together 2026-03-19 03:54:45 +00:00
aymanrabeemac@gmail.com fad9ea6a5d Update prompt: Code Review Specialist 2026-03-19 03:54:45 +00:00
nolanneff 5e5d20b705 Update prompt: Code Review Specialist 2 2026-03-19 03:54:44 +00:00
thanos0000@gmail.com fa5e180371 Update prompt: Question Quality Lab Game 2026-03-19 03:54:44 +00:00
ersinkoc ba3a120f6d Update prompt: Echoes of the Rust Age 2026-03-19 03:54:44 +00:00
cnwdy888@gmail.com 8c54e612ac Update prompt: Interview Preparation Coach 2026-03-19 03:54:43 +00:00
guangzhongzhang978@gmail.com 239119a12e Update prompt: Virtual Doctor 2026-03-19 03:54:43 +00:00
f 34e529cc70 Update prompt: Code Review Assistant 2026-03-19 03:54:42 +00:00
devisasari e2a494e39d Update prompt: Astrologer 2026-03-19 03:54:42 +00:00
iuzn cee1e2d521 Update prompt: Prompt Generator 2026-03-19 03:54:42 +00:00
37 changed files with 29789 additions and 2573 deletions
+5
View File
@@ -27,6 +27,11 @@ dist
# Docker
docker-compose*.yml
Dockerfile*
docker/
# Cloudflare
cloudflare/
wrangler.jsonc
# Development files
.github
+1 -1
View File
@@ -66,5 +66,5 @@ CRON_SECRET="your-secret-key-here"
# SENTRY_AUTH_TOKEN=sentry-auth-token
# GOOGLE_ADSENSE_ACCOUNT=ca-pub-xxxxxxxxxxxxxxxx
# EZOIC_ENABLED=true
# NEXT_PUBLIC_EZOIC_ENABLED=true
# EZOIC_SITE_DOMAIN=prompts.chat
+1
View File
@@ -48,3 +48,4 @@ next-env.d.ts
# Sentry Config File
.env.sentry-build-plugin
.env.production.cloudflare
+68
View File
@@ -0,0 +1,68 @@
# Production Dockerfile for Cloudflare Containers
# Multi-stage build producing a lean standalone Next.js image
# ---------------------------------------------------------------------------
# Stage 1: Install dependencies
# ---------------------------------------------------------------------------
FROM node:24-alpine AS deps
RUN apk add --no-cache libc6-compat openssl
WORKDIR /app
COPY package.json package-lock.json ./
COPY prisma ./prisma/
RUN npm ci
# ---------------------------------------------------------------------------
# Stage 2: Build the application
# ---------------------------------------------------------------------------
FROM node:24-alpine AS builder
RUN apk add --no-cache libc6-compat openssl
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
ENV NODE_OPTIONS="--max-old-space-size=4096"
# Dummy URL for prisma generate (only needs schema, not a real connection)
ENV DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy"
# Skip Sentry source map upload during container build
ENV SENTRY_AUTH_TOKEN=""
RUN npx prisma generate
# Use webpack instead of Turbopack to stay within Docker memory limits
RUN npx next build --webpack
# ---------------------------------------------------------------------------
# Stage 3: Production runner
# ---------------------------------------------------------------------------
FROM node:24-alpine AS runner
RUN apk add --no-cache libc6-compat openssl
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder /app/prisma ./prisma
# YAML prompt files read at runtime via readFileSync(process.cwd() + path)
COPY --from=builder /app/src/lib/ai/*.prompt.yml ./src/lib/ai/
COPY --from=builder /app/src/app/api/prompt-builder/chat/*.prompt.yml ./src/app/api/prompt-builder/chat/
# Next.js standalone output includes a minimal server and all dependencies
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
+14392 -1111
View File
File diff suppressed because it is too large Load Diff
+102
View File
@@ -0,0 +1,102 @@
# Deploying prompts.chat to Cloudflare Containers
This guide covers deploying the Next.js application to [Cloudflare Containers](https://developers.cloudflare.com/containers/).
## Prerequisites
- [Docker](https://docs.docker.com/desktop/) running locally
- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/) (`npm i -g wrangler`)
- A Cloudflare account on the [Workers Paid plan](https://workers.cloudflare.com/pricing/)
- An external PostgreSQL database (e.g. Neon, Supabase, or self-hosted)
## Setup
### 1. Install dependencies
```bash
npm install @cloudflare/containers
```
### 2. Configure secrets
Set required environment variables as Wrangler secrets. These are injected into the container at runtime:
```bash
# Required
wrangler secret put DATABASE_URL
wrangler secret put AUTH_SECRET
# OAuth (if using these providers)
wrangler secret put AUTH_GITHUB_ID
wrangler secret put AUTH_GITHUB_SECRET
wrangler secret put AUTH_GOOGLE_ID
wrangler secret put AUTH_GOOGLE_SECRET
# Optional
wrangler secret put OPENAI_API_KEY
wrangler secret put SENTRY_DSN
```
### 3. Deploy
```bash
npx wrangler deploy
```
On the first deploy, Wrangler will:
1. Build the Docker image locally using `Dockerfile.cloudflare`
2. Push the image to Cloudflare's managed registry
3. Deploy the Worker that routes traffic to the container
The first deploy takes a few minutes for image push and container provisioning. Subsequent deploys are faster due to layer caching.
### 4. Check status
```bash
npx wrangler containers list
```
## Architecture
```
User Request → Cloudflare Network → Worker (router) → Container (Next.js on Node.js)
↓
PostgreSQL DB
```
- **Worker** (`cloudflare/worker.js`): Thin routing layer that forwards all requests to the container
- **Container** (`Dockerfile.cloudflare`): Multi-stage Node.js image running the Next.js standalone server
- The container runs the full Node.js runtime, so all dependencies (Prisma, next-auth, next-intl, sharp) work without modification
## Configuration
Key settings in `wrangler.jsonc`:
| Setting | Default | Description |
|---------|---------|-------------|
| `max_instances` | 3 | Maximum concurrent container instances |
| `instance_type` | standard-1 | 2 vCPU, 2 GiB memory |
| `sleepAfter` | 5m | Container sleeps after 5 minutes of inactivity |
Adjust `max_instances` and `instance_type` based on your traffic. See [instance types](https://developers.cloudflare.com/containers/platform-details/#instance-types) for all options.
## Local development
Local development remains unchanged:
```bash
npm run dev
```
To test the Docker image locally:
```bash
docker build -f Dockerfile.cloudflare -t prompts-chat .
docker run -p 3000:3000 --env-file .env prompts-chat
```
## Notes
- **Cold starts**: Containers take 2-3 seconds to wake from sleep. The `sleepAfter` setting in `cloudflare/worker.js` controls when idle containers go to sleep.
- **Database**: Use an external PostgreSQL provider. Cloudflare Hyperdrive can be added for edge connection pooling if needed.
- **Containers Beta**: Cloudflare Containers is currently in beta. See [beta info](https://developers.cloudflare.com/containers/beta-info/) for limitations.
+73
View File
@@ -0,0 +1,73 @@
import { Container } from "@cloudflare/containers";
import { env } from "cloudflare:workers";
const SECRET_KEYS = [
"NEXTAUTH_SECRET",
"GITHUB_CLIENT_ID",
"GITHUB_CLIENT_SECRET",
"GOOGLE_CLIENT_ID",
"GOOGLE_CLIENT_SECRET",
"AUTH_APPLE_ID",
"AUTH_APPLE_SECRET",
"OPENAI_API_KEY",
"SENTRY_AUTH_TOKEN",
"DO_SPACES_ACCESS_KEY_ID",
"DO_SPACES_SECRET_ACCESS_KEY",
"HF_API_TOKEN",
"FAL_API_KEY",
"WIRO_API_KEY",
"CRON_SECRET",
];
function getSecretEnvVars() {
const vars = {};
for (const key of SECRET_KEYS) {
if (env[key]) vars[key] = env[key];
}
return vars;
}
export class PromptsChatContainer extends Container {
defaultPort = 3000;
sleepAfter = "5m";
envVars = {
NODE_ENV: "production",
DATABASE_URL: env.DATABASE_URL || "",
NEXTAUTH_URL: env.NEXTAUTH_URL,
OPENAI_BASE_URL: env.OPENAI_BASE_URL,
OPENAI_EMBEDDING_MODEL: env.OPENAI_EMBEDDING_MODEL,
OPENAI_GENERATIVE_MODEL: env.OPENAI_GENERATIVE_MODEL,
GOOGLE_ANALYTICS_ID: env.GOOGLE_ANALYTICS_ID,
ENABLED_STORAGE: env.ENABLED_STORAGE,
DO_SPACES_BUCKET: env.DO_SPACES_BUCKET,
DO_SPACES_REGION: env.DO_SPACES_REGION,
FAL_IMAGE_MODELS: env.FAL_IMAGE_MODELS,
FAL_VIDEO_MODELS: env.FAL_VIDEO_MODELS,
FAL_AUDIO_MODELS: env.FAL_AUDIO_MODELS,
WIRO_IMAGE_MODELS: env.WIRO_IMAGE_MODELS,
WIRO_VIDEO_MODELS: env.WIRO_VIDEO_MODELS,
EZOIC_SITE_DOMAIN: env.EZOIC_SITE_DOMAIN,
NEXT_PUBLIC_EZOIC_ENABLED: env.NEXT_PUBLIC_EZOIC_ENABLED,
GOOGLE_ADSENSE_ACCOUNT: env.GOOGLE_ADSENSE_ACCOUNT,
...getSecretEnvVars(),
};
onStart() {
console.log("prompts.chat container started");
}
onStop() {
console.log("prompts.chat container stopped");
}
onError(error) {
console.error("prompts.chat container error:", error);
}
}
export default {
async fetch(request, env) {
const container = env.PROMPTS_CHAT.getByName("main");
return container.fetch(request);
},
};
+9 -5
View File
@@ -146,23 +146,27 @@ if [ ! -f "$SEED_MARKER" ]; then
fi
fi
# Wait for supervisord socket to be ready
# Wait for supervisord to be ready
echo "▶ Waiting for supervisord..."
for i in $(seq 1 30); do
if supervisorctl -c /etc/supervisor/conf.d/supervisord.conf status >/dev/null 2>&1; then
if kill -0 $SUPERVISOR_PID 2>/dev/null && [ -S /var/run/supervisor.sock ]; then
echo "✓ Supervisord is ready"
break
fi
if ! kill -0 $SUPERVISOR_PID 2>/dev/null; then
echo "✗ Supervisord process died"
exit 1
fi
if [ $i -eq 30 ]; then
echo "✗ Supervisord failed to start"
echo "✗ Supervisord socket not available after 30s"
exit 1
fi
sleep 1
done
# Start Next.js
# Start Next.js via supervisorctl
echo "▶ Starting Next.js..."
supervisorctl -c /etc/supervisor/conf.d/supervisord.conf start nextjs
supervisorctl start nextjs
echo ""
echo "╔═══════════════════════════════════════════════════════════════╗"
+10
View File
@@ -1,3 +1,13 @@
[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisord]
nodaemon=true
user=root
+5 -13
View File
@@ -55,7 +55,7 @@ const nextConfig: NextConfig = {
destination: "/embed",
permanent: true,
},
// Redirect book PDF downloads to GitHub raw to save Vercel edge bandwidth
// Redirect book PDF downloads to GitHub raw to save bandwidth
{
source: "/book-pdf/:filename",
destination: "https://raw.githubusercontent.com/f/prompts.chat/refs/heads/main/public/book-pdf/:filename",
@@ -82,22 +82,14 @@ export default withSentryConfig(withMDX(withNextIntl(nextConfig)), {
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: "/monitoring",
// tunnelRoute removed — was proxying all browser Sentry events through Vercel edge,
// generating unnecessary edge requests ($2.45/M) and function invocations ($0.60/M).
// Estimated savings: $100-400/month. Trade-off: some ad-blockers may block direct Sentry calls.
// See: https://github.com/f/prompts.chat/issues/1085
webpack: {
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: true,
// Tree-shaking options for reducing bundle size
treeshake: {
// Automatically tree-shake Sentry logger statements to reduce bundle size
removeDebugLogging: true,
},
},
+944 -109
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -32,6 +32,7 @@
"dependencies": {
"@auth/prisma-adapter": "^2.11.1",
"@aws-sdk/client-s3": "^3.948.0",
"@cloudflare/containers": "^0.1.1",
"@hookform/resolvers": "^5.2.2",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
@@ -111,7 +112,8 @@
"tsx": "^4.21.0",
"tw-animate-css": "^1.4.0",
"typescript": "^5",
"vitest": "^2.1.8"
"vitest": "^2.1.8",
"wrangler": "^4.76.0"
},
"engines": {
"node": "24.x"
+2
View File
@@ -139,6 +139,8 @@ model Prompt {
@@index([isPrivate])
@@index([isFeatured])
@@index([isUnlisted])
@@index([slug])
@@index([isPrivate, isUnlisted, deletedAt, createdAt(sort: Desc)])
@@map("prompts")
}
+13528 -979
View File
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
<svg width="240" height="24" viewBox="0 0 240 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.4283 0.610352L26.8565 23.8046H0L13.4283 0.610352Z" fill="#fff"/>
<path d="M236.436 22.7547L235.081 17.1148V23.8045H233.849V15.1372H235.557L236.924 20.7405L238.291 15.1372H240V23.8045H238.767V17.1148L237.412 22.7547H236.436Z" fill="#fff"/>
<path d="M226.237 23.8045L228.776 15.1372H230.436L232.974 23.8045H231.644L231.022 21.5462H228.19L227.568 23.8045H226.237ZM229.606 16.4678L228.52 20.362H230.692L229.606 16.4678Z" fill="#fff"/>
<path d="M219.444 23.8045V15.1372H222.142C223.972 15.1372 225.095 16.126 225.095 17.7252C225.095 18.7628 224.436 19.5197 223.631 19.7761C224.485 19.9348 224.9 20.4109 224.973 21.3386L225.193 23.8045H223.911L223.716 21.5828C223.643 20.716 223.301 20.4231 222.239 20.4231H220.726V23.8045H219.444ZM220.726 19.2267H222.117C223.179 19.2267 223.777 18.7018 223.777 17.7862C223.777 16.8585 223.179 16.3458 222.117 16.3458H220.726V19.2267Z" fill="#fff"/>
<path d="M214.957 23.9999C213.09 23.9999 211.748 22.4251 211.748 19.4831C211.748 16.6876 212.956 14.9419 214.994 14.9419C216.63 14.9419 217.704 16.0772 218.082 17.8107L216.752 17.8961C216.508 16.8341 215.934 16.1504 215.006 16.1504C213.774 16.1504 213.078 17.3468 213.078 19.4831C213.078 21.6072 213.798 22.7913 215.067 22.7913C216.178 22.7913 216.861 21.8758 216.91 20.4963H215.006V19.3854H218.094V23.8045H217.057L217.02 22.6937C216.642 23.4749 215.983 23.9999 214.957 23.9999Z" fill="#fff"/>
<path d="M207.651 23.9999C205.589 23.9999 204.429 22.3763 204.429 19.4831C204.429 16.5655 205.589 14.9419 207.651 14.9419C209.714 14.9419 210.873 16.5655 210.873 19.4831C210.873 22.3763 209.714 23.9999 207.651 23.9999ZM207.651 22.8035C208.872 22.8035 209.543 21.6804 209.543 19.4831C209.543 17.2857 208.872 16.1382 207.651 16.1382C206.431 16.1382 205.76 17.2857 205.76 19.4831C205.76 21.6804 206.431 22.8035 207.651 22.8035Z" fill="#fff"/>
<path d="M197.489 23.8045V15.1372H200.187C202.017 15.1372 203.14 16.126 203.14 17.7252C203.14 18.7628 202.481 19.5197 201.676 19.7761C202.53 19.9348 202.945 20.4109 203.018 21.3386L203.238 23.8045H201.956L201.761 21.5828C201.688 20.716 201.346 20.4231 200.284 20.4231H198.771V23.8045H197.489ZM198.771 19.2267H200.162C201.224 19.2267 201.822 18.7018 201.822 17.7862C201.822 16.8585 201.224 16.3458 200.162 16.3458H198.771V19.2267Z" fill="#fff"/>
<path d="M190.403 23.8045V15.1372H193.088C194.894 15.1372 196.09 16.1626 196.09 17.8351C196.09 19.4953 194.894 20.5207 193.088 20.5207H191.684V23.8045H190.403ZM191.684 19.3122H192.954C194.186 19.3122 194.76 18.7995 194.76 17.8351C194.76 16.8707 194.186 16.3458 192.954 16.3458H191.684V19.3122Z" fill="#fff"/>
<path d="M175.791 23.8045V15.1372H181.112V16.3458H177.073V18.8727H180.966V20.0446H177.073V22.596H181.21V23.8045H175.791Z" fill="#fff"/>
<path d="M168.216 23.8045V15.1372H170.914C172.744 15.1372 173.867 16.126 173.867 17.7252C173.867 18.7628 173.208 19.5197 172.403 19.7761C173.257 19.9348 173.672 20.4109 173.745 21.3386L173.965 23.8045H172.683L172.488 21.5828C172.415 20.716 172.073 20.4231 171.011 20.4231H169.498V23.8045H168.216ZM169.498 19.2267H170.889C171.951 19.2267 172.549 18.7018 172.549 17.7862C172.549 16.8585 171.951 16.3458 170.889 16.3458H169.498V19.2267Z" fill="#fff"/>
<path d="M160.374 23.8045L162.912 15.1372H164.572L167.111 23.8045H165.78L165.158 21.5462H162.326L161.704 23.8045H160.374ZM163.742 16.4678L162.656 20.362H164.828L163.742 16.4678Z" fill="#fff"/>
<path d="M154.032 23.8045L153.104 15.1372H154.386L155.045 22.1565L155.862 16.1138H156.985L157.803 22.1565L158.462 15.1372H159.744L158.816 23.8045H157.144L156.424 18.5309L155.704 23.8045H154.032Z" fill="#fff"/>
<path d="M148.471 23.8045V16.3458H145.932V15.1372H152.279V16.3458H149.74V23.8045H148.471Z" fill="#fff"/>
<path d="M139.322 23.8045V15.1372H144.741V16.3213H140.604V18.9826H144.522V20.1545H140.604V23.8045H139.322Z" fill="#fff"/>
<path d="M134.469 23.9999C132.407 23.9999 131.247 22.3763 131.247 19.4831C131.247 16.5655 132.407 14.9419 134.469 14.9419C136.532 14.9419 137.691 16.5655 137.691 19.4831C137.691 22.3763 136.532 23.9999 134.469 23.9999ZM134.469 22.8035C135.69 22.8035 136.361 21.6804 136.361 19.4831C136.361 17.2857 135.69 16.1382 134.469 16.1382C133.249 16.1382 132.577 17.2857 132.577 19.4831C132.577 21.6804 133.249 22.8035 134.469 22.8035Z" fill="#fff"/>
<path d="M127.261 23.9999C125.381 23.9999 124.161 22.8035 124.014 20.9846L125.332 20.8992C125.503 22.1443 126.211 22.8035 127.31 22.8035C128.371 22.8035 128.969 22.3763 128.969 21.6072C128.969 20.8381 128.554 20.3987 126.736 19.8859C124.954 19.3976 124.209 18.7995 124.209 17.4566C124.209 15.9673 125.308 14.9419 127.126 14.9419C128.933 14.9419 129.97 16.0772 130.141 17.7496L128.823 17.8229C128.689 16.8219 128.127 16.1382 127.09 16.1382C126.138 16.1382 125.528 16.6387 125.528 17.3956C125.528 18.1525 125.955 18.4332 127.493 18.8605C129.567 19.4221 130.288 20.1911 130.288 21.6072C130.288 23.0355 129.079 23.9999 127.261 23.9999Z" fill="#fff"/>
<path d="M109.927 23.8045V15.1372H115.248V16.3458H111.208V18.8727H115.102V20.0446H111.208V22.596H115.346V23.8045H109.927Z" fill="#fff"/>
<path d="M105.232 23.9999C103.145 23.9999 101.974 22.2664 101.974 19.4831C101.974 16.6876 103.145 14.9419 105.232 14.9419C106.807 14.9419 107.942 16.0284 108.296 17.9083L106.953 17.9816C106.709 16.773 106.05 16.1382 105.232 16.1382C103.975 16.1382 103.304 17.3468 103.304 19.4831C103.304 21.6072 103.975 22.8035 105.232 22.8035C106.123 22.8035 106.795 22.1077 107.014 20.7893L108.345 20.8503C108.04 22.8279 106.892 23.9999 105.232 23.9999Z" fill="#fff"/>
<path d="M95.0342 23.8045V15.1372H97.7315C99.5622 15.1372 100.685 16.126 100.685 17.7252C100.685 18.7628 100.026 19.5197 99.2205 19.7761C100.075 19.9348 100.49 20.4109 100.563 21.3386L100.783 23.8045H99.5012L99.3059 21.5828C99.2327 20.716 98.891 20.4231 97.8291 20.4231H96.3157V23.8045H95.0342ZM96.3157 19.2267H97.7071C98.7689 19.2267 99.367 18.7018 99.367 17.7862C99.367 16.8585 98.7689 16.3458 97.7071 16.3458H96.3157V19.2267Z" fill="#fff"/>
<path d="M90.5596 23.9999C88.7166 23.9999 87.606 22.8035 87.606 20.7893V15.1372H88.8875V20.8381C88.8875 22.1077 89.4733 22.8035 90.5596 22.8035C91.6458 22.8035 92.2317 22.1077 92.2317 20.8381V15.1372H93.5132V20.7893C93.5132 22.8035 92.4025 23.9999 90.5596 23.9999Z" fill="#fff"/>
<path d="M83.2417 23.9999C81.179 23.9999 80.0195 22.3763 80.0195 19.4831C80.0195 16.5655 81.179 14.9419 83.2417 14.9419C85.3043 14.9419 86.4638 16.5655 86.4638 19.4831C86.4638 22.3763 85.3043 23.9999 83.2417 23.9999ZM83.2417 22.8035C84.4622 22.8035 85.1334 21.6804 85.1334 19.4831C85.1334 17.2857 84.4622 16.1382 83.2417 16.1382C82.0212 16.1382 81.3499 17.2857 81.3499 19.4831C81.3499 21.6804 82.0212 22.8035 83.2417 22.8035Z" fill="#fff"/>
<path d="M76.0332 23.9999C74.1536 23.9999 72.9331 22.8035 72.7866 20.9846L74.1048 20.8992C74.2756 22.1443 74.9835 22.8035 76.082 22.8035C77.1438 22.8035 77.7419 22.3763 77.7419 21.6072C77.7419 20.8381 77.3269 20.3987 75.5083 19.8859C73.7264 19.3976 72.9819 18.7995 72.9819 17.4566C72.9819 15.9673 74.0804 14.9419 75.8989 14.9419C77.7052 14.9419 78.7427 16.0772 78.9135 17.7496L77.5954 17.8229C77.4611 16.8219 76.8997 16.1382 75.8623 16.1382C74.9103 16.1382 74.3 16.6387 74.3 17.3956C74.3 18.1525 74.7272 18.4332 76.265 18.8605C78.3399 19.4221 79.06 20.1911 79.06 21.6072C79.06 23.0355 77.8517 23.9999 76.0332 23.9999Z" fill="#fff"/>
<path d="M58.4556 23.8045V15.1372H60.0178L62.8616 21.9856V15.1372H64.1187V23.8045H62.5564L59.7127 16.9561V23.8045H58.4556Z" fill="#fff"/>
<path d="M51.3813 23.8045V15.1372H56.7027V16.3458H52.6629V18.8727H56.5563V20.0446H52.6629V22.596H56.8004V23.8045H51.3813Z" fill="#fff"/>
<path d="M44.0386 23.8045V15.1372H46.7237C48.53 15.1372 49.7261 16.1626 49.7261 17.8351C49.7261 19.4953 48.53 20.5207 46.7237 20.5207H45.3201V23.8045H44.0386ZM45.3201 19.3122H46.5894C47.8221 19.3122 48.3958 18.7995 48.3958 17.8351C48.3958 16.8707 47.8221 16.3458 46.5894 16.3458H45.3201V19.3122Z" fill="#fff"/>
<path d="M39.332 23.9999C37.2693 23.9999 36.1099 22.3763 36.1099 19.4831C36.1099 16.5655 37.2693 14.9419 39.332 14.9419C41.3946 14.9419 42.5541 16.5655 42.5541 19.4831C42.5541 22.3763 41.3946 23.9999 39.332 23.9999ZM39.332 22.8035C40.5525 22.8035 41.2238 21.6804 41.2238 19.4831C41.2238 17.2857 40.5525 16.1382 39.332 16.1382C38.1115 16.1382 37.4402 17.2857 37.4402 19.4831C37.4402 21.6804 38.1115 22.8035 39.332 22.8035Z" fill="#fff"/>
<path d="M171.085 9.35094C169.083 9.35094 167.96 7.95929 167.96 5.46895C167.96 2.60019 168.864 0.292969 171.414 0.292969C173.013 0.292969 173.819 1.1597 174.148 2.38045L172.83 2.49032C172.659 1.89215 172.269 1.48931 171.414 1.48931C170.243 1.48931 169.474 2.36825 169.303 4.2482C169.718 3.71107 170.426 3.32043 171.329 3.32043C173.05 3.32043 174.161 4.48015 174.161 6.25024C174.161 8.16681 172.916 9.35094 171.085 9.35094ZM171.085 8.1424C172.147 8.1424 172.83 7.45878 172.83 6.27465C172.83 5.17597 172.196 4.48015 171.183 4.48015C170.096 4.48015 169.315 5.17597 169.315 6.27465C169.315 7.34891 169.999 8.1424 171.085 8.1424Z" fill="#fff" fill-opacity="0.5"/>
<path d="M160.667 9.15562C160.667 7.22684 161.387 6.01829 163.925 4.67547C165.011 4.10171 165.475 3.72328 165.475 2.88096C165.475 2.05085 164.95 1.48931 163.937 1.48931C162.827 1.48931 162.217 2.09968 162.046 3.18615L160.715 3.1007C160.898 1.37944 162.021 0.292969 163.937 0.292969C165.78 0.292969 166.806 1.3184 166.806 2.83213C166.806 4.17496 166.22 4.83416 164.45 5.78635C162.949 6.59205 162.241 7.36112 162.192 7.94708H166.818V9.15562H160.667Z" fill="#fff" fill-opacity="0.5"/>
<path d="M156.424 9.35094C154.508 9.35094 153.348 7.61748 153.348 4.83416C153.348 2.03864 154.508 0.292969 156.424 0.292969C158.34 0.292969 159.499 2.03864 159.499 4.83416C159.499 7.61748 158.34 9.35094 156.424 9.35094ZM154.678 4.83416C154.678 5.55441 154.752 6.1892 154.91 6.68971L157.449 1.98981C157.181 1.66021 156.827 1.48931 156.424 1.48931C155.338 1.48931 154.678 2.72226 154.678 4.83416ZM156.424 8.15461C157.51 8.15461 158.169 6.92165 158.169 4.83416C158.169 4.10171 158.096 3.46692 157.937 2.96641L155.386 7.6541C155.667 7.9837 156.021 8.15461 156.424 8.15461Z" fill="#fff" fill-opacity="0.5"/>
<path d="M146.03 9.15562C146.03 7.22684 146.75 6.01829 149.289 4.67547C150.375 4.10171 150.839 3.72328 150.839 2.88096C150.839 2.05085 150.314 1.48931 149.301 1.48931C148.19 1.48931 147.58 2.09968 147.409 3.18615L146.079 3.1007C146.262 1.37944 147.385 0.292969 149.301 0.292969C151.144 0.292969 152.169 1.3184 152.169 2.83213C152.169 4.17496 151.583 4.83416 149.813 5.78635C148.312 6.59205 147.604 7.36112 147.555 7.94708H152.181V9.15562H146.03Z" fill="#fff" fill-opacity="0.5"/>
<path d="M132.041 10.4985L135.727 0H136.899L133.212 10.4985H132.041Z" fill="#fff" fill-opacity="0.5"/>
<path d="M124.722 10.4985L128.409 0H129.581L125.894 10.4985H124.722Z" fill="#fff" fill-opacity="0.5"/>
<path d="M111.684 9.15582V7.54443H113.344V9.15582H111.684Z" fill="#fff"/>
<path d="M105.232 9.35094C103.145 9.35094 101.974 7.61748 101.974 4.83416C101.974 2.03864 103.145 0.292969 105.232 0.292969C106.807 0.292969 107.942 1.37944 108.296 3.25939L106.953 3.33264C106.709 2.1241 106.05 1.48931 105.232 1.48931C103.975 1.48931 103.304 2.69785 103.304 4.83416C103.304 6.95827 103.975 8.15461 105.232 8.15461C106.123 8.15461 106.795 7.45878 107.014 6.14037L108.345 6.20141C108.04 8.17902 106.892 9.35094 105.232 9.35094Z" fill="#fff"/>
<path d="M95.0464 9.15562V0.488281H96.6086L99.4524 7.3367V0.488281H100.71V9.15562H99.1473L96.3035 2.3072V9.15562H95.0464Z" fill="#fff"/>
<path d="M87.8501 9.15562V7.94707H89.9249V1.69683H87.8501V0.488281H93.2691V1.69683H91.1943V7.94707H93.2691V9.15562H87.8501Z" fill="#fff"/>
<path d="M73.4575 9.15562V0.488281H74.739V7.94707H78.8765V9.15562H73.4575Z" fill="#fff"/>
<path d="M66.0171 9.15562V0.488281H71.3385V1.69683H67.2986V4.22378H71.192V5.3957H67.2986V7.94707H71.4361V9.15562H66.0171Z" fill="#fff"/>
<path d="M61.3232 9.35094C59.2361 9.35094 58.0645 7.61748 58.0645 4.83416C58.0645 2.03864 59.2361 0.292969 61.3232 0.292969C62.8976 0.292969 64.0327 1.37944 64.3866 3.25939L63.0441 3.33264C62.8 2.1241 62.1409 1.48931 61.3232 1.48931C60.0661 1.48931 59.3948 2.69785 59.3948 4.83416C59.3948 6.95827 60.0661 8.15461 61.3232 8.15461C62.2142 8.15461 62.8854 7.45878 63.1051 6.14037L64.4355 6.20141C64.1303 8.17902 62.9831 9.35094 61.3232 9.35094Z" fill="#fff"/>
<path d="M51.125 9.15562V0.488281H53.8223C55.6531 0.488281 56.7759 1.47709 56.7759 3.07627C56.7759 4.11391 56.1168 4.87078 55.3113 5.12714C56.1657 5.28583 56.5806 5.76193 56.6539 6.6897L56.8736 9.15562H55.592L55.3968 6.93385C55.3235 6.06711 54.9818 5.77413 53.9199 5.77413H52.4065V9.15562H51.125ZM52.4065 4.5778H53.7979C54.8597 4.5778 55.4578 4.05288 55.4578 3.13731C55.4578 2.20954 54.8597 1.69683 53.7979 1.69683H52.4065V4.5778Z" fill="#fff"/>
<path d="M44.063 9.15562V0.488281H49.3844V1.69683H45.3445V4.22378H49.2379V5.3957H45.3445V7.94707H49.482V9.15562H44.063Z" fill="#fff"/>
<path d="M38.5753 9.15562L36 0.488281H37.3548L39.332 7.44656L41.3092 0.488281H42.6639L40.0887 9.15562H38.5753Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

+44
View File
@@ -0,0 +1,44 @@
<svg width="240" height="24" viewBox="0 0 240 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.4283 0.610352L26.8565 23.8046H0L13.4283 0.610352Z" fill="#000"/>
<path d="M236.436 22.7547L235.081 17.1148V23.8045H233.849V15.1372H235.557L236.924 20.7405L238.291 15.1372H240V23.8045H238.767V17.1148L237.412 22.7547H236.436Z" fill="#000"/>
<path d="M226.237 23.8045L228.776 15.1372H230.436L232.974 23.8045H231.644L231.022 21.5462H228.19L227.568 23.8045H226.237ZM229.606 16.4678L228.52 20.362H230.692L229.606 16.4678Z" fill="#000"/>
<path d="M219.444 23.8045V15.1372H222.142C223.972 15.1372 225.095 16.126 225.095 17.7252C225.095 18.7628 224.436 19.5197 223.631 19.7761C224.485 19.9348 224.9 20.4109 224.973 21.3386L225.193 23.8045H223.911L223.716 21.5828C223.643 20.716 223.301 20.4231 222.239 20.4231H220.726V23.8045H219.444ZM220.726 19.2267H222.117C223.179 19.2267 223.777 18.7018 223.777 17.7862C223.777 16.8585 223.179 16.3458 222.117 16.3458H220.726V19.2267Z" fill="#000"/>
<path d="M214.957 23.9999C213.09 23.9999 211.748 22.4251 211.748 19.4831C211.748 16.6876 212.956 14.9419 214.994 14.9419C216.63 14.9419 217.704 16.0772 218.082 17.8107L216.752 17.8961C216.508 16.8341 215.934 16.1504 215.006 16.1504C213.774 16.1504 213.078 17.3468 213.078 19.4831C213.078 21.6072 213.798 22.7913 215.067 22.7913C216.178 22.7913 216.861 21.8758 216.91 20.4963H215.006V19.3854H218.094V23.8045H217.057L217.02 22.6937C216.642 23.4749 215.983 23.9999 214.957 23.9999Z" fill="#000"/>
<path d="M207.651 23.9999C205.589 23.9999 204.429 22.3763 204.429 19.4831C204.429 16.5655 205.589 14.9419 207.651 14.9419C209.714 14.9419 210.873 16.5655 210.873 19.4831C210.873 22.3763 209.714 23.9999 207.651 23.9999ZM207.651 22.8035C208.872 22.8035 209.543 21.6804 209.543 19.4831C209.543 17.2857 208.872 16.1382 207.651 16.1382C206.431 16.1382 205.76 17.2857 205.76 19.4831C205.76 21.6804 206.431 22.8035 207.651 22.8035Z" fill="#000"/>
<path d="M197.489 23.8045V15.1372H200.187C202.017 15.1372 203.14 16.126 203.14 17.7252C203.14 18.7628 202.481 19.5197 201.676 19.7761C202.53 19.9348 202.945 20.4109 203.018 21.3386L203.238 23.8045H201.956L201.761 21.5828C201.688 20.716 201.346 20.4231 200.284 20.4231H198.771V23.8045H197.489ZM198.771 19.2267H200.162C201.224 19.2267 201.822 18.7018 201.822 17.7862C201.822 16.8585 201.224 16.3458 200.162 16.3458H198.771V19.2267Z" fill="#000"/>
<path d="M190.403 23.8045V15.1372H193.088C194.894 15.1372 196.09 16.1626 196.09 17.8351C196.09 19.4953 194.894 20.5207 193.088 20.5207H191.684V23.8045H190.403ZM191.684 19.3122H192.954C194.186 19.3122 194.76 18.7995 194.76 17.8351C194.76 16.8707 194.186 16.3458 192.954 16.3458H191.684V19.3122Z" fill="#000"/>
<path d="M175.791 23.8045V15.1372H181.112V16.3458H177.073V18.8727H180.966V20.0446H177.073V22.596H181.21V23.8045H175.791Z" fill="#000"/>
<path d="M168.216 23.8045V15.1372H170.914C172.744 15.1372 173.867 16.126 173.867 17.7252C173.867 18.7628 173.208 19.5197 172.403 19.7761C173.257 19.9348 173.672 20.4109 173.745 21.3386L173.965 23.8045H172.683L172.488 21.5828C172.415 20.716 172.073 20.4231 171.011 20.4231H169.498V23.8045H168.216ZM169.498 19.2267H170.889C171.951 19.2267 172.549 18.7018 172.549 17.7862C172.549 16.8585 171.951 16.3458 170.889 16.3458H169.498V19.2267Z" fill="#000"/>
<path d="M160.374 23.8045L162.912 15.1372H164.572L167.111 23.8045H165.78L165.158 21.5462H162.326L161.704 23.8045H160.374ZM163.742 16.4678L162.656 20.362H164.828L163.742 16.4678Z" fill="#000"/>
<path d="M154.032 23.8045L153.104 15.1372H154.386L155.045 22.1565L155.862 16.1138H156.985L157.803 22.1565L158.462 15.1372H159.744L158.816 23.8045H157.144L156.424 18.5309L155.704 23.8045H154.032Z" fill="#000"/>
<path d="M148.471 23.8045V16.3458H145.932V15.1372H152.279V16.3458H149.74V23.8045H148.471Z" fill="#000"/>
<path d="M139.322 23.8045V15.1372H144.741V16.3213H140.604V18.9826H144.522V20.1545H140.604V23.8045H139.322Z" fill="#000"/>
<path d="M134.469 23.9999C132.407 23.9999 131.247 22.3763 131.247 19.4831C131.247 16.5655 132.407 14.9419 134.469 14.9419C136.532 14.9419 137.691 16.5655 137.691 19.4831C137.691 22.3763 136.532 23.9999 134.469 23.9999ZM134.469 22.8035C135.69 22.8035 136.361 21.6804 136.361 19.4831C136.361 17.2857 135.69 16.1382 134.469 16.1382C133.249 16.1382 132.577 17.2857 132.577 19.4831C132.577 21.6804 133.249 22.8035 134.469 22.8035Z" fill="#000"/>
<path d="M127.261 23.9999C125.381 23.9999 124.161 22.8035 124.014 20.9846L125.332 20.8992C125.503 22.1443 126.211 22.8035 127.31 22.8035C128.371 22.8035 128.969 22.3763 128.969 21.6072C128.969 20.8381 128.554 20.3987 126.736 19.8859C124.954 19.3976 124.209 18.7995 124.209 17.4566C124.209 15.9673 125.308 14.9419 127.126 14.9419C128.933 14.9419 129.97 16.0772 130.141 17.7496L128.823 17.8229C128.689 16.8219 128.127 16.1382 127.09 16.1382C126.138 16.1382 125.528 16.6387 125.528 17.3956C125.528 18.1525 125.955 18.4332 127.493 18.8605C129.567 19.4221 130.288 20.1911 130.288 21.6072C130.288 23.0355 129.079 23.9999 127.261 23.9999Z" fill="#000"/>
<path d="M109.927 23.8045V15.1372H115.248V16.3458H111.208V18.8727H115.102V20.0446H111.208V22.596H115.346V23.8045H109.927Z" fill="#000"/>
<path d="M105.232 23.9999C103.145 23.9999 101.974 22.2664 101.974 19.4831C101.974 16.6876 103.145 14.9419 105.232 14.9419C106.807 14.9419 107.942 16.0284 108.296 17.9083L106.953 17.9816C106.709 16.773 106.05 16.1382 105.232 16.1382C103.975 16.1382 103.304 17.3468 103.304 19.4831C103.304 21.6072 103.975 22.8035 105.232 22.8035C106.123 22.8035 106.795 22.1077 107.014 20.7893L108.345 20.8503C108.04 22.8279 106.892 23.9999 105.232 23.9999Z" fill="#000"/>
<path d="M95.0342 23.8045V15.1372H97.7315C99.5622 15.1372 100.685 16.126 100.685 17.7252C100.685 18.7628 100.026 19.5197 99.2205 19.7761C100.075 19.9348 100.49 20.4109 100.563 21.3386L100.783 23.8045H99.5012L99.3059 21.5828C99.2327 20.716 98.891 20.4231 97.8291 20.4231H96.3157V23.8045H95.0342ZM96.3157 19.2267H97.7071C98.7689 19.2267 99.367 18.7018 99.367 17.7862C99.367 16.8585 98.7689 16.3458 97.7071 16.3458H96.3157V19.2267Z" fill="#000"/>
<path d="M90.5596 23.9999C88.7166 23.9999 87.606 22.8035 87.606 20.7893V15.1372H88.8875V20.8381C88.8875 22.1077 89.4733 22.8035 90.5596 22.8035C91.6458 22.8035 92.2317 22.1077 92.2317 20.8381V15.1372H93.5132V20.7893C93.5132 22.8035 92.4025 23.9999 90.5596 23.9999Z" fill="#000"/>
<path d="M83.2417 23.9999C81.179 23.9999 80.0195 22.3763 80.0195 19.4831C80.0195 16.5655 81.179 14.9419 83.2417 14.9419C85.3043 14.9419 86.4638 16.5655 86.4638 19.4831C86.4638 22.3763 85.3043 23.9999 83.2417 23.9999ZM83.2417 22.8035C84.4622 22.8035 85.1334 21.6804 85.1334 19.4831C85.1334 17.2857 84.4622 16.1382 83.2417 16.1382C82.0212 16.1382 81.3499 17.2857 81.3499 19.4831C81.3499 21.6804 82.0212 22.8035 83.2417 22.8035Z" fill="#000"/>
<path d="M76.0332 23.9999C74.1536 23.9999 72.9331 22.8035 72.7866 20.9846L74.1048 20.8992C74.2756 22.1443 74.9835 22.8035 76.082 22.8035C77.1438 22.8035 77.7419 22.3763 77.7419 21.6072C77.7419 20.8381 77.3269 20.3987 75.5083 19.8859C73.7264 19.3976 72.9819 18.7995 72.9819 17.4566C72.9819 15.9673 74.0804 14.9419 75.8989 14.9419C77.7052 14.9419 78.7427 16.0772 78.9135 17.7496L77.5954 17.8229C77.4611 16.8219 76.8997 16.1382 75.8623 16.1382C74.9103 16.1382 74.3 16.6387 74.3 17.3956C74.3 18.1525 74.7272 18.4332 76.265 18.8605C78.3399 19.4221 79.06 20.1911 79.06 21.6072C79.06 23.0355 77.8517 23.9999 76.0332 23.9999Z" fill="#000"/>
<path d="M58.4556 23.8045V15.1372H60.0178L62.8616 21.9856V15.1372H64.1187V23.8045H62.5564L59.7127 16.9561V23.8045H58.4556Z" fill="#000"/>
<path d="M51.3813 23.8045V15.1372H56.7027V16.3458H52.6629V18.8727H56.5563V20.0446H52.6629V22.596H56.8004V23.8045H51.3813Z" fill="#000"/>
<path d="M44.0386 23.8045V15.1372H46.7237C48.53 15.1372 49.7261 16.1626 49.7261 17.8351C49.7261 19.4953 48.53 20.5207 46.7237 20.5207H45.3201V23.8045H44.0386ZM45.3201 19.3122H46.5894C47.8221 19.3122 48.3958 18.7995 48.3958 17.8351C48.3958 16.8707 47.8221 16.3458 46.5894 16.3458H45.3201V19.3122Z" fill="#000"/>
<path d="M39.332 23.9999C37.2693 23.9999 36.1099 22.3763 36.1099 19.4831C36.1099 16.5655 37.2693 14.9419 39.332 14.9419C41.3946 14.9419 42.5541 16.5655 42.5541 19.4831C42.5541 22.3763 41.3946 23.9999 39.332 23.9999ZM39.332 22.8035C40.5525 22.8035 41.2238 21.6804 41.2238 19.4831C41.2238 17.2857 40.5525 16.1382 39.332 16.1382C38.1115 16.1382 37.4402 17.2857 37.4402 19.4831C37.4402 21.6804 38.1115 22.8035 39.332 22.8035Z" fill="#000"/>
<path d="M171.085 9.35094C169.083 9.35094 167.96 7.95929 167.96 5.46895C167.96 2.60019 168.864 0.292969 171.414 0.292969C173.013 0.292969 173.819 1.1597 174.148 2.38045L172.83 2.49032C172.659 1.89215 172.269 1.48931 171.414 1.48931C170.243 1.48931 169.474 2.36825 169.303 4.2482C169.718 3.71107 170.426 3.32043 171.329 3.32043C173.05 3.32043 174.161 4.48015 174.161 6.25024C174.161 8.16681 172.916 9.35094 171.085 9.35094ZM171.085 8.1424C172.147 8.1424 172.83 7.45878 172.83 6.27465C172.83 5.17597 172.196 4.48015 171.183 4.48015C170.096 4.48015 169.315 5.17597 169.315 6.27465C169.315 7.34891 169.999 8.1424 171.085 8.1424Z" fill="#000" fill-opacity="0.5"/>
<path d="M160.667 9.15562C160.667 7.22684 161.387 6.01829 163.925 4.67547C165.011 4.10171 165.475 3.72328 165.475 2.88096C165.475 2.05085 164.95 1.48931 163.937 1.48931C162.827 1.48931 162.217 2.09968 162.046 3.18615L160.715 3.1007C160.898 1.37944 162.021 0.292969 163.937 0.292969C165.78 0.292969 166.806 1.3184 166.806 2.83213C166.806 4.17496 166.22 4.83416 164.45 5.78635C162.949 6.59205 162.241 7.36112 162.192 7.94708H166.818V9.15562H160.667Z" fill="#000" fill-opacity="0.5"/>
<path d="M156.424 9.35094C154.508 9.35094 153.348 7.61748 153.348 4.83416C153.348 2.03864 154.508 0.292969 156.424 0.292969C158.34 0.292969 159.499 2.03864 159.499 4.83416C159.499 7.61748 158.34 9.35094 156.424 9.35094ZM154.678 4.83416C154.678 5.55441 154.752 6.1892 154.91 6.68971L157.449 1.98981C157.181 1.66021 156.827 1.48931 156.424 1.48931C155.338 1.48931 154.678 2.72226 154.678 4.83416ZM156.424 8.15461C157.51 8.15461 158.169 6.92165 158.169 4.83416C158.169 4.10171 158.096 3.46692 157.937 2.96641L155.386 7.6541C155.667 7.9837 156.021 8.15461 156.424 8.15461Z" fill="#000" fill-opacity="0.5"/>
<path d="M146.03 9.15562C146.03 7.22684 146.75 6.01829 149.289 4.67547C150.375 4.10171 150.839 3.72328 150.839 2.88096C150.839 2.05085 150.314 1.48931 149.301 1.48931C148.19 1.48931 147.58 2.09968 147.409 3.18615L146.079 3.1007C146.262 1.37944 147.385 0.292969 149.301 0.292969C151.144 0.292969 152.169 1.3184 152.169 2.83213C152.169 4.17496 151.583 4.83416 149.813 5.78635C148.312 6.59205 147.604 7.36112 147.555 7.94708H152.181V9.15562H146.03Z" fill="#000" fill-opacity="0.5"/>
<path d="M132.041 10.4985L135.727 0H136.899L133.212 10.4985H132.041Z" fill="#000" fill-opacity="0.5"/>
<path d="M124.722 10.4985L128.409 0H129.581L125.894 10.4985H124.722Z" fill="#000" fill-opacity="0.5"/>
<path d="M111.684 9.15582V7.54443H113.344V9.15582H111.684Z" fill="#000"/>
<path d="M105.232 9.35094C103.145 9.35094 101.974 7.61748 101.974 4.83416C101.974 2.03864 103.145 0.292969 105.232 0.292969C106.807 0.292969 107.942 1.37944 108.296 3.25939L106.953 3.33264C106.709 2.1241 106.05 1.48931 105.232 1.48931C103.975 1.48931 103.304 2.69785 103.304 4.83416C103.304 6.95827 103.975 8.15461 105.232 8.15461C106.123 8.15461 106.795 7.45878 107.014 6.14037L108.345 6.20141C108.04 8.17902 106.892 9.35094 105.232 9.35094Z" fill="#000"/>
<path d="M95.0464 9.15562V0.488281H96.6086L99.4524 7.3367V0.488281H100.71V9.15562H99.1473L96.3035 2.3072V9.15562H95.0464Z" fill="#000"/>
<path d="M87.8501 9.15562V7.94707H89.9249V1.69683H87.8501V0.488281H93.2691V1.69683H91.1943V7.94707H93.2691V9.15562H87.8501Z" fill="#000"/>
<path d="M73.4575 9.15562V0.488281H74.739V7.94707H78.8765V9.15562H73.4575Z" fill="#000"/>
<path d="M66.0171 9.15562V0.488281H71.3385V1.69683H67.2986V4.22378H71.192V5.3957H67.2986V7.94707H71.4361V9.15562H66.0171Z" fill="#000"/>
<path d="M61.3232 9.35094C59.2361 9.35094 58.0645 7.61748 58.0645 4.83416C58.0645 2.03864 59.2361 0.292969 61.3232 0.292969C62.8976 0.292969 64.0327 1.37944 64.3866 3.25939L63.0441 3.33264C62.8 2.1241 62.1409 1.48931 61.3232 1.48931C60.0661 1.48931 59.3948 2.69785 59.3948 4.83416C59.3948 6.95827 60.0661 8.15461 61.3232 8.15461C62.2142 8.15461 62.8854 7.45878 63.1051 6.14037L64.4355 6.20141C64.1303 8.17902 62.9831 9.35094 61.3232 9.35094Z" fill="#000"/>
<path d="M51.125 9.15562V0.488281H53.8223C55.6531 0.488281 56.7759 1.47709 56.7759 3.07627C56.7759 4.11391 56.1168 4.87078 55.3113 5.12714C56.1657 5.28583 56.5806 5.76193 56.6539 6.6897L56.8736 9.15562H55.592L55.3968 6.93385C55.3235 6.06711 54.9818 5.77413 53.9199 5.77413H52.4065V9.15562H51.125ZM52.4065 4.5778H53.7979C54.8597 4.5778 55.4578 4.05288 55.4578 3.13731C55.4578 2.20954 54.8597 1.69683 53.7979 1.69683H52.4065V4.5778Z" fill="#000"/>
<path d="M44.063 9.15562V0.488281H49.3844V1.69683H45.3445V4.22378H49.2379V5.3957H45.3445V7.94707H49.482V9.15562H44.063Z" fill="#000"/>
<path d="M38.5753 9.15562L36 0.488281H37.3548L39.332 7.44656L41.3092 0.488281H42.6639L40.0887 9.15562H38.5753Z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

+1 -1
View File
@@ -12,7 +12,7 @@ Sentry.init({
enabled: process.env.NODE_ENV === "production",
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,
tracesSampleRate: 0.1,
// Enable logs to be sent to Sentry
enableLogs: true,
+1 -1
View File
@@ -11,7 +11,7 @@ Sentry.init({
enabled: process.env.NODE_ENV === "production",
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,
tracesSampleRate: 0.1,
// Enable logs to be sent to Sentry
enableLogs: true,
+1
View File
@@ -8,6 +8,7 @@ export const size = {
height: 630,
};
export const contentType = "image/png";
export const revalidate = 3600;
const radiusMap: Record<string, number> = {
none: 0,
+1 -1
View File
@@ -130,7 +130,7 @@ export default async function BookHomePage() {
{/* JSON-LD structured data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, '\\u003c') }}
/>
<div className="max-w-2xl">
{/* Continue Reading Button */}
+3 -2
View File
@@ -7,7 +7,7 @@ import { Header } from "@/components/layout/header";
import { Footer } from "@/components/layout/footer";
import { CookieConsentBanner } from "@/components/layout/cookie-consent";
import { Analytics } from "@/components/layout/analytics";
import { EzoicAds } from "@/components/layout/ezoic-ads";
import { EzoicScripts, EzoicRouteHandler } from "@/components/layout/ezoic-ads";
import { WebsiteStructuredData } from "@/components/seo/structured-data";
import { AppBanner } from "@/components/layout/app-banner";
import { LocaleDetector } from "@/components/providers/locale-detector";
@@ -185,12 +185,13 @@ export default async function RootLayout({
<meta name="google-adsense-account" content={process.env.GOOGLE_ADSENSE_ACCOUNT} />
)}
<WebsiteStructuredData />
{process.env.NEXT_PUBLIC_EZOIC_ENABLED === "true" && <EzoicScripts />}
</head>
<body className={`${fontClasses} antialiased`}>
{process.env.GOOGLE_ANALYTICS_ID && (
<Analytics gaId={process.env.GOOGLE_ANALYTICS_ID} />
)}
{process.env.EZOIC_ENABLED === "true" && <EzoicAds />}
{process.env.NEXT_PUBLIC_EZOIC_ENABLED === "true" && <EzoicRouteHandler />}
<Providers locale={locale} messages={messages} theme={config.theme} branding={{ ...config.branding, useCloneBranding: config.homepage?.useCloneBranding }}>
{isEmbedRoute || isKidsRoute ? (
children
+18
View File
@@ -313,6 +313,24 @@ export default async function HomePage() {
/>
</BuiltWithLink>
<span>by Cognition</span>
<span className="mx-0.5">·</span>
<span>hosted on</span>
<Link href="https://vercel.com/oss" target="_blank" rel="noopener noreferrer" className="inline-flex items-center opacity-60 hover:opacity-100 transition-opacity">
<Image
src="/sponsors/vercel-oss.svg"
alt="Vercel OSS Program"
width={240}
height={24}
className="h-4.5 w-auto dark:hidden"
/>
<Image
src="/sponsors/vercel-oss-dark.svg"
alt="Vercel OSS Program"
width={240}
height={24}
className="h-4.5 w-auto hidden dark:block"
/>
</Link>
</span>
</div>
)}
+1 -1
View File
@@ -97,7 +97,7 @@ export default function PrivacyPage() {
</p>
</section>
{process.env.EZOIC_ENABLED === "true" && (
{process.env.NEXT_PUBLIC_EZOIC_ENABLED === "true" && (
<section className="space-y-3">
<h2 className="text-lg font-semibold">Advertising & Ezoic</h2>
<p className="text-muted-foreground">
+2
View File
@@ -23,6 +23,8 @@ function getUserIdentifier(user: { email: string; username: string; githubUserna
return user.githubUsername || (isUnclaimedAccount ? user.username : user.email);
}
export const revalidate = 3600;
export async function GET() {
try {
const prompts = await db.prompt.findMany({
+210 -121
View File
@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import crypto from "crypto";
function getUserIdentifier(user: {
username: string;
@@ -8,139 +9,227 @@ function getUserIdentifier(user: {
return user.githubUsername || user.username;
}
export async function GET() {
const CONTENT_PREVIEW_LENGTH = 500;
const DEFAULT_LIMIT = 50;
const MAX_LIMIT = 100;
export const dynamic = "force-dynamic";
export const revalidate = 3600;
function generateETag(count: number, latestUpdatedAt: Date | null): string {
const raw = `${count}-${latestUpdatedAt?.toISOString() ?? "none"}`;
const hash = crypto.createHash("md5").update(raw).digest("hex");
return `"${hash}"`;
}
export async function GET(request: NextRequest) {
try {
const prompts = await db.prompt.findMany({
where: {
isPrivate: false,
isUnlisted: false,
deletedAt: null,
const searchParams = request.nextUrl.searchParams;
const fullContent = searchParams.get("full_content") === "true";
const pageParam = searchParams.get("page");
const limitParam = searchParams.get("limit");
const isPaginated = pageParam !== null || limitParam !== null;
const page = Math.max(1, parseInt(pageParam ?? "1", 10) || 1);
const limit = Math.min(
MAX_LIMIT,
Math.max(1, parseInt(limitParam ?? String(DEFAULT_LIMIT), 10) || DEFAULT_LIMIT)
);
const whereClause = {
isPrivate: false,
isUnlisted: false,
deletedAt: null,
};
// Fetch total count and latest updatedAt for ETag generation
const [totalCount, latestPrompt] = await Promise.all([
db.prompt.count({ where: whereClause }),
db.prompt.findFirst({
where: whereClause,
orderBy: { updatedAt: "desc" },
select: { updatedAt: true },
}),
]);
// Generate and check ETag for conditional requests
const etag = generateETag(totalCount, latestPrompt?.updatedAt ?? null);
const ifNoneMatch = request.headers.get("If-None-Match");
if (ifNoneMatch && ifNoneMatch === etag) {
return new NextResponse(null, {
status: 304,
headers: {
ETag: etag,
"Cache-Control":
"public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400",
},
});
}
const selectClause = {
id: true,
title: true,
slug: true,
description: true,
content: true,
type: true,
mediaUrl: true,
viewCount: true,
createdAt: true,
updatedAt: true,
structuredFormat: true,
isFeatured: true,
featuredAt: true,
requiresMediaUpload: true,
requiredMediaType: true,
requiredMediaCount: true,
category: {
select: {
id: true,
name: true,
slug: true,
icon: true,
},
},
select: {
id: true,
title: true,
slug: true,
description: true,
content: true,
type: true,
mediaUrl: true,
viewCount: true,
createdAt: true,
updatedAt: true,
structuredFormat: true,
isFeatured: true,
featuredAt: true,
requiresMediaUpload: true,
requiredMediaType: true,
requiredMediaCount: true,
category: {
select: {
id: true,
name: true,
slug: true,
icon: true,
},
author: {
select: {
username: true,
name: true,
avatar: true,
githubUsername: true,
verified: true,
},
author: {
select: {
username: true,
name: true,
avatar: true,
githubUsername: true,
verified: true,
},
},
contributors: {
select: {
username: true,
name: true,
avatar: true,
githubUsername: true,
verified: true,
},
contributors: {
select: {
username: true,
name: true,
avatar: true,
githubUsername: true,
verified: true,
},
},
tags: {
select: {
tag: {
select: {
id: true,
name: true,
slug: true,
color: true,
},
},
tags: {
select: {
tag: {
select: {
id: true,
name: true,
slug: true,
color: true,
},
},
},
_count: {
select: {
votes: true,
comments: true,
},
},
_count: {
select: {
votes: true,
comments: true,
},
},
orderBy: { createdAt: "desc" },
} as const;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const queryOptions: any = {
where: whereClause,
select: selectClause,
orderBy: { createdAt: "desc" as const },
};
if (isPaginated) {
queryOptions.skip = (page - 1) * limit;
queryOptions.take = limit;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const prompts: any[] = await db.prompt.findMany(queryOptions);
const includeFullContent = fullContent;
const formattedPrompts = prompts.map((prompt) => {
const content = prompt.content ?? "";
const contentPreview =
content.length > CONTENT_PREVIEW_LENGTH
? content.slice(0, CONTENT_PREVIEW_LENGTH) + "..."
: content;
return {
id: prompt.id,
title: prompt.title,
slug: prompt.slug,
description: prompt.description,
...(includeFullContent
? { content, contentPreview }
: { contentPreview }),
type: prompt.type,
structuredFormat: prompt.structuredFormat,
mediaUrl: prompt.mediaUrl,
viewCount: prompt.viewCount,
voteCount: prompt._count.votes,
commentCount: prompt._count.comments,
isFeatured: prompt.isFeatured,
featuredAt: prompt.featuredAt,
requiresMediaUpload: prompt.requiresMediaUpload,
requiredMediaType: prompt.requiredMediaType,
requiredMediaCount: prompt.requiredMediaCount,
createdAt: prompt.createdAt,
updatedAt: prompt.updatedAt,
category: prompt.category
? {
id: prompt.category.id,
name: prompt.category.name,
slug: prompt.category.slug,
icon: prompt.category.icon,
}
: null,
author: {
username: prompt.author.username,
name: prompt.author.name,
avatar: prompt.author.avatar,
identifier: getUserIdentifier(prompt.author),
verified: prompt.author.verified,
},
contributors: prompt.contributors.map((c: any) => ({
username: c.username,
name: c.name,
avatar: c.avatar,
identifier: getUserIdentifier(c),
verified: c.verified,
})),
tags: prompt.tags.map((pt: any) => ({
id: pt.tag.id,
name: pt.tag.name,
slug: pt.tag.slug,
color: pt.tag.color,
})),
};
});
const formattedPrompts = prompts.map((prompt) => ({
id: prompt.id,
title: prompt.title,
slug: prompt.slug,
description: prompt.description,
content: prompt.content,
type: prompt.type,
structuredFormat: prompt.structuredFormat,
mediaUrl: prompt.mediaUrl,
viewCount: prompt.viewCount,
voteCount: prompt._count.votes,
commentCount: prompt._count.comments,
isFeatured: prompt.isFeatured,
featuredAt: prompt.featuredAt,
requiresMediaUpload: prompt.requiresMediaUpload,
requiredMediaType: prompt.requiredMediaType,
requiredMediaCount: prompt.requiredMediaCount,
createdAt: prompt.createdAt,
updatedAt: prompt.updatedAt,
category: prompt.category
? {
id: prompt.category.id,
name: prompt.category.name,
slug: prompt.category.slug,
icon: prompt.category.icon,
}
: null,
author: {
username: prompt.author.username,
name: prompt.author.name,
avatar: prompt.author.avatar,
identifier: getUserIdentifier(prompt.author),
verified: prompt.author.verified,
},
contributors: prompt.contributors.map((c) => ({
username: c.username,
name: c.name,
avatar: c.avatar,
identifier: getUserIdentifier(c),
verified: c.verified,
})),
tags: prompt.tags.map((pt) => ({
id: pt.tag.id,
name: pt.tag.name,
slug: pt.tag.slug,
color: pt.tag.color,
})),
}));
const totalPages = isPaginated ? Math.ceil(totalCount / limit) : 1;
return NextResponse.json(
{
count: formattedPrompts.length,
prompts: formattedPrompts,
const responseBody = isPaginated
? {
count: totalCount,
page,
limit,
totalPages,
hasMore: page < totalPages,
prompts: formattedPrompts,
}
: {
count: formattedPrompts.length,
prompts: formattedPrompts,
};
return NextResponse.json(responseBody, {
headers: {
"Cache-Control":
"public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400",
ETag: etag,
},
{
headers: {
"Cache-Control": "public, max-age=3600",
},
}
);
});
} catch (error) {
console.error("prompts.json error:", error);
return NextResponse.json(
+1
View File
@@ -8,6 +8,7 @@ export const size = {
height: 630,
};
export const contentType = "image/png";
export const revalidate = 3600;
const typeLabels: Record<string, string> = {
TEXT: "Text Prompt",
+4
View File
@@ -32,6 +32,7 @@ import { AddToCollectionButton } from "@/components/prompts/add-to-collection-bu
import { getConfig } from "@/lib/config";
import { StructuredData } from "@/components/seo/structured-data";
import { AI_MODELS } from "@/lib/works-best-with";
import { EzoicAd } from "@/components/ads/ezoic-ad";
interface PromptPageProps {
params: Promise<{ id: string }>;
@@ -701,6 +702,9 @@ export default async function PromptPage({ params }: PromptPageProps) {
locale={locale}
/>
)}
{/* Ad Placement */}
{process.env.NEXT_PUBLIC_EZOIC_ENABLED === "true" && <EzoicAd id={201} />}
</TabsContent>
<TabsContent value="versions" className="mt-0">
+18
View File
@@ -0,0 +1,18 @@
"use client";
import { EzoicPlaceholder } from "./ezoic-placeholder";
interface EzoicAdProps {
id: number;
}
/**
* Client-only wrapper for EzoicPlaceholder.
* Renders the placeholder div after mount so it's in the DOM
* when Ezoic's showAds() runs.
*
* @see https://docs.ezoic.com/docs/ezoicadsadvanced/nextjs/
*/
export function EzoicAd({ id }: EzoicAdProps) {
return <EzoicPlaceholder id={id} />;
}
+14 -52
View File
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { useEffect } from "react";
import { useTranslations } from "next-intl";
import { runEzoic } from "@/lib/ezoic";
@@ -8,74 +8,36 @@ interface EzoicPlaceholderProps {
id: number;
}
/**
* Manages the lifecycle of a single Ezoic ad slot.
* The placeholder div is always in the DOM so Ezoic can find it
* when showAds() runs. Destroys the placeholder on unmount.
*
* @see https://docs.ezoic.com/docs/ezoicadsadvanced/nextjs/
*/
export function EzoicPlaceholder({ id }: EzoicPlaceholderProps) {
const t = useTranslations("common");
const [isRendered, setIsRendered] = useState(false);
const [hasAdContent, setHasAdContent] = useState(true);
const placeholderRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Render the placeholder div first, then tell Ezoic to fill it.
// This ensures the DOM element exists before showAds is called.
setIsRendered(true);
// Use requestAnimationFrame to guarantee the placeholder div
// is in the DOM before Ezoic tries to find it.
const rafId = requestAnimationFrame(() => {
runEzoic(() => {
window.ezstandalone?.showAds(id);
});
runEzoic(() => {
window.ezstandalone?.showAds(id);
});
// Observe the placeholder for ad content injection by Ezoic.
// If nothing is injected after a timeout, hide the container
// to avoid showing an empty white card.
const el = document.getElementById(`ezoic-pub-ad-placeholder-${id}`);
let observer: MutationObserver | undefined;
let timeoutId: ReturnType<typeof setTimeout> | undefined;
if (el) {
let filled = false;
observer = new MutationObserver(() => {
if (el.childNodes.length > 0 || el.innerHTML.trim().length > 0) {
filled = true;
setHasAdContent(true);
}
});
observer.observe(el, { childList: true, subtree: true });
// If Ezoic doesn't fill the placeholder within 5s, hide it
timeoutId = setTimeout(() => {
if (!filled && el.childNodes.length === 0) {
setHasAdContent(false);
}
}, 5000);
}
return () => {
cancelAnimationFrame(rafId);
observer?.disconnect();
if (timeoutId) clearTimeout(timeoutId);
runEzoic(() => {
window.ezstandalone?.destroyPlaceholders(id);
});
};
}, [id]);
if (!hasAdContent) return null;
return (
<div className="ezoic-ad-container group border rounded-[var(--radius)] overflow-hidden hover:border-foreground/20 transition-colors flex flex-col">
<div className="px-3 py-1.5 border-b bg-muted/50">
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
<div className="ezoic-ad-container my-2 overflow-hidden">
<div className="flex items-center justify-center">
<span className="text-[10px] font-medium uppercase tracking-widest text-muted-foreground/50 select-none">
{t("ad")}
</span>
</div>
<div className="ezoic-ad-content overflow-hidden flex items-center justify-center">
{isRendered && <div id={`ezoic-pub-ad-placeholder-${id}`} ref={placeholderRef} />}
</div>
<div id={`ezoic-pub-ad-placeholder-${id}`} className="overflow-hidden" />
</div>
);
}
+35 -24
View File
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useRef } from "react";
import { useEffect } from "react";
import Script from "next/script";
import { usePathname } from "next/navigation";
import { runEzoic } from "@/lib/ezoic";
@@ -12,34 +12,21 @@ declare global {
showAds: (...ids: number[]) => void;
destroyAll: () => void;
destroyPlaceholders: (...ids: number[]) => void;
[key: string]: unknown;
};
}
}
export function EzoicAds() {
const pathname = usePathname();
const isFirstRender = useRef(true);
// Re-trigger ads on SPA route changes
// Per Ezoic docs: destroyPlaceholders first, then showAds in next frame.
// Skip the first render to avoid racing with individual EzoicPlaceholder
// components that call showAds(id) on mount.
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
runEzoic(() => {
window.ezstandalone?.destroyPlaceholders();
requestAnimationFrame(() => {
window.ezstandalone?.showAds();
});
});
}, [pathname]);
/**
* Ezoic script loader — renders all required <Script> tags.
* Place inside <head> in layout.tsx per official docs.
*
* @see https://docs.ezoic.com/docs/ezoicadsadvanced/nextjs/
*/
export function EzoicScripts() {
return (
<>
{/* Ezoic Privacy / Consent Management - must load first */}
{/* Ezoic Privacy / Consent Management */}
<Script
id="ezoic-cmp"
src="https://cmp.gatekeeperconsent.com/min.js"
@@ -53,12 +40,14 @@ export function EzoicAds() {
data-cfasync="false"
/>
{/* Ezoic Header Script */}
{/* Ezoic standalone script */}
<Script
id="ezoic-sa"
src="//www.ezojs.com/ezoic/sa.min.js"
strategy="afterInteractive"
/>
{/* Ezoic init — cmd queue must exist before any runEzoic() calls */}
<Script id="ezoic-init" strategy="afterInteractive">
{`
window.ezstandalone = window.ezstandalone || {};
@@ -75,3 +64,25 @@ export function EzoicAds() {
</>
);
}
/**
* Handles SPA route changes for Ezoic.
* Destroys stale placeholders and re-scans the DOM on each navigation.
*
* @see https://docs.ezoic.com/docs/ezoicadsadvanced/nextjs/
*/
export function EzoicRouteHandler() {
const pathname = usePathname();
useEffect(() => {
runEzoic(() => {
window.ezstandalone?.destroyPlaceholders();
requestAnimationFrame(() => {
window.ezstandalone?.showAds();
});
});
}, [pathname]);
return null;
}
@@ -5,6 +5,7 @@ import { db } from "@/lib/db";
import { Button } from "@/components/ui/button";
import { Masonry } from "@/components/ui/masonry";
import { PromptCard } from "@/components/prompts/prompt-card";
import { EzoicAd } from "@/components/ads/ezoic-ad";
interface DiscoveryPromptsProps {
isHomepage?: boolean;
@@ -157,6 +158,15 @@ export async function DiscoveryPrompts({ isHomepage = false }: DiscoveryPromptsP
</section>
)}
{/* Ad Placement - after featured */}
{isHomepage && process.env.NEXT_PUBLIC_EZOIC_ENABLED === "true" && (
<section className="py-8 border-b">
<div className="container max-w-2xl">
<EzoicAd id={202} />
</div>
</section>
)}
{/* Today's Most Upvoted Section */}
{todaysMostUpvoted.length > 0 && (
<section className={isHomepage ? "py-12 border-b" : "pb-8 mb-8 border-b"}>
@@ -207,6 +217,15 @@ export async function DiscoveryPrompts({ isHomepage = false }: DiscoveryPromptsP
</section>
)}
{/* Ad Placement - after latest */}
{isHomepage && process.env.NEXT_PUBLIC_EZOIC_ENABLED === "true" && (
<section className="py-8 border-b">
<div className="container max-w-2xl">
<EzoicAd id={203} />
</div>
</section>
)}
{/* Recently Updated Section */}
{recentlyUpdated.length > 0 && (
<section className={isHomepage ? "py-12 border-b" : "pb-8 mb-8 border-b"}>
+1 -1
View File
@@ -199,7 +199,7 @@ export async function StructuredData({ type, data }: StructuredDataProps) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema).replace(/</g, '\\u003c') }}
/>
);
}
+1 -1
View File
@@ -32,7 +32,7 @@ Sentry.init({
integrations: [Sentry.replayIntegration()],
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,
tracesSampleRate: 0.1,
// Enable logs to be sent to Sentry
enableLogs: true,
+5 -8
View File
@@ -2,15 +2,12 @@
* Safely push a function to the Ezoic command queue.
* Initializes window.ezstandalone and cmd array if they don't exist yet,
* preventing errors when components mount before the Ezoic script loads.
*
* @see https://docs.ezoic.com/docs/ezoicadsadvanced/nextjs/
*/
export function runEzoic(fn: () => void) {
if (typeof window === "undefined") return;
if (!window.ezstandalone) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).ezstandalone = { cmd: [] };
}
if (!window.ezstandalone!.cmd) {
window.ezstandalone!.cmd = [];
}
window.ezstandalone!.cmd.push(fn);
const ez = (window.ezstandalone ??= {} as NonNullable<typeof window.ezstandalone>);
ez!.cmd = ez!.cmd || [];
ez!.cmd.push(fn);
}
+2 -2
View File
@@ -21,9 +21,9 @@ export const ezoicWidget: WidgetPlugin = {
position: 4,
mode: "repeat",
repeatEvery: 12,
maxCount: 5,
maxCount: 20,
},
shouldInject: () => true,
shouldInject: () => process.env.NEXT_PUBLIC_EZOIC_ENABLED === "true",
render: (instanceIndex: number) => (
<EzoicPlaceholder id={EZOIC_FEED_BASE_ID + instanceIndex} />
),
+161 -139
View File
@@ -10,7 +10,6 @@ import {
import { z } from "zod";
import { db } from "@/lib/db";
import { isValidApiKeyFormat } from "@/lib/api-key";
import { improvePrompt } from "@/lib/ai/improve-prompt";
import { parseSkillFiles, serializeSkillFiles, DEFAULT_SKILL_FILE } from "@/lib/skill-files";
import appConfig from "@/../prompts.config";
@@ -103,7 +102,7 @@ function createServer(options: ServerOptions = {}) {
{
capabilities: {
prompts: { listChanged: false },
tools: {},
tools: { listChanged: false },
},
}
);
@@ -206,24 +205,30 @@ function createServer(options: ServerOptions = {}) {
const promptSlug = request.params.name;
const args = request.params.arguments || {};
// Fetch all matching prompts and find by slug
const prompts = await db.prompt.findMany({
where: promptFilter,
select: {
id: true,
slug: true,
title: true,
description: true,
content: true,
},
});
const promptSelect = { id: true, slug: true, title: true, description: true, content: true };
// Find by slug field first, then by slugified title, then by id
const prompt = prompts.find((p) =>
p.slug === promptSlug ||
slugify(p.title) === promptSlug ||
p.id === promptSlug
);
// Try direct lookup by slug first
let prompt = await db.prompt.findFirst({
where: { ...promptFilter, slug: promptSlug },
select: promptSelect,
});
// Fallback: lookup by id
if (!prompt) {
prompt = await db.prompt.findFirst({
where: { ...promptFilter, id: promptSlug },
select: promptSelect,
});
}
// Fallback: lookup by slugified title (for prompts without stored slug)
// TODO: Backfill slug column for all existing prompts so this fallback can be removed
if (!prompt) {
const unslugged = await db.prompt.findMany({
where: { ...promptFilter, slug: null },
select: promptSelect,
take: 500,
});
prompt = unslugged.find((p) => slugify(p.title) === promptSlug) || null;
}
if (!prompt) {
throw new Error(`Prompt not found: ${promptSlug}`);
@@ -318,7 +323,6 @@ function createServer(options: ServerOptions = {}) {
description: true,
content: true,
type: true,
structuredFormat: true,
createdAt: true,
author: { select: { username: true, name: true } },
category: { select: { name: true, slug: true } },
@@ -332,9 +336,8 @@ function createServer(options: ServerOptions = {}) {
slug: getPromptName(p),
title: p.title,
description: p.description,
content: p.content,
contentPreview: p.content.substring(0, 1000) + (p.content.length > 1000 ? '...' : ''),
type: p.type,
structuredFormat: p.structuredFormat,
author: p.author.name || p.author.username,
category: p.category?.name || null,
tags: p.tags.map((t) => t.tag.name),
@@ -346,7 +349,7 @@ function createServer(options: ServerOptions = {}) {
content: [
{
type: "text" as const,
text: JSON.stringify({ query, count: results.length, prompts: results }, null, 2),
text: JSON.stringify({ query, count: results.length, prompts: results }),
},
],
};
@@ -368,9 +371,12 @@ function createServer(options: ServerOptions = {}) {
"Get a prompt by ID and optionally fill in its variables. If the prompt contains template variables (like {{variable}}), you will be asked to provide values for them.",
inputSchema: {
id: z.string().describe("The ID of the prompt to retrieve"),
fill_variables: z.boolean().default(false).describe(
"If true and the prompt has template variables, triggers interactive variable filling. Default false — returns raw prompt with variable metadata."
),
},
},
async ({ id }, extra) => {
async ({ id, fill_variables }, extra) => {
try {
const prompt = await db.prompt.findFirst({
where: {
@@ -402,7 +408,7 @@ function createServer(options: ServerOptions = {}) {
const variables = extractVariables(prompt.content);
if (variables.length > 0) {
if (fill_variables && variables.length > 0) {
const properties: Record<string, PrimitiveSchemaDefinition> = {};
const requiredFields: string[] = [];
for (const variable of variables) {
@@ -436,73 +442,75 @@ function createServer(options: ServerOptions = {}) {
},
ElicitResultSchema
);
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Elicitation timeout")), timeoutMs)
);
const result = await Promise.race([elicitationPromise, timeoutPromise]);
if (result.action === "accept" && result.content) {
let filledContent = prompt.content;
for (const [key, value] of Object.entries(result.content)) {
// Replace ${key} or ${key:default} patterns
filledContent = filledContent.replace(
new RegExp(`\\$\\{${key}(?::[^}]*)?\\}`, "g"),
String(value)
);
let timeoutId: NodeJS.Timeout;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => reject(new Error("Elicitation timeout")), timeoutMs);
});
try {
const elicitResult = await Promise.race([elicitationPromise, timeoutPromise]);
clearTimeout(timeoutId!);
if (elicitResult.action === "accept" && elicitResult.content) {
let filledContent = prompt.content;
for (const [key, value] of Object.entries(elicitResult.content)) {
// Skip keys that don't match valid variable name format (ReDoS prevention)
if (!/^[a-zA-Z_][a-zA-Z0-9_\s]*$/.test(key)) {
continue;
}
// Replace ${key} or ${key:default} patterns
filledContent = filledContent.replace(
new RegExp(`\\$\\{${key}(?::[^}]*)?\\}`, "g"),
String(value)
);
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
...prompt,
content: filledContent,
originalContent: prompt.content,
variables: elicitResult.content,
author: prompt.author.name || prompt.author.username,
category: prompt.category?.name || null,
tags: prompt.tags.map((t) => t.tag.name),
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
}),
},
],
};
} else {
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
...prompt,
variablesRequired: variables,
message: "User declined to provide variable values. Returning original prompt.",
author: prompt.author.name || prompt.author.username,
category: prompt.category?.name || null,
tags: prompt.tags.map((t) => t.tag.name),
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
}),
},
],
};
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
...prompt,
content: filledContent,
originalContent: prompt.content,
variables: result.content,
author: prompt.author.name || prompt.author.username,
category: prompt.category?.name || null,
tags: prompt.tags.map((t) => t.tag.name),
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
},
null,
2
),
},
],
};
} else {
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
...prompt,
variablesRequired: variables,
message: "User declined to provide variable values. Returning original prompt.",
author: prompt.author.name || prompt.author.username,
category: prompt.category?.name || null,
tags: prompt.tags.map((t) => t.tag.name),
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
},
null,
2
),
},
],
};
} catch (e) {
clearTimeout(timeoutId!);
throw e;
}
} catch {
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
text: JSON.stringify({
...prompt,
variablesRequired: variables,
message: "Elicitation not supported. Variables need to be filled manually.",
@@ -510,31 +518,41 @@ function createServer(options: ServerOptions = {}) {
category: prompt.category?.name || null,
tags: prompt.tags.map((t) => t.tag.name),
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
},
null,
2
),
}),
},
],
};
}
} else if (variables.length > 0) {
// Return prompt with variable metadata, no timeout
return {
content: [{ type: "text" as const, text: JSON.stringify({
id: prompt.id,
slug: getPromptName(prompt),
title: prompt.title,
description: prompt.description,
content: prompt.content,
type: prompt.type,
author: prompt.author.name || prompt.author.username,
category: prompt.category?.name || null,
tags: prompt.tags.map((t) => t.tag.name),
variables: variables.map(v => ({ name: v.name, defaultValue: v.defaultValue })),
hint: "This prompt has template variables. Call get_prompt with fill_variables=true to fill them interactively.",
}) }],
};
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
text: JSON.stringify({
...prompt,
author: prompt.author.name || prompt.author.username,
category: prompt.category?.name || null,
tags: prompt.tags.map((t) => t.tag.name),
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
},
null,
2
),
}),
},
],
};
@@ -639,8 +657,7 @@ function createServer(options: ServerOptions = {}) {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
text: JSON.stringify({
success: true,
prompt: {
...prompt,
@@ -648,10 +665,7 @@ function createServer(options: ServerOptions = {}) {
category: prompt.category?.name || null,
link: prompt.isPrivate ? null : `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
},
},
null,
2
),
}),
},
],
};
@@ -693,13 +707,14 @@ function createServer(options: ServerOptions = {}) {
}
try {
const { improvePrompt } = await import("@/lib/ai/improve-prompt");
const result = await improvePrompt({ prompt, outputType, outputFormat });
return {
content: [
{
type: "text" as const,
text: JSON.stringify(result, null, 2),
text: JSON.stringify(result),
},
],
};
@@ -810,8 +825,7 @@ function createServer(options: ServerOptions = {}) {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
text: JSON.stringify({
success: true,
skill: {
...skill,
@@ -820,10 +834,7 @@ function createServer(options: ServerOptions = {}) {
category: skill.category?.name || null,
link: skill.isPrivate ? null : `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
},
},
null,
2
),
}),
},
],
};
@@ -910,17 +921,13 @@ function createServer(options: ServerOptions = {}) {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
text: JSON.stringify({
success: true,
message: `File '${filename}' added to skill`,
skillId,
files: files.map(f => f.filename),
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
},
null,
2
),
}),
},
],
};
@@ -1000,17 +1007,13 @@ function createServer(options: ServerOptions = {}) {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
text: JSON.stringify({
success: true,
message: `File '${filename}' updated in skill`,
skillId,
files: files.map(f => f.filename),
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
},
null,
2
),
}),
},
],
};
@@ -1096,17 +1099,13 @@ function createServer(options: ServerOptions = {}) {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
text: JSON.stringify({
success: true,
message: `File '${filename}' removed from skill`,
skillId,
files: updatedFiles.map(f => f.filename),
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
},
null,
2
),
}),
},
],
};
@@ -1181,8 +1180,7 @@ function createServer(options: ServerOptions = {}) {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
text: JSON.stringify({
id: skill.id,
slug: getPromptName(skill),
title: skill.title,
@@ -1199,10 +1197,7 @@ function createServer(options: ServerOptions = {}) {
content: f.content,
})),
link: skill.isPrivate ? null : `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
},
null,
2
),
}),
},
],
};
@@ -1294,7 +1289,8 @@ function createServer(options: ServerOptions = {}) {
category: s.category?.name || null,
tags: s.tags.map((t) => t.tag.name),
votes: s._count.votes,
files: files.map(f => f.filename),
fileNames: files.map(f => f.filename),
fileCount: files.length,
createdAt: s.createdAt.toISOString(),
link: `https://prompts.chat/prompts/${s.id}_${getPromptName(s)}`,
};
@@ -1304,7 +1300,7 @@ function createServer(options: ServerOptions = {}) {
content: [
{
type: "text" as const,
text: JSON.stringify({ query, count: results.length, skills: results }, null, 2),
text: JSON.stringify({ query, count: results.length, skills: results }),
},
],
};
@@ -1321,10 +1317,27 @@ function createServer(options: ServerOptions = {}) {
return server;
}
class PayloadTooLargeError extends Error {
constructor() {
super("Body too large");
this.name = "PayloadTooLargeError";
}
}
async function parseBody(req: NextApiRequest): Promise<unknown> {
const MAX_BODY_SIZE = 1024 * 1024; // 1MB
return new Promise((resolve, reject) => {
let body = "";
req.on("data", (chunk) => (body += chunk));
let bytesReceived = 0;
req.on("data", (chunk: Buffer | string) => {
bytesReceived += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
body += chunk;
if (bytesReceived > MAX_BODY_SIZE) {
req.destroy();
reject(new PayloadTooLargeError());
return;
}
});
req.on("end", () => {
try {
resolve(JSON.parse(body));
@@ -1342,6 +1355,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
if (req.method === "GET") {
res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate=86400');
return res.status(200).json({
name: "prompts-chat",
version: "1.0.0",
@@ -1458,11 +1472,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} catch (error) {
console.error("MCP error:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal server error" },
id: null,
});
if (error instanceof PayloadTooLargeError) {
res.status(413).json({
jsonrpc: "2.0",
error: { code: -32600, message: "Payload too large. Maximum body size is 1MB." },
id: null,
});
} else {
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal server error" },
id: null,
});
}
}
}
}
+59
View File
@@ -0,0 +1,59 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "prompts-chat",
"main": "cloudflare/worker.js",
"compatibility_date": "2026-03-22",
"compatibility_flags": ["nodejs_compat"],
"hyperdrive": [
{
"binding": "HYPERDRIVE",
"id": "2c6ef99cbde84baaac5edacc418eee72"
}
],
"vars": {
"NEXTAUTH_URL": "https://prompts.chat",
"OPENAI_BASE_URL": "https://api.openai.com/v1",
"OPENAI_EMBEDDING_MODEL": "text-embedding-3-large",
"OPENAI_GENERATIVE_MODEL": "gpt-4o",
"GOOGLE_ANALYTICS_ID": "G-MSNHFWTE77",
"ENABLED_STORAGE": "do-spaces",
"DO_SPACES_BUCKET": "prompts-chat-space",
"DO_SPACES_REGION": "fra1",
"FAL_IMAGE_MODELS": "fal-ai/flux-pro/v1.1-ultra,fal-ai/nano-banana-pro",
"FAL_VIDEO_MODELS": "fal-ai/veo3.1/fast",
"FAL_AUDIO_MODELS": "sonauto/v2/text-to-music,cassetteai/music-generator",
"WIRO_IMAGE_MODELS": "google/nano-banana-pro,google/nano-banana",
"WIRO_VIDEO_MODELS": "google/veo3.1-fast",
"EZOIC_SITE_DOMAIN": "prompts.chat",
"NEXT_PUBLIC_EZOIC_ENABLED": "true",
"GOOGLE_ADSENSE_ACCOUNT": "ca-pub-6945602608405209"
},
"containers": [
{
"class_name": "PromptsChatContainer",
"image": "./Dockerfile.cloudflare",
"max_instances": 3,
// standard-1: 2 vCPU, 2 GiB memory
"instance_type": "standard-1"
}
],
"durable_objects": {
"bindings": [
{
"name": "PROMPTS_CHAT",
"class_name": "PromptsChatContainer"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["PromptsChatContainer"]
}
]
}