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
neondb
and theproduction
branch.
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_at
andupdated_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. -
jsonwebtoken
andexpress-jwt
: For JWT authentication. -
@auth0/auth0-spa-js
: For Auth0 integration. -
nodemon
: Automatically restarts the server during development.
-
Configure Environment Variables:
Create a
.env
file 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.js
file:
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.js
to 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
.env
in 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
, andAudience
from 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.env
files in both backend and frontend projects.
Step 2: Secure Admin Endpoints
-
Install Auth0 Middleware:
Ensure
express-jwt
andjwks-rsa
are installed:
npm install jwks-rsa
-
Create Middleware:
Create
middleware/auth.js
in 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.js
to 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
admin
role.
-
Frontend Testing:
- Run the frontend:
npm start
. - Visit
http://localhost:3000/blogs
to see the blogs page. - Visit
http://localhost:3000/admin
to 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_URL
to 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!