| 1 |
import { uploadMediaFromUrl } from '@auto-launch/functions/upload-from-url'; |
| 2 |
|
| 3 |
const CONCURRENCY = 3; |
| 4 |
|
| 5 |
// Not the markup: it also carries avatar placeholders the build never chose. |
| 6 |
const placedImages = (pattern) => pattern.images ?? []; |
| 7 |
|
| 8 |
// blogImages can name urls the markup never places, and buildBlogPosts prefers them. |
| 9 |
const importableImages = (pattern) => [ |
| 10 |
...placedImages(pattern), |
| 11 |
...(pattern.blogImages ?? []), |
| 12 |
]; |
| 13 |
|
| 14 |
const sideload = async (urls) => { |
| 15 |
const map = new Map(); |
| 16 |
for (let i = 0; i < urls.length; i += CONCURRENCY) { |
| 17 |
const chunk = urls.slice(i, i + CONCURRENCY); |
| 18 |
const uploaded = await Promise.all( |
| 19 |
chunk.map(async (url) => { |
| 20 |
const fileObj = await uploadMediaFromUrl(url, { |
| 21 |
prefix: 'ext-imported', |
| 22 |
caller: 'importBuiltPagesImages', |
| 23 |
}); |
| 24 |
return [url, fileObj?.url ?? null]; |
| 25 |
}), |
| 26 |
); |
| 27 |
for (const [url, newUrl] of uploaded) { |
| 28 |
if (newUrl) map.set(url, newUrl); |
| 29 |
} |
| 30 |
} |
| 31 |
return map; |
| 32 |
}; |
| 33 |
|
| 34 |
// The build's URLs are signed and expire minutes after launch. |
| 35 |
export const importBuiltPagesImages = async (builtPages) => { |
| 36 |
if (!builtPages?.length) return builtPages; |
| 37 |
|
| 38 |
const urls = [ |
| 39 |
...new Set( |
| 40 |
builtPages.flatMap((page) => |
| 41 |
(page.patterns ?? []).flatMap(importableImages), |
| 42 |
), |
| 43 |
), |
| 44 |
]; |
| 45 |
if (!urls.length) return builtPages; |
| 46 |
|
| 47 |
const map = await sideload(urls); |
| 48 |
if (!map.size) return builtPages; |
| 49 |
|
| 50 |
// A shorter URL that prefixes another would eat it mid-replace. |
| 51 |
const swaps = [...map].sort(([a], [b]) => b.length - a.length); |
| 52 |
const swap = (code) => { |
| 53 |
if (!code) return code; |
| 54 |
let out = code; |
| 55 |
for (const [oldUrl, newUrl] of swaps) { |
| 56 |
out = out.replaceAll(oldUrl, newUrl); |
| 57 |
} |
| 58 |
return out; |
| 59 |
}; |
| 60 |
|
| 61 |
return builtPages.map((page) => ({ |
| 62 |
...page, |
| 63 |
patterns: (page.patterns ?? []).map((pattern) => ({ |
| 64 |
...pattern, |
| 65 |
code: swap(pattern.code), |
| 66 |
// Plugin sections ship a second copy that process-placeholders promotes over code. |
| 67 |
patternReplacementCode: swap(pattern.patternReplacementCode), |
| 68 |
images: pattern.images?.map((url) => map.get(url) ?? url), |
| 69 |
blogImages: pattern.blogImages?.map((url) => map.get(url) ?? url), |
| 70 |
})), |
| 71 |
})); |
| 72 |
}; |
| 73 |
|
| 74 |
// A hero has to stay unique, and a named person's face must not decorate another page. |
| 75 |
const POOL_EXCLUDED_TYPES = new Set(['hero-header', 'team']); |
| 76 |
|
| 77 |
// Durable image URLs for the siteImages pool: blog-section first, so blog posts |
| 78 |
// reuse them card-for-card. |
| 79 |
export const collectBuiltPageImageUrls = (builtPages) => { |
| 80 |
if (!builtPages?.length) return []; |
| 81 |
const reusable = builtPages |
| 82 |
.flatMap((page) => page.patterns ?? []) |
| 83 |
.filter( |
| 84 |
(pattern) => |
| 85 |
!pattern.patternTypes?.some((type) => POOL_EXCLUDED_TYPES.has(type)), |
| 86 |
); |
| 87 |
const ordered = [ |
| 88 |
...reusable.filter((p) => p.patternTypes?.includes('blog-section')), |
| 89 |
...reusable.filter((p) => !p.patternTypes?.includes('blog-section')), |
| 90 |
]; |
| 91 |
return [...new Set(ordered.flatMap(placedImages))]; |
| 92 |
}; |
| 93 |
|