Deploying a static website is usually simpler than deploying a backend application. A website built with HTML, CSS, and JavaScript does not need a database connection, application server, or complex backend infrastructure. It only needs a web server that can deliver files to visitors.

However, deployment can become confusing when developers try to host a static website on a platform traditionally used for backend applications. They may encounter missing build directories, incorrect start commands, routing errors, or deployment failures because the platform expects a running web process.

Heroku supports static website deployment through its Front-End Web Cloud Native Buildpack. This allows developers to serve static files without writing a custom Express or PHP server.

In this article, we will build a static website, configure the required files, deploy it to Heroku, and troubleshoot common problems. We will also look at what happens when the website uses a frontend framework such as React or Vite.

What Is a Static Website?

A static website delivers files directly to the browser without generating HTML through server-side application code for every request.

A typical static website contains three main technologies:

Technology

Responsibility

HTML

Defines the structure and content of a webpage.

CSS

Controls layout, colors, typography, and responsiveness.

JavaScript

Adds browser-side interactions and functionality.

For example, consider a company website containing a homepage, an About page, a Services page, and a Contact page.

If these pages are already written as HTML files, the server does not need to execute backend code to generate them.

A static website can be useful for:

  • Personal portfolios.

  • Company websites.

  • Product landing pages.

  • Documentation websites.

  • Marketing pages.

  • Frontend applications that consume external APIs.

However, static websites have limitations. They cannot securely access private database credentials or perform server-side operations directly. Those responsibilities must be handled by a backend service.

How Static Website Hosting Works on Heroku

Heroku traditionally runs applications through processes defined in a Procfile or through platform-specific buildpack configuration.

For static websites, the Front-End Web Cloud Native Buildpack provides a web server that delivers the generated frontend files.

The deployment process follows this structure:

Code
HTML, CSS, JavaScript
          |
          v
     Git Repository
          |
          v
   Heroku Build Process
          |
          v
   Static Web Buildpack
          |
          v
    Static Web Server
          |
          v
       Browser

The buildpack identifies the directory containing the website files and prepares the application to serve them.

For a basic website, developers do not need to create a separate Node.js server just to return HTML files.

One important distinction is that the buildpack serves static content. It does not automatically provide backend functionality, database operations, or server-side rendering.

Creating a Static Website Project

Let's create a simple website using HTML, CSS, and JavaScript.

Start by creating a project directory:

Bash
mkdir heroku-static-site
cd heroku-static-site

Create the following directory structure:

Code
heroku-static-site/
│
├── public/
│   ├── index.html
│   ├── about.html
│   ├── style.css
│   └── app.js
│
└── project.toml

The public directory contains the files that the web server will deliver to visitors.

Creating the Homepage

Create a file named index.html inside the public directory.

HTML / XML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">

    <meta name="viewport"
          content="width=device-width, initial-scale=1.0">

    <title>My Static Website</title>

    <link rel="stylesheet" href="/style.css">
</head>
<body>

    <header>
        <h1>Welcome to My Website</h1>
        <p>A simple website built with HTML, CSS, and JavaScript.</p>
    </header>

    <main>
        <section>
            <h2>About This Website</h2>

            <p>
                This is a static website that does not require
                a backend application server.
            </p>

            <button id="welcomeButton">
                Click Me
            </button>

            <p id="message"></p>
        </section>

        <a href="/about.html">About Us</a>
    </main>

    <script src="/app.js"></script>

</body>
</html>

The page contains a heading, description, button, and navigation link.

The CSS and JavaScript files are loaded directly by the browser.

Adding CSS

Create style.css:

CSS
body {
    margin: 0;
    font-family: Arial, sans-serif;
    background: #f4f6f8;
    color: #333;
}

header {
    background: #354b9c;
    color: white;
    padding: 40px 20px;
    text-align: center;
}

main {
    max-width: 900px;
    margin: 40px auto;
    padding: 20px;
    background: white;
    border-radius: 8px;
}

button {
    padding: 10px 20px;
    border: none;
    background: #354b9c;
    color: white;
    cursor: pointer;
    border-radius: 5px;
}

This stylesheet controls the appearance of the website.

Adding JavaScript

Create app.js:

JavaScript
const button = document.getElementById("welcomeButton");

const message = document.getElementById("message");

button.addEventListener("click", () => {
    message.textContent = "The website is working correctly!";
});

This code adds a browser-side interaction.

