Stormkit Logo
Stormkit Logo
docsblogwhats new?FAQpricingGitHubTwitterDiscordDiscordlogin

Building Dynamic Web Applications with SSR, Htmx and Handlebars.js

Nov 09, 2023

In today's rapidly evolving web development landscape, it has become easier than ever to create dynamic and interactive web applications.

One powerful combination of technologies that enables this process includes Server-Side Rendering (SSR), htmx for seamless AJAX interactions, and Handlebars.js for dynamic templating. Additionally, we will leverage Vite.js, a lightning-fast build tool.

This tutorial will walk you through the process of building a simple web application that features infinite scrolling using these technologies. Additionally, we'll deploy the application on

Configuring Vite.js for Stormkit

To get started, we'll configure Vite.js to generate output compatible with Stormkit. This involves setting up a development server for local development and generating a .stormkit folder during the build process. This folder can contain three subfolders:

  • public for static assets and pages
  • api for defining API endpoints (which won't be covered in this post)
  • server for server-side rendering, also implemented as a lambda function

In our example, we only want views that are generated with handlebars.js so we don’t need to configure anything for public and we will go through api in another post.

For simplicity, we will focus on the important sections of the configuration. Most of the configuration is adapted from the Vite.js SSR guide. You can find the complete configuration here.

build: {
    ssr: true,
    ssrManifest: true,
    minify: false,
    rollupOptions: {
      preserveEntrySignatures: "strict",
      input: { server: "src/entry-server.ts" },
      output: {
        dir: ".stormkit/server",
        format: "esm",
        entryFileNames: "[name].mjs",
        preserveModules: true,
        preserveModulesRoot: "src",
        exports: "named",
plugins: [
      targets: [
        // copy html files to be used as template
          src: 'src/ssr/handlers/views/**', // Adjust the source pattern according to your project structure
          dest: "../.stormkit/server/ssr/handlers/views/",
          src: "public/**",
          dest: "../.stormkit/public",

This configuration instructs Vite.js to output files to .stormkit/server, with the entry file specified in src/entry-server.ts. Static assets are also copied to the appropriate directories so that we can use them in our dynamically generated view.

Implementing Server-Side Rendering Logic

The next step is to create the entry-server.js file. This file serves as the entry point for routing different paths to their respective views:

import serverless from "@stormkit/serverless";
import { Response, Render } from "./ssr/render";

export type RenderFunction = (url: string) => Promise<Response>;

export const render: RenderFunction = async (url) => {
   return Render(url);

// This handler add support for Stormkit environment. This is
// the entry point of the serverless application.
export const handler = serverless(async (req: any, res: any) => {
  // We are in assets folder
  // const dir = path.dirname(fileURLToPath(import.meta.url));
  // const html = fs.readFileSync(path.join(dir, "./index.html"), "utf-8");

  const { content, head } = await Render(req.url?.split(/\?#/)[0] || "/");

    head.statusCode || 200,
    head.statusMessage || "OK",
    Object.assign({}, head.headers, {
      "Content-Type": "text/html; charset=utf-8",

We export two functions, render and handler . render will be used by dev server while handler is a wrapper provided by to make the code compatible with AWS Lambda. Stormkit will use that during build to upload your function.

In the Render function, we use path-to-regexp to match the URL path to functions that will render views. This library is used in Express.js. The function also includes middleware support and handles query parameters.

Below you can see contents of the Render function

import { pathToRegexp } from "path-to-regexp";
import { testHandler } from "./handlers/test-handler";
import { indexHandler } from "./handlers/index-handler";

// Add more routes as needed
const routes = [
  { path: "/foo/:name", handler: testHandler },
  { path: "/", handler: indexHandler },

function middleware1(_params) {
  // write your middleware

const middlewares = [middleware1];

export interface Response {
  head: Head,
  content: string,

interface Head {
  statusCode: number;
  statusMessage?: string;
  headers?: Record<string, string | string[]>;

export const Render = (url: string): Response => {

  const params = {};
  // extract query parameters place them
  // inside params
  if (url.includes("?")) {
    params["query"] = {};
    let [tempUrl, urlParams] = url.split("?");
    url = tempUrl;
    let paramPair = new URLSearchParams(urlParams);
    paramPair.forEach((v, k) => {
      params["query"][k] = v;

  // if not path is matched return 404, default view
  let res = {
    head: {statusCode: 404},
    content: `
       <!DOCTYPE html>
       <html lang="en">
           <meta charset="UTF-8">
           <meta name="viewport" content="width=device-width, initial-scale=1.0">
           <title>404 - Page Not Found</title>
           <div class="container">
               <h1>404 - Page Not Found</h1>
               <p>The page you are looking for does not exist.</p>
               <p><a href="/">Go back to the homepage</a></p>
  for (const route of routes) {
    const keys = [];
    const pattern = pathToRegexp(route.path, keys);
    const match = pattern.exec(url);
    if (match) {
      keys.forEach((key, index) => {
        params[] = match[index + 1];

      // go through middleware if you want to run functions
      // for every view
      middlewares.forEach(middleware => middleware(params));
      res = route.handler(params);

  return res;

export default Render;

We have added comments to explain the code, but essentially what we are doing is invoking a function to render an HTML using handlerbars.js for a given URL. You have the option to use any other templating engine or simply return a string that will be treated as HTML.

Now, let's dive into creating a basic view using Handlebars.js:

import { Response } from "../render";
import fs from "fs";
import path from "path";
import Handlebars from "handlebars";

export function indexHandler(_): Response {
  const currentFile = import.meta.url;
  const fileUrl = path.join(path.dirname(currentFile), "views", "index.html");
  // convert to path
  const contents = fs.readFileSync(fileUrl.replace("file:", ""), "utf8");
  const template = Handlebars.compile(contents);
  const body = template({});

  return {
    head: { statusCode: 200 },
    content: body,

This is function will be invoked when we visit / . It reads index.html and renders with Handlebars. For this view we don't necessarily need handlerbars.js since we are not passing any data.

You can see the dev server configuration here. It's mostly copy/paste from the vitejs guide. Basically, we are passing everything to the render function we exported.

Implementing Infinite Scrolling with htmx

Now that we have everything setup for SSR lets to a infinite scrolling using htmx. Idea is to have a table that presents data and when we view last row of the table we need to request next page. For sake of simplicity we implemented a function that generates random array of strings so our view will get page= parameter and it will fill the table, htmx will fetch data and append to our table.

Lets go through our view first generated in server side.

import path from "path";
import { Response } from "../render";
import fs from "fs";
import Handlebars from "handlebars";

export function testHandler(params): Response {
  const currentFile = import.meta.url;
  const page = params['query']['page'] * 1 || 1
  const words = generateRandomStrings(10)
  // return only partial html
  let contents = ''
  // for upcoming pages return html with data
 // htmx will append it to table
  if (page > 1) {
    contents = `
      {{#each words}}
        {{#if @last}}
        <tr hx-get="/foo/contacts/?page={{../nextPage}}" hx-trigger="revealed" hx-swap="afterend">
          <td> *** {{this}} *** {{../nextPage}}  </td>
          <tr> <td> {{this}}  </td> </tr>
  } else {
   const fileUrl = path.join(path.dirname(currentFile), "views", "test.html");
   contents = fs.readFileSync(fileUrl.replace("file:", ""), "utf8");

  const template = Handlebars.compile(contents);
  const body = template({words: words, nextPage: page + 1 });

  return {
    head: { statusCode: 200 },
    content: body,

function generateRandomStrings(numStrings) {
   // check repo for details if you like

We will read test.html and populate with our first set of data.

  <script src=""></script>
    <h1>Name Table</h1>
    <table border="1">
      {{#each words}}
        {{#if @last}}
        <tr hx-get="/foo/contacts/?page={{../nextPage}}" hx-trigger="revealed" hx-swap="afterend">
          <td> *** {{this}} *** {{../nextPage}}  </td>
          <tr> <td> {{this}}  </td> </tr>

When we create our last row of the table, we sprinkle some htmx. Htmx will make request for next page and append returning html to table. With very little code we added interactivity to our server side rendered page.

This approach enables us to create a highly interactive web application with minimal code, thanks to the power of SSR, htmx, and Handlebars.js.

You can find an example project here. Deployment links are in

Deploying this application to Stormkit from this point on is a breeze. Simply import your application and click the "Deploy Now" button.

Stormkit Logo

© 2024 Stormkit, Inc.

pricingprivacy policyterms
\n \n

Name Table

\n \n \n \n \n {{#each words}}\n {{#if @last}}\n \n \n \n {{else}}\n \n {{/if}}\n {{/each}}\n
*** {{this}} *** {{../nextPage}}
\n \n\n```\n\nWhen we create our last row of the table, we sprinkle some htmx. Htmx will make request for next page and append returning html to table. With very little code we added interactivity to our server side rendered page.\n\nThis approach enables us to create a highly interactive web application with minimal code, thanks to the power of SSR, htmx, and Handlebars.js.\n\nYou can find an example project [here]( Deployment links are in [](\n\nDeploying this application to Stormkit from this point on is a breeze. Simply import your application and click the \"Deploy Now\" button.","navigation":[{"path":"how-to-deploy-docusarous","title":"How do deploy Docusaurus project to Stromkit","description":"Quick guide to show how to deploy Docusaurus project to Stormkit","search":false,"date":"2024-04-05","active":false},{"path":"how-to-deploy-gatsby","title":"How do deploy Gatsby project to Stromkit","description":"Quick guide to show how to deploy Gatsby project to Stormkit","search":false,"date":"2024-04-04","active":false},{"path":"how-to-deploy-vitepress","title":"How do deploy VitePress project to Stromkit","description":"Quick guide to show how to deploy VitePress project to Stormkit","search":false,"date":"2024-04-04","active":false},{"path":"guide-to-understanding-choosing-right-analytic-tools-for-your-website","title":"A Guide to Understanding and Choosing the Right Analytic Tools for Your Website","description":"This post explores the diverse landscape of web analytics tools, delving into alternatives like GoatCounter, Plausible, Umami, PostHog and Stormkit. Highlighting their unique features and emphasizing the growing importance of self hosted solutions.","search":false,"date":"2024-01-24","active":false},{"path":"postgresql-with-golang-crafting-insert-queries","title":"PostgreSQL with Golang Crafting Insert Queries with Text/Templates","description":"Stormkit's approach of eschewing ORM and query builders in favor of raw SQL queries using text/templates.","search":false,"date":"2024-01-21","active":false},{"path":"site-search-with-react-and-minisearch","title":"Local search with react and minisearch","description":"This blog post walks you through our implementation for local search using minisearch and creating react component as search box.","search":false,"date":"2023-12-16","active":false},{"path":"building-dynamic-web-applications-with-ssr-and-htmx","title":"Building Dynamic Web Applications with SSR, Htmx and Handlebars.js","description":"Learn how to create interactive web applications using Server-Side Rendering (SSR), htmx for seamless AJAX interactions, and Handlebars.js for dynamic templating.","search":true,"date":"2023-11-09","active":true},{"path":"unlocking-dynamic-web-interactions-with-htmx","title":"Unlocking dynamic web interactions with htmx","description":"Unlock the potential of htmx, a lightweight JavaScript library for dynamic web interactions. Simplify AJAX requests, page updates, and DOM manipulation with ease. See how htmx integrates seamlessly with server-side rendering.","search":true,"date":"2023-11-02","active":false},{"path":"getting-started-with-vitejs","title":"Combining Vite.js with Stormkit for React Static Sites Guide","description":"Learn how to leverage the lightning-fast Vite.js build system and Stormkit's seamless deployment platform for React static sites. Follow our quick start guide and explore migration options from Webpack. Plus, discover an all-in-one template project for server-side rendering, static page generation, and API development using React and Vite.js.","search":false,"date":"2023-10-08","active":false},{"path":"how-ai-helping-us-catch-phishing-websites","title":"How AI helps us catching phishing websites","description":"This post explores how Stormkit combats phishing on their hosting platform. They initially used a javascript program for detection, but faced false positives and evasion tactics. By integrating AI through Teachable Machine and TensorFlow.js, they markedly improved accuracy.","search":false,"date":"2023-09-26","active":false},{"path":"faq","title":"FAQ","description":"Our Frequently Asked Questions (FAQ) page is designed to provide you with quick and helpful information about Stormkit.","search":false,"date":"2023-08-17","active":false},{"path":"writing-a-headless-cms-from-scratch--planning","title":"Writing a Headless CMS from scratch","subtitle":"Part 1 - Planning","description":"Welcome to the exciting journey of building a headless CMS from scratch! In this blog post series, we will embark on a step-by-step adventure, exploring the process of creating a powerful content management system tailored to our unique needs.","search":false,"date":"2023-06-04","active":false,"author":{"name":"Savas Vedova","img":"","twitter":"@savasvedova"}},{"path":"why-we-are-dropping-support-for-next-js","title":"Why we are dropping support for Next.js","description":"In our continuous pursuit of delivering the best developer experience, we've made a bold decision. We're dropping serverless support for Next.js.","search":true,"date":"2023-05-21","active":false},{"path":"monitoring-app-using-stormkit-and-supabase","title":"Monitoring system using Stormkit and Supabase","description":"Learn how to monitor your website's URL status using Stormkit and Supabase. Easy to set up and cost-efficient, this powerful tool can send notifications to Discord, track historical data, and more. Check out the source code now!","search":false,"date":"2023-01-24","active":false},{"path":"factory-pattern-for-go-tests","title":"Factory pattern for Go tests","description":"This article will show you how we improved our Go unit tests worklow using a factory pattern.","search":false,"date":"2022-11-01","active":false},{"path":"how-to-create-content-site","title":"How to create and deploy documentation page using nuxt.js and Stormkit under 5 minutes","description":"Learn how to create a website in under 5 minutes with this step-by-step guide.","search":true,"date":"2022-09-23","active":false},{"path":"migrating-your-app-from-webpack-to-vite","title":"Migrating your app from Webpack to Vite","description":"Learn how to migrate your app from Webpack to Vite with this quick guide. Discover the configuration options used in Vite and the rationale behind the migration.","search":false,"date":"2022-08-29","active":false},{"path":"how-to-deploy-on-aws-s3","title":"Deploy your app on AWS S3","description":"Learn how to deploy your static applications on your own AWS account using Stormkit.","search":true,"date":"2022-04-10","active":false},{"path":"using-analytic-tools-with-stormkit","title":"Tracking user data using Stormkit and Plausible","description":"Learn how to track user data using Stormkit and Plausible without deploying your application. Inject snippets instantly with this powerful functionality.","search":true,"date":"2022-04-09","active":false},{"path":"how-to-deploy-to-bunny-cdn","title":"How to deploy to BunnyCDN","description":"Learn how to deploy your static applications on your own Bunny CDN account using Stormkit.","search":false,"date":"2022-04-01","active":false},{"path":"whats-new","title":"What's New?","description":"Discover the latest changes and improvements to Stormkit. Stay up-to-date and get the most out of our platform.","search":false,"active":false}]}