Library / SDK
letsencrypt/website avatar
letsencrypt/website

letsencrypt.org: the static Hugo site behind a certificate authority

Let's Encrypt Website and Documentation

912 stars593 forksHTMLMPL-2.0

At a glance

What is it?
The Let's Encrypt website and documentation are a fully static Hugo site with no server-side code, translated into roughly eighteen languages, with client certificate data regenerated by scripts in the repository. It is a small, tidy static site that most people will only ever read rather than run.
Who is it for?
letsencrypt.org suits a documentation writer, a translator or a contributor who wants to change what the certificate authority says about itself, since the site is a plain Hugo project that runs on a laptop with one command. It does not suit anyone looking for the certificate authority itself, because the ACME implementation, the CA software and the client tooling all live in other projects, and this repository has no GitHub releases and no changelog of its own.
Can I use it commercially?
Yes, with conditions. MPL-2.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly HTML, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The public face of a certificate authority

This repository holds the main Let's Encrypt website, and the project description is simply Let's Encrypt Website and Documentation. The primary language of the repository is HTML, which is itself a clue about how the site is built.

The audience for a CA's website is broader than a developer audience. It has to explain what a certificate is to somebody who has never configured TLS, tell an administrator how to renew without downtime, and hold documentation for clients that will still be in use in five years. That is a different job from running the CA, and the project keeps them apart: the ACME protocol, the ISRG CA software and the client tooling referenced in the repository topics all live elsewhere.

What this repository therefore contains is content, structure and a build. content/ holds the pages, data/ the structured data, i18n/ the translation strings, themes/ the presentation, src/ and static/ the assets and scripts, and config/ the Hugo configuration.

That makes it an unusual thing to review as a project, and worth being explicit about it: adopting this repository means contributing to a website, not deploying an infrastructure component. The interesting questions are about the build, the translation workflow and the data pipeline.

Entirely static, with no server-side code

The README states the site is built with Hugo and that it is entirely static, with no server-side code or scripting.

That single property removes a category of problems and creates another. There is no database to operate, no runtime to patch, and no request-time code to be exploited: the output is files, served by whatever is in front of them. For a project whose subject is transport security, a site with no server-side attack surface is a coherent choice rather than a stylistic one.

The cost is that anything dynamic has to happen at build time or not at all. The repository accommodates this with the data/ directory and a pair of Python scripts, client-data-lastmod.py and client-data-prune.py, which handle the client certificate data used by the site, and with a maintenance script template, _maintenance_update_graph_data.sh.example, for regenerating the graph data.

The static nature also shapes the deployment. netlify.toml is the deployment configuration, and it also has a secondary job: it pins the Hugo version the site expects, which matters for the next section.

Running it locally, and the Hugo version trap

To see changes, the README says to install Hugo Extended and then run the site with the development server:

sh
hugo server -F

You then open http://localhost:1313/ in your browser. The -F flag is worth understanding rather than copying blindly: it shows items to be published in the future, like blog posts with dates in the future. The site's content includes dated posts, so without -F a page that is scheduled for next week simply does not appear, which reads as a broken build if you did not expect it.

There is a troubleshooting section for one specific failure. If you see the error Failed to load translations in file "en.toml": unsupported file extension .toml, the README says your version of Hugo is probably too old and to use the version specified in netlify.toml.

That error message is worth a moment's thought, because it is a good example of a dependency pinned by configuration rather than by a manifest. Hugo gained TOML support at some version, and a site that uses TOML for its translation files simply stops building on an older binary. The version lives in netlify.toml, which is a deployment file, so the pin is easy to miss when you are running the site locally for the first time.

There is also a Docker route. The compose file starts a Hugo image with the repository mounted and port 1313 published:

yaml
services:
  server:
    image: klakegg/hugo:ext-alpine
    command: server
    volumes:
      - ".:/src"
    ports:
      - "1313:1313"

That gives you the same URL, http://localhost:1313/, without installing Hugo at all, which is the lower friction path if you only want to read the site.

Translations need a stub for every language

Internationalisation is the workflow that most shapes a contribution here, and it has a rule that surprises people: when creating new pages you need to add a translation stub for each language. The new-page.sh script does that automatically.

Its usage is:

sh
Usage: ./new-page.sh <page-path> <page title>
Examples:
./new-page.sh my-page "My Page Title"
./new-page.sh post/my-post "My Post Title"

and the sample output shows the files it creates, one per language directory: vi, sv, he, ja, base-l10n, it, ru, zh-cn, uk, sr, zh-tw, pt-br, de, ko, id, fr, es and en. That is eighteen paths, including base-l10n, which is the base stub other translations hang off.

The rule exists because a missing translation in a static multilingual site is not a blank file, it is a 404 or a fallback. By creating every stub at the moment the English page is created, the site never has a page that exists in one language and not in another. The translator then fills in the content later through the normal translation process.

