Sapientis: Blogging Website
Name of student Karan Nigal
Roll Number S-39
PRN Number 23510039
Subject Web Development
Semester 4th
Year 2nd
Guide Dr. Pragati Mahale
Aim: The primary aim of the Sapientis Blogging Website is to develop a modern, user-
friendly, and scalable blogging platform that allows content creators to publish, manage, and
share their articles seamlessly.
Objectives:
The primary goal of the Sapientis Blogging Website is to provide a modern,
responsive, and feature-rich blogging platform that allows users to:
Create, edit, and delete blog posts efficiently.
Categorize content under different topics for better organization.
Upload and display images to enhance visual appeal.
Ensure seamless navigation with a responsive design that works across all devices.
Offer an intuitive admin dashboard for managing blog content effortlessly.
Additionally, the project was designed with scalability in mind, allowing for future
integrations such as:
User authentication (login/signup).
Commenting and social sharing features.
SEO optimization for better search engine visibility.
Analytics to track post performance and reader engagement.
By achieving these objectives, the Sapientis Blogging Website serves as both a
functional blogging platform and a learning tool for understanding full-stack
development best practices.
Introduction:
In today’s digital age, blogging remains one of the most influential platforms for sharing
knowledge, personal experiences, and professional insights. Recognizing the growing need
for a modern, user-friendly blogging platform, the Sapientis Blogging Website was
developed as a full-stack content management system (CMS). This project serves as a
dynamic platform where users can create, manage, and publish blog posts with ease while
enjoying a visually appealing and responsive design.
1
The name “Sapientis” (derived from the Latin word for "wisdom") reflects the platform’s
purpose—to serve as a hub for insightful and engaging content. The website supports rich
media integration, category-based organization, and an intuitive admin panel for seamless
content management. Built with scalability in mind, the platform lays the foundation for
future enhancements such as user authentication, SEO optimization, and social engagement
features.
This report outlines the project’s objectives, technical architecture, key features, design
considerations, and future development plans, demonstrating a robust understanding of full-
stack web development and user-centric design principles.
Technical stack and tools:
The Sapientis Blogging Website was built using a MERN-like stack, with the
following technologies:
Backend Development
Node.js – A JavaScript runtime for server-side logic.
Express.js – A lightweight framework for handling HTTP requests and routing.
MongoDB – A NoSQL database for storing blog posts, categories, and user data.
Mongoose – An ODM (Object Data Modeling) library for MongoDB, simplifying
data validation and query handling.
Multer – Middleware for handling file uploads (e.g., blog post images).
Frontend Development
EJS (Embedded JavaScript) – A templating engine for rendering dynamic content
on the server side.
HTML5 & CSS3 – Core technologies for structuring and styling the website.
JavaScript – Used for interactive elements, animations, and client-side functionality.
Responsive Design – Ensures compatibility with desktops, tablets, and mobile devices
using media queries.
Development & Deployment Tools
Git & GitHub – Version control and collaboration.
Visual Studio Code – Primary code editor.
Postman – API testing.
Render/Vercel/Netlify – Potential deployment platforms for future hosting.
Key features
The Sapientis Blogging Website includes several essential features to enhance user
experience and content management:
1. Blog Post Management
Create, Edit, and Delete Posts – Users can write new blog posts, modify existing
ones, or remove outdated content.
Featured Images – Each post supports an uploaded image for visual appeal.
2
Category Tagging – Posts are organized under different categories (e.g., Technology,
Lifestyle, Travel) with color-coded tags for easy identification.
2. Responsive & Interactive UI
Card-Based Layout – Blog posts are displayed in an aesthetically pleasing grid
format.
Hover Effects & Animations – Smooth transitions and scaling effects improve
engagement.
Read Time Estimation – Automatically calculates and displays the estimated reading
time for each post.
3. Admin Dashboard
Intuitive Interface – Easy-to-use controls for managing blog content.
Quick Actions – Buttons for editing, deleting, and previewing posts.
4. Responsive Design
Mobile-First Approach – Ensures optimal performance on smartphones, tablets, and
desktops.
Flexible Grid System – Adjusts layout based on screen size.
Design System & UI/UX Considerations
The Sapientis Blogging Website follows a minimalist and modern design philosophy, with
attention to:
Typography – Clear hierarchy with headings (1.75rem for featured posts, 1.5rem for
regular titles).
Color Scheme – Consistent use of color-coded categories (e.g., blue for Technology,
green for Lifestyle).
Spacing & Layout – Proper padding, margins, and grid alignment for readability.
Dark/Light Mode (Future Plan) – To reduce eye strain and improve accessibility.
Project Structure & File Organization:
The project is organized for scalability and maintainability:
sapientis-blog/
├── public/ # Static assets (CSS, JS, images)
├── views/ # EJS templates
│ ├── partials/ # Reusable components (header, footer)
│ └── pages/ # Main pages (home, post, admin)
├── routes/ # Express route handlers
3
├── models/ # MongoDB schemas
├── app.js # Main server configuration
└── package.json # Dependencies & scripts
Future Enhancements:
To further improve the platform, the following features can be added:
1. User Authentication – Allow multiple authors to contribute.
2. SEO Optimization – Meta tags, sitemaps, and schema markup.
3. Rich Text Editor – Enhanced formatting options (e.g., Markdown or WYSIWYG).
4. Commenting System – Reader engagement through discussions.
5. Analytics Dashboard – Track views, likes, and shares.
Program code:
Config/multer.js:
const multer = require('multer');
const path = require('path');
// Multer configuration
const storage = multer.diskStorage({
destination: './public/images/uploads/',
filename: function(req, file, cb) {
cb(null, 'blog-' + Date.now() + path.extname(file.originalname));
}
});
// Check file type
function checkFileType(file, cb) {
const filetypes = /jpeg|jpg|png|gif/;
const extname = filetypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = filetypes.test(file.mimetype);
if (mimetype && extname) {
return cb(null, true);
} else {
cb('Error: Images Only!');
}
}
const upload = multer({
storage: storage,
limits: { fileSize: 1000000 }, // 1MB limit
fileFilter: function(req, file, cb) {
checkFileType(file, cb);
4
}
}).single('image');
module.exports = upload;
Models/Blog.js:
const mongoose = require('mongoose');
const blogSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
content: {
type: String,
required: true
},
category: {
type: String,
enum: ['Philosophy', 'Social Issues', 'Personal Journey', 'Uncategorized'],
default: 'Uncategorized'
},
image: {
type: String,
default: '/images/default-blog.jpg'
},
featured: {
type: Boolean,
default: false
},
views: {
type: Number,
default: 0
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('Blog', blogSchema);
Routes/blog.js:
const express = require('express');
const router = express.Router();
const Blog = require('../models/Blog');
const upload = require('../config/multer');
// Helper function for category icons
function getCategoryIcon(category) {
switch(category) {
case 'Philosophy':
return 'fas fa-brain';
case 'Social Issues':
return 'fas fa-users';
case 'Personal Journey':
return 'fas fa-road';
5
default:
return 'fas fa-bookmark';
}
}
// CREATE - Show form to create new blog
router.get('/add', (req, res) => {
res.render('add');
});
// CREATE - Handle new blog submission
router.post('/add', upload, async (req, res) => {
try {
const { title, content, category } = req.body;
const image = req.file ? `/images/uploads/${req.file.filename}` : '/images/default-blog.jpg';
const newBlog = new Blog({
title,
content,
category,
image
});
await newBlog.save();
res.redirect('/');
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Error creating blog post' });
}
});
// READ - Get all blogs
router.get('/', async (req, res) => {
try {
const category = req.query.category;
let query = {};
if (category && category !== 'all') {
// Convert URL-friendly category back to proper format
const formattedCategory = category
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
query.category = formattedCategory;
}
const blogs = await Blog.find(query).sort({ createdAt: -1 });
const featuredBlogs = await Blog.find({ featured: true }).sort({ createdAt: -1 });
const categories = ['Philosophy', 'Social Issues', 'Personal Journey'];
if (req.xhr || req.headers.accept.indexOf('json') > -1) {
// If AJAX request, return JSON
return res.json({
blogs,
currentCategory: category || 'all'
});
}
6
// Otherwise render the full page
res.render('index', {
blogs,
featuredBlogs,
categories,
currentCategory: category || 'all',
getCategoryIcon
});
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Error fetching blogs' });
}
});
// READ - Get single blog
router.get('/blog/:id', async (req, res) => {
try {
const blog = await Blog.findById(req.params.id);
if (!blog) {
return res.status(404).json({ error: 'Blog not found' });
}
res.render('blog', { blog });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Error fetching blog' });
}
});
// UPDATE - Show edit form
router.get('/blog/:id/edit', async (req, res) => {
try {
const blog = await Blog.findById(req.params.id);
if (!blog) {
return res.status(404).json({ error: 'Blog not found' });
}
res.render('edit', { blog });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Error fetching blog for edit' });
}
});
// UPDATE - Handle blog update
router.put('/blog/:id', upload, async (req, res) => {
try {
const { title, content, category } = req.body;
const updateData = { title, content, category };
if (req.file) {
updateData.image = `/images/uploads/${req.file.filename}`;
}
const blog = await Blog.findByIdAndUpdate(
req.params.id,
updateData,
{ new: true }
);
7
if (!blog) {
return res.status(404).json({ error: 'Blog not found' });
}
res.json({ success: true, blog });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Error updating blog' });
}
});
// DELETE - Remove blog
router.delete('/blog/:id', async (req, res) => {
try {
const blog = await Blog.findByIdAndDelete(req.params.id);
if (!blog) {
return res.status(404).json({ error: 'Blog not found' });
}
res.json({ redirect: '/' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Error deleting blog' });
}
});
module.exports = router;
Footer.ejs:
</main>
<footer class="site-footer">
<div class="footer-content">
<div class="footer-newsletter">
<h3>
<i class="far fa-envelope" aria-hidden="true"></i>
<span>Subscribe to Our Newsletter</span>
</h3>
<form class="newsletter-form" id="newsletterForm" aria-label="Newsletter subscription form">
<input
type="email"
class="newsletter-input"
placeholder="Enter your email address"
required
aria-label="Email address"
>
<button type="submit" class="subscribe-btn">
<i class="fas fa-paper-plane" aria-hidden="true"></i>
<span>Subscribe</span>
</button>
</form>
</div>
<div class="footer-social">
<h3>
<i class="fas fa-users" aria-hidden="true"></i>
<span>Connect With Us</span>
</h3>
<div class="social-links">
8
<a href="#" class="social-btn twitter">
<i class="fab fa-twitter" aria-hidden="true"></i>
<span>Twitter</span>
</a>
<a href="#" class="social-btn youtube">
<i class="fab fa-youtube" aria-hidden="true"></i>
<span>YouTube</span>
</a>
<a href="#" class="social-btn instagram">
<i class="fab fa-instagram" aria-hidden="true"></i>
<span>Instagram</span>
</a>
<a href="#" class="social-btn linkedin">
<i class="fab fa-linkedin-in" aria-hidden="true"></i>
<span>LinkedIn</span>
</a>
</div>
</div>
<div class="footer-info">
<h3>About Sapientis Blog</h3>
<p>Exploring ideas, sharing knowledge, and fostering meaningful discussions.</p>
<p class="copyright">© <%= new Date().getFullYear() %> Sapientis Blog. All rights reserved.</p>
</div>
</div>
</footer>
<script src="/js/main.js"></script>
</body>
</html>
Header.ejs:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sapientis Blog</title>
<link rel="stylesheet" href="/css/style.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
</head>
<body>
<header>
<div class="header-container">
<h1><a href="/"><i class="fas fa-pen-fancy"></i> Sapientis Blog</a></h1>
<nav>
<a href="/"><i class="fas fa-home"></i> Home</a>
<a href="/add"><i class="fas fa-plus"></i> New Post</a>
</nav>
</div>
</header>
<main>
Add.ejs:
<%- include('partials/header') %>
<section class="add-form">
9
<h2><i class="fas fa-pen"></i> Create New Blog Post</h2>
<form action="/add" method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="title">Title</label>
<input type="text" id="title" name="title" required
placeholder="Enter your blog title">
</div>
<div class="form-group">
<label for="category">Category</label>
<select id="category" name="category" required>
<option value="" disabled selected>Select a category</option>
<option value="Philosophy">Philosophy</option>
<option value="Social Issues">Social Issues</option>
<option value="Personal Journey">Personal Journey</option>
</select>
<small>Choose the most relevant category for your post</small>
</div>
<div class="form-group">
<label for="image">Featured Image</label>
<input type="file" id="image" name="image" accept="image/*">
<small>Max size: 1MB. Supported formats: JPG, PNG, GIF</small>
</div>
<div class="form-group">
<label for="content">Content</label>
<textarea id="content" name="content" rows="10" required
placeholder="Write your blog content here..."></textarea>
</div>
<button type="submit" class="btn">
<i class="fas fa-paper-plane"></i> Publish Post
</button>
</form>
</section>
<%- include('partials/footer') %>
Blogs.ejs:
<%- include('partials/header') %>
<article class="full-blog">
<div class="blog-actions">
<a href="/blog/<%= blog._id %>/edit" class="edit-btn" title="Edit Post">
<i class="fas fa-pen"></i>
</a>
<button class="delete-btn" data-id="<%= blog._id %>" title="Delete Post">
<i class="fas fa-trash-alt"></i>
</button>
</div>
<h1><%= blog.title %></h1>
<p class="date">
<i class="far fa-calendar-alt"></i>
<%= new Date(blog.createdAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}) %>
</p>
<div class="content"><%= blog.content %></div>
<a href="/" class="btn back-btn">
<i class="fas fa-arrow-left"></i> Back to all posts
10
</a>
</article>
<script>
document.querySelector('.delete-btn').addEventListener('click', async (e) => {
if (!confirm('Are you sure you want to delete this blog post? This action cannot be undone.')) {
return;
}
const blogId = e.target.closest('.delete-btn').getAttribute('data-id');
try {
const response = await fetch(`/blog/${blogId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.redirect) {
window.location.href = data.redirect;
} else {
throw new Error(data.error || 'Failed to delete post');
}
} catch (err) {
console.error('Error:', err);
alert('Failed to delete the blog post. Please try again.');
}
});
</script>
<%- include('partials/footer') %>
Edit.ejs:
<%- include('partials/header') %>
<section class="edit-form">
<h2><i class="fas fa-edit"></i> Edit Blog Post</h2>
<form id="editBlogForm" class="blog-form" enctype="multipart/form-data">
<div class="form-group">
<label for="title">Title</label>
<input type="text" id="title" name="title" required
value="<%= blog.title %>"
placeholder="Enter your blog title">
</div>
<div class="form-group">
<label for="category">Category</label>
<select id="category" name="category" required>
<option value="" disabled>Select a category</option>
<option value="Philosophy" <%= blog.category === 'Philosophy' ? 'selected' : '' %>>Philosophy</option>
<option value="Social Issues" <%= blog.category === 'Social Issues' ? 'selected' : '' %>>Social Issues</option>
<option value="Personal Journey" <%= blog.category === 'Personal Journey' ? 'selected' : '' %>>Personal
Journey</option>
</select>
</div>
11
<div class="form-group">
<label for="image">Featured Image</label>
<input type="file" id="image" name="image" accept="image/*" onchange="previewImage(event)">
<small>Current image: <%= blog.image %></small>
<img src="<%= blog.image %>" alt="Current featured image" class="preview-image" id="imagePreview">
</div>
<div class="form-group">
<label for="content">Content</label>
<textarea id="content" name="content" rows="10" required
placeholder="Write your blog content here..."><%= blog.content %></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn save-btn">
<i class="fas fa-save"></i> Save Changes
</button>
<a href="/blog/<%= blog._id %>" class="btn cancel-btn">
<i class="fas fa-times"></i> Cancel
</a>
</div>
</form>
</section>
<script>
function previewImage(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
document.getElementById('imagePreview').src = e.target.result;
}
reader.readAsDataURL(file);
}
}
document.getElementById('editBlogForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
const response = await fetch(`/blog/<%= blog._id %>`, {
method: 'PUT',
body: formData
});
const data = await response.json();
if (data.success) {
window.location.href = `/blog/<%= blog._id %>`;
} else {
throw new Error(data.error || 'Error updating blog post');
}
} catch (err) {
console.error('Error:', err);
alert('Failed to update the blog post. Please try again.');
}
12
});
</script>
<%- include('partials/footer') %>
App.ejs:
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const path = require('path');
const blogRoutes = require('./routes/blog');
const app = express();
// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI)
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.error('MongoDB connection error:', err));
// Middleware
app.set('view engine', 'ejs');
app.use(express.static('public'));
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
// Routes
app.use('/', blogRoutes);
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Package.json:
{
"name": "mp-karan",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"dotenv": "^16.4.7",
"ejs": "^3.1.10",
"express": "^5.1.0",
"mongoose": "^8.13.2",
"multer": "^1.4.5-lts.2"
}
}
13
User interface :
14
Conclusion
The Sapientis Blogging Website is a fully functional, modern blogging platform that
demonstrates strong proficiency in full-stack development, UI/UX design, and database
management. By combining Node.js, Express, MongoDB, and EJS, the project delivers a
responsive and scalable solution for content creators.
With future enhancements, the platform can evolve into a complete CMS, supporting multi-
user roles, SEO, and advanced engagement features. This project serves as an excellent
foundation for further learning and real-world application in web development.
15