Server Side Rendering with Netlify Functions

I’d love to know if Netlify Functions can be used for server side rendering.

This is what needs to happen:

Build

  • Netlify runs the build command to generate both the server lamdba and the static files.
  • Netlify uploads the server file (for example /build/server.js) to AWS lambda.
  • Netlify uploads static files found in a folder (for example, /build/static/*).

Runtime

  • Netlify serves the static files when the /static/* URLs are accessed. Those files are cached by the CDN according to cache-control headers.
  • Netlify servers the HTML generated by the /build/server.js lambda to any other URL. Those HTML responses are cached by the CDN according to cache-control headers.

I took a look at the netlify.toml and I saw you can configure redirects.

  • Could those redirects be used to make Netlify serve HTMLs generated by the server lamba?
  • If that’s the case, could those HTML responses be cached by the CDN?
  • Is there any other way I’m not aware of to make this happen?

Thanks for this question @luisherranz!

I’m actually in the process of writing a post which explains a model similar to this right this very moment! (With some differences in the nuance I suspect). I presented a little demo during a presentation at NEJS. (You might find the slides helpful)

The model I was exploring was in allowing user generated content to be submitted via a form which would create content on a new URL and initiate a regeneration of the site to include that new page. While that page was being generated. I use Netlify’s custom 404 handling in the redirects to pass requests to that new URL to a serverless function which gets the content directly from the content API and does a serverless render.

Once the site has been regenerated and the new URL exists, the request to it no longer 404 and are simply satisfied by the pre-generated static page.

While I finish up my detailed post describing this all a bit more clearly, you might like to play with the demo as it is now, and you can also node around in the code.

Demo: https://vlolly.net
Repo: https://github.com/philhawksworth/virtual-lolly

I think of it as static first, with a serverless render fallback.
And I think this gets pretty close to the model you describe.

That’s interesting. Thanks for sharing @philhawksworth.

I have created a simpler repository and I think I have managed to make it work :slight_smile:

This is the live site: https://flamboyant-euclid-90b896.netlify.com/

This is the toml file:

[build]
  command = "npm run build"
  publish = "build/static"
  functions = "build"

[[redirects]]
  from = "/static/*"
  to = "/:splat"
  status = 200

[[redirects]]
  from = "/*"
  to = "/.netlify/functions/server"
  status = 200

The server.js lambda is taking care of all the urls with the /* redirection:
https://flamboyant-euclid-90b896.netlify.com/
https://flamboyant-euclid-90b896.netlify.com/some-blog-post
https://flamboyant-euclid-90b896.netlify.com/category/some-category

And the static files are properly served with the other /static/* redirection.
https://flamboyant-euclid-90b896.netlify.com/static/static.js

For some reason adding [[headers]] to /* doesn’t work for lambdas:

[[headers]]
  for = "/*"
  [headers.values]
    X-Custom-Toml = "Toml"

The _headers file is also not working for lamdbas:

/*
  X-Custom-Root: Root

Both of these work fine for the static file. I don’t know if I’m doing something wrong or that is a bug.

They only work if they are returned by the lambda:

exports.handler = (event, context, callback) => {
  setTimeout(() => {
    callback(null, {
      statusCode: 200,
      body: `Dynamic page. Path: ${event.path}. Random: ${Math.random()}`,
      headers: {
        "Cache-Control": "public, s-maxage=15, stale-while-revalidate=300"
      }
    });
  }, 3000);
};

This lambda simulates a SSR request with a setTimeout of 3 seconds.

The CDN works, and it is honoring the s-maxage directive. It serves the cached version for 15 seconds.

But the CDN is ignoring the state-while-revalidate directive. After 15 seconds, it doesn’t serve a the stale version of the cache, it serves a new asset, making the user wait 3 seconds again.

Is that a bug? Should I open an issue somewhere?

you cannot set custom headers via netlify.toml or_headers for functions; your function needs to return the headers itself, instead, as you’ve discovered. That is operating as expected.

I think you need to rely on max-age/public rather than stale-while-revalidate to achieve your goals; I don’t know what we intend to do with stale-while-revalidate for functions or if our proxy (which connects the function to a web request) handles it correctly, but since your experiment indicates no, I wouldn’t count on it (or on that changing anytime soon).

I’ll review with our operations team next week whether they feel like it is a bug or just intended not to work and follow up here if we come up with anything interesting.

The guys from Zeit call it serverless pre-rendering although we’ve already been doing this for years for our clients with KeyCDN. Most CDN’s support the state-while-revalidate directive as far as I know.

The mechanism is simple:

  • The first time a URL is visited, the CDN requests the static HTML to the serverless function, serves the file and saves it in disk/memory for later use. This is the normal behaviour of any CDN, of course.
  • The second time a URL is visited, CDN serves the static file. Still normal.
  • The change is here: once the file is stale (controlled by the s-maxage directive), if the state-while-revalidate directive is used, the CDN returns the stale static HTML file to the user. After that, it requests a fresh one in the background to the serverless function. It then overwrites the stale one in its disk/memory.
  • The next user who visits the site gets the fresh static file from the CDN.

The time-to-stale is controlled by s-maxage directive and it can be 1 second (always check for a fresh file in the background) or any other time, for example 15 minutes. That obviously depends on the site needs. It can be further optimized setting s-maxage to the max and using manual cache invalidation, usually with a soft-purge hook of the CDN API that marks all the files as stale.

As you can see, the approach is similar to the static-generated site, but it has some benefits for medium/large publisher sites with thousands/tens of thousands posts: the static HTML files are generated on demand and over time. The static HTML files most used are generated first and served instantly, while the rest of the static HTML files are generated over time, if ever. And the final user always gets static files from the CDN (but the very first time).

It’d be amazing to be able to use this approach with Netlify :slight_smile:

@philhawksworth very impressive!
image

2 Likes