DEV Community

Cover image for Building a Dynamic Client-Side Blog with Secutio & Bootstrap
Henrique Dias
Henrique Dias

Posted on • Edited on

Building a Dynamic Client-Side Blog with Secutio & Bootstrap

Traditionally, creating a blog involved server-side scripting languages like PHP. But what if you could achieve the same functionality with JavaScript and the power of client-side rendering?

This is where the Secutio framework steps in. Secutio empowers you to build dynamic content on the client-side, allowing you to create a full-fledged blog experience using JavaScript literal templates.

Imagine this: you can develop a web application that feels and functions like a traditional website, complete with individual post URLs that are SEO-friendly. This means search engines can easily discover and index your blog content, boosting your online visibility.

Bootstrap Blog Example

Get a Sneak Peek! Want to see the final project before diving into the details?

$ git clone https://github.com/mrhdias/secutio
$ cd secutio/examples/blog
$ python3 -m http.server -d public
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
Enter fullscreen mode Exit fullscreen mode

The Secutio project is relatively new, which presents a challenge in capturing developers' attention and gaining traction, given the multitude of frameworks that share the same goal of providing developers with tools to create websites quickly and easily. Despite this competitive scenario, I believe in the alignment of our project with these objectives, which is what propels us forward.

Leveraging Bootstrap for Enhanced User Experience

Bootstrap Blog Example

To effectively demonstrate Secutio's capabilities for rapid web development, we've chosen the popular Bootstrap framework as a foundation. Bootstrap provides a robust and user-friendly interface, making it an ideal choice for building the project's base.

We'll be building upon the concept of the Bootstrap Blog, but with a key innovation: each blog post will be displayed within a modal window when accessed from the main page. Additionally, each post will have a dedicated URL, ensuring optimal search engine visibility for improved discoverability.

Streamlining Website Development with Templates

Developing a dynamic website often involves the repetition of content, a challenge that can be effectively addressed by converting it into reusable templates or web components, enhancing modularity and efficiency.

The process begins with a thorough analysis of the website's structure to identify recurring sections, such as navigation menus, recent posts, or archive listings. Each identified section is then extracted and transformed into a template or component. These templates operate akin to WordPress widgets, facilitating seamless content management and modification.

In this particular case, the process resulted in the creation of twelve distinct templates, each tailored to specific functionalities. These templates were seamlessly integrated into the website's directory, enhancing its overall flexibility and ease of management.

blog

├── blog/
│   ├── public/
│   │   ├── templates/
│   │   │   ├── top-bar.html
│   │   │   ├── nav-bar.html
│   │   │   ├── featured-post.html
│   │   │   ├── featured-posts.html
│   │   │   ├── panel-posts.html
│   │   │   ├── list-posts.html
│   │   │   ├── panel-archives.html
│   │   │   ├── panel-elsewhere.html
│   │   │   ├── footer.html
│   │   │   ├── btn-scroll-to-top.html
│   │   │   ├── modal-post.html
│   │   │   ├── nav-modal.html
│   │   ├── assets/
│   │   │   ├── css/
│   │   │   │   ├── styles.css
│   │   ├── index.html
│   │   ├── post.html
│   │   ├── tasks.json
│   │   ├── tasks-post.json
└── server.py
Enter fullscreen mode Exit fullscreen mode

In the case of a SPA (Single Page Application), it would only be necessary to use an HTML page as the base of the project. However, it's important to consider that many SPA-based projects require alternative means of accessibility for robots to index the content for SEO purposes. Therefore, we will include an additional page called "post.html", which is designed to display the content of the post when accessed via URL.

Associate the Events of HTML Elements with Tasks

The working principle of the Secutio framework involves associating tasks with elements to generate dynamic content.

Define the Tasks

After cutting each piece from the 'index.html' page and placing them into templates, the next step is to assign tasks to each HTML element where content will be displayed. These tasks are then linked or associated with the corresponding template.

The HTML code provided below corresponds to the Blog home page of the Bootstrap example. In this project, a link to the JSON file containing tasks (<script data-tasktable type="application/json" src="tasks.json"></script>) has been added. Additionally, the address of the Secutio library has been included at the end of the HTML body element.

public/index.html

<!doctype html>
<html lang="en" data-bs-theme="auto">
<head>
    <!-- missing content -->
    <link rel="stylesheet"
        href="https://cdn.jsdelivr.net/npm/@docsearch/css@3">
    <link rel="stylesheet"
        href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
        crossorigin="anonymous">
    <!-- Custom styles for this example -->
    <link rel="stylesheet"
        href="https://fonts.googleapis.com/css?family=Playfair&#43;Display:700,900&amp;display=swap">
    <link rel="stylesheet"
        href="assets/css/styles.css" rel="stylesheet">
    <script data-tasktable type="application/json" src="tasks.json"></script>
