Skip to content

Commit 6062419

Browse files
committed
chore: images
1 parent 383c877 commit 6062419

File tree

316 files changed

+1926
-229
lines changed

Some content is hidden

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

316 files changed

+1926
-229
lines changed

.eslintcache

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

app/components/ProxiedPrismicImage.vue

Lines changed: 24 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@
22
/**
33
* Proxied Prismic Image Component
44
*
5-
* Downloads and serves Prismic images locally to prevent future attacks on Prismic CMS.
6-
* We handle the proxy ourselves by caching images during build time in static environments.
5+
* Serves Prismic images locally to prevent future attacks on Prismic CMS.
6+
* Images are downloaded during build time by the crawler system.
77
*
88
* Adapted from https://github.com/prismicio/prismic-vue/blob/68a8be98a79c4627f83ca33735f07668329fe1e3/src/PrismicImage.vue
99
*/
1010
1111
import type { PrismicImageProps } from '@prismicio/vue'
1212
1313
import { asImagePixelDensitySrcSet, asImageWidthSrcSet, isFilled } from '@prismicio/client'
14-
import { join } from 'pathe'
1514
1615
const props = defineProps<PrismicImageProps>()
1716
const { fallbackAlt, field, widths, alt, pixelDensities, imgixParams } = props
@@ -46,32 +45,23 @@ function castInt(input: string | number | undefined): number | undefined {
4645
if (!isFilled.imageThumbnail(field))
4746
throw new Error('Image is not filled')
4847
49-
const originalFieldUrl = field.url
50-
const mainImage = processImageForLocal(originalFieldUrl)
48+
// Transform field to use local paths
49+
const localField = transformResponsiveImageFieldToLocal(field)
5150
52-
const responsiveImages: Array<{ key: string } & ReturnType<typeof processImageForLocal>> = []
53-
const responsiveViews = ['Lg', 'Md', 'Sm', 'Xs'] as const
54-
55-
for (const viewKey of responsiveViews) {
56-
const responsiveField = (field as any)[viewKey]
57-
if (responsiveField && responsiveField.url) {
58-
const responsive = processImageForLocal(responsiveField.url)
59-
responsiveImages.push({ key: viewKey, ...responsive })
60-
}
61-
}
62-
63-
// asImageWidthSrcSet requires full URLs
51+
// asImageWidthSrcSet requires full URLs, so we use a dummy domain
6452
const DUMMY_DOMAIN = 'https://localhost'
6553
const tempField = {
66-
...field,
67-
url: `${DUMMY_DOMAIN}${mainImage.localPath}`,
54+
...localField,
55+
url: `${DUMMY_DOMAIN}${localField.url}`,
6856
}
6957
70-
for (const responsive of responsiveImages) {
71-
if ((tempField as any)[responsive.key]) {
72-
(tempField as any)[responsive.key] = {
73-
...(tempField as any)[responsive.key],
74-
url: `${DUMMY_DOMAIN}${responsive.localPath}`,
58+
// Transform responsive variants to use dummy domain for srcSet generation
59+
const responsiveViews = ['Lg', 'Md', 'Sm', 'Xs'] as const
60+
for (const viewKey of responsiveViews) {
61+
if ((tempField as any)[viewKey]?.url) {
62+
(tempField as any)[viewKey] = {
63+
...(tempField as any)[viewKey],
64+
url: `${DUMMY_DOMAIN}${(tempField as any)[viewKey].url}`,
7565
}
7666
}
7767
}
@@ -84,16 +74,20 @@ if (widths || !pixelDensities) {
8474
...imgixParams,
8575
widths: widths === 'defaults' ? components?.imageWidthSrcSetDefaults : widths,
8676
})
87-
src = res.src.replace(DUMMY_DOMAIN, '')
88-
srcSet = res.srcset.replaceAll(DUMMY_DOMAIN, '')
77+
if (res) {
78+
src = res.src.replace(DUMMY_DOMAIN, '')
79+
srcSet = res.srcset.replaceAll(DUMMY_DOMAIN, '')
80+
}
8981
}
9082
else if (pixelDensities) {
9183
const res = asImagePixelDensitySrcSet(tempField, {
9284
...imgixParams,
9385
pixelDensities: pixelDensities === 'defaults' ? components?.imagePixelDensitySrcSetDefaults : pixelDensities,
9486
})
95-
src = res.src.replace(DUMMY_DOMAIN, '')
96-
srcSet = res.srcset.replaceAll(DUMMY_DOMAIN, '')
87+
if (res) {
88+
src = res.src.replace(DUMMY_DOMAIN, '')
89+
srcSet = res.srcset.replaceAll(DUMMY_DOMAIN, '')
90+
}
9791
}
9892
9993
const ar = field.dimensions.width / field.dimensions.height
@@ -117,39 +111,8 @@ const image = {
117111
height: Math.round(resolvedHeight),
118112
}
119113
120-
const { isNuxthubPreview, isNuxthubProduction } = useRuntimeConfig().public.environment
121-
const isNuxthub = isNuxthubPreview || isNuxthubProduction
122-
123-
if (import.meta.server && !isNuxthub) {
124-
try {
125-
const { access, writeFile, mkdir } = await import('node:fs/promises')
126-
const { constants } = await import('node:fs')
127-
const { Buffer } = await import('node:buffer')
128-
129-
async function downloadImageIfNeeded(imageInfo: ReturnType<typeof processImageForLocal>) {
130-
const publicFilePath = join(process.cwd(), 'public', imageInfo.localPath)
131-
const publicDir = join(process.cwd(), 'public', imageInfo.localPath.split('/').slice(0, -1).join('/'))
132-
133-
try {
134-
await access(publicFilePath, constants.F_OK)
135-
}
136-
catch {
137-
console.warn(`[ProxiedPrismicImage] Downloading image: ${imageInfo.fileName}`)
138-
await mkdir(publicDir, { recursive: true })
139-
const response = await $fetch(imageInfo.originalUrl, { responseType: 'arrayBuffer' })
140-
await writeFile(publicFilePath, Buffer.from(response as ArrayBuffer))
141-
}
142-
}
143-
144-
await downloadImageIfNeeded(mainImage)
145-
for (const responsiveImage of responsiveImages) {
146-
await downloadImageIfNeeded(responsiveImage)
147-
}
148-
}
149-
catch (error) {
150-
console.error('Failed to download Prismic images:', error)
151-
}
152-
}
114+
// Images are now downloaded during build time by the crawler
115+
// This component only handles URL transformation to local paths
153116
</script>
154117

155118
<template>

lib/crawler.ts

Lines changed: 34 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,10 @@
1-
import type { Query } from '@prismicio/client'
2-
import type { BlogPageDocument, PageDocument } from '~~/prismicio-types'
3-
import { filter } from '@prismicio/client'
4-
import { $fetch } from 'ofetch'
1+
import type { PrerenderPagesOptions } from '../shared/services/prismic-data'
2+
import { getPrismicData } from '../shared/services/prismic-data'
53
import { getBlogMetadata } from '../shared/utils/blog-post'
6-
import { analyzeImageSync, cleanupOrphanedImages, extractImageUrlsFromDocument, logImageSyncStatus } from '../shared/utils/prismic-images'
7-
import { repositoryName } from '../slicemachine.config.json'
84

9-
interface PrerenderPagesOptions {
10-
prismicAccessToken: string
11-
showDrafts?: boolean
12-
}
13-
14-
// Global collections for crawled data
155
const blogPosts: Post[] = []
16-
const allImageUrls: string[] = []
176

18-
// These pages are excluded from the prerender process
7+
// Pages excluded from prerendering
198
export const EXCLUDED_PAGES = [
209
// Custom apps built by other projects
2110
'/vote',
@@ -30,41 +19,42 @@ export const EXCLUDED_PAGES = [
3019
]
3120

3221
export async function getDynamicPages(options: PrerenderPagesOptions) {
33-
const pagesUrl = await buildPrismicUrl('page', options)
34-
const pages = await getPages(pagesUrl)
22+
const data = await getPrismicData(options)
23+
24+
const pages = data.pages.map(({ uid }) => {
25+
if (uid === 'home')
26+
return '/'
27+
return `/${uid}`
28+
})
29+
30+
const blogArticles = data.blogPosts.map(post => `/blog/${post.uid}`)
3531

36-
const blogPostsUrl = await buildPrismicUrl('blog_page', options)
37-
const blogArticles = await getBlogPosts(blogPostsUrl).then(posts => posts.map(post => `/blog/${post.slug}`))
32+
const blogPaginationRoutes = generateBlogPaginationRoutes(data.blogPosts.length)
3833

39-
const blogPaginationRoutes = await getBlogPaginationRoutes(blogPostsUrl)
34+
blogPosts.length = 0
35+
data.blogPosts.forEach((post) => {
36+
const { titleText: title, url, abstract: description, prose: content, date, imageURL: image, authors } = getBlogMetadata(post)
37+
blogPosts.push({ title, url, description, content, date, image, slug: post.uid, authors })
38+
})
4039

4140
return [...pages, ...blogArticles, ...blogPaginationRoutes].filter(page => !EXCLUDED_PAGES.includes(page))
4241
}
4342

44-
async function getPages(url: URL) {
45-
const prerenderPaths: string[] = []
46-
let page: number = 1
47-
while (true) {
48-
url.searchParams.set('page', page.toString())
49-
const { next_page, results } = await $fetch<Query<PageDocument>>(url.href)
50-
prerenderPaths.push(...results.map(({ uid }) => {
51-
if (uid === 'home')
52-
return '/'
53-
return `/${uid}`
54-
}))
55-
56-
// Extract images from page documents
57-
results.forEach((result) => {
58-
const documentImages = extractImageUrlsFromDocument(result)
59-
allImageUrls.push(...documentImages)
60-
})
43+
function generateBlogPaginationRoutes(totalPosts: number): string[] {
44+
const postsPerPage = 30
45+
const totalPages = Math.ceil(totalPosts / postsPerPage)
46+
const paginationRoutes: string[] = []
6147

62-
if (next_page === null)
63-
break
64-
page++
48+
for (let i = 1; i <= totalPages; i++) {
49+
if (i === 1) {
50+
paginationRoutes.push('/blog')
51+
}
52+
else {
53+
paginationRoutes.push(`/blog?page=${i}`)
54+
}
6555
}
6656

67-
return prerenderPaths
57+
return paginationRoutes
6858
}
6959

7060
interface Post {
@@ -78,101 +68,12 @@ interface Post {
7868
authors?: string[]
7969
}
8070

81-
export async function getBlogPosts(url: URL) {
82-
if (blogPosts.length > 0)
71+
export async function getBlogPosts(): Promise<Post[]> {
72+
if (blogPosts.length > 0) {
8373
return blogPosts
84-
let page: number = 1
85-
while (true) {
86-
url.searchParams.set('page', page.toString())
87-
const { next_page, results } = await $fetch<Query<BlogPageDocument>>(url.href)
88-
results.forEach((result) => {
89-
const { titleText: title, url, abstract: description, prose: content, date, imageURL: image, authors } = getBlogMetadata(result)
90-
blogPosts.push({ title, url, description, content, date, image, slug: result.uid, authors })
91-
92-
// Extract all image URLs from the document
93-
const documentImages = extractImageUrlsFromDocument(result)
94-
allImageUrls.push(...documentImages)
95-
})
96-
97-
if (next_page === null)
98-
break
99-
page++
10074
}
10175

76+
// Should not happen if getDynamicPages was called first
77+
console.warn('⚠️ getBlogPosts called before getDynamicPages - this may cause duplicate API calls')
10278
return blogPosts
10379
}
104-
105-
export async function getBlogPaginationRoutes(url: URL) {
106-
const paginationRoutes: string[] = []
107-
108-
// Get the first page to determine total number of posts
109-
url.searchParams.set('page', '1')
110-
const firstPageResult = await $fetch<Query<BlogPageDocument>>(url.href)
111-
const totalPages = firstPageResult.total_pages || 1
112-
113-
// Generate pagination routes
114-
for (let i = 1; i <= totalPages; i++) {
115-
if (i === 1) {
116-
// First page is just /blog
117-
paginationRoutes.push('/blog')
118-
}
119-
else {
120-
// Other pages are /blog?page=2, /blog?page=3, etc.
121-
paginationRoutes.push(`/blog?page=${i}`)
122-
}
123-
}
124-
125-
return paginationRoutes
126-
}
127-
128-
const prismicUrl = new URL(`https://${repositoryName}.cdn.prismic.io`)
129-
130-
interface RefsResponse { refs: { id: 'master', ref: string }[] }
131-
let ref: string
132-
133-
export async function buildPrismicUrl(documentType: 'blog_page' | 'page', { prismicAccessToken, showDrafts = false }: PrerenderPagesOptions) {
134-
if (!ref) {
135-
const refsUrl = new URL('/api/v2', prismicUrl)
136-
refsUrl.searchParams.set('access_token', prismicAccessToken)
137-
138-
const refsResponse = await $fetch<RefsResponse>(refsUrl.href)
139-
const _ref = refsResponse?.refs.find(({ id }) => id === 'master')?.ref
140-
if (!_ref)
141-
throw new Error('Could not find master ref')
142-
ref = _ref
143-
}
144-
145-
const searchUrl = new URL('/api/v2/documents/search', prismicUrl)
146-
147-
// Add routes for blog posts
148-
const documentTypeQuery = `[at(document.type,"${documentType}")]`
149-
150-
// Apply the draft filter only when we don't want to show drafts
151-
let filtering = ''
152-
if (!showDrafts)
153-
filtering = filter.not(`my.${documentType}.draft`, true)
154-
155-
searchUrl.searchParams.set('q', `[${documentTypeQuery}${filtering}]`)
156-
searchUrl.searchParams.set('pageSize', '100') // 100 is the maximum
157-
searchUrl.searchParams.set('ref', ref!)
158-
searchUrl.searchParams.set('access_token', prismicAccessToken)
159-
160-
return searchUrl
161-
}
162-
163-
export function getAllImageUrls(): string[] {
164-
return [...new Set(allImageUrls)]
165-
}
166-
167-
export async function performImageAnalysis(): Promise<void> {
168-
const imageUrls = getAllImageUrls()
169-
console.warn(`\n🔍 Analyzing ${imageUrls.length} unique Prismic images...`)
170-
171-
const status = await analyzeImageSync(imageUrls)
172-
logImageSyncStatus(status)
173-
174-
if (status.orphaned.length > 0) {
175-
console.warn('\n🧹 Cleaning up orphaned images...')
176-
await cleanupOrphanedImages(status)
177-
}
178-
}

0 commit comments

Comments
 (0)