Skip to content

Create WP-Revalidate Next.js Plugin! #51

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
WORDPRESS_URL="https://wordpress.com"
WORDPRESS_HOSTNAME="wordpress.com"

# If using the revalidate plugin
# You can generate by running `openssl rand -base64 32` in the terminal
WORDPRESS_WEBHOOK_SECRET="your-secret-key-here"
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

CLAUDE.md
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -433,15 +433,15 @@ The WordPress API functions use a hierarchical cache tag system:

1. **Install the WordPress Plugin:**

- Navigate to `wordpress/next-revalidate/`
- Create a zip file of the folder
- Navigate to the `/plugin` directory
- Use the pre-built `next-revalidate.zip` file or create a ZIP from the `next-revalidate` folder
- Install and activate through WordPress admin
- Go to Settings > Next.js Revalidation
- Configure your Next.js URL and webhook secret

2. **Configure Next.js:**

- Add `WORDPRESS_WEBHOOK_SECRET` to your environment variables
- Add `WORDPRESS_WEBHOOK_SECRET` to your environment variables (same secret as in WordPress plugin)
- The webhook endpoint at `/api/revalidate` is already set up
- No additional configuration needed

Expand All @@ -451,6 +451,16 @@ The WordPress API functions use a hierarchical cache tag system:
- Next.js automatically revalidates the appropriate cache tags
- Only affected content is updated, maintaining performance

### Plugin Features

The Next.js Revalidation plugin includes:

- Automatic revalidation when posts, pages, categories, tags, authors, or media are modified
- Settings page to configure your Next.js site URL and webhook secret
- Manual revalidation option for full site refresh
- Support for custom post types and taxonomies
- Optional admin notifications for revalidation events

### Manual Revalidation