</head>
<body>
    <!-- missing content -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
        crossorigin="anonymous"></script>
    <script type="text/javascript"
        src="https://cdn.jsdelivr.net/gh/mrhdias/secutio@master/dist/js/secutio.min.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

After the "body" HTML element, we will include the page header, which consists of a top bar and a menu featuring various categories.

public/index.html

<!-- missing content -->
<!-- header with topbar and menu with categories -->
<div class="container">
    <header id="top-bar"
        data-tasks="add-topbar"
        class="border-bottom lh-1 py-3"></header>

    <div class="py-1 mb-3 border-bottom">
        <nav class="navbar navbar-expand-lg">
            <div class="container-fluid">
                <button class="navbar-toggler"
                    type="button"
                    data-bs-toggle="collapse"
                    data-bs-target="#large-nav-bar"
                    aria-controls="large-nav-bar"
                    aria-expanded="false"
                    aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div id="large-nav-bar"
                    data-tasks="add-navbar"
                    class="collapse navbar-collapse justify-content-center">
                </div>
            </div>
        </nav>
    </div>
</div>
<!-- missing content -->
Enter fullscreen mode Exit fullscreen mode

The HTML header element and the element with the identifier "#large-nav-bar" both have an attribute called "data-tasks". This attribute contains the name of the task to be executed when a specified event occurs, as defined within the "tasks.json" file. In this particular case, the event responsible for triggering the task execution is named "init". It is automatically initiated upon loading of the HTML page and does not require any user interaction.

public/tasks.json

{
    "add-topbar": {
        "trigger": "init",
        "template": "templates/top-bar.html",
        "target": "#top-bar",
        "swap": "inner"
    },
    "add-navbar": {
        "trigger": "init",
        "src-data": "data/navbar.json",
        "template": "templates/nav-bar.html",
        "target": "#large-nav-bar",
        "swap": "inner"
    }
}
Enter fullscreen mode Exit fullscreen mode

All properties are documented in the Secutio documentation. As mentioned above, only tasks with the "init" event load automatically on page load. However, the "next" property allows loading additional content within the same event, as exemplified by the following code snippet where the scroll-to-top button is loaded.

public/tasks.json

{
    "add-footer": {
        "trigger": "init",
        "template": "templates/footer.html",
        "target": "footer",
        "swap": "inner",
        "before": "add-footer-classes",
        "next": "add-btn-scroll-to-top"
    },
    "add-footer-classes": {
        "selector": "footer",
        "add": {
            "class": "py-5 text-center text-body-secondary bg-body-tertiary"
        }
    },
    "add-btn-scroll-to-top": {
        "template": "templates/btn-scroll-to-top.html",
        "target": "footer",
        "swap": "after"
    }
}
Enter fullscreen mode Exit fullscreen mode

The "next" property within tasks allows chaining multiple tasks to execute in a specified order. Similarly, the "data-tasks" attribute can hold a space-separated list of tasks, enabling the execution of multiple tasks for an element, as long as these tasks are not intended for the same event as the element itself.

Three additional properties, "then", "before", and "after", allow you to add or remove classes and styles during event handling. The "then" property executes immediately after the event fires, making it perfect for initiating a spinner to indicate progress. The "before" property executes before updating ("swap") the content in the target element, while the "after" property executes after the update is complete.

The provided tasks snippet demonstrates the use of the "src-data" property. This property specifies the static data source, which can be a JSON or text file. When declared within a task, the data from this source is inherited by subsequent tasks triggered by some events. This inheritance continues unless a new "src-data" property is explicitly defined for a specific task or override by the attribute "attribute-src-data".

Data Sources

This blog project leverages static data, meaning the content needs to be prepared and stored in JSON files beforehand. While manual creation is an option, it's not recommended. A more efficient approach is to utilize a script that fetches the relevant data for each post directly from a database or backend.

In this project, we've opted for the scripted approach. The data for each post is stored in separate JSON files located within the "data/raw" directory, as shown in the following illustration:

public/data/raw

├── blog/
│   ├── bin/
│   │   │── metamorph.py
│   ├── public/
│   │   ├── data/
│   │   │   ├── raw/
│   │   │   │   ├── 1.json
│   │   │   │   ├── 2.json
│   │   │   │   ├── 3.json
│   │   ├── templates/
│   │   ├── assets/
│   │   ├── index.html
│   │   ├── post.html
│   │   ├── tasks.json
│   │   ├── tasks-post.json
└── server.py
Enter fullscreen mode Exit fullscreen mode

