What's a Static Site?

Sure, it's a site that doesn't change. Oh, no, I mean, you can have Javascript there, and content can be dynamic, but every user gets the same HTML delivered to them up front. Oh, but, I guess a React app that makes fetch requests can be served as a static site.

I've spent a lot of time thinking about this question. I built a blog back in 8th grade, and nowadays I work on applications with React frontends. So, I sort of missed the progression. I won't credit this question with being the only reason this site took so long to get going, but, honestly, not being sure what my hosting provider would put up for free was a blocker.

As my first answers above illustrate, and from my many varied readings on this topic, I think this is a bit of a loaded question. The answer is obvious to many. But really, there's some nuance.

I think context is important when giving an answer here. For me, and probably for many, I think the most useful re-framing of "What is a static site?" is "What can my static site hosting provider actually host?". Whatever can be hosted, must be a static site. Thinking in extremes, the other useful re-framing is, "What does the simplest dynamic site generator do?". Whatever a dynamic site generator does, it must build a dynamic site, which is the opposite of a static site, right?

So, let's look at those re-framings.

What Can My Static Site Hosting Provider Actually Host?

Maybe you've seen or used something like this before:

I find Express' documents describe static files particularly well, so let's start there.

To serve static files, Express uses the express.static middleware. You specify a static directory, for example, public. Each [HTTP] request flows through the middleware, and if request.path matches a file in the specified directory, Express will serve that file (from Express' examples - GET /js/app.js will cause Express to look for ./public/js/app.js). Under the hood, express.static uses serve-static, which itself is basically a wrapper over send, which actually handles piping a file to an http.ServerResponse.

Nginx, used as a web server, does something similar. You specify a root directory that will be used to search for a file (e.g. /static). Then, if your request is /images/X, Nginx will look for /static/images/X. If found, to actually serve the file: "By default, NGINX handles file transmission itself and copies the file into the buffer before sending it." (from the previous link).

What does it mean to "serve" a file?

This is another buzzword that is almost impossible to Google. In a nutshell, this means processing a request looking for a file, finding and reading the corresponding file, and [somehow] piping it back through the [TCP] connection as the response body.

I linked to the line in send that does this above. In an Express app, Express starts a a Node http.Server, and processes IncomingMessages and outputs ServerResponses (here is a nice overview of that Node API). The express.static middleware looks at each request and looks in the specified directory for a file matching the request's path. If one is found, send just writes to a ServerReponse using a Node readable.pipe.

And, the sendfile disclaimer in the Nginx static files link above hints at the mechanism Nginx uses - "By default, NGINX handles file transmission itself and copies the file into the buffer before sending it. Enabling the sendfile directive eliminates the step of copying the data into the buffer and enables direct copying [of] data from one file descriptor to another." Nginx is a very well known production web server software - it processes requests and outputs responses (I imagine there is C recving and sending involved).

So, physically "serving a file" is piping data over a client / server connection.

How is HostingProvider Serving Files?

So, the next question is, is my static hosting provider serving files using an Express app? Nginx? Well, I dug a bit, and couldn't find anything too concrete, but here's a blog post from Cloudflare about outgrowing Nginx.

The point is, they must be using a piece of software with a type of file serving mechanism like those discussed above! My guess is that they're using the simplest form of static serving possible. For example - Nginx only serving a single root pointing to your build, or, an Express app with no views, only an express.static middleware pointing to your build (although, even simpler is just creating an http.Server which registers serve-static.serve to serve the build - as shown in that package's examples).

What is HostingProvider's serving software *not* doing? Proxying requests to another server (a la Nginx), finding a non-static endpoint in the Express app and running its logic (a la Express). HostingProvider is ready to provide files from a folder you tell it files should be in. They're not going to spend money having their serving software do anything other than accept a request, look for a file, and pipe it back as a response.

A Point of Confusion (React Apps as Static Sites)

For some (👋), I think a point of confusion about what a static site is, when some people tell you a React app is a static site, is that it's not uncommon to serve a React app coupled with your API. Express' http.Server can be used in production, and can serve static files, so many projects do just fine running the whole "App" (UI and API) as a single Express instance.

