Skip to content

Commit 80e658b

Browse files
feat: add admin page for managing blog images and related API endpoints (#843)
* feat: add admin page for managing blog images and related API endpoints - Created BlogImagesAdminPage component for managing blog images. - Implemented API endpoints for Cloudinary folder listing, image listing, resource retrieval, image searching, uploading images, and generating upload signatures. - Added functionality to display statistics and lists of images used in blogs. - Integrated authentication checks for API endpoints to ensure only authorized users can access them. - Updated yarn.lock to include new dependencies for Cloudinary integration. * fix: remove unused import in blog-image-manager
1 parent 26773d4 commit 80e658b

File tree

58 files changed

+3381
-42
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+3381
-42
lines changed

.env.example

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,9 @@ PHI3_API_KEY="your-phi3-api-key"
3838
# Shopify Storefront API
3939
SHOPIFY_STORE_DOMAIN="your-store.myshopify.com"
4040
SHOPIFY_STOREFRONT_ACCESS_TOKEN="your-storefront-access-token"
41-
SHOPIFY_ADMIN_ACCESS_TOKEN="your-admin-access-token"
41+
SHOPIFY_ADMIN_ACCESS_TOKEN="your-admin-access-token"
42+
43+
# Cloudinary (for image uploads and management)
44+
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME="your-cloud-name"
45+
CLOUDINARY_API_KEY="your-api-key"
46+
CLOUDINARY_API_SECRET="your-api-secret"

next.config.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,12 @@ const nextConfig = {
8282
},
8383

8484
images: {
85-
domains: [],
85+
domains: ['res.cloudinary.com'],
8686
remotePatterns: [
87+
{
88+
protocol: 'https',
89+
hostname: 'res.cloudinary.com',
90+
},
8791
{
8892
protocol: 'https',
8993
hostname: '**',

package-lock.json

Lines changed: 80 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"ai": "^5.0.93",
4040
"axios": "^1.4.0",
4141
"class-variance-authority": "^0.7.0",
42+
"cloudinary": "^2.8.0",
4243
"clsx": "^1.2.1",
4344
"complexity-report": "^2.0.0-alpha",
4445
"dayjs": "^1.11.4",
@@ -55,6 +56,7 @@
5556
"motion": "^11.13.3",
5657
"next": "15.0.7",
5758
"next-auth": "^4.24.11",
59+
"next-cloudinary": "^6.17.5",
5860
"next-mdx-remote": "^5.0.0",
5961
"next-pwa": "^5.6.0",
6062
"next-react-svg": "^1.1.3",
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* Script to migrate Cloudinary URLs in blog post content (not frontmatter)
3+
*
4+
* This script will:
5+
* 1. Read all blog markdown files
6+
* 2. Find Cloudinary image URLs in the content body
7+
* 3. Extract the public ID from each URL
8+
* 4. Replace the full URL with the public ID
9+
* 5. Show preview or apply changes
10+
*
11+
* Usage:
12+
* node scripts/migrate-blog-content-images.js --dry-run # Preview changes
13+
* node scripts/migrate-blog-content-images.js --apply # Apply changes
14+
*/
15+
16+
const fs = require('fs');
17+
const path = require('path');
18+
19+
const BLOGS_DIR = path.join(process.cwd(), 'src/data/blogs');
20+
const CLOUDINARY_URL_PATTERN =
21+
/https:\/\/res\.cloudinary\.com\/vetswhocode\/image\/upload\/[^"')>\s]+/g;
22+
23+
/**
24+
* Extract public ID from a Cloudinary URL
25+
*/
26+
function extractPublicId(url) {
27+
if (!url || !url.includes('cloudinary.com')) {
28+
return null;
29+
}
30+
31+
try {
32+
// Match pattern: /upload/[transformations]/[version]/[public_id].[extension]
33+
// Keep the extension and version!
34+
const match = url.match(/\/upload\/(?:.*?\/)?(v\d+\/.+)$/);
35+
36+
if (match) {
37+
return match[1];
38+
}
39+
40+
// Fallback: try without version
41+
const fallbackMatch = url.match(/\/upload\/(?:.*?\/)?(.+)$/);
42+
if (fallbackMatch) {
43+
return fallbackMatch[1];
44+
}
45+
46+
return null;
47+
} catch (error) {
48+
console.error('Error extracting public ID from URL:', error);
49+
return null;
50+
}
51+
}
52+
53+
/**
54+
* Process a single blog file
55+
*/
56+
function processBlogFile(filePath, applyChanges = false) {
57+
const content = fs.readFileSync(filePath, 'utf8');
58+
const matches = content.match(CLOUDINARY_URL_PATTERN);
59+
60+
if (!matches || matches.length === 0) {
61+
return { changed: false, file: filePath, reason: 'No Cloudinary URLs in content' };
62+
}
63+
64+
let newContent = content;
65+
const replacements = [];
66+
67+
matches.forEach((url) => {
68+
const publicId = extractPublicId(url);
69+
70+
if (publicId) {
71+
// Replace the full URL with just the public ID
72+
newContent = newContent.replace(url, publicId);
73+
replacements.push({ url, publicId });
74+
}
75+
});
76+
77+
if (replacements.length === 0) {
78+
return {
79+
changed: false,
80+
file: filePath,
81+
reason: 'Could not extract public IDs',
82+
};
83+
}
84+
85+
if (applyChanges) {
86+
fs.writeFileSync(filePath, newContent, 'utf8');
87+
}
88+
89+
return {
90+
changed: true,
91+
file: path.basename(filePath),
92+
replacements,
93+
count: replacements.length,
94+
};
95+
}
96+
97+
/**
98+
* Main migration function
99+
*/
100+
function migrateBlogContentImages(applyChanges = false) {
101+
console.log('\n🔍 Scanning blog posts for in-content Cloudinary URLs...\n');
102+
103+
const files = fs.readdirSync(BLOGS_DIR).filter((f) => f.endsWith('.md'));
104+
const results = {
105+
total: files.length,
106+
changed: [],
107+
unchanged: [],
108+
};
109+
110+
files.forEach((file) => {
111+
const filePath = path.join(BLOGS_DIR, file);
112+
const result = processBlogFile(filePath, applyChanges);
113+
114+
if (result.changed) {
115+
results.changed.push(result);
116+
} else {
117+
results.unchanged.push(result);
118+
}
119+
});
120+
121+
// Display results
122+
console.log('📊 Migration Summary:\n');
123+
console.log(` Total files scanned: ${results.total}`);
124+
console.log(` Files with URLs: ${results.changed.length}`);
125+
console.log(` Files without URLs: ${results.unchanged.length}`);
126+
console.log('');
127+
128+
if (results.changed.length > 0) {
129+
console.log('📝 Files to migrate:\n');
130+
131+
results.changed.forEach((item, index) => {
132+
console.log(`${index + 1}. ${item.file} (${item.count} URLs)`);
133+
item.replacements.slice(0, 2).forEach((r) => {
134+
console.log(` Old: ${r.url.substring(0, 80)}...`);
135+
console.log(` New: ${r.publicId}`);
136+
console.log('');
137+
});
138+
if (item.replacements.length > 2) {
139+
console.log(` ... and ${item.replacements.length - 2} more\n`);
140+
}
141+
});
142+
143+
if (applyChanges) {
144+
console.log('✅ Changes have been applied!\n');
145+
console.log(
146+
'⚠️ IMPORTANT: Review the changes and test your blog posts before committing.\n'
147+
);
148+
} else {
149+
console.log('ℹ️ This is a DRY RUN. No files were modified.');
150+
console.log(
151+
' To apply these changes, run: node scripts/migrate-blog-content-images.js --apply\n'
152+
);
153+
}
154+
} else {
155+
console.log('✅ No Cloudinary URLs found in blog content.\n');
156+
}
157+
158+
// Summary of total replacements
159+
const totalReplacements = results.changed.reduce(
160+
(sum, item) => sum + item.count,
161+
0
162+
);
163+
if (totalReplacements > 0) {
164+
console.log(`📈 Total URLs to replace: ${totalReplacements}\n`);
165+
}
166+
}
167+
168+
// Parse command line arguments
169+
const args = process.argv.slice(2);
170+
const applyChanges = args.includes('--apply');
171+
172+
if (args.length === 0 || args.includes('--dry-run')) {
173+
console.log('🔧 Running in DRY RUN mode...');
174+
migrateBlogContentImages(false);
175+
} else if (applyChanges) {
176+
console.log('⚠️ WARNING: This will modify your blog files!');
177+
console.log(' Make sure you have committed your current changes first.');
178+
console.log('');
179+
console.log('🔧 Applying migrations...');
180+
migrateBlogContentImages(true);
181+
} else {
182+
console.log('Usage:');
183+
console.log(
184+
' node scripts/migrate-blog-content-images.js --dry-run # Preview changes'
185+
);
186+
console.log(
187+
' node scripts/migrate-blog-content-images.js --apply # Apply changes'
188+
);
189+
}

0 commit comments

Comments
 (0)