TRANSLATION.md is the document to read for that process, and the repository also carries DOCUMENTATION.md and contributing.md for the wider documentation structure. The .pre-commit-config.yaml at the top of the tree suggests some of these checks also run before a commit is even pushed.

Three tests, and what they are for

If you update JavaScript, CSS or layouts, the README says you can run the tests with:

sh
npm install && npm run build && npm test

The test script in package.json chains three checks. test:htmllint runs htmlhint over the content HTML files. test:eslint runs eslint over the JavaScript in static/js and src/js. test:inline-styles runs a shell script, scripts/check-inline-styles.sh, that looks for inline styles.

The third check is the one that encodes a design rule rather than a correctness rule. A site built with Tailwind and PostCSS wants its styling in the stylesheet rather than in style attributes on elements, and an inline style is both a maintenance problem and a way to bypass the build. The check is a small script, which means it is cheap to understand and cheap to change, and it makes the rule enforceable on every pull request rather than a matter of reviewer discipline.

There is a fourth script that is not part of npm test: test:html5validate runs html5validator over the built public directory, with a blacklist of documents and directories that are known not to validate and an ignore for the aspect-ratio property. It is kept out of the default test run, probably because it needs the built output and takes longer.

The style side of the build is otherwise conventional for a modern static site: Tailwind CSS at version 4 with its CLI, PostCSS with autoprefixer and postcss-import, and a tailwind.config.js and postcss.config.js at the top level.

Licence, maintenance and the shape of the repository

The licence field on the repository says MPL-2.0, and package.json declares the same. The MPL is file-level copyleft: if you modify a file of the site, your modified version of that file stays under the MPL, while the rest of the site and your own separate work do not have to be.

That is a sensible choice for a website whose content is contributed by many people, because it protects the translations and the shared partials from being relicensed while leaving the design system and any downstream work free. The tree carries LICENSE.txt and the LICENSE field in the manifest, plus a CODE_OF_CONDUCT.md, a SECURITY.md, a pull_request_template.md and an .editorconfig.

On maintenance, the repository is not archived and the last push was on 2026-10-19, so the site is being worked on daily. There are no GitHub releases and no changelog, which is normal for a website where every change is deployed rather than versioned, but it does mean that the commit history is the only record of what changed.

The repository layout confirms the division of labour one more time. Besides content, data, i18n, themes and config, there is a scripts/ directory with the shell tooling, a src/ directory, static/ for assets, a themes/ directory, a docker-compose.yml for local serving, client-data-lastmod.py and client-data-prune.py for the client data pipeline, and _maintenance_update_graph_data.sh.example as a template for a maintenance job. In other words, the interesting engineering here is a small build pipeline and a data refresh process, and the bulk of the repository is content.

Editorial conclusion

letsencrypt.org suits a documentation writer, a translator or a contributor who wants to change what the certificate authority says about itself, since the site is a plain Hugo project that runs on a laptop with one command. It does not suit anyone looking for the certificate authority itself, because the ACME implementation, the CA software and the client tooling all live in other projects, and this repository has no GitHub releases and no changelog of its own. Before contributing, read DOCUMENTATION.md and TRANSLATION.md, run hugo server -F with the Hugo version pinned in netlify.toml, and add the translation stubs for every language with new-page.sh rather than creating an English page alone.

Frequently asked questions

How do I run the letsencrypt.org website locally?

Install Hugo Extended and run hugo server -F, then open http://localhost:1313/ in a browser. The -F flag shows items scheduled for future publication, such as dated blog posts. Alternatively the docker-compose.yml starts a Hugo container with the same port published.

Which Hugo version does the Let's Encrypt site need?

The README's troubleshooting section says that the error Failed to load translations in file "en.toml": unsupported file extension .toml means your Hugo version is too old, and to use the version specified in netlify.toml.

How do I add a new page to the Let's Encrypt site?

Use the new-page.sh script with a page path and title, for example ./new-page.sh docs/new-page "My New Page". The script creates a stub for every language, including base-l10n, because a new page needs a translation stub for each language.

What tests run when I change JavaScript, CSS or layouts?

Run npm install and npm run build and npm test. The test script runs htmlhint over the content HTML, eslint over the JavaScript in static/js and src/js, and a shell script that checks for inline styles.

What licence does the Let's Encrypt website use?

The licence field on the repository and the package.json both say MPL-2.0. That is file level copyleft, so a modified file stays under the MPL while the rest of the site and separate downstream work are not affected.

Official sources

  1. Issues
  2. letsencrypt/website on GitHub
  3. License: MPL-2.0
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/letsencrypt-website.svg)](https://hysenlabs.com/projects/letsencrypt-website)

Was this useful?