Dynamic Blog with JavaScript

by Kuligaposten 2022-02-16

Building a fully dynamic blog without a backend

Building a Dynamic Blog with JavaScript: Pagination, Search, and Recent Posts

Building a fully dynamic blog without a backend can seem challenging, but with JavaScript, you can efficiently handle pagination, search, category filtering, and recent posts—all while keeping it fast and user-friendly.

In this guide, we'll create a blog system that:

  • Dynamically displays blog posts
  • Supports pagination using Bootstrap 5
  • Includes search and category filters
  • Shows recent posts (excluding the current post)
  • Updates the URL (?page=2&search=term) for better UX

Let's dive in!


1. Preparing the Blog Data

Instead of manually adding blog posts to the page, we'll store them in a JavaScript file (blogData.js) that looks like this:

window.data = [
  {
    title: "Useful links for devs",
    excerpt: "A collection of great resources for developers.",
    image: "assets/img/fundamentals.svg",
    pageLink: "article/useful-links-for-devs.html",
    date: "2024-12-12",
    categories: ["Development", "Resources"],
  },
  {
    title: "Spring in Kyoto",
    excerpt: "Exploring Kyoto during the beautiful spring season.",
    image: "assets/img/articles/kyoto.webp",
    pageLink: "article/spring-in-kyoto.html",
    date: "2024-09-28",
    categories: ["Travel", "Japan"],
  },
];

This allows us to dynamically generate the blog posts instead of hardcoding them in HTML.


2. Implementing Pagination, Search & Filtering

We’ll create a BlogPagination class to manage search, pagination, and category filtering.

JavaScript: BlogPagination.js

class BlogPagination {
  constructor(
    containerSelector,
    paginationSelector,
    searchSelector,
    filterSelector,
    perPage = 6,
  ) {
    this.container = document.querySelector(containerSelector);
    this.paginationContainer = document.querySelector(paginationSelector);
    this.searchInput = document.querySelector(searchSelector);
    this.filterSelect = document.querySelector(filterSelector);
    this.perPage = perPage;
    this.currentPage = 1;
    this.searchQuery = "";
    this.selectedCategory = "";
    this.data = window.data;
    this.filteredData = [...this.data];

    this.init();
  }

  init() {
    this.handleURLParams();
    this.renderPage(this.currentPage);
    this.setupPagination();

    if (this.searchInput) {
      this.searchInput.addEventListener("input", () => this.filterResults());
    }

    if (this.filterSelect) {
      this.populateCategories();
      this.filterSelect.addEventListener("change", () => this.filterResults());
    }
  }

  // Populate categories from data
  populateCategories() {
    const categories = [
      ...new Set(this.data.flatMap((post) => post.categories)),
    ];
    this.filterSelect.innerHTML =
      `<option value="">All Categories</option>` +
      categories
        .map((category) => `<option value="${category}">${category}</option>`)
        .join("");
  }

  renderPage(page = 1) {
    this.container.innerHTML = "";
    this.currentPage = page;

    const start = (page - 1) * this.perPage;
    const end = start + this.perPage;
    const currentPosts = this.filteredData.slice(start, end);

    currentPosts.forEach((post) => {
      const postElement = document.createElement("div");
      postElement.classList.add("col-md-4", "mb-4");
      postElement.innerHTML = `
        <div class="card h-100">
          <a class="link-light d-block text-decoration-none" href="${post.pageLink}">
            <img src="${post.image}" class="card-img-top" alt="${post.title}">
            <div class="card-body">
              <h5 class="card-title">${post.title}</h5>
              <p class="card-text">${post.excerpt}</p>
              <small class="text-muted d-block mt-2">${post.date}</small>
            </div>
          </a>
        </div>
      `;
      this.container.appendChild(postElement);
    });

    this.setupPagination();
  }

  // Filter results based on search and category
  filterResults(isUserAction = true) {
    this.searchQuery = this.searchInput.value.trim().toLowerCase();
    this.selectedCategory = this.filterSelect.value;

    this.filteredData = this.data.filter(
      (post) =>
        post.title.toLowerCase().includes(this.searchQuery) &&
        (!this.selectedCategory ||
          post.categories.includes(this.selectedCategory)),
    );

    if (isUserAction) {
      this.currentPage = 1;
    }

    this.renderPage(this.currentPage);
    this.setupPagination();
    this.updateURL();
  }

  setupPagination() {
    const totalPages = Math.ceil(this.filteredData.length / this.perPage);
    this.paginationContainer.innerHTML = "";

    if (totalPages <= 1) return;

    const ul = document.createElement("ul");
    ul.classList.add("pagination", "justify-content-center");

    for (let i = 1; i <= totalPages; i++) {
      const li = document.createElement("li");
      li.classList.add("page-item");
      if (i === this.currentPage) li.classList.add("active");

      li.innerHTML = `<a class="page-link" href="?page=${i}">${i}</a>`;
      li.addEventListener("click", (event) => {
        event.preventDefault();
        this.changePage(i);
      });
      ul.appendChild(li);
    }

    this.paginationContainer.appendChild(ul);
  }

  changePage(page) {
    this.currentPage = page;
    this.renderPage(page);
    this.updateURL();
  }

  updateURL() {
    const params = new URLSearchParams();
    if (this.searchQuery) params.set("search", this.searchQuery);
    if (this.selectedCategory) params.set("category", this.selectedCategory);
    if (this.currentPage > 1) params.set("page", this.currentPage);

    history.pushState({}, "", `?${params.toString()}`);
  }

  handleURLParams() {
    const params = new URLSearchParams(window.location.search);
    this.searchQuery = params.get("search") || "";
    this.selectedCategory = params.get("category") || "";
    this.currentPage = parseInt(params.get("page"), 10) || 1;

    this.filterResults(false);
  }
}

// Initialize the pagination system
document.addEventListener("DOMContentLoaded", () => {
  new BlogPagination(
    "#blog-container",
    "#pagination-controls",
    "#search-input",
    "#category-filter",
    6,
  );
});

3. Rendering Recent Posts

For individual post pages, exclude the current post from the recent posts list:

function renderRecentPosts() {
  const recentContainer = document.querySelector("#recent-post");
  if (!recentContainer) return;

  const currentPageUrl = window.location.pathname;
  const recentPosts = window.data
    .filter((post) => !currentPageUrl.includes(post.pageLink))
    .sort((a, b) => new Date(b.date) - new Date(a.date))
    .slice(0, 3);

  recentContainer.innerHTML = recentPosts
    .map(
      (post) => `
    <div class="col-md-4 mb-4">
      <div class="card h-100">
        <a class="link-light d-block text-decoration-none" href="${post.pageLink}">
          <img src="${post.image}" class="card-img-top" alt="${post.title}">
          <div class="card-body">
            <h5 class="card-title">${post.title}</h5>
            <p class="card-text">${post.excerpt}</p>
          </div>
        </a>
      </div>
    </div>
  `,
    )
    .join("");
}

document.addEventListener("DOMContentLoaded", renderRecentPosts);

Now Your Blog is Fully Dynamic!

✔ Pagination, search, and filtering
✔ Excludes current post from recent posts
✔ Works with Bootstrap 5

Ready to build your own blog?

Back to Home