ASP.NET is very similar to Express in how it handles static files. By convention, you place static content in wwwroot, and when you add static file middleware, it'll look there for files when processing requests. "It" is an IIS or Kestrel web server, which works in development and is certainly production ready as well.

With Express and ASP.NET, you can probably get pretty far before worrying about serving static content via a reverse proxy like Nginx. The Flask world is a bit different. The development server is explicitly not production-ready, so you have to decide on a production WSGI server to run your app. You can still have your single app instance serve a static directory (your UI build) and process API endpoints. Since you're already deciding on a production server, it may be a little more natural to also additionally think about a reverse proxy for static content. This Stack Overflow post makes a really nice delineation between UI and API (for Express + Nginx).

Because the API and UI are often coupled (which is convenient, since you can probably just use relative paths when fetching, rather than requesting from an independent API - this feels pretty natural for a relatively small app), I think it's natural that some people may wonder how a React app can be a static site. Well, a coupled UI + API app can't. You can't host an entire Flask or Express App on your static hosting provider. But, you can point your static hosting provider to your build! The API then needs to be accessible somewhere else and referenced from the UI.

React's Progression

As Josh Comeau points out, React has come a long way. In its earliest days you shipped all your component code as part of your build, and React worked on client computers to build an HTML document in real time. From our perspective, this is a static site! HostingProvider serves you the React bundle, and your computer takes care of building and rendering HTML. Over time, React added the ability to pre-generate HTML layouts on a server (server side rendering), and even to pre-generate entire components filled with data (server components). That earliest version of React meets our definition of static, but these later versions start to blur the lines.

I *highly* recommend reading his article I linked above (and much more of his!).

⚠️ Another Easy Thing to Forget