There is no need to run an Express application because the JavaScript executes in the visitor's browser.

Configuring the Heroku Static Web Buildpack

After creating the website, configure the buildpack to identify the directory containing the static files.

Create a project.toml file in the root directory.

C#
[_]
schema-version = "0.2"

[[io.buildpacks.group]]
id = "heroku/static-web-server"

[com.heroku.static-web-server]
root = "public"
index = "index.html"

There are two important configuration properties.

The root property identifies the directory containing the website files.

The index property identifies the default HTML document returned when someone opens the website's root address.

In this example, Heroku serves files from the public directory and uses index.html as the homepage.

A common mistake is setting the root directory to the project directory instead of the directory containing the actual website files. This can cause missing pages or unexpected file responses.

Deploying the Website to Heroku

Once the project is ready, prepare the deployment environment.

Step 1: Install and Configure Heroku CLI

Install the Heroku CLI and authenticate:

Bash
heroku login

Verify that authentication is successful before continuing.

Step 2: Create a Heroku Application

Create an application:

Bash
heroku create my-static-site

Use an available application name.

Step 3: Initialize Git

If the project is not already a Git repository, initialize it:

Bash
git init

Add the project files:

Bash
git add .

Create a commit:

Bash
git commit -m "Add static website"

Step 4: Deploy the Application

Push the application to Heroku:

Bash
git push heroku main

During deployment, Heroku reads the buildpack configuration and prepares the static web server.

If your local branch is named master, use the appropriate Git push command for that branch.

Step 5: Open the Website

After deployment, open the application:

Bash
heroku open --app my-static-site

The homepage should now be available through the application's Heroku address.

If the website does not load, inspect the deployment logs:

Bash
heroku logs --tail --app my-static-site

The logs help identify build failures, startup problems, and missing configuration.

Deploying React and Vite Applications

A static website does not have to contain manually written HTML files.

Modern frontend frameworks generate static files during their production build process.

For example, a React application created with Vite usually generates its production output inside a directory named dist.

The source code may contain JSX, components, and development dependencies, but the final output consists of HTML, CSS, and JavaScript files.

Understanding the Build Process

Consider this project structure:

Code
react-website/
│
├── src/
│   ├── App.jsx
│   └── main.jsx
│
├── public/
├── package.json
├── package-lock.json
└── project.toml

The build command generates the production files.

Bash
npm install
npm run build

After the build completes, the directory structure generally looks like this:

Code
dist/
├── index.html
└── assets/
    ├── index-abc123.js
    └── index-def456.css

These generated files are what the static web server needs to deliver.

Configure the Buildpacks

For a frontend project that requires Node.js during the build, configure the Node.js buildpack before the static web server buildpack.

Update project.toml:

C#
[_]
schema-version = "0.2"

[[io.buildpacks.group]]
id = "heroku/nodejs"

[[io.buildpacks.group]]
id = "heroku/static-web-server"

[com.heroku.static-web-server]
root = "dist"
index = "index.html"

The Node.js buildpack handles dependency installation and the frontend build process. The static web server then serves the generated files.

The order matters because the static server needs the production output generated by the frontend build.

For reproducible deployments, commit your package lockfile and make sure the required build script exists in package.json.

Handling 404 Errors in Single-Page Applications

One common problem with React and other single-page applications is direct URL navigation.

For example, your application may support these routes:

Code
/
 /about
 /products
 /contact

When users navigate inside the application, the JavaScript router handles these paths.

However, when someone directly opens /products, the web server may search for a physical file named products.

If that file does not exist, the server can return a 404 response.

For a single-page application, configure the server to return the main HTML document for application routes.

Code
[com.heroku.static-web-server.not-found]
status = 200
path = "/index.html"

This allows the frontend router to process the requested path.

Use this fallback only when appropriate. If every missing file also returns the homepage, genuine missing JavaScript, CSS, or image files may become harder to identify.

Test both application routes and missing static assets after deployment.

Configuring Environment-Specific API Endpoints

A static website may need to communicate with a backend API.

For example, a frontend application might use one API endpoint in development and another in production.

A simple approach is to use a runtime configuration mechanism supported by the static web server.

For example, the HTML can contain a configurable attribute:

HTML / XML
<html data-api-url="{{API_URL}}">

The application can read that value using JavaScript:

JavaScript
const apiUrl =
    document.documentElement.dataset.apiUrl;

