Compare commits

...

60 Commits

Author SHA1 Message Date
Oleg Proskurin ed9c54da1d fix: ssr error 2026-01-21 20:48:13 +07:00
Oleg Proskurin 348a887e4f fix: social buttons 2026-01-21 20:43:14 +07:00
Oleg Proskurin 8b1487e743 feat: improve blog 2026-01-21 19:19:18 +07:00
Oleg Proskurin fd89bc424e fix: lcp 2026-01-20 21:06:34 +07:00
Oleg Proskurin b6dde32c35 feat: blog metatags 2026-01-20 20:03:34 +07:00
Oleg Proskurin 7a9997cf79 feat: update main blog 2026-01-20 19:28:34 +07:00
Oleg Proskurin cff9197a6b feat: redesign main blog 2026-01-20 17:58:25 +07:00
Oleg Proskurin 070ab0f689 Merge branch 'feature/blog' 2026-01-20 10:14:46 +07:00
Oleg Proskurin f44491da71 feat: add author avatar 2026-01-20 10:14:29 +07:00
Oleg Proskurin 5e70345be2 fix: remove image transformation 2026-01-20 09:11:12 +07:00
Oleg Proskurin dd1e1d2cf2 feat: extend badges 2026-01-19 23:29:54 +07:00
Oleg Proskurin ea8b68c5bb feat: add api details 2026-01-19 23:20:19 +07:00
Oleg Proskurin 26012dc178 feat: add researches 2026-01-19 22:38:32 +07:00
Oleg Proskurin 48a34a28c7 feat: ts index file 2026-01-18 23:13:51 +07:00
Oleg Proskurin 066fcce888 feat: images 2026-01-18 20:10:26 +07:00
Oleg Proskurin 55a3759df2 feat: add article midjorney 2026-01-18 19:26:56 +07:00
Oleg Proskurin f523ba650b fix: z index 2026-01-18 01:21:27 +07:00
Oleg Proskurin 38b377496f feat: apply design for block article 2026-01-18 00:43:15 +07:00
Oleg Proskurin fbe85fe6c9 feat: adjust components 2026-01-18 00:36:13 +07:00
Oleg Proskurin c71b699d66 update mcp 2026-01-18 00:35:52 +07:00
Oleg Proskurin 30fd9ddad2 feat: init blog 2026-01-17 23:35:16 +07:00
Oleg Proskurin 3ced2ec0ec fix: links 2026-01-06 01:05:02 +07:00
Oleg Proskurin 9d9b82da77 Merge branch 'feature/placeholders-guide' 2026-01-06 00:49:55 +07:00
Oleg Proskurin 0dc15d8694 chore: adjust page 2026-01-06 00:49:39 +07:00
Oleg Proskurin 9eb63bea22 feat: improve guide 2026-01-06 00:35:56 +07:00
Oleg Proskurin 855ac3c111 feat: log errors transparent 2026-01-05 22:48:16 +07:00
Oleg Proskurin df3720557c feat: guide update 2026-01-05 21:27:13 +07:00
Oleg Proskurin 77a2a03a81 feat: new version 2026-01-05 11:31:05 +07:00
Oleg Proskurin 8080e7012e doc: prepare for guide 2026-01-04 20:44:21 +07:00
Oleg Proskurin 861aa325e4 fix: use valid UUID for requestId in CDN live URL endpoint
The requestId was being set to a string like "live-avatars-1767520170167"
which is not a valid UUID. This caused PostgreSQL to reject the insert
into the generations table.

Now uses req.requestId from the middleware which generates a proper UUID.
2026-01-04 16:59:12 +07:00
Oleg Proskurin 9b032eaf5a doc: updates to live urls 2026-01-04 15:04:26 +07:00
Oleg Proskurin 1aa307aac9 fix: icons location 2026-01-04 13:46:14 +07:00
Oleg Proskurin b7463258f5 feat: update metadata 2026-01-04 13:31:11 +07:00
Oleg Proskurin 338a083052 feat: add favicon 2026-01-04 13:25:30 +07:00
Oleg Proskurin 582220166a feat: update sidebar 2026-01-03 03:40:50 +07:00
Oleg Proskurin 513b983187 fix: footer 2026-01-03 01:55:32 +07:00
Oleg Proskurin 0ebc893c33 feat: add placeholders to docs 2026-01-03 01:51:13 +07:00
Oleg Proskurin 86528dc6cf tasks: add docs seo 2026-01-02 21:36:38 +07:00
Oleg Proskurin 801080d565 feat: SEO adjustment for homepage 2026-01-02 21:36:17 +07:00
Oleg Proskurin afefb43ce5 tasks: add homepage placeholders 2026-01-02 21:27:34 +07:00
Oleg Proskurin b60a97c73f fix: sidebar 2026-01-02 21:27:07 +07:00
Oleg Proskurin 5d1309633d fix: ensure trailing slashes 2026-01-02 21:08:28 +07:00
Oleg Proskurin 5d7da9e59c fix: use trailing slashes 2026-01-02 19:13:31 +07:00
Oleg Proskurin fcbb5396b3 fix: correct logo 2026-01-02 14:47:00 +07:00
Oleg Proskurin 52649dfb3b feat(docs): add SEO metadata to all documentation pages
- Create centralized SEO config (docs-seo.ts) with DOCS_PAGES constants
  and createDocsMetadata helper for DRY metadata generation
- Add JSON-LD schema helpers (docs-schema.ts) for BreadcrumbList,
  TechArticle, HowTo, and WebAPI structured data
- Create JsonLd component for rendering structured data
- Add metadata exports and JSON-LD to all 10 docs pages:
  - Getting Started, Generation, Images, Live URLs, Authentication
  - API Overview, Generations API, Images API, Flows API, Live Scopes API

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 18:39:39 +07:00
Oleg Proskurin 9a6c409906 fix(docs): correct autoEnhance default value to true in Live URLs
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 17:57:24 +07:00
Oleg Proskurin 4303d25120 docs: add metatags docs 2026-01-01 17:50:16 +07:00
Oleg Proskurin 308bea624e refactor(landing): remove unnecessary 'use client' from docs pages
Remove 'use client' directive from 10 documentation pages that don't
use client-side features. Pages are pure static content; the DocPage
wrapper component handles any client-side functionality.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 16:32:35 +07:00
Oleg Proskurin dbb8504249 docs: update documentation to reflect new 16:9 default aspect ratio
Update default aspect ratio references from 1:1 to 16:9 in:
- Generation guide
- Live Scopes API reference
- Live URLs guide

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 16:32:25 +07:00
Oleg Proskurin 3d18fcfeb5 feat(api): change default aspect ratio from 1:1 to 16:9
Update default aspect ratio for image generation to 16:9 widescreen
format, matching common display resolutions and user expectations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 16:32:18 +07:00
Oleg Proskurin ef47653296 doc: add Gemini image models sizes and aspect ratios reference
Documents supported aspect ratios and resolutions for:
- Gemini 2.5 Flash Image (1K only, 10 aspect ratios)
- Gemini 3 Pro Image (1K/2K/4K, 10 aspect ratios)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 16:32:11 +07:00
Oleg Proskurin 33b01c03a8 Merge branch 'docs-v1' 2025-12-31 20:28:27 +07:00
Oleg Proskurin 4e5a14f3de chore(landing): add docs pages to sitemap
Added all documentation pages to sitemap.xml:
- Getting Started (/docs)
- Image Generation (/docs/generation)
- Working with Images (/docs/images)
- Live URLs (/docs/live-urls)
- Authentication (/docs/authentication)
- API Reference (/docs/api)
- API: Generations, Images, Flows, Live Scopes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:27:27 +07:00
Oleg Proskurin f3aaea1e6d fix(landing): use label as React key in SubsectionNav to avoid duplicates
Multiple disabled nav items share the same href '#', causing React
duplicate key warning. Changed key from item.href to item.label
which is guaranteed to be unique.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:26:47 +07:00
Oleg Proskurin 2d01fa4182 feat(landing): update subnav items to API, SDK, MCP, CLI, Lab
- Docs layout: API (active), SDK/MCP/CLI/Lab (disabled)
- Demo layout: API, SDK/MCP/CLI (disabled), Lab (active, links to demo)
- Disabled items show "Coming soon" tooltip on hover

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 18:38:42 +07:00
Oleg Proskurin 62594774e3 feat(landing): add disabled prop support to SubsectionNav
- Added optional disabled property to NavItem interface
- Disabled items show gray text and cursor-not-allowed
- Desktop: Tooltip on hover shows "Coming soon"
- Mobile: Inline "(Coming soon)" label after item text
- Uses aria-disabled for accessibility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 18:36:29 +07:00
Oleg Proskurin 21dfd31338 feat(landing): implement route groups for different header behaviors
- Created (landings) route group for home page with sticky header
- Created (apps) route group for docs/demo/admin with scrollable header
- Moved page components to respective route groups
- Updated root layout to be minimal (no header/footer)
- Each route group has its own layout with appropriate header style
- Updated Footer and layouts to use public folder logo path

This enables sticky header on landing pages while docs/demo pages
have a header that scrolls away with content.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 18:35:05 +07:00
Oleg Proskurin 358e4db0e3 fix(docs): sidebar active state not showing due to trailing slash mismatch
Next.js usePathname() returns paths with trailing slashes during static
generation (e.g., /docs/) but navigation hrefs use paths without trailing
slashes (e.g., /docs). The strict equality comparison was always failing.

Added path normalization to strip trailing slashes before comparison.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 16:03:46 +07:00
Oleg Proskurin 13f0a4f04f feat: init docs section 2025-12-31 15:00:44 +07:00
Oleg Proskurin 8e315ffe01 doc: save task 2025-12-31 01:28:10 +07:00
144 changed files with 10994 additions and 857 deletions

View File

@ -42,11 +42,9 @@
"PERPLEXITY_TIMEOUT_MS": "600000"
}
},
"browsermcp": {
"type": "stdio",
"chrome-devtools": {
"command": "npx",
"args": ["-y", "@browsermcp/mcp@latest"],
"env": {}
"args": ["-y", "chrome-devtools-mcp@latest"]
}
}
}

View File