Let's say we're using a browser. When you go to a URI (make a request), and HostingProvider delivers a static HTML file to your browser (the client), maybe it has a <script> tag referencing a Javascript file. The browser will then request this file from the server. HostingProvider will then deliver that Javascript file to the browser. At that point, the browser (your computer) will run that Javascript and it will take up your computer's compute resources. So, HostingProvider is perfectly happy with that scenario. Again, it's perfectly fine to have Javascript in a "static" site (as long as you're footing the bill for it!).

So, the answer to "What can my static site hosting provider actually host" is probably - your hosting provider uses some server software to process requests, and that software is set up to look for files in a specified directory, and "serve" those back to clients.

A static site, then, is just a set of files that the hosting provider can read and send back via this software. Any html/js/css/image/etc. Javascript is fine, but keep in mind that your HostingProvider isn't willing to perform and computation on your behalf, so any endpoints you request need to be accessible elsewhere.

What Does the Simplest Dynamic Site Generator Do?

If a static site generator creates a build folder that can be served via one of the static serving approaches described above, what would a dynamic site generator create?

Moving outside the world of just static hosting providers, imagine there is a piece of software running on a machine, and we can send requests to that piece of software. That piece of software is configured to accept HTTP requests, look at the URI of the request, and do something based on that URI.

If the URI starts with /static, the software will look for a file matching the URI in /static, and it will read the file and pipe it back in the HTTP response (it will serve it 😃).

If the URI is just /, or starts with /api or anything else, the software will perform some computation - it will do some math, or access a database, or perform some authentication. Then, it will return an HTTP response with the result of the computation.

This is a basic "server" software that "routes" requests to different "endpoints" that each do some particular thing. Frameworks like Flask, Express, ASP.NET, etc. all handle things like processing requests, routing, configuring responses, and they use some mechanism to pipe data back over the connection.

Definition: The simplest dynamic site is one which asks our server software to do one of its defined bits of computation, rather than simply serving a file.

This is sort of a leap of faith. I wish I could define dynamic in isolation, as opposed to defining it as "any request that isn't met by serving a file" since you might say that's just computation too. The thing is, serving files is such a common thing that it is carved out and treated separately. Web servers can highly optimize file serving (compression, caching, etc) when they know that's all that's being asked for. When you ask for something custom, that's dynamic.

For example, imagine a simple Express app with an endpoint of /api/servefile. When you access www.mysite.com, assume it redirects to that endpoint, and that endpoint simply reads a file from the file system and sends the contents back as the response. That's a dynamic site! The contents can be HTML, JSON, images, whatever (like when you hit an API route in a browser). That is circumventing the highly optimized file serving route and asking the server software to do it your way instead.

What's happening there is that when you request something from the server, it goes and does something bespoke, as opposed to just serving a file. That is a dynamic site.

If a static hosting provider can host a static build that has been outputted by a static site generator, then let's assume a dynamic hosting provider can host a dynamic set of custom_functions that has been outputted by a dynamic site generator. Just as each request to build returns a document, each request to custom_functions runs a computation and returns the result (if any).

How would that dynamic generator work?

A custom_function is the dynamic equivalent of a blog post, or static asset. We might have a file that lists these functions

def show_homepage():
    return "Homepage"

def process_contact_form(data):
    data.save_to_database()

Now that we have our custom functions defined, we have to define how requests map to the functions. This isn't required for a static site, since as we discussed above, a build is hosted, and the server software looks for a file matching the request path in that build directory and serves it.

This might look something like:

request:
  name: show_homepage
  path: /
  action: GET
  computation: show_homepage

request:
  name: submit_contact
  path: /contact
  action: POST
  data: "firstname, lastname, email"
  computation: process_contact_form

Finally, we have to have some general handling logic that will process each request / response. Again, a static hosting provider will do this for you, since it knows what computation to run (serve a file) and it knows whether that succeeds or fails (file not found).

def process_request(req) -> Response:
    try:
        if req.action == HttpActions.POST:
            res = req.computation(data)
        else:
            res = req.computation()
        return Response(Success(res))
    except:
        return Response(Error)

So, our directory might look like:

dynamic_site/
    computations
    request_to_computation_mappings
    request_response_handling

Then, we run our dynamic site generator against dynamic_site. We then get a single App (code-only) file that can be served by a dynamic hosting server! The only leap of faith we'll take here is assuming that process_request is a common dynamic site API, and a dynamic hosting server calls that function for each request, and returns the response it's given.

Okay, in reality what we've just done is illustrate what a framework like Flask or Express looks like. In an App, you define your computations, tie them to routes, and the framework has logic to process each request, map it to one of the routes, perform the computation, and return a Response that can be piped back to the client. The process_request API is a veil for real HTTP / server APIs like CGI generally, or WSGI in the Python world.

Conclusion

I think an alternate title for this article could be "Differentiating Between Static and Dynamic Endpoints in a Typical Webapp." Or, "What Does It Mean To Serve a File, and What Does a Webapp 'Framework' Do?"

I think the re-framings discussed here really help in defining "What Is a Static Site?" Well, it is something a static hosting provider can deliver. A static hosting provider is likely set up to simply "serve" static files (find a file, pipe it over the connection). If you ask that server to do a custom computation, it won't do that (it won't even know what you're asking for, it just won't find a file!).

It's not uncommon to have a service with an API and a UI, and have these defined in one place. It's also typical to then host those together on a single machine, where an Express or Flask app is set to serve static content AND to perform computations.

Our dynamic site generator outputs an App which defines computations, ties them to requests, and uses an API the server understands to process a request and deliver the server a response. A static site generator simply creates a fixed build of assets, and the static hosting provider knows how to look one of those up based on a request and send it back to the client.

So, can a static site have dynamic content updated by Javascript - absolutely. Can I host my React app as a static site - maybe. You can host the UI build on a static site, but you'd have to host the API separately. If your React UI build already only references separately-fetchable sites, you're good to go.

I don't know enough about frameworks like NextJS, but they and others certainly blur lines. That's why I think the fundamentals described above are the key to understanding what your "app" really is (just UI, UI and computations, etc) and how its parts are delivered.