From 641ee5c3b2e201e865cc830ce7a7ab54a2afed10 Mon Sep 17 00:00:00 2001 From: sachigoyal Date: Sat, 1 Nov 2025 21:31:13 +0600 Subject: [PATCH 1/4] feat: using vercel blob as the intermediate layer --- templates/next-image/.env.example | 3 + templates/next-image/.env.local | 3 +- templates/next-image/.gitignore | 4 + templates/next-image/package.json | 1 + .../src/app/api/blob/upload-handler/route.ts | 54 ++++++++++++ .../src/app/api/edit-image/google.ts | 10 ++- .../src/app/api/edit-image/openai.ts | 6 +- .../src/app/api/edit-image/route.ts | 9 -- .../src/app/api/generate-image/route.ts | 9 -- templates/next-image/src/app/page.tsx | 1 - .../components/ai-elements/prompt-input.tsx | 2 +- .../src/components/image-generator.tsx | 55 +++++------- templates/next-image/src/lib/blob-utils.ts | 84 ++++++++++++++++++ .../next-image/src/lib/server-blob-utils.ts | 87 +++++++++++++++++++ templates/next-image/src/lib/types.ts | 2 +- 15 files changed, 273 insertions(+), 57 deletions(-) create mode 100644 templates/next-image/.env.example create mode 100644 templates/next-image/src/app/api/blob/upload-handler/route.ts create mode 100644 templates/next-image/src/lib/blob-utils.ts create mode 100644 templates/next-image/src/lib/server-blob-utils.ts diff --git a/templates/next-image/.env.example b/templates/next-image/.env.example new file mode 100644 index 000000000..3c44a5fad --- /dev/null +++ b/templates/next-image/.env.example @@ -0,0 +1,3 @@ +NEXT_PUBLIC_ECHO_APP_ID="74d9c979-e036-4e43-904f-32d214b361fc" +ECHO_APP_ID="74d9c979-e036-4e43-904f-32d214b361fc" +BLOB_READ_WRITE_TOKEN= \ No newline at end of file diff --git a/templates/next-image/.env.local b/templates/next-image/.env.local index 054d0b66d..3c44a5fad 100644 --- a/templates/next-image/.env.local +++ b/templates/next-image/.env.local @@ -1,2 +1,3 @@ NEXT_PUBLIC_ECHO_APP_ID="74d9c979-e036-4e43-904f-32d214b361fc" -ECHO_APP_ID="74d9c979-e036-4e43-904f-32d214b361fc" \ No newline at end of file +ECHO_APP_ID="74d9c979-e036-4e43-904f-32d214b361fc" +BLOB_READ_WRITE_TOKEN= \ No newline at end of file diff --git a/templates/next-image/.gitignore b/templates/next-image/.gitignore index 5340dd9a1..0e6ea09ac 100644 --- a/templates/next-image/.gitignore +++ b/templates/next-image/.gitignore @@ -33,6 +33,10 @@ yarn-error.log* # vercel .vercel +# env +.env* +!.env.example + # typescript *.tsbuildinfo next-env.d.ts diff --git a/templates/next-image/package.json b/templates/next-image/package.json index 144db1268..41aed4afb 100644 --- a/templates/next-image/package.json +++ b/templates/next-image/package.json @@ -18,6 +18,7 @@ "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", + "@vercel/blob": "^0.25.1", "ai": "5.0.47", "autonumeric": "^4.10.9", "class-variance-authority": "^0.7.1", diff --git a/templates/next-image/src/app/api/blob/upload-handler/route.ts b/templates/next-image/src/app/api/blob/upload-handler/route.ts new file mode 100644 index 000000000..085595071 --- /dev/null +++ b/templates/next-image/src/app/api/blob/upload-handler/route.ts @@ -0,0 +1,54 @@ +/** + * API Route: Blob Upload Handler + * + * This route handles client-side blob upload token generation. + * It's called by @vercel/blob/client's upload() function to generate + * secure tokens that allow direct client-to-blob uploads. + * + * SECURITY: + * - Validates file types (images only) + * - Sets maximum file size (50MB) + * - Generates temporary tokens + * - Blobs are auto-deleted after server processing + */ + +import { handleUpload, type HandleUploadBody } from '@vercel/blob/client'; +import { NextResponse } from 'next/server'; + +export async function POST(request: Request): Promise { + const body = (await request.json()) as HandleUploadBody; + + try { + const jsonResponse = await handleUpload({ + body, + request, + onBeforeGenerateToken: async (pathname, clientPayload, multipart) => { + // Here you can add authentication/authorization logic + // For example: verify user session, check quotas, etc. + + return { + allowedContentTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], + maximumSizeInBytes: 50 * 1024 * 1024, // 50MB + addRandomSuffix: true, // Prevent filename collisions and add obscurity + }; + }, + onUploadCompleted: async ({ blob, tokenPayload }) => { + // Called after successful upload + // You can add logging, analytics, etc. here + console.log('Blob upload completed:', blob.pathname); + + // Note: The blob will be automatically deleted after the server + // downloads it for processing (see server-blob-utils.ts) + }, + }); + + return NextResponse.json(jsonResponse); + } catch (error) { + console.error('Upload handler error:', error); + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Upload failed' }, + { status: 400 } + ); + } +} + diff --git a/templates/next-image/src/app/api/edit-image/google.ts b/templates/next-image/src/app/api/edit-image/google.ts index 527c4879d..d3b9b6a8d 100644 --- a/templates/next-image/src/app/api/edit-image/google.ts +++ b/templates/next-image/src/app/api/edit-image/google.ts @@ -5,25 +5,29 @@ import { google } from '@/echo'; import { generateText } from 'ai'; import { getMediaTypeFromDataUrl } from '@/lib/image-utils'; +import { processImageUrls } from '@/lib/server-blob-utils'; import { ERROR_MESSAGES } from '@/lib/constants'; /** * Handles Google Gemini image editing + * Downloads images from Vercel Blob URLs and converts to data URLs */ export async function handleGoogleEdit( prompt: string, imageUrls: string[] ): Promise { try { + const dataUrls = await processImageUrls(imageUrls); + const content = [ { type: 'text' as const, text: prompt, }, - ...imageUrls.map(imageUrl => ({ + ...dataUrls.map(dataUrl => ({ type: 'image' as const, - image: imageUrl, // Direct data URL - Gemini handles it - mediaType: getMediaTypeFromDataUrl(imageUrl), + image: dataUrl, + mediaType: getMediaTypeFromDataUrl(dataUrl), })), ]; diff --git a/templates/next-image/src/app/api/edit-image/openai.ts b/templates/next-image/src/app/api/edit-image/openai.ts index 6fbee624d..d5bae8d26 100644 --- a/templates/next-image/src/app/api/edit-image/openai.ts +++ b/templates/next-image/src/app/api/edit-image/openai.ts @@ -5,10 +5,12 @@ import { getEchoToken } from '@/echo'; import OpenAI from 'openai'; import { dataUrlToFile } from '@/lib/image-utils'; +import { processImageUrls } from '@/lib/server-blob-utils'; import { ERROR_MESSAGES } from '@/lib/constants'; /** * Handles OpenAI image editing + * Downloads images from Vercel Blob URLs and converts to data URLs */ export async function handleOpenAIEdit( prompt: string, @@ -32,7 +34,9 @@ export async function handleOpenAIEdit( }); try { - const imageFiles = imageUrls.map(url => dataUrlToFile(url, 'image.png')); + const dataUrls = await processImageUrls(imageUrls); + + const imageFiles = dataUrls.map(url => dataUrlToFile(url, 'image.png')); const result = await openaiClient.images.edit({ image: imageFiles, diff --git a/templates/next-image/src/app/api/edit-image/route.ts b/templates/next-image/src/app/api/edit-image/route.ts index 11c52b38e..c268282c4 100644 --- a/templates/next-image/src/app/api/edit-image/route.ts +++ b/templates/next-image/src/app/api/edit-image/route.ts @@ -3,7 +3,6 @@ * * This route demonstrates Echo SDK integration with AI image editing: * - Uses both Google Gemini and OpenAI for image editing - * - Supports both data URLs (base64) and regular URLs * - Validates input images and prompts * - Returns edited images in appropriate format */ @@ -17,14 +16,6 @@ const providers = { gemini: handleGoogleEdit, }; -export const config = { - api: { - bodyParser: { - sizeLimit: '4mb', - }, - }, -}; - export async function POST(req: Request) { try { const body = await req.json(); diff --git a/templates/next-image/src/app/api/generate-image/route.ts b/templates/next-image/src/app/api/generate-image/route.ts index 15bf30c3a..fbd600b90 100644 --- a/templates/next-image/src/app/api/generate-image/route.ts +++ b/templates/next-image/src/app/api/generate-image/route.ts @@ -18,15 +18,6 @@ const providers = { openai: handleOpenAIGenerate, gemini: handleGoogleGenerate, }; - -export const config = { - api: { - bodyParser: { - sizeLimit: '4mb', - }, - }, -}; - export async function POST(req: Request) { try { const body = await req.json(); diff --git a/templates/next-image/src/app/page.tsx b/templates/next-image/src/app/page.tsx index c8a822b13..e17b65c4f 100644 --- a/templates/next-image/src/app/page.tsx +++ b/templates/next-image/src/app/page.tsx @@ -21,7 +21,6 @@ import { isSignedIn } from '@/echo'; import ImageGenerator from '@/components/image-generator'; -import { EchoWidget } from '@/components/echo-tokens'; import { EchoSignIn } from '@merit-systems/echo-next-sdk/client'; import { EchoAccount } from '@/components/echo-account-next'; diff --git a/templates/next-image/src/components/ai-elements/prompt-input.tsx b/templates/next-image/src/components/ai-elements/prompt-input.tsx index 600b77bca..25fb1ef6a 100644 --- a/templates/next-image/src/components/ai-elements/prompt-input.tsx +++ b/templates/next-image/src/components/ai-elements/prompt-input.tsx @@ -688,7 +688,7 @@ export const PromptInputModelSelectTrigger = ({ { @@ -162,30 +163,33 @@ export default function ImageGenerator() { // Generate unique ID for this request const imageId = `img_${Date.now()}`; - // Convert attachment blob URLs to permanent data URLs for persistent display - const attachmentDataUrls = + // Convert blob URLs to File objects ONCE (for both upload and history display) + const imageFiles = message.files && message.files.length > 0 ? await Promise.all( message.files - .filter(f => f.mediaType?.startsWith('image/')) - .map(async f => { + .filter(f => f.mediaType?.startsWith('image/') || f.type === 'file') + .map(async (f, index) => { try { const response = await fetch(f.url); const blob = await response.blob(); - return await fileToDataUrl( - new File([blob], f.filename || 'image', { - type: f.mediaType, - }) + return new File( + [blob], + f.filename || `image-${index}.png`, + { type: f.mediaType } ); } catch (error) { - console.error( - 'Failed to convert attachment to data URL:', - error - ); - return f.url; // fallback + console.error('Failed to convert attachment:', error); + throw error; } }) ) + : []; + + // Convert to data URLs for history display (only if we have files) + const attachmentDataUrls = + imageFiles.length > 0 + ? await Promise.all(imageFiles.map(file => fileToDataUrl(file))) : undefined; // Create placeholder entry immediately for optimistic UI @@ -206,31 +210,20 @@ export default function ImageGenerator() { let imageUrl: ImageResponse['imageUrl']; if (isEdit) { - const imageFiles = - message.files?.filter( - file => - file.mediaType?.startsWith('image/') || file.type === 'file' - ) || []; - if (imageFiles.length === 0) { throw new Error('No image files found in attachments'); } try { - const imageUrls = await Promise.all( - imageFiles.map(async imageFile => { - // Convert blob URL to data URL for API - const response = await fetch(imageFile.url); - const blob = await response.blob(); - return await fileToDataUrl( - new File([blob], 'image', { type: imageFile.mediaType }) - ); - }) - ); + // Upload files to Vercel Blob storage (files already converted above) + const blobResults = await uploadFilesToBlob(imageFiles); + + // Extract blob URLs for API + const blobUrls = blobResults.map(result => result.url); const result = await editImage({ prompt, - imageUrls, + imageUrls: blobUrls, provider: model, }); imageUrl = result.imageUrl; diff --git a/templates/next-image/src/lib/blob-utils.ts b/templates/next-image/src/lib/blob-utils.ts new file mode 100644 index 000000000..b2eb7efd3 --- /dev/null +++ b/templates/next-image/src/lib/blob-utils.ts @@ -0,0 +1,84 @@ +/** + * Client-side Vercel Blob utilities + * + * This module handles uploading images DIRECTLY to Vercel Blob storage from the client. + * This bypasses the 4.5MB server request limit entirely! + * + * Images are uploaded directly to Vercel Blob, then deleted after server processing. + */ + +import { upload } from '@vercel/blob/client'; + +export interface BlobUploadResult { + url: string; + pathname: string; + contentType?: string; + contentDisposition: string; + downloadUrl: string; +} + +export async function uploadFileToBlob(file: File): Promise { + try { + const blob = await upload(file.name, file, { + access: 'public', + handleUploadUrl: '/api/blob/upload-handler', + }); + + return blob; + } catch (error) { + console.error('Blob upload error:', error); + throw new Error( + `Failed to upload file to blob storage: ${error instanceof Error ? error.message : 'Unknown error'}` + ); + } +} + +export async function uploadFilesToBlob( + files: File[] +): Promise { + if (files.length === 0) { + return []; + } + + const results = await Promise.allSettled( + files.map(file => uploadFileToBlob(file)) + ); + + const successful: BlobUploadResult[] = []; + const failed: { file: File; error: Error }[] = []; + + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + successful.push(result.value); + } else { + failed.push({ + file: files[index], + error: result.reason instanceof Error + ? result.reason + : new Error('Unknown upload error'), + }); + } + }); + + if (failed.length > 0) { + console.warn(`${failed.length} of ${files.length} uploads failed:`, failed); + + if (successful.length === 0) { + throw new Error( + `All ${files.length} file uploads failed. First error: ${failed[0].error.message}` + ); + } + } + + return successful; +} + +export async function blobUrlToFile( + blobUrl: string, + filename: string +): Promise { + const response = await fetch(blobUrl); + const blob = await response.blob(); + return new File([blob], filename, { type: blob.type }); +} + diff --git a/templates/next-image/src/lib/server-blob-utils.ts b/templates/next-image/src/lib/server-blob-utils.ts new file mode 100644 index 000000000..82f5d89fb --- /dev/null +++ b/templates/next-image/src/lib/server-blob-utils.ts @@ -0,0 +1,87 @@ +/** + * Server-side Vercel Blob utilities + * + * This module handles downloading images from Vercel Blob storage on the server + * and converting them to base64 for AI model consumption. + * + * IMPORTANT: Blobs are deleted after download to ensure user privacy. + */ + +import { del } from '@vercel/blob'; + +export function isBlobUrl(url: string): boolean { + try { + const urlObj = new URL(url); + return urlObj.hostname.includes('blob.vercel-storage.com'); + } catch { + return false; + } +} + +export async function downloadBlobToDataUrl( + blobUrl: string, + deleteAfter: boolean = true +): Promise { + try { + const response = await fetch(blobUrl); + + if (!response.ok) { + throw new Error(`Failed to download blob: ${response.statusText}`); + } + + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + const base64 = buffer.toString('base64'); + + // Get content type from response headers + const contentType = response.headers.get('content-type') || 'image/png'; + + // Delete the blob immediately after download for privacy + if (deleteAfter && isBlobUrl(blobUrl)) { + try { + await del(blobUrl); + console.log('Deleted blob after download:', blobUrl); + } catch (deleteError) { + console.error('Failed to delete blob:', deleteError); + // Don't throw - the image was downloaded successfully + } + } + + return `data:${contentType};base64,${base64}`; + } catch (error) { + console.error('Error downloading blob:', error); + throw new Error( + `Failed to download image from blob storage: ${error instanceof Error ? error.message : 'Unknown error'}` + ); + } +} + +export async function downloadBlobsToDataUrls( + blobUrls: string[], + deleteAfter: boolean = true +): Promise { + return Promise.all(blobUrls.map(url => downloadBlobToDataUrl(url, deleteAfter))); +} + +export async function processImageUrls( + imageUrls: string[], + deleteAfter: boolean = true +): Promise { + return Promise.all( + imageUrls.map(async url => { + // If it's already a data URL, return as-is + if (url.startsWith('data:')) { + return url; + } + + // If it's a blob URL, download, convert, and delete + if (isBlobUrl(url)) { + return downloadBlobToDataUrl(url, deleteAfter); + } + + // For any other URL type, try to download it (but don't delete) + return downloadBlobToDataUrl(url, false); + }) + ); +} + diff --git a/templates/next-image/src/lib/types.ts b/templates/next-image/src/lib/types.ts index aa6e13a39..a8b79761b 100644 --- a/templates/next-image/src/lib/types.ts +++ b/templates/next-image/src/lib/types.ts @@ -53,7 +53,7 @@ export interface GenerateImageRequest { */ export interface EditImageRequest { prompt: string; - imageUrls: string[]; // Array of data URLs or regular URLs + imageUrls: string[]; // Array of Vercel Blob URLs, data URLs, or regular URLs provider: ModelOption; } From f489b2e8fc30b0377b26b8ab34cbee8ecdce5712 Mon Sep 17 00:00:00 2001 From: sachigoyal Date: Sat, 1 Nov 2025 21:33:16 +0600 Subject: [PATCH 2/4] . --- templates/next-image/.env.local | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 templates/next-image/.env.local diff --git a/templates/next-image/.env.local b/templates/next-image/.env.local deleted file mode 100644 index 3c44a5fad..000000000 --- a/templates/next-image/.env.local +++ /dev/null @@ -1,3 +0,0 @@ -NEXT_PUBLIC_ECHO_APP_ID="74d9c979-e036-4e43-904f-32d214b361fc" -ECHO_APP_ID="74d9c979-e036-4e43-904f-32d214b361fc" -BLOB_READ_WRITE_TOKEN= \ No newline at end of file From ccb7fc1abc77ca4b3d1942236b1e373582809ee4 Mon Sep 17 00:00:00 2001 From: sachigoyal Date: Tue, 11 Nov 2025 01:20:03 +0600 Subject: [PATCH 3/4] remove delete feature --- .../src/app/api/blob/upload-handler/route.ts | 4 --- .../src/app/api/edit-image/google.ts | 2 +- .../src/app/api/edit-image/openai.ts | 2 +- templates/next-image/src/lib/blob-utils.ts | 2 -- .../next-image/src/lib/server-blob-utils.ts | 34 +++++-------------- 5 files changed, 10 insertions(+), 34 deletions(-) diff --git a/templates/next-image/src/app/api/blob/upload-handler/route.ts b/templates/next-image/src/app/api/blob/upload-handler/route.ts index 085595071..60c7081eb 100644 --- a/templates/next-image/src/app/api/blob/upload-handler/route.ts +++ b/templates/next-image/src/app/api/blob/upload-handler/route.ts @@ -9,7 +9,6 @@ * - Validates file types (images only) * - Sets maximum file size (50MB) * - Generates temporary tokens - * - Blobs are auto-deleted after server processing */ import { handleUpload, type HandleUploadBody } from '@vercel/blob/client'; @@ -36,9 +35,6 @@ export async function POST(request: Request): Promise { // Called after successful upload // You can add logging, analytics, etc. here console.log('Blob upload completed:', blob.pathname); - - // Note: The blob will be automatically deleted after the server - // downloads it for processing (see server-blob-utils.ts) }, }); diff --git a/templates/next-image/src/app/api/edit-image/google.ts b/templates/next-image/src/app/api/edit-image/google.ts index d3b9b6a8d..a71f78e5b 100644 --- a/templates/next-image/src/app/api/edit-image/google.ts +++ b/templates/next-image/src/app/api/edit-image/google.ts @@ -10,7 +10,7 @@ import { ERROR_MESSAGES } from '@/lib/constants'; /** * Handles Google Gemini image editing - * Downloads images from Vercel Blob URLs and converts to data URLs + * Converts image URLs to data URLs for processing */ export async function handleGoogleEdit( prompt: string, diff --git a/templates/next-image/src/app/api/edit-image/openai.ts b/templates/next-image/src/app/api/edit-image/openai.ts index d5bae8d26..8eabd9260 100644 --- a/templates/next-image/src/app/api/edit-image/openai.ts +++ b/templates/next-image/src/app/api/edit-image/openai.ts @@ -10,7 +10,7 @@ import { ERROR_MESSAGES } from '@/lib/constants'; /** * Handles OpenAI image editing - * Downloads images from Vercel Blob URLs and converts to data URLs + * Converts image URLs to data URLs for processing */ export async function handleOpenAIEdit( prompt: string, diff --git a/templates/next-image/src/lib/blob-utils.ts b/templates/next-image/src/lib/blob-utils.ts index b2eb7efd3..30f758b30 100644 --- a/templates/next-image/src/lib/blob-utils.ts +++ b/templates/next-image/src/lib/blob-utils.ts @@ -3,8 +3,6 @@ * * This module handles uploading images DIRECTLY to Vercel Blob storage from the client. * This bypasses the 4.5MB server request limit entirely! - * - * Images are uploaded directly to Vercel Blob, then deleted after server processing. */ import { upload } from '@vercel/blob/client'; diff --git a/templates/next-image/src/lib/server-blob-utils.ts b/templates/next-image/src/lib/server-blob-utils.ts index 82f5d89fb..116b84a1d 100644 --- a/templates/next-image/src/lib/server-blob-utils.ts +++ b/templates/next-image/src/lib/server-blob-utils.ts @@ -3,12 +3,8 @@ * * This module handles downloading images from Vercel Blob storage on the server * and converting them to base64 for AI model consumption. - * - * IMPORTANT: Blobs are deleted after download to ensure user privacy. */ -import { del } from '@vercel/blob'; - export function isBlobUrl(url: string): boolean { try { const urlObj = new URL(url); @@ -19,8 +15,7 @@ export function isBlobUrl(url: string): boolean { } export async function downloadBlobToDataUrl( - blobUrl: string, - deleteAfter: boolean = true + blobUrl: string ): Promise { try { const response = await fetch(blobUrl); @@ -36,17 +31,6 @@ export async function downloadBlobToDataUrl( // Get content type from response headers const contentType = response.headers.get('content-type') || 'image/png'; - // Delete the blob immediately after download for privacy - if (deleteAfter && isBlobUrl(blobUrl)) { - try { - await del(blobUrl); - console.log('Deleted blob after download:', blobUrl); - } catch (deleteError) { - console.error('Failed to delete blob:', deleteError); - // Don't throw - the image was downloaded successfully - } - } - return `data:${contentType};base64,${base64}`; } catch (error) { console.error('Error downloading blob:', error); @@ -57,15 +41,13 @@ export async function downloadBlobToDataUrl( } export async function downloadBlobsToDataUrls( - blobUrls: string[], - deleteAfter: boolean = true + blobUrls: string[] ): Promise { - return Promise.all(blobUrls.map(url => downloadBlobToDataUrl(url, deleteAfter))); + return Promise.all(blobUrls.map(url => downloadBlobToDataUrl(url))); } export async function processImageUrls( - imageUrls: string[], - deleteAfter: boolean = true + imageUrls: string[] ): Promise { return Promise.all( imageUrls.map(async url => { @@ -74,13 +56,13 @@ export async function processImageUrls( return url; } - // If it's a blob URL, download, convert, and delete + // If it's a blob URL, download and convert if (isBlobUrl(url)) { - return downloadBlobToDataUrl(url, deleteAfter); + return downloadBlobToDataUrl(url); } - // For any other URL type, try to download it (but don't delete) - return downloadBlobToDataUrl(url, false); + // For any other URL type, try to download it + return downloadBlobToDataUrl(url); }) ); } From 96d0a1fd7cd8ff79ad5903240d2f6a463d5ad5a3 Mon Sep 17 00:00:00 2001 From: sachigoyal Date: Thu, 13 Nov 2025 00:23:08 +0600 Subject: [PATCH 4/4] updated --- templates/next-image/.env.local | 5 +++++ templates/next-image/.gitignore | 3 +-- .../next-image/src/app/api/blob/upload-handler/route.ts | 2 +- templates/next-image/src/app/page.tsx | 4 ++-- 4 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 templates/next-image/.env.local diff --git a/templates/next-image/.env.local b/templates/next-image/.env.local new file mode 100644 index 000000000..005e0844f --- /dev/null +++ b/templates/next-image/.env.local @@ -0,0 +1,5 @@ +NEXT_PUBLIC_ECHO_APP_ID="74d9c979-e036-4e43-904f-32d214b361fc" +ECHO_APP_ID="74d9c979-e036-4e43-904f-32d214b361fc" + +# get your blob read write token from your vercel storage dashboard +BLOB_READ_WRITE_TOKEN="your-blob-read-write-token-here" \ No newline at end of file diff --git a/templates/next-image/.gitignore b/templates/next-image/.gitignore index 0e6ea09ac..3b8750ae4 100644 --- a/templates/next-image/.gitignore +++ b/templates/next-image/.gitignore @@ -34,8 +34,7 @@ yarn-error.log* .vercel # env -.env* -!.env.example +.env # typescript *.tsbuildinfo diff --git a/templates/next-image/src/app/api/blob/upload-handler/route.ts b/templates/next-image/src/app/api/blob/upload-handler/route.ts index 60c7081eb..42fbae4e8 100644 --- a/templates/next-image/src/app/api/blob/upload-handler/route.ts +++ b/templates/next-image/src/app/api/blob/upload-handler/route.ts @@ -26,7 +26,7 @@ export async function POST(request: Request): Promise { // For example: verify user session, check quotas, etc. return { - allowedContentTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], + allowedContentTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/heic', 'image/heif'], maximumSizeInBytes: 50 * 1024 * 1024, // 50MB addRandomSuffix: true, // Prevent filename collisions and add obscurity }; diff --git a/templates/next-image/src/app/page.tsx b/templates/next-image/src/app/page.tsx index e17b65c4f..ba3a3fda1 100644 --- a/templates/next-image/src/app/page.tsx +++ b/templates/next-image/src/app/page.tsx @@ -39,9 +39,9 @@ export default async function Home() { return (
{/* Header with title and token display */} -
+
-

+

Echo Image Gen