ABC3
Hanzla Baig

Hanzla Baig @hanzla-baig

About: In my projects, I aim to push the boundaries of conventional web design, incorporating advanced animations, responsive layouts, and dynmic features that captivate and retain users. So i like this

Location:
Chichawatni, District Sahiwal
Joined:
Aug 18, 2024

ABC3

Publish Date: May 18
0 0

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

  1. 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
    
  1. 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.
Enter fullscreen mode Exit fullscreen mode
  • 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.
  1. 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.

  1. 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}`));
    
  2. Environment Variables

    • Create server/.env:

    DATABASE_URL=your_neon_database_connection_string
    JWT_SECRET=your_jwt_secret
    PORT=5000

  • Replace your_neon_database_connection_string with the Neon connection string and your_jwt_secret with a secure random string.
  1. 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.
  1. Run the Backend

    • Add a start script to server/package.json:
     "scripts": {
       "start": "node index.js",
       "dev": "nodemon index.js"
     }
    
  • 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.

  1. 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>
       );
     }
    
  2. 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>
       );
     }
    
  3. 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>
       );
     }
    
  4. 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>
       );
     }
    
  5. 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>
       );
     }
    

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;
  }
Enter fullscreen mode Exit fullscreen mode

Step 5: Testing

  1. Run the Backend:
   cd server
   npm run dev
Enter fullscreen mode Exit fullscreen mode
  1. Run the Frontend:
   cd client
   npm run dev
Enter fullscreen mode Exit fullscreen mode
  1. 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.

Step 6: Deployment

  1. 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.
  2. 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).
  3. 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!

Comments 0 total

    Add comment