You can also manually revalidate content using the `revalidateWordPressData` function:
Expand Down
81 changes: 81 additions & 0 deletions app/api/revalidate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { revalidateTag } from "next/cache";
import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
try {
const requestBody = await request.json();
const secret = request.headers.get("x-webhook-secret");

// Validate webhook secret
if (secret !== process.env.WORDPRESS_WEBHOOK_SECRET) {
console.error("Invalid webhook secret");
return NextResponse.json(
{ message: "Invalid webhook secret" },
{ status: 401 }
);
}

// Extract content type and ID from the webhook payload
const { contentType, contentId } = requestBody;

if (!contentType) {
return NextResponse.json(
{ message: "Missing content type" },
{ status: 400 }
);
}

// Determine which tags to revalidate
const tagsToRevalidate = ["wordpress"];

// Add content type specific tag
if (contentType === "post") {
tagsToRevalidate.push("posts");
if (contentId) {
tagsToRevalidate.push(`post-${contentId}`);
}
} else if (contentType === "page") {
tagsToRevalidate.push("pages");
if (contentId) {
tagsToRevalidate.push(`page-${contentId}`);
}
} else if (contentType === "category") {
tagsToRevalidate.push("categories");
if (contentId) {
tagsToRevalidate.push(`category-${contentId}`);
}
} else if (contentType === "tag") {
tagsToRevalidate.push("tags");
if (contentId) {
tagsToRevalidate.push(`tag-${contentId}`);
}
} else if (contentType === "author" || contentType === "user") {
tagsToRevalidate.push("authors");
if (contentId) {
tagsToRevalidate.push(`author-${contentId}`);
}
} else if (contentType === "media") {
tagsToRevalidate.push("media");
if (contentId) {
tagsToRevalidate.push(`media-${contentId}`);
}
}
Comment on lines +28 to +62
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add support for custom post types

The current implementation doesn't explicitly handle custom post types which are mentioned in the plugin documentation. Consider adding support for them.

  // Determine which tags to revalidate
  const tagsToRevalidate = ["wordpress"];

  // Add content type specific tag
  if (contentType === "post") {
    tagsToRevalidate.push("posts");
    if (contentId) {
      tagsToRevalidate.push(`post-${contentId}`);
    }
  } else if (contentType === "page") {
    tagsToRevalidate.push("pages");
    if (contentId) {
      tagsToRevalidate.push(`page-${contentId}`);
    }
  } else if (contentType === "category") {
    tagsToRevalidate.push("categories");
    if (contentId) {
      tagsToRevalidate.push(`category-${contentId}`);
    }
  } else if (contentType === "tag") {
    tagsToRevalidate.push("tags");
    if (contentId) {
      tagsToRevalidate.push(`tag-${contentId}`);
    }
  } else if (contentType === "author" || contentType === "user") {
    tagsToRevalidate.push("authors");
    if (contentId) {
      tagsToRevalidate.push(`author-${contentId}`);
    }
  } else if (contentType === "media") {
    tagsToRevalidate.push("media");
    if (contentId) {
      tagsToRevalidate.push(`media-${contentId}`);
    }
+ } else {
+   // Handle custom post types
+   tagsToRevalidate.push(`${contentType}s`);
+   if (contentId) {
+     tagsToRevalidate.push(`${contentType}-${contentId}`);
+   }
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Determine which tags to revalidate
const tagsToRevalidate = ["wordpress"];
// Add content type specific tag
if (contentType === "post") {
tagsToRevalidate.push("posts");
if (contentId) {
tagsToRevalidate.push(`post-${contentId}`);
}
} else if (contentType === "page") {
tagsToRevalidate.push("pages");
if (contentId) {
tagsToRevalidate.push(`page-${contentId}`);
}
} else if (contentType === "category") {
tagsToRevalidate.push("categories");
if (contentId) {
tagsToRevalidate.push(`category-${contentId}`);
}
} else if (contentType === "tag") {
tagsToRevalidate.push("tags");
if (contentId) {
tagsToRevalidate.push(`tag-${contentId}`);
}
} else if (contentType === "author" || contentType === "user") {
tagsToRevalidate.push("authors");
if (contentId) {
tagsToRevalidate.push(`author-${contentId}`);
}
} else if (contentType === "media") {
tagsToRevalidate.push("media");
if (contentId) {
tagsToRevalidate.push(`media-${contentId}`);
}
}
// Determine which tags to revalidate
const tagsToRevalidate = ["wordpress"];
// Add content type specific tag
if (contentType === "post") {
tagsToRevalidate.push("posts");
if (contentId) {
tagsToRevalidate.push(`post-${contentId}`);
}
} else if (contentType === "page") {
tagsToRevalidate.push("pages");
if (contentId) {
tagsToRevalidate.push(`page-${contentId}`);
}
} else if (contentType === "category") {
tagsToRevalidate.push("categories");
if (contentId) {
tagsToRevalidate.push(`category-${contentId}`);
}
} else if (contentType === "tag") {
tagsToRevalidate.push("tags");
if (contentId) {
tagsToRevalidate.push(`tag-${contentId}`);
}
} else if (contentType === "author" || contentType === "user") {
tagsToRevalidate.push("authors");
if (contentId) {
tagsToRevalidate.push(`author-${contentId}`);
}
} else if (contentType === "media") {
tagsToRevalidate.push("media");
if (contentId) {
tagsToRevalidate.push(`media-${contentId}`);
}
} else {
// Handle custom post types
tagsToRevalidate.push(`${contentType}s`);
if (contentId) {
tagsToRevalidate.push(`${contentType}-${contentId}`);
}
}


// Revalidate all determined tags
for (const tag of tagsToRevalidate) {
console.log(`Revalidating tag: ${tag}`);
revalidateTag(tag);
}

return NextResponse.json({
revalidated: true,
message: `Revalidated tags: ${tagsToRevalidate.join(", ")}`,
});
} catch (error) {
console.error("Revalidation error:", error);
return NextResponse.json(
{ message: "Error revalidating content" },
{ status: 500 }
);
}
}
13 changes: 7 additions & 6 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import "./globals.css";

import type { Metadata } from "next";
import { Section, Container } from "@/components/craft";
import { Inter as FontSans } from "next/font/google";
import { ThemeProvider } from "@/components/theme/theme-provider";
import { Button } from "@/components/ui/button";
import { MobileNav } from "@/components/nav/mobile-nav";
import { ThemeToggle } from "@/components/theme/theme-toggle";
import { mainMenu, contentMenu } from "@/menu.config";
import { Section, Container } from "@/components/craft";
import { MobileNav } from "@/components/nav/mobile-nav";
import { Analytics } from "@vercel/analytics/react";
import { Button } from "@/components/ui/button";

