diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts index 06d42a23e534..4589d0b2743f 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -121,6 +121,11 @@ export function createCollectionToGlobResultMap({ return collectionToGlobResultMap; } +const cacheHintSchema = z.object({ + tags: z.array(z.string()).optional(), + maxAge: z.number().optional(), +}); + async function parseLiveEntry( entry: LiveDataEntry, schema: z.ZodType, @@ -137,6 +142,21 @@ async function parseLiveEntry( ), }); } + if (entry.cacheHint) { + const cacheHint = cacheHintSchema.safeParse(entry.cacheHint); + + if (!cacheHint.success) { + throw new AstroError({ + ...AstroErrorData.InvalidCacheHintError, + message: AstroErrorData.InvalidCacheHintError.message( + collection, + entry.id, + cacheHint.error, + ), + }); + } + entry.cacheHint = cacheHint.data; + } return { ...entry, data: parsed.data, @@ -184,6 +204,22 @@ export function createGetCollection({ ); } + if (response.cacheHint) { + const cacheHint = cacheHintSchema.safeParse(response.cacheHint); + + if (!cacheHint.success) { + throw new AstroError({ + ...AstroErrorData.InvalidCacheHintError, + message: AstroErrorData.InvalidCacheHintError.message( + collection, + undefined, + cacheHint.error, + ), + }); + } + response.cacheHint = cacheHint.data; + } + return { ...response, collection, diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 2319c4cb5069..016648da3229 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -1614,8 +1614,9 @@ export const InvalidContentEntryDataError = { title: 'Content entry data does not match schema.', message(collection: string, entryId: string, error: ZodError) { return [ - `**${String(collection)} → ${String(entryId)}** data does not match collection schema.`, - ...error.errors.map((zodError) => zodError.message), + `**${String(collection)} → ${String(entryId)}** data does not match collection schema.\n`, + ...error.errors.map((zodError) => ` **${zodError.path.join('.')}**: ${zodError.message}`), + '', ].join('\n'); }, hint: 'See https://docs.astro.build/en/guides/content-collections/ for more information on content schemas.', @@ -1665,13 +1666,39 @@ export const ContentEntryDataError = { title: 'Content entry data does not match schema.', message(collection: string, entryId: string, error: ZodError) { return [ - `**${String(collection)} → ${String(entryId)}** data does not match collection schema.`, - ...error.errors.map((zodError) => zodError.message), + `**${String(collection)} → ${String(entryId)}** data does not match collection schema.\n`, + ...error.errors.map((zodError) => ` **${zodError.path.join('.')}**: ${zodError.message}`), + '', ].join('\n'); }, hint: 'See https://docs.astro.build/en/guides/content-collections/ for more information on content schemas.', } satisfies ErrorData; +/** + * @docs + * @message + * **Example error message:**
+ * **blog** → **post** returned an invalid cache hint.
+ * **maxAge**: Expected number, received string + * @description + * The loader for a live content collection returned an invalid cache hint. + * Make sure that `cacheHint` is an object with the correct properties, or is undefined. + * @see + * - [Experimental live content](https://astro.build/en/reference/experimental-flags/live-content/) + */ +export const InvalidCacheHintError = { + name: 'InvalidCacheHintError', + title: 'Invalid cache hint.', + message(collection: string, entryId: string | undefined, error: ZodError) { + return [ + `**${String(collection)}${entryId ? ` → ${String(entryId)}` : ''}** returned an invalid cache hint.\n`, + ...error.errors.map((zodError) => ` **${zodError.path.join('.')}**: ${zodError.message}`), + '', + ].join('\n'); + }, + hint: 'See https://docs.astro.build/en/reference/experimental-flags/live-content/ for more information.', +} satisfies ErrorData; + /** * @docs * @message diff --git a/packages/astro/src/types/public/content.ts b/packages/astro/src/types/public/content.ts index 9810ac1c480b..b1fac5712c38 100644 --- a/packages/astro/src/types/public/content.ts +++ b/packages/astro/src/types/public/content.ts @@ -125,16 +125,26 @@ export interface DataEntryType { export type GetDataEntryInfoReturnType = { data: Record; rawData?: string }; +export interface CacheHint { + /** Cache tags */ + tags?: Array; + /** Maximum age of the response in seconds */ + maxAge?: number; +} + export interface LiveDataEntry = Record> { /** The ID of the entry. Unique per collection. */ id: string; /** The parsed entry data */ data: TData; + /** A hint for how to cache this entry */ + cacheHint?: CacheHint; } export interface LiveDataCollection< TData extends Record = Record, > { entries: Array>; - // TODO: pagination etc. + /** A hint for how to cache this collection. Individual entries can also have cache hints */ + cacheHint?: CacheHint; } diff --git a/packages/astro/test/fixtures/live-loaders/src/live.config.ts b/packages/astro/test/fixtures/live-loaders/src/live.config.ts index c44259114788..70f132ac39e0 100644 --- a/packages/astro/test/fixtures/live-loaders/src/live.config.ts +++ b/packages/astro/test/fixtures/live-loaders/src/live.config.ts @@ -14,11 +14,24 @@ const entries = { const loader: LiveLoader = { name: 'test-loader', loadEntry: async (context) => { - return entries[context.filter.id] || null; + if(!entries[context.filter.id]) { + return; + } + return { + ...entries[context.filter.id], + cacheHint: { + tags: [`page:${context.filter.id}`], + maxAge: 60, + }, + }; }, loadCollection: async (context) => { return { entries: Object.values(entries), + cacheHint: { + tags: ['page'], + maxAge: 60, + }, }; }, };