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!
Top comments (0)