{"id":31956,"date":"2025-08-18T16:12:11","date_gmt":"2025-08-18T21:12:11","guid":{"rendered":"https:\/\/wpengine.com\/builders\/?p=31956"},"modified":"2025-08-18T16:12:13","modified_gmt":"2025-08-18T21:12:13","slug":"astro-wordpress-post-previews","status":"publish","type":"post","link":"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/","title":{"rendered":"Astro + WordPress: Post Previews"},"content":{"rendered":"\n<p>Historically, previews for headless WordPress have been quite complicated. <a href=\"https:\/\/www.gatsbyjs.com\/plugins\/gatsby-source-wordpress\/\" target=\"_blank\" rel=\"noreferrer noopener\">Gatsby<\/a> and <a href=\"https:\/\/faustjs.org\/docs\/how-to\/post-previews\/\" target=\"_blank\" rel=\"noreferrer noopener\">Faust.js<\/a> have both implemented their own solutions, but required specific buy-in on those solutions. To alleviate this, our headless OSS team released the <a href=\"https:\/\/github.com\/wpengine\/hwptoolkit\/tree\/main\/plugins\/hwp-previews#readme\" target=\"_blank\" rel=\"noreferrer noopener\">HWP Previews plugin<\/a> for headless WordPress that sets you up to do previews without using Faust or Gatsby. Now, you don\u2019t have to use Faust to give your publishers the preview experience they expect in WordPress.<\/p>\n\n\n\n<p>This plugin overrides WordPress&#8217;s default preview behavior and allows you to control how previews are requested (URL, path, query params, etc.). The plugin is in beta, so we\u2019d love to hear about any missing features or bugs.<\/p>\n\n\n\n<p>Since the HWP Previews plugin covers our needs on the WP side, this article will cover implementing the functionality on the framework side, i.e., Astro. Join me as we dive into WPGraphQL, authentication, and previews!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Goal<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>We want the same components\/code that renders our production pages to render previews so they\u2019re identical.<\/li>\n\n\n\n<li>We want the WP experience to be seamless for content creators.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">Requirements<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Astro<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Needs to know whether a request is for a preview or production page<\/li>\n\n\n\n<li>Needs to know what content is being previewed<\/li>\n\n\n\n<li>Needs to be able to render the preview on demand<\/li>\n\n\n\n<li>Needs to use the same template\/components\/queries as production pages<\/li>\n\n\n\n<li>Needs to be able to authenticate the preview request<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">WordPress<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Needs to know where our front-end is for previews(domain\/path)<\/li>\n\n\n\n<li>Needs to be able to know how to request a preview for a given post\/page (path\/queries\/headers\/etc)<\/li>\n\n\n\n<li>Needs to leverage the existing \u201cpreview\u201d links\/buttons\/popups provided by native previews to keep the experience seamless.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">Pesky Details<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Auth<\/h3>\n\n\n\n<p>What you\u2019re not seeing in the URL is also the authentication that\u2019s happening. This preview URL will redirect the user to the WordPress login if they\u2019re not already authenticated. Unpublished posts are by default non-public. You must be authenticated and authorized to view them. This holds true in WPGraphQL; if you query an unpublished post, it\u2019ll return null unless you authenticate your GraphQL query. We\u2019ll need to authenticate users and make sure any GraphQL requests receive proper Authentication headers for preview routes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Database ID<\/h3>\n\n\n\n<p>So why doesn\u2019t WordPress use a URL like <code>https:\/\/mywordpresssite.com\/blog\/hello-world\/?preview=true<\/code>? After all, this URL is unique to the post, and the query param tells WordPress to render the draft version. If only it were that simple!<\/p>\n\n\n\n<p>WordPress doesn\u2019t assign a URI to a post until <em>after<\/em> it has been published. This means drafts and scheduled posts don\u2019t have a dedicated URI. They may have a slug, but this isn\u2019t necessarily unique in WordPress land. Thus, to correctly render previews, we must handle routing and data fetching based on the databaseId, not the URI. This will come up in several ways later, but for now, know that this is a constraint of WordPress we must adapt to.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Strategy<\/h2>\n\n\n\n<p>Like in any headless WordPress setup, we\u2019ll need to start with working routing and data fetching. For this article, I\u2019ll be starting from where we left off in the <a href=\"https:\/\/wpengine.com\/builders\/astro-wordpress-routing-and-graphql\/\" target=\"_blank\" rel=\"noreferrer noopener\">Routing and GraphQL article<\/a> and adding on previews. This means we\u2019ll be using URQL for data fetching and the template hierarchy for routing.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">SSR<\/h3>\n\n\n\n<p>First, we need SSR. Our original <code>[...uri].astro<\/code> catch-all route was static. We have two options: convert it to SSR or add a dedicated <code>\/preview\/<\/code> route that is SSR. For this example, I\u2019ve opted to convert my catch-all route to SSR.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Detect Preview<\/h3>\n\n\n\n<p>Next, like WordPress, we need to detect whether we need to authenticate the request and fetch preview data. The <code>preview=true<\/code> query parameter does this for us. We\u2019ll use this to detect previews and handle them.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Database ID<\/h3>\n\n\n\n<p>As we discussed before, the database ID is required by WordPress for previews. In my example, I get this by having the Previews plugin pass this as a query param: <code>post_id={ID}<\/code>.\u00a0<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Auth<\/h3>\n\n\n\n<p>The goal of this article is to show you previews, not implement authentication. Because of that, I\u2019ve opted for the simplest possible authentication method, which is not secure. I\u2019ve opted to hard-code my admin credentials in my code and use them for <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Guides\/Authentication\" target=\"_blank\" rel=\"noreferrer noopener\">Basic authentication<\/a>.\u00a0<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><strong>Note<\/strong>: I can do this because the WordPress server used in this example is not public; its entire DB and front-end example are running on your computer if you start it from the <a href=\"https:\/\/github.com\/wpengine\/hwptoolkit\/tree\/main\/examples\/astro\/previews\" target=\"_blank\" rel=\"noreferrer noopener\">repo<\/a>. If you\u2019re going to implement previews, you\u2019ll need to implement proper authentication if you don\u2019t want a security breach. I\u2019d highly recommend the <a href=\"https:\/\/github.com\/AxeWP\/wp-graphql-headless-login\" target=\"_blank\" rel=\"noreferrer noopener\">WPGraphQL Headless Login<\/a> plugin by Dovid Levine.<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\">Configuring WordPress<\/h2>\n\n\n\n<p>Selecting to preview a post or page in WordPress results in a URL path that will look something like <code>https:\/\/mywordpresssite.com\/?preview=true&amp;p=23<\/code>, with <code>p<\/code> being the post&#8217;s database ID.<\/p>\n\n\n\n<p>What needs to change here? First, our front-end is not on the WordPress server; we need to tell previews to go to our JS framework. This is likely your production server, but static site builders like Gatsby may require a dedicated preview server. Others may require a dedicated path, query parameters, or headers.<\/p>\n\n\n\n<p>Finally, WordPress renders the appropriate PHP template at this route. To keep our experience seamless, I\u2019d rather the content creators see the headless front-end here, not get kicked out to a new tab or have to find their way back into WordPress admin.<\/p>\n\n\n\n<p>This means we need to customize the URL that we\u2019ll route to when clicking \u201cpreview\u201d and customize the <code>?preview=true<\/code> behavior to embed an iframe of our front-end. The good news is that this is exactly what the <a href=\"https:\/\/github.com\/wpengine\/hwptoolkit\/tree\/main\/plugins\/hwp-previews#readme\">HWP Previews<\/a> plugin does!\u00a0<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/lh7-rt.googleusercontent.com\/docsz\/AD_4nXc5mCndiNUrobaCuFJI3pkgDFfBRtlxsC1KirNnP55oLekbOelL8NgOlX65lrgCLksukdkRq7niT_lphOpLToFNSSj8ulbi-l_xl1W4_r9ZVUcwrn-fvNZ8rPxuDdb6IFCrTbVMKA?key=6C1_LQYGKr0EUdv4a37WUw\" alt=\"\"\/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Building It out<\/h2>\n\n\n\n<p>Alright, now that WordPress is configured and our basic strategy is in place, let\u2019s start implementing this in Astro.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Catch-all Route<\/h3>\n\n\n\n<p>Our first changes will be in the <a href=\"https:\/\/github.com\/wpengine\/hwptoolkit\/blob\/main\/examples\/astro\/previews\/example-app\/src\/pages\/%5B...uri%5D.astro\" target=\"_blank\" rel=\"noreferrer noopener\">catch-all route,<\/a> where we fetch the template. We\u2019ll start by capturing and storing our <code>preview<\/code> and <code>post_id<\/code> search params.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">const<\/span> isPreview = Astro.url.searchParams.get(<span class=\"hljs-string\">\"preview\"<\/span>) === <span class=\"hljs-string\">\"true\"<\/span>;\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-keyword\">const<\/span> postId = Astro.url.searchParams.get(<span class=\"hljs-string\">\"post_id\"<\/span>) || <span class=\"hljs-literal\">undefined<\/span>;\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><\/p>\n\n\n\n<p>We\u2019ll also want to store this search parameter for later use. Because we\u2019re using Astro\u2019s rewrite functionality, this param gets stripped from the URL accessed by templates. Thus, we\u2019ll save this for use later.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-comment\">\/\/ Locals is an Astro pattern for sharing route data.<\/span>\n<\/span><\/span><span class='shcb-loc'><span>Astro.locals.isPreview = isPreview;\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">Authentication<\/h3>\n\n\n\n<p>I\u2019ve told you I did some really basic things. Are you ready for it?<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> authHeaders = <span class=\"hljs-function\">(<span class=\"hljs-params\">isPreview<\/span>) =&gt;<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>\u00a0\u00a0<span class=\"hljs-keyword\">return<\/span> isPreview\n<\/span><\/span><span class='shcb-loc'><span>\u00a0\u00a0\u00a0\u00a0? {\n<\/span><\/span><span class='shcb-loc'><span>\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-attr\">Authorization<\/span>: <span class=\"hljs-string\">`Basic <span class=\"hljs-subst\">${Buffer.<span class=\"hljs-keyword\">from<\/span>(<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-string\"><span class=\"hljs-subst\">\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-string\">`admin:password`<\/span><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-string\"><span class=\"hljs-subst\">\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0).toString(<span class=\"hljs-string\">\"base64\"<\/span>)}<\/span>`<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\n<\/span><\/span><span class='shcb-loc'><span>\u00a0\u00a0\u00a0\u00a0: <span class=\"hljs-literal\">undefined<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>};\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><\/p>\n\n\n\n<p>As you can see, if <code>isPreview<\/code> is <code>true<\/code>, we add the <code>Authorization<\/code> header; otherwise, we don\u2019t. This is used in combination with a great feature of the URQL GraphQL client.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">const<\/span> response = <span class=\"hljs-keyword\">await<\/span> client.query(QUERY, VARIABLES,{\n<\/span><\/span><span class='shcb-loc'><span>\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-attr\">fetchOptions<\/span>: {\n<\/span><\/span><span class='shcb-loc'><span>\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-attr\">headers<\/span>: {\n<\/span><\/span><span class='shcb-loc'><span>\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0...authHeaders(isPreview),\n<\/span><\/span><span class='shcb-loc'><span>\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0},\n<\/span><\/span><span class='shcb-loc'><span>\u00a0\u00a0\u00a0\u00a0},\n<\/span><\/span><span class='shcb-loc'><span>\u00a0\u00a0}\n<\/span><\/span><span class='shcb-loc'><span>);\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><\/p>\n\n\n\n<p>On top of taking the query and variables for a query, the third parameter of URQL\u2019s query function takes a config. This is the identical config you can pass when creating the client. That means I can create a single client with good defaults and override as needed. I don\u2019t have to select between any number of clients. I can use a single client and add headers and other config as needed.&nbsp;<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Template Hierarchy<\/h3>\n\n\n\n<p>In the last article, we built the <code>uriToTemplate<\/code> function for handling our template-hierarchy routing. Now that we want to implement previews, we need this to handle the database ID or URI to the template. If you happened to pay attention to the original <a href=\"https:\/\/github.com\/wpengine\/hwptoolkit\/blob\/main\/examples\/astro\/previews\/example-app\/src\/lib\/seedQuery.js#L13-L91\" target=\"_blank\" rel=\"noreferrer noopener\">seed query<\/a> we were using, you would have noticed that because we copied it from Faust, the query was already set up to handle this.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span>query GetSeedNode(\n<\/span><\/span><span class='shcb-loc'><span>    $id: ID! = <span class=\"hljs-number\">0<\/span>\n<\/span><\/span><span class='shcb-loc'><span>    $uri: String! = <span class=\"hljs-string\">\"\"<\/span>\n<\/span><\/span><span class='shcb-loc'><span>    $asPreview: Boolean = <span class=\"hljs-keyword\">false<\/span>\n<\/span><\/span><span class='shcb-loc'><span>  ) {\n<\/span><\/span><span class='shcb-loc'><span>    ... on RootQuery @skip(<span class=\"hljs-keyword\">if<\/span>: $asPreview) {\n<\/span><\/span><span class='shcb-loc'><span>      nodeByUri(uri: $uri) {\n<\/span><\/span><span class='shcb-loc'><span>        __typename\n<\/span><\/span><span class='shcb-loc'><span>        ...GetNode\n<\/span><\/span><span class='shcb-loc'><span>      }\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    ... on RootQuery @<span class=\"hljs-keyword\">include<\/span>(<span class=\"hljs-keyword\">if<\/span>: $asPreview) {\n<\/span><\/span><span class='shcb-loc'><span>      contentNode(id: $id, idType: DATABASE_ID, asPreview: <span class=\"hljs-keyword\">true<\/span>) {\n<\/span><\/span><span class='shcb-loc'><span>        __typename\n<\/span><\/span><span class='shcb-loc'><span>        ...GetNode\n<\/span><\/span><span class='shcb-loc'><span>      }\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><\/p>\n\n\n\n<p>Thus, I updated my <code>getSeedQuery<\/code> function to handle the additional variables. Auth was also handled <a href=\"https:\/\/github.com\/wpengine\/hwptoolkit\/blob\/main\/examples\/astro\/previews\/example-app\/src\/lib\/seedQuery.js#L3-L11\" target=\"_blank\" rel=\"noreferrer noopener\">here<\/a><em>.<\/em><\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">getSeedQuery<\/span>(<span class=\"hljs-params\">variables<\/span>) <\/span>{\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">return<\/span> client.query(SEED_QUERY, variables, {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attr\">fetchOptions<\/span>: {\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">headers<\/span>: {\n<\/span><\/span><span class='shcb-loc'><span>        ...authHeaders(variables.asPreview),\n<\/span><\/span><span class='shcb-loc'><span>      },\n<\/span><\/span><span class='shcb-loc'><span>    },\n<\/span><\/span><span class='shcb-loc'><span>  });\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><\/p>\n\n\n\n<p>Finally, I updated <code>uriToTemplate<\/code> to <code><a href=\"https:\/\/github.com\/wpengine\/hwptoolkit\/blob\/main\/examples\/astro\/previews\/example-app\/src\/lib\/templateHierarchy.ts#L27-L98\" target=\"_blank\" rel=\"noreferrer noopener\">idToTemplate<\/a><\/code>, which handles both <code>uri<\/code> and <code>databaseId<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">idToTemplate<\/span>(<span class=\"hljs-params\"><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">  options: ToTemplateArgs<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\"><\/span>): <span class=\"hljs-title\">Promise<\/span>&lt;<span class=\"hljs-title\">TemplateData<\/span>&gt; <\/span>{\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> id = <span class=\"hljs-string\">\"id\"<\/span> <span class=\"hljs-keyword\">in<\/span> options ? options.id : <span class=\"hljs-literal\">undefined<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> uri = <span class=\"hljs-string\">\"uri\"<\/span> <span class=\"hljs-keyword\">in<\/span> options ? options.uri : <span class=\"hljs-literal\">undefined<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> asPreview = <span class=\"hljs-string\">\"asPreview\"<\/span> <span class=\"hljs-keyword\">in<\/span> options ? options.asPreview : <span class=\"hljs-literal\">false<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (asPreview &amp;&amp; !id) {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-built_in\">console<\/span>.error(<span class=\"hljs-string\">\"HTTP\/400 - preview requires database id\"<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">return<\/span> returnData;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> { data, error } = <span class=\"hljs-keyword\">await<\/span> getSeedQuery({ uri, id, asPreview });\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-comment\">\/\/...<\/span>\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><\/p>\n\n\n\n<p>Finally, we update the call to this function from the <a href=\"https:\/\/github.com\/wpengine\/hwptoolkit\/blob\/main\/examples\/astro\/previews\/example-app\/src\/pages\/%5B...uri%5D.astro#L14\" target=\"_blank\" rel=\"noreferrer noopener\">catch-all route<\/a>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">const<\/span> results = <span class=\"hljs-keyword\">await<\/span> idToTemplate({ uri, <span class=\"hljs-attr\">asPreview<\/span>: isPreview, <span class=\"hljs-attr\">id<\/span>: postId });\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">Updating Templates<\/h3>\n\n\n\n<p>You\u2019d be forgiven for thinking we\u2019re done. Remember that pesky thing I mentioned about having to use databaseIds for preview queries? Well, we now have to update our templates to do this.<\/p>\n\n\n\n<p>While we could make our templates use the <code>@skip<\/code> and <code>@include<\/code> pattern like the seed query\u2026there is just no point. The seed query handled this complexity for us and returned a bunch of data that we used to select a template. That data included the database ID. We can now use that for all further queries instead of the URI!<\/p>\n\n\n\n<p>Let\u2019s start by grabbing those <code>isPreview<\/code> and <code>databaseId<\/code> variables so they\u2019re handy.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">const<\/span> isPreview = Astro.locals.isPreview;\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-keyword\">const<\/span> databaseId = Astro.locals.templateData?.databaseId;\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><\/p>\n\n\n\n<p>Like with our seed query, we will also need to add authentication when appropriate.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">const<\/span> { data, error } = <span class=\"hljs-keyword\">await<\/span> client.query(\n<\/span><\/span><span class='shcb-loc'><span>  gql<span class=\"hljs-string\">`<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-string\">    #...<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-string\">  `<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>  {\n<\/span><\/span><span class='shcb-loc'><span>    databaseId,\n<\/span><\/span><span class='shcb-loc'><span>    isPreview,\n<\/span><\/span><span class='shcb-loc'><span>  },\n<\/span><\/span><span class='shcb-loc'><span>  {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attr\">fetchOptions<\/span>: {\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">headers<\/span>: {\n<\/span><\/span><span class='shcb-loc'><span>        ...authHeaders(isPreview),\n<\/span><\/span><span class='shcb-loc'><span>      },\n<\/span><\/span><span class='shcb-loc'><span>    },\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>);\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><\/p>\n\n\n\n<p>Next, we need to update our query. <a href=\"https:\/\/github.com\/wpengine\/hwptoolkit\/blob\/main\/examples\/astro\/template-hierarchy-data-fetching-urql\/example-app\/src\/pages\/wp-templates\/single.astro#L23-L48\" target=\"_blank\" rel=\"noreferrer noopener\">Previously<\/a>, we used <code>nodeByUri<\/code> for all of our queries. This works great, but it doesn\u2019t accept database IDs or support returning preview data. Thus, for posts and pages, we need to use <code>contentNode<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">const<\/span> { data, error } = await client.query(\n<\/span><\/span><span class='shcb-loc'><span>  gql`\n<\/span><\/span><span class='shcb-loc'><span>    query singleTemplatePageQuery(\n<\/span><\/span><span class='shcb-loc'><span>      $databaseId: ID!\n<\/span><\/span><span class='shcb-loc'><span>      $isPreview: Boolean = <span class=\"hljs-keyword\">false<\/span>\n<\/span><\/span><span class='shcb-loc'><span>    ) {\n<\/span><\/span><mark class='shcb-loc'><span>      contentNode(\n<\/span><\/mark><mark class='shcb-loc'><span>        id: $databaseId\n<\/span><\/mark><mark class='shcb-loc'><span>        idType: DATABASE_ID\n<\/span><\/mark><mark class='shcb-loc'><span>        asPreview: $isPreview\n<\/span><\/mark><mark class='shcb-loc'><span>      ) {\n<\/span><\/mark><span class='shcb-loc'><span>        id\n<\/span><\/span><span class='shcb-loc'><span>        uri\n<\/span><\/span><span class='shcb-loc'><span>        ... on NodeWithTitle {\n<\/span><\/span><span class='shcb-loc'><span>          title\n<\/span><\/span><span class='shcb-loc'><span>        }\n<\/span><\/span><span class='shcb-loc'><span>        ... on NodeWithContentEditor {\n<\/span><\/span><span class='shcb-loc'><span>          content\n<\/span><\/span><span class='shcb-loc'><span>        }\n<\/span><\/span><span class='shcb-loc'><span>        ... on Post {\n<\/span><\/span><span class='shcb-loc'><span>          categories {\n<\/span><\/span><span class='shcb-loc'><span>            nodes {\n<\/span><\/span><span class='shcb-loc'><span>              name\n<\/span><\/span><span class='shcb-loc'><span>              uri\n<\/span><\/span><span class='shcb-loc'><span>            }\n<\/span><\/span><span class='shcb-loc'><span>          }\n<\/span><\/span><span class='shcb-loc'><span>          tags {\n<\/span><\/span><span class='shcb-loc'><span>            nodes {\n<\/span><\/span><span class='shcb-loc'><span>              name\n<\/span><\/span><span class='shcb-loc'><span>              uri\n<\/span><\/span><span class='shcb-loc'><span>            }\n<\/span><\/span><span class='shcb-loc'><span>          }\n<\/span><\/span><span class='shcb-loc'><span>        }\n<\/span><\/span><span class='shcb-loc'><span>      }\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>  `,\n<\/span><\/span><span class='shcb-loc'><span>  {\n<\/span><\/span><span class='shcb-loc'><span>    databaseId,\n<\/span><\/span><span class='shcb-loc'><span>    isPreview,\n<\/span><\/span><span class='shcb-loc'><span>  },\n<\/span><\/span><span class='shcb-loc'><span>  {\n<\/span><\/span><span class='shcb-loc'><span>    fetchOptions: {\n<\/span><\/span><span class='shcb-loc'><span>      headers: {\n<\/span><\/span><span class='shcb-loc'><span>        ...authHeaders(isPreview),\n<\/span><\/span><span class='shcb-loc'><span>      },\n<\/span><\/span><span class='shcb-loc'><span>    },\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>);\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><\/p>\n\n\n\n<p>For this change, I didn\u2019t have to alter any of the actual query that defines the returned data. I also updated the Astro <a href=\"https:\/\/github.com\/wpengine\/hwptoolkit\/blob\/main\/examples\/astro\/template-hierarchy-data-fetching-urql\/example-app\/src\/pages\/wp-templates\/single.astro\" target=\"_blank\" rel=\"noreferrer noopener\">html template<\/a> to access <code>data.contentNode<\/code> instead of <code>data.nodeByUri<\/code>.<\/p>\n\n\n\n<p>Finally, I added a quick check to validate that I got a post back. If you\u2019re not aware, having no or incorrect credentials for a query in GraphQL doesn\u2019t result in an <code>HTTP\/401<\/code> error, but a <code>null<\/code> value. I\u2019ll add a check to return <code>HTTP\/404<\/code> if the value is <code>null<\/code>. This handles incorrect database IDs and unauthorized queries.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">if<\/span> (!data.post) {\n<\/span><\/span><span class='shcb-loc'><span>\u00a0\u00a0<span class=\"hljs-built_in\">console<\/span>.error(<span class=\"hljs-string\">\"HTTP\/404 - Not Found in WordPress:\"<\/span>, databaseId);\n<\/span><\/span><span class='shcb-loc'><span>\u00a0\u00a0<span class=\"hljs-keyword\">return<\/span> Astro.rewrite(<span class=\"hljs-string\">\"\/404\"<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\">It works!<\/h2>\n\n\n\n<h2 class=\"wp-block-heading\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/lh7-rt.googleusercontent.com\/docsz\/AD_4nXfu4-QVHO1lOfk1LR3MpPU8Ztk0L3OjSc5jbb88afdhdtRQWKWKvBsYj9Ld2hU-PtOcSvo_XxMReMDB1kNZES63VODRAh5s4ba4VYv7LPJgZVVYl3wE-EQdh1ZH_kcoVVlAyxhfbQ?key=6C1_LQYGKr0EUdv4a37WUw\" width=\"693\" height=\"526\"><\/h2>\n\n\n\n<p>Previews! We\u2019ve implemented one of the biggest missing features of headless WordPress. HWP Previews did all the heavy lifting on the WP side, and we took what it provided to render the posts. Our content creators can now publish with confidence, knowing exactly what their work will look like on the front end!<\/p>\n\n\n\n<p>I\u2019m excited to have this plugin available for folks to build custom preview experiences outside of Gatsby and Faust. What are you going to build with it? Come join our <a href=\"https:\/\/faustjs.org\/discord\">Headless WordPress Discord<\/a> and let us know!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Historically, previews for headless WordPress have been quite complicated. Gatsby and Faust.js have both implemented their own solutions, but required specific buy-in on those solutions. To alleviate this, our headless [&hellip;]<\/p>\n","protected":false},"author":25,"featured_media":31958,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_EventAllDay":false,"_EventTimezone":"","_EventStartDate":"","_EventEndDate":"","_EventStartDateUTC":"","_EventEndDateUTC":"","_EventShowMap":false,"_EventShowMapLink":false,"_EventURL":"","_EventCost":"","_EventCostDescription":"","_EventCurrencySymbol":"","_EventCurrencyCode":"","_EventCurrencyPosition":"","_EventDateTimeSeparator":"","_EventTimeRangeSeparator":"","_EventOrganizerID":[],"_EventVenueID":[],"_OrganizerEmail":"","_OrganizerPhone":"","_OrganizerWebsite":"","_VenueAddress":"","_VenueCity":"","_VenueCountry":"","_VenueProvince":"","_VenueState":"","_VenueZip":"","_VenuePhone":"","_VenueURL":"","_VenueStateProvince":"","_VenueLat":"","_VenueLng":"","_VenueShowMap":false,"_VenueShowMapLink":false,"footnotes":""},"categories":[23],"tags":[53,54],"class_list":["post-31956","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-headless","tag-astro","tag-preview"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.7 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Astro + WordPress: Post Previews - Builders<\/title>\n<meta name=\"description\" content=\"Learn how to implement post previews in Astro for headless WordPress using the HWP Previews plugin. This guide covers WPGraphQL, authentication, and ensuring a seamless content creator experience.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Astro + WordPress: Post Previews\" \/>\n<meta property=\"og:description\" content=\"Learn how to implement post previews in Astro for headless WordPress using the HWP Previews plugin. This guide covers WPGraphQL, authentication, and ensuring a seamless content creator experience.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/\" \/>\n<meta property=\"og:site_name\" content=\"Builders\" \/>\n<meta property=\"article:published_time\" content=\"2025-08-18T21:12:11+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-08-18T21:12:13+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/08\/astro-preview-1024x576.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1024\" \/>\n\t<meta property=\"og:image:height\" content=\"576\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Alex Moon\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@moon_meister\" \/>\n<meta name=\"twitter:site\" content=\"@wpebuilders\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Alex Moon\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"8 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/\"},\"author\":{\"name\":\"Alex Moon\",\"@id\":\"https:\/\/wpengine.com\/builders\/#\/schema\/person\/868e9d9f8b846000f45cc7a25f46255a\"},\"headline\":\"Astro + WordPress: Post Previews\",\"datePublished\":\"2025-08-18T21:12:11+00:00\",\"dateModified\":\"2025-08-18T21:12:13+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/\"},\"wordCount\":1592,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/wpengine.com\/builders\/#organization\"},\"image\":{\"@id\":\"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/08\/New-Draft-Post-_-astro-headless-previews-scaled.jpeg\",\"keywords\":[\"Astro\",\"Preview\"],\"articleSection\":[\"Headless\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/\",\"url\":\"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/\",\"name\":\"Astro + WordPress: Post Previews - Builders\",\"isPartOf\":{\"@id\":\"https:\/\/wpengine.com\/builders\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/08\/New-Draft-Post-_-astro-headless-previews-scaled.jpeg\",\"datePublished\":\"2025-08-18T21:12:11+00:00\",\"dateModified\":\"2025-08-18T21:12:13+00:00\",\"description\":\"Learn how to implement post previews in Astro for headless WordPress using the HWP Previews plugin. This guide covers WPGraphQL, authentication, and ensuring a seamless content creator experience.\",\"breadcrumb\":{\"@id\":\"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/#primaryimage\",\"url\":\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/08\/New-Draft-Post-_-astro-headless-previews-scaled.jpeg\",\"contentUrl\":\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/08\/New-Draft-Post-_-astro-headless-previews-scaled.jpeg\",\"width\":2560,\"height\":1124},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/wpengine.com\/builders\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Astro + WordPress: Post Previews\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/wpengine.com\/builders\/#website\",\"url\":\"https:\/\/wpengine.com\/builders\/\",\"name\":\"Builders\",\"description\":\"Reimagining the way we build with WordPress.\",\"publisher\":{\"@id\":\"https:\/\/wpengine.com\/builders\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/wpengine.com\/builders\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/wpengine.com\/builders\/#organization\",\"name\":\"WP Engine\",\"url\":\"https:\/\/wpengine.com\/builders\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/wpengine.com\/builders\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2024\/05\/WP-Engine-Horizontal@2x.png\",\"contentUrl\":\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2024\/05\/WP-Engine-Horizontal@2x.png\",\"width\":348,\"height\":68,\"caption\":\"WP Engine\"},\"image\":{\"@id\":\"https:\/\/wpengine.com\/builders\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/x.com\/wpebuilders\",\"https:\/\/www.youtube.com\/channel\/UCh1WuL54XFb9ZI6m6goFv1g\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/wpengine.com\/builders\/#\/schema\/person\/868e9d9f8b846000f45cc7a25f46255a\",\"name\":\"Alex Moon\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/wpengine.com\/builders\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/999d8bf58fa6790945f2aff83a31c3df?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/999d8bf58fa6790945f2aff83a31c3df?s=96&d=mm&r=g\",\"caption\":\"Alex Moon\"},\"description\":\"Alex Moon is a Developer Advocate at WP Engine. He's been working in Open Source for over a decade and headless WordPress since the early days of WP GraphQL. When he's not writing code he's often found in the mountains or sailing the seas. Follow him on X or Bluesky for more headless WordPress goodness!\",\"sameAs\":[\"https:\/\/gurugoes.net\",\"https:\/\/x.com\/moon_meister\"],\"url\":\"https:\/\/wpengine.com\/builders\/author\/alexmoon-2-2-2-2-2-2-2-2-2-2-2\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Astro + WordPress: Post Previews - Builders","description":"Learn how to implement post previews in Astro for headless WordPress using the HWP Previews plugin. This guide covers WPGraphQL, authentication, and ensuring a seamless content creator experience.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/","og_locale":"en_US","og_type":"article","og_title":"Astro + WordPress: Post Previews","og_description":"Learn how to implement post previews in Astro for headless WordPress using the HWP Previews plugin. This guide covers WPGraphQL, authentication, and ensuring a seamless content creator experience.","og_url":"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/","og_site_name":"Builders","article_published_time":"2025-08-18T21:12:11+00:00","article_modified_time":"2025-08-18T21:12:13+00:00","og_image":[{"width":1024,"height":576,"url":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/08\/astro-preview-1024x576.png","type":"image\/png"}],"author":"Alex Moon","twitter_card":"summary_large_image","twitter_creator":"@moon_meister","twitter_site":"@wpebuilders","twitter_misc":{"Written by":"Alex Moon","Est. reading time":"8 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/#article","isPartOf":{"@id":"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/"},"author":{"name":"Alex Moon","@id":"https:\/\/wpengine.com\/builders\/#\/schema\/person\/868e9d9f8b846000f45cc7a25f46255a"},"headline":"Astro + WordPress: Post Previews","datePublished":"2025-08-18T21:12:11+00:00","dateModified":"2025-08-18T21:12:13+00:00","mainEntityOfPage":{"@id":"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/"},"wordCount":1592,"commentCount":0,"publisher":{"@id":"https:\/\/wpengine.com\/builders\/#organization"},"image":{"@id":"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/#primaryimage"},"thumbnailUrl":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/08\/New-Draft-Post-_-astro-headless-previews-scaled.jpeg","keywords":["Astro","Preview"],"articleSection":["Headless"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/","url":"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/","name":"Astro + WordPress: Post Previews - Builders","isPartOf":{"@id":"https:\/\/wpengine.com\/builders\/#website"},"primaryImageOfPage":{"@id":"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/#primaryimage"},"image":{"@id":"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/#primaryimage"},"thumbnailUrl":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/08\/New-Draft-Post-_-astro-headless-previews-scaled.jpeg","datePublished":"2025-08-18T21:12:11+00:00","dateModified":"2025-08-18T21:12:13+00:00","description":"Learn how to implement post previews in Astro for headless WordPress using the HWP Previews plugin. This guide covers WPGraphQL, authentication, and ensuring a seamless content creator experience.","breadcrumb":{"@id":"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/#primaryimage","url":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/08\/New-Draft-Post-_-astro-headless-previews-scaled.jpeg","contentUrl":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/08\/New-Draft-Post-_-astro-headless-previews-scaled.jpeg","width":2560,"height":1124},{"@type":"BreadcrumbList","@id":"https:\/\/wpengine.com\/builders\/astro-wordpress-post-previews\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/wpengine.com\/builders\/"},{"@type":"ListItem","position":2,"name":"Astro + WordPress: Post Previews"}]},{"@type":"WebSite","@id":"https:\/\/wpengine.com\/builders\/#website","url":"https:\/\/wpengine.com\/builders\/","name":"Builders","description":"Reimagining the way we build with WordPress.","publisher":{"@id":"https:\/\/wpengine.com\/builders\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/wpengine.com\/builders\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/wpengine.com\/builders\/#organization","name":"WP Engine","url":"https:\/\/wpengine.com\/builders\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/wpengine.com\/builders\/#\/schema\/logo\/image\/","url":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2024\/05\/WP-Engine-Horizontal@2x.png","contentUrl":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2024\/05\/WP-Engine-Horizontal@2x.png","width":348,"height":68,"caption":"WP Engine"},"image":{"@id":"https:\/\/wpengine.com\/builders\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/x.com\/wpebuilders","https:\/\/www.youtube.com\/channel\/UCh1WuL54XFb9ZI6m6goFv1g"]},{"@type":"Person","@id":"https:\/\/wpengine.com\/builders\/#\/schema\/person\/868e9d9f8b846000f45cc7a25f46255a","name":"Alex Moon","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/wpengine.com\/builders\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/999d8bf58fa6790945f2aff83a31c3df?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/999d8bf58fa6790945f2aff83a31c3df?s=96&d=mm&r=g","caption":"Alex Moon"},"description":"Alex Moon is a Developer Advocate at WP Engine. He's been working in Open Source for over a decade and headless WordPress since the early days of WP GraphQL. When he's not writing code he's often found in the mountains or sailing the seas. Follow him on X or Bluesky for more headless WordPress goodness!","sameAs":["https:\/\/gurugoes.net","https:\/\/x.com\/moon_meister"],"url":"https:\/\/wpengine.com\/builders\/author\/alexmoon-2-2-2-2-2-2-2-2-2-2-2\/"}]}},"_links":{"self":[{"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/posts\/31956","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/users\/25"}],"replies":[{"embeddable":true,"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/comments?post=31956"}],"version-history":[{"count":0,"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/posts\/31956\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/media\/31958"}],"wp:attachment":[{"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/media?parent=31956"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/categories?post=31956"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/tags?post=31956"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}