import { mainMenu, contentMenu } from "@/menu.config";
import { siteConfig } from "@/site.config";
import { cn } from "@/lib/utils";

import Balancer from "react-wrap-balancer";
import Logo from "@/public/logo.svg";
import Image from "next/image";
import Link from "next/link";

import { cn } from "@/lib/utils";
import type { Metadata } from "next";

const font = FontSans({
subsets: ["latin"],
Expand Down
31 changes: 16 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,41 @@
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-dialog": "^1.1.7",
"@radix-ui/react-dropdown-menu": "^2.1.7",
"@radix-ui/react-label": "^2.1.3",
"@radix-ui/react-navigation-menu": "^1.2.6",
"@radix-ui/react-scroll-area": "^1.2.4",
"@radix-ui/react-select": "^2.1.7",
"@radix-ui/react-separator": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.11",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-label": "^2.1.4",
"@radix-ui/react-navigation-menu": "^1.2.10",
"@radix-ui/react-scroll-area": "^1.2.6",
"@radix-ui/react-select": "^2.2.2",
"@radix-ui/react-separator": "^1.1.4",
"@radix-ui/react-slot": "^1.2.0",
"@vercel/analytics": "^1.5.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.469.0",
"next": "^15.3.0",
"next": "^15.3.1",
"next-themes": "^0.4.6",
"query-string": "^9.1.1",
"query-string": "^9.1.2",
"react": "19.0.0-rc.1",
"react-dom": "19.0.0-rc.1",
"react-hook-form": "^7.55.0",
"react-hook-form": "^7.56.2",
"react-wrap-balancer": "^1.1.1",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"use-debounce": "^10.0.4",
"zod": "^3.24.2"
"zod": "^3.24.4"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^20.17.30",
"@types/node": "^20.17.32",
"@types/react": "^18.3.20",
"@types/react-dom": "^18.3.6",
"@types/react-dom": "^18.3.7",
"autoprefixer": "^10.4.21",
"eslint": "^9.24.0",
"eslint-config-next": "^15.3.0",
"eslint": "^9.26.0",
"eslint-config-next": "^15.3.1",
"postcss": "^8.5.3",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3"
"typescript": "^5.8.3"
Comment on lines +39 to +48
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove duplicated typescript entry.

There's a duplicate "typescript": "^5.8.3" key in devDependencies. JSON objects ignore all but the last occurrence, so please remove the redundant entry to avoid confusion.

@@ devDependencies
-    "typescript": "^5.8.3"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@types/node": "^20.17.32",
"@types/react": "^18.3.20",
"@types/react-dom": "^18.3.6",
"@types/react-dom": "^18.3.7",
"autoprefixer": "^10.4.21",
"eslint": "^9.24.0",
"eslint-config-next": "^15.3.0",
"eslint": "^9.26.0",
"eslint-config-next": "^15.3.1",
"postcss": "^8.5.3",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3"
"typescript": "^5.8.3"
"@types/node": "^20.17.32",
"@types/react": "^18.3.20",
"@types/react-dom": "^18.3.7",
"autoprefixer": "^10.4.21",
"eslint": "^9.26.0",
"eslint-config-next": "^15.3.1",
"postcss": "^8.5.3",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3"
🧰 Tools
🪛 Biome (1.9.4)

[error] 48-48: expected , but instead found "typescript"

Remove "typescript"

(parse)


[error] 47-47: The key typescript was already declared.

This where a duplicated key was declared again.

If a key is defined multiple times, only the last definition takes effect. Previous definitions are ignored.

(lint/suspicious/noDuplicateObjectKeys)

}
}
53 changes: 53 additions & 0 deletions plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Next.js WordPress Revalidation Plugin

This plugin enables automatic revalidation of your Next.js site when content is changed in WordPress.

## Installation

