Skip to content

Commit ff82867

Browse files
authored
Merge pull request #548 from wpengine/previews-plugin-astro-example
docs(previews): Astro tutorial and example
2 parents 682fdce + f99b3a0 commit ff82867

File tree

34 files changed

+343
-10
lines changed

34 files changed

+343
-10
lines changed
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
---
2+
title: "Build Previews with Astro and WPGraphQL"
3+
description: "Learn how to build an Astro application with WordPress preview functionality using WPGraphQL and the HWP Previews plugin."
4+
---
5+
6+
In this tutorial, we will build an Astro application that displays WordPress content and enables preview functionality for draft posts. By the end, you will have a working headless WordPress setup where clicking "Preview" in WordPress opens your draft content in Astro.
7+
8+
We will use Astro's server-side rendering, TanStack Query for GraphQL data fetching, and WordPress Application Passwords for authentication.
9+
10+
## What you'll build
11+
12+
By following this tutorial, you will create:
13+
14+
- An Astro application that fetches WordPress content via GraphQL
15+
- A dynamic catch-all route that handles preview requests
16+
- Preview functionality that shows draft content when you click "Preview" in WordPress
17+
- Authentication using WordPress Application Passwords
18+
19+
## Prerequisites
20+
21+
Before starting, make sure you have:
22+
23+
- Node.js 18 or higher installed
24+
- A WordPress site with HWP Previews and **WPGraphQL plugins installed**
25+
- Basic familiarity with Astro and JavaScript
26+
27+
## Step 1: Create the Astro application
28+
29+
First, we will create a new Astro project.
30+
31+
Open your terminal and run:
32+
33+
```bash
34+
npm create astro@latest my-wordpress-preview
35+
```
36+
37+
When prompted, select:
38+
39+
- How would you like to start your new project? **Empty**
40+
- Install dependencies? **Yes**
41+
- Initialize a new git repository? **Yes** (optional)
42+
43+
Navigate into your project:
44+
45+
```bash
46+
cd my-wordpress-preview
47+
```
48+
49+
You should now see a basic Astro project structure with a `src` directory.
50+
51+
## Step 2: Configure Astro for server-side rendering
52+
53+
We need to enable server-side rendering (SSR) in Astro to handle preview requests dynamically.
54+
55+
Open `astro.config.mjs` and update it:
56+
57+
```javascript
58+
import { defineConfig } from "astro/config";
59+
60+
export default defineConfig({
61+
output: "server",
62+
});
63+
```
64+
65+
Notice how we set `output` to `'server'`. This tells Astro to render pages on-demand rather than at build time, which is necessary for previews.
66+
67+
## Step 3: Install TanStack Query
68+
69+
We will use TanStack Query to fetch data from WordPress via GraphQL.
70+
71+
Install the required package:
72+
73+
```bash
74+
npm install @tanstack/query-core
75+
```
76+
77+
Notice that your `package.json` now includes this new dependency.
78+
79+
## Step 4: Create the GraphQL fetcher
80+
81+
Now we will create a function to fetch data from WordPress GraphQL endpoint.
82+
83+
Create a new file `src/lib/graphql.js`:
84+
85+
```javascript
86+
const GRAPHQL_URL = "index.php?graphql";
87+
const graphqlApi = new URL(GRAPHQL_URL, import.meta.env.PUBLIC_WORDPRESS_URL).href;
88+
89+
export async function fetchGraphQL(query, variables = {}, isPreview = false) {
90+
const headers = {
91+
"Content-Type": "application/json",
92+
};
93+
94+
// Add authentication only for preview requests
95+
if (isPreview) {
96+
const username = import.meta.env.WP_USERNAME;
97+
const password = import.meta.env.WP_APP_PASSWORD;
98+
const auth = Buffer.from(`${username}:${password}`).toString("base64");
99+
headers.Authorization = `Basic ${auth}`;
100+
}
101+
102+
const response = await fetch(graphqlApi, {
103+
method: "POST",
104+
headers,
105+
body: JSON.stringify({
106+
query,
107+
variables,
108+
}),
109+
});
110+
111+
if (!response.ok) {
112+
throw new Error(`GraphQL request failed: ${response.statusText}`);
113+
}
114+
115+
const json = await response.json();
116+
117+
if (json.errors) {
118+
throw new Error(json.errors.map((e) => e.message).join(", "));
119+
}
120+
121+
return json.data;
122+
}
123+
```
124+
125+
This creates a fetcher function that sends GraphQL requests to your WordPress endpoint. Notice how authentication headers are only added when `isPreview` is true.
126+
127+
## Step 5: Create environment variables
128+
129+
Create a `.env` file in your project root:
130+
131+
```bash
132+
PUBLIC_WORDPRESS_URL=http://your-wordpress-site.com
133+
134+
WP_USERNAME=admin # WordPress username which you created Application Password for
135+
WP_APP_PASSWORD=**** # WordPress Application Password
136+
```
137+
138+
Use your actual WordPress URL and username here. We will cover the Application Password in a later step.
139+
140+
## Step 6: Create the content fetcher
141+
142+
We will create a function that fetches both published and preview content using TanStack Query.
143+
144+
Create `src/lib/getContent.js`:
145+
146+
```javascript
147+
import { QueryClient } from "@tanstack/query-core";
148+
import { fetchGraphQL } from "./graphql.js";
149+
150+
const queryClient = new QueryClient();
151+
152+
const SEED_QUERY = `
153+
query GetSeedNode($id: ID! = 0, $uri: String! = "", $asPreview: Boolean = false) {
154+
nodeByUri(uri: $uri) @skip(if: $asPreview) {
155+
__typename
156+
uri
157+
id
158+
... on NodeWithTitle {
159+
title
160+
}
161+
... on NodeWithContentEditor {
162+
content
163+
}
164+
... on ContentNode {
165+
databaseId
166+
}
167+
}
168+
169+
contentNode(id: $id, idType: DATABASE_ID, asPreview: true) @include(if: $asPreview) {
170+
__typename
171+
uri
172+
id
173+
... on NodeWithTitle {
174+
title
175+
}
176+
... on NodeWithContentEditor {
177+
content
178+
}
179+
... on ContentNode {
180+
databaseId
181+
}
182+
}
183+
}
184+
`;
185+
186+
export async function getContent({ uri, id, asPreview = false }) {
187+
return queryClient.fetchQuery({
188+
queryKey: ["content", asPreview ? id : uri, asPreview],
189+
queryFn: async () => {
190+
const data = await fetchGraphQL(SEED_QUERY, { uri, id, asPreview }, asPreview);
191+
return asPreview ? data.contentNode : data.nodeByUri;
192+
},
193+
});
194+
}
195+
```
196+
197+
This function uses TanStack Query to fetch and cache content. The query uses GraphQL directives (`@skip` and `@include`) to conditionally fetch either published content via `nodeByUri` or preview content via `contentNode`. Notice how the query key changes based on whether it's a preview request.
198+
199+
## Step 7: Create the catch-all route
200+
201+
Now we will create a dynamic route that handles all requests, including previews.
202+
203+
Create `src/pages/[...uri].astro`:
204+
205+
```astro
206+
---
207+
import { getContent } from '../lib/getContent.js';
208+
209+
const { uri = '/' } = Astro.params;
210+
211+
// Check if this is a preview request by looking for /preview/ in the path
212+
const isPreview = uri.startsWith('preview/');
213+
const postId = isPreview ? uri.replace('preview/', '') : undefined;
214+
215+
// Fetch the content from WordPress
216+
let node;
217+
try {
218+
node = await getContent({
219+
uri,
220+
id: postId,
221+
asPreview: isPreview,
222+
});
223+
} catch (error) {
224+
console.error('Error fetching content:', error);
225+
return new Response('Content not found', { status: 404 });
226+
}
227+
228+
if (!node) {
229+
return new Response('Content not found', { status: 404 });
230+
}
231+
---
232+
233+
<!DOCTYPE html>
234+
<html lang="en">
235+
<head>
236+
<meta charset="UTF-8" />
237+
<meta name="viewport" content="width=device-width" />
238+
<title>{node.title}</title>
239+
</head>
240+
<body>
241+
<main>
242+
<article>
243+
<h1 set:html={node.title} />
244+
<div set:html={node.content} />
245+
{isPreview && (
246+
<div style="background: yellow; padding: 1rem; margin-top: 2rem;">
247+
<strong>Preview Mode</strong>
248+
<p>You are viewing draft content.</p>
249+
</div>
250+
)}
251+
</article>
252+
</main>
253+
</body>
254+
</html>
255+
```
256+
257+
Notice how this single route handles both regular content and previews. When the URL path starts with `/preview/`, it extracts the post ID from the path and fetches preview content. Otherwise, it fetches published content using the URI. TanStack Query automatically caches the results to improve performance.
258+
259+
## Step 8: Generate a WordPress Application Password
260+
261+
Now we need to create an Application Password in WordPress for authentication.
262+
263+
1. Log into your WordPress admin
264+
2. Go to Users > Profile
265+
3. Scroll down to "Application Passwords"
266+
4. Enter a name like "Astro Preview"
267+
5. Click "Add Application Password"
268+
269+
![WordPress Application Passwords section showing the form to generate a new application password with a name field and "Add Application Password" button](../screenshots/generate-application-password.png)
270+
271+
Copy the generated password (it will look like `xxxx xxxx xxxx xxxx xxxx xxxx`). You will not be able to see it again.
272+
273+
Update your `.env` file with this password:
274+
275+
```bash
276+
WP_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx
277+
```
278+
279+
## Step 9: Configure HWP Previews in WordPress
280+
281+
We will now configure the preview URL in WordPress to point to your Astro app.
282+
283+
1. In WordPress admin, go to Settings > HWP Previews
284+
2. Click the "Posts" tab
285+
3. Check "Enable HWP Previews"
286+
4. In the Preview URL Template field, enter:
287+
```
288+
http://localhost:4321/preview/{ID}
289+
```
290+
5. Click "Save Changes"
291+
292+
![WordPress HWP Previews settings page showing the Posts tab with "Enable HWP Previews" checkbox checked and Preview URL Template field containing "http://localhost:4321/preview/{ID}"](../screenshots/preview-astro-settings.png)
293+
294+
Notice how we use `/preview/{ID}` in the URL path. This works reliably for draft posts which may not have a slug yet. WordPress will automatically replace `{ID}` with the actual post ID. The presence of `/preview/` in the path will signal that this is a preview request.
295+
296+
## Step 10: Start your application
297+
298+
Start the Astro development server:
299+
300+
```bash
301+
npm run dev
302+
```
303+
304+
You should see output indicating the server is running at `http://localhost:4321`.
305+
306+
## Step 11: Test the preview
307+
308+
Now we will test that previews work correctly.
309+
310+
1. In WordPress, create or edit a post
311+
2. Make some changes but do not publish
312+
3. Click the "Preview" button
313+
314+
You should be redirected to your Astro application showing your draft content. Notice the yellow banner at the bottom indicating you're in preview mode.
315+
316+
![Screenshot showing an Astro application displaying WordPress draft content in preview mode, with the post title and content visible on the page](../screenshots/preview-astro-view.png)
317+
318+
If you see your draft content with the preview banner, congratulations! Your preview system is working.
319+
320+
## What you've learned
321+
322+
Through building this application, you have learned:
323+
324+
- How to set up Astro with server-side rendering for dynamic content
325+
- How to use TanStack Query to fetch data from WordPress GraphQL API
326+
- How to authenticate preview requests using WordPress Application Passwords
327+
- How WordPress query parameters control preview behavior
328+
329+
## Next steps
330+
331+
Now that you have a working preview system, you can:
332+
333+
- Add support for Pages and custom post types
334+
- Create a proper layout component for consistent styling
335+
- Add loading states and better error handling
336+
- Implement the WordPress template hierarchy for different content types
337+
- Deploy your application to a hosting platform that supports SSR
338+
339+
For a more complete example with template hierarchy support, see the [full Astro example](https://github.com/wpengine/hwptoolkit/tree/main/plugins/hwp-previews/examples/hwp-preview-astro) which includes these additional features.
163 KB
Loading
54.3 KB
Loading

examples/astro/previews/.wp-env.json renamed to plugins/hwp-previews/examples/hwp-preview-astro/.wp-env.json

File renamed without changes.

examples/astro/previews/README.md renamed to plugins/hwp-previews/examples/hwp-preview-astro/README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
---
2-
title: "Astro Headless WordPress Previews"
2+
title: "Headless WordPress Previews with Astro"
33
description: "In this example, we show how to implement **Headless WordPress Previews in Astro** using the **`hwp-previews`** plugin and WPGraphQL. This setup allows content creators to preview draft posts directly in the Astro frontend from the WordPress admin panel. We use **URQL** for all routing and fetching page content."
44
---
55

6-
# Astro Headless WordPress Previews Example
6+
# Example: Headless WordPress Previews with Astro
77

88
In this example, we show how to implement **Headless WordPress Previews in Astro** using the **`hwp-previews`** plugin and WPGraphQL. This setup allows content creators to preview draft posts directly in the Astro frontend from the WordPress admin panel. We use **URQL** for all routing and fetching page content.
99

1010
## Getting Started
1111

12-
> [!IMPORTANT]
13-
> **Docker Desktop** needs to be installed to run WordPress locally.
12+
> [!IMPORTANT] > **Docker Desktop** needs to be installed to run WordPress locally.
1413
1514
1. Run `npm run example:setup` to install dependencies and configure the local WP server.
1615
2. Run `npm run example:start` to start the WP server and Astro development server.
@@ -27,4 +26,4 @@ In this example, we show how to implement **Headless WordPress Previews in Astro
2726

2827
## Trouble Shooting
2928

30-
To reset the WP server and re-run setup you can run `npm run example:prune` and confirm "Yes" at any prompts.
29+
To reset the WP server and re-run setup you can run `npm run example:prune` and confirm "Yes" at any prompts.

examples/astro/previews/example-app/.gitignore renamed to plugins/hwp-previews/examples/hwp-preview-astro/example-app/.gitignore

File renamed without changes.

examples/astro/previews/example-app/README.md renamed to plugins/hwp-previews/examples/hwp-preview-astro/example-app/README.md

File renamed without changes.

examples/astro/previews/example-app/astro.config.mjs renamed to plugins/hwp-previews/examples/hwp-preview-astro/example-app/astro.config.mjs

File renamed without changes.

examples/astro/previews/example-app/package.json renamed to plugins/hwp-previews/examples/hwp-preview-astro/example-app/package.json

File renamed without changes.

examples/astro/previews/example-app/public/favicon.svg renamed to plugins/hwp-previews/examples/hwp-preview-astro/example-app/public/favicon.svg

File renamed without changes.

0 commit comments

Comments
 (0)