Repository: opennextjs/opennextjs-cloudflare
Pull request #551: Set "cache-tag" on the regional cache entries
Author: login: SamyPesse
Created: 2025-04-08T10:25:55Z | Merged: 2025-04-09T09:00:21Z

This PR aims at improving the use of the regional cache by making sure that entries on it can be purged by cache-tag on Cloudflare.

In our application (https://github.com/GitbookIO/gitbook), we purge the cache both using `revalidateTag` but also using the Cloudflare API (https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-tags/).

I think this header has no negative impact on any other OpenNext users while unblocking us but also potentially unblocking someone who is using the long-lived cache and needs to quickly purge it (they could do it from the CF dashboard).


Diff:
diff --git a/.changeset/happy-bananas-buy.md b/.changeset/happy-bananas-buy.md
new file mode 100644
index 000000000..35ec8d3d5
--- /dev/null
+++ b/.changeset/happy-bananas-buy.md
@@ -0,0 +1,5 @@
+---
+"@opennextjs/cloudflare": patch
+---
+
+Set `Cache-Tag` on the entries created by the regional cache, to be purged using the Cloudflare API or dashboard.
diff --git a/packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts b/packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
index 8c8014130..838063e24 100644
--- a/packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
+++ b/packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
@@ -18,6 +18,14 @@ type Options = {
    */
   mode: "short-lived" | "long-lived";
 
+  /**
+   * The default TTL of long-lived cache entries.
+   * When no revalidate is provided, the default age will be used.
+   *
+   * @default `THIRTY_MINUTES_IN_SECONDS`
+   */
+  defaultLongLivedTtlSec?: number;
+
   /**
    * Whether the regional cache entry should be updated in the background or not when it experiences
    * a cache hit.
@@ -27,6 +35,12 @@ type Options = {
   shouldLazilyUpdateOnCacheHit?: boolean;
 };
 
+interface PutToCacheInput {
+  key: string;
+  isFetch: boolean | undefined;
+  entry: IncrementalCacheEntry<boolean>;
+}
+
 /**
  * Wrapper adding a regional cache on an `IncrementalCache` implementation
  */
@@ -52,10 +66,10 @@ class RegionalCache implements IncrementalCache {
   ): Promise<WithLastModified<CacheValue<IsFetch>> | null> {
     try {
       const cache = await this.getCacheInstance();
-      const localCacheKey = this.getCacheKey(key, isFetch);
+      const urlKey = this.getCacheUrlKey(key, isFetch);
 
       // Check for a cached entry as this will be faster than the store response.
-      const cachedResponse = await cache.match(localCacheKey);
+      const cachedResponse = await cache.match(urlKey);
       if (cachedResponse) {
         debugCache("Get - cached response");
 
@@ -66,7 +80,7 @@ class RegionalCache implements IncrementalCache {
               const { value, lastModified } = rawEntry ?? {};
 
               if (value && typeof lastModified === "number") {
-                await this.putToCache(localCacheKey, { value, lastModified });
+                await this.putToCache({ key, isFetch, entry: { value, lastModified } });
               }
             })
           );
@@ -80,7 +94,7 @@ class RegionalCache implements IncrementalCache {
       if (!value || typeof lastModified !== "number") return null;
 
       // Update the locale cache after retrieving from the store.
-      getCloudflareContext().ctx.waitUntil(this.putToCache(localCacheKey, { value, lastModified }));
+      getCloudflareContext().ctx.waitUntil(this.putToCache({ key, isFetch, entry: { value, lastModified } }));
 
       return { value, lastModified };
     } catch (e) {
@@ -97,11 +111,15 @@ class RegionalCache implements IncrementalCache {
     try {
       await this.store.set(key, value, isFetch);
 
-      await this.putToCache(this.getCacheKey(key, isFetch), {
-        value,
-        // Note: `Date.now()` returns the time of the last IO rather than the actual time.
-        //       See https://developers.cloudflare.com/workers/reference/security-model/
-        lastModified: Date.now(),
+      await this.putToCache({
+        key,
+        isFetch,
+        entry: {
+          value,
+          // Note: `Date.now()` returns the time of the last IO rather than the actual time.
+          //       See https://developers.cloudflare.com/workers/reference/security-model/
+          lastModified: Date.now(),
+        },
       });
     } catch (e) {
       error(`Failed to get from regional cache`, e);
@@ -113,7 +131,7 @@ class RegionalCache implements IncrementalCache {
       await this.store.delete(key);
 
       const cache = await this.getCacheInstance();
-      await cache.delete(this.getCacheKey(key));
+      await cache.delete(this.getCacheUrlKey(key));
     } catch (e) {
       error("Failed to delete from regional cache", e);
     }
@@ -126,27 +144,36 @@ class RegionalCache implements IncrementalCache {
     return this.localCache;
   }
 
-  protected getCacheKey(key: string, isFetch?: boolean) {
-    return new Request(
-      new URL(
-        `${process.env.NEXT_BUILD_ID ?? FALLBACK_BUILD_ID}/${key}.${isFetch ? "fetch" : "cache"}`,
-        "http://cache.local"
-      )
+  protected getCacheUrlKey(key: string, isFetch?: boolean) {
+    const buildId = process.env.NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
+    return (
+      "http://cache.local" + `/${buildId}/${key}`.replace(/\/+/g, "/") + `.${isFetch ? "fetch" : "cache"}`
     );
   }
 
-  protected async putToCache(key: Request, entry: IncrementalCacheEntry<boolean>): Promise<void> {
+  protected async putToCache({ key, isFetch, entry }: PutToCacheInput): Promise<void> {
+    const urlKey = this.getCacheUrlKey(key, isFetch);
     const cache = await this.getCacheInstance();
 
     const age =
       this.opts.mode === "short-lived"
         ? ONE_MINUTE_IN_SECONDS
-        : entry.value.revalidate || THIRTY_MINUTES_IN_SECONDS;
+        : entry.value.revalidate || this.opts.defaultLongLivedTtlSec || THIRTY_MINUTES_IN_SECONDS;
 
+    // We default to the entry key if no tags are found.
+    // so that we can also revalidate page router based entry this way.
+    const tags = getTagsFromCacheEntry(entry) ?? [key];
     await cache.put(
-      key,
+      urlKey,
       new Response(JSON.stringify(entry), {
-        headers: new Headers({ "cache-control": `max-age=${age}` }),
+        headers: new Headers({
+          "cache-control": `max-age=${age}`,
+          ...(tags.length > 0
+            ? {
+                "cache-tag": tags.join(","),
+              }
+            : {}),
+        }),
       })
     );
   }
@@ -169,9 +196,33 @@ class RegionalCache implements IncrementalCache {
  *                                  or an ISR/SSG entry for up to 30 minutes.
  * @param opts.shouldLazilyUpdateOnCacheHit Whether the regional cache entry should be updated in
  *                                          the background or not when it experiences a cache hit.
+ * @param opts.defaultLongLivedTtlSec The default age to use for long-lived cache entries.
+ *                                  When no revalidate is provided, the default age will be used.
+ *                                  @default `THIRTY_MINUTES_IN_SECONDS`
  *
  * @default `false` for the `short-lived` mode, and `true` for the `long-lived` mode.
  */
 export function withRegionalCache(cache: IncrementalCache, opts: Options) {
   return new RegionalCache(cache, opts);
 }
+
+/**
+ * Extract the list of tags from a cache entry.
+ */
+function getTagsFromCacheEntry(entry: IncrementalCacheEntry<boolean>): string[] | undefined {
+  if ("tags" in entry.value && entry.value.tags) {
+    return entry.value.tags;
+  }
+
+  if (
+    "meta" in entry.value &&
+    entry.value.meta &&
+    "headers" in entry.value.meta &&
+    entry.value.meta.headers
+  ) {
+    const rawTags = entry.value.meta.headers["x-next-cache-tags"];
+    if (typeof rawTags === "string") {
+      return rawTags.split(",");
+    }
+  }
+}


Reviews:
user: login: conico974
body: Ideally, we'd want the purge to be automatic.
This means we'd have another override for the tag cache that would work in the same way as this one (by wrapping the tag cache used) and that would call cloudflare api to purge cache entry automatically on `writeTags`.
We'd also want to provide this https://opennext.js.org/aws/config/overrides/automatic_cdn_invalidation so that it would work for `res.revalidate` from page router as well.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T12:29:23Z
user: login: SamyPesse
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T16:35:27Z
user: login: SamyPesse
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T16:37:53Z
user: login: conico974
body: I just made 2 small comments but other than that, it looks good
> It makes sense, I guess it can be done in another PR?

Yeah we can do it in another PR later.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T17:18:07Z
user: login: conico974
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T17:19:39Z
user: login: SamyPesse
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T17:28:31Z
user: login: SamyPesse
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T17:28:47Z
user: login: conico974
body: LGTM Thanks
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-08T17:37:02Z
user: login: vicb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T19:27:06Z
user: login: vicb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T19:28:28Z
user: login: vicb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T19:43:06Z
user: login: vicb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T19:45:40Z
user: login: vicb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T19:51:54Z
user: login: vicb
body: Let's merge once the comments are resolved.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T19:59:13Z
user: login: conico974
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T20:09:59Z
user: login: SamyPesse
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T20:20:29Z
user: login: SamyPesse
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T20:20:41Z

Review comments:
diff_hunk: @@ -175,3 +183,25 @@ class RegionalCache implements IncrementalCache {
 export function withRegionalCache(cache: IncrementalCache, opts: Options) {
   return new RegionalCache(cache, opts);
 }
+
+/**
+ * Extract the list of tags from a cache entry.
+ */
+export function getTagsFromCacheEntry(entry: IncrementalCacheEntry<boolean>): string[] {
+  if ("tags" in entry.value && entry.value.tags) {
+    return entry.value.tags;
+  }
+
+  if (
+    "meta" in entry.value &&
+    entry.value.meta &&
+    "headers" in entry.value.meta &&
+    entry.value.meta.headers
+  ) {
+    const rawTags = entry.value.meta.headers["x-next-cache-tags"];
+    if (typeof rawTags === "string") {
+      return rawTags.split(",");
+    }
+  }
+  return [];
path: packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
user: login: conico974
body: If we don't find tags here, we probably want to return the original key here so that we could also revalidate page router based entry this way.
created_at: 2025-04-08T12:06:34Z
author_association: COLLABORATOR
diff_hunk: @@ -175,3 +183,25 @@ class RegionalCache implements IncrementalCache {
 export function withRegionalCache(cache: IncrementalCache, opts: Options) {
path: packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
user: login: conico974
body: We should add an option here to change the ttl of the cache from the default 30 minutes if we're adding cache tag (default would stay at 30 mins)
created_at: 2025-04-08T12:10:34Z
author_association: COLLABORATOR
diff_hunk: @@ -175,3 +183,25 @@ class RegionalCache implements IncrementalCache {
 export function withRegionalCache(cache: IncrementalCache, opts: Options) {
path: packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
user: login: SamyPesse
body: Agree, I'll add one, it's just a bit redundant with `mode`, it could have been nice to have instead:

```
maxAge: 'short-lived' | 'long-lived' | number;
```

For now, I'll add a `defaultLongLivedAge`
created_at: 2025-04-08T16:35:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -175,3 +183,25 @@ class RegionalCache implements IncrementalCache {
 export function withRegionalCache(cache: IncrementalCache, opts: Options) {
   return new RegionalCache(cache, opts);
 }
+
+/**
+ * Extract the list of tags from a cache entry.
+ */
+export function getTagsFromCacheEntry(entry: IncrementalCacheEntry<boolean>): string[] {
+  if ("tags" in entry.value && entry.value.tags) {
+    return entry.value.tags;
+  }
+
+  if (
+    "meta" in entry.value &&
+    entry.value.meta &&
+    "headers" in entry.value.meta &&
+    entry.value.meta.headers
+  ) {
+    const rawTags = entry.value.meta.headers["x-next-cache-tags"];
+    if (typeof rawTags === "string") {
+      return rawTags.split(",");
+    }
+  }
+  return [];
path: packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
user: login: SamyPesse
body: I've done it in https://github.com/opennextjs/opennextjs-cloudflare/pull/551/commits/cedca1c3dea5ae90a66f8f964188c3fdd8e3f346

Let me know if I understood correctly as I'm not sure how the page router works exactly.
created_at: 2025-04-08T16:37:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,6 +18,14 @@ type Options = {
    */
   mode: "short-lived" | "long-lived";
 
+  /**
+   * The default age to use for long-lived cache entries.
+   * When no revalidate is provided, the default age will be used.
+   *
+   * @default `THIRTY_MINUTES_IN_SECONDS`
+   */
+  defaultLongLivedAge?: number;
path: packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
user: login: conico974
body: We use ttl in other places, for consistency we should use something similar here, something like `defaultLongLivedTtlSec`
created_at: 2025-04-08T17:08:57Z
author_association: COLLABORATOR
diff_hunk: @@ -135,18 +143,32 @@ class RegionalCache implements IncrementalCache {
     );
   }
 
-  protected async putToCache(key: Request, entry: IncrementalCacheEntry<boolean>): Promise<void> {
+  protected async putToCache(
+    key: Request,
path: packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
user: login: conico974
body: For readability i think it would be better to have an object instead of 3 params to this function, something like 
```ts
interface PutToCacheOpts {
  requestKey: string,
  entryKey: string,
  entry: IncrementalCacheEntry<boolean>
}
```
created_at: 2025-04-08T17:12:48Z
author_association: COLLABORATOR
diff_hunk: @@ -175,3 +183,25 @@ class RegionalCache implements IncrementalCache {
 export function withRegionalCache(cache: IncrementalCache, opts: Options) {
   return new RegionalCache(cache, opts);
 }
+
+/**
+ * Extract the list of tags from a cache entry.
+ */
+export function getTagsFromCacheEntry(entry: IncrementalCacheEntry<boolean>): string[] {
+  if ("tags" in entry.value && entry.value.tags) {
+    return entry.value.tags;
+  }
+
+  if (
+    "meta" in entry.value &&
+    entry.value.meta &&
+    "headers" in entry.value.meta &&
+    entry.value.meta.headers
+  ) {
+    const rawTags = entry.value.meta.headers["x-next-cache-tags"];
+    if (typeof rawTags === "string") {
+      return rawTags.split(",");
+    }
+  }
+  return [];
path: packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
user: login: conico974
body: That's basically what i had in mind yeah
created_at: 2025-04-08T17:19:39Z
author_association: COLLABORATOR
diff_hunk: @@ -18,6 +18,14 @@ type Options = {
    */
   mode: "short-lived" | "long-lived";
 
+  /**
+   * The default age to use for long-lived cache entries.
+   * When no revalidate is provided, the default age will be used.
+   *
+   * @default `THIRTY_MINUTES_IN_SECONDS`
+   */
+  defaultLongLivedAge?: number;
path: packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
user: login: SamyPesse
body: Makes sense, I just changed it
created_at: 2025-04-08T17:28:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -135,18 +143,32 @@ class RegionalCache implements IncrementalCache {
     );
   }
 
-  protected async putToCache(key: Request, entry: IncrementalCacheEntry<boolean>): Promise<void> {
+  protected async putToCache(
+    key: Request,
path: packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
user: login: SamyPesse
body: Agree, I changed it
created_at: 2025-04-08T17:28:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,5 @@
+---
+"@opennextjs/cloudflare": patch
+---
+
+Set `Cache-Tag` on the entries created by the regional cache, to be purged using the Cloudflare API.
path: .changeset/happy-bananas-buy.md
user: login: vicb
body: ```suggestion
Set `Cache-Tag` on the entries created by the regional cache, to be purged using the Cloudflare API or dashboard.
```
created_at: 2025-04-08T19:27:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,6 +18,14 @@ type Options = {
    */
   mode: "short-lived" | "long-lived";
 
+  /**
+   * The default age to use for long-lived cache entries.
path: packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
user: login: vicb
body: ```suggestion
   * The default TTL of long-lived cache entries.
```

"When no revalidate is provided, the default age will be used." does not match the logic,
could you please update
created_at: 2025-04-08T19:28:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -135,18 +155,28 @@ class RegionalCache implements IncrementalCache {
     );
   }
 
-  protected async putToCache(key: Request, entry: IncrementalCacheEntry<boolean>): Promise<void> {
+  protected async putToCache({ requestKey, entryKey, entry }: PutToCacheInput): Promise<void> {
     const cache = await this.getCacheInstance();
 
     const age =
       this.opts.mode === "short-lived"
         ? ONE_MINUTE_IN_SECONDS
-        : entry.value.revalidate || THIRTY_MINUTES_IN_SECONDS;
+        : entry.value.revalidate || this.opts.defaultLongLivedTtlSec || THIRTY_MINUTES_IN_SECONDS;
path: packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
user: login: vicb
body: Shouldn't the TTL be longer when revalidate = false?
created_at: 2025-04-08T19:43:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,6 +35,12 @@ type Options = {
   shouldLazilyUpdateOnCacheHit?: boolean;
 };
 
+interface PutToCacheInput {
+  requestKey: Request;
path: packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
user: login: vicb
body: Maybe pass `entryKey` and `isFetch` instead of `requestKey`?

`requestKey` could be changed to a string, would be nice to apply `.replaceAll(/\/+/, "/")` as we do in other cache handler because keys start with '/'
created_at: 2025-04-08T19:45:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -169,9 +199,33 @@ class RegionalCache implements IncrementalCache {
  *                                  or an ISR/SSG entry for up to 30 minutes.
  * @param opts.shouldLazilyUpdateOnCacheHit Whether the regional cache entry should be updated in
  *                                          the background or not when it experiences a cache hit.
+ * @param opts.defaultLongLivedTtlSec The default age to use for long-lived cache entries.
+ *                                  When no revalidate is provided, the default age will be used.
+ *                                  @default `THIRTY_MINUTES_IN_SECONDS`
  *
  * @default `false` for the `short-lived` mode, and `true` for the `long-lived` mode.
  */
 export function withRegionalCache(cache: IncrementalCache, opts: Options) {
   return new RegionalCache(cache, opts);
 }
+
+/**
+ * Extract the list of tags from a cache entry.
+ */
+export function getTagsFromCacheEntry(entry: IncrementalCacheEntry<boolean>): string[] | undefined {
path: packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
user: login: vicb
body: Does this needs to be exported?
created_at: 2025-04-08T19:51:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -135,18 +155,28 @@ class RegionalCache implements IncrementalCache {
     );
   }
 
-  protected async putToCache(key: Request, entry: IncrementalCacheEntry<boolean>): Promise<void> {
+  protected async putToCache({ requestKey, entryKey, entry }: PutToCacheInput): Promise<void> {
     const cache = await this.getCacheInstance();
 
     const age =
       this.opts.mode === "short-lived"
         ? ONE_MINUTE_IN_SECONDS
-        : entry.value.revalidate || THIRTY_MINUTES_IN_SECONDS;
+        : entry.value.revalidate || this.opts.defaultLongLivedTtlSec || THIRTY_MINUTES_IN_SECONDS;
path: packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
user: login: conico974
body: No at the moment there is no revalidate value at all for ISR/SSG route, so as a default we fallback to a smaller value
created_at: 2025-04-08T20:09:59Z
author_association: COLLABORATOR
diff_hunk: @@ -27,6 +35,12 @@ type Options = {
   shouldLazilyUpdateOnCacheHit?: boolean;
 };
 
+interface PutToCacheInput {
+  requestKey: Request;
path: packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
user: login: SamyPesse
body: Changed
created_at: 2025-04-08T20:20:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -169,9 +199,33 @@ class RegionalCache implements IncrementalCache {
  *                                  or an ISR/SSG entry for up to 30 minutes.
  * @param opts.shouldLazilyUpdateOnCacheHit Whether the regional cache entry should be updated in
  *                                          the background or not when it experiences a cache hit.
+ * @param opts.defaultLongLivedTtlSec The default age to use for long-lived cache entries.
+ *                                  When no revalidate is provided, the default age will be used.
+ *                                  @default `THIRTY_MINUTES_IN_SECONDS`
  *
  * @default `false` for the `short-lived` mode, and `true` for the `long-lived` mode.
  */
 export function withRegionalCache(cache: IncrementalCache, opts: Options) {
   return new RegionalCache(cache, opts);
 }
+
+/**
+ * Extract the list of tags from a cache entry.
+ */
+export function getTagsFromCacheEntry(entry: IncrementalCacheEntry<boolean>): string[] | undefined {
path: packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
user: login: SamyPesse
body: No, I removed the `export`
created_at: 2025-04-08T20:20:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-08T10:25:59Z
body: ###  🦋  Changeset detected

Latest commit: 54b0ed8f908ca4b284833814943b224635f5e024

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                   | Type  |
| ---------------------- | ----- |
| @opennextjs/cloudflare | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/SamyPesse/opennextjs-cloudflare/new/regional-cache-tags?filename=.changeset/lucky-knives-accept.md&value=---%0A%22%40opennextjs%2Fcloudflare%22%3A%20patch%0A---%0A%0ASet%20%22cache-tag%22%20on%20the%20regional%20cache%20entries%0A)


author_association: NONE
user: login: pkg-pr-new[bot]
created_at: 2025-04-08T10:49:22Z
body: 
[Open in StackBlitz](https://pkg.pr.new/template/7d17266f-75b3-4174-9901-c6bd3355e0fa)


```
pnpm add https://pkg.pr.new/@opennextjs/cloudflare@551
```


_commit: <a href="https://github.com/opennextjs/opennextjs-cloudflare/runs/40230704774"><code>54b0ed8</code></a>_

author_association: NONE
user: login: SamyPesse
created_at: 2025-04-08T16:39:17Z
body: > Ideally, we'd want the purge to be automatic.
This means we'd have another override for the tag cache that would work in the same way as this one (by wrapping the tag cache used) and that would call cloudflare api to purge cache entry automatically on writeTags.
We'd also want to provide this https://opennext.js.org/aws/config/overrides/automatic_cdn_invalidation so that it would work for res.revalidate from page router as well.

It makes sense, I guess it can be done in another PR?

@conico974 I've made the other suggested changes, let me know if they match what you had in mind.
author_association: CONTRIBUTOR
user: login: vicb
created_at: 2025-04-08T19:56:38Z
body: Thanks @SamyPesse for the PR, could you please prepare a PR for the docs at https://github.com/opennextjs/docs

@SamyPesse or @conico974 could you please create a feature request for the follow up.

Thanks!




author_association: CONTRIBUTOR
user: login: SamyPesse
created_at: 2025-04-08T20:21:39Z
body: Thanks @vicb and @conico974, I'll merge it and look at making a PR on the docs (It'd be great to have it hosted on GitBook :D )

EDIT: I can't merge, so one of you will have to do it if changes are good for you
author_association: CONTRIBUTOR
user: login: SamyPesse
created_at: 2025-04-08T20:24:00Z
body: Actually, for the documentation, it could be better to wait for https://github.com/opennextjs/opennextjs-cloudflare/issues/555 before writing it as by itself this PR doesn't make it a standalone solution for most people using Opennext.
author_association: CONTRIBUTOR
user: login: vicb
created_at: 2025-04-08T20:33:19Z
body: I added one last commit + rebase, let me know if this looks good
author_association: CONTRIBUTOR
user: login: SamyPesse
created_at: 2025-04-08T21:29:08Z
body: Perfect! thanks @vicb 
author_association: CONTRIBUTOR