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,
+ },
};
},
};