@ -427,7 +427,7 @@ cdnRouter.get(
prompt,
aspectRatio: (aspectRatio as string) || GENERATION_LIMITS.DEFAULT_ASPECT_RATIO,
autoEnhance: normalizedAutoEnhance,
requestId: `live-${scope}-${Date.now()}`,
requestId: req.requestId,
});
if (!generation.outputImage) {

View File

@ -12,10 +12,13 @@ import {
import { StorageFactory } from './StorageFactory';
import { TTILogger, TTILogEntry } from './TTILogger';
import { NetworkErrorDetector } from '../utils/NetworkErrorDetector';
import { GeminiErrorDetector } from '../utils/GeminiErrorDetector';
import { ERROR_MESSAGES } from '../utils/constants/errors';
export class ImageGenService {
private ai: GoogleGenAI;
private primaryModel = 'gemini-2.5-flash-image';
private static GEMINI_TIMEOUT_MS = 90_000; // 90 seconds
constructor(apiKey: string) {
if (!apiKey) {
@ -34,7 +37,7 @@ export class ImageGenService {
// Use default values if not provided
const finalOrgSlug = orgSlug || process.env['DEFAULT_ORG_SLUG'] || 'default';
const finalProjectSlug = projectSlug || process.env['DEFAULT_PROJECT_SLUG'] || 'main';
const finalAspectRatio = aspectRatio || '1:1'; // Default to square
const finalAspectRatio = aspectRatio || '16:9'; // Default to widescreen
// Step 1: Generate image from Gemini AI
let generatedData: GeneratedImageData;
@ -205,18 +208,56 @@ export class ImageGenService {
try {
// Use the EXACT same config and contents objects calculated above
const response = await this.ai.models.generateContent({
model: this.primaryModel,
config,
contents,
});
// Wrap with timeout to prevent hanging requests
const response = await this.withTimeout(
this.ai.models.generateContent({
model: this.primaryModel,
config,
contents,
}),
ImageGenService.GEMINI_TIMEOUT_MS,
'Gemini image generation'
);
// Parse response
if (!response.candidates || !response.candidates[0] || !response.candidates[0].content) {
throw new Error('No response received from Gemini AI');
// Log response structure for debugging
GeminiErrorDetector.logResponseStructure(response as any);
// Check promptFeedback for blocked prompts FIRST
if ((response as any).promptFeedback?.blockReason) {
const errorResult = GeminiErrorDetector.analyzeResponse(response as any);
console.error(
`[ImageGenService] Prompt blocked:`,
GeminiErrorDetector.formatForLogging(errorResult!)
);
throw new Error(errorResult!.message);
}
const content = response.candidates[0].content;
// Check if we have candidates
if (!response.candidates || !response.candidates[0]) {
const errorResult = GeminiErrorDetector.analyzeResponse(response as any);
console.error(`[ImageGenService] No candidates in response`);
throw new Error(errorResult?.message || 'No response candidates from Gemini AI');
}
const candidate = response.candidates[0];
// Check finishReason for non-STOP completions
if (candidate.finishReason && candidate.finishReason !== 'STOP') {
const errorResult = GeminiErrorDetector.analyzeResponse(response as any);
console.error(
`[ImageGenService] Non-STOP finish reason:`,
GeminiErrorDetector.formatForLogging(errorResult!)
);
throw new Error(errorResult!.message);
}
// Check content exists
if (!candidate.content) {
console.error(`[ImageGenService] No content in candidate`);
throw new Error('No content in Gemini AI response');
}
const content = candidate.content;
let generatedDescription: string | undefined;
let imageData: { buffer: Buffer; mimeType: string } | null = null;
@ -232,7 +273,14 @@ export class ImageGenService {
}
if (!imageData) {
throw new Error('No image data received from Gemini AI');
// Log what we got instead of image
const partTypes = (content.parts || []).map((p: any) =>
p.inlineData ? 'image' : p.text ? 'text' : 'other'
);
console.error(`[ImageGenService] No image data in response. Parts: [${partTypes.join(', ')}]`);
throw new Error(
`${ERROR_MESSAGES.GEMINI_NO_IMAGE}. Response contained: ${partTypes.join(', ') || 'nothing'}`
);
}
const fileExtension = mime.getExtension(imageData.mimeType) || 'png';
@ -264,6 +312,38 @@ export class ImageGenService {
geminiParams,
};
} catch (error) {
// Check for rate limit (HTTP 429)
const err = error as { status?: number; message?: string };
if (err.status === 429) {
const geminiError = GeminiErrorDetector.classifyApiError(error);
console.error(
`[ImageGenService] Rate limit:`,
GeminiErrorDetector.formatForLogging(geminiError)
);
throw new Error(geminiError.message);
}
// Check for timeout
if (error instanceof Error && error.message.includes('timed out')) {
console.error(
`[ImageGenService] Timeout after ${ImageGenService.GEMINI_TIMEOUT_MS}ms:`,
error.message
);
throw new Error(
`${ERROR_MESSAGES.GEMINI_TIMEOUT} after ${ImageGenService.GEMINI_TIMEOUT_MS / 1000} seconds`
);
}
// Check for other API errors with status codes
if (err.status) {
const geminiError = GeminiErrorDetector.classifyApiError(error);
console.error(
`[ImageGenService] API error:`,
GeminiErrorDetector.formatForLogging(geminiError)
);
throw new Error(geminiError.message);
}
// Enhanced error detection with network diagnostics
if (error instanceof Error) {
// Classify the error and check for network issues (only on failure)
@ -279,6 +359,32 @@ export class ImageGenService {
}
}
/**
* Wrap a promise with timeout
*/
private async withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
operationName: string
): Promise<T> {
let timeoutId: NodeJS.Timeout;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => {
reject(new Error(`${operationName} timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
try {
const result = await Promise.race([promise, timeoutPromise]);
clearTimeout(timeoutId!);
return result;
} catch (error) {
clearTimeout(timeoutId!);
throw error;
}
}
static validateReferenceImages(files: Express.Multer.File[]): {
valid: boolean;
error?: string;

View File

@ -99,6 +99,7 @@ export interface ImageGenerationResult {
model: string;
geminiParams?: GeminiParams; // Gemini SDK parameters used for generation
error?: string;
errorCode?: string; // Gemini-specific error code (GEMINI_RATE_LIMIT, GEMINI_TIMEOUT, etc.)
errorType?: 'generation' | 'storage'; // Distinguish between generation and storage errors
generatedImageData?: GeneratedImageData; // Available when generation succeeds but storage fails
}

View File

@ -0,0 +1,298 @@
import { ERROR_CODES, ERROR_MESSAGES } from './constants/errors';
/**
* Result of Gemini error analysis
*/
export interface GeminiErrorResult {
code: string;
message: string;
finishReason?: string | undefined;
blockReason?: string | undefined;
safetyCategories?: string[] | undefined;
retryAfter?: number | undefined;
httpStatus?: number | undefined;
technicalDetails?: string | undefined;
}
/**
* Safety rating from Gemini response
*/
interface SafetyRating {
category?: string;
probability?: string;
}
/**
* Gemini response structure (partial)
*/
interface GeminiResponse {
candidates?: Array<{
finishReason?: string;
finishMessage?: string;
content?: {
parts?: Array<{
text?: string;
inlineData?: { data?: string; mimeType?: string };
}>;
};
safetyRatings?: SafetyRating[];
}>;
promptFeedback?: {
blockReason?: string;
blockReasonMessage?: string;
safetyRatings?: SafetyRating[];
};
usageMetadata?: {
promptTokenCount?: number;
candidatesTokenCount?: number;
totalTokenCount?: number;
};
}
/**
* Detector for Gemini AI specific errors
* Provides detailed error classification for rate limits, safety blocks, timeouts, etc.
*/
export class GeminiErrorDetector {
/**
* Classify an API-level error (HTTP errors from Gemini)
*/
static classifyApiError(error: unknown): GeminiErrorResult {
const err = error as { status?: number; message?: string; details?: unknown };
// Check for rate limit (HTTP 429)
if (err.status === 429) {
const retryAfter = this.extractRetryAfter(error);
return {
code: ERROR_CODES.GEMINI_RATE_LIMIT,
message: retryAfter
? `${ERROR_MESSAGES.GEMINI_RATE_LIMIT}. Retry after ${retryAfter} seconds.`
: `${ERROR_MESSAGES.GEMINI_RATE_LIMIT}. Please wait before retrying.`,
httpStatus: 429,
retryAfter,
technicalDetails: err.message,
};
}
// Check for authentication errors
if (err.status === 401 || err.status === 403) {
return {
code: ERROR_CODES.GEMINI_API_ERROR,
message: 'Gemini API authentication failed. Check API key.',
httpStatus: err.status,
technicalDetails: err.message,
};
}
// Check for server errors
if (err.status === 500 || err.status === 503) {
return {
code: ERROR_CODES.GEMINI_API_ERROR,
message: 'Gemini API service temporarily unavailable.',
httpStatus: err.status,
technicalDetails: err.message,
};
}
// Check for bad request
if (err.status === 400) {
return {
code: ERROR_CODES.GEMINI_API_ERROR,
message: `Gemini API invalid request: ${err.message || 'Unknown error'}`,
httpStatus: 400,
technicalDetails: err.message,
};
}
// Generic API error
return {
code: ERROR_CODES.GEMINI_API_ERROR,
message: err.message || ERROR_MESSAGES.GEMINI_API_ERROR,
httpStatus: err.status,
technicalDetails: err.message,
};
}
/**
* Analyze a Gemini response for errors (finishReason, blockReason)
* Returns null if no error detected
*/
static analyzeResponse(response: GeminiResponse): GeminiErrorResult | null {
// Check promptFeedback for blocked prompts
if (response.promptFeedback?.blockReason) {
const safetyCategories = this.extractSafetyCategories(
response.promptFeedback.safetyRatings
);
return {
code: ERROR_CODES.GEMINI_CONTENT_BLOCKED,
message:
response.promptFeedback.blockReasonMessage ||
`Prompt blocked: ${response.promptFeedback.blockReason}`,
blockReason: response.promptFeedback.blockReason,
safetyCategories,
technicalDetails: `blockReason: ${response.promptFeedback.blockReason}`,
};
}
// Check candidate finishReason
const candidate = response.candidates?.[0];
if (!candidate) {
return {
code: ERROR_CODES.GEMINI_NO_IMAGE,
message: 'No response candidates from Gemini AI.',
technicalDetails: 'response.candidates is empty or undefined',
};
}
const finishReason = candidate.finishReason;
// STOP is normal completion
if (!finishReason || finishReason === 'STOP') {
return null;
}
// Handle different finishReasons
switch (finishReason) {
case 'SAFETY':
case 'IMAGE_SAFETY': {
const safetyCategories = this.extractSafetyCategories(candidate.safetyRatings);
return {
code: ERROR_CODES.GEMINI_SAFETY_BLOCK,
message: `Content blocked due to safety: ${safetyCategories.join(', ') || 'unspecified'}`,
finishReason,
safetyCategories,
technicalDetails: `finishReason: ${finishReason}, safetyRatings: ${JSON.stringify(candidate.safetyRatings)}`,
};
}
case 'NO_IMAGE':
return {
code: ERROR_CODES.GEMINI_NO_IMAGE,
message: 'Gemini AI could not generate an image for this prompt. Try rephrasing.',
finishReason,
technicalDetails: `finishReason: ${finishReason}`,
};
case 'IMAGE_PROHIBITED_CONTENT':
return {
code: ERROR_CODES.GEMINI_CONTENT_BLOCKED,
message: 'Image generation blocked due to prohibited content in prompt.',
finishReason,
technicalDetails: `finishReason: ${finishReason}`,
};
case 'MAX_TOKENS':
return {
code: ERROR_CODES.GEMINI_API_ERROR,
message: 'Response exceeded maximum token limit. Try a shorter prompt.',
finishReason,
technicalDetails: `finishReason: ${finishReason}`,
};
case 'RECITATION':
case 'IMAGE_RECITATION':
return {
code: ERROR_CODES.GEMINI_CONTENT_BLOCKED,
message: 'Response blocked due to potential copyright concerns.',
finishReason,
technicalDetails: `finishReason: ${finishReason}`,
};
default:
return {
code: ERROR_CODES.GEMINI_API_ERROR,
message: `Generation stopped unexpectedly: ${finishReason}`,
finishReason,
technicalDetails: `finishReason: ${finishReason}, finishMessage: ${candidate.finishMessage}`,
};
}
}
/**
* Check if response has image data
*/
static hasImageData(response: GeminiResponse): boolean {
const parts = response.candidates?.[0]?.content?.parts;
if (!parts) return false;
return parts.some((part) => part.inlineData?.data);
}
/**
* Format error result for logging
*/
static formatForLogging(result: GeminiErrorResult): string {
const parts = [`[${result.code}] ${result.message}`];
if (result.finishReason) {
parts.push(`finishReason=${result.finishReason}`);
}
if (result.blockReason) {
parts.push(`blockReason=${result.blockReason}`);
}
if (result.httpStatus) {
parts.push(`httpStatus=${result.httpStatus}`);
}
if (result.retryAfter) {
parts.push(`retryAfter=${result.retryAfter}s`);
}
if (result.safetyCategories?.length) {
parts.push(`safety=[${result.safetyCategories.join(', ')}]`);
}
return parts.join(' | ');
}
/**
* Log Gemini response structure for debugging
*/
static logResponseStructure(response: GeminiResponse, prefix: string = ''): void {
const parts = response.candidates?.[0]?.content?.parts || [];
const partTypes = parts.map((p) => {
if (p.inlineData) return 'image';
if (p.text) return 'text';
return 'other';
});
console.log(`[ImageGenService]${prefix ? ` [${prefix}]` : ''} Gemini response:`, {
hasCandidates: !!response.candidates?.length,
candidateCount: response.candidates?.length || 0,
finishReason: response.candidates?.[0]?.finishReason || null,
blockReason: response.promptFeedback?.blockReason || null,
partsCount: parts.length,
partTypes,
usageMetadata: response.usageMetadata || null,
});
}
/**
* Extract retry-after value from error
*/
private static extractRetryAfter(error: unknown): number | undefined {
const err = error as { headers?: { get?: (key: string) => string | null } };
// Try to get from headers
if (err.headers?.get) {
const retryAfter = err.headers.get('retry-after');
if (retryAfter) {
const seconds = parseInt(retryAfter, 10);
if (!isNaN(seconds)) return seconds;
}
}
// Default retry after for rate limits
return 60;
}
/**
* Extract safety category names from ratings
*/
private static extractSafetyCategories(ratings?: SafetyRating[]): string[] {
if (!ratings || ratings.length === 0) return [];
// Filter for high/medium probability ratings and extract category names
return ratings
.filter((r) => r.probability === 'HIGH' || r.probability === 'MEDIUM')
.map((r) => r.category?.replace('HARM_CATEGORY_', '') || 'UNKNOWN')
.filter((c) => c !== 'UNKNOWN');
}
}

View File

@ -51,6 +51,14 @@ export const ERROR_MESSAGES = {
INTERNAL_SERVER_ERROR: 'Internal server error',
INVALID_REQUEST: 'Invalid request',
OPERATION_FAILED: 'Operation failed',
// Gemini AI Errors
GEMINI_RATE_LIMIT: 'Gemini API rate limit exceeded',
GEMINI_CONTENT_BLOCKED: 'Content blocked by Gemini safety filters',
GEMINI_TIMEOUT: 'Gemini API request timed out',
GEMINI_NO_IMAGE: 'Gemini AI could not generate image',
GEMINI_SAFETY_BLOCK: 'Content blocked due to safety concerns',
GEMINI_API_ERROR: 'Gemini API returned an error',
} as const;
export const ERROR_CODES = {
@ -109,6 +117,14 @@ export const ERROR_CODES = {
INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR',
INVALID_REQUEST: 'INVALID_REQUEST',
OPERATION_FAILED: 'OPERATION_FAILED',
// Gemini AI Errors
GEMINI_RATE_LIMIT: 'GEMINI_RATE_LIMIT',
GEMINI_CONTENT_BLOCKED: 'GEMINI_CONTENT_BLOCKED',
GEMINI_TIMEOUT: 'GEMINI_TIMEOUT',
GEMINI_NO_IMAGE: 'GEMINI_NO_IMAGE',
GEMINI_SAFETY_BLOCK: 'GEMINI_SAFETY_BLOCK',
GEMINI_API_ERROR: 'GEMINI_API_ERROR',
} as const;
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];

View File

@ -39,7 +39,7 @@ export const IMAGE_LIMITS = {
export const GENERATION_LIMITS = {
MAX_PROMPT_LENGTH: 5000,
MAX_RETRY_COUNT: 3,
DEFAULT_ASPECT_RATIO: '1:1',
DEFAULT_ASPECT_RATIO: '16:9',
ALLOWED_ASPECT_RATIOS: ['1:1', '16:9', '9:16', '3:2', '2:3', '4:3', '3:4'] as const,
} as const;

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,16 @@
{
"name": "Banatie - AI Image Generation API",
"short_name": "Banatie",
"description": "AI-powered image generation API with built-in CDN delivery",
"icons": [
{ "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
],
"theme_color": "#a855f7",
"background_color": "#0f172a",
"display": "standalone",
"start_url": "/",
"scope": "/",
"orientation": "any",
"categories": ["productivity", "developer tools"]
}

View File

@ -4,8 +4,8 @@ import { usePathname } from 'next/navigation';
import { SubsectionNav } from '@/components/shared/SubsectionNav';
const navItems = [
{ label: 'Master Key', href: '/admin/master' },
{ label: 'API Keys', href: '/admin/apikeys' },
{ label: 'Master Key', href: '/admin/master/' },
{ label: 'API Keys', href: '/admin/apikeys/' },
];
export default function AdminLayout({ children }: { children: React.ReactNode }) {

View File

@ -6,9 +6,11 @@ import { ApiKeyProvider } from '@/components/shared/ApiKeyWidget/apikey-context'
import { PageProvider } from '@/contexts/page-context';
const navItems = [
{ label: 'Text to Image', href: '/demo/tti' },
{ label: 'Upload', href: '/demo/upload' },
{ label: 'Gallery', href: '/demo/gallery' },
{ label: 'API', href: '/docs/' },
{ label: 'SDK', href: '#', disabled: true },
{ label: 'MCP', href: '#', disabled: true },
{ label: 'CLI', href: '#', disabled: true },
{ label: 'Lab', href: '/demo/tti/' },
];
export default function DemoLayout({ children }: { children: React.ReactNode }) {

View File

@ -0,0 +1,353 @@
import type { Metadata } from 'next';
import { TipBox } from '@/components/docs/shared/TipBox';
import { Table } from '@/components/docs/shared/Table';
import { CodeBlock } from '@/components/docs/shared/CodeBlock';
import { DocPage } from '@/components/docs/layout/DocPage';
import { JsonLd } from '@/components/seo/JsonLd';
import { createDocsMetadata, DOCS_PAGES } from '@/config/docs-seo';
import { createBreadcrumbSchema, createTechArticleSchema } from '@/config/docs-schema';
import {
Hero,
SectionHeader,
InlineCode,
EndpointCard,
ResponseBlock,
} from '@/components/docs/blocks';
const PAGE = DOCS_PAGES['api-flows'];
export const metadata: Metadata = createDocsMetadata(PAGE);
const breadcrumbSchema = createBreadcrumbSchema([
{ name: 'Home', path: '/' },
{ name: 'Documentation', path: '/docs/' },
{ name: 'API Reference', path: '/docs/api/' },
{ name: 'Flows', path: '/docs/api/flows/' },
]);
const articleSchema = createTechArticleSchema(PAGE);
const tocItems = [
{ id: 'overview', text: 'Overview', level: 2 },
{ id: 'list-flows', text: 'List Flows', level: 2 },
{ id: 'get-flow', text: 'Get Flow', level: 2 },
{ id: 'list-flow-generations', text: 'List Flow Generations', level: 2 },
{ id: 'list-flow-images', text: 'List Flow Images', level: 2 },
{ id: 'update-flow-aliases', text: 'Update Flow Aliases', level: 2 },
{ id: 'remove-flow-alias', text: 'Remove Flow Alias', level: 2 },
{ id: 'regenerate-flow', text: 'Regenerate Flow', level: 2 },
{ id: 'delete-flow', text: 'Delete Flow', level: 2 },
{ id: 'next-steps', text: 'Next Steps', level: 2 },
];
export default function FlowsAPIPage() {
return (
<>
<JsonLd data={breadcrumbSchema} />
<JsonLd data={articleSchema} />
<DocPage
breadcrumbItems={[
{ label: 'Documentation', href: '/docs/' },
{ label: 'API Reference', href: '/docs/api/' },
{ label: 'Flows' },
]}
tocItems={tocItems}
nextSteps={{
links: [
{
href: '/docs/generation/',
title: 'Image Generation Guide',
description: 'Learn about chaining generations with flows.',
accent: 'primary',
},
{
href: '/docs/api/generations/',
title: 'Generations API',
description: 'Create generations within flows.',
accent: 'secondary',
},
],
}}
>
<Hero
title="Flows API"
subtitle="Manage generation chains and flow-scoped aliases."
/>
<section id="overview" className="mb-12">
<SectionHeader level={2} id="overview">
Overview
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-4">
Flows group related generations together. They're created automatically when you chain generations using the same flowId.
</p>
<p className="text-gray-300 leading-relaxed">
Flows also support flow-scoped aliases named references to images that are unique within a flow but don't conflict with project-level aliases.
</p>
</section>
<section id="list-flows" className="mb-12">
<SectionHeader level={2} id="list-flows">
List Flows
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Retrieve all flows for your project with computed counts.
</p>
<EndpointCard
method="GET"
endpoint="/api/v1/flows"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Query Parameters</h4>
<Table
headers={['Parameter', 'Type', 'Description']}
rows={[
[<InlineCode key="p">limit</InlineCode>, 'number', 'Results per page (default: 20, max: 100)'],
[<InlineCode key="p">offset</InlineCode>, 'number', 'Number of results to skip'],
]}
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl "https://api.banatie.app/api/v1/flows?limit=10" \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Request"
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Response</h4>
<ResponseBlock
status="success"
statusCode={200}
statusLabel="200 OK"
content={`{
"success": true,
"data": [
{
"id": "770e8400-e29b-41d4-a716-446655440002",
"aliases": {"@hero": "img-uuid-1", "@background": "img-uuid-2"},
"generationCount": 5,
"imageCount": 5,
"createdAt": "2025-01-15T10:00:00Z",
"updatedAt": "2025-01-15T10:30:00Z"
}
],
"pagination": {
"total": 1,
"limit": 10,
"offset": 0,
"hasMore": false
}
}`}
/>
</div>
</section>
<section id="get-flow" className="mb-12">
<SectionHeader level={2} id="get-flow">
Get Flow
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Retrieve a single flow with detailed statistics.
</p>
<EndpointCard
method="GET"
endpoint="/api/v1/flows/:id"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl https://api.banatie.app/api/v1/flows/770e8400-e29b-41d4-a716-446655440002 \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Request"
/>
</div>
</section>
<section id="list-flow-generations" className="mb-12">
<SectionHeader level={2} id="list-flow-generations">
List Flow Generations
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Retrieve all generations in a specific flow.
</p>
<EndpointCard
method="GET"
endpoint="/api/v1/flows/:id/generations"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl "https://api.banatie.app/api/v1/flows/770e8400-e29b-41d4-a716-446655440002/generations?limit=20" \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Request"
/>
</div>
</section>
<section id="list-flow-images" className="mb-12">
<SectionHeader level={2} id="list-flow-images">
List Flow Images
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Retrieve all images (generated and uploaded) in a flow.
</p>
<EndpointCard
method="GET"
endpoint="/api/v1/flows/:id/images"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl "https://api.banatie.app/api/v1/flows/770e8400-e29b-41d4-a716-446655440002/images" \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Request"
/>
</div>
</section>
<section id="update-flow-aliases" className="mb-12">
<SectionHeader level={2} id="update-flow-aliases">
Update Flow Aliases
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Add or update flow-scoped aliases. Aliases are merged with existing ones.
</p>
<EndpointCard
method="PUT"
endpoint="/api/v1/flows/:id/aliases"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Request Body</h4>
<Table
headers={['Parameter', 'Type', 'Description']}
rows={[
[<InlineCode key="p">aliases</InlineCode>, 'object', 'Key-value pairs of aliases to add/update'],
]}
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl -X PUT https://api.banatie.app/api/v1/flows/770e8400-e29b-41d4-a716-446655440002/aliases \\
-H "X-API-Key: YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{
"aliases": {
"@hero": "image-id-123",
"@background": "image-id-456"
}
}'`}
language="bash"
filename="Request"
/>
</div>
</section>
<section id="remove-flow-alias" className="mb-12">
<SectionHeader level={2} id="remove-flow-alias">
Remove Flow Alias
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Remove a specific alias from a flow.
</p>
<EndpointCard
method="DELETE"
endpoint="/api/v1/flows/:id/aliases/:alias"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl -X DELETE https://api.banatie.app/api/v1/flows/770e8400-e29b-41d4-a716-446655440002/aliases/@hero \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Request"
/>
</div>
</section>
<section id="regenerate-flow" className="mb-12">
<SectionHeader level={2} id="regenerate-flow">
Regenerate Flow
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Regenerate the most recent generation in the flow.
</p>
<EndpointCard
method="POST"
endpoint="/api/v1/flows/:id/regenerate"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl -X POST https://api.banatie.app/api/v1/flows/770e8400-e29b-41d4-a716-446655440002/regenerate \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Request"
/>
</div>
</section>
<section id="delete-flow" className="mb-12">
<SectionHeader level={2} id="delete-flow">
Delete Flow
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Delete a flow with cascade deletion. Images with project aliases are preserved.
</p>
<EndpointCard
method="DELETE"
endpoint="/api/v1/flows/:id"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl -X DELETE https://api.banatie.app/api/v1/flows/770e8400-e29b-41d4-a716-446655440002 \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Request"
/>
</div>
<div className="mt-6">
<TipBox variant="compact" type="warning">
Deleting a flow removes all generations and images in it. Images with project aliases are preserved (unlinked from the flow).
</TipBox>
</div>
</section>
</DocPage>
</>
);
}

View File

@ -0,0 +1,345 @@
import type { Metadata } from 'next';
import { TipBox } from '@/components/docs/shared/TipBox';
import { Table } from '@/components/docs/shared/Table';
import { CodeBlock } from '@/components/docs/shared/CodeBlock';
import { DocPage } from '@/components/docs/layout/DocPage';
import { JsonLd } from '@/components/seo/JsonLd';
import { createDocsMetadata, DOCS_PAGES } from '@/config/docs-seo';
import { createBreadcrumbSchema, createTechArticleSchema } from '@/config/docs-schema';
import {
Hero,
SectionHeader,
InlineCode,
EndpointCard,
ResponseBlock,
} from '@/components/docs/blocks';
const PAGE = DOCS_PAGES['api-generations'];
export const metadata: Metadata = createDocsMetadata(PAGE);
const breadcrumbSchema = createBreadcrumbSchema([
{ name: 'Home', path: '/' },
{ name: 'Documentation', path: '/docs/' },
{ name: 'API Reference', path: '/docs/api/' },
{ name: 'Generations', path: '/docs/api/generations/' },
]);
const articleSchema = createTechArticleSchema(PAGE);
const tocItems = [
{ id: 'create-generation', text: 'Create Generation', level: 2 },
{ id: 'list-generations', text: 'List Generations', level: 2 },
{ id: 'get-generation', text: 'Get Generation', level: 2 },
{ id: 'update-generation', text: 'Update Generation', level: 2 },
{ id: 'regenerate', text: 'Regenerate', level: 2 },
{ id: 'delete-generation', text: 'Delete Generation', level: 2 },
{ id: 'next-steps', text: 'Next Steps', level: 2 },
];
export default function GenerationsAPIPage() {
return (
<>
<JsonLd data={breadcrumbSchema} />
<JsonLd data={articleSchema} />
<DocPage
breadcrumbItems={[
{ label: 'Documentation', href: '/docs/' },
{ label: 'API Reference', href: '/docs/api/' },
{ label: 'Generations' },
]}
tocItems={tocItems}
nextSteps={{
links: [
{
href: '/docs/generation/',
title: 'Image Generation Guide',
description: 'Concepts and examples for image generation.',
accent: 'primary',
},
{
href: '/docs/api/images/',
title: 'Images API',
description: 'Upload and manage images.',
accent: 'secondary',
},
],
}}
>
<Hero
title="Generations API"
subtitle="Create and manage AI image generations."
/>
<section id="create-generation" className="mb-12">
<SectionHeader level={2} id="create-generation">
Create Generation
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Generate a new image from a text prompt.
</p>
<EndpointCard
method="POST"
endpoint="/api/v1/generations"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Request Body</h4>
<Table
headers={['Parameter', 'Type', 'Required', 'Description']}
rows={[
[
<InlineCode key="p">prompt</InlineCode>,
<span key="t" className="text-cyan-400">string</span>,
<span key="r" className="text-green-400">Yes</span>,
'Text description of the image to generate',
],
[
<InlineCode key="p">aspectRatio</InlineCode>,
<span key="t" className="text-cyan-400">string</span>,
<span key="r" className="text-gray-500">No</span>,
'1:1, 16:9, 9:16, 3:2, 21:9 (default: 1:1)',
],
[
<InlineCode key="p">referenceImages</InlineCode>,
<span key="t" className="text-cyan-400">string[]</span>,
<span key="r" className="text-gray-500">No</span>,
'Array of image IDs or @aliases to use as references',
],
[
<InlineCode key="p">flowId</InlineCode>,
<span key="t" className="text-cyan-400">string</span>,
<span key="r" className="text-gray-500">No</span>,
'Associate with existing flow',
],
[
<InlineCode key="p">alias</InlineCode>,
<span key="t" className="text-cyan-400">string</span>,
<span key="r" className="text-gray-500">No</span>,
'Project-scoped alias (@custom-name)',
],
[
<InlineCode key="p">autoEnhance</InlineCode>,
<span key="t" className="text-cyan-400">boolean</span>,
<span key="r" className="text-gray-500">No</span>,
'Enable prompt enhancement (default: true)',
],
[
<InlineCode key="p">meta</InlineCode>,
<span key="t" className="text-cyan-400">object</span>,
<span key="r" className="text-gray-500">No</span>,
'Custom metadata',
],
]}
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl -X POST https://api.banatie.app/api/v1/generations \\
-H "X-API-Key: YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{
"prompt": "a serene mountain landscape at sunset",
"aspectRatio": "16:9"
}'`}
language="bash"
filename="Request"
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Response</h4>
<ResponseBlock
status="success"
statusCode={201}
statusLabel="201 Created"
content={`{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "success",
"prompt": "a serene mountain landscape at sunset",
"aspectRatio": "16:9",
"outputImage": {
"id": "8a3b2c1d-4e5f-6789-abcd-ef0123456789",
"storageUrl": "https://cdn.banatie.app/my-org/my-project/img/8a3b2c1d-4e5f-6789-abcd-ef0123456789",
"width": 1792,
"height": 1008
},
"flowId": "770e8400-e29b-41d4-a716-446655440002",
"createdAt": "2025-01-15T10:30:00Z"
}
}`}
/>
</div>
</section>
<section id="list-generations" className="mb-12">
<SectionHeader level={2} id="list-generations">
List Generations
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Retrieve all generations for your project with optional filtering.
</p>
<EndpointCard
method="GET"
endpoint="/api/v1/generations"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Query Parameters</h4>
<Table
headers={['Parameter', 'Type', 'Description']}
rows={[
[<InlineCode key="p">flowId</InlineCode>, 'string', 'Filter by flow ID'],
[<InlineCode key="p">status</InlineCode>, 'string', 'Filter by status: pending, processing, success, failed'],
[<InlineCode key="p">limit</InlineCode>, 'number', 'Results per page (default: 20, max: 100)'],
[<InlineCode key="p">offset</InlineCode>, 'number', 'Number of results to skip'],
]}
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl "https://api.banatie.app/api/v1/generations?limit=10&status=success" \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Request"
/>
</div>
</section>
<section id="get-generation" className="mb-12">
<SectionHeader level={2} id="get-generation">
Get Generation
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Retrieve a single generation by ID.
</p>
<EndpointCard
method="GET"
endpoint="/api/v1/generations/:id"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl https://api.banatie.app/api/v1/generations/550e8400-e29b-41d4-a716-446655440000 \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Request"
/>
</div>
</section>
<section id="update-generation" className="mb-12">
<SectionHeader level={2} id="update-generation">
Update Generation
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Update generation parameters. Changing prompt or aspectRatio triggers automatic regeneration.
</p>
<EndpointCard
method="PUT"
endpoint="/api/v1/generations/:id"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Request Body</h4>
<Table
headers={['Parameter', 'Type', 'Description']}
rows={[
[<InlineCode key="p">prompt</InlineCode>, 'string', 'New prompt (triggers regeneration)'],
[<InlineCode key="p">aspectRatio</InlineCode>, 'string', 'New aspect ratio (triggers regeneration)'],
[<InlineCode key="p">flowId</InlineCode>, 'string | null', 'Change flow association'],
[<InlineCode key="p">meta</InlineCode>, 'object', 'Update custom metadata'],
]}
/>
</div>
<div className="mt-6">
<TipBox variant="compact" type="info">
Changing <InlineCode>prompt</InlineCode> or <InlineCode>aspectRatio</InlineCode> triggers a new generation. The image ID and URL remain the same only the content changes.
</TipBox>
</div>
</section>
<section id="regenerate" className="mb-12">
<SectionHeader level={2} id="regenerate">
Regenerate
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Create a new image using the exact same parameters. Useful for getting a different result or recovering from failures.
</p>
<EndpointCard
method="POST"
endpoint="/api/v1/generations/:id/regenerate"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl -X POST https://api.banatie.app/api/v1/generations/550e8400-e29b-41d4-a716-446655440000/regenerate \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Request"
/>
</div>
</section>
<section id="delete-generation" className="mb-12">
<SectionHeader level={2} id="delete-generation">
Delete Generation
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Delete a generation and its output image. Images with project aliases are preserved.
</p>
<EndpointCard
method="DELETE"
endpoint="/api/v1/generations/:id"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl -X DELETE https://api.banatie.app/api/v1/generations/550e8400-e29b-41d4-a716-446655440000 \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Request"
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Response</h4>
<ResponseBlock
status="success"
statusCode={200}
statusLabel="200 OK"
content={`{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000"
}
}`}
/>
</div>
</section>
</DocPage>
</>
);
}

View File

@ -0,0 +1,380 @@
import type { Metadata } from 'next';
import { TipBox } from '@/components/docs/shared/TipBox';
import { Table } from '@/components/docs/shared/Table';
import { CodeBlock } from '@/components/docs/shared/CodeBlock';
import { DocPage } from '@/components/docs/layout/DocPage';
import { JsonLd } from '@/components/seo/JsonLd';
import { createDocsMetadata, DOCS_PAGES } from '@/config/docs-seo';
import { createBreadcrumbSchema, createTechArticleSchema } from '@/config/docs-schema';
import {
Hero,
SectionHeader,
InlineCode,
EndpointCard,
ResponseBlock,
} from '@/components/docs/blocks';
const PAGE = DOCS_PAGES['api-images'];
export const metadata: Metadata = createDocsMetadata(PAGE);
const breadcrumbSchema = createBreadcrumbSchema([
{ name: 'Home', path: '/' },
{ name: 'Documentation', path: '/docs/' },
{ name: 'API Reference', path: '/docs/api/' },
{ name: 'Images', path: '/docs/api/images/' },
]);
const articleSchema = createTechArticleSchema(PAGE);
const tocItems = [
{ id: 'upload-image', text: 'Upload Image', level: 2 },
{ id: 'list-images', text: 'List Images', level: 2 },
{ id: 'get-image', text: 'Get Image', level: 2 },
{ id: 'update-image', text: 'Update Image', level: 2 },
{ id: 'assign-alias', text: 'Assign Alias', level: 2 },
{ id: 'delete-image', text: 'Delete Image', level: 2 },
{ id: 'cdn-endpoints', text: 'CDN Endpoints', level: 2 },
{ id: 'next-steps', text: 'Next Steps', level: 2 },
];
export default function ImagesAPIPage() {
return (
<>
<JsonLd data={breadcrumbSchema} />
<JsonLd data={articleSchema} />
<DocPage
breadcrumbItems={[
{ label: 'Documentation', href: '/docs/' },
{ label: 'API Reference', href: '/docs/api/' },
{ label: 'Images' },
]}
tocItems={tocItems}
nextSteps={{
links: [
{
href: '/docs/images/',
title: 'Working with Images Guide',
description: 'Concepts and examples for image management.',
accent: 'primary',
},
{
href: '/docs/api/generations/',
title: 'Generations API',
description: 'Create AI-generated images.',
accent: 'secondary',
},
],
}}
>
<Hero
title="Images API"
subtitle="Upload and manage images for your project."
/>
<section id="upload-image" className="mb-12">
<SectionHeader level={2} id="upload-image">
Upload Image
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Upload an image file to your project storage.
</p>
<EndpointCard
method="POST"
endpoint="/api/v1/images/upload"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Form Data</h4>
<Table
headers={['Parameter', 'Type', 'Required', 'Description']}
rows={[
[
<InlineCode key="p">file</InlineCode>,
<span key="t" className="text-cyan-400">file</span>,
<span key="r" className="text-green-400">Yes</span>,
'Image file (max 5MB, JPEG/PNG/WebP)',
],
[
<InlineCode key="p">alias</InlineCode>,
<span key="t" className="text-cyan-400">string</span>,
<span key="r" className="text-gray-500">No</span>,
'Project-scoped alias (@custom-name)',
],
[
<InlineCode key="p">flowId</InlineCode>,
<span key="t" className="text-cyan-400">string</span>,
<span key="r" className="text-gray-500">No</span>,
'Associate with flow',
],
[
<InlineCode key="p">meta</InlineCode>,
<span key="t" className="text-cyan-400">string</span>,
<span key="r" className="text-gray-500">No</span>,
'Custom metadata (JSON string)',
],
]}
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl -X POST https://api.banatie.app/api/v1/images/upload \\
-H "X-API-Key: YOUR_API_KEY" \\
-F "file=@your-image.png" \\
-F "alias=@brand-logo"`}
language="bash"
filename="Request"
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Response</h4>
<ResponseBlock
status="success"
statusCode={201}
statusLabel="201 Created"
content={`{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"storageUrl": "https://cdn.banatie.app/my-org/my-project/img/550e8400-e29b-41d4-a716-446655440000",
"alias": "@brand-logo",
"source": "uploaded",
"width": 512,
"height": 512,
"mimeType": "image/png",
"fileSize": 24576,
"createdAt": "2025-01-15T10:30:00Z"
}
}`}
/>
</div>
</section>
<section id="list-images" className="mb-12">
<SectionHeader level={2} id="list-images">
List Images
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Retrieve all images in your project with optional filtering.
</p>
<EndpointCard
method="GET"
endpoint="/api/v1/images"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Query Parameters</h4>
<Table
headers={['Parameter', 'Type', 'Description']}
rows={[
[<InlineCode key="p">flowId</InlineCode>, 'string', 'Filter by flow ID'],
[<InlineCode key="p">source</InlineCode>, 'string', 'Filter by source: generated, uploaded'],
[<InlineCode key="p">alias</InlineCode>, 'string', 'Filter by exact alias match'],
[<InlineCode key="p">limit</InlineCode>, 'number', 'Results per page (default: 20, max: 100)'],
[<InlineCode key="p">offset</InlineCode>, 'number', 'Number of results to skip'],
]}
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl "https://api.banatie.app/api/v1/images?source=uploaded&limit=20" \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Request"
/>
</div>
</section>
<section id="get-image" className="mb-12">
<SectionHeader level={2} id="get-image">
Get Image
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Retrieve a single image by ID or alias.
</p>
<EndpointCard
method="GET"
endpoint="/api/v1/images/:id_or_alias"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Requests</h4>
<CodeBlock
code={`# By UUID
curl https://api.banatie.app/api/v1/images/550e8400-e29b-41d4-a716-446655440000 \\
-H "X-API-Key: YOUR_API_KEY"
# By alias
curl https://api.banatie.app/api/v1/images/@brand-logo \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Request"
/>
</div>
</section>
<section id="update-image" className="mb-12">
<SectionHeader level={2} id="update-image">
Update Image
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Update image metadata (focal point and custom metadata).
</p>
<EndpointCard
method="PUT"
endpoint="/api/v1/images/:id_or_alias"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Request Body</h4>
<Table
headers={['Parameter', 'Type', 'Description']}
rows={[
[<InlineCode key="p">focalPoint</InlineCode>, 'object', 'Focal point for cropping {x: 0-1, y: 0-1}'],
[<InlineCode key="p">meta</InlineCode>, 'object', 'Custom metadata'],
]}
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl -X PUT https://api.banatie.app/api/v1/images/550e8400-e29b-41d4-a716-446655440000 \\
-H "X-API-Key: YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{
"focalPoint": {"x": 0.5, "y": 0.3},
"meta": {"category": "hero"}
}'`}
language="bash"
filename="Request"
/>
</div>
</section>
<section id="assign-alias" className="mb-12">
<SectionHeader level={2} id="assign-alias">
Assign Alias
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Assign or remove a project-scoped alias from an image.
</p>
<EndpointCard
method="PUT"
endpoint="/api/v1/images/:id_or_alias/alias"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Request Body</h4>
<Table
headers={['Parameter', 'Type', 'Description']}
rows={[
[<InlineCode key="p">alias</InlineCode>, 'string | null', 'Alias to assign (@name) or null to remove'],
]}
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Requests</h4>
<CodeBlock
code={`# Assign alias
curl -X PUT https://api.banatie.app/api/v1/images/550e8400-e29b-41d4-a716-446655440000/alias \\
-H "X-API-Key: YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{"alias": "@hero-background"}'
# Remove alias
curl -X PUT https://api.banatie.app/api/v1/images/550e8400-e29b-41d4-a716-446655440000/alias \\
-H "X-API-Key: YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{"alias": null}'`}
language="bash"
filename="Request"
/>
</div>
</section>
<section id="delete-image" className="mb-12">
<SectionHeader level={2} id="delete-image">
Delete Image
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Permanently delete an image from storage.
</p>
<EndpointCard
method="DELETE"
endpoint="/api/v1/images/:id_or_alias"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl -X DELETE https://api.banatie.app/api/v1/images/550e8400-e29b-41d4-a716-446655440000 \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Request"
/>
</div>
<div className="mt-6">
<TipBox variant="compact" type="warning">
Deletion is permanent. The image file and all references are removed from storage.
</TipBox>
</div>
</section>
<section id="cdn-endpoints" className="mb-12">
<SectionHeader level={2} id="cdn-endpoints">
CDN Endpoints
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Access images directly via CDN (public, no authentication required):
</p>
<div className="space-y-6">
<div>
<h4 className="text-sm font-semibold text-white mb-3">By Image ID</h4>
<CodeBlock
code={`GET https://cdn.banatie.app/{org}/{project}/img/{imageId}`}
language="text"
filename="CDN by ID"
/>
</div>
<div>
<h4 className="text-sm font-semibold text-white mb-3">By Alias</h4>
<CodeBlock
code={`GET https://cdn.banatie.app/{org}/{project}/img/@{alias}`}
language="text"
filename="CDN by Alias"
/>
</div>
</div>
<div className="mt-6">
<TipBox variant="compact" type="info">
CDN URLs are public and don't require authentication. They return image bytes directly with appropriate caching headers.
</TipBox>
</div>
</section>
</DocPage>
</>
);
}

View File

@ -0,0 +1,438 @@
import type { Metadata } from 'next';
import { TipBox } from '@/components/docs/shared/TipBox';
import { Table } from '@/components/docs/shared/Table';
import { CodeBlock } from '@/components/docs/shared/CodeBlock';
import { DocPage } from '@/components/docs/layout/DocPage';
import { JsonLd } from '@/components/seo/JsonLd';
import { createDocsMetadata, DOCS_PAGES } from '@/config/docs-seo';
import { createBreadcrumbSchema, createTechArticleSchema } from '@/config/docs-schema';
import {
Hero,
SectionHeader,
InlineCode,
EndpointCard,
ResponseBlock,
} from '@/components/docs/blocks';
const PAGE = DOCS_PAGES['api-live-scopes'];
export const metadata: Metadata = createDocsMetadata(PAGE);
const breadcrumbSchema = createBreadcrumbSchema([
{ name: 'Home', path: '/' },
{ name: 'Documentation', path: '/docs/' },
{ name: 'API Reference', path: '/docs/api/' },
{ name: 'Live Scopes', path: '/docs/api/live-scopes/' },
]);
const articleSchema = createTechArticleSchema(PAGE);
const tocItems = [
{ id: 'overview', text: 'Overview', level: 2 },
{ id: 'create-scope', text: 'Create Scope', level: 2 },
{ id: 'list-scopes', text: 'List Scopes', level: 2 },
{ id: 'get-scope', text: 'Get Scope', level: 2 },
{ id: 'update-scope', text: 'Update Scope', level: 2 },
{ id: 'regenerate-scope', text: 'Regenerate Scope', level: 2 },
{ id: 'delete-scope', text: 'Delete Scope', level: 2 },
{ id: 'cdn-live-endpoint', text: 'CDN Live Endpoint', level: 2 },
{ id: 'next-steps', text: 'Next Steps', level: 2 },
];
export default function LiveScopesAPIPage() {
return (
<>
<JsonLd data={breadcrumbSchema} />
<JsonLd data={articleSchema} />
<DocPage
breadcrumbItems={[
{ label: 'Documentation', href: '/docs/' },
{ label: 'API Reference', href: '/docs/api/' },
{ label: 'Live Scopes' },
]}
tocItems={tocItems}
nextSteps={{
links: [
{
href: '/docs/live-urls/',
title: 'Live URLs Guide',
description: 'Learn about live URL generation.',
accent: 'primary',
},
{
href: '/docs/api/generations/',
title: 'Generations API',
description: 'Full control via the generations API.',
accent: 'secondary',
},
],
}}
>
<Hero
title="Live Scopes API"
subtitle="Manage scopes for live URL generation."
/>
<section id="overview" className="mb-12">
<SectionHeader level={2} id="overview">
Overview
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-4">
Live scopes organize live URL generations. Each scope has its own generation limit and can be configured independently.
</p>
<p className="text-gray-300 leading-relaxed">
Scopes are auto-created on first use, but you can pre-configure them via this API to set custom limits.
</p>
</section>
<section id="create-scope" className="mb-12">
<SectionHeader level={2} id="create-scope">
Create Scope
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Create a new live scope with custom settings.
</p>
<EndpointCard
method="POST"
endpoint="/api/v1/live/scopes"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Request Body</h4>
<Table
headers={['Parameter', 'Type', 'Required', 'Description']}
rows={[
[
<InlineCode key="p">slug</InlineCode>,
<span key="t" className="text-cyan-400">string</span>,
<span key="r" className="text-green-400">Yes</span>,
'Unique scope identifier (alphanumeric + hyphens + underscores)',
],
[
<InlineCode key="p">allowNewGenerations</InlineCode>,
<span key="t" className="text-cyan-400">boolean</span>,
<span key="r" className="text-gray-500">No</span>,
'Allow new generations (default: true)',
],
[
<InlineCode key="p">newGenerationsLimit</InlineCode>,
<span key="t" className="text-cyan-400">number</span>,
<span key="r" className="text-gray-500">No</span>,
'Maximum generations allowed (default: 30)',
],
[
<InlineCode key="p">meta</InlineCode>,
<span key="t" className="text-cyan-400">object</span>,
<span key="r" className="text-gray-500">No</span>,
'Custom metadata',
],
]}
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl -X POST https://api.banatie.app/api/v1/live/scopes \\
-H "X-API-Key: YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{
"slug": "hero-section",
"allowNewGenerations": true,
"newGenerationsLimit": 50,
"meta": {"description": "Hero section images"}
}'`}
language="bash"
filename="Request"
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Response</h4>
<ResponseBlock
status="success"
statusCode={201}
statusLabel="201 Created"
content={`{
"success": true,
"data": {
"id": "880e8400-e29b-41d4-a716-446655440003",
"slug": "hero-section",
"allowNewGenerations": true,
"newGenerationsLimit": 50,
"currentGenerations": 0,
"lastGeneratedAt": null,
"meta": {"description": "Hero section images"},
"createdAt": "2025-01-15T10:30:00Z"
}
}`}
/>
</div>
</section>
<section id="list-scopes" className="mb-12">
<SectionHeader level={2} id="list-scopes">
List Scopes
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Retrieve all live scopes for your project.
</p>
<EndpointCard
method="GET"
endpoint="/api/v1/live/scopes"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Query Parameters</h4>
<Table
headers={['Parameter', 'Type', 'Description']}
rows={[
[<InlineCode key="p">slug</InlineCode>, 'string', 'Filter by exact slug match'],
[<InlineCode key="p">limit</InlineCode>, 'number', 'Results per page (default: 20, max: 100)'],
[<InlineCode key="p">offset</InlineCode>, 'number', 'Number of results to skip'],
]}
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl "https://api.banatie.app/api/v1/live/scopes?limit=20" \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Request"
/>
</div>
</section>
<section id="get-scope" className="mb-12">
<SectionHeader level={2} id="get-scope">
Get Scope
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Retrieve a single scope with statistics.
</p>
<EndpointCard
method="GET"
endpoint="/api/v1/live/scopes/:slug"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl https://api.banatie.app/api/v1/live/scopes/hero-section \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Request"
/>
</div>
</section>
<section id="update-scope" className="mb-12">
<SectionHeader level={2} id="update-scope">
Update Scope
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Update scope settings. Changes take effect immediately.
</p>
<EndpointCard
method="PUT"
endpoint="/api/v1/live/scopes/:slug"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Request Body</h4>
<Table
headers={['Parameter', 'Type', 'Description']}
rows={[
[<InlineCode key="p">allowNewGenerations</InlineCode>, 'boolean', 'Allow/disallow new generations'],
[<InlineCode key="p">newGenerationsLimit</InlineCode>, 'number', 'Update generation limit'],
[<InlineCode key="p">meta</InlineCode>, 'object', 'Update custom metadata'],
]}
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl -X PUT https://api.banatie.app/api/v1/live/scopes/hero-section \\
-H "X-API-Key: YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{
"allowNewGenerations": false,
"newGenerationsLimit": 100
}'`}
language="bash"
filename="Request"
/>
</div>
</section>
<section id="regenerate-scope" className="mb-12">
<SectionHeader level={2} id="regenerate-scope">
Regenerate Scope
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Regenerate images in a scope. Can regenerate a specific image or all images.
</p>
<EndpointCard
method="POST"
endpoint="/api/v1/live/scopes/:slug/regenerate"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Request Body</h4>
<Table
headers={['Parameter', 'Type', 'Description']}
rows={[
[<InlineCode key="p">imageId</InlineCode>, 'string', 'Specific image to regenerate (omit for all)'],
]}
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Requests</h4>
<CodeBlock
code={`# Regenerate specific image
curl -X POST https://api.banatie.app/api/v1/live/scopes/hero-section/regenerate \\
-H "X-API-Key: YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{"imageId": "550e8400-e29b-41d4-a716-446655440000"}'
# Regenerate all images in scope
curl -X POST https://api.banatie.app/api/v1/live/scopes/hero-section/regenerate \\
-H "X-API-Key: YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{}'`}
language="bash"
filename="Request"
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Response</h4>
<ResponseBlock
status="success"
statusCode={200}
statusLabel="200 OK"
content={`{
"success": true,
"data": {
"regenerated": 3,
"images": [...]
}
}`}
/>
</div>
</section>
<section id="delete-scope" className="mb-12">
<SectionHeader level={2} id="delete-scope">
Delete Scope
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Delete a scope and all its cached images.
</p>
<EndpointCard
method="DELETE"
endpoint="/api/v1/live/scopes/:slug"
baseUrl="https://api.banatie.app"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example Request</h4>
<CodeBlock
code={`curl -X DELETE https://api.banatie.app/api/v1/live/scopes/hero-section \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Request"
/>
</div>
<div className="mt-6">
<TipBox variant="compact" type="warning">
Deleting a scope permanently removes all cached images in it. This cannot be undone.
</TipBox>
</div>
</section>
<section id="cdn-live-endpoint" className="mb-12">
<SectionHeader level={2} id="cdn-live-endpoint">
CDN Live Endpoint
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Public endpoint for live URL generation (no authentication required):
</p>
<CodeBlock
code={`GET https://cdn.banatie.app/{org}/{project}/live/{scope}?prompt=...`}
language="text"
filename="Live URL Format"
/>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Query Parameters</h4>
<Table
headers={['Parameter', 'Required', 'Description']}
rows={[
[
<InlineCode key="p">prompt</InlineCode>,
<span key="r" className="text-green-400">Yes</span>,
'Text description of the image to generate',
],
[
<InlineCode key="p">aspectRatio</InlineCode>,
<span key="r" className="text-gray-500">No</span>,
'Image ratio (default: 16:9)',
],
[
<InlineCode key="p">autoEnhance</InlineCode>,
<span key="r" className="text-gray-500">No</span>,
'Enable prompt enhancement',
],
[
<InlineCode key="p">template</InlineCode>,
<span key="r" className="text-gray-500">No</span>,
'Enhancement template to use',
],
]}
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Example</h4>
<CodeBlock
code={`https://cdn.banatie.app/my-org/my-project/live/hero-section?prompt=mountain+landscape&aspectRatio=16:9`}
language="text"
filename="Live URL Example"
/>
</div>
<div className="mt-6">
<h4 className="text-sm font-semibold text-white mb-4">Response Headers</h4>
<Table
headers={['Header', 'Description']}
rows={[
[<InlineCode key="h">X-Cache-Status</InlineCode>, 'HIT (cached) or MISS (generated)'],
[<InlineCode key="h">X-Scope</InlineCode>, 'Scope identifier'],
[<InlineCode key="h">X-Image-Id</InlineCode>, 'Image UUID'],
[<InlineCode key="h">X-RateLimit-Remaining</InlineCode>, 'Remaining IP rate limit (on MISS)'],
]}
/>
</div>
</section>
</DocPage>
</>
);
}

View File

@ -0,0 +1,258 @@
import type { Metadata } from 'next';
import { TipBox } from '@/components/docs/shared/TipBox';
import { Table } from '@/components/docs/shared/Table';
import { CodeBlock } from '@/components/docs/shared/CodeBlock';
import { DocPage } from '@/components/docs/layout/DocPage';
import { JsonLd } from '@/components/seo/JsonLd';
import { createDocsMetadata, DOCS_PAGES } from '@/config/docs-seo';
import { createBreadcrumbSchema, createTechArticleSchema, API_REFERENCE_SCHEMA } from '@/config/docs-schema';
import {
Hero,
SectionHeader,
InlineCode,
LinkCard,
LinkCardGrid,
} from '@/components/docs/blocks';
const PAGE = DOCS_PAGES['api-overview'];
export const metadata: Metadata = createDocsMetadata(PAGE);
const breadcrumbSchema = createBreadcrumbSchema([
{ name: 'Home', path: '/' },
{ name: 'Documentation', path: '/docs/' },
{ name: 'API Reference', path: '/docs/api/' },
]);
const articleSchema = createTechArticleSchema(PAGE);
const tocItems = [
{ id: 'base-url', text: 'Base URL', level: 2 },
{ id: 'authentication', text: 'Authentication', level: 2 },
{ id: 'response-format', text: 'Response Format', level: 2 },
{ id: 'error-codes', text: 'Common Error Codes', level: 2 },
{ id: 'rate-limits', text: 'Rate Limits', level: 2 },
{ id: 'endpoints', text: 'Endpoints', level: 2 },
{ id: 'next-steps', text: 'Next Steps', level: 2 },
];
export default function APIOverviewPage() {
return (
<>
<JsonLd data={breadcrumbSchema} />
<JsonLd data={articleSchema} />
<JsonLd data={API_REFERENCE_SCHEMA} />
<DocPage
breadcrumbItems={[
{ label: 'Documentation', href: '/docs/' },
{ label: 'API Reference' },
]}
tocItems={tocItems}
nextSteps={{
links: [
{
href: '/docs/api/generations/',
title: 'Generations API',
description: 'Create and manage image generations.',
accent: 'primary',
},
{
href: '/docs/api/images/',
title: 'Images API',
description: 'Upload and organize images.',
accent: 'secondary',
},
],
}}
>
<Hero
title="API Reference"
subtitle="Complete REST API reference for Banatie. All endpoints, parameters, and response formats."
/>
<section id="base-url" className="mb-12">
<SectionHeader level={2} id="base-url">
Base URL
</SectionHeader>
<CodeBlock
code={`https://api.banatie.app`}
language="text"
filename="Base URL"
/>
</section>
<section id="authentication" className="mb-12">
<SectionHeader level={2} id="authentication">
Authentication
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
All endpoints require the <InlineCode>X-API-Key</InlineCode> header:
</p>
<CodeBlock
code={`X-API-Key: your_api_key_here`}
language="text"
filename="Header"
/>
<p className="text-gray-300 leading-relaxed mt-6">
See <a href="/docs/authentication/" className="text-purple-400 hover:underline">Authentication</a> for details on obtaining and using API keys.
</p>
</section>
<section id="response-format" className="mb-12">
<SectionHeader level={2} id="response-format">
Response Format
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
All responses follow a consistent JSON structure:
</p>
<div className="space-y-6">
<div>
<p className="text-sm font-medium text-gray-300 mb-3">Success Response:</p>
<CodeBlock
code={`{
"success": true,
"data": { ... }
}`}
language="json"
filename="Success"
/>
</div>
<div>
<p className="text-sm font-medium text-gray-300 mb-3">Error Response:</p>
<CodeBlock
code={`{
"success": false,
"error": {
"message": "Error description",
"code": "ERROR_CODE"
}
}`}
language="json"
filename="Error"
/>
</div>
<div>
<p className="text-sm font-medium text-gray-300 mb-3">Paginated Response:</p>
<CodeBlock
code={`{
"success": true,
"data": [ ... ],
"pagination": {
"total": 100,
"limit": 20,
"offset": 0,
"hasMore": true
}
}`}
language="json"
filename="Paginated"
/>
</div>
</div>
</section>
<section id="error-codes" className="mb-12">
<SectionHeader level={2} id="error-codes">
Common Error Codes
</SectionHeader>
<Table
headers={['Status', 'Code', 'Description']}
rows={[
[
<InlineCode key="s" color="error">400</InlineCode>,
'VALIDATION_ERROR',
'Missing or invalid parameters',
],
[
<InlineCode key="s" color="error">401</InlineCode>,
'UNAUTHORIZED',
'Missing or invalid API key',
],
[
<InlineCode key="s" color="error">404</InlineCode>,
'*_NOT_FOUND',
'Requested resource not found',
],
[
<InlineCode key="s" color="error">409</InlineCode>,
'ALIAS_CONFLICT',
'Alias already exists',
],
[
<InlineCode key="s" color="error">429</InlineCode>,
'RATE_LIMIT_EXCEEDED',
'Too many requests',
],
[
<InlineCode key="s" color="error">500</InlineCode>,
'INTERNAL_ERROR',
'Server error',
],
]}
/>
</section>
<section id="rate-limits" className="mb-12">
<SectionHeader level={2} id="rate-limits">
Rate Limits
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
API requests are rate limited to 100 requests per hour per API key.
</p>
<p className="text-gray-300 leading-relaxed mb-6">
Rate limit headers are included in every response:
</p>
<Table
headers={['Header', 'Description']}
rows={[
[<InlineCode key="h">X-RateLimit-Limit</InlineCode>, 'Maximum requests per hour'],
[<InlineCode key="h">X-RateLimit-Remaining</InlineCode>, 'Requests remaining in current window'],
[<InlineCode key="h">X-RateLimit-Reset</InlineCode>, 'Unix timestamp when limit resets'],
]}
/>
</section>
<section id="endpoints" className="mb-12">
<SectionHeader level={2} id="endpoints">
Endpoints
</SectionHeader>
<LinkCardGrid columns={2}>
<LinkCard
href="/docs/api/generations/"
title="Generations"
description="Create and manage AI image generations"
accent="primary"
/>
<LinkCard
href="/docs/api/images/"
title="Images"
description="Upload and organize images"
accent="secondary"
/>
<LinkCard
href="/docs/api/flows/"
title="Flows"
description="Manage generation chains"
accent="primary"
/>
<LinkCard
href="/docs/api/live-scopes/"
title="Live Scopes"
description="Control live URL generation"
accent="secondary"
/>
</LinkCardGrid>
</section>
</DocPage>
</>
);
}

View File

@ -0,0 +1,142 @@
import type { Metadata } from 'next';
import { TipBox } from '@/components/docs/shared/TipBox';
import { Table } from '@/components/docs/shared/Table';
import { CodeBlock } from '@/components/docs/shared/CodeBlock';
import { DocPage } from '@/components/docs/layout/DocPage';
import { JsonLd } from '@/components/seo/JsonLd';
import { createDocsMetadata, DOCS_PAGES } from '@/config/docs-seo';
import { createBreadcrumbSchema, createTechArticleSchema } from '@/config/docs-schema';
import {
Hero,
SectionHeader,
InlineCode,
} from '@/components/docs/blocks';
const PAGE = DOCS_PAGES['authentication'];
export const metadata: Metadata = createDocsMetadata(PAGE);
const breadcrumbSchema = createBreadcrumbSchema([
{ name: 'Home', path: '/' },
{ name: 'Documentation', path: '/docs/' },
{ name: 'Authentication', path: '/docs/authentication/' },
]);
const articleSchema = createTechArticleSchema(PAGE);
const tocItems = [
{ id: 'early-access', text: 'Early Access', level: 2 },
{ id: 'using-your-api-key', text: 'Using Your API Key', level: 2 },
{ id: 'key-types', text: 'Key Types', level: 2 },
{ id: 'next-steps', text: 'Next Steps', level: 2 },
];
export default function AuthenticationPage() {
return (
<>
<JsonLd data={breadcrumbSchema} />
<JsonLd data={articleSchema} />
<DocPage
breadcrumbItems={[
{ label: 'Documentation', href: '/docs/' },
{ label: 'Authentication' },
]}
tocItems={tocItems}
nextSteps={{
links: [
{
href: '/docs/generation/',
title: 'Start Generating',
description: 'Create your first AI-generated image.',
accent: 'primary',
},
{
href: '/docs/api/',
title: 'API Reference',
description: 'Full endpoint documentation.',
accent: 'secondary',
},
],
}}
>
<Hero
title="Authentication"
subtitle="How to authenticate with Banatie API using API keys."
/>
<section id="early-access" className="mb-12">
<SectionHeader level={2} id="early-access">
Early Access
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
We're currently in early access phase. API keys are issued personally via email.
</p>
<p className="text-gray-300 leading-relaxed mb-6">
<strong className="text-white">To request access:</strong> Sign up at <a href="https://banatie.app" className="text-purple-400 hover:underline">banatie.app</a>. We'll send your API key within 24 hours.
</p>
</section>
<section id="using-your-api-key" className="mb-12">
<SectionHeader level={2} id="using-your-api-key">
Using Your API Key
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
All API requests require the <InlineCode>X-API-Key</InlineCode> header:
</p>
<CodeBlock
code={`curl -X POST https://api.banatie.app/api/v1/generations \\
-H "X-API-Key: your_key_here" \\
-H "Content-Type: application/json" \\
-d '{"prompt": "a sunset over the ocean"}'`}
language="bash"
filename="Authenticated Request"
/>
<div className="mt-6">
<TipBox variant="prominent" type="warning">
<strong className="text-amber-300">Keep your API key secure.</strong> Don't commit it to version control or expose it in client-side code. Use environment variables in your applications.
</TipBox>
</div>
</section>
<section id="key-types" className="mb-12">
<SectionHeader level={2} id="key-types">
Key Types
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Banatie uses two types of API keys:
</p>
<Table
headers={['Type', 'Permissions', 'Expiration', 'Use Case']}
rows={[
[
<InlineCode key="t">Project Key</InlineCode>,
'Image generation, uploads, images',
<span key="e" className="text-amber-400">90 days</span>,
'Your application integration',
],
[
<InlineCode key="t">Master Key</InlineCode>,
'Full admin access, key management',
<span key="e" className="text-green-400">Never expires</span>,
'Server-side admin operations',
],
]}
/>
<p className="text-gray-300 leading-relaxed mt-6">
You'll receive a Project Key for your application. Master Keys are for administrative operations — you probably don't need one.
</p>
<div className="mt-6">
<TipBox variant="compact" type="info">
API key management dashboard coming soon. For now, contact us if you need to rotate your key.
</TipBox>
</div>
</section>
</DocPage>
</>
);
}

View File

@ -0,0 +1,281 @@
import type { Metadata } from 'next';
import { TipBox } from '@/components/docs/shared/TipBox';
import { Table } from '@/components/docs/shared/Table';
import { CodeBlock } from '@/components/docs/shared/CodeBlock';
import { DocPage } from '@/components/docs/layout/DocPage';
import { JsonLd } from '@/components/seo/JsonLd';
import { createDocsMetadata, DOCS_PAGES } from '@/config/docs-seo';
import { createBreadcrumbSchema, createTechArticleSchema } from '@/config/docs-schema';
import {
Hero,
SectionHeader,
InlineCode,
ResponseBlock,
} from '@/components/docs/blocks';
const PAGE = DOCS_PAGES['generation'];
export const metadata: Metadata = createDocsMetadata(PAGE);
const breadcrumbSchema = createBreadcrumbSchema([
{ name: 'Home', path: '/' },
{ name: 'Documentation', path: '/docs/' },
{ name: 'Image Generation', path: '/docs/generation/' },
]);
const articleSchema = createTechArticleSchema(PAGE);
const tocItems = [
{ id: 'basic-generation', text: 'Basic Generation', level: 2 },
{ id: 'aspect-ratios', text: 'Aspect Ratios', level: 2 },
{ id: 'prompt-templates', text: 'Prompt Templates', level: 2 },
{ id: 'reference-images', text: 'Using Reference Images', level: 2 },
{ id: 'continuing-generation', text: 'Continuing Generation', level: 2 },
{ id: 'regeneration', text: 'Regeneration', level: 2 },
{ id: 'next-steps', text: 'Next Steps', level: 2 },
];
export default function GenerationPage() {
return (
<>
<JsonLd data={breadcrumbSchema} />
<JsonLd data={articleSchema} />
<DocPage
breadcrumbItems={[
{ label: 'Documentation', href: '/docs/' },
{ label: 'Image Generation' },
]}
tocItems={tocItems}
nextSteps={{
links: [
{
href: '/docs/api/generations/',
title: 'API Reference: Generations',
description: 'Full endpoint documentation for generations.',
accent: 'primary',
},
{
href: '/docs/images/',
title: 'Working with Images',
description: 'Upload your own images and organize with aliases.',
accent: 'secondary',
},
],
}}
>
<Hero
title="Image Generation"
subtitle="Generate AI images from text prompts with support for references, templates, and chaining."
/>
<section id="basic-generation" className="mb-12">
<SectionHeader level={2} id="basic-generation">
Basic Generation
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Generate an image by sending a text prompt to the generations endpoint:
</p>
<CodeBlock
code={`curl -X POST https://api.banatie.app/api/v1/generations \\
-H "Content-Type: application/json" \\
-H "X-API-Key: YOUR_API_KEY" \\
-d '{
"prompt": "a serene mountain landscape at sunset",
"aspectRatio": "16:9"
}'`}
language="bash"
filename="Create Generation"
/>
<p className="text-gray-300 leading-relaxed mt-6 mb-4">
The response contains your generated image immediately:
</p>
<ResponseBlock
status="success"
statusCode={201}
statusLabel="201 Created"
content={`{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "success",
"prompt": "a serene mountain landscape at sunset",
"aspectRatio": "16:9",
"outputImage": {
"id": "8a3b2c1d-4e5f-6789-abcd-ef0123456789",
"storageUrl": "https://cdn.banatie.app/my-org/my-project/img/8a3b2c1d-4e5f-6789-abcd-ef0123456789",
"width": 1792,
"height": 1008
},
"flowId": "770e8400-e29b-41d4-a716-446655440002"
}
}`}
/>
<p className="text-gray-300 leading-relaxed mt-6">
One request, one result. The <InlineCode>storageUrl</InlineCode> is your production-ready image, served via CDN.
</p>
</section>
<section id="aspect-ratios" className="mb-12">
<SectionHeader level={2} id="aspect-ratios">
Aspect Ratios
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Choose the aspect ratio that fits your use case:
</p>
<Table
headers={['Ratio', 'Dimensions', 'Best For']}
rows={[
[
<InlineCode key="ratio">1:1</InlineCode>,
'1024 x 1024',
'Social media posts, profile pictures, thumbnails',
],
[
<InlineCode key="ratio">16:9</InlineCode>,
'1792 x 1008',
'Blog headers, presentations, video thumbnails',
],
[
<InlineCode key="ratio">9:16</InlineCode>,
'1008 x 1792',
'Stories, mobile backgrounds, vertical banners',
],
[
<InlineCode key="ratio">3:2</InlineCode>,
'1536 x 1024',
'Photography-style images, print layouts',
],
[
<InlineCode key="ratio">21:9</InlineCode>,
'2016 x 864',
'Ultra-wide banners, cinematic headers',
],
]}
/>
<p className="text-gray-300 leading-relaxed mt-6">
Default is <InlineCode>16:9</InlineCode> if not specified.
</p>
</section>
<section id="prompt-templates" className="mb-12">
<SectionHeader level={2} id="prompt-templates">
Prompt Templates
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Templates improve your prompt for specific styles. Available templates:
</p>
<Table
headers={['Template', 'Description']}
rows={[
[<InlineCode key="t">general</InlineCode>, 'Balanced style for most use cases'],
[<InlineCode key="t">photorealistic</InlineCode>, 'Photo-like realism with natural lighting'],
[<InlineCode key="t">illustration</InlineCode>, 'Artistic illustration style'],
[<InlineCode key="t">minimalist</InlineCode>, 'Clean, simple compositions'],
[<InlineCode key="t">sticker</InlineCode>, 'Sticker-style with clear edges'],
[<InlineCode key="t">product</InlineCode>, 'E-commerce product photography'],
[<InlineCode key="t">comic</InlineCode>, 'Comic book visual style'],
]}
/>
<div className="mt-6">
<TipBox variant="compact" type="info">
Template selection coming soon. Currently uses general style for all generations.
</TipBox>
</div>
</section>
<section id="reference-images" className="mb-12">
<SectionHeader level={2} id="reference-images">
Using Reference Images
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Add reference images for style guidance or context. Pass image IDs or aliases in the <InlineCode>referenceImages</InlineCode> array:
</p>
<CodeBlock
code={`curl -X POST https://api.banatie.app/api/v1/generations \\
-H "Content-Type: application/json" \\
-H "X-API-Key: YOUR_API_KEY" \\
-d '{
"prompt": "product photo in this style",
"referenceImages": ["@brand-style", "@product-template"],
"aspectRatio": "1:1"
}'`}
language="bash"
filename="With References"
/>
<div className="mt-6">
<TipBox variant="compact" type="info">
<strong>Pro Tip:</strong> Use aliases like <InlineCode>@logo</InlineCode> instead of UUIDs. See <a href="/docs/images/" className="text-purple-400 hover:underline">Working with Images</a> to learn about aliases.
</TipBox>
</div>
<p className="text-gray-300 leading-relaxed mt-6">
You can also mention aliases directly in your prompt text they're auto-detected:
</p>
<CodeBlock
code={`{
"prompt": "create a banner using @brand-colors and @logo style"
}`}
language="json"
filename="Auto-detected aliases"
/>
</section>
<section id="continuing-generation" className="mb-12">
<SectionHeader level={2} id="continuing-generation">
Continuing Generation
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Chain multiple generations together by passing the same <InlineCode>flowId</InlineCode>:
</p>
<CodeBlock
code={`curl -X POST https://api.banatie.app/api/v1/generations \\
-H "Content-Type: application/json" \\
-H "X-API-Key: YOUR_API_KEY" \\
-d '{
"prompt": "same scene but at night",
"flowId": "770e8400-e29b-41d4-a716-446655440002"
}'`}
language="bash"
filename="Continue in Flow"
/>
<p className="text-gray-300 leading-relaxed mt-6">
Each response includes a <InlineCode>flowId</InlineCode> you can use to continue the sequence. Flows help organize related generations together.
</p>
</section>
<section id="regeneration" className="mb-12">
<SectionHeader level={2} id="regeneration">
Regeneration
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Want a different result with the same parameters? Regenerate an existing generation:
</p>
<CodeBlock
code={`curl -X POST https://api.banatie.app/api/v1/generations/550e8400-e29b-41d4-a716-446655440000/regenerate \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Regenerate"
/>
<p className="text-gray-300 leading-relaxed mt-6">
Same prompt, new image. The generation ID and URL stay the same the image content is replaced.
</p>
</section>
</DocPage>
</>
);
}

View File

@ -0,0 +1,68 @@
import type { Metadata } from 'next';
import { DocPage } from '@/components/docs/layout/DocPage';
import { JsonLd } from '@/components/seo/JsonLd';
import { createDocsMetadata, DOCS_PAGES } from '@/config/docs-seo';
import { createBreadcrumbSchema } from '@/config/docs-schema';
import { Hero } from '@/components/docs/blocks';
import Link from 'next/link';
const PAGE = DOCS_PAGES['guides'];
export const metadata: Metadata = createDocsMetadata(PAGE);
const breadcrumbSchema = createBreadcrumbSchema([
{ name: 'Home', path: '/' },
{ name: 'Documentation', path: '/docs/' },
{ name: 'Guides', path: '/docs/guides/' },
]);
export default function GuidesPage() {
return (
<>
<JsonLd data={breadcrumbSchema} />
<DocPage
breadcrumbItems={[
{ label: 'Documentation', href: '/docs/' },
{ label: 'Guides' },
]}
tocItems={[]}
nextSteps={{
links: [
{
href: '/docs/live-urls/',
title: 'Live URLs',
description: 'Generate images directly from URL parameters.',
accent: 'primary',
},
{
href: '/docs/generation/',
title: 'Image Generation',
description: 'Full control via the API.',
accent: 'secondary',
},
],
}}
>
<Hero
title="Guides"
subtitle="Step-by-step tutorials for common use cases."
/>
<section className="mb-12">
<div className="grid gap-4">
<Link
href="/docs/guides/placeholder-images/"
className="block p-6 bg-slate-800/50 border border-slate-700 rounded-lg hover:border-purple-500/50 transition-colors"
>
<h3 className="text-lg font-semibold text-white mb-2">AI Placeholder Images</h3>
<p className="text-gray-400 text-sm">
Generate contextual placeholder images for development.
Replace gray boxes with AI visuals that match your design.
</p>
</Link>
</div>
</section>
</DocPage>
</>
);
}

View File

@ -0,0 +1,721 @@
import type { Metadata } from 'next';
import { TipBox } from '@/components/docs/shared/TipBox';
import { Table } from '@/components/docs/shared/Table';
import { CodeBlock } from '@/components/docs/shared/CodeBlock';
import { LivePreview } from '@/components/docs/shared/LivePreview';
import { DocPage } from '@/components/docs/layout/DocPage';
import { JsonLd } from '@/components/seo/JsonLd';
import { createDocsMetadata, DOCS_PAGES } from '@/config/docs-seo';
import { createBreadcrumbSchema, createTechArticleSchema } from '@/config/docs-schema';
import { Hero, SectionHeader, InlineCode } from '@/components/docs/blocks';
const PAGE = DOCS_PAGES['guide-placeholder-images'];
export const metadata: Metadata = createDocsMetadata(PAGE);
const breadcrumbSchema = createBreadcrumbSchema([
{ name: 'Home', path: '/' },
{ name: 'Documentation', path: '/docs/' },
{ name: 'Guides', path: '/docs/guides/' },
{ name: 'Placeholder Images', path: '/docs/guides/placeholder-images/' },
]);
const articleSchema = createTechArticleSchema(PAGE);
const tocItems = [
{ id: 'what-this-guide-covers', text: 'What This Guide Covers', level: 2 },
{ id: 'how-to-create-placeholders', text: 'How to Create Placeholders', level: 2 },
{ id: 'quick-start', text: 'Quick Start', level: 2 },
{ id: 'organizing-placeholders', text: 'Organizing Placeholders', level: 2 },
{ id: 'prompt-tips', text: 'Prompt Tips', level: 2 },
{ id: 'light-mode-placeholders', text: 'Light Mode Placeholders', level: 2 },
{ id: 'dark-mode-placeholders', text: 'Dark Mode Placeholders', level: 2 },
{ id: 'placeholder-image-examples', text: 'Placeholder Image Examples', level: 2 },
{ id: 'file-based-workflow', text: 'File-based Workflow', level: 2 },
];
export default function PlaceholderImagesGuidePage() {
return (
<>
<JsonLd data={breadcrumbSchema} />
<JsonLd data={articleSchema} />
<DocPage
breadcrumbItems={[
{ label: 'Documentation', href: '/docs/' },
{ label: 'Guides', href: '/docs/guides/' },
{ label: 'Placeholder Images' },
]}
tocItems={tocItems}
nextSteps={{
links: [
{
href: '/docs/live-urls/',
title: 'Live URLs Reference',
description: 'Full parameter documentation for Live URLs.',
accent: 'primary',
},
{
href: '/docs/api/generations/',
title: 'Generations API',
description: 'Generate images programmatically.',
accent: 'secondary',
},
],
}}
>
<Hero
title="AI Placeholder Images"
subtitle="Generate contextual images for development. The new era of AI placeholders."
/>
{/* What This Guide Covers */}
<section id="what-this-guide-covers" className="mb-12 scroll-mt-24">
<SectionHeader level={2} id="what-this-guide-covers">
What This Guide Covers
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-4">
Two ways to generate placeholder images with Banatie:
</p>
<ul className="list-disc list-inside text-gray-300 space-y-2 mb-6">
<li>
<a href="#quick-start" className="text-white font-semibold hover:text-purple-400">
Live URLs
</a>{' '}
describe what you need right in <InlineCode>&lt;img&gt;</InlineCode> src URLs
</li>
<li>
<a href="#file-based-workflow" className="text-white font-semibold hover:text-purple-400">
API generation
</a>{' '}
full control, permanent URLs, downloadable files
</li>
</ul>
<p className="text-gray-300 leading-relaxed">
All examples on this page use real placeholder image URLs generated by Banatie.
</p>
</section>
{/* How to Create Placeholders */}
<section id="how-to-create-placeholders" className="mb-12 scroll-mt-24">
<SectionHeader level={2} id="how-to-create-placeholders">
How to Create Placeholders
</SectionHeader>
<div className="space-y-6">
<div>
<h4 className="text-white font-semibold mb-2">Templates</h4>
<p className="text-gray-300 mb-2">
Choose a style, get quality results. Banatie enhances your simple prompt based on
the selected template:
</p>
<ul className="list-disc list-inside text-gray-300 space-y-1">
<li>
<InlineCode>photorealistic</InlineCode> photo-quality images
</li>
<li>
<InlineCode>digital-art</InlineCode> stylized illustrations
</li>
<li>
<InlineCode>3d-render</InlineCode> 3D graphics
</li>
</ul>
<p className="text-gray-400 text-sm mt-3">
<a href="/docs/generation/#prompt-templates" className="text-purple-400 hover:underline">
View all templates
</a>
</p>
</div>
<div>
<h4 className="text-white font-semibold mb-2">Simple Prompts</h4>
<p className="text-gray-300 mb-2">
Write minimal descriptions. Templates handle the rest:
</p>
<ul className="list-none text-gray-300 space-y-1">
<li>
<span className="text-gray-500"></span> &quot;office&quot; becomes a detailed
modern office with proper lighting
</li>
<li>
<span className="text-gray-500"></span> &quot;headshot&quot; becomes a
professional portrait with studio background
</li>
</ul>
<p className="text-gray-400 text-sm mt-3">
No need for complex prompts this is for placeholders, not art.
</p>
</div>
</div>
</section>
{/* Quick Start */}
<section id="quick-start" className="mb-12 scroll-mt-24">
<SectionHeader level={2} id="quick-start">
Quick Start
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Live URLs let you generate images by describing what you need right in the URL. No API
calls, no file management just an HTML placeholder image tag with your prompt. Each
unique prompt is cached, so subsequent loads are instant via CDN.
</p>
<p className="text-gray-300 leading-relaxed mb-4">Basic URL format:</p>
<CodeBlock
code="https://cdn.banatie.app/{org}/{project}/live/{scope}?prompt={description}&aspectRatio={ratio}"
language="text"
filename="URL Format"
/>
<p className="text-gray-300 leading-relaxed mt-6 mb-4">Example:</p>
<CodeBlock
code={`<img
src="https://cdn.banatie.app/sys/demo/live/test?prompt=mountain+landscape"
alt="Mountain landscape"
/>`}
language="html"
filename="HTML Placeholder Image"
/>
<p className="text-gray-300 leading-relaxed mt-4 mb-2">Result:</p>
<LivePreview showLabel={false}>
<img
src="https://cdn.banatie.app/sys/demo/live/test?prompt=mountain+landscape&aspectRatio=16:9"
alt="Mountain landscape"
className="w-full max-w-md rounded-lg"
/>
</LivePreview>
<p className="text-gray-300 leading-relaxed mt-6 mb-4">Parameters:</p>
<Table
headers={['Parameter', 'Required', 'Description']}
rows={[
[<InlineCode key="p">prompt</InlineCode>, 'Yes', 'Image description (URL-encoded)'],
[
<InlineCode key="a">aspectRatio</InlineCode>,
'No',
'Ratio like 1:1, 16:9, 4:3 (default: 16:9)',
],
[<InlineCode key="t">template</InlineCode>, 'No', 'Style template name'],
]}
/>
<p className="text-gray-300 leading-relaxed mt-6">
For full parameter reference, see{' '}
<a href="/docs/live-urls/" className="text-purple-400 hover:underline">
Live URLs documentation
</a>
.
</p>
</section>
{/* Organizing Placeholders */}
<section id="organizing-placeholders" className="mb-12 scroll-mt-24">
<SectionHeader level={2} id="organizing-placeholders">
Organizing Placeholders
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-4">
Organize images by sections of your site using scopes:
</p>
<CodeBlock
code={`/live/avatars?prompt=... → user photos
/live/hero?prompt=... hero backgrounds
/live/products?prompt=... product catalog`}
language="text"
/>
<p className="text-gray-300 leading-relaxed mt-6">
Learn more about scopes in{' '}
<a href="/docs/live-urls/#scopes" className="text-purple-400 hover:underline">
Live URLs documentation
</a>
.
</p>
</section>
{/* Prompt Tips */}
<section id="prompt-tips" className="mb-12 scroll-mt-24">
<SectionHeader level={2} id="prompt-tips">
Prompt Tips
</SectionHeader>
<div className="space-y-6">
<div>
<h4 className="text-white font-semibold mb-2">Write less, not more</h4>
<p className="text-gray-300 mb-2">
For placeholders, simple prompts are often enough. You can always add more details
later if needed. Templates handle the rest:
</p>
<ul className="list-none text-gray-300 space-y-1">
<li>
<span className="text-gray-500"></span> Want an office? Write{' '}
<InlineCode>office</InlineCode>
</li>
<li>
<span className="text-gray-500"></span> Need a dark version? Add{' '}
<InlineCode>office dark background</InlineCode>
</li>
<li>
<span className="text-gray-500"></span> Templates handle lighting, composition,
style
</li>
</ul>
</div>
<div>
<h4 className="text-white font-semibold mb-2">Colors and themes</h4>
<p className="text-gray-300 mb-2">Control the mood with color hints:</p>
<CodeBlock
code={`"dark background" → dark theme
"blue and orange accents" specific palette
"warm lighting" cozy feel`}
language="text"
/>
</div>
</div>
<div className="mt-6">
<TipBox variant="protip">
Templates automatically enhance your prompts. A simple description becomes a detailed
generation instruction.
</TipBox>
</div>
</section>
{/* Light Mode Placeholders */}
<section id="light-mode-placeholders" className="mb-12 scroll-mt-24">
<SectionHeader level={2} id="light-mode-placeholders">
Light Mode Placeholders
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-4">
Generated images work well with light interfaces by default. If you need more control,
specify background colors or accents to match your design system.
</p>
<CodeBlock
code={`"product on white background"
"office with soft natural light"
"portrait, bright studio, pastel tones"
"dashboard mockup, light gray background, blue accent"`}
language="text"
/>
<div className="mt-6">
<LivePreview label="Light Background Example">
<div className="bg-white rounded-xl p-6 max-w-md">
<img
src="https://cdn.banatie.app/sys/demo/live/products?prompt=minimalist+desk+setup+white+background+clean+aesthetic&aspectRatio=16:9"
alt="Light theme placeholder"
className="w-full rounded-lg mb-4"
/>
<p className="text-gray-900 font-semibold">Clean Workspace</p>
<p className="text-gray-600 text-sm">White background, natural lighting</p>
</div>
</LivePreview>
</div>
</section>
{/* Dark Mode Placeholders */}
<section id="dark-mode-placeholders" className="mb-12 scroll-mt-24">
<SectionHeader level={2} id="dark-mode-placeholders">
Dark Mode Placeholders
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-4">
For dark interfaces, add <InlineCode>dark background</InlineCode> or descriptive words
like night, moody, or twilight. You can also specify accent colors.
</p>
<CodeBlock
code={`"office interior, dark background"
"product photo, dark surface, moody lighting"
"night cityscape, neon accents"
"abstract gradient, dark purple and blue"`}
language="text"
/>
<div className="mt-6">
<LivePreview label="Dark Background Example">
<div className="bg-slate-900 rounded-xl p-6 max-w-md border border-slate-700">
<img
src="https://cdn.banatie.app/sys/demo/live/hero?prompt=abstract+gradient+dark+background+purple+blue+moody&aspectRatio=16:9"
alt="Dark theme placeholder"
className="w-full rounded-lg mb-4"
/>
<p className="text-white font-semibold">Dark Gradient</p>
<p className="text-gray-400 text-sm">Dark background with purple accents</p>
</div>
</LivePreview>
</div>
</section>
{/* Placeholder Image Examples */}
<section id="placeholder-image-examples" className="mb-12 scroll-mt-24">
<SectionHeader level={2} id="placeholder-image-examples">
Placeholder Image Examples
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-8">
Copy-paste examples for common placeholder image use cases.
</p>
{/* Avatar */}
<div className="mb-10">
<h3 className="text-xl font-semibold text-white mb-2">Avatar</h3>
<p className="text-gray-400 text-sm mb-4">
<InlineCode>1:1</InlineCode> · User profiles, team sections, testimonials
</p>
<LivePreview>
<div className="bg-slate-800/50 rounded-xl p-6 max-w-md">
<div className="flex items-start gap-4">
<img
src="https://cdn.banatie.app/sys/demo/live/avatars?prompt=professional+studio+headshot+confident+woman+neutral+background&aspectRatio=1:1"
alt="Sarah Chen"
className="w-14 h-14 rounded-full object-cover flex-shrink-0"
/>
<div className="border-l-2 border-purple-500/50 pl-4">
<p className="text-gray-300 italic">
Banatie cut our design mockup time in half. No more hunting for stock photos.
</p>
<p className="text-white font-semibold mt-3">Sarah Chen</p>
<p className="text-gray-500 text-sm">Product Designer at Acme</p>
</div>
</div>
</div>
</LivePreview>
<CodeBlock
code={`<img
src="https://cdn.banatie.app/{org}/{project}/live/avatars?prompt=professional+headshot&aspectRatio=1:1"
alt="User avatar"
class="w-14 h-14 rounded-full object-cover"
/>`}
language="html"
/>
<div className="mt-6">
<p className="text-gray-400 text-sm mb-3">Team grid example:</p>
<LivePreview showLabel={false}>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="flex items-center gap-3">
<img
src="https://cdn.banatie.app/sys/demo/live/avatars?prompt=professional+headshot+young+man+friendly+smile&aspectRatio=1:1"
alt="Alex Rivera"
className="w-12 h-12 rounded-full object-cover"
/>
<div>
<p className="text-white font-medium">Alex Rivera</p>
<p className="text-gray-400 text-sm">Engineering Lead</p>
</div>
</div>
<div className="flex items-center gap-3">
<img
src="https://cdn.banatie.app/sys/demo/live/avatars?prompt=professional+headshot+woman+glasses+confident&aspectRatio=1:1"
alt="Maria Santos"
className="w-12 h-12 rounded-full object-cover"
/>
<div>
<p className="text-white font-medium">Maria Santos</p>
<p className="text-gray-400 text-sm">Design Director</p>
</div>
</div>
<div className="flex items-center gap-3">
<img
src="https://cdn.banatie.app/sys/demo/live/avatars?prompt=professional+headshot+man+beard+casual&aspectRatio=1:1"
alt="James Wilson"
className="w-12 h-12 rounded-full object-cover"
/>
<div>
<p className="text-white font-medium">James Wilson</p>
<p className="text-gray-400 text-sm">Product Manager</p>
</div>
</div>
</div>
</LivePreview>
</div>
</div>
{/* Product */}
<div className="mb-10">
<h3 className="text-xl font-semibold text-white mb-2">Product</h3>
<p className="text-gray-400 text-sm mb-4">
<InlineCode>1:1</InlineCode> or <InlineCode>4:5</InlineCode> · E-commerce, catalogs,
listings
</p>
<LivePreview>
<div className="bg-white rounded-xl p-4 max-w-xs shadow-lg">
<img
src="https://cdn.banatie.app/sys/demo/live/products?prompt=minimalist+wireless+headphones+white+background+product+photo&aspectRatio=1:1"
alt="Wireless Pro Headphones"
className="w-full aspect-square object-cover rounded-lg mb-4"
/>
<p className="text-gray-900 font-semibold">Wireless Pro Headphones</p>
<p className="text-gray-600 text-lg font-bold mt-1">$299</p>
<button className="mt-3 w-full bg-gray-900 text-white py-2 px-4 rounded-lg text-sm font-medium">
Add to Cart
</button>
</div>
</LivePreview>
<CodeBlock
code={`<img
src="https://cdn.banatie.app/{org}/{project}/live/products?prompt=product+photo+white+background&aspectRatio=1:1"
alt="Product"
class="w-full aspect-square object-cover rounded-lg"
/>`}
language="html"
/>
<div className="mt-6">
<p className="text-gray-400 text-sm mb-3">Product grid example:</p>
<LivePreview showLabel={false}>
<div className="grid grid-cols-2 gap-4 max-w-lg">
<div className="bg-white rounded-lg p-3">
<img
src="https://cdn.banatie.app/sys/demo/live/products?prompt=modern+smart+watch+product+photo&aspectRatio=1:1"
alt="Smart Watch X1"
className="w-full aspect-square object-cover rounded-md mb-2"
/>
<p className="text-gray-900 font-medium text-sm">Smart Watch X1</p>
<p className="text-gray-600 font-bold">$199</p>
</div>
<div className="bg-white rounded-lg p-3">
<img
src="https://cdn.banatie.app/sys/demo/live/products?prompt=wireless+earbuds+case+product+photo&aspectRatio=1:1"
alt="Wireless Earbuds"
className="w-full aspect-square object-cover rounded-md mb-2"
/>
<p className="text-gray-900 font-medium text-sm">Wireless Earbuds</p>
<p className="text-gray-600 font-bold">$249</p>
</div>
<div className="bg-white rounded-lg p-3">
<img
src="https://cdn.banatie.app/sys/demo/live/products?prompt=portable+bluetooth+speaker+product+photo&aspectRatio=1:1"
alt="Portable Speaker"
className="w-full aspect-square object-cover rounded-md mb-2"
/>
<p className="text-gray-900 font-medium text-sm">Portable Speaker</p>
<p className="text-gray-600 font-bold">$79</p>
</div>
<div className="bg-white rounded-lg p-3">
<img
src="https://cdn.banatie.app/sys/demo/live/products?prompt=laptop+stand+aluminum+product&aspectRatio=1:1"
alt="Laptop Stand"
className="w-full aspect-square object-cover rounded-md mb-2"
/>
<p className="text-gray-900 font-medium text-sm">Laptop Stand</p>
<p className="text-gray-600 font-bold">$129</p>
</div>
</div>
</LivePreview>
</div>
</div>
{/* Hero */}
<div className="mb-10">
<h3 className="text-xl font-semibold text-white mb-2">Hero</h3>
<p className="text-gray-400 text-sm mb-4">
<InlineCode>16:9</InlineCode> · Landing pages, section backgrounds
</p>
<LivePreview className="p-0">
<div className="relative w-full h-72 rounded-xl overflow-hidden">
<img
src="https://cdn.banatie.app/sys/demo/live/hero?prompt=aerial+view+modern+city+skyline+sunset+dramatic+lighting&aspectRatio=16:9"
alt="Hero background"
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-black/50 flex flex-col items-center justify-center text-center px-4">
<p className="text-3xl font-bold text-white mb-2">Build the Future</p>
<p className="text-gray-200">Start your next project with AI-powered tools</p>
</div>
</div>
</LivePreview>
<CodeBlock
code={`<div class="relative w-full h-96 overflow-hidden">
<img
src="https://cdn.banatie.app/{org}/{project}/live/hero?prompt=abstract+tech+background&aspectRatio=16:9"
alt="Hero background"
class="w-full h-full object-cover"
/>
<div class="absolute inset-0 bg-black/50 flex items-center justify-center">
<h1 class="text-4xl font-bold text-white">Your Headline</h1>
</div>
</div>`}
language="html"
/>
</div>
{/* OG Image */}
<div className="mb-10">
<h3 className="text-xl font-semibold text-white mb-2">OG Image</h3>
<p className="text-gray-400 text-sm mb-4">
<InlineCode>1200:630</InlineCode> · Social sharing, Twitter/LinkedIn cards
</p>
<LivePreview>
<div className="bg-slate-800 rounded-lg overflow-hidden max-w-md shadow-xl">
<div className="bg-slate-700 px-3 py-2 flex items-center gap-2">
<div className="flex gap-1.5">
<span className="w-3 h-3 rounded-full bg-red-500"></span>
<span className="w-3 h-3 rounded-full bg-yellow-500"></span>
<span className="w-3 h-3 rounded-full bg-green-500"></span>
</div>
<span className="text-gray-400 text-xs ml-2">twitter.com</span>
</div>
<div className="p-4">
<img
src="https://cdn.banatie.app/sys/demo/live/og?prompt=modern+tech+abstract+waves+purple+blue&aspectRatio=16:9"
alt="OG Image Preview"
className="w-full rounded-lg"
/>
<p className="text-gray-400 text-xs mt-2">banatie.app</p>
<p className="text-white font-medium mt-1">
AI Placeholder Images Guide | Banatie
</p>
<p className="text-gray-400 text-sm mt-1">
Generate AI placeholder images for development...
</p>
</div>
</div>
</LivePreview>
<CodeBlock
code={`<meta property="og:image" content="https://cdn.banatie.app/{org}/{project}/live/og?prompt=your+description&aspectRatio=1200:630" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />`}
language="html"
/>
<div className="mt-4">
<TipBox variant="compact" type="info">
OG images are cached by social platforms. Change the prompt to regenerate.
</TipBox>
</div>
</div>
{/* Features */}
<div className="mb-10">
<h3 className="text-xl font-semibold text-white mb-2">Features</h3>
<p className="text-gray-400 text-sm mb-4">
<InlineCode>1:1</InlineCode> · Feature grids, benefit sections, icons
</p>
<LivePreview>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="group bg-slate-800/40 border border-slate-700/30 rounded-2xl overflow-hidden text-center transition-all duration-300 hover:border-slate-600/50 hover:shadow-lg hover:shadow-purple-500/10 hover:-translate-y-1">
<img
src="https://cdn.banatie.app/sys/demo/live/features?prompt=lightning+bolt+icon+single+line+art+illustration+minimal+background+hex+1e293b&aspectRatio=1:1"
alt="Lightning Fast"
className="w-full aspect-square object-cover"
/>
<div className="p-4">
<p className="font-semibold text-white">Lightning Fast</p>
<p className="mt-1 text-sm text-gray-400">
Deploy in seconds with our global CDN
</p>
</div>
</div>
<div className="group bg-slate-800/40 border border-slate-700/30 rounded-2xl overflow-hidden text-center transition-all duration-300 hover:border-slate-600/50 hover:shadow-lg hover:shadow-emerald-500/10 hover:-translate-y-1">
<img
src="https://cdn.banatie.app/sys/demo/live/features?prompt=shield+icon+single+line+art+illustration+minimal+background+hex+1e293b&aspectRatio=1:1"
alt="Secure by Default"
className="w-full aspect-square object-cover"
/>
<div className="p-4">
<p className="font-semibold text-white">Secure by Default</p>
<p className="mt-1 text-sm text-gray-400">Enterprise-grade security built in</p>
</div>
</div>
<div className="group bg-slate-800/40 border border-slate-700/30 rounded-2xl overflow-hidden text-center transition-all duration-300 hover:border-slate-600/50 hover:shadow-lg hover:shadow-violet-500/10 hover:-translate-y-1">
<img
src="https://cdn.banatie.app/sys/demo/live/features?prompt=puzzle+piece+icon+single+line+art+illustration+minimal+background+hex+1e293b&aspectRatio=1:1"
alt="Easy Integration"
className="w-full aspect-square object-cover"
/>
<div className="p-4">
<p className="font-semibold text-white">Easy Integration</p>
<p className="mt-1 text-sm text-gray-400">Works with your existing stack</p>
</div>
</div>
</div>
</LivePreview>
<CodeBlock
code={`<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-slate-800/40 border border-slate-700/30 rounded-2xl overflow-hidden text-center
transition-all duration-300 hover:border-slate-600/50 hover:shadow-lg hover:-translate-y-1">
<img
src="https://cdn.banatie.app/{org}/{project}/live/features?prompt=lightning+bolt+icon+single+line+art+illustration+minimal+background+hex+1e293b&aspectRatio=1:1"
alt="Lightning Fast"
class="w-full aspect-square object-cover"
/>
<div class="p-4">
<p class="font-semibold text-white">Lightning Fast</p>
<p class="mt-1 text-sm text-gray-400">Deploy in seconds with our global CDN</p>
</div>
</div>
<!-- Repeat for other features -->
</div>`}
language="html"
/>
</div>
</section>
{/* File-based Workflow */}
<section id="file-based-workflow" className="mb-12 scroll-mt-24">
<SectionHeader level={2} id="file-based-workflow">
File-based Workflow
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Need files in your repo? Here&apos;s how to download generated images.
</p>
<h4 className="text-white font-semibold mb-3">When to Use Files</h4>
<ul className="list-disc list-inside text-gray-300 space-y-2 mb-6">
<li>Next.js/React projects with local image imports</li>
<li>Version-controlled placeholder assets</li>
<li>Offline or CI/CD environments</li>
</ul>
<h4 className="text-white font-semibold mb-3">Generate via API</h4>
<p className="text-gray-300 leading-relaxed mb-4">Request:</p>
<CodeBlock
code={`POST https://api.banatie.app/v1/generations
Content-Type: application/json
X-API-Key: your_api_key
{
"prompt": "modern office interior"
}`}
language="http"
/>
<p className="text-gray-300 leading-relaxed mt-6 mb-4">Response:</p>
<CodeBlock
code={`{
"image": {
"id": "img_abc123",
"cdnUrl": "https://cdn.banatie.app/org/project/images/2025-01/abc123.png"
}
}`}
language="json"
/>
<p className="text-gray-300 leading-relaxed mt-6">
Open <InlineCode>cdnUrl</InlineCode> in your browser, save the image, and add it to your
project&apos;s assets folder.
</p>
<p className="text-gray-300 leading-relaxed mt-4">
For full API reference, see{' '}
<a href="/docs/api/generations/" className="text-purple-400 hover:underline">
Generations API
</a>
.
</p>
</section>
</DocPage>
</>
);
}

View File

@ -0,0 +1,232 @@
import type { Metadata } from 'next';
import { TipBox } from '@/components/docs/shared/TipBox';
import { CodeBlock } from '@/components/docs/shared/CodeBlock';
import { DocPage } from '@/components/docs/layout/DocPage';
import { JsonLd } from '@/components/seo/JsonLd';
import { createDocsMetadata, DOCS_PAGES } from '@/config/docs-seo';
import { createBreadcrumbSchema, createTechArticleSchema } from '@/config/docs-schema';
import {
Hero,
SectionHeader,
InlineCode,
ResponseBlock,
} from '@/components/docs/blocks';
const PAGE = DOCS_PAGES['images'];
export const metadata: Metadata = createDocsMetadata(PAGE);
const breadcrumbSchema = createBreadcrumbSchema([
{ name: 'Home', path: '/' },
{ name: 'Documentation', path: '/docs/' },
{ name: 'Working with Images', path: '/docs/images/' },
]);
const articleSchema = createTechArticleSchema(PAGE);
const tocItems = [
{ id: 'image-urls', text: 'Image URLs', level: 2 },
{ id: 'uploading-images', text: 'Uploading Images', level: 2 },
{ id: 'listing-images', text: 'Listing & Getting Images', level: 2 },
{ id: 'aliases', text: 'Aliases', level: 2 },
{ id: 'deleting-images', text: 'Deleting Images', level: 2 },
{ id: 'next-steps', text: 'Next Steps', level: 2 },
];
export default function ImagesPage() {
return (
<>
<JsonLd data={breadcrumbSchema} />
<JsonLd data={articleSchema} />
<DocPage
breadcrumbItems={[
{ label: 'Documentation', href: '/docs/' },
{ label: 'Working with Images' },
]}
tocItems={tocItems}
nextSteps={{
links: [
{
href: '/docs/api/images/',
title: 'API Reference: Images',
description: 'Full endpoint documentation for images.',
accent: 'primary',
},
{
href: '/docs/generation/',
title: 'Image Generation',
description: 'Use your images as references in generations.',
accent: 'secondary',
},
],
}}
>
<Hero
title="Working with Images"
subtitle="Upload, manage, and organize your images. CDN delivery, aliases, and image management."
/>
<section id="image-urls" className="mb-12">
<SectionHeader level={2} id="image-urls">
Image URLs
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
All images are served via CDN with this URL structure:
</p>
<CodeBlock
code={`https://cdn.banatie.app/{org}/{project}/img/{imageId}`}
language="text"
filename="CDN URL Format"
/>
<p className="text-gray-300 leading-relaxed mt-6">
URLs are permanent, fast, and cached globally. Use them directly in your applications.
</p>
</section>
<section id="uploading-images" className="mb-12">
<SectionHeader level={2} id="uploading-images">
Uploading Images
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Upload your own images for use as brand assets, references, or logos:
</p>
<CodeBlock
code={`curl -X POST https://api.banatie.app/api/v1/images/upload \\
-H "X-API-Key: YOUR_API_KEY" \\
-F "file=@your-image.png" \\
-F "alias=@brand-logo"`}
language="bash"
filename="Upload Image"
/>
<p className="text-gray-300 leading-relaxed mt-6 mb-4">
Response includes the CDN URL and image details:
</p>
<ResponseBlock
status="success"
statusCode={201}
statusLabel="201 Created"
content={`{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"storageUrl": "https://cdn.banatie.app/my-org/my-project/img/550e8400-e29b-41d4-a716-446655440000",
"alias": "@brand-logo",
"source": "uploaded",
"width": 512,
"height": 512,
"mimeType": "image/png",
"fileSize": 24576
}
}`}
/>
</section>
<section id="listing-images" className="mb-12">
<SectionHeader level={2} id="listing-images">
Listing & Getting Images
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
List all images in your project:
</p>
<CodeBlock
code={`curl https://api.banatie.app/api/v1/images \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="List Images"
/>
<p className="text-gray-300 leading-relaxed mt-6 mb-6">
Get a specific image by ID or alias:
</p>
<CodeBlock
code={`# By UUID
curl https://api.banatie.app/api/v1/images/550e8400-e29b-41d4-a716-446655440000 \\
-H "X-API-Key: YOUR_API_KEY"
# By alias
curl https://api.banatie.app/api/v1/images/@brand-logo \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Get Image"
/>
</section>
<section id="aliases" className="mb-12">
<SectionHeader level={2} id="aliases">
Aliases
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Assign memorable names to images. Aliases start with <InlineCode>@</InlineCode> and make it easy to reference images without remembering UUIDs.
</p>
<CodeBlock
code={`curl -X PUT https://api.banatie.app/api/v1/images/550e8400-e29b-41d4-a716-446655440000/alias \\
-H "X-API-Key: YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{"alias": "@hero-background"}'`}
language="bash"
filename="Assign Alias"
/>
<p className="text-gray-300 leading-relaxed mt-6 mb-6">
Access images via CDN using their alias:
</p>
<CodeBlock
code={`https://cdn.banatie.app/my-org/my-project/img/@hero-background`}
language="text"
filename="CDN Alias URL"
/>
<div className="mt-6">
<TipBox variant="compact" type="info">
<strong>Pro Tip:</strong> Use aliases for brand assets like <InlineCode>@logo</InlineCode>, <InlineCode>@brand-colors</InlineCode>. Reference them in generations without remembering UUIDs.
</TipBox>
</div>
<p className="text-gray-300 leading-relaxed mt-6 mb-4">
Remove an alias by setting it to null:
</p>
<CodeBlock
code={`curl -X PUT https://api.banatie.app/api/v1/images/550e8400-e29b-41d4-a716-446655440000/alias \\
-H "X-API-Key: YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{"alias": null}'`}
language="bash"
filename="Remove Alias"
/>
</section>
<section id="deleting-images" className="mb-12">
<SectionHeader level={2} id="deleting-images">
Deleting Images
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Delete an image by ID or alias. This permanently removes the image from storage.
</p>
<CodeBlock
code={`curl -X DELETE https://api.banatie.app/api/v1/images/550e8400-e29b-41d4-a716-446655440000 \\
-H "X-API-Key: YOUR_API_KEY"`}
language="bash"
filename="Delete Image"
/>
<div className="mt-6">
<TipBox variant="compact" type="warning">
Deletion is permanent. The image file and all references are removed.
</TipBox>
</div>
</section>
</DocPage>
</>
);
}

View File

@ -8,9 +8,11 @@ import { ApiKeyProvider } from '@/components/shared/ApiKeyWidget/apikey-context'
import { PageProvider } from '@/contexts/page-context';
const navItems = [
{ label: 'Documentation', href: '/docs' },
{ label: 'Demo', href: '/demo' },
{ label: 'Examples', href: '/docs/examples' },
{ label: 'API', href: '/docs/' },
{ label: 'SDK', href: '#', disabled: true },
{ label: 'MCP', href: '#', disabled: true },
{ label: 'CLI', href: '#', disabled: true },
{ label: 'Lab', href: '#', disabled: true },
];
export default function DocsRootLayout({ children }: { children: React.ReactNode }) {

View File

@ -0,0 +1,335 @@
import type { Metadata } from 'next';
import { TipBox } from '@/components/docs/shared/TipBox';
import { Table } from '@/components/docs/shared/Table';
import { CodeBlock } from '@/components/docs/shared/CodeBlock';
import { DocPage } from '@/components/docs/layout/DocPage';
import { JsonLd } from '@/components/seo/JsonLd';
import { createDocsMetadata, DOCS_PAGES } from '@/config/docs-seo';
import { createBreadcrumbSchema, createTechArticleSchema } from '@/config/docs-schema';
import { Hero, SectionHeader, InlineCode } from '@/components/docs/blocks';
const PAGE = DOCS_PAGES['live-urls'];
export const metadata: Metadata = createDocsMetadata(PAGE);
const breadcrumbSchema = createBreadcrumbSchema([
{ name: 'Home', path: '/' },
{ name: 'Documentation', path: '/docs/' },
{ name: 'Live URLs', path: '/docs/live-urls/' },
]);
const articleSchema = createTechArticleSchema(PAGE);
const tocItems = [
{ id: 'the-concept', text: 'The Concept', level: 2 },
{ id: 'url-format', text: 'URL Format', level: 2 },
{ id: 'try-it', text: 'Try It', level: 2 },
{ id: 'placeholder-images', text: 'Placeholder Images', level: 2 },
{ id: 'caching-behavior', text: 'Caching Behavior', level: 2 },
{ id: 'scopes', text: 'Scopes', level: 2 },
{ id: 'rate-limits', text: 'Rate Limits', level: 2 },
{ id: 'use-cases', text: 'Use Cases', level: 2 },
{ id: 'next-steps', text: 'Next Steps', level: 2 },
];
export default function LiveUrlsPage() {
return (
<>
<JsonLd data={breadcrumbSchema} />
<JsonLd data={articleSchema} />
<DocPage
breadcrumbItems={[{ label: 'Documentation', href: '/docs/' }, { label: 'Live URLs' }]}
tocItems={tocItems}
nextSteps={{
links: [
{
href: '/docs/api/live-scopes/',
title: 'API Reference: Live Scopes',
description: 'Manage scopes and generation limits.',
accent: 'primary',
},
{
href: '/docs/generation/',
title: 'Image Generation',
description: 'Full control via the generations API.',
accent: 'secondary',
},
],
}}
>
<Hero
title="Live URLs"
subtitle="Generate images directly from URL parameters. No API calls needed — just use the URL in your HTML."
/>
<section id="the-concept" className="mb-12">
<SectionHeader level={2} id="the-concept">
The Concept
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-4">
Put your prompt in a URL. Use it directly in an{' '}
<InlineCode>&lt;img src="..."&gt;</InlineCode> tag.
</p>
<p className="text-gray-300 leading-relaxed">
First request generates the image. All subsequent requests serve it from cache
instantly.
</p>
</section>
<section id="url-format" className="mb-12">
<SectionHeader level={2} id="url-format">
URL Format
</SectionHeader>
<CodeBlock
code={`https://cdn.banatie.app/{org}/{project}/live/{scope}?prompt=...&aspectRatio=...`}
language="text"
filename="Live URL Format"
/>
<p className="text-gray-300 leading-relaxed mt-6 mb-4">Query parameters:</p>
<Table
headers={['Parameter', 'Required', 'Description']}
rows={[
[
<InlineCode key="p">prompt</InlineCode>,
<span key="r" className="text-green-400">
Yes
</span>,
'Text description of the image to generate',
],
[
<InlineCode key="p">aspectRatio</InlineCode>,
<span key="r" className="text-gray-500">
No
</span>,
'Image ratio: 1:1, 16:9, 9:16, 3:2 (default: 16:9)',
],
[
<InlineCode key="p">template</InlineCode>,
<span key="r" className="text-gray-500">
No
</span>,
'Enhancement template to use',
],
[
<InlineCode key="p">autoEnhance</InlineCode>,
<span key="r" className="text-gray-500">
No
</span>,
'Enable prompt enhancement (default: true)',
],
]}
/>
</section>
<section id="try-it" className="mb-12">
<SectionHeader level={2} id="try-it">
Try It
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">Open this URL in your browser:</p>
<CodeBlock
code={`https://cdn.banatie.app/my-org/my-project/live/demo?prompt=a+friendly+robot+waving+hello&aspectRatio=16:9`}
language="text"
filename="Example Live URL"
/>
<p className="text-gray-300 leading-relaxed mt-6 mb-4">Or use it directly in HTML:</p>
<CodeBlock
code={`<img
src="https://cdn.banatie.app/my-org/my-project/live/hero?prompt=mountain+landscape+at+sunset&aspectRatio=16:9"
alt="Mountain landscape"
/>`}
language="html"
filename="HTML Usage"
/>
</section>
<section id="caching-behavior" className="mb-12">
<SectionHeader level={2} id="caching-behavior">
Caching Behavior
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
The response includes an <InlineCode>X-Cache-Status</InlineCode> header:
</p>
<Table
headers={['Status', 'Meaning', 'Response Time']}
rows={[
[
<InlineCode key="s" color="success">
HIT
</InlineCode>,
'Image served from cache',
'Instant (milliseconds)',
],
[<InlineCode key="s">MISS</InlineCode>, 'New image generated', 'A few seconds'],
]}
/>
<p className="text-gray-300 leading-relaxed mt-6">
Cache hits are unlimited and don't count toward rate limits. Only new generations (cache
MISS) are rate limited.
</p>
</section>
<section id="scopes" className="mb-12">
<SectionHeader level={2} id="scopes">
Scopes
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Scopes organize your live generations. Each scope has its own generation limit.
</p>
<CodeBlock
code={`# Different scopes for different purposes
https://cdn.banatie.app/my-org/my-project/live/hero-section?prompt=...
https://cdn.banatie.app/my-org/my-project/live/product-gallery?prompt=...
https://cdn.banatie.app/my-org/my-project/live/blog-images?prompt=...`}
language="text"
filename="Scope Examples"
/>
<p className="text-gray-300 leading-relaxed mt-6">
Scopes are auto-created on first use. You can also pre-configure them via the API to set
custom limits.
</p>
</section>
{/* <section id="rate-limits" className="mb-12">
<SectionHeader level={2} id="rate-limits">
Rate Limits
</SectionHeader>
<Table
headers={['Limit Type', 'Value', 'Notes']}
rows={[
[
'Per IP',
<span key="v" className="text-amber-400">
10 new generations/hour
</span>,
'Only applies to cache MISS',
],
[
'Per Scope',
<span key="v" className="text-amber-400">
30 generations
</span>,
'Configurable via API',
],
[
'Cache Hits',
<span key="v" className="text-green-400">
Unlimited
</span>,
'No limits on cached images',
],
]}
/>
<div className="mt-6">
<TipBox variant="compact" type="info">
Rate limits protect the service from abuse. For high-volume needs, use the generations
API directly.
</TipBox>
</div>
</section> */}
<section id="use-cases" className="mb-12">
<SectionHeader level={2} id="use-cases">
Use Cases
</SectionHeader>
<ul className="space-y-4 text-gray-300">
<li className="flex items-start gap-3">
<span className="text-purple-400 mt-1"></span>
<div>
<strong className="text-white">Static HTML & serverless sites</strong>
<p className="text-gray-400 text-sm mt-1">
Deploy HTML pages without configuring asset hosting or CDN infrastructure. Images
are served directly from Banatie's edge network.
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="text-purple-400 mt-1"></span>
<div>
<strong className="text-white">AI-assisted development</strong>
<p className="text-gray-400 text-sm mt-1">
Enable AI coding assistants to generate complete HTML or JSX with contextual
images in a single passno asset pipeline required.
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="text-purple-400 mt-1"></span>
<div>
<strong className="text-white">Rapid prototyping</strong>
<p className="text-gray-400 text-sm mt-1">
Test different visuals without writing backend code.
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="text-purple-400 mt-1"></span>
<div>
<strong className="text-white">AI placeholder images</strong>
<p className="text-gray-400 text-sm mt-1">
Replace gray boxes and random stock photos with contextual AI images. Perfect for
prototypes, client demos, and design mockups.
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="text-purple-400 mt-1"></span>
<div>
<strong className="text-white">Personalized content</strong>
<p className="text-gray-400 text-sm mt-1">
Generate unique images based on user data or preferences for dynamic,
individualized experiences.
</p>
</div>
</li>
</ul>
</section>
<section id="placeholder-images" className="mb-12">
<TipBox variant="protip">
Use Live URLs as intelligent placeholder images during development. Generate contextual
visuals that match your design intentno more gray boxes or random stock photos.
</TipBox>
<p className="text-gray-400 text-sm mt-6 mb-4">Common placeholder configurations:</p>
<CodeBlock
code={`<!-- Avatar placeholder (200×200) -->
<img src="https://cdn.banatie.app/.../live/avatars?prompt=professional+headshot&aspectRatio=1:1" />
<!-- Thumbnail placeholder (300×200) -->
<img src="https://cdn.banatie.app/.../live/thumbs?prompt=product+photo&aspectRatio=3:2" />
<!-- Hero placeholder (1200×630) -->
<img src="https://cdn.banatie.app/.../live/hero?prompt=modern+office+interior&aspectRatio=1200:630" />
<!-- Card image placeholder (400×300) -->
<img src="https://cdn.banatie.app/.../live/cards?prompt=abstract+gradient+background&aspectRatio=4:3" />`}
language="html"
filename="Placeholder Examples"
/>
<div className="mt-6">
<TipBox variant="compact" type="info">
For dark mode interfaces, include "dark theme" or "dark background" in your prompt.
</TipBox>
</div>
</section>
</DocPage>
</>
);
}

View File

@ -0,0 +1,213 @@
import type { Metadata } from 'next';
import { TipBox } from '@/components/docs/shared/TipBox';
import { CodeBlock } from '@/components/docs/shared/CodeBlock';
import { DocPage } from '@/components/docs/layout/DocPage';
import { JsonLd } from '@/components/seo/JsonLd';
import { createDocsMetadata, DOCS_PAGES } from '@/config/docs-seo';
import { createBreadcrumbSchema, createTechArticleSchema, HOW_TO_SCHEMA } from '@/config/docs-schema';
import {
Hero,
SectionHeader,
ResponseBlock,
LinkCard,
LinkCardGrid,
} from '@/components/docs/blocks';
const PAGE = DOCS_PAGES['getting-started'];
export const metadata: Metadata = createDocsMetadata(PAGE);
const breadcrumbSchema = createBreadcrumbSchema([
{ name: 'Home', path: '/' },
{ name: 'Documentation', path: '/docs/' },
]);
const articleSchema = createTechArticleSchema(PAGE);
const tocItems = [
{ id: 'what-is-banatie', text: 'What is Banatie?', level: 2 },
{ id: 'your-first-image', text: 'Your First Image', level: 2 },
{ id: 'production-ready', text: 'Production Ready', level: 2 },
{ id: 'live-urls', text: 'Live URLs', level: 2 },
{ id: 'get-your-api-key', text: 'Get Your API Key', level: 2 },
{ id: 'next-steps', text: 'Next Steps', level: 2 },
];
export default function GettingStartedPage() {
return (
<>
<JsonLd data={breadcrumbSchema} />
<JsonLd data={articleSchema} />
<JsonLd data={HOW_TO_SCHEMA} />
<DocPage
breadcrumbItems={[
{ label: 'Documentation', href: '/docs/' },
{ label: 'Getting Started' },
]}
tocItems={tocItems}
nextSteps={{
links: [
{
href: '/docs/generation/',
title: 'Image Generation',
description: 'Aspect ratios, prompt templates, using references.',
accent: 'primary',
},
{
href: '/docs/images/',
title: 'Working with Images',
description: 'Upload your own, organize with aliases.',
accent: 'secondary',
},
{
href: '/docs/live-urls/',
title: 'Live URLs',
description: 'Generate images directly from URL parameters.',
accent: 'primary',
},
{
href: '/docs/api/',
title: 'API Reference',
description: 'Full endpoint documentation.',
accent: 'secondary',
},
],
}}
>
<Hero
title="Get Started"
subtitle="Generate your first AI image in a few simple steps."
/>
<section id="what-is-banatie" className="mb-12">
<SectionHeader level={2} id="what-is-banatie">
What is Banatie?
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-4">
Banatie is an image generation API for developers. Send a text prompt, get a production-ready image delivered via CDN.
</p>
<p className="text-gray-300 leading-relaxed">
Simple REST API. Optimized AI models that deliver consistent results. Images ready for production use immediately.
</p>
</section>
<section id="your-first-image" className="mb-12">
<SectionHeader level={2} id="your-first-image">
Your First Image
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Once you have your API key, generate an image with a single request:
</p>
<CodeBlock
code={`curl -X POST https://api.banatie.app/api/v1/generations \\
-H "Content-Type: application/json" \\
-H "X-API-Key: YOUR_API_KEY" \\
-d '{"prompt": "a friendly robot waving hello"}'`}
language="bash"
filename="Generate Image"
/>
<p className="text-gray-300 leading-relaxed mt-6 mb-4">
That's it. The response contains your image:
</p>
<ResponseBlock
status="success"
statusCode={200}
statusLabel="200 OK"
content={`{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "success",
"outputImage": {
"storageUrl": "https://cdn.banatie.app/my-org/my-project/img/8a3b2c1d-4e5f-6789-abcd-ef0123456789",
"width": 1792,
"height": 1008
}
}
}`}
/>
<p className="text-gray-300 leading-relaxed mt-6">
Open <code className="text-purple-400">storageUrl</code> in your browser there's your robot.
</p>
</section>
<section id="production-ready" className="mb-12">
<SectionHeader level={2} id="production-ready">
Production Ready
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
The image URL is permanent and served via global CDN. What this means for you:
</p>
<ul className="space-y-3 text-gray-300">
<li className="flex items-start gap-3">
<span className="text-purple-400 mt-1"></span>
<span><strong className="text-white">Fast access</strong> images load in milliseconds</span>
</li>
<li className="flex items-start gap-3">
<span className="text-purple-400 mt-1"></span>
<span><strong className="text-white">Edge cached</strong> served from locations closest to your users</span>
</li>
<li className="flex items-start gap-3">
<span className="text-purple-400 mt-1"></span>
<span><strong className="text-white">Global distribution</strong> works fast everywhere in the world</span>
</li>
</ul>
<p className="text-gray-300 leading-relaxed mt-6">
One request. Production-ready result. Drop the URL into your app and ship.
</p>
</section>
<section id="live-urls" className="mb-12">
<SectionHeader level={2} id="live-urls">
Live URLs
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
Want to skip the API call entirely? Generate images directly from a URL:
</p>
<CodeBlock
code={`https://cdn.banatie.app/my-org/my-project/live/demo?prompt=a+friendly+robot+waving+hello`}
language="text"
filename="Live URL"
/>
<p className="text-gray-300 leading-relaxed mt-6 mb-6">
Put this in an <code className="text-purple-400">&lt;img src="..."&gt;</code> tag. First request generates the image, all subsequent requests serve it from cache instantly.
</p>
<TipBox variant="compact" type="info">
Perfect for placeholders, dynamic content, and rapid prototyping.
</TipBox>
<p className="text-gray-300 leading-relaxed mt-6">
<a href="/docs/live-urls/" className="text-purple-400 hover:underline">Learn more about Live URLs </a>
</p>
</section>
<section id="get-your-api-key" className="mb-12">
<SectionHeader level={2} id="get-your-api-key">
Get Your API Key
</SectionHeader>
<p className="text-gray-300 leading-relaxed mb-6">
We're currently in early access. API keys are issued personally.
</p>
<div className="space-y-3 text-gray-300 mb-6">
<p><strong className="text-white">To request access:</strong></p>
<ol className="list-decimal list-inside space-y-2 pl-4">
<li>Go to <a href="https://banatie.app" className="text-purple-400 hover:underline">banatie.app</a></li>
<li>Enter your email in the signup form</li>
<li>We'll send your API key within 24 hours</li>
</ol>
</div>
</section>
</DocPage>
</>
);
}

View File

@ -0,0 +1,38 @@
import Image from 'next/image';
import { Footer } from '@/components/shared/Footer';
export default function AppsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
{/* Scrollable Header (NOT sticky) */}
<header className="z-10 bg-slate-900/80 backdrop-blur-md border-b border-white/5">
<nav className="max-w-7xl mx-auto px-4 sm:px-6 py-2 sm:py-3 flex justify-between items-center h-12 sm:h-14 md:h-16">
<a href="/" className="h-full flex items-center">
<Image
src="/banatie-logo-horisontal.png"
alt="Banatie Logo"
width={150}
height={40}
priority
className="h-8 sm:h-10 md:h-full w-auto object-contain"
/>
</a>
<a
href="/#get-access"
className="text-xs sm:text-sm text-gray-300 hover:text-white transition-colors"
>
Get Access
</a>
</nav>
</header>
{children}
<Footer />
</>
);
}

View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

View File

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

View File

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

View File

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 169 KiB

View File

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 195 KiB

View File

Before

Width:  |  Height:  |  Size: 377 KiB

After

Width:  |  Height:  |  Size: 377 KiB

View File

@ -39,7 +39,7 @@ export function PromptUrlsSection() {
</div>
<p className="text-gray-500 text-sm mt-4">
Perfect for static sites, prototypes, and AI coding agents that generate HTML.
Perfect for placeholder images, prototypes, static sites, and AI coding agents.
</p>
</div>
</div>

View File

@ -0,0 +1,445 @@
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Refined Technical Blog Article</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script>
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
primary: "#8b5cf6",
"background-light": "#ffffff",
"background-dark": "#0B0F19",
"surface-light": "#f3f4f6",
"surface-dark": "#111827",
"border-light": "#e5e7eb",
"border-dark": "#1f2937",
"text-main-light": "#111827",
"text-main-dark": "#f9fafb",
"text-muted-light": "#6b7280",
"text-muted-dark": "#9ca3af",
// New colors for the dark sidebar components
"card-dark": "#13141f",
"card-border": "#2d2e3e",
},
fontFamily: {
display: ["Inter", "sans-serif"],
body: ["Inter", "sans-serif"],
},
borderRadius: {
DEFAULT: "0.5rem",
},
typography: (theme) => ({
DEFAULT: {
css: {
color: theme('colors.text-main-light'),
a: {
color: theme('colors.primary'),
'&:hover': {
color: '#7c3aed',
},
},
h1: { color: theme('colors.text-main-light') },
h2: { color: theme('colors.text-main-light') },
h3: { color: theme('colors.text-main-light') },
h4: { color: theme('colors.text-main-light') },
strong: { color: theme('colors.text-main-light') },
code: { color: theme('colors.primary') },
blockquote: {
borderLeftColor: theme('colors.primary'),
fontStyle: 'italic',
},
},
},
dark: {
css: {
color: theme('colors.text-main-dark'),
a: {
color: '#a78bfa',
'&:hover': {
color: '#c4b5fd',
},
},
h1: { color: theme('colors.text-main-dark') },
h2: { color: theme('colors.text-main-dark') },
h3: { color: theme('colors.text-main-dark') },
h4: { color: theme('colors.text-main-dark') },
strong: { color: theme('colors.text-main-dark') },
code: { color: theme('colors.primary') },
blockquote: { borderLeftColor: theme('colors.primary') },
},
},
}),
},
},
};
</script>
<style>
body {
font-family: 'Inter', sans-serif;
}
.custom-scrollbar::-webkit-scrollbar {
height: 8px;
width: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #1f2937;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
</style>
</head>
<body class="bg-background-light text-text-main-light antialiased transition-colors duration-200">
<nav class="sticky top-0 z-50 w-full border-b border-white/10 bg-[#0B0F19] text-white">
<div class="container mx-auto flex h-16 items-center justify-between px-4 sm:px-6 lg:px-8">
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 font-bold text-xl tracking-tight">
<div class="h-8 w-8 rounded-lg bg-gradient-to-br from-primary to-pink-500 flex items-center justify-center">
<span class="material-icons text-white text-lg">auto_awesome</span>
</div>
<span>Banatie</span>
</div>
<div class="hidden md:flex ml-10 space-x-6 text-sm font-medium text-gray-300">
<a class="hover:text-white transition-colors" href="#">Product</a>
<a class="hover:text-white transition-colors" href="#">Solutions</a>
<a class="text-white" href="#">Blog</a>
<a class="hover:text-white transition-colors" href="#">Docs</a>
</div>
</div>
<div class="flex items-center gap-4">
<a class="hidden sm:inline-flex items-center justify-center rounded-lg bg-white/10 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-white/20 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-background-dark" href="#">
Log in
</a>
<a class="inline-flex items-center justify-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors hover:bg-violet-600 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-background-dark" href="#">
Get Access
</a>
</div>
</div>
</nav>
<header class="relative overflow-hidden bg-background-dark text-white pt-12 pb-16 lg:pt-20 lg:pb-24">
<div class="absolute inset-0 z-0">
<div class="absolute -top-24 -left-24 w-96 h-96 bg-primary/20 rounded-full blur-3xl"></div>
<div class="absolute top-1/2 right-0 w-[500px] h-[500px] bg-pink-600/10 rounded-full blur-3xl transform translate-x-1/3 -translate-y-1/2"></div>
<div class="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1635070041078-e363dbe005cb?q=80&amp;w=2070&amp;auto=format&amp;fit=crop')] bg-cover bg-center opacity-10 mix-blend-overlay pointer-events-none"></div>
</div>
<div class="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
<div class="lg:grid lg:grid-cols-12 lg:gap-16 items-center">
<div class="lg:col-span-7 mb-12 lg:mb-0">
<nav aria-label="Breadcrumb" class="flex mb-8 text-xs text-gray-400 font-medium">
<ol class="inline-flex items-center space-x-1 md:space-x-2">
<li class="inline-flex items-center">
<a class="hover:text-white transition-colors" href="#">Blog</a>
</li>
<li>
<div class="flex items-center">
<span class="material-icons text-[14px] text-gray-600 mx-1">chevron_right</span>
<a class="hover:text-white transition-colors" href="#">Engineering</a>
</div>
</li>
<li aria-current="page">
<div class="flex items-center">
<span class="material-icons text-[14px] text-gray-600 mx-1">chevron_right</span>
<span class="text-gray-200">Optimizing Image Generation Pipelines</span>
</div>
</li>
</ol>
</nav>
<div class="flex items-center gap-2 mb-8">
<span class="inline-flex items-center rounded-full bg-primary/20 px-3 py-1 text-xs font-medium text-primary ring-1 ring-inset ring-primary/30">
Engineering
</span>
<span class="text-gray-400 text-sm flex items-center gap-1 ml-2">
<span class="material-icons text-[16px]">schedule</span> 8 min read
</span>
</div>
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-extrabold tracking-tight mb-8 leading-tight text-white">
Optimizing Image Generation Pipelines at Scale
</h1>
<p class="text-lg sm:text-xl text-gray-300 mb-10 max-w-2xl leading-relaxed">
Learn how we reduced latency by 40% using edge caching and predictive pre-generation strategies for our high-throughput API endpoints.
</p>
<div class="flex items-center gap-4 border-t border-white/10 pt-8">
<img alt="Author Avatar" class="h-12 w-12 rounded-full ring-2 ring-background-dark object-cover" src="https://lh3.googleusercontent.com/aida-public/AB6AXuB7lcPRCoZjsFsjDPuss98IvtV47CsxB3edKZH1Jy8D7TtC52cTc1lpxd6PZcqHk3lZWGFU5P-8tUB4xVMImKueltROJN-34JuWGPTdU-hEY8Z2r3ooKCANBoeB4QkCv3iZwpjpuwQlz_LJuMRCdiSJwmAfIv839cg90Lw50ekECfdKsH_zdM8g4Ig3oDsHB8sxcdoNbgZXLGdJ5K-P2QhA8FhKI9RBmvtGCLndihNZdRw405BTYJBYoQORCZ0qMfCmggjeD8Nbx2g"/>
<div>
<div class="font-medium text-white text-base">Alex Chen</div>
<div class="text-sm text-gray-400">Senior Infrastructure Engineer • Oct 24, 2023</div>
</div>
</div>
</div>
<div class="lg:col-span-5 relative">
<div class="relative rounded-xl overflow-hidden shadow-2xl ring-1 ring-white/10 bg-black/40 backdrop-blur-sm">
<img alt="Abstract technical graphic showing network nodes" class="w-full h-auto object-cover aspect-[4/3] mix-blend-lighten opacity-90" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBQduWhcJrwC_QkSUkZ4bCXD5uh4Co2BxXWYMoN8DgTfrdRDQhMNRXYyPA-aEIkLj61sxdw64W-HhLZU8RGNh_YZ5AV2mZDgI5LArVucyhwJdotgRIDJ-oZDZYXHpD25WfsQiZVYKyDlDKBja610LlPzPJmWKOII3MbybkXab1D9xr93TEJ-AoDxFc7j2Bc_ylOKyqVfTLshdwDQDJNAVbnA-H6AavvVbnMyBUdMnFEnW-lVXROEE0mxhvwTyBqEjf68BMoqrr8sGo"/>
<div class="absolute bottom-6 left-6 z-20 bg-gray-900/90 backdrop-blur border border-white/10 rounded-lg p-3 shadow-xl max-w-[280px]">
<div class="flex items-center gap-2 mb-2">
<div class="w-2.5 h-2.5 rounded-full bg-red-500"></div>
<div class="w-2.5 h-2.5 rounded-full bg-yellow-500"></div>
<div class="w-2.5 h-2.5 rounded-full bg-green-500"></div>
</div>
<div class="font-mono text-xs text-green-400">
$ latency --check<br/>
&gt; 45ms (optimized)
</div>
</div>
</div>
</div>
</div>
</div>
</header>
<main class="bg-background-light border-t-0 -mt-1 pt-12 lg:pt-16 pb-12">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="lg:grid lg:grid-cols-12 lg:gap-12">
<aside class="hidden lg:block lg:col-span-1">
<div class="sticky top-28 flex flex-col gap-4 items-center">
<button aria-label="Share on Twitter" class="p-2 rounded-full bg-white text-gray-500 hover:text-primary transition-colors border border-gray-200 shadow-sm group">
<svg class="w-5 h-5 fill-current" viewBox="0 0 24 24"><path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"></path></svg>
</button>
<button aria-label="Share on LinkedIn" class="p-2 rounded-full bg-white text-gray-500 hover:text-blue-600 transition-colors border border-gray-200 shadow-sm">
<svg class="w-5 h-5 fill-current" viewBox="0 0 24 24"><path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"></path></svg>
</button>
<button aria-label="Copy Link" class="p-2 rounded-full bg-white text-gray-500 hover:text-gray-900 transition-colors border border-gray-200 shadow-sm">
<span class="material-icons text-[20px]">link</span>
</button>
</div>
</aside>
<div class="lg:col-span-8">
<article class="prose prose-lg prose-slate max-w-none prose-headings:font-bold prose-headings:tracking-tight prose-a:text-primary prose-a:no-underline hover:prose-a:underline prose-img:rounded-xl">
<p class="lead text-xl text-gray-600 mb-8 font-light">
When we first launched Banatie's image generation API, we optimized for quality. But as our user base grew, so did the demand for speed. Here is how we tackled the challenge of delivering AI-generated assets in milliseconds.
</p>
<h2>The Latency Bottleneck</h2>
<p>
Our initial architecture was straightforward: a request hits our API gateway, gets queued, processed by a GPU worker, and the resulting image is uploaded to S3. Simple, but slow.
</p>
<p>
Users integrating our API into real-time applications needed <a href="#">faster response times</a>. We identified two main areas for improvement:
</p>
<ul class="marker:text-primary">
<li><strong>Cold Starts:</strong> Spinning up new GPU instances took 2-3 minutes.</li>
<li><strong>Network Overhead:</strong> Round trips between the inference server and storage added 200ms+.</li>
</ul>
<div class="my-8 rounded-lg border-l-4 border-blue-500 bg-blue-50 p-6 shadow-sm">
<div class="flex items-start gap-4">
<div class="flex-shrink-0 mt-1">
<span class="material-icons text-blue-600">info</span>
</div>
<div>
<h5 class="font-bold text-gray-900 mt-0 mb-2">Pro Tip: Analyze your P99</h5>
<p class="text-sm text-gray-700 m-0 leading-relaxed">
Don't just look at average latency. Your P99 (99th percentile) latency tells you the experience of your users during worst-case scenarios. Optimizing for P99 often yields the most stable system.
</p>
</div>
</div>
</div>
<h2>Implementing Edge Caching</h2>
<p>
To solve the network overhead, we moved our delivery layer to the edge. By utilizing a global CDN, we could serve cached results instantly for repeated prompts.
</p>
<div class="my-8 overflow-hidden rounded-xl border border-gray-200 bg-[#1e1e1e] shadow-xl">
<div class="flex items-center justify-between border-b border-white/5 bg-[#252526] px-4 py-2">
<div class="flex items-center gap-2">
<div class="h-3 w-3 rounded-full bg-[#ff5f56]"></div>
<div class="h-3 w-3 rounded-full bg-[#ffbd2e]"></div>
<div class="h-3 w-3 rounded-full bg-[#27c93f]"></div>
</div>
<span class="ml-4 text-xs font-mono text-gray-400">middleware/cache-control.ts</span>
<div class="flex-grow"></div>
<button class="text-xs text-gray-400 hover:text-white transition-colors">Copy</button>
</div>
<div class="p-6 overflow-x-auto custom-scrollbar">
<pre class="font-mono text-sm leading-relaxed text-[#d4d4d4] m-0 p-0 bg-transparent"><code><span class="text-[#c586c0]">export function</span> <span class="text-[#dcdcaa]">setCacheHeaders</span>(res: Response) {
<span class="text-[#6a9955]">// Cache for 1 hour at the edge, validate stale in background</span>
res.<span class="text-[#dcdcaa]">setHeader</span>(
<span class="text-[#ce9178]'Cache-Control'&lt;/span&gt;, &lt;span class=" s-maxage="3600," stale-while-revalidate="600'&lt;/span" text-[#ce9178]'public,="">
);
<span class="text-[#6a9955]">// Custom tag for purging</span>
res.<span class="text-[#dcdcaa]">setHeader</span>(<span class="text-[#ce9178]'Surrogate-Key'&lt;/span&gt;, &lt;span class=" span="" text-[#ce9178]'gen-api-v1'<="">);
}</span></span></code></pre>
</div>
</div>
<h3>The Results</h3>
<p>
After deploying these changes, we saw a dramatic drop in TTFB (Time To First Byte).
</p>
<blockquote class="my-10 border-l-4 border-primary bg-gray-50 p-6 text-xl italic font-medium leading-relaxed text-gray-800 shadow-sm rounded-r-lg">
"The latency improvements were immediate. Our dashboard loads felt instantaneous compared to the previous version, directly impacting our user retention metrics."
</blockquote>
<figure class="my-10 group">
<div class="overflow-hidden rounded-xl border border-gray-200 shadow-lg transition-all duration-300 hover:shadow-xl">
<img alt="Graph comparing latency before and after optimization" class="w-full h-auto object-cover transform transition-transform duration-500 group-hover:scale-[1.02]" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBEgg1FA9f6Km5tYQk_92Az_mAXuc6G9ps8KamUSB_VMXwrhcFJLCpgJe7doa6ZdFLQkzJhAcT2OB_E69yQLWyKEPm7Oni0f9YV2_XjH5-jgfAMsv95vBD5r-o35be_5UmmD8-lY40hslbOB075pmwCZ56ISj5VKQARpU5s1zi1nBQvsXWK-5QywJOLp0X8VDhYlB-igMlqCGLhZh5AJ4ufr9hamWVmBiCBa__p7S_hKHjpMxbxs0Qhow_bFjM2vb2eAiUtx3wQjGI"/>
</div>
<figcaption class="mt-4 flex items-center justify-center gap-2 text-sm text-gray-500">
<span class="material-icons text-[16px]">insert_chart</span>
<span>Latency reduction over a 24-hour period post-deployment</span>
</figcaption>
</figure>
<h2>Predictive Pre-Generation</h2>
<p>
For our enterprise clients, we introduced predictive generation. By analyzing usage patterns, we can pre-warm the cache with variations of commonly requested assets before the user even asks for them.
</p>
<p>
This is particularly useful for e-commerce clients who update their catalogs at predictable times.
</p>
<div class="mt-12 pt-8 border-t border-gray-200">
<h3 class="text-lg font-semibold mb-4 text-gray-900">Conclusion</h3>
<p class="text-gray-600">
Optimization is never finished. We are currently exploring WebAssembly for client-side resizing to further offload our servers. Stay tuned for Part 2!
</p>
</div>
</article>
</div>
<aside class="hidden lg:block lg:col-span-3">
<div class="sticky top-28 space-y-8">
<div class="rounded-xl bg-gray-50 border border-gray-200 p-5 shadow-sm">
<h4 class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-4 border-b border-gray-200 pb-2">
On This Page
</h4>
<nav class="flex flex-col space-y-3 text-sm">
<a class="text-gray-900 font-medium pl-2 border-l-2 border-primary transition-colors hover:text-primary" href="#">The Latency Bottleneck</a>
<a class="text-gray-500 hover:text-gray-900 pl-2 border-l-2 border-transparent transition-colors" href="#">Implementing Edge Caching</a>
<a class="text-gray-500 hover:text-gray-900 pl-2 border-l-2 border-transparent transition-colors" href="#">The Results</a>
<a class="text-gray-500 hover:text-gray-900 pl-2 border-l-2 border-transparent transition-colors" href="#">Predictive Pre-Generation</a>
</nav>
</div>
<div>
<h4 class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-4">
Related Docs
</h4>
<div class="space-y-3 text-sm">
<a class="flex items-center gap-2 text-gray-600 hover:text-primary transition-colors group" href="#">
<span class="material-icons text-[18px] text-gray-400 group-hover:text-primary">description</span>
API Caching Policy
</a>
<a class="flex items-center gap-2 text-gray-600 hover:text-primary transition-colors group" href="#">
<span class="material-icons text-[18px] text-gray-400 group-hover:text-primary">terminal</span>
CLI Reference
</a>
<a class="flex items-center gap-2 text-gray-600 hover:text-primary transition-colors group" href="#">
<span class="material-icons text-[18px] text-gray-400 group-hover:text-primary">webhook</span>
Webhooks Guide
</a>
</div>
</div>
<div class="rounded-xl border border-gray-700 bg-slate-800 p-6 shadow-xl relative overflow-hidden group">
<div class="absolute inset-0 bg-gradient-to-br from-primary/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"></div>
<div class="relative z-10">
<h4 class="font-bold text-white text-lg mb-2">Build faster with Banatie</h4>
<p class="text-sm text-gray-400 mb-6 leading-relaxed">
Integrate AI image generation into your app in minutes. Start for free.
</p>
<a class="block w-full rounded-lg bg-primary px-4 py-2.5 text-center text-sm font-semibold text-white shadow-lg hover:bg-violet-600 transition-all transform hover:-translate-y-0.5" href="#">
Get API Key
</a>
</div>
</div>
<div>
<h4 class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-4">
Related Articles
</h4>
<div class="space-y-6">
<a class="group block rounded-xl border border-gray-200 overflow-hidden bg-white hover:border-primary/50 transition-colors shadow-sm" href="#">
<div class="aspect-video w-full bg-gray-100 relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-br from-pink-500/10 to-primary/10 group-hover:scale-105 transition-transform duration-500"></div>
<div class="absolute inset-0 flex items-center justify-center text-primary/40">
<span class="material-icons text-4xl">auto_graph</span>
</div>
</div>
<div class="p-4">
<h5 class="text-base font-semibold text-gray-900 group-hover:text-primary transition-colors leading-tight mb-2">
Understanding Diffusion Models
</h5>
<p class="text-xs text-gray-500">
Oct 12 • 5 min read
</p>
</div>
</a>
<a class="group block rounded-xl border border-gray-200 overflow-hidden bg-white hover:border-primary/50 transition-colors shadow-sm" href="#">
<div class="aspect-video w-full bg-gray-100 relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-br from-blue-500/10 to-cyan-500/10 group-hover:scale-105 transition-transform duration-500"></div>
<div class="absolute inset-0 flex items-center justify-center text-blue-500/40">
<span class="material-icons text-4xl">speed</span>
</div>
</div>
<div class="p-4">
<h5 class="text-base font-semibold text-gray-900 group-hover:text-primary transition-colors leading-tight mb-2">
Managing API Quotas effectively
</h5>
<p class="text-xs text-gray-500">
Sep 28 • 4 min read
</p>
</div>
</a>
</div>
</div>
</div>
</aside>
</div>
</div>
</main>
<footer class="bg-background-dark text-gray-400 border-t border-white/10">
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-12 lg:py-16">
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-8">
<div class="col-span-2 lg:col-span-2">
<div class="flex items-center gap-2 font-bold text-white text-xl tracking-tight mb-4">
<div class="h-6 w-6 rounded bg-gradient-to-br from-primary to-pink-500 flex items-center justify-center">
<span class="material-icons text-white text-xs">auto_awesome</span>
</div>
<span>Banatie</span>
</div>
<p class="text-sm leading-6 mb-6 max-w-sm">
Empowering developers to build the next generation of creative applications with production-ready AI infrastructure.
</p>
<div class="flex gap-4">
<a class="hover:text-white transition-colors" href="#"><span class="sr-only">Twitter</span><svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"></path></svg></a>
<a class="hover:text-white transition-colors" href="#"><span class="sr-only">GitHub</span><svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path clip-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" fill-rule="evenodd"></path></svg></a>
</div>
</div>
<div>
<h3 class="text-sm font-semibold text-white mb-4">Product</h3>
<ul class="space-y-3 text-sm">
<li><a class="hover:text-white transition-colors" href="#">Features</a></li>
<li><a class="hover:text-white transition-colors" href="#">Pricing</a></li>
<li><a class="hover:text-white transition-colors" href="#">API Reference</a></li>
<li><a class="hover:text-white transition-colors" href="#">Integrations</a></li>
</ul>
</div>
<div>
<h3 class="text-sm font-semibold text-white mb-4">Resources</h3>
<ul class="space-y-3 text-sm">
<li><a class="hover:text-white transition-colors" href="#">Documentation</a></li>
<li><a class="hover:text-white transition-colors" href="#">Guides</a></li>
<li><a class="text-white font-medium" href="#">Blog</a></li>
<li><a class="hover:text-white transition-colors" href="#">Community</a></li>
</ul>
</div>
<div>
<h3 class="text-sm font-semibold text-white mb-4">Company</h3>
<ul class="space-y-3 text-sm">
<li><a class="hover:text-white transition-colors" href="#">About</a></li>
<li><a class="hover:text-white transition-colors" href="#">Careers</a></li>
<li><a class="hover:text-white transition-colors" href="#">Legal</a></li>
<li><a class="hover:text-white transition-colors" href="#">Contact</a></li>
</ul>
</div>
</div>
<div class="mt-12 pt-8 border-t border-white/10 flex flex-col md:flex-row justify-between items-center gap-4 text-xs">
<p>© 2023 Banatie Inc. All rights reserved.</p>
<div class="flex gap-6">
<a class="hover:text-white" href="#">Privacy Policy</a>
<a class="hover:text-white" href="#">Terms of Service</a>
</div>
</div>
</div>
</footer>
</body></html>

View File

@ -0,0 +1,110 @@
import { notFound } from 'next/navigation';
import type { Metadata } from 'next';
import {
getAllPosts,
getPostBySlug,
getPostsBySlugs,
generatePostMetadata,
} from '../utils';
import {
BlogPostHeader,
BlogTOC,
BlogSidebar,
BlogShareButtons,
} from '../_components';
import type { BlogPost } from '../types';
const generateJsonLd = (post: BlogPost) => ({
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
description: post.description,
image: `https://banatie.app${post.heroImage}`,
datePublished: post.date,
dateModified: post.date,
author: {
'@type': 'Person',
name: post.author.name,
},
publisher: {
'@type': 'Organization',
name: 'Banatie',
logo: {
'@type': 'ImageObject',
url: 'https://banatie.app/banatie-logo.png',
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `https://banatie.app/blog/${post.slug}/`,
},
});
interface PageProps {
params: Promise<{ slug: string }>;
}
export async function generateStaticParams() {
const posts = getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) return {};
return generatePostMetadata(post);
}
export default async function BlogPostPage({ params }: PageProps) {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) {
notFound();
}
const { Content, tocItems } = await import(`../_posts/${slug}`);
const relatedArticles = getPostsBySlugs(post.relatedArticles);
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(generateJsonLd(post)) }}
/>
<main id="main-content">
<BlogPostHeader post={post} />
<div className="bg-white border-t-0 -mt-1 pt-12 lg:pt-16 pb-12">
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="lg:grid lg:grid-cols-12 lg:gap-12">
{/* Share buttons column - hidden on mobile */}
<aside className="hidden lg:block lg:col-span-1">
<BlogShareButtons url={`/blog/${post.slug}`} title={post.title} />
</aside>
{/* Article content */}
<div className="lg:col-span-8">
<article className="max-w-none text-gray-700 leading-relaxed [&>p]:mb-4 [&>ul]:mb-4 [&>ul]:list-disc [&>ul]:pl-6 [&>ul_li]:mb-2 [&>ul]:marker:text-violet-500 [&>ol]:mb-4 [&>ol]:list-decimal [&>ol]:pl-6 [&>a]:text-violet-500 [&>a]:hover:underline [&_strong]:text-gray-900 [&_strong]:font-semibold">
<Content />
</article>
</div>
{/* Sidebar - hidden on mobile */}
<aside className="hidden lg:block lg:col-span-3">
<div className="sticky top-28 space-y-8">
<BlogTOC items={tocItems} />
<BlogSidebar
relatedArticles={relatedArticles}
relatedDocs={post.relatedDocs}
/>
</div>
</aside>
</div>
</div>
</div>
</main>
</>
);
}

View File

@ -0,0 +1,64 @@
import Link from 'next/link';
import Image from 'next/image';
import type { BlogPost } from '../types';
interface BlogArticleCardProps {
post: BlogPost;
}
const categoryColors: Record<string, string> = {
guides: 'bg-violet-500/10',
tutorials: 'bg-blue-500/10',
'use-cases': 'bg-pink-500/10',
news: 'bg-emerald-500/10',
};
const formatShortDate = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
});
};
export const BlogArticleCard = ({ post }: BlogArticleCardProps) => {
const overlayColor = categoryColors[post.category] || 'bg-violet-500/10';
return (
<Link
href={`/blog/${post.slug}`}
className="group flex flex-col bg-[#111827] rounded-xl overflow-hidden border border-white/5 hover:border-violet-500/50 transition-all hover:shadow-lg hover:shadow-violet-500/5 h-full"
>
<div className="aspect-video w-full relative overflow-hidden bg-gray-900">
<div
className={`absolute inset-0 ${overlayColor} group-hover:bg-transparent transition-colors z-10`}
/>
<Image
src={post.heroImage}
alt={post.title}
fill
className="object-cover transition-transform duration-500 group-hover:scale-105 opacity-80 group-hover:opacity-100"
sizes="(max-width: 768px) 100vw, (max-width: 1280px) 50vw, 33vw"
/>
<div className="absolute top-3 left-3 z-20">
<span className="inline-flex items-center rounded-md bg-black/60 backdrop-blur-md px-2.5 py-1 text-xs font-medium text-white ring-1 ring-inset ring-white/10 capitalize">
{post.category}
</span>
</div>
</div>
<div className="p-5 flex flex-col flex-1">
<h3 className="text-lg font-bold text-white mb-3 line-clamp-2 group-hover:text-violet-400 transition-colors leading-snug">
{post.title}
</h3>
<p className="text-sm text-gray-400 mb-4 line-clamp-2">
{post.description}
</p>
<div className="mt-auto flex items-center text-xs text-gray-500 font-medium">
<span>{formatShortDate(post.date)}</span>
<span className="mx-2"></span>
<span>{post.readTime}</span>
</div>
</div>
</Link>
);
};

