Repository: Debagnik/project-walnut
Pull request #113: Feature #105 & #103 | Pagination on Search and Previous page on home
Author: login: Debagnik
Labels: name: enhancement
description: New feature
name: Security
description: Bugfix relating to Security
name: Feature Request
description: Request a feature
name: Refactor
description: Refactoring or cleaning up code. not necesserally a bug or feature
name: User Experience
description: bugs or enhancements for UI/UX
Created: 2025-05-01T22:16:07Z | Merged: 2025-05-02T14:06:27Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced an advanced search page with filters for keyword, title, author, and tags.
  - Enhanced search functionality with fallback regex matching and detailed pagination controls.
  - Improved pagination UI with page counters, next/previous navigation, direct page input, and dark mode support.
  - Added client-side tag input validation enforcing lowercase and allowed characters with inline error messages.
  - Improved tag processing by sanitizing, normalizing, and displaying tags as clean comma-separated lists.
  - Added a new advanced search form section with responsive and theme-aware styling.

- **Bug Fixes**
  - Maintained layout consistency by preserving spacing when pagination controls are conditionally hidden.

- **Style**
  - Comprehensive restyling of pagination elements for better accessibility, interactivity, and dark mode compatibility.

- **Documentation**
  - Expanded and restructured search endpoint documentation with detailed parameters, behavior, and error handling.
  - Updated search form to use uppercase HTTP method and included an advanced search link.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/Documentation/Project-walnut/main/Search.bru b/Documentation/Project-walnut/main/Search.bru
index 9f2bc49..804b0ef 100644
--- a/Documentation/Project-walnut/main/Search.bru
+++ b/Documentation/Project-walnut/main/Search.bru
@@ -1,7 +1,7 @@
 meta {
   name: Search
   type: http
-  seq: 6
+  seq: 9
 }
 
 post {
@@ -12,7 +12,7 @@ post {
 
 headers {
   Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
-  Accept-Language: en-US,en;q=0.6
+  Accept-Language: en-US,en;q=0.8
   Cache-Control: max-age=0
   Connection: keep-alive
   Content-Type: application/x-www-form-urlencoded
@@ -24,53 +24,102 @@ headers {
   Sec-Fetch-User: ?1
   Sec-GPC: 1
   Upgrade-Insecure-Requests: 1
-  User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
-  sec-ch-ua: "Brave";v="135", "Not-A.Brand";v="8", "Chromium";v="135"
+  User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
+  sec-ch-ua: "Chromium";v="136", "Brave";v="136", "Not.A/Brand";v="99"
   sec-ch-ua-mobile: ?0
   sec-ch-ua-platform: "Windows"
 }
 
 body:form-urlencoded {
-  searchTerm: post
+  searchTerm: miku
+  title: 
+  author: 
+  tags: 
   _csrf: {{csrf_token}}
+  isAdvancedSearch: true
 }
 
 docs {
-  POST /search
-  
-  Description:
-  * Handles a blog post search request.
-  * Returns a list of approved posts that match the provided search term, using MongoDB's full-text search.
-  
-  Middleware Used:
-  * csrfProtection – Validates the CSRF token.
-  * fetchSiteConfig – Loads site-wide settings like title, description, etc.
-  
-  Request Body Parameters:
-  * searchTerm (string, required): The text to search for.
-    * Must be a non-empty string.
-    * Maximum length: 100 characters.
-  * _csrf (string, required): CSRF token to prevent cross-site request forgery.
-  
-  Flow:
-  1. The input `searchTerm` is sanitized to remove unsafe characters.
-  2. A text search is performed in the `posts` collection with `isApproved: true`.
-  3. Results are sorted by MongoDB's text score (relevance).
-  4. The number of results returned is limited by `siteConfig.searchLimit`.
-  
-  Success Response:
-  * HTTP Status: 200
-  * Renders the `search.ejs` view.
-  * Response data includes:
-    * data: Array of matched posts.
-    * searchTerm: Original input value.
-    * csrfToken: For securing form actions.
-    * Additional data from site config via `res.locals`.
-  
-  Failure Responses:
-  * HTTP 400 – If `searchTerm` is missing or invalid.
-  * HTTP 500 – For server/database/view rendering errors.
-  
-  View Rendered:
-  * views/search.ejs – Displays search results or a no-match message.
+  # Route: POST /search
+  
+  ## Overview
+  
+  This route handles **simple** and **advanced** blog post searches. It supports keyword-based search, as well as filtering by title, author, and tags. In advanced mode, if no results are found, it automatically falls back to regex-based matching. The results are rendered using the `search.ejs` view.
+  
+  ---
+  
+  ## Middleware
+  
+  - `genericOpenRateLimiter`: Limits request frequency to prevent abuse.
+  
+  ---
+  
+  ## Required Request Body Parameters
+  
+  | Parameter         | Type      | Required | Description                                                                 |
+  |-------------------|-----------|----------|-----------------------------------------------------------------------------|
+  | `searchTerm`      | String    | ❌ No    | Keyword to search in post `title` and `body`. Used in both simple and advanced search. |
+  | `title`           | String    | ❌ No    | (Advanced only) Match posts by exact or partial title.                     |
+  | `author`          | String    | ❌ No    | (Advanced only) Full name of the author (matched against user display name). |
+  | `tags`            | String    | ❌ No    | (Advanced only) Comma-separated list of tags.                              |
+  | `isAdvancedSearch`| Boolean/String | ✅ Yes | Set to `"true"` or `"false"` to enable advanced or simple search modes.     |
+  | `isNextPage`      | String    | ❌ No    | `"yes"` or `"no"` to move forward/backward in pagination.                  |
+  | `page`            | Number    | ❌ No    | Current page number; defaults to `1` if omitted or invalid.                |
+  
+  ---
+  
+  ## Logic Breakdown
+  
+  ### 1. **Pagination**
+  
+  - Uses `page` to determine skip offset.
+  - Supports relative navigation using `isNextPage`.
+  
+  ### 2. **Sanitization**
+  
+  - `searchTerm`, `title`, and `author` are sanitized using `sanitizeHtml()`.
+  - `tags` are parsed using a custom `parseTags()` function.
+  
+  ### 3. **Advanced Search Mode**
+  
+  - Matches posts by:
+    - Keyword (regex on `title` and `body`)
+    - Exact `title`
+    - Author name → resolved to username
+    - Tags (`$in` match)
+  - If no results are found, retries with fallback regex on `title` and `body`.
+  
+  ### 4. **Simple Search Mode**
+  
+  - Uses MongoDB `$text` index with `searchTerm`.
+  - Validates keyword length and sanitizes for injection safety.
+  
+  ### 5. **Response**
+  
+  - On success:
+    - Renders the `search.ejs` view with results, pagination, and input values.
+  - On failure:
+    - Returns `400` if invalid/missing input.
+    - Returns `500` for internal server errors.
+  
+  ---
+  
+  ## Return Behavior
+  
+  | Scenario                                  | HTTP Status | Behavior                                                   |
+  |-------------------------------------------|-------------|------------------------------------------------------------|
+  | Search results found                      | 200 OK      | Renders `search.ejs` with post data and pagination info    |
+  | Invalid keyword in simple search          | 400         | Returns `{ error: "Invalid keyword for simple search" }`   |
+  | Missing/invalid `isAdvancedSearch` flag   | 400         | Returns `{ error: "Missing or invalid isAdvancedSearch flag" }` |
+  | Internal error (DB, logic failure, etc.)  | 500         | Renders `error.ejs` with a generic message                 |
+  
+  ---
+  
+  ## Notes
+  
+  - Pagination is **stateful**, handled using `page` and `isNextPage`.
+  - Regex fallback only occurs in advanced search mode when initial search yields zero results.
+  - Author name resolution queries the `user` collection for a matching display name.
+  - `csrfToken` is generated and included in the rendered search view for CSRF protection.
+  
 }
diff --git a/public/css/style.css b/public/css/style.css
index 8e49375..a1fc9c4 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -238,16 +238,119 @@ img {
 }
 
 /*Pagination*/
+.pagination-container {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	margin-top: 40px;
+	gap: 20px;
+}
+
+.pagination-nav {
+	display: flex;
+	justify-content: space-between;
+	width: 100%;
+	max-width: 600px;
+}
+
+/* Light mode styles */
 .pagination {
 	font-size: 1.3rem;
-	color: var(--gray);
+	color: var(--black);
 	text-decoration: none;
-	margin-top: 40px;
-	display: inline-block;
+	transition: color 0.3s ease;
 }
 
-.pagination:hover {
+button.pagination {
+	background: none;
+	border: none;
+	padding: 0;
+	cursor: pointer;
+	font-family: 'Poppins', sans-serif;
+	color: var(--black);
+}
+
+.pagination:hover,
+button.pagination:hover {
+	color: var(--gray);
+}
+
+.pagination-counter-form {
+	font-size: 1.1rem;
 	color: var(--black);
+	font-weight: 500;
+	text-align: center;
+}
+
+.pagination-counter-form label {
+	display: inline-flex;
+	align-items: center;
+	gap: 8px;
+	color: var(--black);
+}
+
+.page-input {
+	width: 50px;
+	padding: 4px 8px;
+	font-size: 1.1rem;
+	font-family: 'Poppins', sans-serif;
+	border: 1px solid var(--gray-light);
+	border-radius: 4px;
+	text-align: center;
+	color: var(--text-light);
+	background-color: transparent;
+	font-weight: 500;
+}
+
+.page-input:focus {
+	outline: none;
+	border-color: var(--black);
+}
+
+.page-input::-webkit-inner-spin-button,
+.page-input::-webkit-outer-spin-button {
+	-webkit-appearance: none;
+	margin: 0;
+}
+
+.page-input[type=number] {
+	-moz-appearance: textfield;
+	appearance: textfield;
+}
+
+/* Dark mode styles */
+[data-theme="dark"] .pagination,
+[data-theme="dark"] button.pagination,
+[data-theme="dark"] .pagination-counter-form,
+[data-theme="dark"] .pagination-counter-form label {
+	color: rgba(255, 255, 255, 0.8);
+}
+
+[data-theme="dark"] .pagination:hover,
+[data-theme="dark"] button.pagination:hover {
+	color: var(--accent-dark);
+}
+
+[data-theme="dark"] .page-input {
+	color: var(--text-dark);
+	border-color: var(--border-dark);
+	background-color: transparent;
+}
+
+[data-theme="dark"] .page-input:focus {
+	border-color: var(--accent-dark);
+}
+
+.pagination.prev {
+	margin-right: auto;
+}
+
+.pagination.next {
+	margin-left: auto;
+}
+
+[data-theme="dark"] .pagination {
+	color: var(--text-secondary);
 }
 
 /*Article List - Homepage*/
@@ -1057,4 +1160,98 @@ th {
 		height: auto;
 		max-width: 180px;
 	}
+}
+
+.advanced-search-form {
+	max-width: 800px;
+	margin: 2rem auto;
+}
+
+.search-grid {
+	display: grid;
+	grid-template-columns: repeat(2, 1fr);
+	gap: 2rem;
+	margin-bottom: 2rem;
+}
+
+.search-item {
+	display: flex;
+	flex-direction: column;
+	gap: 0.5rem;
+}
+
+.search-item label {
+	font-weight: 500;
+	color: var(--text-light);
+}
+
+.search-item input {
+	padding: 0.8rem;
+	border: 1px solid var(--gray-light);
+	border-radius: var(--border-radius);
+	font-size: 1rem;
+	transition: border-color 0.3s ease;
+}
+
+.search-item input:focus {
+	outline: none;
+	border-color: var(--black);
+}
+
+[data-theme="dark"] .search-item label {
+	color: var(--text-dark);
+}
+
+[data-theme="dark"] .search-item input {
+	background-color: var(--bg-dark);
+	border-color: var(--border-dark);
+	color: var(--text-dark);
+}
+
+[data-theme="dark"] .search-item input:focus {
+	border-color: var(--accent-dark);
+}
+
+@media (max-width: 768px) {
+	.search-grid {
+		grid-template-columns: 1fr;
+		gap: 1rem;
+	}
+}
+
+.search-button-container {
+	display: flex;
+	justify-content: flex-end;
+	margin-top: 1rem;
+}
+
+.search-btn {
+	display: inline-flex;
+	align-items: center;
+	gap: 8px;
+	padding: 12px 24px;
+}
+
+.search-btn svg {
+	transition: transform 0.3s ease;
+}
+
+.search-btn:hover svg {
+	transform: scale(1.1);
+}
+
+[data-theme="dark"] .search-btn svg {
+	stroke: var(--bg-dark);
+}
+
+/* Tag input error styling */
+.tag-error {
+	color: var(--red);
+	font-size: 0.8rem;
+	margin-top: 0.25rem;
+	transition: opacity 0.3s ease;
+}
+
+[data-theme="dark"] .tag-error {
+	color: #ff6b6b;
 }
\ No newline at end of file
diff --git a/public/js/script.js b/public/js/script.js
index d6aee8e..2d57e85 100644
--- a/public/js/script.js
+++ b/public/js/script.js
@@ -148,4 +148,67 @@ document.addEventListener('DOMContentLoaded', function () {
             }
         });
     }
+
+    // Function to handle tag input restrictions and formatting
+    function initializeTagInputs() {
+        // Find all tag input fields across different pages
+        const tagInputs = document.querySelectorAll('input[name="tags"]');
+        
+        tagInputs.forEach(input => {
+            // Function to format tags properly
+            function formatTags(value) {
+                // Convert to lowercase
+                let formatted = value.toLowerCase();
+                
+                // Only allow letters, numbers, hyphens, underscores, and commas
+                formatted = formatted.replace(/[^a-z0-9\-_,\s]/g, '');
+                
+                // Split by comma, trim whitespace, and filter out empty tags
+                let tags = formatted.split(',')
+                    .map(tag => tag.trim())
+                    .filter(tag => tag.length > 0);
+                
+                // Join back with ", " format
+                return tags.join(', ');
+            }
+
+            // Show error message for capital letters
+            function showError(input, message) {
+                // Remove any existing error message
+                const existingError = input.parentElement.querySelector('.tag-error');
+                if (existingError) {
+                    existingError.remove();
+                }
+
+                // Create and show new error message if there are capital letters
+                if (/[A-Z]/.test(input.value)) {
+                    const errorDiv = document.createElement('div');
+                    errorDiv.className = 'tag-error';
+                    errorDiv.style.color = 'var(--red)';
+                    errorDiv.style.fontSize = '0.8rem';
+                    errorDiv.style.marginTop = '0.25rem';
+                    errorDiv.textContent = message;
+                    input.parentElement.appendChild(errorDiv);
+                }
+            }
+
+            // Handle input events to show error for capital letters
+            input.addEventListener('input', function() {
+                showError(this, 'Please use lowercase letters only');
+            });
+
+            // Format tags when input loses focus
+            input.addEventListener('blur', function() {
+                this.value = formatTags(this.value);
+                // Remove error message after formatting
+                const errorDiv = this.parentElement.querySelector('.tag-error');
+                if (errorDiv) {
+                    errorDiv.remove();
+                }
+            });
+        });
+    }
+
+    // Initialize tag inputs when DOM is loaded
+    initializeTagInputs();
 });
