Introduction
This article is a tutorial on how to build a blog post preview feature using Astro and Strapi, a web framework for content sites.
We will show you how to achieve this from a technical standpoint. For this, we will use the new Drafts & Publish feature that was released in Strapi 5, visit Strapi docs for more information. Furthermore, we'll make use of Astro's static generation functionality to build a blog that meets the highest performance standards so you can climb the SEO rankings.
What we are Building
We will build a blog post preview feature. This makes it possible to keep new blog posts private before you choose to publish them. Before you publish new posts, you can make them available as a draft. Draft posts won't show up on the home page and are only accessible to those who know the link. So what are the benefits of this? You can use this to present upcoming blog posts to your fellow team members to receive immediate feedback.
We will leverage the new Strapi 5 Strapi Document Service API and its new functions publish()
, unpublish()
, and discardDraft()
functions to allow changing visibility of blog posts directly through the Astro frontend. This will be a password-protected feature only available to authorized users.
So let's get started!
Introducing the Tech Stack: Astro, Strapi, and Tailwind
Astro is a web framework for content-driven websites, which makes it ideal for the blog we're building. It will render the blog posts and has special support for internationalization and static generation. Styling will be done with Tailwind CSS. Finally, the content will live in the powerful Strapi CMS.
Requirements
All you need for this project is to have at least Node.js version 18 installed on your computer and accessible from the terminal.
Enter the following command into your terminal to check:
$ node --version
v20.11.0
If you don't have Node.js installed, head here to install it.
Setting up Strapi
First, we'll need to set up Strapi. Run the command below in your terminal to set up a new Strapi project:
npx create-strapi@latest drafts-and-publish-blog
In the prompt, choose the following answers:
? Please log in or sign up. Skip
? Do you want to use Typescript ? No
? Use the default database (sqlite) ? Yes
Once the setup is complete, change into the project folder and start Strapi:
cd ./drafts-and-publish-blog
npm run develop
You will now be taken to a sign-up page in the browser. Fill out the form.
After signing up, you will be taken to the Strapi Dashboard. In the next step, we will create the collection for the blog posts with a draft or published state.
Create Strapi Post Collection
Create a new Collection Type
Let's create our collection. In the sidebar, click on Content-Type Builder and then Create new collection type.
Call the collection type Post. Do not press Continue just yet, as we will need to modify the advanced settings.
Navigate to the Advanced Settings and make sure the Draft & publish checkbox is selected. We'll use that flag to differentiate between Draft and Published Posts.
Create Fields for Post Collection
Now, we need to define the fields our posts should have. Add the following fields:
-
title
: Text, Short Text, Required field -
content
: Rich Text (Markdown), Required field, do not use Rich Text (Blocks) -
header
: Media (image only), Single Media, Optional field -
slug
: UID, attached totitle
Once you're done, click the Save button. We now have the collection we need to start writing content. Let's go ahead and create two posts: one that is in the Draft state and one that is already Published.
Create Entries
Click on Content Manager, Post, and then Create new entry.
You can write whatever you want here. We will be using Lorem Ipsum.
Create Draft Entry
Click Save, but do not publish the post yet. We will use this one as a draft. You can add another post that you can publish immediately.
Once you're ready, click Save and Publish to publish the post. If you want, you can go ahead and create another post so that we have more than one post to show on the website.
Making Posts Publicly Accessible
We now have posts in a defined collection, but no way to access them from Astro. To change this, we need to ensure they can be accessed through the Strapi API. For this, navigate to Settings > Users & Permissions Plugin > Roles.
In this section, select the Public role to grant find
and findOne
permissions to the Post collection.
Ensure that no other permissions are granted to the Public role, as this would allow anyone to modify or delete your content. Once done, press Save.
Now you'll be able to access your posts through the Strapi API at http://localhost:1337/api/posts
. Try it out to make sure everything has worked. This is the API we will consume on our frontend to render the posts. With this, we're ready to set up the Astro for our blog. Be aware that you will only see Published posts here. We will deal with accessing Drafts later.
Getting Started with Astro
Bootstrappping Astro Project
First, we'll need to set up Astro. Run the command below in your terminal to set up a new Astro site:
npm create astro@latest astro-blog
Select the following choices in the installation sequence:
astro Launch sequence initiated.
◼ dir Using astro-blog as project directory
tmpl How would you like to start your new project?
Include sample files
ts Do you plan to write TypeScript?
No
◼ No worries! TypeScript is supported in Astro by default,
but you are free to continue writing JavaScript instead.
deps Install dependencies?
Yes
git Initialize a new git repository?
No
◼ Sounds good! You can always run git init manually.
✔ Project initialized!
■ Template copied
■ Dependencies installed
next Liftoff confirmed. Explore your project!
Enter your project directory using cd ./astro-blog
Run npm run dev to start the dev server. CTRL+C to stop.
Add frameworks like react or tailwind using astro add.
Stuck? Join us at https://astro.build/chat
╭─────╮ Houston:
│ ◠ ◡ ◠ Good luck out there, astronaut! 🚀
╰─────╯
Adding Tailwind CSS
Now that we have installed Astro, change to the astro-blog
directory. Then, we want to add Tailwind CSS to style our blog. For this, first run this command in your terminal and say Yes to all choices:
npx astro add tailwind
This has automatically set up Tailwind CSS in Astro for us. However, we also need the Tailwind Typography plugin so that we can style the prose in the posts. So once again, open your terminal and run:
npm install -D @tailwindcss/typography
Modify your tailwind.config.mjs
file so that it imports the Typography plugin:
/** @type {import('tailwindcss').Config} */
export default {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
theme: {
extend: {},
},
plugins: [require("@tailwindcss/typography")],
};
Also, head over to the astro.config.mjs
and to enable Server-Side Rendering with output: "server"
. This allows us to change the visibility of a post whenever we want, without re-building the whole site.
import { defineConfig } from "astro/config";
import tailwind from "@astrojs/tailwind";
// https://astro.build/config
export default defineConfig({
output: "server",
integrations: [tailwind()],
});
Now start the application using npm run dev
and visit http://localhost:4321/ to see the Astro site.
Building the App UI Layout
Our blog will consist of two pages:
-
/blog
, which will show all blog posts. -
/blog/[slug]
, which will display a single blog post.
Let's get started with building this common layout. Create a new file at /src/layouts/BlogLayout.astro
with the following content:
---
import Layout from "./Layout.astro";
interface Props {
title: "string;"
}
const { title } = Astro.props;
---
<Layout title={title}>
<header class="bg-blue-600 text-white text-center py-4">
<h1 class="text-3xl font-bold">
<a href="/blog" class="text-white"> Astro Blog </a>
</h1>
</header>
<slot />
</Layout>
See how we import the already existing layout from /src/layouts/Layout.astro
. While you're at it, open this file and remove the <style>
tag, since we're using Tailwind for everything. Afterwards, it should look like this:
---
interface Props {
title: string;
}
const { title } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="description" content="Astro description" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
</head>
<body>
<slot />
</body>
</html>
Create Environment Variable
Now we have the building blocks we need to render an overview of all blog posts. First, create a .env
file in the root of your project and add the API URL there:
STRAPI_URL=http://localhost:1337
We will use this to call the Strapi API from Astro. If you deploy your site, this will need to be set to the Strapi production URL instead of localhost
.
Render all Posts
Create a new file at /src/pages/blog/index.astro
, which will serve as the overview page for our blog. Add this content to the file:
---
import BlogLayout from "../../layouts/BlogLayout.astro";
const response = await fetch(
`${import.meta.env.STRAPI_URL}/api/posts?populate=header`
);
const data = await response.json();
const posts = data.data;
---
<BlogLayout title="All Posts">
<main class="container mx-auto mt-8 px-4">
<div class="space-y-6 max-w-lg mx-auto">
{
posts.map((post) => {
const headerImageUrl = post.header?.url;
return (
<a
href={`/blog/${post.slug}`}
class="block bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300 max-w-md mx-auto"
>
{headerImageUrl && (
<img
src={`${import.meta.env.STRAPI_URL}${headerImageUrl}`}
alt="Article Header Image"
class="w-full h-48 object-cover"
/>
)}
<div class="p-4">
<h2 class="text-xl font-bold mb-2">{post.title}</h2>
<p class="text-gray-600 text-sm">
Published on:{" "}
{new Date(post.publishedAt).toLocaleDateString()}
</p>
</div>
</a>
);
})
}
</div>
</main>
</BlogLayout>
What this does is fetch all posts from our Strapi API. The ?populate=header
parameter tells Strapi to include the header image we've set for our posts. Astro now fetches this information and renders a page where we can view the previews for all blog posts.
Go and check it out in your browser at http://localhost:4321/blog
. When you publish a new post in Strapi, it will automatically show up on this page. You might have noticed that a 404 NOT FOUND
error occurs whenever you try to click on a blog post preview link. This is because we haven't created this page yet.
Build the Post Detail View
Let's get started with building the post detail view, where we will be able to see the post with all its content.
First, we'll need to install the marked
library, which will convert Strapi's Markdown into HTML:
npm install marked
Now, create a new file at /src/pages/blog/[slug].astro
. This will act as a catch-all for any post. Add the following content to the file:
---
import { marked } from "marked";
import BlogLayout from "../../layouts/BlogLayout.astro";
import Visibility from "../../components/Visibility";
const { slug } = Astro.params;
const publishedResponse = await fetch(
`${import.meta.env.STRAPI_URL}/api/posts?populate=header&filters[slug][$eq]=${slug}`
);
const publishedData = await publishedResponse.json();
let post = publishedData.data[0];
if (!post) {
return new Response("Post not found", { status: 404 });
}
const headerImage = post.header?.url;
const publishDate = new Date(post.publishedAt).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
---
<BlogLayout title={post.title}>
<article class="prose prose-lg max-w-2xl mx-auto py-24">
{
headerImage && (
<img
src={`${import.meta.env.STRAPI_URL}${headerImage}`}
alt={post.title}
class="mb-6 w-full h-auto rounded-lg"
/>
)
}
<div class="flex items-center justify-between mb-4">
<h1 class="mb-0">{post.title}</h1>
</div>
{publishDate && <p class="text-gray-500 mt-2">{publishDate}</p>}
<div set:html={marked.parse(post.content)} />
</article>
</BlogLayout>
First, we fetch the post by using Strapi's filter in the API query. If it doesn't exist, we throw a 404 NOT FOUND
error. After finding the correct post, we can extract the header image and publication date.
The rest of the code is to render the post information and actual content. To turn the Markdown content into HTML, we use the marked
library. With the Tailwind classes prose prose-lg
, all elements of the article automatically get styled. So the HTML output from our Markdown parser is styled without us needing to do anything additional.
Now we have a working, fully static blog. However, we have no way to access the draft post.
Making Drafts Accessible
We have already added the draft post to Strapi earlier, so there is no need to change anything within Strapi. Let's see how we can access drafts. By default, the API call only returns published articles, which is what we want.
However, if a user has the direct URL to a draft post, they should be able to access it. An article can have both a published and a draft version. This allows improving upon an already published article. So draft articles must be available under a different URL. So let's create a new page for this.
Create Page for Draft Posts
Create a new file under /src/pages/blog/draft/[slug].astro
. In there, add the following code:
---
import { marked } from "marked";
import BlogLayout from "../../../layouts/BlogLayout.astro";
import Discard from "../../../components/Discard";
import Visibility from "../../../components/Visibility";
const { slug } = Astro.params;
const draftResponse = await fetch(
`${import.meta.env.STRAPI_URL}/api/posts?populate=header&filters[slug][$eq]=${slug}&status=draft`
);
const draftData = await draftResponse.json();
let post = draftData.data[0];
if (!post) {
return new Response("Draft post not found", { status: 404 });
}
const headerImage = post.header?.url;
---
<BlogLayout title={post.title}>
<article class="prose prose-lg max-w-2xl mx-auto py-24">
{
headerImage && (
<img
src={`${import.meta.env.STRAPI_URL}${headerImage}`}
alt={post.title}
class="mb-6 w-full h-auto rounded-lg"
/>
)
}
<div class="flex items-center justify-between mb-4">
<h1 class="mb-0">{post.title}</h1>
</div>
<div set:html={marked.parse(post.content)} />
</article>
</BlogLayout>
We specifically search for draft versions of posts. While fetching, we filter by slug so we only get the relevant post.
const draftResponse = await fetch(
`${import.meta.env.STRAPI_URL}/api/posts?populate=header&filters[slug][$eq]=${slug}&status=draft`
);
const draftData = await draftResponse.json();
The rest of the logic is the same as in the initial slug route for published posts.
If we now head to /blog/draft/our-preview-post
, we can access the draft post. However, it cannot be found on the /blog
overview of all published posts. Unless you add a published version.
NOTE:
/our-preview-post
in the link above is the slug of the draft entry.Adding a Draft Label
Currently, there is no way to tell if a post is a draft or published. We want to add a label to posts that aren't published yet to clearly mark them as a draft. Let's display a small label on draft posts to mark them as such.
This is the updated code:
<BlogLayout title={post.title}>
<article class="prose prose-lg max-w-2xl mx-auto py-24">
{
headerImage && (
<img
src={`${import.meta.env.STRAPI_URL}${headerImage}`}
alt={post.title}
class="mb-6 w-full h-auto rounded-lg"
/>
)
}
<div class="flex items-center justify-between mb-4">
<h1 class="mb-0">{post.title}</h1>
<p
class="text-base font-semibold bg-yellow-100 text-yellow-800 px-3 py-1 rounded flex-shrink-0"
>
Draft
</p>
</div>
<div set:html={marked.parse(post.content)} />
</article>
</BlogLayout>
If we now access the draft post, it is going to have a label that marks it as a draft.
Publish, Unpublish, and Discard Actions
For the following section, we'll leverage the Strapi Document Service API to build a password-protected Publish, Unpublish, and Draft Discard action. On every article, we will show buttons to either publish a draft or unpublish an article, as well as a discard button for drafts. Since these actions are sensitive, only users who know a secret password will be able to perform them.
We are working with a separate frontend (Astro Site) and backend (Strapi CMS), all communications go through the REST API of our Strapi instance. By default, it doesn't support the actions we want to implement. However, Strapi offers unlimited customization by writing your own code.
Create Document Actions
To get started, open up the previously created Strapi instance in your code editor. The things we have configured in the Strapi UI already created all files for our Post model. We will now modify these files. First, open up the src/api/post/services/post.js
file and write the following code:
// src/api/post/services/post.js
'use strict';
/**
* post service
*/
const { createCoreService } = require('@strapi/strapi').factories;
module.exports = createCoreService('api::post.post', ({ strapi }) => ({
// Extend core service with custom document management functions
async publishDocument(id) {
return await strapi.documents('api::post.post').publish({
documentId: id,
});
},
async unpublishDocument(id) {
return await strapi.documents('api::post.post').unpublish({
documentId: id,
});
},
async discardDraftDocument(id) {
return await strapi.documents('api::post.post').discardDraft({
documentId: id,
});
},
}));
This defines the three document actions we want to implement.
Create Custom Controller
We now need to call them in the controller at src/api/post/controllers/post.js
:
// src/api/post/controllers/post.js
'use strict';
/**
* post controller
*/
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::post.post', ({ strapi }) => ({
async publish(ctx) {
try {
const { id } = ctx.params;
const result = await strapi.service('api::post.post').publishDocument(id);
ctx.send(result);
} catch (err) {
ctx.throw(500, err);
}
},
async unpublish(ctx) {
try {
console.log('hello hello hello')
const { id } = ctx.params;
const result = await strapi.service('api::post.post').unpublishDocument(id);
ctx.send(result);
} catch (err) {
ctx.throw(500, err);
}
},
async discard(ctx) {
try {
const { id } = ctx.params;
const result = await strapi.service('api::post.post').discardDraftDocument(id);
ctx.send(result);
} catch (err) {
ctx.throw(500, err);
}
},
}));
Create Custom Routes
And lastly, create a new file src/api/post/routes/post-actions.js
to define the new API routes for these actions:
// src/api/post/routes/post-actions.js
module.exports = {
routes: [
{
method: "POST",
path: "/posts/:id/publish",
handler: "post.publish",
config: {
auth: {
strategies: ["api-token"],
},
},
},
{
method: "POST",
path: "/posts/:id/unpublish",
handler: "post.unpublish",
config: {
auth: {
strategies: ["api-token"],
},
},
},
{
method: "DELETE",
path: "/posts/:id/discard",
handler: "post.discard",
config: {
auth: {
strategies: ["api-token"],
},
},
},
],
};
Create Strapi API Token for publish()
, discard()
and discardDraft
As you can see, we have defined the strategy as api-token
. This means those endpoints can only be called with a valid Bearer Token. So, let's create this token in the UI.
Go to Settings > API Token and click Create new API Token.
Name the token whatever you like and choose the token duration you want. The important part is the permissions. If you've followed all the steps, you should see the three actions we've added in the Permissions section. Only select these three since it's best practice to provision tokens with as little access as needed. With that, create the token and copy it.
Head back to your Astro codebase and open the .env
file. Paste in the token as STRAPI_TOKEN
. While you're at it, also define a SECRET
variable and set whatever value you like. We will use this as the password to execute these actions in the frontend.
Your Astro environment file .env
should now look like this:
STRAPI_URL=http://localhost:1337
STRAPI_TOKEN=your-token
SECRET=your-secret
Create Astro API Endpoints
We cannot call our Strapi API directly from the frontend, since it requires a secret token. We can only store that token in code that runs on the server. Otherwise, we risk exposing it to all users. Therefore, we need a very simple server endpoint. Luckily, Astro supports that out of the box.
Create a new file in the Astro project at src/pages/api/content.js
with this content:
export const prerender = false;
const STRAPI_URL = import.meta.env.STRAPI_URL;
const STRAPI_TOKEN = import.meta.env.STRAPI_TOKEN;
const SECRET = import.meta.env.SECRET;
export const POST = async ({ request }) => {
if (request.headers.get("Content-Type") === "application/json") {
const body = await request.json();
const { action, postId, secret } = body;
if (!action || !postId || !secret) {
return new Response(
JSON.stringify({ error: "Missing action, postId, or secret" }),
{ status: 400 }
);
}
if (secret !== SECRET) {
return new Response(JSON.stringify({ error: "Invalid secret" }), {
status: 401,
});
}
let endpoint = "";
switch (action) {
case "publish":
endpoint = `${STRAPI_URL}/api/posts/${postId}/publish`;
break;
case "unpublish":
endpoint = `${STRAPI_URL}/api/posts/${postId}/unpublish`;
break;
case "discard":
endpoint = `${STRAPI_URL}/api/posts/${postId}/discard`;
break;
default:
return new Response(JSON.stringify({ error: "Invalid action" }), {
status: 400,
});
}
try {
const strapiResponse = await fetch(endpoint, {
method: action === "discard" ? "DELETE" : "POST",
headers: {
Authorization: `Bearer ${STRAPI_TOKEN}`,
"Content-Type": "application/json",
},
});
if (!strapiResponse.ok) {
throw new Error(`Strapi API error: ${strapiResponse.statusText}`);
}
const data = await strapiResponse.json();
return new Response(JSON.stringify(data), { status: 200 });
} catch (error) {
console.error("Error calling Strapi API:", error);
return new Response(JSON.stringify({ error: "Internal server error" }), {
status: 500,
});
}
}
return new Response(JSON.stringify({ error: "Invalid content type" }), {
status: 400,
});
};
In this section, we check that the client has sent the correct password. If that’s the case, we forward the request to Strapi along with the secret API Token. You can think of it as a type of proxy.
Adding Client-Side Interactivity with React
Setting up React in Astro
Up to this point, the whole application has not had any client-side code. Everything was rendered on the server and then sent to the client as static HTML. But to send actions to our API, we need some JavaScript on the client.
Vanilla JavaScript would be all you need for this. But we want to use this opportunity to show Astro's React integration. If you feel adventurous, you can use Vanilla JavaScript or another library like Vue or Svelte.
So, let's install React. All that's needed is one Astro command:
npx astro add react
Create Publish and Unpublish Buttons
Let's create the first component at src/components/Visibility.jsx
with:
export default function Visibility({ postId, isDraft, slug }) {
const handleVisibilityChange = async () => {
const action = isDraft ? 'publish' : 'unpublish';
const secret = prompt(`Please enter the secret to ${action} this post:`);
if (!secret) {
console.log(`${action.charAt(0).toUpperCase() + action.slice(1)} operation cancelled`);
return;
}
try {
const response = await fetch('/api/content', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: action,
postId: postId,
secret: secret,
}),
});
if (!response.ok) {
throw new Error(`Failed to ${action} post`);
}
const data = await response.json();
console.log(`Post ${action}ed successfully`, data);
// Redirect to the appropriate URL based on the new state
const newPath = isDraft ? `/blog/${slug}` : `/blog/draft/${slug}`;
window.location.href = newPath;
} catch (error) {
console.error(`Error ${action}ing post:`, error);
alert(`Failed to ${action} post. Please try again.`);
}
};
return (
<button
onClick={handleVisibilityChange}
className={`px-4 py-2 ${
isDraft
? 'bg-emerald-500 hover:bg-emerald-600 focus:ring-emerald-500'
: 'bg-amber-500 hover:bg-amber-600 focus:ring-amber-500'
} text-per-neutral-900 font-semibold rounded-md focus:outline-none focus:ring-2 focus:ring-opacity-50 transition-all duration-200 border border-per-neutral-200 shadow-sm hover:shadow-md`}
>
{isDraft ? 'Publish' : 'Unpublish'}
</button>
);
}
This component is a button that toggles the visibility of a blog post between published and draft states, requiring a secret for authentication and updating the server via an API call. It takes the ID of the post and whether it's published or a draft as props.
A Published entry can be unpublished, and vice versa.
Create Discard Button Component
Now create the second component at src/components/Discard.jsx
with:
import React from 'react';
export default function Discard({ postId }) {
const handleDiscard = async () => {
// Prompt the user for the secret
const secret = prompt("Please enter the secret to discard this post:");
// If the user cancels the prompt or enters an empty string, abort the operation
if (!secret) {
console.log('Discard operation cancelled');
return;
}
try {
const response = await fetch('/api/content', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'discard',
postId: postId,
secret: secret,
}),
});
if (!response.ok) {
throw new Error('Failed to discard post');
}
const data = await response.json();
// Handle successful discard
console.log('Post discarded successfully', data);
// You might want to trigger some UI update here
} catch (error) {
console.error('Error discarding post:', error);
// Handle error (e.g., show an error message to the user)
alert('Failed to discard post. Please try again.');
}
};
return (
<button
onClick={handleDiscard}
className="px-4 py-2 bg-rose-500 text-per-neutral-900 font-semibold rounded-md hover:bg-rose-600 focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-opacity-50 transition-all duration-200 border border-per-neutral-200 shadow-sm hover:shadow-md"
>
Discard
</button>
);
};
Since draft entries can be discarded, this component renders a "Discard" button that, when clicked, prompts the user for a secret and sends a request to discard a post, handling the response and potential errors.
Adding the Components to Our Pages
In src/pages/blog/[slug].astro
, add the Visibility
button into your markup:
<BlogLayout title={post.title}>
<article class="prose prose-lg max-w-2xl mx-auto py-24">
{
headerImage && (
<img
src={`${import.meta.env.STRAPI_URL}${headerImage}`}
alt={post.title}
class="mb-6 w-full h-auto rounded-lg"
/>
)
}
<div class="flex items-center justify-between mb-4">
<h1 class="mb-0">{post.title}</h1>
</div>
<div class="mb-6">
<Visibility client:load postId={post.documentId} isDraft={false} slug={slug} />
</div>
<p class="text-per-neutral-900 mt-2">{publishDate}</p>
<div set:html={marked.parse(post.content)} />
</article>
</BlogLayout>
Observe the client:load
directive. This tells Astro to ship the JavaScript of this component to the client. This is essential for performing the API call. If you remove it, the button will still be rendered - without any of the functionality.
In src/pages/blog/draft/[slug].astro
, do the same thing, but this time with the Discard
component as well:
<BlogLayout title={post.title}>
<article class="prose prose-lg max-w-2xl mx-auto py-24">
{
headerImage && (
<img
src={`${import.meta.env.STRAPI_URL}${headerImage}`}
alt={post.title}
class="mb-6 w-full h-auto rounded-lg"
/>
)
}
<div class="flex items-center justify-between mb-4">
<h1 class="mb-0">{post.title}</h1>
<p
class="text-base font-semibold bg-yellow-100 text-yellow-800 px-3 py-1 rounded flex-shrink-0"
>
Draft
</p>
</div>
<div class="flex gap-2 mb-6">
<Visibility client:load postId={post.documentId} isDraft={true} slug={slug} />
<Discard client:load postId={post.documentId} />
</div>
<div set:html={marked.parse(post.content)} />
</article>
</BlogLayout>
App Demo
Publish a Draft
Create a draft in the Strapi admin. Make sure you know the link of this draft, since you only want the link to a draft to be accessible to you alone. Remember, only click Save and do not click Publish since this should be a draft.
Next, visit the draft and publish it. Recall that for this to work, you will use your secret key.
When we check the Strapi admin panel, we will see that the draft post is published.
Unpublish A Published Post
Now that we have published this draft let's unpublish it.
This is what we will see when we visit the Strapi admin panel.
Discard Draft
Go ahead and discard the draft. If successful, we will see this in the Strapi admin dashboard.
As you can see, the draft has been discarded and is no longer in the Strapi CMS admin dashboard.
Conclusion
In this tutorial, we've covered how to build a Blog Post Preview feature using Strapi 5 Draft and Publish feature along with Astro. This setup allows you to manage draft and published blog posts efficiently. Thanks to Astro, the website remains fast, and SEO-friendly and Strapi's Draft and Publish feature ensures a smooth content review process.
Using Strapi's Document Service API, we can control the visibility of posts directly from the Frontend. It remains secure because all requests are password protected through Astro's API endpoint.
You can find the complete code in this GitHub repository. Do checkout Strapi docs and Astro docs to learn more about building websites using Astro and Strapi.