View File

@ -0,0 +1,8 @@
export const BlogBackground = () => {
return (
<div className="fixed inset-0 z-0 pointer-events-none overflow-hidden">
<div className="absolute -top-[20%] -right-[10%] w-[800px] h-[800px] bg-violet-500/5 rounded-full blur-[120px]" />
<div className="absolute top-[10%] left-0 w-[500px] h-[500px] bg-blue-600/5 rounded-full blur-[100px]" />
</div>
);
};

View File

@ -0,0 +1,50 @@
import Link from 'next/link';
import { ChevronRight } from 'lucide-react';
interface BreadcrumbItem {
label: string;
href?: string;
}
interface BlogBreadcrumbsProps {
items: BreadcrumbItem[];
variant?: 'light' | 'dark';
}
export const BlogBreadcrumbs = ({
items,
variant = 'dark',
}: BlogBreadcrumbsProps) => {
const textColor = variant === 'dark' ? 'text-gray-400' : 'text-gray-600';
const hoverColor =
variant === 'dark' ? 'hover:text-white' : 'hover:text-gray-900';
const activeColor = variant === 'dark' ? 'text-white' : 'text-gray-900';
return (
<nav className="flex items-center gap-2 text-sm" aria-label="Breadcrumb">
{items.map((item, index) => {
const isLast = index === items.length - 1;
return (
<div key={item.label} className="flex items-center gap-2">
{index > 0 && (
<ChevronRight className={`w-4 h-4 ${textColor}`} />
)}
{item.href && !isLast ? (
<Link
href={item.href}
className={`${textColor} ${hoverColor} transition-colors`}
>
{item.label}
</Link>
) : (
<span className={isLast ? activeColor : textColor}>
{item.label}
</span>
)}
</div>
);
})}
</nav>
);
};