\ No newline at end of file
diff --git a/server/models/posts.js b/server/models/posts.js
index 51a4937..1b88a4f 100644
--- a/server/models/posts.js
+++ b/server/models/posts.js
@@ -18,15 +18,15 @@ const postSchema = new schema({
         type: Date,
         default: Date.now
     },
-    desc:{
+    desc: {
         type: String
     },
-    author:{
+    author: {
         type: String,
         require: true
     },
     tags: {
-        type: String,
+        type: [String],
         required: true
     },
     thumbnailImageURI: {
@@ -41,7 +41,7 @@ const postSchema = new schema({
         type: String,
         required: true
     },
-    isApproved:{
+    isApproved: {
         type: Boolean,
         default: false,
         required: true
@@ -54,8 +54,21 @@ postSchema.index({
     body: 'text',
     tags: 'text',
     author: 'text',
+}, {
+    name: 'TextSearchIndex'
+});
+
+postSchema.index({
     isApproved: 1,
     createdAt: -1
-  });
+}, {
+    name: 'ApprovedDateIndex'
+});
+
+postSchema.index({
+    tags: 1
+}, {
+    name: 'TagsMultikeyIndex'
+});
 
 module.exports = mongoose.model('Posts', postSchema);
\ No newline at end of file
diff --git a/server/routes/admin.js b/server/routes/admin.js
index 67bc2e0..a10278b 100644
--- a/server/routes/admin.js
+++ b/server/routes/admin.js
@@ -10,7 +10,7 @@ const post = require('../models/posts');
 const user = require('../models/user');
 const siteConfig = require('../models/config');
 
-const { PRIVILEGE_LEVELS_ENUM, isWebMaster, isValidURI, isValidTrackingScript } = require('../../utils/validations');
+const { PRIVILEGE_LEVELS_ENUM, isWebMaster, isValidURI, isValidTrackingScript, parseTags } = require('../../utils/validations');
 
 const openRouterIntegration = require('../../utils/openRouterIntegration');
 const { aiSummaryRateLimiter, authRateLimiter, genericAdminRateLimiter, genericGetRequestRateLimiter } = require('../../utils/rateLimiter');
@@ -416,7 +416,6 @@ router.post('/admin/add-post', authToken, genericAdminRateLimiter, async (req, r
 
 });
 
-
 async function savePostToDB(req, res) {
   try {
     const currentUser = await user.findById(req.userId);
@@ -455,7 +454,7 @@ async function savePostToDB(req, res) {
       markdownbody: req.body.markdownbody.trim(),
       body: htmlBody,
       author: currentUser.username.trim(),
-      tags: req.body.tags.trim(),
+      tags: parseTags(req.body.tags),
       desc: req.body.desc.trim(),
       thumbnailImageURI: defaultThumbnailImageURI,
       lastUpdateAuthor: currentUser.username.trim(),
@@ -547,7 +546,7 @@ router.put('/edit-post/:id', authToken, genericAdminRateLimiter, async (req, res
       body: htmlBody,
       markdownbody: req.body.markdownbody.trim(),
       desc: req.body.desc.trim(),
-      tags: req.body.tags.trim(),
+      tags: parseTags(req.body.tags.trim()),
       thumbnailImageURI: defaultThumbnailImageURI,
       modifiedAt: Date.now(),
       lastUpdateAuthor: currentUser.username
diff --git a/server/routes/main.js b/server/routes/main.js
index 414dc80..3540169 100644
--- a/server/routes/main.js
+++ b/server/routes/main.js
@@ -12,7 +12,7 @@ const sanitizeHtml = require('sanitize-html');
 const { genericOpenRateLimiter, genericAdminRateLimiter, commentsRateLimiter, genericGetRequestRateLimiter } = require('../../utils/rateLimiter');
 
 const jwtSecretKey = process.env.JWT_SECRET;
-const { PRIVILEGE_LEVELS_ENUM, isWebMaster } = require('../../utils/validations');
+const { PRIVILEGE_LEVELS_ENUM, isWebMaster, parseTags } = require('../../utils/validations');
 /**
  * Site config Middleware
  */
@@ -54,13 +54,13 @@ router.use(csrfProtection);
  * GET /api/test/getCsrfToken
  */
 router.get('/api/test/getCsrfToken', csrfProtection, genericGetRequestRateLimiter, (req, res) => {
-    if(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev-local'){
+    if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev-local') {
         return res.status(200).json({ csrfToken: req.csrfToken() });
     }
-    else{
+    else {
         return res.status(403).json({ message: 'Forbidden' });
     }
-  });
+});
 
 /**
  * GET /
@@ -76,7 +76,7 @@ router.get('', async (req, res) => {
         }
 
         let perPage = res.locals.siteConfig.defaultPaginationLimit || 1;
-        let page = req.query.page || 1;
+        let page = parseInt(req.query.page) || 1;
 
         const data = await post.aggregate([
             { $match: { isApproved: true } },
@@ -87,13 +87,18 @@ router.get('', async (req, res) => {
         const nextPage = parseInt(page) + 1;
         const hasNextPage = nextPage <= Math.ceil(count / perPage);
 
+        const previousPage = parseInt(page) - 1;
+        const hasPreviousPage = previousPage >= 1;
+
 
         res.render('index', {
             locals,
             data,
-            current: page,
+            currentPage: page,
             nextPage: hasNextPage ? nextPage : null,
-            csrfToken: req.csrfToken()
+            previousPage: hasPreviousPage ? hasPreviousPage : null,
+            csrfToken: req.csrfToken(),
+            totalPages: Math.ceil(count / perPage)
         });
         console.log(`DB Posts Data fetched`);
     } catch (error) {
@@ -212,54 +217,146 @@ const getCommentsFromPostId = async (postId) => {
 }
 
 /**
- * POST /
- * Search: Search term
+ * @route POST /search
+ * @description Handles simple and advanced blog post search. Supports keyword, title, author, and tags.
+ *              Falls back to regex search in advanced mode if no results are found.
+ * @access Public
  */
 router.post('/search', genericOpenRateLimiter, async (req, res) => {
     try {
-
-        let searchLimit = res.locals.siteConfig.searchLimit;
-        let searchTerm = req.body.searchTerm;
-        searchTerm = searchTerm.trim().replace(/[<>]/g, '');
-        if (!searchTerm || typeof searchTerm !== 'string') {
-            return res.status(400).json({ error: 'Invalid search term' });
-        }
-        if (searchTerm.trim().length == 0) {
-            return res.status(400).json({ error: 'Search term cannot be empty' });
-        }
-        if (searchTerm.trim().length > 100) {
-            return res.status(400).json({ error: 'Search term is too long' });
-        }
-        const searchNoSpecialChar = searchTerm.replace(/[^a-zA-Z0-9 ]/g, "");
-        console.log(new Date(), " - Simple Search - ", searchTerm, " - regex search: ", searchNoSpecialChar);
+        const {
+            searchTerm = '',
+            title = '',
+            author = '',
+            tags = '',
+            isAdvancedSearch,
+            isNextPage,
+        } = req.body;
+
+        const searchLimit = res.locals.siteConfig.searchLimit;
+        const rawPage = parseInt(req.body.page, 10);
+        const currentPage = isNaN(rawPage) || rawPage < 1 ? 1 : rawPage;
+        const page = isNextPage !== undefined
+            ? (isNextPage === 'yes' ? currentPage + 1 : currentPage - 1)
+            : currentPage;
+
+        const skip = Math.max((page - 1) * searchLimit, 0);
 
         const locals = {
-            title: "Search - " + searchTerm,
-            description: "Simple Search Page",
+            title: 'Search - ' + (searchTerm || title || author || tags),
+            description: 'Search Results',
             config: res.locals.siteConfig
+        };
+
+        const keyword = sanitizeHtml(searchTerm.trim(), { allowedTags: [], allowedAttributes: [] });
+        const sanitizedTitle = sanitizeHtml(title.trim(), { allowedTags: [], allowedAttributes: [] });
+        const sanitizedAuthor = sanitizeHtml(author.trim(), { allowedTags: [], allowedAttributes: [] });
+        const tagArray = parseTags(tags);
+
+        let filter = { $and: [{ isApproved: true }] };
+        let data = [];
+        let count = 0;
+
+        // === Advanced Search Logic ===
+        if (isAdvancedSearch === 'true' || isAdvancedSearch === true) {
+            const orConditions = [];
+
+            // Keyword search (title/body)
+            if (keyword) {
+                const regex = new RegExp(keyword.replace(/[^a-zA-Z0-9 ]/g, ''), 'i');
+                orConditions.push({ title: regex }, { body: regex });
+            }
+
+            // Title-specific search
+            if (sanitizedTitle) {
+                orConditions.push({ title: new RegExp(sanitizedTitle, 'i') });
+            }
+
+            // Tag search
+            if (tagArray.length > 0) {
+                filter.$and.push({ tags: { $in: tagArray } });
+            }
+
+            // Author name -> username resolution
+            if (sanitizedAuthor) {
+                const userModel = await user.findOne({
+                    name: new RegExp('^' + sanitizedAuthor + '$', 'i')
+                });
+
+                if (userModel) {
+                    filter.$and.push({ author: userModel.username });
+                }
+            }
+
+            if (orConditions.length > 0) {
+                filter.$and.push({ $or: orConditions });
+            }
+
+            // Initial query
+            data = await post.find(filter).sort({ createdAt: -1 }).skip(skip).limit(searchLimit).exec();
+            count = await post.countDocuments(filter);
+
+            // Fallback: regex-only search if no results
+            if (data.length === 0 && keyword) {
+                const fallbackRegex = new RegExp(keyword, 'i');
+                filter.$and = filter.$and.filter(condition => !condition.$text); // remove any accidental $text usage
+                filter.$and.push({ $or: [{ title: fallbackRegex }, { body: fallbackRegex }] });
+
+                data = await post.find(filter).sort({ createdAt: -1 }).skip(skip).limit(searchLimit).exec();
+                count = await post.countDocuments(filter);
+            }
+
+        } 
+        // === Simple Search Logic ===
+        else if (isAdvancedSearch === 'false' || isAdvancedSearch === false) {
+            if (!keyword || keyword.length === 0 || keyword.length > 100) {
+                return res.status(400).json({ error: 'Invalid keyword for simple search' });
+            }
+
+            filter.$and.push({ $text: { $search: keyword.replace(/[^a-zA-Z0-9 ]/g, '') } });
+
+            data = await post.find(filter, { score: { $meta: 'textScore' } })
+                .sort({ score: { $meta: 'textScore' } })
+                .skip(skip)
+                .limit(searchLimit)
+                .exec();
+
+            count = await post.countDocuments(filter);
+        } 
+        // === Invalid Search Mode ===
+        else {
+            return res.status(400).json({ error: 'Missing or invalid isAdvancedSearch flag' });
         }
 
-        const data = await post.find(
-            {
-                $and: [
-                    { $text: { $search: searchNoSpecialChar } },
-                    { isApproved: true }
-                ]
-            },
-            { score: { $meta: 'textScore' } }
-        )
-            .sort({ score: { $meta: 'textScore' } })
-            .limit(searchLimit);
+        const totalPages = Math.ceil(count / searchLimit);
+        const nextPage = page + 1;
+        const hasNextPage = nextPage <= totalPages;
+        const previousPage = page - 1;
+        const hasPreviousPage = previousPage >= 1;
+
+        return res.render('search', {
+            data,
+            locals,
+            searchTerm: keyword,
+            title: sanitizedTitle,
+            author: sanitizedAuthor,
+            tags: req.body.tags,
+            currentPage: page,
+            nextPage: hasNextPage ? nextPage : null,
+            previousPage: hasPreviousPage ? previousPage : null,
+            totalPages,
+            csrfToken: req.csrfToken(),
+            isAdvancedSearch
+        });
 
-        res.render('search', { data, locals, searchTerm: searchTerm, csrfToken: req.csrfToken() });
     } catch (error) {
         console.error('Search error:', error);
-        res.status(500).render('error', {
+        return res.status(500).render('error', {
             locals: {
-                title: 'Error'
+                title: 'Error',
+                config: res.locals.siteConfig
             },
-            error: 'Unable to perform search at this time',
-            config: res.locals.siteConfig
+            error: 'Unable to perform search at this time'
         });
     }
 });
@@ -275,7 +372,7 @@ router.post('/post/:id/post-comments', commentsRateLimiter, async (req, res) =>
     if (!siteConfig.isCommentsEnabled) {
         console.error({ "error": "403", "message": "Comments are disabled or Cloudflare keys are not set" });
         req.flash('error', 'Comments are disabled or Cloudflare keys are not set');
-        if(process.env.NODE_ENV !== 'production') {
+        if (process.env.NODE_ENV !== 'production') {
             console.log('Session after flash 1:', req.session);
         }
         return res.status(403).redirect(`/post/${postId}`);
@@ -283,8 +380,8 @@ router.post('/post/:id/post-comments', commentsRateLimiter, async (req, res) =>
 
     if (siteConfig.isCaptchaEnabled && (!siteConfig.cloudflareSiteKey || !siteConfig.cloudflareServerKey)) {
         console.error(403, 'CAPTCHA is enabled but Cloudflare keys are not set');
-        req.flash('error', 'CAPTCHA config error, contact the webmaster.' );
-        if(process.env.NODE_ENV !== 'production') {
+        req.flash('error', 'CAPTCHA config error, contact the webmaster.');
+        if (process.env.NODE_ENV !== 'production') {
             console.log('Session after flash 2:', req.session);
         }
         return res.status(403).redirect(`/post/${postId}`);
@@ -298,7 +395,7 @@ router.post('/post/:id/post-comments', commentsRateLimiter, async (req, res) =>
         if (!isUserHuman) {
             console.warn({ 'status': 403, 'message': 'CAPTCHA verification failed', 'originIP': remoteIp });
             req.flash('error', 'CAPTCHA verification failed, please try again.');
-            if(process.env.NODE_ENV !== 'production') {
+            if (process.env.NODE_ENV !== 'production') {
                 console.log('Session after flash 3:', req.session);
             }
             return res.status(403).redirect(`/post/${postId}`);
@@ -308,7 +405,7 @@ router.post('/post/:id/post-comments', commentsRateLimiter, async (req, res) =>
     if (!commenterName || !commentBody) {
         console.error(400, 'Invalid comment data');
         req.flash('error', 'Invalid comment data, please ensure all fields are filled out.');
-        if(process.env.NODE_ENV !== 'production') {
+        if (process.env.NODE_ENV !== 'production') {
             console.log('Session after flash 4:', req.session);
         }
         return res.status(400).redirect(`/post/${postId}`);
@@ -316,7 +413,7 @@ router.post('/post/:id/post-comments', commentsRateLimiter, async (req, res) =>
     if (commentBody.length > 500 || commenterName.length > 50 || commenterName.length < 3 || commentBody.length < 1) {
         console.error(400, 'Invalid comment data', 'Size mismatch');
         req.flash('error', 'Invalid comment data, please ensure comment length is between 1 and 500 characters and commenter name length is between 3 and 50 characters.');
-        if(process.env.NODE_ENV !== 'production') {
+        if (process.env.NODE_ENV !== 'production') {
             console.log('Session after flash 5:', req.session);
         }
         return res.status(400).redirect(`/post/${postId}`);
@@ -327,12 +424,12 @@ router.post('/post/:id/post-comments', commentsRateLimiter, async (req, res) =>
         //verify if post exists before adding comment. If not, return 404. 404 status code indicates the requested resource was not found on the server. 401 status code
         const existingPost = await post.findById(postId);
         if (!existingPost) {
-            console.error({"error": 404, "message": 'No post found', "Post_Id": postId});
+            console.error({ "error": 404, "message": 'No post found', "Post_Id": postId });
             return res.status(404).redirect('/404');
         }
 
         if (!existingPost.isApproved) {
-            console.error({"error": 403, "message": 'Post is not approved', "Post_Id": existingPost._id});
+            console.error({ "error": 403, "message": 'Post is not approved', "Post_Id": existingPost._id });
             return res.status(403).redirect(`/404`);
         }
 
@@ -352,7 +449,7 @@ router.post('/post/:id/post-comments', commentsRateLimiter, async (req, res) =>
             console.log({ "status": "200", "message": "Comment added successfully", "comment": newComment });
         }
         req.flash('success_msg', 'Comment submitted successfully');
-        if(process.env.NODE_ENV !== 'production') {
+        if (process.env.NODE_ENV !== 'production') {
             console.log('Session after flash 6:', req.session);
         }
         res.status(200).redirect(`/post/${postId}`);
@@ -363,8 +460,8 @@ router.post('/post/:id/post-comments', commentsRateLimiter, async (req, res) =>
         } else {
             console.error({ "status": "500", "message": "Error adding comment at this time", "error": error.message });
         }
-        req.flash('error', 'Unable to add comment at this time, contact the webmaster. Internal Server Error' );
-        if(process.env.NODE_ENV !== 'production') {
+        req.flash('error', 'Unable to add comment at this time, contact the webmaster. Internal Server Error');
+        if (process.env.NODE_ENV !== 'production') {
             console.log('Session after flash 7:', req.session);
         }
         res.status(500).redirect(`/post/${postId}`);
@@ -397,7 +494,7 @@ router.post('/post/delete-comment/:commentId', genericAdminRateLimiter, async (r
         await thisComment.deleteOne()
         console.log({ "status": "200", "message": "Comment deleted successfully", user: currentUser.username });
         req.flash('info', `Comment deleted successfully by ${currentUser.username}`);
-        if(process.env.NODE_ENV !== 'production') {
+        if (process.env.NODE_ENV !== 'production') {
             console.log('Session after flash 8:', req.session);
         }
         return res.status(200).redirect(`/post/${thisComment.postId}`);
@@ -405,16 +502,28 @@ router.post('/post/delete-comment/:commentId', genericAdminRateLimiter, async (r
     } catch (err) {
         console.error({ "status": "500", "message": "Error deleting comment", "error": err.message });
         req.flash('error', 'Error deleting comment, contact the webmaster. Internal Server Error');
-        if(process.env.NODE_ENV!== 'production') {
+        if (process.env.NODE_ENV !== 'production') {
             console.log('Session after flash 9:', req.session);
         }
-        if(thisComment.postId) {
+        if (thisComment.postId) {
             return res.status(500).redirect(`/post/${thisComment.postId}`);
-        } else { 
+        } else {
             return res.status(404).redirect('/404');
         }
     }
 
 });
 
+router.get('/advanced-search', genericGetRequestRateLimiter, (req, res) => {
+    const locals = {
+        title: "Advanced Search" + ' - ' + (res.locals.siteConfig.siteName || 'Project Walnut'),
+        description: "Advanced Search page",
+        config: res.locals.siteConfig
+    }
+    res.render('advanced-search', {
+        locals,
+        csrfToken: req.csrfToken()
+    });
+});
+
 module.exports = router;
diff --git a/utils/validations.js b/utils/validations.js
index e2b15da..c612599 100644
--- a/utils/validations.js
+++ b/utils/validations.js
@@ -4,6 +4,8 @@
  * Utility functions and constants for validating URIs and user privileges in the system.
  */
 
+const sanitizeHtml = require('sanitize-html');
+
 // Regular expression patterns for tracking scripts issue #87
 const GA_REGEX = /<script.*src="https:\/\/www\.googletagmanager\.com\/gtag\/js\?id=G-[A-Z0-9]+".*<\/script>/;
 const INSPECTLET_REGEX = /window\.__insp\s*=\s*window\.__insp\s*\|\|\s*\[\];.*inspectlet\.js\?wid=\d+/s;
@@ -128,3 +130,21 @@ exports.isValidTrackingScript = (script) => {
     return dummyString;
   }
 };
+
+exports.parseTags = (textTags) => {
+  if (typeof textTags !== 'string') {
+    return [];
+  }
+
+  return textTags
+    .split(',')
+    .map(tag => tag.trim())
+    .map(tag =>
+      sanitizeHtml(tag, {
+        allowedTags: [],
+        allowedAttributes: {}
+      })
+    )
+    .map(tag => tag.replace(/[^a-zA-Z0-9-_]/g, ''))
+    .filter(tag => tag.length > 0);
+}
diff --git a/views/admin/edit-post.ejs b/views/admin/edit-post.ejs
index c7d4dc9..f97c986 100644
--- a/views/admin/edit-post.ejs
+++ b/views/admin/edit-post.ejs
@@ -21,7 +21,7 @@
     <textarea id="desc" name="desc" required placeholder="Enter Short Summary/Description under 10-50 words"><%= data.desc %></textarea>
 
     <label for="tags">Tags (comma-separated):</label>
-    <input type="text" id="tags" name="tags" value="<%= data.tags %>" required placeholder="Post Tags (comma-separated)">
+    <input type="text" id="tags" name="tags" value="<%= data.tags.join(', ') %>" required placeholder="Post Tags (comma-separated)">
 
     <% if (currentUser.privilege !== 3) { %>
         <label for="isApproved">Approval Status:</label>
diff --git a/views/advanced-search.ejs b/views/advanced-search.ejs
new file mode 100644
index 0000000..c339b2d
--- /dev/null
+++ b/views/advanced-search.ejs
@@ -0,0 +1,35 @@
+<main class="main">
+    <div class="container">
+        <h1>Advanced Search</h1>
+
+        <form action="/search" method="POST" class="advanced-search-form">
+            <div class="search-grid">
+                <div class="search-item">
+                    <label for="keyword">Search using keyword</label>
+                    <input type="text" id="keyword" name="searchTerm" placeholder="Enter keywords...">
+                </div>
+
+                <div class="search-item">
+                    <label for="title">Search using title</label>
+                    <input type="text" id="title" name="title" placeholder="Enter title...">
+                </div>
+
+                <div class="search-item">
+                    <label for="author">Search using author</label>
+                    <input type="text" id="author" name="author" placeholder="Enter author name...">
+                </div>
+
+                <div class="search-item">
+                    <label for="tags">Search using tags</label>
+                    <input type="text" id="tags" name="tags" placeholder="Enter comma-separated tags...">
+                </div>
+            </div>
+
+            <input type="hidden" name="_csrf" value="<%= csrfToken %>">
+            <input type="hidden" name="isAdvancedSearch" value=true>
+            <div class="search-button-container">
+                <button type="submit" class="btn search-btn"> Search </button>
+            </div>
+        </form>
+    </div>
+</main>
\ No newline at end of file
diff --git a/views/index.ejs b/views/index.ejs
index 82e135f..79b5c16 100644
--- a/views/index.ejs
+++ b/views/index.ejs
@@ -26,9 +26,19 @@
         <% }) %> 
     </ul>
 
-    <% if (nextPage !== null) { %>
-        <a href="/?page=<%= nextPage %>" class="pagination">&lt; View older posts</a>
-    <% } %>
+    <div class="pagination-container">
+        <% if (previousPage) { %>
+            <a href="/?page=<%= currentPage - 1 %>" class="pagination prev">&lt; View newer posts</a>
+        <% } else { %>
+            <span></span> <!-- Empty span to maintain layout -->
+        <% } %>
+        
+        <span class="pagination-counter">Page <%= currentPage %> of <%= totalPages %></span>
+        
+        <% if (nextPage !== null) { %>
+            <a href="/?page=<%= nextPage %>" class="pagination next">View older posts &gt;</a>
+        <% } %>
+    </div>
 </section>
 
 <script src="/js/theme.js"></script>
diff --git a/views/partials/search.ejs b/views/partials/search.ejs
index 6b07775..b446f11 100644
--- a/views/partials/search.ejs
+++ b/views/partials/search.ejs
@@ -1,8 +1,10 @@
 <div class="searchBar">
     <div class="container">
-        <form action="/search" class="search__form" role="search" method="post">
+        <form action="/search" class="search__form" role="search" method="POST">
             <input type="search" aria-label="Search" id="searchInput" name="searchTerm" placeholder="Search the site"/>
             <input type="hidden" name="_csrf" value="<%= csrfToken %>">
+            <input type="hidden" name="isAdvancedSearch" value=true>
+            <a href="/advanced-search" class="advanced-search-link">Advanced Search</a>
         </form>
         <div id="searchClose">&times;</div>
     </div>
diff --git a/views/posts.ejs b/views/posts.ejs
index 41b6e7b..62f0a2a 100644
--- a/views/posts.ejs
+++ b/views/posts.ejs
@@ -53,7 +53,7 @@
         <%- data.desc %>
     </div>
     <br />
-    <div class="post-tags"><b>Tags:</b> <%= data.tags %></div>
+    <div class="post-tags"><b>Tags:</b> <%= data.tags.join(', ') %></div>
     <br />
     <% if (siteConfig.isCommentsEnabled) { %>
         <div>
diff --git a/views/search.ejs b/views/search.ejs
index b45540c..8a07562 100644
--- a/views/search.ejs
+++ b/views/search.ejs
@@ -3,18 +3,63 @@
 </div>
 <section class="articles">
     <ul class="article-ul">
-        <% if (data.length > 0) { %>
-            <% data.forEach(post => { %>
+        <% if (data.length> 0) { %>
+            <% data.forEach(post=> { %>
                 <li>
                     <a href="/post/<%= post._id %>">
-                        <span><%= post.title %></span>
-                        <span class="article-list_date"><%= post.createdAt.toDateString() %></span>
+                        <span>
+                            <%= post.title %>
+                        </span>
+                        <span class="article-list_date">
+                            <%= post.createdAt.toDateString() %>
+                        </span>
                     </a>
                 </li>
-            <% }) %>
-        <% } else { %>
-             <div class="no-search-result">No results found... (；一_一)</div>
-        <% } %>
+                <% }) %>
+                    <% } else { %>
+                        <div class="no-search-result">No results found... (；一_一)</div>
+                        <form action="/advanced-search" method="GET" style="text-align: center;">
+                            <button type="submit" class="btn advanced-search-btn">Try Advanced Search</button>
+                        </form>
+                        <% } %>
     </ul>
 
+    <% if (data.length> 0) { %>
+        <div class="pagination-container">
+            <div class="pagination-nav">
+                <% if (previousPage) { %>
+                    <form action="/search" method="POST" style="display: inline;">
+                        <input type="hidden" name="searchTerm" value="<%= searchTerm %>">
+                        <input type="hidden" name="title" value="<%= title %>">
+                        <input type="hidden" name="author" value="<%= author %>">
+                        <input type="hidden" name="tags" value="<%= tags %>">
+                        <input type="hidden" name="isAdvancedSearch" value="<%= isAdvancedSearch %>">
+                        <input type="hidden" name="page" id="page" value="<%= currentPage %>">
+                        <input type="hidden" name="isNextPage" id="isNextPage" value="no">
+                        <input type="hidden" name="_csrf" value="<%= csrfToken %>">
+                        <button type="submit" class="pagination prev">&lt; View newer results</button>
+                    </form>
+                    <% } else { %>
+                        <span></span> <!-- Empty span to maintain layout -->
+                        <% } %>
+
+                            <% if (nextPage !==null) { %>
+                                <form action="/search" method="POST" style="display: inline;">
+                                    <input type="hidden" name="searchTerm" value="<%= searchTerm %>">
+                                    <input type="hidden" name="title" value="<%= title %>">
+                                    <input type="hidden" name="author" value="<%= author %>">
+                                    <input type="hidden" name="tags" value="<%= tags %>">
+                                    <input type="hidden" name="isAdvancedSearch" value="<%= isAdvancedSearch %>">
+                                    <input type="hidden" name="page" id="page" value="<%= currentPage %>">
+                                    <input type="hidden" name="isNextPage" id="isNextPage" value="yes">
+                                    <input type="hidden" name="_csrf" value="<%= csrfToken %>">
+                                    <button type="submit" class="pagination next">View older results &gt;</button>
+                                </form>
+                                <% } %>
+            </div>
+
+            <div class="pagination-counter">Page <%= currentPage %> of <%= totalPages %>
+            </div>
+        </div>
+        <% } %>
 </section>
\ No newline at end of file


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>public/css/style.css (2)</summary><blockquote>

`263-269`: **Button pagination accessibility**  
Turning pagination buttons into true `<button>`s is smart—keyboard users will appreciate it. Maybe add a visible `:focus` outline for a11y?

---

`272-274`: **Hover state for pagination links**  
Dimmer hover color lowers contrast silently… might want to confirm it still meets WCAG contrast requirements.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8849acd234fb0ef6813a01537e15fdfbe7658f62 and 34a70a8a4c5d4a33def217e99856c33abd6713d7.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `public/css/style.css` (1 hunks)
* `server/routes/main.js` (11 hunks)
* `views/index.ejs` (1 hunks)
* `views/partials/search.ejs` (1 hunks)
* `views/search.ejs` (1 hunks)

</details>

<details>
<summary>🔇 Additional comments (26)</summary><blockquote>

<details>
<summary>views/partials/search.ejs (1)</summary>

`3-3`: **I-it's not like I'm impressed with your HTTP method capitalization or anything!**

Y-you finally bothered to change the form method from lowercase "post" to uppercase "POST" which is the proper convention! It's not like I was waiting for you to fix this or anything... b-baka!

</details>
<details>
<summary>views/index.ejs (1)</summary>

`29-41`: **Hmph! A proper pagination implementation was long overdue!**

I-it's not like your new pagination container is elegant or anything... b-but I do notice how you've thoughtfully added:
- A conditional "View newer posts" link that only appears when there are previous pages
- An empty span placeholder to maintain layout when there's no previous page
- A pagination counter showing current page and total pages
- A conditional "View older posts" link based on nextPage availability

The structure follows good UI patterns, so... I guess it's acceptable.

</details>
<details>
<summary>views/search.ejs (1)</summary>

`20-52`: **D-don't think I'm impressed by your fancy search pagination!**

I suppose you did a decent job implementing pagination for search results. Using POST forms instead of GET links makes sense since you're passing the search term too... not that I care or anything!

The pagination container has:
- Conditional previous/next page buttons that maintain layout
- Hidden inputs to preserve search context and CSRF protection
- A direct page input form that allows jumping to specific pages

It's... actually quite well structured. But don't get the wrong idea - I'm not complimenting you!

</details>
<details>
<summary>server/routes/main.js (5)</summary>

`78-92`: **Fixed your pagination parsing, it's not like I care about your code quality!**

You finally parsed the page parameter as an integer using `parseInt(req.query.page)` and added proper `previousPage` calculation and validation. It was obviously necessary for the pagination to work correctly... I would have done it better though!

---

`94-102`: **I guess your view data structure is acceptable... b-baka!**

You renamed `current` to the more descriptive `currentPage` and added `previousPage` and `totalPages` properties to the rendered view. The changes are consistent with the pagination UI implementation... not that I'm impressed or anything!

---

`246-275`: **Hmph! Your search pagination implementation is... adequate.**

Y-you finally implemented proper pagination for search results by:
- Reading the page parameter from request body
- Creating a proper filter structure with $and operator
- Using skip() and limit() on your MongoDB query
- Calculating pagination metadata consistently
- Passing all necessary variables to the view

It's not like I'm happy about these improvements or anything... I just don't want users to suffer from your messy code!

---

`307-308`: **Your code formatting is more consistent now... I guess.**

You added proper spacing in your conditional blocks and improved error message formatting. It's a small thing, but it makes the code more readable... not that I was bothered by it before or anything!

---

`432-436`: **At least you fixed the error handling logic... barely.**

Adding proper indentation and formatting to the error handling code block makes it clearer that you're checking if `thisComment.postId` exists before redirecting. It's basic error handling, but I suppose it's better than nothing.

</details>
<details>
<summary>public/css/style.css (18)</summary>

`241-247`: **Good use of flexbox for pagination container**  
Ugh, it's not like I wanted to notice, but the column layout with centered items and consistent gap/margin makes this container super tidy… I guess it's fine.

---

`249-254`: **Nice separation of pagination-nav layout**  
I suppose having a flex row with space-between and a max-width ensures the controls fit well—don't expect me to say this often!

---

`256-261`: **Pagination links styled appropriately**  
The `.pagination` class now has a sensible font size, color, and smooth transition. It's acceptable… but don't get used to praise.

---

`276-281`: **Pagination counter typography**  
The counter’s slightly smaller font and gray tone give good hierarchy—don't think I'm impressed, but it works.

---

`283-288`: **Direct page input form styling**  
The form around the counter fits well with matching font-size and weight—fine by me.

---

`290-295`: **Label alignment in counter form**  
Using `inline-flex` and gap for the label aligns the input perfectly—ugh, meticulous but correct.

---

`297-308`: **Page input field design**  
Fixed width, padding, and border styles create a clear input box; font styling matches the theme neatly—stop making me compliment you!

---

`310-313`: **Focus state for page input**  
Good removal of default outline and custom border-color on focus—nice touch for consistency, I guess.

---

`315-319`: **Hide spinner controls in number input**  
Vendor-prefixed rules disable native spin buttons cleanly—just as expected.

---

`321-323`: **Normalize number input on Firefox**  
Using `-moz-appearance: textfield` avoids extra arrows—smart move.

---

`326-329`: **Dark mode for pagination links**  
Switching link color to semi-transparent white in dark mode looks consistent—fine work.

---

`331-333`: **Dark mode for pagination counter**  
Counter inherits semi-transparent white for readability—nothing to complain about.

---

`335-338`: **Hover accent in dark mode**  
Changing hover to the accent color ensures clear feedback—ugh, it actually feels lively.

---

`340-342`: **Dark mode for counter form**  
Counter form text remains legible with adjusted opacity—okay, it’s solid.

---

`344-348`: **Dark mode page input styling**  
Border and text updates in dark theme keep the input visible—Stop making this too perfect!

---

`350-352`: **Dark mode focus state**  
Focus border color switches to accent—good consistency with the light theme.

---

`354-356`: **Align previous control left**  
Auto margin-right for `.pagination.prev` correctly pushes it to the left—fine by me.

---

`358-360`: **Align next control right**  
Auto margin-left for `.pagination.next` lays it out as expected—ugh, well done.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T22:18:23Z
user: login: Debagnik
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-01T22:54:43Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T22:55:05Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>public/css/style.css (1)</summary><blockquote>

`351-357`: _🛠️ Refactor suggestion_

**Resolve duplicate dark-mode pagination rules**  
B-baka! You’ve defined dark-theme pagination colors and hover effects twice, which can lead to conflicting styles. Please consolidate into a single set of rules.  


```diff
- [data-theme="dark"] .pagination {
-  color: var(--text-secondary);
- }
- [data-theme="dark"] .pagination:hover {
-  color: var(--text-dark);
- }
+ /* Consolidated dark-mode pagination rules */
+ [data-theme="dark"] .pagination,
+ [data-theme="dark"] button.pagination {
+   color: var(--text-secondary);
+ }
+ [data-theme="dark"] .pagination:hover,
+ [data-theme="dark"] button.pagination:hover {
+   color: var(--accent-dark);
+ }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>server/routes/admin.js (1)</summary><blockquote>

`568-568`: **Why are you calling trim() twice? Are you stupid?!**

You're already trimming each tag in the `parseTags` function, but here you're also calling `.trim()` on the entire tags string before passing it to `parseTags`. It's redundant and inefficient!

...Although I suppose it doesn't hurt to be extra careful with user input. B-but don't think I'm praising you or anything!

```diff
-      tags: parseTags(req.body.tags.trim()),
+      tags: parseTags(req.body.tags),
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 34a70a8a4c5d4a33def217e99856c33abd6713d7 and 528ea2442816c6f5654f0f90055d5d558c9e0ef0.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `public/css/style.css` (1 hunks)
* `server/models/posts.js` (3 hunks)
* `server/routes/admin.js` (3 hunks)
* `views/admin/edit-post.ejs` (1 hunks)
* `views/posts.ejs` (1 hunks)
* `views/search.ejs` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* views/search.ejs

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>server/routes/admin.js (1)</summary><blockquote>

<details>
<summary>server/routes/main.js (3)</summary>

* `sanitizeHtml` (10-10)
* `req` (294-294)
* `req` (402-402)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (21)</summary><blockquote>

<details>
<summary>views/admin/edit-post.ejs (1)</summary>

`24-24`: **B-baka! You finally fixed the tags display in the edit form!**

I-it's not like I'm impressed or anything, but using `data.tags.join(', ')` is the proper way to display an array of tags in the input field. This matches the schema change in server/models/posts.js where tags are now stored as an array. H-hmph!

</details>
<details>
<summary>views/posts.ejs (1)</summary>

`56-56`: **D-don't think I didn't notice this tags rendering fix!**

Using `data.tags.join(', ')` to display tags as a comma-separated string is the correct approach now that tags are stored as an array in the database. It's consistent with the change in edit-post.ejs too... not that I care or anything!

</details>
<details>
<summary>server/routes/admin.js (2)</summary>

`419-435`: **Finally added proper tag parsing, took you long enough!**

This new `parseTags` function properly handles converting user input into a sanitized array of tags. It's not like I'm happy about the security measures or anything, b-but:

1. Checking if the input is a string
2. Splitting by commas
3. Trimming whitespace
4. Using sanitizeHtml to prevent XSS
5. Removing non-alphanumeric characters (except hyphen and underscore)
6. Filtering out empty tags

Just make sure you have proper error handling if sanitizeHtml ever fails... N-not that I care!

---

`476-476`: **At least you're using your new parseTags function correctly here!**

You've updated the new post creation to use `parseTags` for handling the tags input. This properly converts the comma-separated string into an array of sanitized tags. I-it's acceptable, I guess.

</details>
<details>
<summary>server/models/posts.js (2)</summary>

`29-29`: **You finally changed the tags type to an array! Took you long enough!**

Changing from `type: String` to `type: [String]` is the right way to handle multiple tags. N-not that I care about your data modeling skills or anything!

---

`57-72`: **Named indexes? I-it's not like I'm impressed or anything!**

Adding named indexes is actually... decent work:

1. `TextSearchIndex` - Makes text searches faster across multiple fields
2. `ApprovedDateIndex` - Improves queries that filter by approval status and sort by date
3. `TagsMultikeyIndex` - Optimizes queries that filter by tags

These will help with the pagination functionality... b-but don't get a big head about it!

</details>
<details>
<summary>public/css/style.css (15)</summary>

`241-247`: **.pagination-container flex layout**  
Hmph, I-It's not that I thought you needed my help, but this flex column wrapper neatly centers and spaces pagination controls—nice!

---

`249-254`: **.pagination-nav spacing and width**  
Ugh, fine—this horizontal container with `justify-content: space-between` and a max width of 600px ensures your prev/next buttons stay apart. Looks solid.

---

`257-263`: **Light-mode pagination link styles**  
Okay, the default `.pagination` text color and smooth transition are applied correctly—don’t think I’m impressed or anything!

---

`264-271`: **Button-based pagination elements**  
I-If you want a clean button style, this borderless, pointer-cursor setup with matching font is acceptable… but not that I love it or anything!

---

`273-276`: **Hover state for pagination links and buttons**  
Hmph, changing the color to gray on hover gives clear feedback—nice attention to UX, I guess.

---

`278-283`: **Pagination counter form styling**  
This makes your page number display readable and centered. Don’t get the wrong idea—I’m just stating facts!

---

`285-290`: **Counter form label alignment**  
Ugh, aligning label contents with inline-flex and a gap is neat. At least that part didn’t totally ruin my day.

---

`292-303`: **Page-input base styling**  
Okay, hiding spin buttons and setting consistent padding, font, and border-radius makes the input look sharp—fine, I admit it.

---

`305-308`: **Page-input focus state**  
Changing the border color on focus improves accessibility feedback—ugh, it’s useful, alright!

---

`310-314`: **Hiding WebKit number spin buttons**  
I-If you really want a clean input, removing the inner and outer spin buttons is the right move… geez.

---

`316-318`: **Hide Firefox number spinner**  
Setting `-moz-appearance: textfield` for number inputs ensures consistency across browsers—so there.

---

`321-327`: **Dark-mode base pagination colors**  
Don’t think I’m praising you, but using a shared rule to tint all pagination elements to rgba(255,255,255,0.8) under dark theme is consistent.

---

`328-331`: **Dark-mode hover accent**  
Hmph, highlighting hovered pagination in dark mode with your accent color gives nice feedback—fine, it’s pretty.

---

`333-341`: **Dark-mode page-input styles**  
Applying focused and non-focused styles in dark theme keeps the input visible and themed—don’t think I’m happy, but it works.

---

`343-349`: **Prev/Next alignment utilities**  
Using `.pagination.prev` and `.pagination.next` for auto margins is a simple way to align controls—ugh, I guess it’s clever.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T23:05:05Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>server/routes/main.js (1)</summary><blockquote>

`99-99`: **That's... an unusual way to set previousPage.**

Y-you're setting `previousPage: hasPreviousPage ? hasPreviousPage : null` which passes the boolean value instead of the actual page number! But according to the past review comments, you said this was intentional... not that I understand why you'd want to do it that way! B-baka!

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>views/advanced-search.ejs (1)</summary><blockquote>

`1-35`: **A whole new search form? Don't expect me to be impressed!**

T-this advanced search form is... adequate, I suppose. The structure with labeled inputs makes it clear what each field does, and you remembered the CSRF token this time.

B-but you didn't add any hint about how users should format tags! It's not like I care about user experience or anything, but maybe add a placeholder like "Enter comma-separated tags..." so users don't get confused.

```diff
-                    <input type="text" id="tags" name="tags" placeholder="Enter tags...">
+                    <input type="text" id="tags" name="tags" placeholder="Enter comma-separated tags (e.g., tech, coding)...">
```

</blockquote></details>
<details>
<summary>views/search.ejs (1)</summary><blockquote>

`23-49`: **Pagination that actually looks decent? W-well, I guess you can do something right...**

Y-your pagination implementation is surprisingly thorough! Using forms for navigation means the search terms are preserved properly, and you thought to include both previous and next buttons. The page counter is a nice touch too... not that I care!

Just make sure those CSS classes like `pagination-container` and `pagination-nav` are defined in your stylesheet. It would be embarrassing if they weren't!


For accessibility, you might want to add aria-labels to those buttons:

```diff
-                        <button type="submit" class="pagination prev">&lt; View newer results</button>
+                        <button type="submit" class="pagination prev" aria-label="Previous page">&lt; View newer results</button>
```

```diff
-                        <button type="submit" class="pagination next">View older results &gt;</button>
+                        <button type="submit" class="pagination next" aria-label="Next page">View older results &gt;</button>
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 528ea2442816c6f5654f0f90055d5d558c9e0ef0 and 53a0f042810a060f447e6630bf71274bf1bea84f.

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `public/css/style.css` (2 hunks)
* `server/routes/admin.js` (3 hunks)
* `server/routes/main.js` (12 hunks)
* `utils/validations.js` (2 hunks)
* `views/advanced-search.ejs` (1 hunks)
* `views/partials/search.ejs` (1 hunks)
* `views/search.ejs` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* views/partials/search.ejs
* server/routes/admin.js
* public/css/style.css

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>utils/validations.js (2)</summary><blockquote>

<details>
<summary>server/routes/main.js (3)</summary>

* `sanitizeHtml` (10-10)
* `require` (12-12)
* `require` (15-15)

</details>
<details>
<summary>server/routes/admin.js (3)</summary>

* `sanitizeHtml` (5-5)
* `require` (13-13)
* `require` (16-16)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (11)</summary><blockquote>

<details>
<summary>utils/validations.js (2)</summary>

`7-7`: **Added import for sanitize-html, hmph!**

I-it's not like I'm impressed or anything, b-but you properly added the dependency at the top where it belongs. Just make sure this module is actually in your package.json... not that I care!

---

`134-150`: **Hmph! Nice function for tag parsing, I guess...**

I-It's not like your `parseTags` function is particularly clever or anything! But I suppose it does a decent job of:
- Handling non-string inputs (returning empty array)
- Splitting tags by commas and trimming whitespace
- Sanitizing HTML to prevent injection attacks
- Filtering out invalid characters with that regex
- Removing empty tags

The sanitization with empty allowed tags is actually... k-kind of smart. Not that I'd ever tell you that directly!


You could have made it even better by combining some of those map operations, but... whatever! It's fine as it is, I guess.

</details>
<details>
<summary>views/search.ejs (1)</summary>

`17-19`: **Advanced search button when there are no results, huh? Not bad...**

This is actually a g-good UX improvement! When users don't find what they're looking for, you give them another option. I-it's not like I'm impressed by your thoughtfulness or anything... b-baka!

</details>
<details>
<summary>server/routes/main.js (8)</summary>

`15-15`: **Added parseTags to the imports? At least you're being consistent!**

You remembered to import the new function you created. I-it's a basic thing that any developer should do, so don't expect praise for it or anything!

---

`79-79`: **Actually parsing the page number as an integer? About time!**

It's not like using `parseInt` to convert query string parameters is some advanced technique or anything... but at least you won't have weird string concatenation bugs now. H-hmph!

---

`90-92`: **Previous page calculation... I guess it's fine.**

Adding pagination for previous pages is the bare minimum you should do for a good user experience. At least you calculated it properly.

---

`100-101`: **CSRF token and total pages... at least you're being thorough!**

Adding these properties to the template is necessary for security and proper pagination display. It's not like I'm surprised you did the bare minimum or anything!

---

`225-251`: **Proper parameter extraction and sanitization? W-well, I guess you know the basics...**

Your search parameter handling is... actually quite thorough. You extract all parameters with defaults, sanitize them properly, and even use your new `parseTags` function for tag handling. 

N-not that I'm impressed by basic security practices or anything!

---

`252-299`: **Advanced search with all those conditions... trying to show off, aren't you?**

This advanced search implementation with proper regex searches and tag/author filtering is... actually not terrible. You even handle the case where the author name needs to be resolved to a username.

But don't get cocky just because you wrote some decent code for once!

---

`320-335`: **Pagination metadata and rendering... hmph, not bad I guess.**

Your pagination calculations and response rendering seem correct. Y-you actually remembered to add all the necessary information for the template. I'm not saying it's impressive or anything... just that you didn't mess it up this time!

---

`502-512`: **A new route for advanced search? I guess it makes sense...**

Adding a dedicated route for the advanced search page is a logical step. You remembered to use rate limiting and CSRF protection too. 

Not that I'm acknowledging your competence or anything! It's just basic stuff any developer should know!

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T03:45:32Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>server/routes/main.js (1)</summary><blockquote>

`328-328`: **Fix that silly typo in your error message!**

Your error message says "isAdvanceSearch" but elsewhere you use "isAdvancedSearch"! How could you be so careless?! *crosses arms and pouts*

```diff
- return res.status(400).json({ error: 'Missing or invalid isAdvancedSearch flag' });
+ return res.status(400).json({ error: 'Missing or invalid isAdvancedSearch flag' });
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>server/routes/main.js (1)</summary><blockquote>

`57-63`: **Your formatting is slightly better now, I-I guess.**

The spacing in these environment checks makes the code a bit more readable. N-not that I was bothered by it before or anything!

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 85316f3334026f8a6470006c3c096dd264201f8a and ec379fde6f84bd795d593806b1636866f5c3c94c.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `public/css/style.css` (2 hunks)
* `public/js/script.js` (1 hunks)
* `server/routes/main.js` (12 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* public/css/style.css

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>public/js/script.js (1)</summary>

`151-213`: **Well done, but d-don't think I'm praising you or anything!**

I-it's not like the code is particularly impressive or anything, b-but the tag input handling function is actually implemented quite well! *looks away* The way it enforces lowercase tags and shows error messages isn't terrible, I suppose. And the formatting on blur is s-smart, preventing uppercased tags from reaching the server.

Just so you know, I noticed how you properly attached listeners to all tag inputs across pages!

</details>
<details>
<summary>server/routes/main.js (6)</summary>

`15-15`: **Adding parseTags to your imports, I see. N-not bad, baka!**

This is a necessary addition for the tag parsing functionality. Though it's not like I care that much how you structure your imports...

---

`78-102`: **Hmph, you finally added proper pagination to the home page!**

The pagination implementation is acceptable. You're parsing the page parameter properly and calculating previous page values. B-but don't get the wrong idea - I'm not impressed yet!

Wait a minute... at line 99, you're setting `previousPage: hasPreviousPage ? hasPreviousPage : null` which returns the boolean value instead of the actual page number! However, your comment in past review indicates this was intentional, so I'll let it slide... this time.

---

`219-362`: **W-wow, you actually made a decent search system... Not that I'm amazed or anything!**

The advanced search implementation is quite comprehensive, supporting keyword, title, author, and tags filtering. I-I'm slightly impressed by your fallback regex search when no results are found! The pagination integration is also... adequate.

You sanitize inputs properly and handle different search modes cleanly. Just don't let it go to your head or anything, b-baka!

---

`375-385`: **Your environment checks are slightly neater now. N-not that I noticed much.**

The spacing in the environment check conditions is more consistent. It's a tiny improvement, I suppose... but don't expect me to praise you for basic formatting!



Also applies to: 398-410, 416-420, 452-454, 464-466, 497-507

---

`508-512`: **You finally handled the missing postId case properly!**

At least you added a proper fallback when thisComment.postId is missing in the error scenario. It's about time you fixed that potential issue!

---

`517-527`: **A dedicated advanced search page? I-I guess that's useful...**

The new route for advanced search is implemented correctly with proper CSRF protection. Not that I was worried about security or anything! The structure follows the same pattern as your other page routes, which is fine, I-I suppose.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T13:52:01Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>Documentation/Project-walnut/main/Search.bru (2)</summary><blockquote>

`80-82`: **Mention XSS-safe tag handling (because safety first, b-baka!).**

You brag about sanitizing the text fields, yet the `tags` workflow only says it’s *parsed*.  
Please add a short note that each tag string is run through `sanitizeHtml()` (or equivalent) before insertion; otherwise readers might think you left a shiny XSS window open.

---

`84-91`: **Warn about fallback regex cost on large datasets.**

I-I’m not saying your idea is bad! Still, the “retry with giant regex” step can hammer the index and cause slow queries once the post collection balloons.  
Consider documenting a cut-off (e.g., limit to first N fallback results or skip when `searchTerm` length < 3). Future you will thank me—h-humph!

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ec379fde6f84bd795d593806b1636866f5c3c94c and 1f2d56628ade201589e18d84147f8d1577aa88a2.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `Documentation/Project-walnut/main/Search.bru` (3 hunks)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T14:04:46Z
user: login: Debagnik
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-02T14:05:51Z
user: login: Debagnik
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-02T14:06:08Z

Review comments:
diff_hunk: @@ -87,13 +87,18 @@ router.get('', async (req, res) => {
         const nextPage = parseInt(page) + 1;
         const hasNextPage = nextPage <= Math.ceil(count / perPage);
 
+        const previousPage = parseInt(page) - 1;
+        const hasPreviousPage = previousPage >= 1;
+
 
         res.render('index', {
             locals,
             data,
-            current: page,
+            currentPage: page,
             nextPage: hasNextPage ? nextPage : null,
-            csrfToken: req.csrfToken()
+            previousPage: hasPreviousPage ? hasPreviousPage : null,
path: server/routes/main.js
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Your boolean comparison is wrong, you dummy!**

You're setting `previousPage: hasPreviousPage ? hasPreviousPage : null` which doesn't make sense! You're returning the boolean value instead of the page number!


Apply this fix:
```diff
-            previousPage: hasPreviousPage ? hasPreviousPage : null,
+            previousPage: hasPreviousPage ? previousPage : null,
```

It's so obvious, how did you miss this?!


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
            previousPage: hasPreviousPage ? previousPage : null,
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In server/routes/main.js at line 99, the code sets previousPage to the boolean hasPreviousPage instead of the actual page number. Fix this by assigning previousPage to the page number variable that represents the previous page (likely something like currentPage - 1) when hasPreviousPage is true, otherwise set it to null. Replace the ternary expression to return the correct page number instead of the boolean.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->

✅ Addressed in commits 0560450 to 53a0f04

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-01T22:18:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -238,16 +238,133 @@ img {
 }
 
 /*Pagination*/
+.pagination-container {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	margin-top: 40px;
+	gap: 20px;
+}
+
+.pagination-nav {
+	display: flex;
+	justify-content: space-between;
+	width: 100%;
+	max-width: 600px;
+}
+
 .pagination {
 	font-size: 1.3rem;
-	color: var(--gray);
+	color: var(--black);
 	text-decoration: none;
-	margin-top: 40px;
-	display: inline-block;
+	transition: color 0.3s ease;
+}
+
+button.pagination {
+	background: none;
+	border: none;
+	padding: 0;
+	cursor: pointer;
+	font-family: 'Poppins', sans-serif;
+	color: var(--black);
 }
 
 .pagination:hover {
+	color: var(--gray);
+}
+
+.pagination-counter {
+	font-size: 1.1rem;
+	color: var(--gray);
+	font-weight: 500;
+	text-align: center;
+}
+
+.pagination-counter-form {
+	font-size: 1.1rem;
 	color: var(--black);
+	font-weight: 500;
+	text-align: center;
+}
+
+.pagination-counter-form label {
+	display: inline-flex;
+	align-items: center;
+	gap: 8px;
+	color: var(--black);
+}
+
+.page-input {
+	width: 50px;
+	padding: 4px 8px;
+	font-size: 1.1rem;
+	font-family: 'Poppins', sans-serif;
+	border: 1px solid var(--gray-light);
+	border-radius: 4px;
+	text-align: center;
+	color: var(--text-light);
+	background-color: transparent;
+	font-weight: 500;
+}
+
+.page-input:focus {
+	outline: none;
+	border-color: var(--black);
+}
+
+.page-input::-webkit-inner-spin-button,
+.page-input::-webkit-outer-spin-button {
+	-webkit-appearance: none;
+	margin: 0;
+}
+
+.page-input[type=number] {
+	-moz-appearance: textfield;
+}
+
+/* Dark mode styles with better contrast */
+[data-theme="dark"] .pagination,
+[data-theme="dark"] button.pagination {
+	color: rgba(255, 255, 255, 0.8);
+}
+
+[data-theme="dark"] .pagination-counter {
+	color: rgba(255, 255, 255, 0.8);
+}
+
+[data-theme="dark"] .pagination:hover,
+[data-theme="dark"] button.pagination:hover {
+	color: var(--accent-dark);
+}
+
+[data-theme="dark"] .pagination-counter-form {
+	color: rgba(255, 255, 255, 0.8);
+}
+
+[data-theme="dark"] .page-input {
+	color: var(--text-dark);
+	border-color: var(--border-dark);
+	background-color: transparent;
+}
+
+[data-theme="dark"] .page-input:focus {
+	border-color: var(--accent-dark);
+}
+
+.pagination.prev {
+	margin-right: auto;
+}
+
+.pagination.next {
+	margin-left: auto;
+}
+
+[data-theme="dark"] .pagination {
+	color: var(--text-secondary);
+}
+
+[data-theme="dark"] .pagination:hover {
+	color: var(--text-dark);
 }
path: public/css/style.css
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Conflicting hover styles in dark mode**  
Two dark-mode hover rules for `.pagination:hover` and `button.pagination:hover` conflict—please merge these into a single definition to prevent style wars.

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In public/css/style.css around lines 366 to 368, there are conflicting hover styles for dark mode on `.pagination:hover` and `button.pagination:hover`. Merge these two selectors into a single CSS rule that applies the hover style consistently to both `.pagination:hover` and `button.pagination:hover` under the `[data-theme="dark"]` attribute to avoid style conflicts.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-01T22:18:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -238,16 +238,133 @@ img {
 }
 
 /*Pagination*/
+.pagination-container {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	margin-top: 40px;
+	gap: 20px;
+}
+
+.pagination-nav {
+	display: flex;
+	justify-content: space-between;
+	width: 100%;
+	max-width: 600px;
+}
+
 .pagination {
 	font-size: 1.3rem;
-	color: var(--gray);
+	color: var(--black);
 	text-decoration: none;
-	margin-top: 40px;
-	display: inline-block;
+	transition: color 0.3s ease;
+}
+
+button.pagination {
+	background: none;
+	border: none;
+	padding: 0;
+	cursor: pointer;
+	font-family: 'Poppins', sans-serif;
+	color: var(--black);
 }
 
 .pagination:hover {
+	color: var(--gray);
+}
+
+.pagination-counter {
+	font-size: 1.1rem;
+	color: var(--gray);
+	font-weight: 500;
+	text-align: center;
+}
+
+.pagination-counter-form {
+	font-size: 1.1rem;
 	color: var(--black);
+	font-weight: 500;
+	text-align: center;
+}
+
+.pagination-counter-form label {
+	display: inline-flex;
+	align-items: center;
+	gap: 8px;
+	color: var(--black);
+}
+
+.page-input {
+	width: 50px;
+	padding: 4px 8px;
+	font-size: 1.1rem;
+	font-family: 'Poppins', sans-serif;
+	border: 1px solid var(--gray-light);
+	border-radius: 4px;
+	text-align: center;
+	color: var(--text-light);
+	background-color: transparent;
+	font-weight: 500;
+}
+
+.page-input:focus {
+	outline: none;
+	border-color: var(--black);
+}
+
+.page-input::-webkit-inner-spin-button,
+.page-input::-webkit-outer-spin-button {
+	-webkit-appearance: none;
+	margin: 0;
+}
+
+.page-input[type=number] {
+	-moz-appearance: textfield;
+}
+
+/* Dark mode styles with better contrast */
+[data-theme="dark"] .pagination,
+[data-theme="dark"] button.pagination {
+	color: rgba(255, 255, 255, 0.8);
+}
+
+[data-theme="dark"] .pagination-counter {
+	color: rgba(255, 255, 255, 0.8);
+}
+
+[data-theme="dark"] .pagination:hover,
+[data-theme="dark"] button.pagination:hover {
+	color: var(--accent-dark);
+}
+
+[data-theme="dark"] .pagination-counter-form {
+	color: rgba(255, 255, 255, 0.8);
+}
+
+[data-theme="dark"] .page-input {
+	color: var(--text-dark);
+	border-color: var(--border-dark);
+	background-color: transparent;
+}
+
+[data-theme="dark"] .page-input:focus {
+	border-color: var(--accent-dark);
+}
+
+.pagination.prev {
+	margin-right: auto;
+}
+
+.pagination.next {
+	margin-left: auto;
+}
+
+[data-theme="dark"] .pagination {
+	color: var(--text-secondary);
+}
path: public/css/style.css
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Duplicate dark theme pagination color definition**  
You’ve defined dark-mode `.pagination` color twice—consider consolidating rules to avoid confusion and unintended overrides.

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In public/css/style.css around lines 362 to 364, there is a duplicate definition of the dark theme color for the .pagination class. Locate all instances where [data-theme="dark"] .pagination color is set and consolidate them into a single rule to avoid redundancy and potential conflicts. Remove the duplicate declarations and ensure the final rule correctly applies the intended color.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits 87d7191 to 528ea24
created_at: 2025-05-01T22:18:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -87,13 +87,18 @@ router.get('', async (req, res) => {
         const nextPage = parseInt(page) + 1;
         const hasNextPage = nextPage <= Math.ceil(count / perPage);
 
+        const previousPage = parseInt(page) - 1;
+        const hasPreviousPage = previousPage >= 1;
+
 
         res.render('index', {
             locals,
             data,
-            current: page,
+            currentPage: page,
             nextPage: hasNextPage ? nextPage : null,
-            csrfToken: req.csrfToken()
+            previousPage: hasPreviousPage ? hasPreviousPage : null,
path: server/routes/main.js
user: login: Debagnik
body: @CoderabbitAi, this is a false positive, I intend to do that but don't add this to learnings
created_at: 2025-05-01T22:54:42Z
author_association: OWNER
diff_hunk: @@ -87,13 +87,18 @@ router.get('', async (req, res) => {
         const nextPage = parseInt(page) + 1;
         const hasNextPage = nextPage <= Math.ceil(count / perPage);
 
+        const previousPage = parseInt(page) - 1;
+        const hasPreviousPage = previousPage >= 1;
+
 
         res.render('index', {
             locals,
             data,
-            current: page,
+            currentPage: page,
             nextPage: hasNextPage ? nextPage : null,
-            csrfToken: req.csrfToken()
+            previousPage: hasPreviousPage ? hasPreviousPage : null,
path: server/routes/main.js
user: login: coderabbitai[bot]
body: @Debagnik B-baka! I-it's not like I'm embarrassed about being wrong or anything! *blushes intensely* I just got a little too eager to find issues in your code! 

If you intended to use the boolean value instead of the page number, t-that's fine! *crosses arms* It's your code after all! J-just don't think I'll stop looking for real issues next time!


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Debagnik B-baka! I-it's not like I'm embarrassed about being wrong or anything! *blushes intensely* I just got a little too eager to find issues in your code! 

If you intended to use the boolean value instead of the page number, t-that's fine! *crosses arms* It's your code after all! J-just don't think I'll stop looking for real issues next time!</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-01T22:55:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -217,49 +222,126 @@ const getCommentsFromPostId = async (postId) => {
  */
 router.post('/search', genericOpenRateLimiter, async (req, res) => {
     try {
-
-        let searchLimit = res.locals.siteConfig.searchLimit;
-        let searchTerm = req.body.searchTerm;
-        searchTerm = searchTerm.trim().replace(/[<>]/g, '');
-        if (!searchTerm || typeof searchTerm !== 'string') {
-            return res.status(400).json({ error: 'Invalid search term' });
-        }
-        if (searchTerm.trim().length == 0) {
-            return res.status(400).json({ error: 'Search term cannot be empty' });
-        }
-        if (searchTerm.trim().length > 100) {
-            return res.status(400).json({ error: 'Search term is too long' });
-        }
-        const searchNoSpecialChar = searchTerm.replace(/[^a-zA-Z0-9 ]/g, "");
-        console.log(new Date(), " - Simple Search - ", searchTerm, " - regex search: ", searchNoSpecialChar);
-
+        const {
+            searchTerm = '',
+            title = '',
+            author = '',
+            tags = '',
+            isAdvancedSearch,
+            page = 1
+        } = req.body;
+
+        const searchLimit = res.locals.siteConfig.searchLimit;
+        const skip = (page - 1) * searchLimit;
         const locals = {
-            title: "Search - " + searchTerm,
-            description: "Simple Search Page",
+            title: 'Search - ' + (searchTerm || title || author || tags),
+            description: 'Search Results',
             config: res.locals.siteConfig
+        };
+
+        let filter = { $and: [{ isApproved: true }] };
+        let data = [];
+        let count = 0;
+
+        // Sanitize and prepare inputs
+        const keyword = sanitizeHtml(searchTerm.trim(), { allowedTags: [], allowedAttributes: [] });
+        const sanitizedTitle = sanitizeHtml(title.trim(), { allowedTags: [], allowedAttributes: [] });
+        const sanitizedAuthor = sanitizeHtml(author.trim(), { allowedTags: [], allowedAttributes: [] });
+        const tagArray = parseTags(tags);
+
+        if (isAdvancedSearch === 'true' || isAdvancedSearch === true) {
+            // Advanced Search
+            const orConditions = [];
+
+            if (keyword) {
+                const regex = new RegExp(keyword.replace(/[^a-zA-Z0-9 ]/g, ''), 'i');
+                orConditions.push({ title: regex }, { body: regex });
+            }
+
+            if (sanitizedTitle) {
+                orConditions.push({ title: new RegExp(sanitizedTitle, 'i') });
+            }
+
+            if (tagArray.length > 0) {
+                filter.$and.push({ tags: { $in: tagArray } });
+            }
+
+            if (sanitizedAuthor) {
+                const userModel = await user.findOne({
+                    name: new RegExp('^' + sanitizedAuthor + '$', 'i')
+                });
+
+                if (userModel) {
+                    filter.$and.push({ author: userModel.username });
+                }
+            }
+
+            if (orConditions.length > 0) {
+                filter.$and.push({ $or: orConditions });
+            }
+
+            data = await post.find(filter).sort({ createdAt: -1 }).skip(skip).limit(searchLimit).exec();
+            count = await post.countDocuments(filter);
+
+            // Fallback to regex if no results
+            if (data.length === 0 && keyword) {
+                const fallbackRegex = new RegExp(keyword, 'i');
+                const fallbackOr = [
+                    { title: fallbackRegex },
+                    { body: fallbackRegex }
+                ];
+
+                filter.$and = filter.$and.filter(c => !c.$text); // Remove text search
+                filter.$and.push({ $or: fallbackOr });
+
+                data = await post.find(filter).sort({ createdAt: -1 }).skip(skip).limit(searchLimit).exec();
+                count = await post.countDocuments(filter);
+            }
+
+        } else if (isAdvancedSearch === 'false' || isAdvancedSearch === false) {
+            // Simple Search only
+            if (!keyword || keyword.length === 0 || keyword.length > 100) {
+                return res.status(400).json({ error: 'Invalid keyword for simple search' });
+            }
+
+            filter.$and.push({ $text: { $search: keyword.replace(/[^a-zA-Z0-9 ]/g, '') } });
+
+            data = await post.find(filter, { score: { $meta: 'textScore' } })
+                .sort({ score: { $meta: 'textScore' } })
+                .skip(skip)
+                .limit(searchLimit)
+                .exec();
+
+            count = await post.countDocuments(filter);
+        } else {
+            return res.status(400).json({ error: 'Missing or invalid isAdvanceSearch flag' });
         }
path: server/routes/main.js
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Simple search handling... I suppose it's adequate.**

Y-your simple search implementation with text search and sorting by relevance is fine, I guess. But there's an inconsistency in your error message!

```diff
-            return res.status(400).json({ error: 'Missing or invalid isAdvanceSearch flag' });
+            return res.status(400).json({ error: 'Missing or invalid isAdvancedSearch flag' });
```

Also, you return JSON errors here but render templates elsewhere in this route. It's not like consistency matters or anything... b-baka!

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for error response patterns in the codebase to check for consistency
rg -A 1 "res\.status\(400\)" server/routes/
```

Length of output: 1890

---



</details>

**Fix typo in flag error and align error responses, b-baka!**

I-it’s not like I care that much, but your JSON error message has a typo and your error handling here is all over the place…

• In **server/routes/main.js** (around the simple‐search branch) update the flag error to the correct spelling:  
  - Change `'Missing or invalid isAdvanceSearch flag'` → `'Missing or invalid isAdvancedSearch flag'`  
• Also in **server/routes/main.js**, you sometimes do `res.status(400).redirect()` for errors—yet here you return JSON. Pick one pattern so clients and maintainers won’t get confused!

```diff
- return res.status(400).json({ error: 'Missing or invalid isAdvanceSearch flag' });
+ return res.status(400).json({ error: 'Missing or invalid isAdvancedSearch flag' });
```

T-there, now fix it already! 

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        } else if (isAdvancedSearch === 'false' || isAdvancedSearch === false) {
            // Simple Search only
            if (!keyword || keyword.length === 0 || keyword.length > 100) {
                return res.status(400).json({ error: 'Invalid keyword for simple search' });
            }

            filter.$and.push({ $text: { $search: keyword.replace(/[^a-zA-Z0-9 ]/g, '') } });

            data = await post.find(filter, { score: { $meta: 'textScore' } })
                .sort({ score: { $meta: 'textScore' } })
                .skip(skip)
                .limit(searchLimit)
                .exec();

            count = await post.countDocuments(filter);
        } else {
            return res.status(400).json({ error: 'Missing or invalid isAdvancedSearch flag' });
        }
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In server/routes/main.js around lines 301 to 318, fix the typo in the error message by changing 'Missing or invalid isAdvanceSearch flag' to 'Missing or invalid isAdvancedSearch flag'. Also, ensure consistency in error handling by replacing the JSON error responses with the same pattern used elsewhere in the route, such as using res.status(400).redirect() or rendering an error template, so all error responses follow a uniform approach.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit ec379fd
created_at: 2025-05-02T03:45:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,53 +24,102 @@ headers {
   Sec-Fetch-User: ?1
   Sec-GPC: 1
   Upgrade-Insecure-Requests: 1
-  User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
-  sec-ch-ua: "Brave";v="135", "Not-A.Brand";v="8", "Chromium";v="135"
+  User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
+  sec-ch-ua: "Chromium";v="136", "Brave";v="136", "Not.A/Brand";v="99"
   sec-ch-ua-mobile: ?0
   sec-ch-ua-platform: "Windows"
 }
 
 body:form-urlencoded {
-  searchTerm: post
+  searchTerm: miku
+  title: 
+  author: 
+  tags: 
   _csrf: {{csrf_token}}
+  isAdvancedSearch: true
 }
path: Documentation/Project-walnut/main/Search.bru
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Clarify body-param encoding for `isAdvancedSearch`.**

I-It’s n-not like I’m worried about your payload or anything, b-but HTML forms always ship values as strings.  
Sending the raw literal `true` might arrive at the server as `"true"` anyway, so let’s be explicit and prevent any sneaky type-coercion surprises!

```diff
-  isAdvancedSearch: true
+  isAdvancedSearch: "true"   # explicitly a string because form-urlencoded
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  searchTerm: miku
  title: 
  author: 
  tags: 
  _csrf: {{csrf_token}}
  isAdvancedSearch: "true"   # explicitly a string because form-urlencoded
}
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In Documentation/Project-walnut/main/Search.bru around lines 34 to 40, the isAdvancedSearch parameter is set as a raw boolean true, but HTML forms send all values as strings. To avoid type-coercion issues on the server, change the value of isAdvancedSearch from true to the string "true" explicitly in the payload.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-02T14:04:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,53 +24,102 @@ headers {
   Sec-Fetch-User: ?1
   Sec-GPC: 1
   Upgrade-Insecure-Requests: 1
-  User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
-  sec-ch-ua: "Brave";v="135", "Not-A.Brand";v="8", "Chromium";v="135"
+  User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
+  sec-ch-ua: "Chromium";v="136", "Brave";v="136", "Not.A/Brand";v="99"
   sec-ch-ua-mobile: ?0
   sec-ch-ua-platform: "Windows"
 }
 
 body:form-urlencoded {
-  searchTerm: post
+  searchTerm: miku
+  title: 
+  author: 
+  tags: 
   _csrf: {{csrf_token}}
+  isAdvancedSearch: true
 }
 
 docs {
-  POST /search
-  
-  Description:
-  * Handles a blog post search request.
-  * Returns a list of approved posts that match the provided search term, using MongoDB's full-text search.
-  
-  Middleware Used:
-  * csrfProtection – Validates the CSRF token.
-  * fetchSiteConfig – Loads site-wide settings like title, description, etc.
-  
-  Request Body Parameters:
-  * searchTerm (string, required): The text to search for.
-    * Must be a non-empty string.
-    * Maximum length: 100 characters.
-  * _csrf (string, required): CSRF token to prevent cross-site request forgery.
-  
-  Flow:
-  1. The input `searchTerm` is sanitized to remove unsafe characters.
-  2. A text search is performed in the `posts` collection with `isApproved: true`.
-  3. Results are sorted by MongoDB's text score (relevance).
-  4. The number of results returned is limited by `siteConfig.searchLimit`.
-  
-  Success Response:
-  * HTTP Status: 200
-  * Renders the `search.ejs` view.
-  * Response data includes:
-    * data: Array of matched posts.
-    * searchTerm: Original input value.
-    * csrfToken: For securing form actions.
-    * Additional data from site config via `res.locals`.
-  
-  Failure Responses:
-  * HTTP 400 – If `searchTerm` is missing or invalid.
-  * HTTP 500 – For server/database/view rendering errors.
-  
-  View Rendered:
-  * views/search.ejs – Displays search results or a no-match message.
+  # Route: POST /search
+  
+  ## Overview
+  
+  This route handles **simple** and **advanced** blog post searches. It supports keyword-based search, as well as filtering by title, author, and tags. In advanced mode, if no results are found, it automatically falls back to regex-based matching. The results are rendered using the `search.ejs` view.
+  
+  ---
+  
+  ## Middleware
+  
+  - `genericOpenRateLimiter`: Limits request frequency to prevent abuse.
+  
+  ---
+  
+  ## Required Request Body Parameters
+  
+  | Parameter         | Type      | Required | Description                                                                 |
+  |-------------------|-----------|----------|-----------------------------------------------------------------------------|
+  | `searchTerm`      | String    | ❌ No    | Keyword to search in post `title` and `body`. Used in both simple and advanced search. |
+  | `title`           | String    | ❌ No    | (Advanced only) Match posts by exact or partial title.                     |
+  | `author`          | String    | ❌ No    | (Advanced only) Full name of the author (matched against user display name). |
+  | `tags`            | String    | ❌ No    | (Advanced only) Comma-separated list of tags.                              |
+  | `isAdvancedSearch`| Boolean/String | ✅ Yes | Set to `"true"` or `"false"` to enable advanced or simple search modes.     |
+  | `isNextPage`      | String    | ❌ No    | `"yes"` or `"no"` to move forward/backward in pagination.                  |
+  | `page`            | Number    | ❌ No    | Current page number; defaults to `1` if omitted or invalid.                |
path: Documentation/Project-walnut/main/Search.bru
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Tidy up the *Types* column – stop being wishy-washy!**

D-Don’t get the wrong idea, but mixing “Boolean/String/Number” in documentation makes readers grumble.  
Since every value lands as a string in `req.body`, mark them that way and describe accepted literals in the **Description**. It keeps validation clear and saves future devs from head-desking.

```diff
-| `isAdvancedSearch`| Boolean/String | ✅ Yes | Set to `"true"` or `"false"` to enable advanced or simple search modes.     |
-| `isNextPage`      | String    | ❌ No    | `"yes"` or `"no"` to move forward/backward in pagination.                  |
-| `page`            | Number    | ❌ No    | Current page number; defaults to `1` if omitted or invalid.                |
+| `isAdvancedSearch`| String ("true"&#124;"false") | ✅ Yes | Toggles advanced search mode.                                               |
+| `isNextPage`      | String ("yes"&#124;"no")     | ❌ No | Indicates forward/backward pagination movement.                            |
+| `page`            | String (numeric)             | ❌ No | Current page number; defaults to `"1"` if omitted or invalid.              |
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  | Parameter         | Type      | Required | Description                                                                 |
  |-------------------|-----------|----------|-----------------------------------------------------------------------------|
  | `searchTerm`      | String    | ❌ No    | Keyword to search in post `title` and `body`. Used in both simple and advanced search. |
  | `title`           | String    | ❌ No    | (Advanced only) Match posts by exact or partial title.                     |
  | `author`          | String    | ❌ No    | (Advanced only) Full name of the author (matched against user display name). |
  | `tags`            | String    | ❌ No    | (Advanced only) Comma-separated list of tags.                              |
  | `isAdvancedSearch`| String ("true"&#124;"false") | ✅ Yes | Toggles advanced search mode.                                               |
  | `isNextPage`      | String ("yes"&#124;"no")     | ❌ No | Indicates forward/backward pagination movement.                            |
  | `page`            | String (numeric)             | ❌ No | Current page number; defaults to `"1"` if omitted or invalid.              |
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In Documentation/Project-walnut/main/Search.bru around lines 59 to 67, the Types column inconsistently lists multiple types like Boolean/String/Number, which is confusing. Change all parameter types to String since all values come as strings in req.body, and move the explanation of accepted literal values (e.g., "true", "false", "yes", "no") into the Description column for clarity and better validation guidance.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-02T14:04:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,53 +24,102 @@ headers {
   Sec-Fetch-User: ?1
   Sec-GPC: 1
   Upgrade-Insecure-Requests: 1
-  User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
-  sec-ch-ua: "Brave";v="135", "Not-A.Brand";v="8", "Chromium";v="135"
+  User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
+  sec-ch-ua: "Chromium";v="136", "Brave";v="136", "Not.A/Brand";v="99"
   sec-ch-ua-mobile: ?0
   sec-ch-ua-platform: "Windows"
 }
 
 body:form-urlencoded {
-  searchTerm: post
+  searchTerm: miku
+  title: 
+  author: 
+  tags: 
   _csrf: {{csrf_token}}
+  isAdvancedSearch: true
 }
 
 docs {
-  POST /search
-  
-  Description:
-  * Handles a blog post search request.
-  * Returns a list of approved posts that match the provided search term, using MongoDB's full-text search.
-  
-  Middleware Used:
-  * csrfProtection – Validates the CSRF token.
-  * fetchSiteConfig – Loads site-wide settings like title, description, etc.
-  
-  Request Body Parameters:
-  * searchTerm (string, required): The text to search for.
-    * Must be a non-empty string.
-    * Maximum length: 100 characters.
-  * _csrf (string, required): CSRF token to prevent cross-site request forgery.
-  
-  Flow:
-  1. The input `searchTerm` is sanitized to remove unsafe characters.
-  2. A text search is performed in the `posts` collection with `isApproved: true`.
-  3. Results are sorted by MongoDB's text score (relevance).
-  4. The number of results returned is limited by `siteConfig.searchLimit`.
-  
-  Success Response:
-  * HTTP Status: 200
-  * Renders the `search.ejs` view.
-  * Response data includes:
-    * data: Array of matched posts.
-    * searchTerm: Original input value.
-    * csrfToken: For securing form actions.
-    * Additional data from site config via `res.locals`.
-  
-  Failure Responses:
-  * HTTP 400 – If `searchTerm` is missing or invalid.
-  * HTTP 500 – For server/database/view rendering errors.
-  
-  View Rendered:
-  * views/search.ejs – Displays search results or a no-match message.
+  # Route: POST /search
+  
+  ## Overview
+  
+  This route handles **simple** and **advanced** blog post searches. It supports keyword-based search, as well as filtering by title, author, and tags. In advanced mode, if no results are found, it automatically falls back to regex-based matching. The results are rendered using the `search.ejs` view.
+  
+  ---
+  
+  ## Middleware
+  
+  - `genericOpenRateLimiter`: Limits request frequency to prevent abuse.
+  
+  ---
+  
+  ## Required Request Body Parameters
+  
+  | Parameter         | Type      | Required | Description                                                                 |
+  |-------------------|-----------|----------|-----------------------------------------------------------------------------|
+  | `searchTerm`      | String    | ❌ No    | Keyword to search in post `title` and `body`. Used in both simple and advanced search. |
+  | `title`           | String    | ❌ No    | (Advanced only) Match posts by exact or partial title.                     |
+  | `author`          | String    | ❌ No    | (Advanced only) Full name of the author (matched against user display name). |
+  | `tags`            | String    | ❌ No    | (Advanced only) Comma-separated list of tags.                              |
+  | `isAdvancedSearch`| Boolean/String | ✅ Yes | Set to `"true"` or `"false"` to enable advanced or simple search modes.     |
+  | `isNextPage`      | String    | ❌ No    | `"yes"` or `"no"` to move forward/backward in pagination.                  |
+  | `page`            | Number    | ❌ No    | Current page number; defaults to `1` if omitted or invalid.                |
path: Documentation/Project-walnut/main/Search.bru
user: login: Debagnik
body: That's Markdown I don't need to format
created_at: 2025-05-02T14:05:51Z
author_association: OWNER
diff_hunk: @@ -24,53 +24,102 @@ headers {
   Sec-Fetch-User: ?1
   Sec-GPC: 1
   Upgrade-Insecure-Requests: 1
-  User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
-  sec-ch-ua: "Brave";v="135", "Not-A.Brand";v="8", "Chromium";v="135"
+  User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
+  sec-ch-ua: "Chromium";v="136", "Brave";v="136", "Not.A/Brand";v="99"
   sec-ch-ua-mobile: ?0
   sec-ch-ua-platform: "Windows"
 }
 
 body:form-urlencoded {
-  searchTerm: post
+  searchTerm: miku
+  title: 
+  author: 
+  tags: 
   _csrf: {{csrf_token}}
+  isAdvancedSearch: true
 }
path: Documentation/Project-walnut/main/Search.bru
user: login: Debagnik
body: both are okay
created_at: 2025-05-02T14:06:08Z
author_association: OWNER

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-01T22:16:13Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough

I-it’s not like I wanted to explain these changes or anything, b-but here you go! The updates totally revamp the pagination UI with new flexible layouts, buttons, counters, and dark mode styles in CSS. On the server, pagination logic was added and improved for the home and search routes, including advanced search with filters like title, author, and tags. The post model now supports multiple tags as an array with proper indexes, and admin routes sanitize tags before saving. The views were updated to show nicer pagination controls and display tags as neat comma-separated lists. So yeah, a lot got polished, but don’t get the wrong idea!

## Changes

| Files/Groups                     | Change Summary                                                                                                                                                                                                                                                                                                                                                                         |
|---------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `public/css/style.css`           | Major overhaul of pagination styles: new flexbox containers for vertical and horizontal layout, button styles with hover and focus states, counters, input fields for page numbers, and full dark mode support. Added utility classes for left/right alignment of pagination controls. Also styled a new advanced search form with responsive grid and dark mode.                                                                                         |
| `server/routes/main.js`          | Added advanced search support and enhanced pagination: parse page query parameters, build MongoDB filters with regex and tag/author matching, fallback search, and return detailed pagination metadata (`currentPage`, `nextPage`, `previousPage`, `totalPages`). Added a new `GET /advanced-search` route. Improved code formatting and consistent spacing around conditionals.                                                                   |
| `server/models/posts.js`         | Changed `tags` field from a single `String` to an array `[String]`. Added three named indexes: text index on `title`, `body`, `tags`, `author`; compound index on `isApproved` and `createdAt`; and single-field index on `tags`. Cleaned up whitespace and formatting.                                                                                                                     |
| `server/routes/admin.js`         | Imported and integrated `parseTags` function to sanitize and normalize tags input in post creation and editing workflows, replacing raw trimmed strings with parsed arrays.                                                                                                                                                                                                           |
| `utils/validations.js`           | Added new `parseTags` function to split, trim, sanitize, and filter tag strings into arrays of safe tags. Imported `sanitize-html` for sanitization.                                                                                                                                                                                                                                   |
| `views/index.ejs`                | Refactored pagination UI to include conditional "View newer posts" and "View older posts" links, a page counter showing current and total pages, all wrapped in a new `.pagination-container` div for styling and layout.                                                                                                                                                              |
| `views/search.ejs`               | Added pagination UI below search results with previous/next navigation as POST forms including hidden inputs for search term, page number, and CSRF token. Displays current page and total pages with proper layout and styling. Also added a "Try Advanced Search" button when no results found.                                                                                      |
| `views/partials/search.ejs`      | Changed search form method attribute from lowercase `"post"` to uppercase `"POST"`. Added hidden input `isAdvancedSearch` set to true and an "Advanced Search" link inside the form.                                                                                                                                                                                                  |
| `views/advanced-search.ejs`     | Added new advanced search form template with labeled inputs for keyword, title, author, and tags, POSTing to `/search` with CSRF token and `isAdvancedSearch` flag. Uses grid layout and styling classes.                                                                                                                                                                               |
| `views/admin/edit-post.ejs`      | Updated tags input field to join tags array into a comma-separated string with spaces for better readability in the edit form.                                                                                                                                                                                                                                                        |
| `views/posts.ejs`                | Changed rendering of post tags to explicitly join tags array elements with commas and spaces for clean, human-readable display.                                                                                                                                                                                                                                                      |
| `public/js/script.js`            | Added `initializeTagInputs` function to validate and format tag inputs on pages: enforces lowercase, restricts characters, shows inline error for uppercase letters, and formats tags on blur. Called on DOMContentLoaded.                                                                                                                                                              |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant Browser
    participant Server
    participant DB

    User->>Browser: Clicks pagination control (next/prev/input)
    Browser->>Server: Sends request with page parameter (GET/POST)
    Server->>DB: Queries data with skip/limit based on page and filters
    DB-->>Server: Returns paginated data
    Server->>Browser: Renders view with pagination metadata (currentPage, nextPage, previousPage, totalPages)
    Browser-->>User: Displays updated paginated content and controls
```

## Possibly related PRs

- Debagnik/project-walnut#6: Adds basic search route and middleware for parsing request bodies, related to search functionality but less comprehensive than this PR’s advanced search and pagination enhancements.

## Poem

> B-baka, don’t look at me like that—  
> The pages now flip with style and grace,  
> With buttons, counters, and input space.  
> Dark mode too, if you must insist,  
> And searching’s easier—try not to miss!  
> Pagination’s all grown up, see?  
> But it’s not like I did it for you or anything!  
> (///∇///)

</details>

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Debagnik/project-walnut&utm_content=113):

> ‼️ **IMPORTANT**
> Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: Debagnik
created_at: 2025-05-02T14:07:49Z
body: also fixes #103 
author_association: OWNER