For example, we can create the following file with relevant information to populate the blog, either from a backend via REST API or even from the WordPress REST API. This approach can also be used to generate static multilingual content by adding additional directories with languages to the "data/raw" directory.

public/data/raw/1.json

{
    "id": 1,
    "category": "Naturale",
    "date_created": "2023-09-30T18:01:14",
    "title": "Curabitur volutpat",
    "excerpt": "Gravida ligula accumsan cursus. Donec consequat nibh id diam accumsan, eu suscipit ipsum condimentum.",
    "text": "Maecenas lacinia est hendrerit lobortis suscipit. Curabitur purus neque, dignissim vel lacinia eleifend, pellentesque imperdiet augue. Suspendisse vestibulum rutrum nibh, in facilisis urna pharetra at. Integer posuere nunc eu justo ultrices semper. Aliquam ut ullamcorper purus, et malesuada dui. Fusce nec lorem ultrices, malesuada ipsum a, feugiat dui. Donec tortor nulla, sagittis et erat a, finibus dapibus libero.",
    "image": {
        "src": "uploads/woman-tearing-mint-leaves.jpg",
        "name": "",
        "alt": ""
    },
    "permalink": "post.html?id=1",
    "author": "Aliquam Tincidunt",
    "views": 5,
    "featured": true
}
Enter fullscreen mode Exit fullscreen mode

Using a Python script named "metamorph.py", located in the repository's "bin" directory, various types of JSON files can be generated for consumption by the templates. The script can be analyzed in detail here.

After executing the "metamorph.py" script, the resulting data and file directory structure are as follows:

├── blog/
│   ├── bin/
│   │   │── metamorph.py
│   ├── public/
│   │   ├── data/
│   │   │   ├── archives/
│   │   │   │   ├── 2023-09/
│   │   │   │   │   ├── 1.json
│   │   │   │   ├── 2024-02/
│   │   │   │   ├── 2024-04/
│   │   │   ├── categories/
│   │   │   │   ├── all/
│   │   │   │   │   ├── 1.json
│   │   │   │   ├── imaginatio/
│   │   │   │   ├── mundos/
│   │   │   ├── posts/
│   │   │   │   ├── 1.json
│   │   │   │   ├── 2.json
│   │   │   │   ├── 3.json
│   │   │   ├── raw/
│   │   │   │   ├── 1.json
│   │   │   │   ├── 2.json
│   │   │   │   ├── 3.json
│   │   │   ├── archives.json
│   │   │   ├── elsewhere.json
│   │   │   ├── featured-post.json
│   │   │   ├── featured-posts.json
│   │   │   ├── most-read.json
│   │   │   ├── navbar.json
│   │   │   ├── recent-posts.json
│   │   ├── templates/
│   │   ├── assets/
│   │   ├── index.html
│   │   ├── post.html
│   │   ├── tasks.json
│   │   ├── tasks-post.json
└── server.py
Enter fullscreen mode Exit fullscreen mode

The contents of the directory structure can be viewed in detail within the repository where the example blog resides.

Below is the JSON file used to populate the menu bar template.

public/data/navbar.json

[
    {
        "category": "Most Recent",
        "category_slug": "all",
        "page": 1
    },
    {
        "category": "Imaginatio",
        "category_slug": "imaginatio",
        "page": 1
    },
    {
        "category": "Mundos",
        "category_slug": "mundos",
        "page": 1
    }
]
Enter fullscreen mode Exit fullscreen mode

Populating Templates with Data

Now, let's examine templates and how data is associated with them. We'll examine the template used to generate the navigation menu, triggered by the "add-navbar" task:

public/templates/nav-bar.html

<ul class="navbar-nav nav-underline">${(() => {
    if (!task.ok) {
        return task.status === 404 ? `<strong>${data}</strong>` : 'an error happened!';
    }

    return data.map(record => `<li class="nav-item">
        <a href="/?page=1&category-slug=${record.category_slug}#posts-list"
            ${window.location.pathname !== '/post.html' ?
                `data-tasks="show-posts-list"
                data-trigger="click"
                data-after="remove-active-menu add-active-menu scroll-into-posts-list"
                data-src-data="data/categories/${record.category_slug}/1.json"` : ''}
                class="nav-item nav-link ${window.location.pathname !== '/post.html' &&
                    record.category_slug === 'all' ? 'active' : ''}">${record.category}</a>
        </li>`).join("");
})()}</ul>
Enter fullscreen mode Exit fullscreen mode