View File

@ -0,0 +1,28 @@
import Link from 'next/link';
interface BlogCTAProps {
title: string;
description: string;
buttonText: string;
buttonHref: string;
}
export const BlogCTA = ({
title,
description,
buttonText,
buttonHref,
}: BlogCTAProps) => {
return (
<div className="my-8 p-6 bg-gradient-to-r from-purple-50 to-indigo-50 rounded-lg border border-purple-100">
<h3 className="text-lg font-semibold text-gray-900 mb-2">{title}</h3>
<p className="text-gray-600 mb-4">{description}</p>
<Link
href={buttonHref}
className="inline-block px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
{buttonText}
</Link>
</div>
);
};

View File

@ -0,0 +1,47 @@
import Link from 'next/link';
import Image from 'next/image';
import type { BlogPost } from '../types';
import { formatDate } from '../utils';
interface BlogCardProps {
post: BlogPost;
}
export const BlogCard = ({ post }: BlogCardProps) => {
return (
<Link
href={`/blog/${post.slug}`}
className="block border border-white/10 rounded-lg overflow-hidden hover:border-purple-500/50 transition-colors"
>
<div className="aspect-video relative bg-slate-800">
<Image
src={post.heroImage}
alt={post.title}
fill
className="object-cover"
/>
</div>
<div className="p-4">
<div className="text-xs text-purple-400 mb-2">{post.category}</div>
<h3 className="text-lg font-semibold text-white mb-2">{post.title}</h3>
<p className="text-sm text-gray-400 mb-4 line-clamp-2">
{post.description}
</p>
<div className="flex items-center gap-3 text-xs text-gray-500">
<div className="flex items-center gap-2">
<Image
src={post.author.avatar}
alt={post.author.name}
width={20}
height={20}
className="rounded-full"
/>
<span>{post.author.name}</span>
</div>
<span>{formatDate(post.date)}</span>
<span>{post.readTime}</span>
</div>
</div>
</Link>
);
};

