Val Town Code SearchReturn to Val Town

API Access

You can access search results via JSON API by adding format=json to your query:

https://codesearch.val.run/image-url.jpg?q=image&page=10&format=json

For typeahead suggestions, use the /typeahead endpoint:

https://codesearch.val.run/typeahead?q=image

Returns an array of strings in format "username" or "username/projectName"

Found 11157 results for "image"(5377ms)

postherousREADME.md41 matches

@charmaineโ€ขUpdated 2 days ago
12- **๐Ÿ”’ Email Security**: Allowlist and verification system to prevent unauthorized posts
13- **๐ŸŽจ Multi-format Support**: HTML and plain text posts
14- **๐Ÿ–ผ๏ธ Image Storage**: Upload images via web interface or email attachments with automatic processing
15- **๐Ÿ“ก RSS Feed**: Full RSS 2.0 support for syndication
16- **๐ŸŒ WebSub**: Real-time feed updates via WebSub protocol (configured)
58- `ALLOWED_EMAIL_ADDRESSES` - Comma-separated list of allowed email addresses (required for email publishing)
59- `BASE_URL` - Your blog's base URL (e.g., https://myblog.com or just myblog.com) - **Required for custom domains and ActivityPub federation**
60- `UPLOAD_PASSWORD` - Password required for image uploads (required to enable image upload functionality)
61
62### Environment Variables (Optional)
129- `GET /` - Main blog interface (with ActivityPub Link header)
130- `GET /post/:slug` - Individual post page (supports content negotiation for ActivityPub)
131- `GET /upload` - Image upload interface โœ…
132- `GET /images/:filename` - Serve uploaded images โœ…
133- `GET /rss` - RSS 2.0 feed
134- `GET /api/posts` - JSON API for posts
135- `GET /api/posts/:slug` - JSON API for single post
136- `POST /api/images/upload` - Upload image endpoint โœ…
137- `GET /api/posts/:slug/images` - Get images for a specific post โœ…
138- `GET /api/images/user/:email` - Get images uploaded by a user โœ…
139- `GET /api/images` - Get all images (admin endpoint) โœ…
140- `DELETE /api/images/:filename` - Delete an image โœ…
141- `GET /websub` - WebSub subscription endpoint
142- `GET /.well-known/webfinger` - WebFinger discovery for ActivityPub โœ…
176- **Automatic cleanup**: Expired drafts are automatically removed
177
178## ๐Ÿ–ผ๏ธ Image Storage & Management
179
180The platform includes comprehensive image storage capabilities for both static assets and email attachments.
181
182### Image Upload Methods
183
1841. **Web Interface**: Visit `/upload` (requires UPLOAD_PASSWORD) to upload images via drag-and-drop interface
1852. **Email Attachments**: Images attached to blog post emails are automatically processed and stored
186
187### Supported Image Formats
188
189- JPEG/JPG
193- SVG
194
195**File Size Limit**: 5MB per image
196
197### Image Storage Features
198
199- **Password Protection**: Upload interface protected by UPLOAD_PASSWORD environment variable
200- **Automatic Processing**: Email attachments are automatically extracted and stored
201- **Content Reference Replacement**: Image references in email content are automatically updated to use stored URLs
202- **Metadata Storage**: Original filename, MIME type, file size, alt text, and post associations
203- **Secure Access**: Password-protected upload system prevents unauthorized access
204- **Blob Storage**: Images stored using Val Town's blob storage with unique filenames
205- **URL Generation**: Clean `/images/filename` URLs for serving images
206
207### Image API Usage
208
209```javascript
210// Upload an image (requires password)
211const formData = new FormData();
212formData.append('image', file);
213formData.append('password', 'your-upload-password');
214formData.append('email', 'your-email@example.com');
215formData.append('altText', 'Description of image');
216formData.append('postSlug', 'my-blog-post'); // optional
217
218const response = await fetch('/api/images/upload', {
219 method: 'POST',
220 body: formData
221});
222
223// Get images for a post (no password required)
224const images = await fetch('/api/posts/my-post-slug/images').then(r => r.json());
225
226// Get images by user (requires password)
227const userImages = await fetch('/api/images/user/user@example.com?password=your-upload-password').then(r => r.json());
228
229// Get all images (requires password)
230const allImages = await fetch('/api/images?password=your-upload-password&limit=50').then(r => r.json());
231```
232
233### Email Attachment Processing
234
235When you send an email with image attachments:
236
2371. **Automatic Detection**: System identifies image attachments by MIME type
2382. **Storage**: Images are stored with unique filenames in blob storage
2393. **Database Records**: Metadata is saved linking images to the post
2404. **Content Updates**: Email content is updated to reference stored image URLs
2415. **Reference Replacement**: Common patterns like `cid:image.jpg` are replaced with proper URLs
242
243Example email with attachment:
245To: your-blog@val.town
246From: author@example.com
247Subject: My Post with Images
248Attachments: photo.jpg, diagram.png
249
256```html
257<p>Check out this photo:</p>
258<img src="/images/1234567890-abc123-photo.jpg" alt="My photo" />
259<p>And this diagram: ![diagram.png](/images/1234567890-def456-diagram.png)</p>
260```
261
322- โœ… **Rich Post Previews**: Posts display with titles, summaries, and full content in Mastodon
323- โœ… **Content Negotiation**: Individual posts serve both HTML and ActivityPub JSON based on Accept headers
324- โœ… **Profile Metadata**: Actor profile includes avatar, header image, and custom fields
325- โœ… **Link Discovery**: Proper HTTP Link headers for ActivityPub discovery
326- โœ… **Post Permalinks**: Each post has its own ActivityPub Note endpoint