async function loadProducts() {
    const response = await fetch(
        `${apiUrl}/products`
    );

    if (!response.ok) {
        throw new Error("Unable to load products");
    }

    return await response.json();
}

The API endpoint must be configured using the static web server's supported runtime templating configuration.

Do not assume that setting a Heroku environment variable automatically replaces text inside HTML files. Verify that the runtime configuration is enabled and configured correctly.

Also remember that values exposed to frontend JavaScript are public. Never put database passwords, private API keys, or other confidential credentials in browser-delivered configuration.

Troubleshooting Common Deployment Problems

Problem

Possible reason

What to check

Homepage returns 404

Incorrect root directory or missing index file

Verify root and index in project.toml

CSS files are missing

Incorrect asset paths

Check HTML references and generated files

JavaScript does not load

Build output or asset path problem

Inspect browser developer tools

Deployment build fails

Missing dependencies or build configuration

Review Heroku build logs

React routes return 404

SPA fallback is missing

Configure the Not Found fallback

API requests fail

Incorrect endpoint or CORS configuration

Check API configuration and browser network logs

Old website content appears

Browser or asset caching

Review cache-control headers and asset filenames

Checking Generated Files Before Deployment

For frontend applications, always verify the production output locally.

Bash
npm ci
npm run build

Then inspect the generated directory:

Bash
ls -la dist

Confirm that index.html exists and that its referenced assets are available.

This simple check can prevent many deployment failures.

When Should You Use Heroku for Static Websites?

Heroku is one possible hosting option, but it is not automatically the right choice for every static website.

The decision depends on your existing infrastructure, deployment workflow, operational requirements, and expected traffic.

Requirement

Heroku static hosting

Dedicated static hosting

Existing Heroku infrastructure

Fits an existing deployment workflow

Requires a separate platform

Simple HTML website

Supported

Usually straightforward

Frontend build process

Supported through buildpacks

Supported by many providers

Backend integration

Requires a separate backend service

Usually requires a separate backend service

Custom server-side logic

Not provided by the static web server

Depends on the hosting platform

Cost considerations

Includes dyno and platform costs

Depends on provider and usage model

Operational management

Uses Heroku application workflows

Depends on the hosting provider

If your organization already deploys backend applications on Heroku, hosting a static frontend on the same platform may simplify infrastructure management.

For a small portfolio or documentation website that only needs static file delivery, a dedicated static hosting platform may be more economical and require less operational configuration.

Evaluate the actual hosting requirements instead of choosing a platform only because it supports a particular deployment method.

Best Practices for Static Website Deployment

  1. Keep source files separate from generated production files.

  2. Commit the package lockfile when using frontend build tools.

  3. Verify the build output before deploying.

  4. Configure the correct static root directory.

  5. Use appropriate fallback rules for single-page applications.

  6. Keep private credentials on the backend.

  7. Review caching behavior after every frontend deployment.

  8. Test direct navigation, missing pages, and asset loading.

  9. Inspect deployment logs when a build or startup fails.

  10. Keep the frontend and backend responsibilities separate.

Advantages and Disadvantages

Advantages

  • Developers can deploy plain HTML, CSS, and JavaScript without creating a custom backend server.

  • Frontend build tools can generate production assets during deployment.

  • Existing Heroku deployment workflows can be reused.

  • Single-page application routing can be configured.

  • Runtime configuration can support different deployment environments.

Disadvantages

  • The static web server cannot execute custom backend application logic.

  • Frontend buildpack configuration requires attention to directory structure and execution order.

  • Dedicated static hosting may be more economical for simple websites.

  • Applications requiring server-side rendering need a different deployment architecture.

  • Incorrect caching and routing configuration can create problems after deployment.

Summary

Hosting static HTML, CSS, and JavaScript websites on Heroku does not require building a custom Express server just to deliver frontend files.

The Front-End Web Cloud Native Buildpack provides a static web server that can serve an existing website directory or the production output generated by frontend build tools.

For a basic website, the main configuration involves selecting the correct static root and index file. For React or Vite applications, the Node.js buildpack must generate the production assets before the static web server serves them.

Developers should pay particular attention to SPA routing, asset paths, runtime configuration, caching, and deployment logs.

Before choosing Heroku, also consider whether your application actually benefits from running on the platform. For teams already using Heroku, it can fit naturally into their deployment workflow. For a website that only needs simple static file delivery, a dedicated static hosting service may be a better option.