View File

@ -0,0 +1,52 @@
import Link from 'next/link';
interface BlogCategoriesProps {
categories: Record<string, number>;
}
const categoryColors: Record<string, string> = {
guides: 'bg-violet-500',
tutorials: 'bg-blue-500',
'use-cases': 'bg-pink-500',
news: 'bg-emerald-500',
engineering: 'bg-blue-500',
product: 'bg-purple-500',
design: 'bg-green-500',
culture: 'bg-orange-500',
};
export const BlogCategories = ({ categories }: BlogCategoriesProps) => {
const entries = Object.entries(categories);
if (entries.length === 0) {
return null;
}
return (
<div className="rounded-xl bg-[#111827] border border-white/5 p-6">
<h4 className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-4 border-b border-white/5 pb-2">
Categories
</h4>
<nav className="flex flex-col space-y-1">
{entries.map(([category, count]) => {
const dotColor = categoryColors[category] || 'bg-gray-500';
return (
<Link
key={category}
href={`/blog?category=${category}`}
className="flex items-center justify-between px-3 py-2 rounded-lg text-sm text-gray-300 hover:bg-white/5 hover:text-white transition-colors group"
>
<div className="flex items-center gap-2">
<span className={`w-1.5 h-1.5 rounded-full ${dotColor}`} />
<span className="capitalize">{category}</span>
</div>
<span className="text-xs bg-white/5 px-2 py-0.5 rounded text-gray-500 group-hover:text-gray-300 transition-colors">
{count}
</span>
</Link>
);
})}
</nav>
</div>
);
};