postherousqueries.ts42 matches

@charmaineโ€ขUpdated 2 days ago
1import { sqlite } from "https://esm.town/v/stevekrouse/sqlite";
2import { BlogPost, CreatePostData, DraftPost, CreateDraftPostData, EmailVerification, BlogImage, CreateImageData } from "../../shared/types.ts";
3import { POSTS_TABLE, DRAFT_POSTS_TABLE, EMAIL_VERIFICATIONS_TABLE, SUBSCRIPTIONS_TABLE, FOLLOWERS_TABLE, ACTIVITIES_TABLE, IMAGES_TABLE } from "./schema.ts";
4
5/**
633}
634
635// ===== IMAGE FUNCTIONS =====
636
637/**
638 * Create a new image record
639 */
640export async function createImage(data: CreateImageData): Promise<BlogImage> {
641 const result = await sqlite.execute(`
642 INSERT INTO ${IMAGES_TABLE} (
643 filename, original_filename, mime_type, file_size, width, height,
644 alt_text, post_slug, uploaded_by_email, blob_key
657 ]);
658
659 // Get the inserted image
660 const image = await getImageByFilename(data.filename);
661 if (!image) {
662 throw new Error('Failed to create image record');
663 }
664
665 return image;
666}
667
668/**
669 * Get an image by filename
670 */
671export async function getImageByFilename(filename: string): Promise<BlogImage | null> {
672 const result = await sqlite.execute(`
673 SELECT * FROM ${IMAGES_TABLE} WHERE filename = ?
674 `, [filename]);
675
678 }
679
680 return result.rows[0] as BlogImage;
681}
682
683/**
684 * Get an image by blob key
685 */
686export async function getImageByBlobKey(blobKey: string): Promise<BlogImage | null> {
687 const result = await sqlite.execute(`
688 SELECT * FROM ${IMAGES_TABLE} WHERE blob_key = ?
689 `, [blobKey]);
690
693 }
694
695 return result.rows[0] as BlogImage;
696}
697
698/**
699 * Get all images for a specific post
700 */
701export async function getImagesByPostSlug(postSlug: string): Promise<BlogImage[]> {
702 const result = await sqlite.execute(`
703 SELECT * FROM ${IMAGES_TABLE}
704 WHERE post_slug = ?
705 ORDER BY created_at ASC
706 `, [postSlug]);
707
708 return result.rows as BlogImage[];
709}
710
711/**
712 * Get all images uploaded by a specific user
713 */
714export async function getImagesByUser(email: string): Promise<BlogImage[]> {
715 const result = await sqlite.execute(`
716 SELECT * FROM ${IMAGES_TABLE}
717 WHERE uploaded_by_email = ?
718 ORDER BY created_at DESC
719 `, [email]);
720
721 return result.rows as BlogImage[];
722}
723
724/**
725 * Get all images (for admin/management purposes)
726 */
727export async function getAllImages(limit: number = 100, offset: number = 0): Promise<BlogImage[]> {
728 const result = await sqlite.execute(`
729 SELECT * FROM ${IMAGES_TABLE}
730 ORDER BY created_at DESC
731 LIMIT ? OFFSET ?
732 `, [limit, offset]);
733
734 return result.rows as BlogImage[];
735}
736
737/**
738 * Update image metadata
739 */
740export async function updateImage(filename: string, updates: Partial<Pick<BlogImage, 'alt_text' | 'post_slug'>>): Promise<BlogImage | null> {
741 const setParts: string[] = [];
742 const values: any[] = [];
753
754 if (setParts.length === 0) {
755 return getImageByFilename(filename);
756 }
757
759
760 await sqlite.execute(`
761 UPDATE ${IMAGES_TABLE}
762 SET ${setParts.join(', ')}
763 WHERE filename = ?
764 `, values);
765
766 return getImageByFilename(filename);
767}
768
769/**
770 * Delete an image record
771 */
772export async function deleteImage(filename: string): Promise<boolean> {
773 const result = await sqlite.execute(`
774 DELETE FROM ${IMAGES_TABLE} WHERE filename = ?
775 `, [filename]);
776
779
780/**
781 * Generate a unique filename for an image
782 */
783export async function generateUniqueImageFilename(originalFilename: string): Promise<string> {
784 const timestamp = Date.now();
785 const randomSuffix = Math.random().toString(36).substring(2, 8);
801 // Ensure uniqueness
802 let counter = 1;
803 while (await getImageByFilename(filename)) {
804 filename = `${timestamp}-${randomSuffix}-${cleanBaseName}-${counter}${extension}`;
805 counter++;

postherousindex.ts100 matches

@charmaineโ€ขUpdated 2 days ago
4 deletePostsByAuthorName,
5 getActiveFollowers,
6 getAllImages,
7 getAllPosts,
8 getEmailVerificationByToken,
9 getFollowerCount,
10 getImagesByPostSlug,
11 getImagesByUser,
12 getPostActivityCounts,
13 getPostBySlug,
24import { isEmailAllowed } from "./services/email-security.ts";
25import {
26 deleteImageCompletely,
27 getImageData,
28 getImageUrl,
29 storeImageFromFile,
30 validateImageFile,
31} from "./services/images.ts";
32import { generateRSSFeed } from "./services/rss.ts";
33import { extractDomain, generateWebFingerResponse, isValidWebFingerResource } from "./services/webfinger.ts";
209
210 <!-- Twitter -->
211 <meta property="twitter:card" content="summary_large_image">
212 <meta property="twitter:url" content="${baseUrl}">
213 <meta property="twitter:title" content="Email Blog">
218 <link rel="alternate" type="application/activity+json" href="${baseUrl}/actor" />
219 <!-- Favicon -->
220 <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>๐Ÿ“ง</text></svg>">
221
222 <!-- Styles -->
301
302 <!-- Twitter -->
303 <meta property="twitter:card" content="summary_large_image">
304 <meta property="twitter:url" content="${postUrl}">
305 <meta property="twitter:title" content="${escapeHtml(post.title)}">
310
311 <!-- Favicon -->
312 <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>๐Ÿ“ง</text></svg>">
313
314 <!-- Styles -->
1250 icon: follower.actor_icon_url
1251 ? {
1252 type: "Image",
1253 url: follower.actor_icon_url,
1254 }
1287 icon: follower.actor_icon_url
1288 ? {
1289 type: "Image",
1290 url: follower.actor_icon_url,
1291 }
1583});
1584
1585// ===== IMAGE ROUTES =====
1586
1587// Serve image upload page
1588app.get("/upload", async c => {
1589 try {
1602 <div class="max-w-md mx-auto bg-white rounded-lg shadow-md p-6 text-center">
1603 <h1 class="text-2xl font-bold text-gray-900 mb-4">Upload Disabled</h1>
1604 <p class="text-gray-600 mb-4">Image upload is currently disabled.</p>
1605 <p class="text-sm text-gray-500">Administrator: Set the UPLOAD_PASSWORD environment variable to enable uploads.</p>
1606 <a href="/" class="text-blue-600 hover:text-blue-800 text-sm">โ† Back to Blog</a>
1619});
1620
1621// Serve images
1622app.get("/images/:filename", async c => {
1623 try {
1624 await ensureDbInitialized();
1625 const filename = c.req.param("filename");
1626
1627 const imageData = await getImageData(filename);
1628 if (!imageData) {
1629 return c.text("Image not found", 404);
1630 }
1631
1632 // Return the image data as a proper Response
1633 return new Response(imageData.data, {
1634 headers: {
1635 "Content-Type": imageData.mimeType,
1636 "Cache-Control": "public, max-age=31536000", // Cache for 1 year
1637 },
1638 });
1639 } catch (error) {
1640 console.error("Error serving image:", error);
1641 return c.text("Error serving image", 500);
1642 }
1643});
1644
1645// Upload image endpoint
1646app.post("/api/images/upload", async c => {
1647 try {
1648 await ensureDbInitialized();
1650 // Get the uploaded file and form data
1651 const body = await c.req.formData();
1652 const file = body.get("image") as File;
1653 const password = body.get("password") as string;
1654 const uploaderEmail = body.get("email") as string;
1657
1658 if (!file) {
1659 return c.json({ error: "No image file provided" }, 400);
1660 }
1661
1676 console.log(`โœ… Valid upload password from ${uploaderEmail}`);
1677
1678 // Store the image
1679 const image = await storeImageFromFile(file, uploaderEmail, {
1680 altText: altText || undefined,
1681 postSlug: postSlug || undefined,
1683
1684 const baseUrl = getBaseUrl(c);
1685 const imageUrl = getImageUrl(image.filename, baseUrl);
1686
1687 return c.json({
1688 success: true,
1689 image: {
1690 id: image.id,
1691 filename: image.filename,
1692 originalFilename: image.original_filename,
1693 url: imageUrl,
1694 mimeType: image.mime_type,
1695 fileSize: image.file_size,
1696 altText: image.alt_text,
1697 postSlug: image.post_slug,
1698 createdAt: image.created_at,
1699 },
1700 });
1701 } catch (error) {
1702 console.error("Error uploading image:", error);
1703 return c.json({ error: error.message || "Failed to upload image" }, 500);
1704 }
1705});
1706
1707// Get images for a specific post
1708app.get("/api/posts/:slug/images", async c => {
1709 try {
1710 await ensureDbInitialized();
1711 const slug = c.req.param("slug");
1712
1713 const images = await getImagesByPostSlug(slug);
1714 const baseUrl = getBaseUrl(c);
1715
1716 return c.json({
1717 images: images.map(image => ({
1718 id: image.id,
1719 filename: image.filename,
1720 originalFilename: image.original_filename,
1721 url: getImageUrl(image.filename, baseUrl),
1722 mimeType: image.mime_type,
1723 fileSize: image.file_size,
1724 altText: image.alt_text,
1725 createdAt: image.created_at,
1726 })),
1727 });
1728 } catch (error) {
1729 console.error("Error fetching post images:", error);
1730 return c.json({ error: "Failed to fetch images" }, 500);
1731 }
1732});
1733
1734// Get images uploaded by a user
1735app.get("/api/images/user/:email", async c => {
1736 try {
1737 await ensureDbInitialized();
1744 }
1745
1746 const images = await getImagesByUser(email);
1747 const baseUrl = getBaseUrl(c);
1748
1749 return c.json({
1750 images: images.map(image => ({
1751 id: image.id,
1752 filename: image.filename,
1753 originalFilename: image.original_filename,
1754 url: getImageUrl(image.filename, baseUrl),
1755 mimeType: image.mime_type,
1756 fileSize: image.file_size,
1757 altText: image.alt_text,
1758 postSlug: image.post_slug,
1759 createdAt: image.created_at,
1760 })),
1761 });
1762 } catch (error) {
1763 console.error("Error fetching user images:", error);
1764 return c.json({ error: "Failed to fetch images" }, 500);
1765 }
1766});
1767
1768// Delete an image
1769app.delete("/api/images/:filename", async c => {
1770 try {
1771 await ensureDbInitialized();
1777 }
1778
1779 const success = await deleteImageCompletely(filename);
1780
1781 if (!success) {
1782 return c.json({ error: "Image not found" }, 404);
1783 }
1784
1785 return c.json({ success: true, message: "Image deleted successfully" });
1786 } catch (error) {
1787 console.error("Error deleting image:", error);
1788 return c.json({ error: "Failed to delete image" }, 500);
1789 }
1790});
1791
1792// Get all images (admin endpoint)
1793app.get("/api/images", async c => {
1794 try {
1795 await ensureDbInitialized();
1803 const offset = parseInt(c.req.query("offset") || "0");
1804
1805 const images = await getAllImages(limit, offset);
1806 const baseUrl = getBaseUrl(c);
1807
1808 return c.json({
1809 images: images.map(image => ({
1810 id: image.id,
1811 filename: image.filename,
1812 originalFilename: image.original_filename,
1813 url: getImageUrl(image.filename, baseUrl),
1814 mimeType: image.mime_type,
1815 fileSize: image.file_size,
1816 altText: image.alt_text,
1817 postSlug: image.post_slug,
1818 uploadedByEmail: image.uploaded_by_email,
1819 createdAt: image.created_at,
1820 })),
1821 pagination: {
1822 limit,
1823 offset,
1824 hasMore: images.length === limit,
1825 },
1826 });
1827 } catch (error) {
1828 console.error("Error fetching all images:", error);
1829 return c.json({ error: "Failed to fetch images" }, 500);
1830 }
1831});

postherousimages.ts72 matches

@charmaineโ€ขUpdated 2 days ago
1import { blob } from "https://esm.town/v/std/blob";
2import { createImage, generateUniqueImageFilename, getImageByFilename, deleteImage } from "../database/queries.ts";
3import { BlogImage, CreateImageData, EmailAttachment } from "../../shared/types.ts";
4
5// Supported image MIME types
6const SUPPORTED_IMAGE_TYPES = [
7 'image/jpeg',
8 'image/jpg',
9 'image/png',
10 'image/gif',
11 'image/webp',
12 'image/svg+xml'
13];
14
17
18/**
19 * Check if a MIME type is a supported image format
20 */
21export function isSupportedImageType(mimeType: string): boolean {
22 return SUPPORTED_IMAGE_TYPES.includes(mimeType.toLowerCase());
23}
24
25/**
26 * Validate image file
27 */
28export function validateImageFile(file: { size: number; type: string }): { valid: boolean; error?: string } {
29 if (file.size > MAX_FILE_SIZE) {
30 return { valid: false, error: `File size too large. Maximum size is ${MAX_FILE_SIZE / (1024 * 1024)}MB` };
31 }
32
33 if (!isSupportedImageType(file.type)) {
34 return { valid: false, error: `Unsupported file type. Supported types: ${SUPPORTED_IMAGE_TYPES.join(', ')}` };
35 }
36
39
40/**
41 * Store an image from a File object (for uploads)
42 */
43export async function storeImageFromFile(
44 file: File,
45 uploadedByEmail: string,
48 postSlug?: string;
49 } = {}
50): Promise<BlogImage> {
51 // Validate the file
52 const validation = validateImageFile(file);
53 if (!validation.valid) {
54 throw new Error(validation.error);
56
57 // Generate unique filename
58 const filename = await generateUniqueImageFilename(file.name);
59 const blobKey = `images/${filename}`;
60
61 // Read file as array buffer and store in blob
63 await blob.set(blobKey, new Uint8Array(arrayBuffer));
64
65 // Try to get image dimensions (basic approach)
66 let width: number | undefined;
67 let height: number | undefined;
68
69 // For now, we'll skip dimension detection as it requires additional libraries
70 // In a production environment, you might want to use a library like 'image-size'
71
72 // Create image record
73 const imageData: CreateImageData = {
74 filename,
75 original_filename: file.name,
84 };
85
86 return await createImage(imageData);
87}
88
89/**
90 * Store an image from base64 data (for email attachments)
91 */
92export async function storeImageFromBase64(
93 base64Data: string,
94 originalFilename: string,
99 postSlug?: string;
100 } = {}
101): Promise<BlogImage> {
102 // Convert base64 to Uint8Array
103 const binaryString = atob(base64Data);
110
111 // Validate the file
112 const validation = validateImageFile({ size: fileSize, type: mimeType });
113 if (!validation.valid) {
114 throw new Error(validation.error);
116
117 // Generate unique filename
118 const filename = await generateUniqueImageFilename(originalFilename);
119 const blobKey = `images/${filename}`;
120
121 // Store in blob
122 await blob.set(blobKey, bytes);
123
124 // Create image record
125 const imageData: CreateImageData = {
126 filename,
127 original_filename: originalFilename,
134 };
135
136 return await createImage(imageData);
137}
138
139/**
140 * Get image data from blob storage
141 */
142export async function getImageData(filename: string): Promise<{ data: Uint8Array; mimeType: string } | null> {
143 const imageRecord = await getImageByFilename(filename);
144 if (!imageRecord) {
145 return null;
146 }
147
148 const blobData = await blob.get(imageRecord.blob_key);
149 if (!blobData) {
150 return null;
169 return {
170 data,
171 mimeType: imageRecord.mime_type
172 };
173}
174
175/**
176 * Delete an image (both record and blob data)
177 */
178export async function deleteImageCompletely(filename: string): Promise<boolean> {
179 const imageRecord = await getImageByFilename(filename);
180 if (!imageRecord) {
181 return false;
182 }
183
184 // Delete from blob storage
185 await blob.delete(imageRecord.blob_key);
186
187 // Delete from database
188 return await deleteImage(filename);
189}
190
191/**
192 * Process email attachments and extract images
193 */
194export async function processEmailAttachments(
196 uploadedByEmail: string,
197 postSlug?: string
198): Promise<BlogImage[]> {
199 const images: BlogImage[] = [];
200
201 for (const attachment of attachments) {
202 if (isSupportedImageType(attachment.contentType)) {
203 try {
204 const image = await storeImageFromBase64(
205 attachment.content,
206 attachment.filename,
209 { postSlug }
210 );
211 images.push(image);
212 console.log(`โœ… Processed email attachment image: ${attachment.filename} -> ${image.filename}`);
213 } catch (error) {
214 console.error(`โŒ Failed to process email attachment ${attachment.filename}:`, error);
217 }
218
219 return images;
220}
221
222/**
223 * Generate image URL for serving
224 */
225export function getImageUrl(filename: string, baseUrl?: string): string {
226 const base = baseUrl || '';
227 return `${base}/images/${filename}`;
228}
229
230/**
231 * Replace image references in content with proper URLs
232 */
233export function replaceImageReferencesInContent(content: string, images: BlogImage[], baseUrl?: string): string {
234 let updatedContent = content;
235
236 for (const image of images) {
237 const imageUrl = getImageUrl(image.filename, baseUrl);
238
239 // Replace various possible references to the original filename
240 const patterns = [
241 new RegExp(`cid:${image.original_filename}`, 'gi'),
242 new RegExp(`src="${image.original_filename}"`, 'gi'),
243 new RegExp(`src='${image.original_filename}'`, 'gi'),
244 new RegExp(`\\[${image.original_filename}\\]`, 'gi'),
245 new RegExp(`\\(${image.original_filename}\\)`, 'gi')
246 ];
247
248 for (const pattern of patterns) {
249 if (pattern.source.includes('src=')) {
250 updatedContent = updatedContent.replace(pattern, `src="${imageUrl}"`);
251 } else if (pattern.source.includes('cid:')) {
252 updatedContent = updatedContent.replace(pattern, imageUrl);
253 } else {
254 updatedContent = updatedContent.replace(pattern, `![${image.alt_text || image.original_filename}](${imageUrl})`);
255 }
256 }

postherousemail.ts15 matches

@charmaineโ€ขUpdated 2 days ago
4import { CreateDraftPostData, EmailAttachment } from "./shared/types.ts";
5import { isEmailAllowed, generateVerificationToken, sendVerificationEmail, getVerificationExpiration } from "./backend/services/email-security.ts";
6import { processEmailAttachments, replaceImageReferencesInContent } from "./backend/services/images.ts";
7
8// Helper function to extract email address from display name format
156 });
157
158 // Process email attachments for images
159 let processedImages: any[] = [];
160 if (email.attachments && email.attachments.length > 0) {
161 console.log('๐Ÿ“Ž Processing email attachments:', {
176 }));
177
178 processedImages = await processEmailAttachments(emailAttachments, authorEmail, slug);
179 console.log('โœ… Processed images from email attachments:', {
180 imageCount: processedImages.length,
181 images: processedImages.map(img => ({
182 filename: img.filename,
183 originalFilename: img.original_filename,
186 });
187
188 // Replace image references in content with proper URLs
189 if (processedImages.length > 0) {
190 const originalContent = content;
191 content = replaceImageReferencesInContent(content, processedImages);
192
193 if (content !== originalContent) {
194 console.log('๐Ÿ”„ Updated content with image references');
195 }
196 }
197 } catch (imageError) {
198 console.error('โŒ Error processing email attachments:', {
199 error: imageError.message,
200 stack: imageError.stack
201 });
202 // Continue with post creation even if image processing fails
203 }
204 } else {

postherousauth.ts1 match

@charmaineโ€ขUpdated 2 days ago
1/**
2 * Authentication service for image uploads and admin functions
3 */
4

postherousactivitypub.ts8 matches

@charmaineโ€ขUpdated 2 days ago
246 // Add avatar/icon for better visual representation
247 icon: {
248 type: "Image",
249 mediaType: "image/png",
250 url: `https://posthero.us/images/1752442753496-q8omo6-logo-small.png`,
251 },
252 // Add header image
253 image: {
254 type: "Image",
255 mediaType: "image/jpeg",
256 url: `https://posthero.us/images/1752443393121-bb51qa-logo-header.jpg`,
257 },
258 // Add some additional fields that Mastodon likes

test123main.tsx1 match

@joinโ€ขUpdated 2 days ago
288 element.textContent = newWord;
289 const colors = generateColorScheme(newWord);
290 element.style.backgroundImage = colors.gradient;
291 element.dataset.shadowColor = colors.shadowColor;
292

postherouswebfinger.ts2 matches

@kerryrmโ€ขUpdated 2 days ago
119 {
120 "rel": "http://webfinger.net/rel/avatar",
121 "type": "image/png",
122 "href": "https://posthero.us/images/1752442753496-q8omo6-logo-small.png",
123 },
124 ],

