The introduction of Paywalls for blogs and articles has been a gamechanger for the writing industry. Companies like Medium have attracted millions of writers who would love to be paid for their writing. Readers too have learnt to appreciate great premium content from their favorite writers. This provides a need to integrate paywalls to your blogs to attract writers and readers and also make money.
This article will guide you on how to create and manage your blog through Strapi CMS and Vue.js frontend for a seamless reader and writer experience. You will also integrate a paywall using Paystack, a popular African payment gateway.
To follow through this tutorial, you need to have the following:
>=18.x.x
and <=20.x.x
installed in your machineInstall Strapi 5 using npx:
npx create-strapi@latest my-blog --quickstart
This command creates a new Strapi project that is kept in the myblog
directory.
The --quickstart
flag is used as a convenient way to quickly create a new Strapi project with minimal configuration. It uses SQLite as the default database and applies default settings to the project.
Navigate to the project directory, myblog
, using the command below:
cd my-blog
Start the Strapi server:
npm run develop
Note: If you use the
--quickstart
flag during installation, you don't need to manually start the server as it is run automatically.
After Strapi is running, navigate to Strapi's default admin URL http://localhost:1337/admin/. Sign up using your personal details to access the dashboard.
To do this, navigate to Content-Type-Builder. Under Collection Types, click Create new collection type.
This prompts you to name your collection. Name it Blog
.
Add the following fields:
Title
(Text)Content
(Rich Text)Is_Paid
(Boolean)Caption
(Media - Single Media)It should look like this before saving:
After saving the fields, create a few blog post entries with some of them with Is_Paid
set as true.
If you haven't already, install Vue CLI:
npm install -g @vue/cli
Create a new Vue application:
vue create blog-frontend
Since you'll be using Vue 3, make sure to select Vue 3 during installation.
Navigate to the project directory:
cd blog-frontend
Test the installation by running the server:
npm run serve
After Paystack login, From the Dashboard, navigate to Settings > API Keys & Webhooks to get your public and secret keys. Copy and save them later when we will perform Paystack intregration into your blog app.
First, install the required libraries:
npm install vue-paystack axios @paystack/inline-js vue-router@4
This command installs the necessary libraries for integrating Paystack payments into a Vue.js application using Strapi:
Inside the public/index.html
file, add the @paystack/inline-js
script within the head
tag:
1<script src="https://js.paystack.co/v1/inline.js"></script>
Create a new file called PaystackButton.vue
in your src/components
directory and add the code below:
1<template>
2 <div>
3 <button @click="payWithPaystack" :disabled="!email">Pay Now</button>
4 </div>
5</template>
6
7<script setup>
8import { onMounted } from 'vue'
9
10const props = defineProps({
11 email: {
12 type: String,
13 required: true
14 },
15 amount: {
16 type: Number,
17 required: true
18 },
19 reference: {
20 type: String,
21 required: true
22 }
23})
24
25const emit = defineEmits(['payment-success', 'payment-closed'])
26
27let PaystackPop
28
29onMounted(() => {
30 PaystackPop = window.PaystackPop
31})
32
33const payWithPaystack = () => {
34 if (PaystackPop) {
35 const handler = PaystackPop.setup({
36 key: 'YOUR_PAYSTACK_PUBLIC_KEY', // Replace with your actual public key
37 email: props.email,
38 amount: props.amount * 100, // Amount is in USD
39 currency: 'USD', // set currency to USD
40 ref: props.reference,
41 callback: (response) => {
42 emit('payment-success', response)
43 },
44 onClose: () => {
45 emit('payment-closed')
46 }
47 })
48 handler.openIframe()
49 } else {
50 console.error('PaystackPop not loaded')
51 }
52}
53</script>
This component provides a user-friendly way to initiate Paystack payments within your Vue application. It handles setting up payment details, success/closure events, and integrates with the external Paystack library for processing transactions.
After successful Paystack login, replace YOUR_PAYSTACK_PUBLIC_KEY
with your actual Paystack public key.
In the src/components
directory, create a BlogList.vue
file component and update it to include the Read More button to access blog details:
1<template>
2 <div>
3 <h1>Blog Posts</h1>
4 <div v-if="loading">Loading...</div>
5 <div v-else-if="error">{{ error }}</div>
6 <div v-else>
7 <div v-for="blog in blogs" :key="blog.documentId" class="blog-preview">
8 <h2>{{ blog.Title }}</h2>
9 <p v-if="blog.Content">
10 {{ getContentPreview(blog.Content) }}
11 </p>
12 <router-link :to="{ name: 'BlogDetail', params: { id: blog.documentId } }" class="read-more">
13 Read More
14 </router-link>
15 </div>
16 </div>
17 </div>
18</template>
19
20<script setup>
21import { ref, onMounted } from 'vue'
22import api from '../api'
23
24const blogs = ref([])
25const loading = ref(true)
26const error = ref(null)
27
28const fetchBlogs = async () => {
29 try {
30 loading.value = true
31 const response = await api.get('/blogs')
32 blogs.value = response.data.data
33 } catch (err) {
34 console.error('Error fetching blogs:', err)
35 error.value = 'An error occurred while fetching blogs. Please try again later.'
36 } finally {
37 loading.value = false
38 }
39}
40
41const getContentPreview = (content) => {
42 if (typeof content === 'string') {
43 return content.substring(0, 100) + '...'
44 } else if (content && content.length > 0) {
45 // Assuming the rich text field returns an array of blocks
46 return content[0].children
47 .map(child => child.text)
48 .join(' ')
49 .substring(0, 100) + '...'
50 }
51 return 'No content available'
52}
53
54onMounted(() => {
55 fetchBlogs()
56})
57</script>
58
59<style scoped>
60.blog-preview {
61 margin-bottom: 30px;
62 border-bottom: 1px solid #eee;
63 padding-bottom: 20px;
64}
65
66.blog-preview h2 {
67 margin-bottom: 10px;
68}
69
70.read-more {
71 display: inline-block;
72 margin-top: 10px;
73 color: #42b983;
74 text-decoration: none;
75 font-weight: bold;
76}
77</style>
/blogs
) and displays a list of blog posts with informative loading and error states.getContentPreview
to display a preview of the blog post content, handling different content formats (string or rich text).In the src/components
directory, create a BlogDetails.vue
file component and update it to include the Paystack pay button before accessing blog details for paid content:
1<template>
2 <div>
3 <div v-if="loading">Loading...</div>
4 <div v-else-if="error">{{ error }}</div>
5 <div v-else-if="blog">
6 <h1>{{ blog.Title }}</h1>
7 <img v-if="blog.Caption && blog.Caption.url"
8 :src="getImageUrl(blog.Caption.url)"
9 alt="Blog Caption"
10 class="blog-image" />
11 <div v-if="!blog.Is_Paid || isPaid">
12 <div v-html="parseContent(blog.Content)"></div>
13 </div>
14 <div v-else>
15 <p>This is a paid article. Please pay to read the full content.</p>
16 <paystack-button
17 :email="userEmail"
18 :amount="5"
19 :reference="generateReference()"
20 @payment-success="handlePaymentSuccess"
21 @payment-closed="handlePaymentClosed"
22 />
23 </div>
24 </div>
25 </div>
26</template>
27
28<script setup>
29import { ref, onMounted } from 'vue'
30import { useRoute } from 'vue-router'
31import api from '../api'
32import PaystackButton from './PaystackButton.vue'
33
34const route = useRoute()
35const blog = ref(null)
36const isPaid = ref(false)
37const userEmail = ref('user@example.com') // In a real app, this would come from user authentication
38const loading = ref(true)
39const error = ref(null)
40
41const fetchBlog = async () => {
42 try {
43 loading.value = true
44 const response = await api.get(`/blogs/${route.params.id}?populate=*`)
45 blog.value = response.data.data
46 } catch (err) {
47 console.error('Error fetching blog:', err)
48 error.value = 'An error occurred while fetching the blog. Please try again later.'
49 } finally {
50 loading.value = false
51 }
52}
53
54const generateReference = () => {
55 return `BLOG_${route.params.id}_${Date.now()}`
56}
57
58const handlePaymentSuccess = async (response) => {
59 console.log('Payment successful:', response)
60 isPaid.value = true
61 // Here you would typically update the backend to record the payment
62 try {
63 await api.post('/payments', {
64 blogId: blog.value.documentId, // Use documentId for the blog
65 paymentReference: response.reference,
66 amount: response.amount,
67 })
68 } catch (err) {
69 console.error('Error recording payment:', err)
70 }
71}
72const parseContent = (content) => {
73 if (Array.isArray(content)) {
74 return content.map(block => {
75 if (block.type === 'paragraph') {
76 return `<p>${block.children.map(child => child.text).join('')}</p>`
77 }
78 // Add more conditions for other block types if needed
79 return ''
80 }).join('')
81 }
82 return content
83}
84
85const getImageUrl = (imageUrl) => {
86 if (imageUrl) {
87 return `http://localhost:1337${imageUrl}`
88 }
89 return ''
90}
91
92const handlePaymentClosed = () => {
93 console.log('Payment window closed')
94}
95
96onMounted(() => {
97 fetchBlog()
98})
99</script>
100
101<style scoped>
102.blog-image {
103width: 500px;
104height: auto;
105}
106</style>
This component displays the details of a specific blog post retrieved based on the route parameter.
Is_Paid
attribute and offering a Paystack button for payment.PaystackButton
component for payment processing.In the src/
directory, create a views/HomeView.vue
file view and update it to display the blog list:
1<template>
2 <div class="home">
3 <h1>Welcome to My Blog</h1>
4 <p>Here you can find a collection of articles on various topics.</p>
5 <router-link to="/blogs" class="cta-button">View All Articles</router-link>
6 </div>
7 </template>
8
9 <style scoped>
10 .home {
11 text-align: center;
12 }
13
14 h1 {
15 font-size: 2.5em;
16 margin-bottom: 20px;
17 }
18
19 p {
20 font-size: 1.2em;
21 margin-bottom: 30px;
22 }
23
24 .cta-button {
25 display: inline-block;
26 background-color: #42b983;
27 color: white;
28 padding: 10px 20px;
29 border-radius: 5px;
30 text-decoration: none;
31 font-weight: bold;
32 transition: background-color 0.3s;
33 }
34
35 .cta-button:hover {
36 background-color: #3aa876;
37 }
38 </style>
This view component serves as the homepage of the blog application.
In the src/
directory, create a routers/index.js
file and update it to include routers to your views and components:
1import { createRouter, createWebHistory } from 'vue-router'
2import Home from '../views/HomeView.vue'
3import BlogList from '../components/BlogList.vue'
4import BlogDetail from '../components/BlogDetail.vue'
5
6const routes = [
7 {
8 path: '/',
9 name: 'Home',
10 component: Home
11 },
12 {
13 path: '/blogs',
14 name: 'BlogList',
15 component: BlogList
16 },
17 {
18 path: '/blogs/:id',
19 name: 'BlogDetail',
20 component: BlogDetail
21 }
22]
23
24const router = createRouter({
25 history: createWebHistory(),
26 routes
27})
28
29export default router
This code configures the routing system for a Vue.js application using Vue Router. It defines the available routes and their corresponding components, allowing users to navigate between different pages or views within the application.
Create a file src/api.js
to centralize your API configuration and add the code below:
1import axios from 'axios'
2
3const api = axios.create({
4 baseURL: 'http://localhost:1337/api',
5 headers: {
6 'Authorization': `Bearer ${process.env.VUE_APP_STRAPI_BEARER_TOKEN}`
7 }
8})
9
10export default api
This code creates an Axios instance configured to interact with the Strapi API at the specified base URL.
blog-frontend
.env
file in your project root.1VUE_APP_STRAPI_BEARER_TOKEN=YOUR_STRAPI_API_TOKEN
Replace YOUR_STRAPI_API_TOKEN
with your actual Strapi API token..env
to your .gitignore
file to keep the token secure
Your API configuration is now set up with Bearer token authentication.For Strapi v5 or later, in the strapi app directory, my-blog
you will update ./config/middlewares.js
as follows:
1module.exports = [
2 'strapi::errors',
3 {
4 name: 'strapi::security',
5 config: {
6 contentSecurityPolicy: {
7 useDefaults: true,
8 directives: {
9 'connect-src': ["'self'", 'https:'],
10 'img-src': ["'self'", 'data:', 'blob:', 'https://yourvueapp.com'],
11 'media-src': ["'self'", 'data:', 'blob:', 'https://yourvueapp.com'],
12 upgradeInsecureRequests: null,
13 },
14 },
15 },
16 },
17 {
18 name: 'strapi::cors',
19 config: {
20 origin: ['http://localhost:8080', 'https://yourvueapp.com'],
21 headers: ['*'],
22 methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'],
23 credentials: true,
24 },
25 },
26 'strapi::poweredBy',
27 'strapi::logger',
28 'strapi::query',
29 'strapi::body',
30 'strapi::session',
31 'strapi::favicon',
32 'strapi::public',
33];
Save the file and restart your Strapi server for the changes to take effect.
Note Only allow origins that actually need access to your Strapi API.
In the src
directory, update the App.vue
file as follows to load the Home page view:
1<template>
2 <div id="app">
3 <header>
4 <nav>
5 <router-link to="/">Home</router-link> |
6 <router-link to="/blogs">Blogs</router-link>
7 </nav>
8 </header>
9
10 <main>
11 <router-view></router-view>
12 </main>
13
14 <footer>
15 <p>© 2024 My Blog Application</p>
16 </footer>
17 </div>
18</template>
19
20<style>
21#app {
22 font-family: Arial, sans-serif;
23 -webkit-font-smoothing: antialiased;
24 -moz-osx-font-smoothing: grayscale;
25 color: #2c3e50;
26 max-width: 800px;
27 margin: 0 auto;
28 padding: 20px;
29}
30
31header {
32 margin-bottom: 20px;
33}
34
35nav {
36 padding: 20px 0;
37}
38
39nav a {
40 font-weight: bold;
41 color: #2c3e50;
42 text-decoration: none;
43 margin-right: 10px;
44}
45
46nav a.router-link-exact-active {
47 color: #42b983;
48}
49
50main {
51 min-height: 300px;
52}
53
54footer {
55 margin-top: 40px;
56 text-align: center;
57 font-size: 0.9em;
58 color: #666;
59}
60</style>
This component serves as the foundation for the application, providing the overall structure and navigation while allowing for dynamic content rendering through the router.
Update the main.js
file to include the routers:
1import { createApp } from 'vue'
2import App from './App.vue'
3import router from './router'
4
5createApp(App).use(router).mount('#app')
Next, in the root directory, create a .eslintrc.js
file and add the following:
1module.exports = {
2 env: {
3 node: true,
4 },
5 extends: [
6 'eslint:recommended',
7 'x:vue/vue3-recommended',
8 ],
9
10 globals: {
11 defineProps: "readonly",
12 defineEmits: "readonly",
13 defineExpose: "readonly",
14 withDefaults: "readonly"
15 }
16 }
This configuration ensures that your code adheres to consistent coding standards and helps catch potential errors or stylistic issues.
globals
section defines commonly used Vue.js 3 functions as read-only, preventing accidental redefinitions.Run the vuejs server:
npm run serve
Access the app through http://localhost:8080/
.
Home page
Blog List page:
Blog detail page:
The source code for this project can be found on Github. Have a look at the Vue app code here and the Strapi backend here.
You have successfully built a Vue.js application that integrates Strapi for content management and Paystack for handling payments. This project demonstrates several key concepts and technologies like Vue.js and Vue Router, content management with Strapi, payment integration with Paystack, API integration, component-based architecture(allows for reusable and maintainable code), error handling and loading states and rich text handling.
You can expand the project in several ways like implementing user authentication, adding more sophisticated content filtering and search functionality, and improving the UI/UX with a more polished design.
A software developer and ML engineer helping businesses automate tasks and keep deployment costs low.