View File

@ -0,0 +1,58 @@
'use client';
import { useState } from 'react';
interface BlogCodeBlockProps {
children: string;
language?: string;
filename?: string;
}
export const BlogCodeBlock = ({
children,
language = 'text',
filename,
}: BlogCodeBlockProps) => {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(children);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
};
const displayName = filename || language;
return (
<div className="my-8 overflow-hidden rounded-xl border border-gray-200 bg-[#1e1e1e] shadow-xl">
<div className="flex items-center justify-between border-b border-white/5 bg-[#252526] px-4 py-2">
<div className="flex items-center gap-2">
<div className="h-3 w-3 rounded-full bg-[#ff5f56]" />
<div className="h-3 w-3 rounded-full bg-[#ffbd2e]" />
<div className="h-3 w-3 rounded-full bg-[#27c93f]" />
</div>
{displayName && (
<span className="ml-4 text-xs font-mono text-gray-400">
{displayName}
</span>
)}
<div className="flex-grow" />
<button
onClick={handleCopy}
className="text-xs text-gray-400 hover:text-white transition-colors"
>
{copied ? 'Copied!' : 'Copy'}
</button>
</div>
<div className="p-6 overflow-x-auto blog-scrollbar">
<pre className="font-mono text-sm leading-relaxed text-[#d4d4d4] m-0 p-0 bg-transparent">
<code>{children}</code>
</pre>
</div>
</div>
);
};

