


Static First: Pre-Generated JAMstack Sites with Serverless Rendering as a Fallback
JAMstack architecture is gaining increasing attention and it provides an efficient method of building a website.
One of the core principles of JAMstack is pre-rendering. This means generating a series of static resources in advance to enable the service of visitors from CDNs or other optimized static hosting environments at the fastest speed and lowest overhead.
But if we were to pre-generate websites in advance, how could we make them look more dynamic? How to build a website that needs frequent changes? How to handle user-generated content?
In fact, this is the ideal application scenario for serverless functions. JAMstack and serverless are the best partners, they complement each other perfectly.
This article will explore a pattern: in a website that consists almost entirely of user-generated content, using serverless functions as a backup solution for pre-generated pages. We will adopt an optimistic URL routing technique where the 404 page is a serverless function to dynamically add serverless rendering.
Sounds complicated? Maybe. But does it work? Absolutely effective!
You can try the demo website to learn about this use case. However, please try it after reading this article.
You are back? Great, let's dive into it.
The philosophy of this sample website is: let you create a warm message and virtual encouragement message to send to friends. You can write a message, customize the lollipop (or popsicle, for my American friends) and get a URL to share with your recipients. That's it, you've illuminated their day. What better than this?
Traditionally, we would use some server-side scripts to process form submissions, add new lollipops (our user-generated content) to the database and generate a unique URL. We then use more server-side logic to parse requests to these pages, query the database to get the data needed to populate the page view, render with the appropriate template, and return it to the user.
This seems reasonable.
But how much does it cost to expand?
Technical architects and technical supervisors often encounter this problem when evaluating the scope of a project. They need to plan, pay and allocate enough resources to cope with successful situations.
This virtual lollipop website is not an ordinary decoration. Since we all want to send each other positive messages, this site will make me a billionaire! As the news spreads, traffic levels will soar. I'd better have a good strategy to ensure that the server can handle heavy loads. I might add some cache tiers, some load balancers, and I would design my database and database servers so I could share the load without being overwhelmed by the need to create and provide all of these lollipops.
But…I don't know how to do these things.
And I don't know how much it costs to add these infrastructures and keep them running. This is complicated.
That's why I like to simplify my hosting as much as possible with pre-rendering.
Providing static pages is much simpler and less expensive than providing pages dynamically from a web server that requires some logic to generate views for each visitor on demand.
Since we are working on a lot of user-generated content, it still makes sense to use the database, but I won't manage it myself. Instead, I'll choose one of many database options that can be used as a service. I'll interact with it through its API.
I might choose Firebase, MongoDB, or any other number of databases. Chris compiled some of these resources on a great website about serverless resources, which is well worth exploring.
In this case, I choose Fauna as my datastore. Fauna provides a great API for storing and querying data. It is a non-SQL style data store, and it is exactly what I need.
Crucially, Fauna has made it a complete business to provide database services. They have deep domain knowledge that I will never have. By using a database-as-a-service provider like this, I inherited an expert data services team for my project , including high availability infrastructure, capacity and compliance security, skilled support engineers, and extensive documentation.
Rather than do it yourself, use such a third-party service, which is its strengths.
Architecture TL;DR
When dealing with proof of concept, I often find myself graffitiing some logical flows. Here is the doodle I made for this website:
And some explanations:
- Users create a new lollipop by filling in a normal HTML form.
- New content is saved in the database, and its submission triggers new site generation and deployment.
- Once the site is deployed, the new lollipop will be accessible via a unique URL. It will be a static page that is quickly served from the CDN without relying on database queries or servers.
- No new lollipop will be accessible as a static page until site generation is complete. An unsuccessful request to the lollipop page will fall back to a page that dynamically generates the lollipop page through the dynamic query database API.
This approach first assumes static/pre-generated resources and then falls back to dynamic rendering when the static view is unavailable, which is called "static first", as described by Unilever's Markus Schork, which I like the statement.
More detailed description
You can dig directly into the website's code (it's open source and you can explore as much as you like), or we can discuss it further.
Do you want to dig deeper and explore the implementation of this example? OK, I will explain in more detail:
- Get data from the database to generate each page
- Publish data to database API using serverless functions
- Trigger a complete site regeneration
- Render on demand when pages have not been generated
Generate pages from database
Later, we will discuss how to publish data to the database, but first, let's assume there are already some entries in the database. We are going to generate a website that contains the pages for each entry.
Static website generators are very good at this. They process the data, apply it to the template, and output HTML files ready to serve. We can use any generator for this example. I chose Eleventy because it is relatively simple and site generation is fast.
To provide Eleventy with some data, we have many options. One way is to provide some JavaScript that returns structured data. This is great for querying database APIs.
Our Eleventy data file will look like this:
<code>// 设置与Fauna 数据库的连接。 // 使用环境变量进行身份验证// 并访问数据库。 const faunadb = require('faunadb'); const q = faunadb.query; const client = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET }); module.exports = () => { return new Promise((resolve, reject) => { // 获取最新的100,000 个条目(为了我们的示例) client.query( q.Paginate(q.Match(q.Ref("indexes/all_lollies")),{size:100000}) ).then((response) => { // 获取每个条目的所有数据const lollies = response.data; const getAllDataQuery = lollies.map((ref) => { return q.Get(ref); }); return client.query(getAllDataQuery).then((ret) => { // 将数据发送回Eleventy 以用于站点构建resolve(ret); }); }).catch((error) => { console.log("error", error); reject(error); }); }) }</code>
I named this file lollies.js and it will make all the data it returns available for Eleventy in a collection named lollies .
We can now use that data in our templates. If you want to view the code that takes that data and generates a page for each project, you can view it in the code repository.
Submit and store data without a server
When we create a new lollipop page, we need to capture the user content into the database so that we can use it to populate the page of the given URL in the future. To do this, we use traditional HTML forms to publish data to the appropriate form handler.
The form looks like this (or see the full code in the repository):
<code></code>
does not have a web server in our hosting scheme, so we need to design a place to handle HTTP posting requests submitted from this form. This is the perfect use case for serverless functions. I'm using Netlify Functions for this. You can use AWS Lambda, Google Cloud, or Azure Functions if you prefer, but I like the simplicity of the Netlify Functions workflow, and the fact that it keeps my serverless API and my UI in the same code repository.
It is a good habit to avoid leaking backend implementation details to the frontend. Clear separation helps make things easier to transplant and tidy. Check out the action properties of the form element above. It publishes the data to a path called /new on my website, which doesn't really hint at which service it will communicate with.
We can route it to any service we like using redirects. I'm sending it to the serverless function I'll configure in this project, but it can be easily customized to send data somewhere else if we want. Netlify provides us with a simple and highly optimized redirection engine that directs our traffic at the CDN level so users can route to the right location very quickly.
The following redirect rule (located in the netlify.toml file of my project) proxies the request for /new to a serverless function called newLolly.js hosted by Netlify Functions.
<code># 将“new”URL 解析为函数[[redirects]] from = "/new" to = "/.netlify/functions/newLolly" status = 200</code>
Let's look at that serverless function:
- Store new data into the database,
- Create a new URL for the new page and
- Redirect users to the newly created page so they can see the results.
First, we will need various utilities to parse form data, connect to the Fauna database and create a short, easy-to-read ID for the new lollipop.
<code>const faunadb = require('faunadb'); // 用于访问FaunaDB const shortid = require('shortid'); // 生成短唯一URL const querystring = require('querystring'); // 帮助我们解析表单数据// 首先,我们使用我们的数据库设置一个新的连接。 // 环境变量帮助我们安全地连接// 到正确的数据库。 const q = faunadb.query const client = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET })</code>
Now, we will add some code to the request to handle serverless functions. The handler function will parse the request to get the required data from the form submission, then generate a unique ID for the new lollipop, and then create it into the database as a new record.
<code>// 处理对无服务器函数的请求exports.handler = (event, context, callback) => { // 获取表单数据const data = querystring.parse(event.body); // 添加一个唯一的路径ID。并记下它- 我们稍后会将用户发送到它const uniquePath = shortid.generate(); data.lollyPath = uniquePath; // 组装准备发送到数据库的数据const lolly = { data: data }; // 在fauna db 中创建棒棒糖条目client.query(q.Create(q.Ref('classes/lollies'), lolly)) .then((response) => { // 成功!将用户重定向到此新棒棒糖页面的唯一URL return callback(null, { statusCode: 302, headers: { Location: `/lolly/${uniquePath}`, } }); }).catch((error) => { console.log('error', error); // 错误!返回带有statusCode 400 的错误return callback(null, { statusCode: 400, body: JSON.stringify(error) }); }); }</code>
Let's check our progress. We have a way to create new lollipop pages in the database. We also have an automatic build that generates a page for each of our lollipops.
To ensure that there is a complete set of pre-generated pages for each lollipop, we should trigger the rebuild every time a new entry is successfully added to the database. This is very easy to do. Thanks to our static website generator, our build has been automated. We only need one way to trigger it. With Netlify, we can define any number of build hooks. They are webhooks and if they receive HTTP POST requests, they will rebuild and deploy our site. This is one I created in Netlify's site management console:
To regenerate the site, including the pages of each lollipop recorded in the database, we can issue an HTTP POST request to this build hook immediately after saving the new data to the database.
Here is the code that does this:
<code>const axios = require('axios'); // 简化发出HTTP POST 请求// 触发新的构建以永久冻结此棒棒糖axios.post('https://api.netlify.com/build_hooks/5d46fa20da4a1b70XXXXXXXXX') .then(function (response) { // 在无服务器函数的日志中报告console.log(response); }) .catch(function (error) { // 描述无服务器函数日志中的任何错误console.log(error); });</code>
You can see it in the full code, which has been added to the successful handler for database insertion.
This is all good if we are willing to wait for the build and deployment to complete before we share the URL of the new lollipop with the recipient. But we are not patient and we immediately want to share it when we get a new URL for the lollipop we just created.
Unfortunately, if we access the URL before the site completes the regeneration to include the new page, we will get a 404. But it's a pleasure to take advantage of this 404.
Optimistic URL routing and serverless back-up scenarios
Using a custom 404 routing, we have the option to send each failed request to the lollipop page to a page that can directly look up lollipop data in the database. We can do this in client JavaScript if we want, but a better way is to dynamically generate a page ready to view from the serverless function.
The method is as follows:
First, we need to tell all requests that want to access the lollipop page (these requests return empty) to go to our serverless function instead. We do this by adding another rule to the Netlify redirect configuration:
<code># 未找到的棒棒糖应该直接代理到API [[redirects]] from = "/lolly/*" to = "/.netlify/functions/showLolly?id=:splat" status = 302</code>
This rule is applied only if the request for the lollipop page does not find a static page ready to serve. It creates a temporary redirect (HTTP 302) to our serverless function, which looks like this:
<code>const faunadb = require('faunadb'); // 用于访问FaunaDB const pageTemplate = require('./lollyTemplate.js'); // JS 模板文字// 设置和授权Fauna DB 客户端const q = faunadb.query; const client = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET }); exports.handler = (event, context, callback) => { // 从请求中获取棒棒糖ID const path = event.queryStringParameters.id.replace("/", ""); // 在DB 中查找棒棒糖数据client.query( q.Get(q.Match(q.Index("lolly_by_path"), path)) ).then((response) => { // 如果找到,则返回视图return callback(null, { statusCode: 200, body: pageTemplate(response.data) }); }).catch((error) => { // 未找到或发生错误,将悲伤的用户发送到通用错误页面console.log('Error:', error); return callback(null, { body: JSON.stringify(error), statusCode: 301, headers: { Location: `/melted/index.html`, } }); }); }</code>
If a request for any other page (not inside the /lolly/ path of the site) returns a 404, we will not send that request to our serverless function to check for lollipops. We can send users directly to the 404 page. Our netlify.toml configuration allows us to define any number of 404 routing levels by adding more fallback rules to the file. The first successful match in the file will be adopted.
<code># 未找到的棒棒糖应该直接代理到API [[redirects]] from = "/lolly/*" to = "/.netlify/functions/showLolly?id=:splat" status = 302 # 真正的404 可以直接转到这里: [[redirects]] from = "/*" to = "/melted/index.html" status = 404</code>
We're done! We now have a "static first" site that will try to render content dynamically using serverless functions if the URL has not been generated with a static file.
Very fast!
Supports larger scale
The technique of triggering a build to regenerate lollipop pages every time a new entry is created may not always be the best. While automation of builds means redeployment of sites is very simple, we may want to start limiting and optimizing things when we start to become very popular. (It's just a matter of time, right?)
It doesn't matter. Here are some things to consider when we are creating a lot of pages and adding content more frequently in the database:
- Instead of triggering a rebuild for each new entry, we can rebuild the site into a scheduled job. Maybe this can happen hourly or once a day.
- If built once a day, we may decide to generate pages only for new lollipops submitted in the past day and cache the pages generated every day for future use. This logic in the build will help us support a large number of lollipop pages without making the build too long. But I won't talk about in-build cache here. If you are curious, you can ask in the Netlify Community Forum.
By combining static pre-generated resources with serverless back-up solutions that provide dynamic rendering, we can meet a surprisingly wide range of use cases – while avoiding the need to configure and maintain a large number of dynamic infrastructure.
What other use cases can you use this "static first" approach to meet?
The above is the detailed content of Static First: Pre-Generated JAMstack Sites with Serverless Rendering as a Fallback. For more information, please follow other related articles on the PHP Chinese website!

Hot AI Tools

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Undress AI Tool
Undress images for free

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Chinese version
Chinese version, very easy to use

Zend Studio 13.0.1
Powerful PHP integrated development environment

Dreamweaver CS6
Visual web development tools

SublimeText3 Mac version
God-level code editing software (SublimeText3)

Hot Topics



It's out! Congrats to the Vue team for getting it done, I know it was a massive effort and a long time coming. All new docs, as well.

With the recent climb of Bitcoin’s price over 20k $USD, and to it recently breaking 30k, I thought it’s worth taking a deep dive back into creating Ethereum

I had someone write in with this very legit question. Lea just blogged about how you can get valid CSS properties themselves from the browser. That's like this.

I'd say "website" fits better than "mobile app" but I like this framing from Max Lynch:

The other day, I spotted this particularly lovely bit from Corey Ginnivan’s website where a collection of cards stack on top of one another as you scroll.

If we need to show documentation to the user directly in the WordPress editor, what is the best way to do it?

There are a number of these desktop apps where the goal is showing your site at different dimensions all at the same time. So you can, for example, be writing

Questions about purple slash areas in Flex layouts When using Flex layouts, you may encounter some confusing phenomena, such as in the developer tools (d...
