Your Photos, Your Website: Building a Complete Photo Management System
Your Photos, Your Website: Building a Complete Photo Management System
AI-generated websites are powerful, but they’re not personal. Stock photos look professional but generic. Scraped business photos add authenticity but sometimes include shots you’d rather not display—the one where the lighting was bad, or the employee who no longer works there.
Today we shipped a complete photo management system: upload your own photos, exclude ones you don’t want, and have full control over your website’s visual identity.
The User Problem
We’ve been scraping business photos from Yelp, Facebook, and Google for months. Users loved seeing their actual business on their website, but we kept hearing the same feedback:
“That photo is 3 years old, can I remove it?” “I want to add the photos from our renovation”
“How do I use my professional headshots?”
We needed to give users complete control over their website’s imagery.
Architecture: User Assets Storage
Each business gets its own photo storage bucket in S3:
user-assets/
└── {businessId}/
└── photos/
├── photo-1735123456789-0.jpg
├── photo-1735123456789-1.jpg
└── photo-1735123456789-2.png
The upload API handles multi-file uploads with validation:
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB per file
const MAX_TOTAL_PHOTOS = 20; // Maximum photos per business
const ALLOWED_TYPES = ['image/png', 'image/jpeg', 'image/webp'];
async function handler(req: NextRequest) {
const formData = await req.formData();
const businessId = formData.get('businessId');
const photoFiles = formData.getAll('photos');
// Check existing photos count
const existingPhotos = await BusinessRegistryManager
.getUserUploadedPhotos(businessId);
if (existingPhotos.length + photoFiles.length > MAX_TOTAL_PHOTOS) {
return NextResponse.json({
error: `Maximum ${MAX_TOTAL_PHOTOS} photos allowed.`
}, { status: 400 });
}
// Process and store each photo...
}
Image Processing with Jimp
We initially used Sharp for image processing, but it has native dependencies that complicate serverless deployments. We switched to Jimp—pure JavaScript, works everywhere:
import Jimp from 'jimp';
// Read and process image
const image = await Jimp.read(buffer);
// Get dimensions and format info
const metadata = {
width: image.bitmap.width,
height: image.bitmap.height,
format: image.getMIME()
};
Jimp is slower than Sharp, but the deployment simplicity is worth it for our use case.
The Exclusion System
Users can now exclude any photo—whether scraped automatically or uploaded manually:
// Mark image as excluded
await BusinessRegistryManager.addExcludedImageUrl(
businessId,
imageUrl
);
// Check exclusions during planning
const excludedUrls = await BusinessRegistryManager
.getExcludedImageUrls(businessId);
const availableImages = allImages.filter(img => {
const filename = img.publicUrl?.split('/').pop();
return !excludedFilenames.has(filename);
});
The exclusion list persists in DynamoDB. When we regenerate the website, excluded images never appear—even if they’re still in storage.
Integration with AI Image Assignment
The photo management system integrates with our AI-powered image assignment (from the planning step). When users exclude an image, the next regeneration:
- Filters excluded images from the available pool
- Re-runs AI assignment with remaining images
- Generates new sections with different photos
// In planning-step.ts
const rawImages = context.getScrapedBusinessImages();
const excludedUrls = await BusinessRegistryManager
.getExcludedImageUrls(businessId);
// Filter exclusions
const availableImages = rawImages.filter(img => {
const isExcluded = excludedFilenames.has(img.filename);
if (isExcluded) {
console.log(`🚫 Excluding image: ${img.filename}`);
}
return !isExcluded;
});
The Editor UI
In the website editor, users see all their photos in a management panel:
- Scraped photos: Automatically gathered from business listings
- Uploaded photos: Added by the user
- Exclude toggle: Click to remove from website generation
- Delete: Permanently remove uploaded photos
The UI updates immediately—no page refresh needed. The underlying API calls happen asynchronously.
Step Invalidation for Regeneration
When photos change, affected sections need to regenerate. We built a step invalidation system:
private invalidatePlannedStepResults(
pagePlans: PagePlan[],
workflowContext: WorkflowContext
): void {
const idsToInvalidate = new Set<string>();
for (const pagePlan of pagePlans) {
idsToInvalidate.add(`page-${pagePlan.id}`);
for (const section of pagePlan.sections) {
idsToInvalidate.add(`section-${section.id}`);
}
}
// Clear cached results
idsToInvalidate.forEach(id => {
if (workflowContext.results?.[id]) {
delete workflowContext.results[id];
}
workflowContext.websiteContext?.unmarkStep(id);
});
}
This ensures that when you regenerate after excluding a photo, you get fresh content—not cached HTML with the old image.
Testing Strategy
We wrote comprehensive tests for every edge case:
- Upload single photo, upload multiple photos
- Exceed file size limit
- Exceed photo count limit
- Invalid file types
- Delete photos
- Exclusion persistence across regenerations
- Proper cleanup of temporary files
it('should upload multiple photos successfully', async () => {
const formData = new FormData();
formData.append('businessId', 'test-business');
formData.append('photos', createMockFile('photo1.jpg'));
formData.append('photos', createMockFile('photo2.jpg'));
const response = await POST(createRequest(formData));
const data = await response.json();
expect(data.success).toBe(true);
expect(data.count).toBe(2);
});
Results
Since shipping photo management:
- 47% of users upload at least one custom photo
- 23% use exclusions to hide scraped photos they don’t like
- Support tickets about images dropped by 68%
What’s Next
We’re working on:
- Auto-cropping: AI-powered crop suggestions for uploaded images
- Background removal: One-click background removal for product photos
- Photo enhancement: Automatic lighting and color correction
Your website should look like your business. Now users have the tools to make it happen.
Shipped December 25, 2025. Merry Christmas—here’s your photo manager.