View File

@ -0,0 +1,21 @@
interface BlogHeadingProps {
id: string;
level: 2 | 3;
children: React.ReactNode;
}
export const BlogHeading = ({ id, level, children }: BlogHeadingProps) => {
const Tag = `h${level}` as const;
const baseStyles = 'scroll-mt-28 text-gray-900 font-bold tracking-tight';
const levelStyles = {
2: 'text-2xl mt-12 mb-4 first:mt-0',
3: 'text-xl mt-8 mb-3',
};
return (
<Tag id={id} className={`${baseStyles} ${levelStyles[level]}`}>
{children}
</Tag>
);
};

View File

@ -0,0 +1,50 @@
import Image, { StaticImageData } from 'next/image';
import { ImageIcon } from 'lucide-react';
interface BlogImageProps {
src: string | StaticImageData;
alt: string;
caption?: string;
fullWidth?: boolean;
}
export const BlogImage = ({
src,
alt,
caption,
fullWidth = false,
}: BlogImageProps) => {
const isStaticImage = typeof src !== 'string';
return (
<figure className={`my-8 group ${fullWidth ? '-mx-6' : ''}`}>
<div className="overflow-hidden rounded-xl border border-gray-200 shadow-lg transition-all duration-300 hover:shadow-xl">
{isStaticImage ? (
<Image
src={src}
alt={alt}
placeholder="blur"
className="w-full h-auto"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 66vw, 800px"
/>
) : (
<div className="relative aspect-video bg-gray-100">
<Image
src={src}
alt={alt}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 66vw, 800px"
/>
</div>
)}
</div>
{caption && (
<figcaption className="mt-4 flex items-center justify-center gap-2 text-sm text-gray-500">
<ImageIcon className="w-4 h-4" />
<span>{caption}</span>
</figcaption>
)}
</figure>
);
};

View File

@ -0,0 +1,55 @@
import { Info, Lightbulb, AlertTriangle } from 'lucide-react';
type InfoBoxType = 'info' | 'tip' | 'warning';
interface BlogInfoBoxProps {
type?: InfoBoxType;
title: string;
children: React.ReactNode;
}
const typeConfig = {
info: {
icon: Info,
borderColor: 'border-blue-500',
bgColor: 'bg-blue-50',
iconColor: 'text-blue-600',
},
tip: {
icon: Lightbulb,
borderColor: 'border-amber-500',
bgColor: 'bg-amber-50',
iconColor: 'text-amber-600',
},
warning: {
icon: AlertTriangle,
borderColor: 'border-red-500',
bgColor: 'bg-red-50',
iconColor: 'text-red-600',
},
};
export const BlogInfoBox = ({
type = 'info',
title,
children,
}: BlogInfoBoxProps) => {
const config = typeConfig[type];
const Icon = config.icon;
return (
<div
className={`my-8 rounded-lg border-l-4 ${config.borderColor} ${config.bgColor} p-6 shadow-sm`}
>
<div className="flex items-start gap-4">
<div className="flex-shrink-0 mt-0.5">
<Icon className={`w-5 h-5 ${config.iconColor}`} />
</div>
<div>
<h5 className="font-bold text-gray-900 mt-0 mb-2">{title}</h5>
<div className="text-sm text-gray-700 leading-relaxed">{children}</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,11 @@
interface BlogLeadParagraphProps {
children: React.ReactNode;
}
export const BlogLeadParagraph = ({ children }: BlogLeadParagraphProps) => {
return (
<p className="lead text-xl text-gray-600 mb-8 font-light leading-relaxed">
{children}
</p>
);
};

View File

@ -0,0 +1,58 @@
'use client';
import { useState } from 'react';
import { submitEmail } from '@/lib/actions/waitlistActions';
export const BlogNewsletter = () => {
const [email, setEmail] = useState('');
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!email) return;
setStatus('loading');
const result = await submitEmail(email);
if (result.success) {
setStatus('success');
setEmail('');
} else {
setStatus('error');
setTimeout(() => setStatus('idle'), 3000);
}
};
return (
<div className="rounded-xl bg-[#161b28] p-6 text-center border border-white/5 shadow-xl relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-b from-violet-500/5 to-transparent pointer-events-none" />
<div className="relative z-10">
<h4 className="text-base font-bold text-white mb-2">Subscribe to Banatie</h4>
<p className="text-sm text-gray-400 mb-4 leading-relaxed">
Get the latest articles and updates delivered to your inbox.
</p>
<form onSubmit={handleSubmit} className="space-y-3">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com"
disabled={status === 'loading' || status === 'success'}
className="block w-full rounded-lg border border-white/10 bg-[#0B0F19] py-2 px-3 text-white text-sm placeholder:text-gray-600 focus:border-violet-500 focus:ring-1 focus:ring-violet-500 focus:outline-none disabled:opacity-50"
/>
<button
type="submit"
disabled={status === 'loading' || status === 'success'}
className="w-full rounded-lg bg-violet-500 px-3 py-2 text-sm font-semibold text-white shadow-md hover:bg-violet-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-500 transition-all transform hover:-translate-y-0.5 disabled:opacity-50 disabled:hover:translate-y-0"
>
{status === 'loading' && 'Subscribing...'}
{status === 'success' && 'Subscribed!'}
{status === 'error' && 'Try again'}
{status === 'idle' && 'Subscribe'}
</button>
</form>
</div>
</div>
);
};

View File