The template combines HTML and JavaScript. To understand this approach, consider how PHP pages are generated. In PHP, code is embedded within the HTML. Similarly, this library leverages JavaScript template literals to achieve the same objective. From the definition: "Template literals are literals delimited with backtick (`) characters, allowing for multi-line strings, string interpolation with embedded expressions, and special constructs called tagged templates".

In this example, we see an object called "task" and a variable named "data". Essentially, the variable "data" serves as a shortcut for "task.result". The properties "ok" and "status" within the "task" object represent the outcome of the task and correspond to the result of the data fetch. Additionally, the "task" object contains the "event" object, which can be accessed through the "event" property: "task.event".

Additionally, templates can be incorporated into the HTML code either through the "script" HTML element with type type "text/template" or transmitted server-side via the "is-template" property, which is added to the task or specified in the "Secutio-Transformation: is-template:true" header. In this scenario, the template property is appended to the "task" object, providing access to the template via "task.template".

public/tasks.json

{
    "show-posts-list": {
        "trigger": "init",
        "attribute-trigger": "data-trigger",
        "src-data": "data/categories/all/1.json",
        "attribute-src-data": "data-src-data",
        "template": "templates/list-posts.html",
        "target": "#posts-list",
        "swap": "inner",
        "after": "scroll-into-posts-list",
        "attribute-after": "data-after"
    }
}
Enter fullscreen mode Exit fullscreen mode

The "show-posts-list" task automatically starts upon blog load, as can be seen in the provided template. This task displays the latest posts. However, when a user clicks on the list to access posts through a click event, the triggering mechanism differs slightly.

Here's a breakdown of the process:

  • The HTML element where the task is declared possesses an attribute named "data-trigger";
  • When a click event loads the list, the value of the "data-trigger" attribute replaces the "trigger" property within the task declaration;
  • This replacement also applies to the "src-data" and "after" properties of the task.

If reusing an existing task is not desired, a new task can always be declared specifically for the click event.

How to Fix SEO Issues in Single-Page Web Applications (SPAs)

To improve search engine optimization (SEO) for a Single-Page Application (SPA), developers can leverage sitemaps and consider creating separate pages. This allows search engines to follow links and potentially improve indexing of the content within the SPA.

In SPAs, the History interface's "replaceState()" method is used to update the URL when users navigate to specific content. This method modifies the current history entry, replacing it with a state object and a URL that reflects the displayed content.

public/templates/single-post.html

${(() => {
    if (!task.ok) {
        return task.status === 404 ?
            `<strong>${data}</strong>` : 'an error happened!';
    }

    history.replaceState({}, "", data.permalink);

    return `<h3 class="pb-4 mb-4 fst-italic border-bottom">From the ${data.category}</h3>
        <article class="blog-post">
        <h2 class="display-6 link-body-emphasis mb-1">${(() => {
            document.title = 'Large - ' + data.title;
            return data.title;
        })()}</h2>
        <p class="blog-post-meta">${(() => {
            const dc = new Date(data.date_created + 'Z');
            return dc.toLocaleString('en-us',{year:'numeric', month:'long', day:'numeric'});
        })()} by <a href="#">${data.author}</a>
        </p>
        <p>
            <img src="${data.image.src}"
                class="figure-img img-thumbnail rounded"
                alt="${data.image.alt}"
                style="width: 50%;float: left;margin-right: 20px;">
            ${data.text}
        </p>
    </article>`;
})()}
Enter fullscreen mode Exit fullscreen mode

As evident from the template above, whenever a user accesses a post, the client's URL changes to the URL specified in the data source for that post ("data.permalink"). The same behavior occurs when navigating to a category from the menu, whereupon the list of articles within that category is displayed (public/templates/list-posts.html).

Live Preview

This example does not use any database; it is based on static files and only needs an HTTP server to run. Any manual modification to the JSON files in the "data/raw" data directory requires running the "metamorph.py" script to update the remaining files.

To view the blog, simply use the Python HTTP server and execute the following command:

$ cd blog
$ python3 -m http.server -d public
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this project, our objective was to demonstrate the effectiveness of integrating the Secutio framework with Bootstrap to streamline the development of dynamic web applications. Comparable to PHP's utilization of templates in website development, Secutio's task-oriented approach with JavaScript literal templates minimizes the need for extensive JavaScript when working with Bootstrap components. This methodology has the potential to substantially decrease development time and complexity, rendering it well-suited for constructing interactive and responsive web applications.


The source code for this project is available here.

Additional details and explanations are available in the framework documentation. You can also access the code here.

Thank you for reading!

Top comments (0)