Creating a post publishing system for your website https://aquascript.xyz with a blogs page and a secure admin panel using Neon Database's free trial is an exciting project. This guide will walk you through the entire process in a detailed, step-by-step manner, covering the architecture, technologies, database setup, backend and frontend development, security measures, and deployment. The goal is to create a robust system where posts are displayed on a public blogs page, and only authorized admins can access a secret admin panel to create, edit, publish, or delete posts.
Table of Contents
- Overview of the System
 - Technologies and Tools
 - Setting Up Neon Database
 - Backend Development (Node.js, Express, PostgreSQL)
 - Frontend Development (React, Tailwind CSS)
 - Implementing Authentication and Security
 - Creating the Admin Panel
 - Building the Blogs Page
 - Testing the System
 - Deployment
 - Additional Considerations and Best Practices
 - Artifacts (Code Samples)
 
1. Overview of the System
The post publishing system will consist of two main components:
- Public Blogs Page: A page on https://aquascript.xyz/blogs that displays all published blog posts. Visitors can view posts without authentication.
 - 
Secret Admin Panel: A secure dashboard accessible only to authenticated admins at a hidden URL (e.g., https://aquascript.xyz/admin). Admins can:
- Create new posts.
 - Edit existing posts.
 - Publish or unpublish posts.
 - Delete posts.
 
 
The system will use:
- Neon Database (serverless PostgreSQL) to store posts and admin credentials.
 - Node.js and Express for the backend API.
 - React with Tailwind CSS for the frontend.
 - Auth0 for secure admin authentication.
 - Vercel for deployment.
 
The architecture will follow a client-server model:
- The frontend (React) communicates with the backend (Express) via RESTful API endpoints.
 - The backend interacts with Neon Database to perform CRUD (Create, Read, Update, Delete) operations.
 - Authentication ensures only admins access the admin panel.
 
2. Technologies and Tools
Here’s a breakdown of the tools and technologies we’ll use:
- Neon Database: A serverless PostgreSQL database with a free tier, ideal for storing posts and user data. It offers features like autoscaling and branching.
 - Node.js and Express: For building a RESTful API to handle post and user management.
 - React: For creating a dynamic and responsive frontend for both the blogs page and admin panel.
 - Tailwind CSS: For styling the frontend with a utility-first approach.
 - Auth0: For secure authentication to restrict admin panel access.
 - Vercel: For hosting the frontend and backend.
 - PostgreSQL Client (pg): To connect the backend to Neon Database.
 - Postman: For testing API endpoints.
 - Git and GitHub: For version control.
 
Prerequisites
- Basic knowledge of JavaScript, Node.js, React, and SQL.
 - A Neon account (sign up at https://neon.tech).
 - An Auth0 account (sign up at https://auth0.com).
 - Node.js installed (v16 or higher).
 - A code editor (e.g., VS Code).
 - A GitHub account.
 
3. Setting Up Neon Database
Neon Database provides a free-tier serverless PostgreSQL database, perfect for this project. Let’s set it up.
Step 1: Create a Neon Project
- Sign Up: Go to https://neon.tech and sign up using your email, GitHub, or Google account.
 - 
Create a Project:
- In the Neon Console, click “Create Project.”
 - Enter a project name (e.g., 
aquascript-blog). - Choose PostgreSQL version 16 (default).
 - Select a region close to your users (e.g., US East).
 - Click “Create Project.”
 
 - 
Get Connection String:
- After creating the project, Neon will display a connection string like:
 
postgresql://username:password@ep-project-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require 
- Copy this string and save it securely. It’s used to connect to the database.
 
Step 2: Create Database Schema
We need two tables:
- 
posts: To store blog posts. - 
admins: To store admin credentials (though Auth0 will handle authentication, we’ll store admin roles). 
- 
Access Neon SQL Editor:
- In the Neon Console, navigate to the “SQL Editor” tab.
 - Select the default database 
neondband theproductionbranch. 
 Create Tables:
Run the following SQL commands in the SQL Editor to create the tables:
   -- Create posts table
   CREATE TABLE posts (
       id SERIAL PRIMARY KEY,
       title VARCHAR(255) NOT NULL,
       content TEXT NOT NULL,
       slug VARCHAR(255) UNIQUE NOT NULL,
       is_published BOOLEAN DEFAULT FALSE,
       created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
       updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
   );
   -- Create admins table
   CREATE TABLE admins (
       id SERIAL PRIMARY KEY,
       auth0_id VARCHAR(255) UNIQUE NOT NULL,
       email VARCHAR(255) NOT NULL,
       role VARCHAR(50) DEFAULT 'admin',
       created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
   );
   -- Grant privileges to public schema
   GRANT CREATE ON SCHEMA public TO PUBLIC;
- 
posts table:
- 
id: Unique identifier for each post. - 
title: Post title. - 
content: Post body. - 
slug: URL-friendly string for post URLs (e.g.,my-first-post). - 
is_published: Boolean to control visibility on the blogs page. - 
created_atandupdated_at: Timestamps for tracking creation and updates. 
 - 
 - 
admins table:
- 
auth0_id: Unique identifier from Auth0 for each admin. - 
email: Admin’s email. - 
role: Role (e.g.,admin). - 
created_at: Timestamp for account creation. 
 - 
 
- Insert Sample Data (Optional): To test the database, insert a sample post:
 
   INSERT INTO posts (title, content, slug, is_published)
   VALUES (
       'Welcome to AquaScript',
       'This is the first blog post on AquaScript.xyz!',
       'welcome-to-aquascript',
       TRUE
   );
- 
Verify Setup:
Run 
SELECT * FROM posts;in the SQL Editor to ensure the table and data are created correctly. 
4. Backend Development (Node.js, Express, PostgreSQL)
The backend will be a Node.js application using Express to create a RESTful API. It will handle CRUD operations for posts and admin authentication.
Step 1: Set Up the Backend Project
- Create a Project Directory:
 
   mkdir aquascript-blog-backend
   cd aquascript-blog-backend
   npm init -y
- Install Dependencies: Install the required packages:
 
   npm install express pg cors dotenv jsonwebtoken express-jwt @auth0/auth0-spa-js
   npm install --save-dev nodemon
- 
express: Web framework. - 
pg: PostgreSQL client for Node.js. - 
cors: Enables cross-origin requests. - 
dotenv: Loads environment variables. - 
jsonwebtokenandexpress-jwt: For JWT authentication. - 
@auth0/auth0-spa-js: For Auth0 integration. - 
nodemon: Automatically restarts the server during development. 
- 
Configure Environment Variables:
Create a 
.envfile in the root directory: 
   DATABASE_URL=postgresql://username:password@ep-project-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require
   PORT=5000
   AUTH0_DOMAIN=your-auth0-domain.auth0.com
   AUTH0_AUDIENCE=your-auth0-api-identifier
   AUTH0_CLIENT_ID=your-auth0-client-id
Replace placeholders with your Neon connection string and Auth0 credentials (obtained later).
- 
Set Up Express Server:
Create 
index.js: 
   const express = require('express');
   const cors = require('cors');
   const { Pool } = require('pg');
   require('dotenv').config();
   const app = express();
   const port = process.env.PORT || 5000;
   // Middleware
   app.use(cors());
   app.use(express.json());
   // Database connection
   const pool = new Pool({
       connectionString: process.env.DATABASE_URL,
       ssl: { rejectUnauthorized: false }
   });
   // Test database connection
   pool.connect((err) => {
       if (err) {
           console.error('Database connection error:', err.stack);
       } else {
           console.log('Connected to Neon Database');
       }
   });
   // Basic route
   app.get('/', (req, res) => {
       res.json({ message: 'AquaScript Blog API' });
   });
   // Start server
   app.listen(port, () => {
       console.log(`Server running on port ${port}`);
   });
- 
Update 
package.json: Add a start script: 
   "scripts": {
       "start": "node index.js",
       "dev": "nodemon index.js"
   }
- Run the Server:
 
   npm run dev
Visit http://localhost:5000 to see the API response.
Step 2: Create API Endpoints
We’ll create endpoints for posts and admin management.
- 
Posts Endpoints:
Create a 
routes/posts.jsfile: 
   const express = require('express');
   const router = express.Router();
   const { Pool } = require('pg');
   require('dotenv').config();
   const pool = new Pool({
       connectionString: process.env.DATABASE_URL,
       ssl: { rejectUnauthorized: false }
   });
   // Get all published posts (public)
   router.get('/', async (req, res) => {
       try {
           const result = await pool.query('SELECT * FROM posts WHERE is_published = TRUE ORDER BY created_at DESC');
           res.json(result.rows);
       } catch (err) {
           console.error(err.stack);
           res.status(500).json({ error: 'Server error' });
       }
   });
   // Get single post by slug (public)
   router.get('/:slug', async (req, res) => {
       const { slug } = req.params;
       try {
           const result = await pool.query('SELECT * FROM posts WHERE slug = $1 AND is_published = TRUE', [slug]);
           if (result.rows.length === 0) {
               return res.status(404).json({ error: 'Post not found' });
           }
           res.json(result.rows[0]);
       } catch (err) {
           console.error(err.stack);
           res.status(500).json({ error: 'Server error' });
       }
   });
   // Create a post (admin only)
   router.post('/', async (req, res) => {
       const { title, content, slug, is_published } = req.body;
       try {
           const result = await pool.query(
               'INSERT INTO posts (title, content, slug, is_published) VALUES ($1, $2, $3, $4) RETURNING *',
               [title, content, slug, is_published]
           );
           res.status(201).json(result.rows[0]);
       } catch (err) {
           console.error(err.stack);
           res.status(500).json({ error: 'Server error' });
       }
   });
   // Update a post (admin only)
   router.put('/:id', async (req, res) => {
       const { id } = req.params;
       const { title, content, slug, is_published } = req.body;
       try {
           const result = await pool.query(
               'UPDATE posts SET title = $1, content = $2, slug = $3, is_published = $4, updated_at = CURRENT_TIMESTAMP WHERE id = $5 RETURNING *',
               [title, content, slug, is_published, id]
           );
           if (result.rows.length === 0) {
               return res.status(404).json({ error: 'Post not found' });
           }
           res.json(result.rows[0]);
       } catch (err) {
           console.error(err.stack);
           res.status(500).json({ error: 'Server error' });
       }
   });
   // Delete a post (admin only)
   router.delete('/:id', async (req, res) => {
       const { id } = req.params;
       try {
           const result = await pool.query('DELETE FROM posts WHERE id = $1 RETURNING *', [id]);
           if (result.rows.length === 0) {
               return res.status(404).json({ error: 'Post not found' });
           }
           res.json({ message: 'Post deleted' });
       } catch (err) {
           console.error(err.stack);
           res.status(500).json({ error: 'Server error' });
       }
   });
   module.exports = router;
- 
Integrate Routes:
Update 
index.jsto include the posts routes: 
   const postsRouter = require('./routes/posts');
   app.use('/api/posts', postsRouter);
- 
Test Endpoints:
Use Postman to test:
- 
GET http://localhost:5000/api/posts: Retrieve all published posts. - 
GET http://localhost:5000/api/posts/welcome-to-aquascript: Retrieve a single post. - 
POST http://localhost:5000/api/posts: Create a post (requires admin authentication, implemented later). 
 - 
 
5. Frontend Development (React, Tailwind CSS)
The frontend will be a React application with two main sections: the blogs page and the admin panel.
Step 1: Set Up the React Project
- Create a React App:
 
   npx create-react-app aquascript-blog-frontend
   cd aquascript-blog-frontend
- Install Dependencies: Install Tailwind CSS, React Router, and Axios:
 
   npm install tailwindcss postcss autoprefixer react-router-dom axios @auth0/auth0-react
   npm install --save-dev @tailwindcss/typography
- Initialize Tailwind CSS:
 
   npx tailwindcss init -p
Update tailwind.config.js:
   module.exports = {
       content: [
           "./src/**/*.{js,jsx,ts,tsx}",
       ],
       theme: {
           extend: {},
       },
       plugins: [
           require('@tailwindcss/typography'),
       ],
   }
Create src/index.css:
   @tailwind base;
   @tailwind components;
   @tailwind utilities;
- 
Update 
src/index.js: Wrap the app with Auth0 provider: 
   import React from 'react';
   import ReactDOM from 'react-dom';
   import './index.css';
   import App from './App';
   import { BrowserRouter } from 'react-router-dom';
   import { Auth0Provider } from '@auth0/auth0-react';
   ReactDOM.render(
       <Auth0Provider
           domain={process.env.REACT_APP_AUTH0_DOMAIN}
           clientId={process.env.REACT_APP_AUTH0_CLIENT_ID}
           redirectUri={window.location.origin}
           audience={process.env.REACT_APP_AUTH0_AUDIENCE}
       >
           <BrowserRouter>
               <App />
           </BrowserRouter>
       </Auth0Provider>,
       document.getElementById('root')
   );
- 
Configure Environment Variables:
Create 
.envin the frontend root: 
   REACT_APP_AUTH0_DOMAIN=your-auth0-domain.auth0.com
   REACT_APP_AUTH0_CLIENT_ID=your-auth0-client-id
   REACT_APP_AUTH0_AUDIENCE=your-auth0-api-identifier
   REACT_APP_API_URL=http://localhost:5000
Step 2: Create the Blogs Page
- 
Create Blogs Component:
Create 
src/components/Blogs.js: 
   import React, { useState, useEffect } from 'react';
   import axios from 'axios';
   import { Link } from 'react-router-dom';
   const Blogs = () => {
       const [posts, setPosts] = useState([]);
       useEffect(() => {
           axios.get(`${process.env.REACT_APP_API_URL}/api/posts`)
               .then(response => setPosts(response.data))
               .catch(error => console.error('Error fetching posts:', error));
       }, []);
       return (
           <div className="container mx-auto p-4">
               <h1 className="text-3xl font-bold mb-6">AquaScript Blogs</h1>
               <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
                   {posts.map(post => (
                       <div key={post.id} className="border rounded-lg p-4 shadow-md">
                           <h2 className="text-xl font-semibold">{post.title}</h2>
                           <p className="text-gray-600 mt-2">{post.content.substring(0, 100)}...</p>
                           <Link to={`/blogs/${post.slug}`} className="text-blue-500 hover:underline">
                               Read More
                           </Link>
                       </div>
                   ))}
               </div>
           </div>
       );
   };
   export default Blogs;
- 
Create Single Post Component:
Create 
src/components/Post.js: 
   import React, { useState, useEffect } from 'react';
   import axios from 'axios';
   import { useParams } from 'react-router-dom';
   const Post = () => {
       const { slug } = useParams();
       const [post, setPost] = useState(null);
       const [loading, setLoading] = useState(true);
       useEffect(() => {
           axios.get(`${process.env.REACT_APP_API_URL}/api/posts/${slug}`)
               .then(response => {
                   setPost(response.data);
                   setLoading(false);
               })
               .catch(error => {
                   console.error('Error fetching post:', error);
                   setLoading(false);
               });
       }, [slug]);
       if (loading) return <div>Loading...</div>;
       if (!post) return <div>Post not found</div>;
       return (
           <div className="container mx-auto p-4">
               <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
               <div className="prose" dangerouslySetInnerHTML={{ __html: post.content }} />
           </div>
       );
   };
   export default Post;
6. Implementing Authentication and Security
To secure the admin panel, we’ll use Auth0 for authentication and role-based access control.
Step 1: Set Up Auth0
- 
Create an Auth0 Application:
- Sign up at https://auth0.com.
 - Create a new application (Single Page Application for the frontend, Regular Web Application for the backend).
 - Note the 
Domain,Client ID, andAudiencefrom the application settings. 
 - 
Create an API:
- In Auth0, go to “APIs” and create a new API.
 - Set the identifier (e.g., 
https://aquascript.xyz/api). - Note the audience.
 
 - 
Configure Rules:
- Create a rule to add admin roles to the JWT token:
 
function (user, context, callback) { const namespace = 'https://aquascript.xyz'; context.accessToken[namespace + '/roles'] = user.roles || ['admin']; callback(null, user, context); } Update Environment Variables:
Add Auth0 credentials to.envfiles in both backend and frontend projects.
Step 2: Secure Admin Endpoints
- 
Install Auth0 Middleware:
Ensure 
express-jwtandjwks-rsaare installed: 
   npm install jwks-rsa
- 
Create Middleware:
Create 
middleware/auth.jsin the backend: 
   const jwt = require('express-jwt');
   const jwksRsa = require('jwks-rsa');
   const checkJwt = jwt({
       secret: jwksRsa.expressJwtSecret({
           cache: true,
           rateLimit: true,
           jwksRequestsPerMinute: 5,
           jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`
       }),
       audience: process.env.AUTH0_AUDIENCE,
       issuer: `https://${process.env.AUTH0_DOMAIN}/`,
       algorithms: ['RS256']
   });
   const checkAdmin = (req, res, next) => {
       const roles = req.user['https://aquascript.xyz/roles'] || [];
       if (!roles.includes('admin')) {
           return res.status(403).json({ error: 'Access denied' });
       }
       next();
   };
   module.exports = { checkJwt, checkAdmin };
- 
Protect Admin Routes:
Update 
routes/posts.jsto protect create, update, and delete endpoints: 
   const { checkJwt, checkAdmin } = require('../middleware/auth');
   router.post('/', checkJwt, checkAdmin, async (req, res) => { /* ... */ });
   router.put('/:id', checkJwt, checkAdmin, async (req, res) => { /* ... */ });
   router.delete('/:id', checkJwt, checkAdmin, async (req, res) => { /* ... */ });
7. Creating the Admin Panel
The admin panel will be a React component accessible only to authenticated admins.
Step 1: Create Admin Component
Create src/components/Admin.js:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useAuth0 } from '@auth0/auth0-react';
import { Link } from 'react-router-dom';
const Admin = () => {
    const { user, isAuthenticated, loginWithRedirect, getAccessTokenSilently } = useAuth0();
    const [posts, setPosts] = useState([]);
    const [form, setForm] = useState({ title: '', content: '', slug: '', is_published: false });
    useEffect(() => {
        if (isAuthenticated) {
            fetchPosts();
        }
    }, [isAuthenticated]);
    const fetchPosts = async () => {
        try {
            const token = await getAccessTokenSilently();
            const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/posts`, {
                headers: { Authorization: `Bearer ${token}` }
            });
            setPosts(response.data);
        } catch (error) {
            console.error('Error fetching posts:', error);
        }
    };
    const handleSubmit = async (e) => {
        e.preventDefault();
        try {
            const token = await getAccessTokenSilently();
            await axios.post(`${process.env.REACT_APP_API_URL}/api/posts`, form, {
                headers: { Authorization: `Bearer ${token}` }
            });
            fetchPosts();
            setForm({ title: '', content: '', slug: '', is_published: false });
        } catch (error) {
            console.error('Error creating post:', error);
        }
    };
    const handleDelete = async (id) => {
        try {
            const token = await getAccessTokenSilently();
            await axios.delete(`${process.env.REACT_APP_API_URL}/api/posts/${id}`, {
                headers: { Authorization: `Bearer ${token}` }
            });
            fetchPosts();
        } catch (error) {
            console.error('Error deleting post:', error);
        }
    };
    if (!isAuthenticated) {
        loginWithRedirect();
        return null;
    }
    return (
        <div className="container mx-auto p-4">
            <h1 className="text-3xl font-bold mb-6">Admin Panel</h1>
            <form onSubmit={handleSubmit} className="mb-8">
                <div className="mb-4">
                    <label className="block text-sm font-medium">Title</label>
                    <input
                        type="text"
                        value={form.title}
                        onChange={(e) => setForm({ ...form, title: e.target.value })}
                        className="w-full border rounded p-2"
                    />
                </div>
                <div className="mb-4">
                    <label className="block text-sm font-medium">Content</label>
                    <textarea
                        value={form.content}
                        onChange={(e) => setForm({ ...form, content: e.target.value })}
                        className="w-full border rounded p-2"
                    />
                </div>
                <div className="mb-4">
                    <label className="block text-sm font-medium">Slug</label>
                    <input
                        type="text"
                        value={form.slug}
                        onChange={(e) => setForm({ ...form, slug: e.target.value })}
                        className="w-full border rounded p-2"
                    />
                </div>
                <div className="mb-4">
                    <label className="inline-flex items-center">
                        <input
                            type="checkbox"
                            checked={form.is_published}
                            onChange={(e) => setForm({ ...form, is_published: e.target.checked })}
                        />
                        <span className="ml-2">Published</span>
                    </label>
                </div>
                <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
                    Create Post
                </button>
            </form>
            <h2 className="text-2xl font-semibold mb-4">Existing Posts</h2>
            <div className="grid grid-cols-1 gap-4">
                {posts.map(post => (
                    <div key={post.id} className="border rounded-lg p-4 shadow-md">
                        <h3 className="text-lg font-semibold">{post.title}</h3>
                        <p>{post.is_published ? 'Published' : 'Draft'}</p>
                        <div className="mt-2">
                            <Link to={`/admin/edit/${post.id}`} className="text-blue-500 hover:underline mr-4">
                                Edit
                            </Link>
                            <button
                                onClick={() => handleDelete(post.id)}
                                className="text-red-500 hover:underline"
                            >
                                Delete
                            </button>
                        </div>
                    </div>
                ))}
            </div>
        </div>
    );
};
export default Admin;
Step 2: Create Edit Post Component
Create src/components/EditPost.js:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useParams, useHistory } from 'react-router-dom';
import { useAuth0 } from '@auth0/auth0-react';
const EditPost = () => {
    const { id } = useParams();
    const history = useHistory();
    const { getAccessTokenSilently } = useAuth0();
    const [form, setForm] = useState({ title: '', content: '', slug: '', is_published: false });
    useEffect(() => {
        const fetchPost = async () => {
            try {
                const token = await getAccessTokenSilently();
                const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/posts/${id}`, {
                    headers: { Authorization: `Bearer ${token}` }
                });
                setForm(response.data);
            } catch (error) {
                console.error('Error fetching post:', error);
            }
        };
        fetchPost();
    }, [id, getAccessTokenSilently]);
    const handleSubmit = async (e) => {
        e.preventDefault();
        try {
            const token = await getAccessTokenSilently();
            await axios.put(`${process.env.REACT_APP_API_URL}/api/posts/${id}`, form, {
                headers: { Authorization: `Bearer ${token}` }
            });
            history.push('/admin');
        } catch (error) {
            console.error('Error updating post:', error);
        }
    };
    return (
        <div className="container mx-auto p-4">
            <h1 className="text-3xl font-bold mb-6">Edit Post</h1>
            <form onSubmit={handleSubmit}>
                <div className="mb-4">
                    <label className="block text-sm font-medium">Title</label>
                    <input
                        type="text"
                        value={form.title}
                        onChange={(e) => setForm({ ...form, title: e.target.value })}
                        className="w-full border rounded p-2"
                    />
                </div>
                <div className="mb-4">
                    <label className="block text-sm font-medium">Content</label>
                    <textarea
                        value={form.content}
                        onChange={(e) => setForm({ ...form, content: e.target.value })}
                        className="w-full border rounded p-2"
                    />
                </div>
                <div className="mb-4">
                    <label className="block text-sm font-medium">Slug</label>
                    <input
                        type="text"
                        value={form.slug}
                        onChange={(e) => setForm({ ...form, slug: e.target.value })}
                        className="w-full border rounded p-2"
                    />
                </div>
                <div className="mb-4">
                    <label className="inline-flex items-center">
                        <input
                            type="checkbox"
                            checked={form.is_published}
                            onChange={(e) => setForm({ ...form, is_published: e.target.checked })}
                        />
                        <span className="ml-2">Published</span>
                    </label>
                </div>
                <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
                    Update Post
                </button>
            </form>
        </div>
    );
};
export default EditPost;
Step 3: Set Up Routing
Update src/App.js:
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import Blogs from './components/Blogs';
import Post from './components/Post';
import Admin from './components/Admin';
import EditPost from './components/EditPost';
const App = () => {
    return (
        <Switch>
            <Route exact path="/blogs" component={Blogs} />
            <Route path="/blogs/:slug" component={Post} />
            <Route exact path="/admin" component={Admin} />
            <Route path="/admin/edit/:id" component={EditPost} />
        </Switch>
    );
};
export default App;
8. Building the Blogs Page
The blogs page is already implemented in Blogs.js and Post.js. It fetches published posts and displays them in a grid. Each post links to a detailed view using the slug.
9. Testing the System
- 
Backend Testing:
- Use Postman to test all API endpoints.
 - Verify that admin-only endpoints require a valid JWT token with the 
adminrole. 
 - 
Frontend Testing:
- Run the frontend: 
npm start. - Visit 
http://localhost:3000/blogsto see the blogs page. - Visit 
http://localhost:3000/adminto test the admin panel (requires login). - Test creating, editing, and deleting posts.
 
 - Run the frontend: 
 - 
Database Testing:
- Use Neon’s SQL Editor to verify that posts are created, updated, and deleted correctly.
 
 
10. Deployment
Deploy the application to Vercel for easy hosting.
Step 1: Deploy Backend
- 
Push to GitHub:
- Create a GitHub repository for the backend.
 - Push the code:
 
git init git add . git commit -m "Initial commit" git remote add origin <repository-url> git push origin main - 
Deploy to Vercel:
- Sign up at https://vercel.com.
 - Import the backend repository.
 - Add environment variables (
DATABASE_URL,AUTH0_*) in Vercel’s dashboard. - Deploy the project. Note the URL (e.g., 
https://aquascript-blog-backend.vercel.app). 
 
Step 2: Deploy Frontend
- 
Push to GitHub:
- Create a separate GitHub repository for the frontend.
 - Push the code.
 
 - 
Deploy to Vercel:
- Import the frontend repository.
 - Add environment variables (
REACT_APP_*). - Deploy the project. Update the 
REACT_APP_API_URLto the backend Vercel URL. 
 - 
Update Auth0:
- Add the Vercel frontend URL to Auth0’s “Allowed Callback URLs” and “Allowed Logout URLs”.
 
 - 
Test Deployment:
- Visit the deployed blogs page (e.g., 
https://aquascript-blog-frontend.vercel.app/blogs). - Test the admin panel and ensure authentication works.
 
 - Visit the deployed blogs page (e.g., 
 
11. Additional Considerations and Best Practices
- 
Security:
- Use HTTPS for all API calls.
 - Sanitize user inputs to prevent SQL injection and XSS attacks.
 - Regularly rotate Auth0 credentials and database passwords.
 
 - 
Performance:
- Use Neon’s autoscaling to handle traffic spikes.
 - Implement caching for the blogs page using a CDN or server-side caching.
 
 - 
SEO:
- Add meta tags to blog posts for better search engine visibility.
 - Generate sitemaps for the blogs page.
 
 - Scalability:
 - 
Backup:
- Regularly back up the Neon database using the Neon Console or automated scripts.
 
 
12. Artifacts (Code Samples)
const express = require('express');
const cors = require('cors');
const { Pool } = require('pg');
require('dotenv').config();
const postsRouter = require('./routes/posts');
const app = express();
const port = process.env.PORT || 5000;
// Middleware
app.use(cors());
app.use(express.json());
// Database connection
const pool = new Pool({
    connectionString: process.env.DATABASE_URL,
    ssl: { rejectUnauthorized: false }
});
// Test database connection
pool.connect((err) => {
    if (err) {
        console.error('Database connection error:', err.stack);
    } else {
        console.log('Connected to Neon Database');
    }
});
// Routes
app.get('/', (req, res) => {
    res.json({ message: 'AquaScript Blog API' });
});
app.use('/api/posts', postsRouter);
// Start server
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});
const express = require('express');
const router = express.Router();
const { Pool } = require('pg');
require('dotenv').config();
const { checkJwt, checkAdmin } = require('../middleware/auth');
const pool = new Pool({
    connectionString: process.env.DATABASE_URL,
    ssl: { rejectUnauthorized: false }
});
// Get all published posts (public)
router.get('/', async (req, res) => {
    try {
        const result = await pool.query('SELECT * FROM posts WHERE is_published = TRUE ORDER BY created_at DESC');
        res.json(result.rows);
    } catch (err) {
        console.error(err.stack);
        res.status(500).json({ error: 'Server error' });
    }
});
// Get single post by slug (public)
router.get('/:slug', async (req, res) => {
    const { slug } = req.params;
    try {
        const result = await pool.query('SELECT * FROM posts WHERE slug = $1 AND is_published = TRUE', [slug]);
        if (result.rows.length === 0) {
            return res.status(404).json({ error: 'Post not found' });
        }
        res.json(result.rows[0]);
    } catch (err) {
        console.error(err.stack);
        res.status(500).json({ error: 'Server error' });
    }
});
// Create a post (admin only)
router.post('/', checkJwt, checkAdmin, async (req, res) => {
    const { title, content, slug, is_published } = req.body;
    try {
        const result = await pool.query(
            'INSERT INTO posts (title, content, slug, is_published) VALUES ($1, $2, $3, $4) RETURNING *',
            [title, content, slug, is_published]
        );
        res.status(201).json(result.rows[0]);
    } catch (err) {
        console.error(err.stack);
        res.status(500).json({ error: 'Server error' });
    }
});
// Update a post (admin only)
router.put('/:id', checkJwt, checkAdmin, async (req, res) => {
    const { id } = req.params;
    const { title, content, slug, is_published } = req.body;
    try {
        const result = await pool.query(
            'UPDATE posts SET title = $1, content = $2, slug = $3, is_published = $4, updated_at = CURRENT_TIMESTAMP WHERE id = $5 RETURNING *',
            [title, content, slug, is_published, id]
        );
        if (result.rows.length === 0) {
            return res.status(404).json({ error: 'Post not found' });
        }
        res.json(result.rows[0]);
    } catch (err) {
        console.error(err.stack);
        res.status(500).json({ error: 'Server error' });
    }
});
// Delete a post (admin only)
router.delete('/:id', checkJwt, checkAdmin, async (req, res) => {
    const { id } = req.params;
    try {
        const result = await pool.query('DELETE FROM posts WHERE id = $1 RETURNING *', [id]);
        if (result.rows.length === 0) {
            return res.status(404).json({ error: 'Post not found' });
        }
        res.json({ message: 'Post deleted' });
    } catch (err) {
        console.error(err.stack);
        res.status(500).json({ error: 'Server error' });
    }
});
module.exports = router;
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useAuth0 } from '@auth0/auth0-react';
import { Link } from 'react-router-dom';
const Admin = () => {
    const { user, isAuthenticated, loginWithRedirect, getAccessTokenSilently } = useAuth0();
    const [posts, setPosts] = useState([]);
    const [form, setForm] = useState({ title: '', content: '', slug: '', is_published: false });
    useEffect(() => {
        if (isAuthenticated) {
            fetchPosts();
        }
    }, [isAuthenticated]);
    const fetchPosts = async () => {
        try {
            const token = await getAccessTokenSilently();
            const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/posts`, {
                headers: { Authorization: `Bearer ${token}` }
            });
            setPosts(response.data);
        } catch (error) {
            console.error('Error fetching posts:', error);
        }
    };
    const handleSubmit = async (e) => {
        e.preventDefault();
        try {
            const token = await getAccessTokenSilently();
            await axios.post(`${process.env.REACT_APP_API_URL}/api/posts`, form, {
                headers: { Authorization: `Bearer ${token}` }
            });
            fetchPosts();
            setForm({ title: '', content: '', slug: '', is_published: false });
        } catch (error) {
            console.error('Error creating post:', error);
        }
    };
    const handleDelete = async (id) => {
        try {
            const token = await getAccessTokenSilently();
            await axios.delete(`${process.env.REACT_APP_API_URL}/api/posts/${id}`, {
                headers: { Authorization: `Bearer ${token}` }
            });
            fetchPosts();
        } catch (error) {
            console.error('Error deleting post:', error);
        }
    };
    if (!isAuthenticated) {
        loginWithRedirect();
        return null;
    }
    return (
        <div className="container mx-auto p-4">
            <h1 className="text-3xl font-bold mb-6">Admin Panel</h1>
            <form onSubmit={handleSubmit} className="mb-8">
                <div className="mb-4">
                    <label className="block text-sm font-medium">Title</label>
                    <input
                        type="text"
                        value={form.title}
                        onChange={(e) => setForm({ ...form, title: e.target.value })}
                        className="w-full border rounded p-2"
                    />
                </div>
                <div className="mb-4">
                    <label className="block text-sm font-medium">Content</label>
                    <textarea
                        value={form.content}
                        onChange={(e) => setForm({ ...form, content: e.target.value })}
                        className="w-full border rounded p-2"
                    />
                </div>
                <div className="mb-4">
                    <label className="block text-sm font-medium">Slug</label>
                    <input
                        type="text"
                        value={form.slug}
                        onChange={(e) => setForm({ ...form, slug: e.target.value })}
                        className="w-full border rounded p-2"
                    />
                </div>
                <div className="mb-4">
                    <label className="inline-flex items-center">
                        <input
                            type="checkbox"
                            checked={form.is_published}
                            onChange={(e) => setForm({ ...form, is_published: e.target.checked })}
                        />
                        <span className="ml-2">Published</span>
                    </label>
                </div>
                <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
                    Create Post
                </button>
            </form>
            <h2 className="text-2xl font-semibold mb-4">Existing Posts</h2>
            <div className="grid grid-cols-1 gap-4">
                {posts.map(post => (
                    <div key={post.id} className="border rounded-lg p-4 shadow-md">
                        <h3 className="text-lg font-semibold">{post.title}</h3>
                        <p>{post.is_published ? 'Published' : 'Draft'}</p>
                        <div className="mt-2">
                            <Link to={`/admin/edit/${post.id}`} className="text-blue-500 hover:underline mr-4">
                                Edit
                            </Link>
                            <button
                                onClick={() => handleDelete(post.id)}
                                className="text-red-500 hover:underline"
                            >
                                Delete
                            </button>
                        </div>
                    </div>
                ))}
            </div>
        </div>
    );
};
export default Admin;
This guide provides a comprehensive roadmap to build your post publishing system. Follow the steps, use the provided code artifacts, and reach out if you encounter issues. Happy coding!
    
Top comments (0)