postherousupload.html116 matches

@kerryrmโ€ขUpdated 2 days ago
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Upload Images - Posterous Blog</title>
7 <script src="https://cdn.twind.style" crossorigin></script>
8 <style>
12 }
13
14 .image-preview {
15 max-width: 200px;
16 max-height: 200px;
27 <div class="bg-white rounded-lg shadow-md p-6">
28 <div class="mb-6">
29 <h1 class="text-3xl font-bold text-gray-900 mb-2">Upload Images</h1>
30 <p class="text-gray-600">Upload images to use in your blog posts or as static assets.</p>
31 <a href="/" class="text-blue-600 hover:text-blue-800 text-sm">โ† Back to Blog</a>
32 </div>
40 class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
41 placeholder="Enter upload password">
42 <p class="text-xs text-gray-500 mt-1">Required to upload images (set via UPLOAD_PASSWORD environment variable)</p>
43 </div>
44
48 class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
49 placeholder="your-email@example.com">
50 <p class="text-xs text-gray-500 mt-1">For tracking who uploaded the image</p>
51 </div>
52
56 class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
57 placeholder="my-blog-post">
58 <p class="text-xs text-gray-500 mt-1">Associate this image with a specific blog post</p>
59 </div>
60
63 <input type="text" id="altText" name="altText"
64 class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
65 placeholder="Description of the image">
66 <p class="text-xs text-gray-500 mt-1">Helps with accessibility and SEO</p>
67 </div>
73 <path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
74 </svg>
75 <p class="text-lg text-gray-600 mb-2">Drop images here or click to select</p>
76 <p class="text-sm text-gray-500">Supports JPEG, PNG, GIF, WebP, SVG (max 5MB)</p>
77 </div>
78 <input type="file" id="fileInput" name="image" accept="image/*" class="hidden" multiple>
79 </div>
80
89 <button type="submit" id="uploadBtn"
90 class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed">
91 Upload Images
92 </button>
93 </form>
102 </div>
103
104 <!-- All Uploaded Images -->
105 <div id="allImagesSection" class="mt-8">
106 <div class="border-t border-gray-200 pt-6">
107 <div class="flex items-center justify-between mb-4">
108 <h2 class="text-xl font-semibold text-gray-900">๐Ÿ“ All Uploaded Images</h2>
109 <button id="refreshImagesBtn"
110 class="bg-blue-600 text-white px-3 py-1 rounded text-sm hover:bg-blue-700">
111 Refresh
112 </button>
113 </div>
114 <div id="allImagesList" class="space-y-4">
115 <div class="text-center py-8 text-gray-500">
116 <p>Enter your upload password above to view all uploaded images.</p>
117 </div>
118 </div>
119 <div id="loadMoreImages" class="hidden text-center mt-4">
120 <button id="loadMoreBtn"
121 class="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700">
122 Load More Images
123 </button>
124 </div>
128 <!-- URL Preview Info -->
129 <div class="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
130 <h3 class="text-sm font-medium text-blue-900 mb-2">๐Ÿ“‹ Image URL Information</h3>
131 <p class="text-sm text-blue-800 mb-2">
132 Uploaded images will be accessible at: <code class="bg-blue-100 px-1 rounded">/images/[unique-filename]</code>
133 </p>
134 <p class="text-xs text-blue-700">
135 The system generates unique filenames like: <code class="bg-blue-100 px-1 rounded">1704067200-abc123-my-image.jpg</code>
136 </p>
137 <div id="urlPreview" class="mt-3 hidden">
138 <p class="text-sm font-medium text-blue-900 mb-1">Preview URLs for selected images:</p>
139 <div id="urlList" class="space-y-1"></div>
140 </div>
141 </div>
142
143 <!-- Image Preview -->
144 <div id="imagePreview" class="hidden">
145 <h2 class="text-xl font-semibold text-gray-900 mb-4">Selected Images</h2>
146 <div id="previewGrid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"></div>
147 </div>
153 <div class="grid md:grid-cols-2 gap-6">
154 <div>
155 <h4 class="text-sm font-medium text-gray-800 mb-2">Using Images in Blog Posts</h4>
156 <div class="space-y-2 text-sm text-gray-600">
157 <div>
158 <strong>In HTML emails:</strong>
159 <code class="block bg-white p-1 mt-1 rounded text-xs">&lt;img src="/images/filename.jpg" alt="Description" /&gt;</code>
160 </div>
161 <div>
162 <strong>In plain text emails:</strong>
163 <code class="block bg-white p-1 mt-1 rounded text-xs">![Description](/images/filename.jpg)</code>
164 </div>
165 </div>
169 <h4 class="text-sm font-medium text-gray-800 mb-2">Email Attachments</h4>
170 <div class="space-y-2 text-sm text-gray-600">
171 <p>โ€ข Attach images to your blog post emails</p>
172 <p>โ€ข System automatically processes and stores them</p>
173 <p>โ€ข References like <code class="bg-white px-1 rounded">cid:image.jpg</code> are auto-replaced</p>
174 <p>โ€ข Supports JPEG, PNG, GIF, WebP, SVG (max 5MB each)</p>
175 </div>
181 <div class="grid md:grid-cols-2 gap-4 text-xs text-gray-600">
182 <div>
183 <code class="bg-white p-1 rounded">GET /images/filename.jpg</code> - Serve image
184 </div>
185 <div>
186 <code class="bg-white p-1 rounded">GET /api/posts/slug/images</code> - Get post images
187 </div>
188 </div>
201 const uploadResults = document.getElementById('uploadResults');
202 const resultsList = document.getElementById('resultsList');
203 const imagePreview = document.getElementById('imagePreview');
204 const previewGrid = document.getElementById('previewGrid');
205 const uploadBtn = document.getElementById('uploadBtn');
208
209 let selectedFiles = [];
210 let allImagesOffset = 0;
211 const allImagesLimit = 20;
212 let hasMoreImages = true;
213
214 // Generate a preview URL (approximation of what the actual URL will be)
229 .replace(/^-|-$/g, '');
230
231 return `/images/${timestamp}-${randomSuffix}-${cleanBaseName}${extension}`;
232 }
233
283 e.preventDefault();
284 dropZone.classList.remove('drag-over');
285 const files = Array.from(e.dataTransfer.files).filter(file => file.type.startsWith('image/'));
286 handleFiles(files);
287 }
294 function handleFiles(files) {
295 selectedFiles = files;
296 showImagePreview(files);
297 updateUrlPreview();
298 uploadBtn.disabled = files.length === 0;
299 }
300
301 function showImagePreview(files) {
302 if (files.length === 0) {
303 imagePreview.classList.add('hidden');
304 return;
305 }
306
307 imagePreview.classList.remove('hidden');
308 previewGrid.innerHTML = '';
309
314 previewItem.className = 'relative bg-gray-100 rounded-lg p-2';
315 previewItem.innerHTML = `
316 <img src="${e.target.result}" alt="${file.name}" class="image-preview w-full h-32 object-cover rounded">
317 <p class="text-xs text-gray-600 mt-1 truncate">${file.name}</p>
318 <p class="text-xs text-gray-500">${(file.size / 1024).toFixed(1)} KB</p>
330 function removeFile(index) {
331 selectedFiles.splice(index, 1);
332 showImagePreview(selectedFiles);
333 updateUrlPreview();
334 uploadBtn.disabled = selectedFiles.length === 0;
359 }
360
361 // Load all uploaded images
362 async function loadAllImages(reset = false) {
363 const password = document.getElementById('password').value;
364 if (!password) {
365 // Show initial message
366 document.getElementById('allImagesList').innerHTML = `
367 <div class="text-center py-8 text-gray-500">
368 <p>Enter your upload password above to view all uploaded images.</p>
369 </div>
370 `;
373
374 if (reset) {
375 allImagesOffset = 0;
376 hasMoreImages = true;
377 document.getElementById('allImagesList').innerHTML = `
378 <div class="text-center py-4 text-gray-500">
379 <p>Loading images...</p>
380 </div>
381 `;
383
384 try {
385 const response = await fetch(`/api/images?password=${encodeURIComponent(password)}&limit=${allImagesLimit}&offset=${allImagesOffset}`);
386 const data = await response.json();
387
388 if (!response.ok) {
389 if (response.status === 403) {
390 document.getElementById('allImagesList').innerHTML = `
391 <div class="text-center py-8 text-red-500">
392 <p>Invalid password. Please check your upload password.</p>
394 `;
395 } else {
396 document.getElementById('allImagesList').innerHTML = `
397 <div class="text-center py-8 text-red-500">
398 <p>Error loading images: ${data.error}</p>
399 </div>
400 `;
403 }
404
405 const allImagesList = document.getElementById('allImagesList');
406
407 if (data.images.length === 0 && allImagesOffset === 0) {
408 allImagesList.innerHTML = `
409 <div class="text-center py-8 text-gray-500">
410 <p>No images uploaded yet. Upload some images above to get started!</p>
411 </div>
412 `;
416 // Clear loading message if this is a reset
417 if (reset) {
418 allImagesList.innerHTML = '';
419 }
420
421 // Add images to the list
422 data.images.forEach(image => {
423 const imageItem = createImageListItem(image);
424 allImagesList.appendChild(imageItem);
425 });
426
427 // Update pagination
428 allImagesOffset += data.images.length;
429 hasMoreImages = data.pagination.hasMore;
430
431 const loadMoreImages = document.getElementById('loadMoreImages');
432 if (hasMoreImages) {
433 loadMoreImages.classList.remove('hidden');
434 } else {
435 loadMoreImages.classList.add('hidden');
436 }
437
438 } catch (error) {
439 console.error('Error loading images:', error);
440 document.getElementById('allImagesList').innerHTML = `
441 <div class="text-center py-8 text-red-500">
442 <p>Error loading images. Please try again.</p>
443 </div>
444 `;
446 }
447
448 // Create an image list item
449 function createImageListItem(image) {
450 const imageItem = document.createElement('div');
451 imageItem.className = 'bg-gray-50 border border-gray-200 rounded-lg p-4';
452
453 const uploadDate = new Date(image.createdAt).toLocaleDateString('en-US', {
454 year: 'numeric',
455 month: 'short',
459 });
460
461 imageItem.innerHTML = `
462 <div class="flex items-start space-x-4">
463 <div class="flex-shrink-0">
464 <img src="${image.url}" alt="${image.altText || image.originalFilename}"
465 class="w-16 h-16 object-cover rounded border">
466 </div>
467 <div class="flex-1 min-w-0">
468 <div class="flex items-center justify-between mb-2">
469 <h3 class="text-sm font-medium text-gray-900 truncate">${image.originalFilename}</h3>
470 <span class="text-xs text-gray-500">${(image.fileSize / 1024).toFixed(1)} KB</span>
471 </div>
472
473 <div class="space-y-2">
474 <div>
475 <label class="block text-xs font-medium text-gray-700 mb-1">Image URL:</label>
476 <div class="flex items-center space-x-2">
477 <code class="flex-1 bg-white border border-gray-300 px-2 py-1 rounded text-xs text-gray-900 break-all">${image.url}</code>
478 <button onclick="copyToClipboard('${image.url}')"
479 class="bg-gray-100 hover:bg-gray-200 text-gray-800 px-2 py-1 rounded text-xs">
480 Copy
486 <label class="block text-xs font-medium text-gray-700 mb-1">HTML Tag:</label>
487 <div class="flex items-center space-x-2">
488 <code class="flex-1 bg-white border border-gray-300 px-2 py-1 rounded text-xs text-gray-900 break-all">&lt;img src="${image.url}" alt="${image.altText || image.originalFilename}" /&gt;</code>
489 <button onclick="copyToClipboard('&lt;img src=&quot;${image.url}&quot; alt=&quot;${image.altText || image.originalFilename}&quot; /&gt;')"
490 class="bg-gray-100 hover:bg-gray-200 text-gray-800 px-2 py-1 rounded text-xs">
491 Copy
497 <label class="block text-xs font-medium text-gray-700 mb-1">Markdown:</label>
498 <div class="flex items-center space-x-2">
499 <code class="flex-1 bg-white border border-gray-300 px-2 py-1 rounded text-xs text-gray-900 break-all">![${image.altText || image.originalFilename}](${image.url})</code>
500 <button onclick="copyToClipboard('![${image.altText || image.originalFilename}](${image.url})')"
501 class="bg-gray-100 hover:bg-gray-200 text-gray-800 px-2 py-1 rounded text-xs">
502 Copy
509 <div class="flex items-center justify-between">
510 <div>
511 <span>๐Ÿ“ง ${image.uploadedByEmail}</span>
512 ${image.postSlug ? `<span class="ml-3">๐Ÿ“ ${image.postSlug}</span>` : ''}
513 </div>
514 <span>๐Ÿ“… ${uploadDate}</span>
519 `;
520
521 return imageItem;
522 }
523
524 // Event listeners for image management
525 document.getElementById('refreshImagesBtn').addEventListener('click', () => {
526 loadAllImages(true);
527 });
528
529 document.getElementById('loadMoreBtn').addEventListener('click', () => {
530 loadAllImages(false);
531 });
532
533 // Load images when password is entered
534 document.getElementById('password').addEventListener('input', (e) => {
535 // Debounce the loading
536 clearTimeout(window.imageLoadTimeout);
537 window.imageLoadTimeout = setTimeout(() => {
538 loadAllImages(true);
539 }, 500);
540 });
545
546 if (selectedFiles.length === 0) {
547 alert('Please select at least one image to upload.');
548 return;
549 }
580 try {
581 const formData = new FormData();
582 formData.append('image', file);
583 formData.append('password', password);
584 formData.append('email', email);
586 if (altText) formData.append('altText', altText);
587
588 const response = await fetch('/api/images/upload', {
589 method: 'POST',
590 body: formData
622 <h3 class="text-sm font-medium text-green-800">${result.file}</h3>
623 <div class="mt-2 text-sm text-green-700">
624 <p>Successfully uploaded as: <strong>${result.data.image.filename}</strong></p>
625
626 <div class="mt-3 space-y-2">
627 <div>
628 <label class="block text-xs font-medium text-green-800 mb-1">Image URL:</label>
629 <div class="flex items-center space-x-2">
630 <code class="flex-1 bg-green-50 border border-green-200 px-2 py-1 rounded text-xs text-green-900 break-all">${result.data.image.url}</code>
631 <button onclick="copyToClipboard('${result.data.image.url}')"
632 class="bg-green-100 hover:bg-green-200 text-green-800 px-2 py-1 rounded text-xs">
633 Copy URL
639 <label class="block text-xs font-medium text-green-800 mb-1">HTML Tag:</label>
640 <div class="flex items-center space-x-2">
641 <code class="flex-1 bg-green-50 border border-green-200 px-2 py-1 rounded text-xs text-green-900 break-all">&lt;img src="${result.data.image.url}" alt="${result.data.image.altText || result.data.image.originalFilename}" /&gt;</code>
642 <button onclick="copyToClipboard('&lt;img src=&quot;${result.data.image.url}&quot; alt=&quot;${result.data.image.altText || result.data.image.originalFilename}&quot; /&gt;')"
643 class="bg-green-100 hover:bg-green-200 text-green-800 px-2 py-1 rounded text-xs">
644 Copy HTML
650 <label class="block text-xs font-medium text-green-800 mb-1">Markdown:</label>
651 <div class="flex items-center space-x-2">
652 <code class="flex-1 bg-green-50 border border-green-200 px-2 py-1 rounded text-xs text-green-900 break-all">![${result.data.image.altText || result.data.image.originalFilename}](${result.data.image.url})</code>
653 <button onclick="copyToClipboard('![${result.data.image.altText || result.data.image.originalFilename}](${result.data.image.url})')"
654 class="bg-green-100 hover:bg-green-200 text-green-800 px-2 py-1 rounded text-xs">
655 Copy MD
685 selectedFiles = [];
686 fileInput.value = '';
687 imagePreview.classList.add('hidden');
688 urlPreview.classList.add('hidden');
689 previewGrid.innerHTML = '';
690
691 // Refresh the images list to show newly uploaded images
692 loadAllImages(true);
693 });
694

thilenius-webcam1 file match

@stabbylambdaโ€ขUpdated 5 days ago
Image proxy for the latest from https://gliderport.thilenius.com

simple-images1 file match

@blazemcworldโ€ขUpdated 1 week ago
simple image generator using pollinations.ai
Chrimage
Atiq
"Focal Lens with Atig Wazir" "Welcome to my photography journey! I'm Atiq Wazir, a passionate photographer capturing life's beauty one frame at a time. Explore my gallery for stunning images, behind-the-scenes stories, and tips & tricks to enhance your own