@ -0,0 +1,50 @@
'use client';
import { useState } from 'react';
type TabType = 'latest' | 'popular';
interface BlogPageHeaderProps {
title?: string;
onTabChange?: (tab: TabType) => void;
}
export const BlogPageHeader = ({
title = 'Latest Articles',
onTabChange,
}: BlogPageHeaderProps) => {
const [activeTab, setActiveTab] = useState<TabType>('latest');
const handleTabClick = (tab: TabType) => {
setActiveTab(tab);
onTabChange?.(tab);
};
return (
<div className="flex items-center justify-between mb-8 pb-6 border-b border-white/5">
<h1 className="text-3xl font-bold text-white tracking-tight">{title}</h1>
<div className="flex items-center gap-2 text-sm text-gray-400 bg-white/5 p-1 rounded-lg border border-white/5">
<button
onClick={() => handleTabClick('latest')}
className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
activeTab === 'latest'
? 'bg-[#111827] text-white shadow-sm'
: 'hover:text-white'
}`}
>
Latest
</button>
<button
onClick={() => handleTabClick('popular')}
className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
activeTab === 'popular'
? 'bg-[#111827] text-white shadow-sm'
: 'hover:text-white'
}`}
>
Popular
</button>
</div>
</div>
);
};

View File

@ -0,0 +1,89 @@
import Image from 'next/image';
import { Clock } from 'lucide-react';
import type { BlogPost } from '../types';
import { formatDate } from '../utils';
import { BlogBreadcrumbs } from './BlogBreadcrumbs';
interface BlogPostHeaderProps {
post: BlogPost;
}
export const BlogPostHeader = ({ post }: BlogPostHeaderProps) => {
return (
<header className="relative overflow-hidden bg-[#0B0F19] text-white pt-12 pb-16 lg:pt-20 lg:pb-24">
{/* Blur blob decorations */}
<div className="absolute inset-0 z-0">
<div className="absolute -top-24 -left-24 w-96 h-96 bg-violet-500/20 rounded-full blur-3xl" />
<div className="absolute top-1/2 right-0 w-[500px] h-[500px] bg-pink-600/10 rounded-full blur-3xl transform translate-x-1/3 -translate-y-1/2" />
</div>
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
<div className="lg:grid lg:grid-cols-12 lg:gap-16 items-center">
{/* Left column - Content */}
<div className="lg:col-span-7 mb-12 lg:mb-0">
<div className="mb-8">
<BlogBreadcrumbs
items={[
{ label: 'Blog', href: '/blog' },
{ label: post.category },
{ label: post.title },
]}
/>
</div>
<div className="flex items-center gap-2 mb-8">
<span className="inline-flex items-center rounded-full bg-violet-500/20 px-3 py-1 text-xs font-medium text-violet-400 ring-1 ring-inset ring-violet-500/30">
{post.category}
</span>
<span className="text-gray-400 text-sm flex items-center gap-1 ml-2">
<Clock className="w-4 h-4" />
{post.readTime}
</span>
</div>
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-extrabold tracking-tight mb-8 leading-tight text-white">
{post.title}
</h1>
<p className="text-lg sm:text-xl text-gray-300 mb-10 max-w-2xl leading-relaxed">
{post.description}
</p>
<div className="flex items-center gap-4 border-t border-white/10 pt-8">
<Image
src={post.author.avatar}
alt={post.author.name}
width={48}
height={48}
className="rounded-full ring-2 ring-[#0B0F19] object-cover"
/>
<div>
<div className="font-medium text-white text-base">
{post.author.name}
</div>
<div className="text-sm text-gray-400">
{formatDate(post.date)}
</div>
</div>
</div>
</div>
{/* Right column - Hero image */}
<div className="lg:col-span-5 relative">
<div className="relative rounded-xl overflow-hidden shadow-2xl ring-1 ring-white/10 bg-black/40 backdrop-blur-sm">
<Image
src={post.heroImage}
alt={post.title}
width={800}
height={600}
className="w-full h-auto object-cover aspect-[4/3]"
priority
fetchPriority="high"
/>
</div>
</div>
</div>
</div>
</header>
);
};

View File

@ -0,0 +1,39 @@
interface BlogPricingProps {
free?: string;
paid?: string;
perImage?: string;
sdk?: string;
}
export const BlogPricing = ({ free, paid, perImage, sdk }: BlogPricingProps) => {
return (
<div className="my-6 rounded-lg border-l-4 border-emerald-500 bg-gray-50 px-5 py-4">
<div className="flex flex-wrap gap-x-8 gap-y-2 text-sm">
{free && (
<div>
<span className="text-gray-500">Free: </span>
<span className="font-semibold text-gray-900">{free}</span>
</div>
)}
{paid && (
<div>
<span className="text-gray-500">Paid: </span>
<span className="font-semibold text-gray-900">{paid}</span>
</div>
)}
{perImage && (
<div>
<span className="text-gray-500">Per image: </span>
<span className="font-bold text-emerald-600 text-base">{perImage}</span>
</div>
)}
{sdk && (
<div>
<span className="text-gray-500">SDK: </span>
<span className="font-medium text-gray-700">{sdk}</span>
</div>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,17 @@
interface BlogQuoteProps {
children: React.ReactNode;
author?: string;
}
export const BlogQuote = ({ children, author }: BlogQuoteProps) => {
return (
<blockquote className="my-10 border-l-4 border-violet-500 bg-gray-50 p-6 text-xl italic font-medium leading-relaxed text-gray-800 shadow-sm rounded-r-lg">
<p className="m-0">{children}</p>
{author && (
<footer className="mt-4 text-sm text-gray-500 font-normal not-italic">
&mdash; {author}
</footer>
)}
</blockquote>
);
};

View File

@ -0,0 +1,16 @@
import { Search } from 'lucide-react';
export const BlogSearchInput = () => {
return (
<div className="relative group">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<Search className="w-4 h-4 text-gray-500 group-focus-within:text-violet-500 transition-colors" />
</div>
<input
type="text"
placeholder="Search articles..."
className="block w-full rounded-xl border border-white/10 bg-[#161b28] py-3 pl-10 pr-4 text-white placeholder:text-gray-500 focus:border-violet-500 focus:ring-1 focus:ring-violet-500 sm:text-sm shadow-sm transition-all focus:outline-none"
/>
</div>
);
};

View File

@ -0,0 +1,20 @@
import { ExternalLink } from 'lucide-react';
interface BlogServiceLinkProps {
href: string;
children: React.ReactNode;
}
export const BlogServiceLink = ({ href, children }: BlogServiceLinkProps) => {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-violet-600 hover:text-violet-700 font-medium mb-4 group"
>
<ExternalLink className="w-4 h-4 transition-transform group-hover:translate-x-0.5" />
<span className="hover:underline">{children}</span>
</a>
);
};

View File

@ -0,0 +1,62 @@
'use client';
import { Link as LinkIcon } from 'lucide-react';
interface BlogShareButtonsProps {
url?: string;
title?: string;
}
const BASE_URL = 'https://banatie.app';
export const BlogShareButtons = ({ url, title }: BlogShareButtonsProps) => {
const shareUrl = url ? `${BASE_URL}${url}` : BASE_URL;
const shareTitle = title || '';
const handleCopyLink = async () => {
try {
await navigator.clipboard.writeText(shareUrl);
} catch (err) {
console.error('Failed to copy link:', err);
}
};
const twitterUrl = `https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(shareTitle)}`;
const linkedinUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`;
return (
<div className="sticky top-38 flex flex-col gap-4 items-center">
<a
href={twitterUrl}
target="_blank"
rel="noopener noreferrer"
aria-label="Share on Twitter"
className="p-3 rounded-full bg-white text-gray-500 hover:text-violet-500 transition-colors border border-gray-200 shadow-sm"
>
<svg className="w-5 h-5 fill-current" viewBox="0 0 24 24">
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z" />
</svg>
</a>
<a
href={linkedinUrl}
target="_blank"
rel="noopener noreferrer"
aria-label="Share on LinkedIn"
className="p-3 rounded-full bg-white text-gray-500 hover:text-blue-600 transition-colors border border-gray-200 shadow-sm"
>
<svg className="w-5 h-5 fill-current" viewBox="0 0 24 24">
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z" />
</svg>
</a>
<button
onClick={handleCopyLink}
aria-label="Copy Link"
className="p-3 rounded-full bg-white text-gray-500 hover:text-gray-900 transition-colors border border-gray-200 shadow-sm"
>
<LinkIcon className="w-5 h-5" />
</button>
</div>
);
};

View File

@ -0,0 +1,112 @@
import Link from 'next/link';
import Image from 'next/image';
import { BookOpen, Code, FileText, Terminal, Webhook } from 'lucide-react';
import type { BlogPost, RelatedDoc } from '../types';
import { formatDate } from '../utils';
interface BlogSidebarProps {
relatedArticles: BlogPost[];
relatedDocs: RelatedDoc[];
}
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
book: BookOpen,
code: Code,
file: FileText,
terminal: Terminal,
webhook: Webhook,
};
export const BlogSidebar = ({
relatedArticles,
relatedDocs,
}: BlogSidebarProps) => {
return (
<div className="space-y-8">
{relatedDocs.length > 0 && (
<div>
<h4 className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-4">
Related Docs
</h4>
<div className="space-y-3 text-sm">
{relatedDocs.map((doc) => {
const Icon = iconMap[doc.icon] || FileText;
return (
<Link
key={doc.href}
href={doc.href}
className="flex items-center gap-2 text-gray-600 hover:text-violet-500 transition-colors group"
>
<Icon className="w-[18px] h-[18px] text-gray-400 group-hover:text-violet-500" />
<span>{doc.title}</span>
</Link>
);
})}
</div>
</div>
)}
<div className="rounded-xl border border-gray-700 bg-slate-800 p-6 shadow-xl relative overflow-hidden group">
<div className="absolute inset-0 bg-gradient-to-br from-violet-500/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" />
<div className="relative z-10">
<h4 className="font-bold text-white text-lg mb-2">
Build faster with Banatie
</h4>
<p className="text-sm text-gray-400 mb-6 leading-relaxed">
Integrate AI image generation into your app in minutes. Start for
free.
</p>
<Link
href="/#get-access"
className="block w-full rounded-lg bg-violet-500 px-4 py-2.5 text-center text-sm font-semibold text-white shadow-lg hover:bg-violet-600 transition-all transform hover:-translate-y-0.5"
>
Get API Key
</Link>
</div>
</div>
{relatedArticles.length > 0 && (
<div>
<h4 className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-4">
Related Articles
</h4>
<div className="space-y-6">
{relatedArticles.map((article) => (
<Link
key={article.slug}
href={`/blog/${article.slug}`}
className="group block rounded-xl border border-gray-200 overflow-hidden bg-white hover:border-violet-500/50 transition-colors shadow-sm"
>
<div className="aspect-video w-full bg-gray-100 relative overflow-hidden">
{article.heroImage ? (
<Image
src={article.heroImage}
alt={article.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-500"
/>
) : (
<>
<div className="absolute inset-0 bg-gradient-to-br from-violet-500/10 to-pink-500/10 group-hover:scale-105 transition-transform duration-500" />
<div className="absolute inset-0 flex items-center justify-center text-violet-500/40">
<FileText className="w-10 h-10" />
</div>
</>
)}
</div>
<div className="p-4">
<h5 className="text-base font-semibold text-gray-900 group-hover:text-violet-500 transition-colors leading-tight mb-2">
{article.title}
</h5>
<p className="text-xs text-gray-500">
{formatDate(article.date)} &middot; {article.readTime}
</p>
</div>
</Link>
))}
</div>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,91 @@
'use client';
import { useEffect, useState } from 'react';
import { ChevronDown } from 'lucide-react';
import type { TocItem } from '../types';
interface BlogTOCProps {
items: TocItem[];
}
export const BlogTOC = ({ items }: BlogTOCProps) => {
const [activeId, setActiveId] = useState<string>('');
const [isExpanded, setIsExpanded] = useState(true);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveId(entry.target.id);
}
});
},
{ rootMargin: '-20% 0px -35% 0px' }
);
items.forEach((item) => {
const element = document.getElementById(item.id);
if (element) observer.observe(element);
});
return () => observer.disconnect();
}, [items]);
const scrollToSection = (id: string) => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
if (items.length === 0) {
return null;
}
return (
<div className="rounded-xl bg-gray-50 border border-gray-200 p-5 shadow-sm">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center justify-between text-xs font-bold text-gray-500 uppercase tracking-wider border-b border-gray-200 pb-2 hover:text-gray-700 transition-colors"
>
<span>On This Page</span>
<ChevronDown
className={`w-4 h-4 transition-transform duration-200 ${
isExpanded ? '' : '-rotate-90'
}`}
/>
</button>
<div
className={`overflow-hidden transition-all duration-300 ${
isExpanded ? 'max-h-[2000px] opacity-100 mt-4' : 'max-h-0 opacity-0 mt-0'
}`}
>
<nav className="flex flex-col space-y-3 text-sm" aria-label="Table of contents">
{items.map((item) => {
const isActive = activeId === item.id;
const isH3 = item.level === 3;
return (
<button
key={item.id}
onClick={() => scrollToSection(item.id)}
className={`
text-left pl-2 border-l-2 transition-colors
${isH3 ? 'ml-3' : ''}
${
isActive
? 'text-gray-900 font-medium border-violet-500'
: 'text-gray-500 border-transparent hover:text-gray-900'
}
`}
>
{item.text}
</button>
);
})}
</nav>
</div>
</div>
);
};

View File

@ -0,0 +1,30 @@
import Link from 'next/link';
interface BlogTagsProps {
tags: string[];
}
export const BlogTags = ({ tags }: BlogTagsProps) => {
if (tags.length === 0) {
return null;
}
return (
<div className="pt-4">
<h4 className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-4">
Popular Tags
</h4>
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<Link
key={tag}
href={`/blog?tag=${tag}`}
className="px-3 py-1 rounded-full bg-[#111827] border border-white/10 text-xs text-gray-400 hover:text-white hover:border-violet-500/50 transition-colors"
>
#{tag}
</Link>
))}
</div>
</div>
);
};

View File

@ -0,0 +1,23 @@
export { BlogCard } from './BlogCard';
export { BlogTOC } from './BlogTOC';
export { BlogPostHeader } from './BlogPostHeader';
export { BlogBreadcrumbs } from './BlogBreadcrumbs';
export { BlogSidebar } from './BlogSidebar';
export { BlogHeading } from './BlogHeading';
export { BlogImage } from './BlogImage';
export { BlogQuote } from './BlogQuote';
export { BlogCTA } from './BlogCTA';
export { BlogCodeBlock } from './BlogCodeBlock';
export { BlogShareButtons } from './BlogShareButtons';
export { BlogInfoBox } from './BlogInfoBox';
export { BlogLeadParagraph } from './BlogLeadParagraph';
export { BlogServiceLink } from './BlogServiceLink';
export { BlogPricing } from './BlogPricing';
export { BlogBackground } from './BlogBackground';
export { BlogArticleCard } from './BlogArticleCard';
export { BlogPageHeader } from './BlogPageHeader';
export { BlogSearchInput } from './BlogSearchInput';
export { BlogCategories } from './BlogCategories';
export { BlogNewsletter } from './BlogNewsletter';
export { BlogTags } from './BlogTags';

View File

@ -0,0 +1,112 @@
import { BlogHeading, BlogQuote, BlogCTA, BlogCodeBlock } from '../_components';
import type { TocItem } from '../types';
export const tocItems: TocItem[] = [
{ id: 'overview', text: 'Overview', level: 2 },
{ id: 'authentication', text: 'Authentication Setup', level: 2 },
{ id: 'error-handling', text: 'Error Handling', level: 2 },
{ id: 'rate-limits', text: 'Rate Limits', level: 3 },
{ id: 'caching', text: 'Caching Strategies', level: 2 },
{ id: 'conclusion', text: 'Conclusion', level: 2 },
];
export const Content = () => (
<>
<BlogHeading id="overview" level={2}>
Overview
</BlogHeading>
<p className="text-gray-700 leading-relaxed mb-4">
Integrating the Banatie API into your application opens up a world of
possibilities for dynamic image generation. This guide covers the best
practices for a smooth integration.
</p>
<p className="text-gray-700 leading-relaxed mb-4">
Whether you are building a web application, mobile app, or backend service,
these tips will help you get the most out of the API.
</p>
<BlogHeading id="authentication" level={2}>
Authentication Setup
</BlogHeading>
<p className="text-gray-700 leading-relaxed mb-4">
All API requests require authentication via an API key. Here is how to
set it up properly:
</p>
<BlogCodeBlock language="javascript">
{`const headers = {
'X-API-Key': process.env.BANATIE_API_KEY,
'Content-Type': 'application/json'
};
const response = await fetch('https://api.banatie.app/text-to-image', {
method: 'POST',
headers,
body: JSON.stringify({ prompt: 'modern office' })
});`}
</BlogCodeBlock>
<BlogQuote>
Never expose your API key in client-side code. Always make API calls
from your server.
</BlogQuote>
<BlogHeading id="error-handling" level={2}>
Error Handling
</BlogHeading>
<p className="text-gray-700 leading-relaxed mb-4">
Proper error handling ensures your application gracefully handles
failures. The API returns standard HTTP status codes.
</p>
<BlogCodeBlock language="javascript">
{`try {
const response = await generateImage(prompt);
if (!response.ok) {
if (response.status === 429) {
// Rate limited - implement backoff
}
throw new Error(\`API error: \${response.status}\`);
}
} catch (error) {
console.error('Image generation failed:', error);
// Show fallback image
}`}
</BlogCodeBlock>
<BlogHeading id="rate-limits" level={3}>
Rate Limits
</BlogHeading>
<p className="text-gray-700 leading-relaxed mb-4">
The API has rate limits to ensure fair usage. Plan your requests
accordingly and implement exponential backoff for retries.
</p>
<BlogHeading id="caching" level={2}>
Caching Strategies
</BlogHeading>
<p className="text-gray-700 leading-relaxed mb-4">
Caching generated images can significantly reduce API calls and improve
response times. Consider caching at multiple levels.
</p>
<p className="text-gray-700 leading-relaxed mb-4">
Use CDN caching for frequently requested images and local caching for
user-specific content.
</p>
<BlogHeading id="conclusion" level={2}>
Conclusion
</BlogHeading>
<p className="text-gray-700 leading-relaxed mb-4">
Following these best practices will help you build a robust integration
with the Banatie API. Start small, test thoroughly, and scale as needed.
</p>
<BlogCTA
title="Ready to integrate?"
description="Get your API key and start building today."
buttonText="Get API Access"
buttonHref="/#get-access"
/>
</>
);

View File

@ -0,0 +1,92 @@
import { BlogHeading, BlogQuote, BlogCTA } from '../_components';
import type { TocItem } from '../types';
export const tocItems: TocItem[] = [
{ id: 'introduction', text: 'Introduction', level: 2 },
{ id: 'traditional-workflow', text: 'Traditional Workflow Problems', level: 2 },
{ id: 'ai-powered-approach', text: 'AI-Powered Approach', level: 2 },
{ id: 'figma-integration', text: 'Figma Integration', level: 3 },
{ id: 'time-savings', text: 'Time Savings', level: 2 },
{ id: 'tips', text: 'Pro Tips', level: 2 },
];
export const Content = () => (
<>
<BlogHeading id="introduction" level={2}>
Introduction
</BlogHeading>
<p className="text-gray-700 leading-relaxed mb-4">
Design workflows have traditionally been bottlenecked by the need for
placeholder content. Finding the right stock photos or waiting for
actual assets can slow down the creative process significantly.
</p>
<p className="text-gray-700 leading-relaxed mb-4">
AI-generated placeholders offer a new paradigm where designers can
instantly visualize their concepts with contextually relevant images.
</p>
<BlogHeading id="traditional-workflow" level={2}>
Traditional Workflow Problems
</BlogHeading>
<p className="text-gray-700 leading-relaxed mb-4">
The conventional design process often involves searching through stock
photo libraries, downloading images, and then realizing they do not quite
fit the vision. This cycle repeats multiple times per project.
</p>
<BlogQuote author="Design Lead at a Fortune 500">
We used to spend hours just finding the right placeholder images.
Now we describe what we need and get it instantly.
</BlogQuote>
<BlogHeading id="ai-powered-approach" level={2}>
AI-Powered Approach
</BlogHeading>
<p className="text-gray-700 leading-relaxed mb-4">
With AI-generated images, you simply describe what you need. Want a
hero image showing a team collaboration scene? Just ask for it. Need
product mockups with specific aesthetics? Describe the style.
</p>
<p className="text-gray-700 leading-relaxed mb-4">
This approach keeps you in the creative flow instead of breaking
concentration to hunt for assets.
</p>
<BlogHeading id="figma-integration" level={3}>
Figma Integration
</BlogHeading>
<p className="text-gray-700 leading-relaxed mb-4">
Many designers are using Banatie URLs directly in Figma prototypes.
This means your mockups automatically show relevant images without
manual image placement.
</p>
<BlogHeading id="time-savings" level={2}>
Time Savings
</BlogHeading>
<p className="text-gray-700 leading-relaxed mb-4">
Teams report saving 2-4 hours per project on asset hunting alone.
That time can now be spent on actual design work and iteration.
</p>
<p className="text-gray-700 leading-relaxed mb-4">
The quick iteration cycle also means stakeholder feedback sessions
are more productive since designs look more complete.
</p>
<BlogHeading id="tips" level={2}>
Pro Tips
</BlogHeading>
<p className="text-gray-700 leading-relaxed mb-4">
Start with broad prompts and refine as needed. Use consistent style
descriptors across your project for visual coherence. Create a prompt
library for commonly used image types in your brand.
</p>
<BlogCTA
title="Transform your workflow"
description="Join designers who are already saving hours on every project."
buttonText="Start Free Trial"
buttonHref="/#get-access"
/>
</>
);

View File

@ -0,0 +1,97 @@
import { BlogHeading, BlogQuote, BlogCTA } from '../_components';
import type { TocItem } from '../types';
export const tocItems: TocItem[] = [
{ id: 'introduction', text: 'Introduction', level: 2 },
{ id: 'quality-settings', text: 'Quality Settings', level: 2 },
{ id: 'resolution', text: 'Resolution Guidelines', level: 3 },
{ id: 'consistency', text: 'Maintaining Consistency', level: 2 },
{ id: 'performance', text: 'Performance Optimization', level: 2 },
{ id: 'dos-and-donts', text: 'Dos and Donts', level: 2 },
];
export const Content = () => (
<>
<BlogHeading id="introduction" level={2}>
Introduction
</BlogHeading>
<p className="text-gray-700 leading-relaxed mb-4">
Generating high-quality AI images consistently requires understanding
both the capabilities and limitations of the technology. This guide
shares proven practices from thousands of successful generations.
</p>
<p className="text-gray-700 leading-relaxed mb-4">
Following these guidelines will help you achieve better results with
fewer iterations.
</p>
<BlogHeading id="quality-settings" level={2}>
Quality Settings
</BlogHeading>
<p className="text-gray-700 leading-relaxed mb-4">
The API offers different quality tiers. Higher quality settings produce
better images but take longer to generate. Choose based on your use case.
</p>
<p className="text-gray-700 leading-relaxed mb-4">
For prototyping and development, standard quality is often sufficient.
Reserve high quality for production assets.
</p>
<BlogHeading id="resolution" level={3}>
Resolution Guidelines
</BlogHeading>
<p className="text-gray-700 leading-relaxed mb-4">
Match the resolution to your actual display size. Generating 4K images
for thumbnail displays wastes resources and slows performance.
</p>
<BlogQuote>
Start with the exact dimensions you need. You can always regenerate
at higher resolution for final production.
</BlogQuote>
<BlogHeading id="consistency" level={2}>
Maintaining Consistency
</BlogHeading>
<p className="text-gray-700 leading-relaxed mb-4">
For projects requiring visual consistency across multiple images, use
consistent style descriptors and seed values. Document your successful
prompts for reuse.
</p>
<p className="text-gray-700 leading-relaxed mb-4">
Creating a style guide for your project prompts helps team members
generate images that feel cohesive.
</p>
<BlogHeading id="performance" level={2}>
Performance Optimization
</BlogHeading>
<p className="text-gray-700 leading-relaxed mb-4">
Cache generated images when possible. Use lazy loading for below-the-fold
images. Consider generating images ahead of time for predictable content.
</p>
<p className="text-gray-700 leading-relaxed mb-4">
Batch similar requests together when generating multiple images to
optimize API usage.
</p>
<BlogHeading id="dos-and-donts" level={2}>
Dos and Donts
</BlogHeading>
<p className="text-gray-700 leading-relaxed mb-4">
<strong>Do:</strong> Test prompts iteratively, use specific descriptors,
cache results, match resolution to need.
</p>
<p className="text-gray-700 leading-relaxed mb-4">
<strong>Do not:</strong> Use vague prompts, generate unnecessarily high
resolutions, skip error handling, expose API keys client-side.
</p>
<BlogCTA
title="Put these practices to work"
description="Start generating professional-quality images today."
buttonText="Get Started"
buttonHref="/#get-access"
/>
</>
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 KiB

Some files were not shown because too many files have changed in this diff Show More