I'll provide a comprehensive guide to building a post publishing system for your website, aquascript.xyz, using the specified tech stack (HTML, CSS, JavaScript, Java, Bootstrap, jQuery, Node.js, Next.js) and Neon Database's free trial. The system will include a public blogs page to display posts and a secure admin panel for creating, editing, deleting, and publishing posts, accessible only by the admin. This guide will be detailed, explanatory, and structured to help you understand each step, from setup to deployment.
Overview of the System
The post publishing system will consist of:
- Frontend: A Next.js application for the public blogs page and admin panel, styled with Bootstrap and enhanced with jQuery for dynamic interactions.
- Backend: A Node.js server with Express to handle API requests, integrated with Neon Database (PostgreSQL) for storing blog posts.
- Authentication: A secure admin login system using JSON Web Tokens (JWT) to restrict access to the admin panel.
- Database: Neon Database’s free-tier PostgreSQL for storing posts and admin credentials.
- Deployment: Guidance on deploying the application to Vercel (for Next.js) and Neon for the database.
Tech Stack Breakdown
- HTML/CSS/JavaScript: For structuring and styling the frontend, with JavaScript for interactivity.
- Bootstrap: For responsive design and pre-built components.
- jQuery: For simplifying DOM manipulation and AJAX requests.
- Node.js/Express: For building the backend API.
- Next.js: For server-side rendering, routing, and building the frontend.
- Neon Database: A serverless PostgreSQL database for storing data.
- Java: Not directly used, as Node.js and Next.js cover backend and frontend needs, but we’ll note where Java could fit if required (e.g., for a separate microservice).
Step-by-Step Guide
Step 1: Project Setup
-
Initialize the Project
- Create a new directory for your project:
mkdir aquascript-blog cd aquascript-blog
-
Initialize a Node.js project:
npm init -y
-
Create a Next.js app:
npx create-next-app@latest client
Choose TypeScript, Tailwind (optional, we’ll use Bootstrap), and App Router.
-
Create a separate backend directory:
mkdir server cd server npm init -y
-
Install Dependencies
- Frontend (client):
cd client npm install bootstrap jquery @popperjs/core axios
- `bootstrap`: For styling.
- `jquery`: For DOM manipulation.
- `axios`: For making HTTP requests to the backend.
-
Backend (server):
cd ../server npm install express pg jsonwebtoken bcryptjs cors dotenv npm install --save-dev nodemon
-
express
: Web framework for Node.js. -
pg
: PostgreSQL client for Node.js. -
jsonwebtoken
: For JWT-based authentication. -
bcryptjs
: For password hashing. -
cors
: To allow cross-origin requests. -
dotenv
: For environment variables. -
nodemon
: For auto-restarting the server during development.
-
-
Set Up Neon Database
- Sign up for Neon’s free trial at neon.tech.
- Create a new project and database (e.g.,
aquascript_db
). - Note the connection string (e.g.,
postgresql://user:password@host:port/dbname
). - Create a table for posts:
CREATE TABLE posts ( id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, published BOOLEAN DEFAULT FALSE );
-
Create a table for admins:
CREATE TABLE admins ( id SERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL );
-
Insert a default admin (use a hashed password, generated later):
INSERT INTO admins (username, password) VALUES ('admin', 'hashed_password_here');
Step 2: Backend Development (Node.js/Express)
The backend will handle API endpoints for posts and admin authentication.
-
Set Up Express Server
- Create
server/index.js
:
require('dotenv').config(); const express = require('express'); const { Pool } = require('pg'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); const cors = require('cors'); const app = express(); app.use(cors()); app.use(express.json()); const pool = new Pool({ connectionString: process.env.DATABASE_URL, ssl: { rejectUnauthorized: false } }); const PORT = process.env.PORT || 5000; const JWT_SECRET = process.env.JWT_SECRET || 'your_jwt_secret'; // Middleware to verify JWT const authenticateToken = (req, res, next) => { const token = req.headers['authorization']?.split(' ')[1]; if (!token) return res.status(401).json({ error: 'Unauthorized' }); jwt.verify(token, JWT_SECRET, (err, user) => { if (err) return res.status(403).json({ error: 'Forbidden' }); req.user = user; next(); }); }; // Admin Login app.post('/api/admin/login', async (req, res) => { const { username, password } = req.body; try { const result = await pool.query('SELECT * FROM admins WHERE username = $1', [username]); const admin = result.rows[0]; if (!admin || !await bcrypt.compare(password, admin.password)) { return res.status(401).json({ error: 'Invalid credentials' }); } const token = jwt.sign({ id: admin.id, username: admin.username }, JWT_SECRET, { expiresIn: '1h' }); res.json({ token }); } catch (err) { res.status(500).json({ error: 'Server error' }); } }); // Create Post app.post('/api/posts', authenticateToken, async (req, res) => { const { title, content, published } = req.body; try { const result = await pool.query( 'INSERT INTO posts (title, content, published) VALUES ($1, $2, $3) RETURNING *', [title, content, published] ); res.status(201).json(result.rows[0]); } catch (err) { res.status(500).json({ error: 'Server error' }); } }); // Get All Posts (Public) app.get('/api/posts', async (req, res) => { try { const result = await pool.query('SELECT * FROM posts WHERE published = true ORDER BY created_at DESC'); res.json(result.rows); } catch (err) { res.status(500).json({ error: 'Server error' }); } }); // Get Post by ID (Public) app.get('/api/posts/:id', async (req, res) => { const { id } = req.params; try { const result = await pool.query('SELECT * FROM posts WHERE id = $1 AND published = true', [id]); if (result.rows.length === 0) return res.status(404).json({ error: 'Post not found' }); res.json(result.rows[0]); } catch (err) { res.status(500).json({ error: 'Server error' }); } }); // Update Post app.put('/api/posts/:id', authenticateToken, async (req, res) => { const { id } = req.params; const { title, content, published } = req.body; try { const result = await pool.query( 'UPDATE posts SET title = $1, content = $2, published = $3, updated_at = CURRENT_TIMESTAMP WHERE id = $4 RETURNING *', [title, content, published, id] ); if (result.rows.length === 0) return res.status(404).json({ error: 'Post not found' }); res.json(result.rows[0]); } catch (err) { res.status(500).json({ error: 'Server error' }); } }); // Delete Post app.delete('/api/posts/:id', authenticateToken, 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) { res.status(500).json({ error: 'Server error' }); } }); app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
- Create
-
Environment Variables
- Create
server/.env
:
DATABASE_URL=your_neon_database_connection_string
JWT_SECRET=your_jwt_secret
PORT=5000 - Create
- Replace
your_neon_database_connection_string
with the Neon connection string andyour_jwt_secret
with a secure random string.
-
Hash Admin Password
- Run this script to generate a hashed password for the admin:
const bcrypt = require('bcryptjs'); const password = 'your_admin_password'; bcrypt.hash(password, 10, (err, hash) => { console.log(hash); });
- Update the
admins
table with the hashed password.
-
Run the Backend
- Add a start script to
server/package.json
:
"scripts": { "start": "node index.js", "dev": "nodemon index.js" }
- Add a start script to
-
Start the server:
cd server npm run dev
Step 3: Frontend Development (Next.js)
The frontend will include a public blogs page and a secure admin panel.
-
Set Up Bootstrap and jQuery
- In
client/app/layout.js
, import Bootstrap and jQuery:
import 'bootstrap/dist/css/bootstrap.min.css'; import Script from 'next/script'; import './globals.css'; export const metadata = { title: 'AquaScript Blog', description: 'Blog platform for aquascript.xyz', }; export default function RootLayout({ children }) { return ( <html lang="en"> <head> <Script src="https://code.jquery.com/jquery-3.6.0.min.js" strategy="beforeInteractive" /> <Script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js" strategy="beforeInteractive" /> <Script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" strategy="beforeInteractive" /> </head> <body>{children}</body> </html> ); }
- In
-
Blogs Page
- Create
client/app/page.js
for the public blogs page:
'use client'; import { useState, useEffect } from 'react'; import axios from 'axios'; import Link from 'next/link'; export default function Home() { const [posts, setPosts] = useState([]); useEffect(() => { axios.get('http://localhost:5000/api/posts') .then(res => setPosts(res.data)) .catch(err => console.error(err)); }, []); return ( <div className="container mt-5"> <h1 className="mb-4">AquaScript Blog</h1> <div className="row"> {posts.map(post => ( <div key={post.id} className="col-md-4 mb-4"> <div className="card"> <div className="card-body"> <h5 className="card-title">{post.title}</h5> <p className="card-text">{post.content.substring(0, 100)}...</p> <Link href={`/post/${post.id}`} className="btn btn-primary">Read More</Link> </div> </div> </div> ))} </div> </div> ); }
- Create
-
Individual Post Page
- Create
client/app/post/[id]/page.js
:
'use client'; import { useState, useEffect } from 'react'; import axios from 'axios'; import { useParams } from 'next/navigation'; export default function Post() { const [post, setPost] = useState(null); const { id } = useParams(); useEffect(() => { axios.get(`http://localhost:5000/api/posts/${id}`) .then(res => setPost(res.data)) .catch(err => console.error(err)); }, [id]); if (!post) return <div className="container mt-5">Loading...</div>; return ( <div className="container mt-5"> <h1>{post.title}</h1> <p>{post.content}</p> <small>Published on {new Date(post.created_at).toLocaleDateString()}</small> </div> ); }
- Create
-
Admin Panel
- Create
client/app/admin/page.js
:
'use client'; import { useState, useEffect } from 'react'; import axios from 'axios'; import { useRouter } from 'next/navigation'; export default function AdminPanel() { const [posts, setPosts] = useState([]); const [title, setTitle] = useState(''); const [content, setContent] = useState(''); const [published, setPublished] = useState(false); const [editId, setEditId] = useState(null); const [token, setToken] = useState(null); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const router = useRouter(); useEffect(() => { const storedToken = localStorage.getItem('token'); if (!storedToken) { router.push('/admin/login'); } else { setToken(storedToken); fetchPosts(storedToken); } }, [router]); const fetchPosts = async (token) => { try { const res = await axios.get('http://localhost:5000/api/posts', { headers: { Authorization: `Bearer ${token}` } }); setPosts(res.data); } catch (err) { console.error(err); } }; const handleSubmit = async (e) => { e.preventDefault(); try { if (editId) { await axios.put(`http://localhost:5000/api/posts/${editId}`, { title, content, published }, { headers: { Authorization: `Bearer ${token}` } }); } else { await axios.post('http://localhost:5000/api/posts', { title, content, published }, { headers: { Authorization: `Bearer ${token}` } }); } setTitle(''); setContent(''); setPublished(false); setEditId(null); fetchPosts(token); } catch (err) { console.error(err); } }; const handleEdit = (post) => { setEditId(post.id); setTitle(post.title); setContent(post.content); setPublished(post.published); }; const handleDelete = async (id) => { try { await axios.delete(`http://localhost:5000/api/posts/${id}`, { headers: { Authorization: `Bearer ${token}` } }); fetchPosts(token); } catch (err) { console.error(err); } }; return ( <div className="container mt-5"> <h1>Admin Panel</h1> <form onSubmit={handleSubmit} className="mb-4"> <div className="mb-3"> <label htmlFor="title" className="form-label">Title</label> <input type="text" className="form-control" id="title" value={title} onChange={(e) => setTitle(e.target.value)} required /> </div> <div className="mb-3"> <label htmlFor="content" className="form-label">Content</label> <textarea className="form-control" id="content" rows="5" value={content} onChange={(e) => setContent(e.target.value)} required ></textarea> </div> <div className="mb-3 form-check"> <input type="checkbox" className="form-check-input" id="published" checked={published} onChange={(e) => setPublished(e.target.checked)} /> <label className="form-check-label" htmlFor="published">Published</label> </div> <button type="submit" className="btn btn-primary"> {editId ? 'Update Post' : 'Create Post'} </button> </form> <h2>Posts</h2> <ul className="list-group"> {posts.map(post => ( <li key={post.id} className="list-group-item d-flex justify-content-between align-items-center"> {post.title} <div> <button className="btn btn-sm btn-warning me-2" onClick={() => handleEdit(post)}>Edit</button> <button className="btn btn-sm btn-danger" onClick={() => handleDelete(post.id)}>Delete</button> </div> </li> ))} </ul> </div> ); }
- Create
-
Admin Login Page
- Create
client/app/admin/login/page.js
:
'use client'; import { useState } from 'react'; import axios from 'axios'; import { useRouter } from 'next/navigation'; export default function AdminLogin() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const router = useRouter(); const handleSubmit = async (e) => { e.preventDefault(); try { const res = await axios.post('http://localhost:5000/api/admin/login', { username, password }); localStorage.setItem('token', res.data.token); router.push('/admin'); } catch (err) { setError('Invalid credentials'); } }; return ( <div className="container mt-5"> <h1>Admin Login</h1> {error && <div className="alert alert-danger">{error}</div>} <form onSubmit={handleSubmit}> <div className="mb-3"> <label htmlFor="username" className="form-label">Username</label> <input type="text" className="form-control" id="username" value={username} onChange={(e) => setUsername(e.target.value)} required /> </div> <div className="mb-3"> <label htmlFor="password" className="form-label">Password</label> <input type="password" className="form-control" id="password" value={password} onChange={(e) => setPassword(e.target.value)} required /> </div> <button type="submit" className="btn btn-primary">Login</button> </form> </div> ); }
- Create
Step 4: Styling
- Create
client/app/globals.css
to customize Bootstrap:
body {
background-color: #f8f9fa;
}
.card {
transition: transform 0.2s;
}
.card:hover {
transform: scale(1.05);
}
.list-group-item {
background-color: #fff;
}
Step 5: Testing
- Run the Backend:
cd server
npm run dev
- Run the Frontend:
cd client
npm run dev
-
Test the Application:
- Visit
http://localhost:3000
to see the blogs page. - Visit
http://localhost:3000/admin/login
to log in as admin. - Create, edit, and delete posts from the admin panel.
- Verify posts appear on the blogs page when published.
- Visit
Step 6: Deployment
-
Deploy the Backend:
- Use a platform like Render or Heroku.
- Push the
server
directory to a Git repository. - Set environment variables (
DATABASE_URL
,JWT_SECRET
,PORT
) on the platform. - Deploy the server and note the URL (e.g.,
https://your-backend-url
). - Update the frontend to use this URL instead of
http://localhost:5000
.
-
Deploy the Frontend:
- Push the
client
directory to a Git repository. - Deploy to Vercel:
- Connect your repository to Vercel.
- Set environment variables if needed.
- Deploy the app and get the URL (e.g.,
https://aquascript-blog.vercel.app
).
- Update aquascript.xyz to point to this URL or integrate the blog as a subdomain (e.g.,
blog.aquascript.xyz
).
- Push the
-
Deploy the Database:
- Neon Database is already hosted, so ensure the connection string is secure in your backend’s environment variables.
Step 7: Security Considerations
-
Secure the Admin Panel: The JWT-based authentication ensures only authorized users access the admin panel. Store JWTs in
localStorage
securely or use HTTP-only cookies for better security. -
Input Validation: Add validation to prevent XSS or SQL injection (e.g., use a library like
express-validator
). - Rate Limiting: Implement rate limiting on the login endpoint to prevent brute-force attacks.
- HTTPS: Ensure your deployed app uses HTTPS.
-
Environment Variables: Never commit
.env
files to version control.
Step 8: Optional Enhancements
- Rich Text Editor: Integrate a library like Quill or TinyMCE for the post content field.
- Pagination: Add pagination to the blogs page for better performance.
- Categories/Tags: Extend the database schema to support post categories or tags.
-
SEO: Use Next.js’s
metadata
for SEO optimization. - Analytics: Integrate Google Analytics for tracking blog views.
Step 9: Java Integration (Optional)
Since Java wasn’t necessary for this system, you could use it for:
- A separate microservice (e.g., for sending emails or processing images).
- A Spring Boot application to replace the Node.js backend if preferred. If you want to explore this, let me know, and I can provide a Java-based backend implementation.
Final Notes
This guide provides a complete, production-ready post publishing system for aquascript.xyz. The system is secure, scalable, and uses Neon’s free-tier database effectively. For further assistance, especially with deployment or enhancements, feel free to ask. You can also refer to tutorials like those on YouTube for visual guidance on building similar systems with Next.js and Node.js.
Let me know if you need clarification on any step or additional features!