1. Upload the `next-revalidate.zip` file through the WordPress admin plugin installer, or
2. Extract the `next-revalidate` folder to your `/wp-content/plugins/` directory
3. Activate the plugin through the WordPress admin interface
4. Go to Settings > Next.js Revalidation to configure your settings

## Configuration

### 1. WordPress Plugin Settings

After installing and activating the plugin:

1. Go to Settings > Next.js Revalidation in your WordPress admin
2. Enter your Next.js site URL (without trailing slash)
3. Create a secure webhook secret (a random string), you can use `openssl rand -base64 32` to generate one
4. Save your settings

### 2. Next.js Environment Variables

Add the webhook secret to your Next.js environment variables:

```bash
# .env.local
WORDPRESS_WEBHOOK_SECRET="your-secret-key-here"
```

## How It Works

1. When content in WordPress is created, updated, or deleted, the plugin sends a webhook to your Next.js API route
2. The webhook contains information about the content type (post, page, category, etc.) and ID
3. The Next.js API validates the request using the secret and revalidates the appropriate cache tags
4. Your Next.js site will fetch new content for the affected pages

## Features

- Automatic revalidation for posts, pages, categories, tags, and media
- Manual revalidation option through the admin interface
- Secure webhook communication with your Next.js site
- Optional admin notifications for revalidation events

## Troubleshooting

If revalidation isn't working:

1. Check that your Next.js URL is correct in the plugin settings
2. Verify the webhook secret matches in both WordPress and Next.js
3. Check your server logs for any errors in the API route
4. Enable notifications in the plugin settings to see revalidation status
Binary file added plugin/next-revalidate.zip
Binary file not shown.
67 changes: 67 additions & 0 deletions plugin/next-revalidate/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
=== Next.js Revalidation ===
Contributors: 9d8
Tags: next.js, headless, revalidation, cache
Requires at least: 5.0
Tested up to: 6.4
Stable tag: 1.0.1
Requires PHP: 7.2
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Automatically revalidate your Next.js site when WordPress content changes.

== Description ==

Next.js Revalidation is a WordPress plugin designed to work with the `next-wp` Next.js starter template. It triggers revalidation of your Next.js site's cache whenever content is added, updated, or deleted in WordPress.

The plugin sends webhooks to your Next.js site's revalidation API endpoint, ensuring your headless frontend always displays the most up-to-date content.

**Key Features:**

* Automatic revalidation when posts, pages, categories, tags, authors, or media are modified
* Settings page to configure your Next.js site URL and webhook secret
* Manual revalidation option for full site refresh
* Support for custom post types and taxonomies
* Optional admin notifications for revalidation events

== Installation ==

1. Upload the `next-revalidate` folder to the `/wp-content/plugins/` directory
2. Activate the plugin through the 'Plugins' menu in WordPress
3. Go to Settings > Next.js Revalidation to configure your Next.js site URL and webhook secret

== Configuration ==

1. Visit Settings > Next.js Revalidation in your WordPress admin
2. Enter your Next.js site URL without a trailing slash (e.g., https://your-site.com)
3. Enter the webhook secret which should match the WORDPRESS_WEBHOOK_SECRET in your Next.js environment
4. Optionally enable admin notifications for revalidation events
5. Click "Save Settings"

== Frequently Asked Questions ==

= What is the webhook secret for? =

The webhook secret provides security for your revalidation API endpoint. It ensures that only your WordPress site can trigger revalidations.

= How do I set up my Next.js site for revalidation? =

Your Next.js site needs an API endpoint at `/api/revalidate` that can process the webhook payloads from this plugin.
See the README in your Next.js project for more details.

= Does this work with custom post types? =

Yes, the plugin automatically detects and handles revalidation for custom post types and taxonomies.

== Changelog ==

= 1.0.1 =
* Fix: Register AJAX actions for manual revalidation
* Fix: Normalize Next.js site URL in settings (remove trailing slash)
= 1.0.0 =
* Initial release

== Upgrade Notice ==

= 1.0.0 =
Initial release of the Next.js Revalidation plugin.
2 changes: 2 additions & 0 deletions plugin/next-revalidate/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?php
// Silence is golden.
Loading