The erlang.org website
See the codeThis is the repository for the erlang.org website.
We use ruby w/ jekyll, nodejs w/ bootstrap 5, and Erlang to build this website.
To launch a local copy, install the correct prerequisites and do make serve and navigate to http://localhost:4000.
You can view the deployed version here: https://www.erlang.org
The makefile supports these targets
_site for exportingThis project can be run as a vscode devcontainer and/or in gitpod.io.
To work with this project in gitpod go to: https://gitpod.io/#https://github.com/erlang/erlang-org/
For instructions on how to run with vscode devcontainers see: https://code.visualstudio.com/docs/remote/containers
Most pages are either html or markdown pages so they can be edited directly. They
are located in the at the same place as the URL. So, for instance, the /about URL
is implemented by /about.md and /community/euc is implemented in
/community/euc.
There are three major collections that you can add new items to: News, Blog and Release. Each of these are markdown files found in _news, _posts and _releases respectively. There is a README file in each of those folders that describe the mandatory front matter for each item.
The markdown dialect used is github flavored markdown.
There are also two yaml data files that contain the documentation and community links.
When doing make setup the auto-generated content is created. All auto-generated
content is cached on github in order to speed up the netlify build.
This is placed under _eeps.
Clone https://github.com/erlang/eep then parse using format-eeps.erl. We do not use the perl markdown formatter for EEPs as the html produced does not look very nice.
This is placed under faq.
Clone https://github.com/matthiasl/Erlang-FAQ and then build it.
This is placed under _data/release.json and _patches.
We fetch the latest otp_versions.table and from there use the Github API and use erlang.org rsync to fetch information about each patch released since OTP-17.0.
_patches/tickets.json maps every OTP-, PR- and GH- id mentioned in a release's notes to the releases
that mention it, which is what lets the version tree be searched by ticket. It is written here,
from the parsed readmes, rather than derived later from the generated pages: the ids are already structured at
that point, and recovering them from our own rendered YAML would be fragile.
The files in _patches and _data/release.json contain a lot of duplicate information. We could have kept the
_data/release.json as the only place to keep the data, but we didn't as doing lookups in it turned out to
be too slow for jekyll.
This is placed under assets/otp-versions.json and _versions, and drives the
version tree page. The generator is create-versions.erl; the page is
otp-versions.ts and otp-versions.scss.
OTP-* git tag dates, read through the Github GraphQL API.
Not the GitHub release dates: tags go back to 2014 while releases only start at OTP 21, and 86 of the older
ones were backfilled later with the date of the backfill rather than of the release (every OTP 21.0.x release
is stamped 2020-09-25). One GraphQL page per 100 tags, against ~570 REST calls.openvex.table from the openvex branch fills in the advisories whose CVE record does not describe
releases, and is the only source for the bundled-component assessments.tickets.json out of the patches cache, copied to
assets/otp-tickets.json and fetched separately by the page._versions would mean having _patches in place first, which is an ordering to get wrong under make -j
and a staleness to reason about on every build. As separate files each cache refreshes on its own and the
page joins the latest of both. Only the releases that introduce a ticket are recorded; everything ordered
above one of them contains it, which the page works out.openvex.table still lists CVE-2025-58050 against
the bundled PCRE2 as under_investigation while the same CVE already has a fixed advisory against erts.
The page suppresses the assessment for that id and major, or it would report one vulnerability as both
open and unassessed.latest - 3, per SECURITY.md. It also decides which releases are expanded by
default, so the page opens on what still receives updates and follows the policy on its own.27.3.4.16 and 26.2.5.21 both carry it and both are recent.Generating needs Erlang and a few hundred GitHub API calls, neither of which the Netlify build has, so the
result is cached in the _versions branch the same way _patches is, and refreshed by
update-gh-cache.yaml. Two differences from _patches, both because of that constraint: a production build
never regenerates (JEKYLL_ENV=production, as docs does) so a stale cache cannot fail the site build, and
make versions builds into a temporary directory and swaps, so a failed run cannot leave the cache empty.
assets/otp-versions.json is copied out of the cache rather than symlinked into it as
_data/releases.json is: jekyll follows a symlinked directory but copies a symlinked file as the link
itself, which dangles in _site. The page's class names are partly built at runtime, so /^otpv-/ and
/^sev-/ are on the purgecss safelist.
The other caches are refreshed by a push to master or by erlang/otp dispatching
update-gh-cache.yaml, which is enough for them: they change when a release
happens, and a release is what does the dispatching. The version data is not like
that. Most of what the page says about an advisory arrives after the release
that fixed it -- the advisory is published later, and the CVE record behind it is
enriched with a score, a CWE and its affected ranges later still. Neither event
can start a workflow: GitHub Actions has no repository-advisory trigger, and the
CVE records come from MITRE, which gives no signal at all.
So update-versions-cache.yaml polls, every six hours. It builds only this cache
-- installing erlang alone, since a full asdf install would compile ruby from
source four times a day for nothing -- and then answers two questions separately.
The branch is redeployed when anything differs, including the marker file that
records which generator built it; the site is only rebuilt when
otp-versions.json itself differs, so a quiet week does not rebuild erlang.org
twenty-eight times to publish nothing.
Both workflows force-push the cache branches, so they share one concurrency
group named for those branches rather than taking one each. Without it a poll and
a full cache update can overlap, and the one that finishes second overwrites the
other -- normally harmless, since the same inputs give a byte-identical file, but
not if an advisory is published in between. The full update supersedes a running
poll, which has deployed nothing until its last two steps; a poll queues behind a
running full update and then usually finds nothing left to do.
Adding a trigger on the openvex branch in erlang/otp would cut the latency for
that one source, but not for the others, and openvex is the fallback rather than
the index. The poll is what actually closes the gap.
make test runs both suites; each can be run on its own while working.
npm test — the version scheme, in otp-version-scheme.test.ts. It is kept apart from the page in
otp-version-scheme.ts so the rules can be exercised without a browser: node --experimental-strip-types
runs the TypeScript directly, so there is nothing to install and no build step between the source and the
test. The cases are the ones in the Version Scheme documentation, so the page can be checked against the
document it claims to implement.make -C _scripts test — the generator, in create-versions_tests.erl. What it pins down is the reading
of the sources rather than the fetching: a CVE record that bounds versions it knows nothing about as
unknown, a CWE description that repeats its own id, the two shapes an affected range comes in, an
openvex purl with a doubled slash. Each of those has been wrong at least once. The tests live in test/
so that editing them does not change VERSIONS_DEPS and force the cache to be regenerated.This is placed under docs.
The latest documentation for each release since OTP-17 is downloaded+flattened and put into the docs folder.
The documentation is not built from scratch but rather fetched from github releases or erlang.org.
The documentation in docs/doc is modified to have the algolia search functionality inserted into it.
We have an agreement with algolia that they run a scraper that goes through our documentation and provides search results from that. We use a customized [Algolia Crawler] that crawls the documentation at www.erlang.org/doc once every week.
For the search widget we use docsearch v3, which is a small react widget.
At the moment there is (as far as I know) no good way for a anybody else to optimize the search results as the crawler and index config is inside my (@garazdawi) account and not available outside. So if you want to attempt to make the search better results, you should contact me and we'll have to work together to improve things.
Getting good results from the search is hard, so maybe we should
implement a way to make sure that lists:map is recognized as a module
and function. However, our react skills are not there yet so this will
have to do for now.
You need to have the following tools installed to build the erlang.org site:
Most likely others versions of these tools will work, but they have not been tested.
If you want to be sure that you use the correct version of the dependencies you can either use the devcontainer or asdf.
To install and use asdf follow their Getting started guide. On Linux using bash in a nutshell you do this:
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.15.0
echo ". $HOME/.asdf/asdf.sh" >> ~/.bashrc
echo ". $HOME/.asdf/completions/asdf.bash" >> ~/.bashrc
. ~/.bashrc
asdf plugin add erlang
asdf plugin add nodejs
asdf plugin add ruby
If you have any issues please refer to the asdf documentation.
erlang.org uses a combination of CSS Grid layout and Bootstrap 5 Grid layout. The goal is to use CSS Grid for all responsive layouts and then use Bootstrap Grid for all the non-responsive things.
Using CSS Grid to do the responsive layout instead of Bootstrap
removes a lot of extra divs and order classes that are needed
otherwise. However, it removes the layout from the html, so it
sometimes becomes less obvious what is going on.
The HTML for a normal page looks something like this:
<body>
<header class="container header">
<nav></nav>
</header>
<div class="container body">
<aside class="sidebar"></aside>
<main class="main">
<div class="top"></div>
<div class="content"></div>
</main>
</div>
<footer class="container footer"></footer>
</body>
In the above the container class is part of bootstrap and is used for
styling and then we use CSS grid to place the content of the body and
main classes responsively:
@include media-breakpoint-up(lg) {
.body {
display: grid;
/* 2 columns on > lg screens */
grid-template-columns: 1fr auto;
}
.main {
display: grid;
}
}
@include media-breakpoint-down(lg) {
.body {
display: grid;
/* Hide the sidebar on small screens */
.sidebar {
display: none;
}
}
.main {
display: grid;
}
}
Bootstrap 5 comes with a lot of css entities built in. You should have a look around in the bootstrap docs to see what you can use.
If you want to change the color of a specific component there is a list of the sass variable that you need to change in the specific page. For example if you want to change the font-size in badges you can lookup the variable here: https://getbootstrap.com/docs/5.0/components/badge/#sass. And then set $badge-font-size: 0.80em in _variables.scss.
A full list of all the variables can be found in node_modules/bootstrap/scss/_variables.scss.
You can of course also create your own styles, but we try to stay with the bootstrap styles as much as possible.
/docs
/community
HTML
38.2%
PostScript
16.0%
TypeScript
12.1%
Erlang
11.6%
SCSS
7.2%
Shell
6.8%
JavaScript
4.7%
Perl
1.5%
Makefile
1.1%
The erlang.org website
See the codeThis is the repository for the erlang.org website.
We use ruby w/ jekyll, nodejs w/ bootstrap 5, and Erlang to build this website.
To launch a local copy, install the correct prerequisites and do make serve and navigate to http://localhost:4000.
You can view the deployed version here: https://www.erlang.org
The makefile supports these targets
_site for exportingThis project can be run as a vscode devcontainer and/or in gitpod.io.
To work with this project in gitpod go to: https://gitpod.io/#https://github.com/erlang/erlang-org/
For instructions on how to run with vscode devcontainers see: https://code.visualstudio.com/docs/remote/containers
Most pages are either html or markdown pages so they can be edited directly. They
are located in the at the same place as the URL. So, for instance, the /about URL
is implemented by /about.md and /community/euc is implemented in
/community/euc.
There are three major collections that you can add new items to: News, Blog and Release. Each of these are markdown files found in _news, _posts and _releases respectively. There is a README file in each of those folders that describe the mandatory front matter for each item.
The markdown dialect used is github flavored markdown.
There are also two yaml data files that contain the documentation and community links.
When doing make setup the auto-generated content is created. All auto-generated
content is cached on github in order to speed up the netlify build.
This is placed under _eeps.
Clone https://github.com/erlang/eep then parse using format-eeps.erl. We do not use the perl markdown formatter for EEPs as the html produced does not look very nice.
This is placed under faq.
Clone https://github.com/matthiasl/Erlang-FAQ and then build it.
This is placed under _data/release.json and _patches.
We fetch the latest otp_versions.table and from there use the Github API and use erlang.org rsync to fetch information about each patch released since OTP-17.0.
_patches/tickets.json maps every OTP-, PR- and GH- id mentioned in a release's notes to the releases
that mention it, which is what lets the version tree be searched by ticket. It is written here,
from the parsed readmes, rather than derived later from the generated pages: the ids are already structured at
that point, and recovering them from our own rendered YAML would be fragile.
The files in _patches and _data/release.json contain a lot of duplicate information. We could have kept the
_data/release.json as the only place to keep the data, but we didn't as doing lookups in it turned out to
be too slow for jekyll.
This is placed under assets/otp-versions.json and _versions, and drives the
version tree page. The generator is create-versions.erl; the page is
otp-versions.ts and otp-versions.scss.
OTP-* git tag dates, read through the Github GraphQL API.
Not the GitHub release dates: tags go back to 2014 while releases only start at OTP 21, and 86 of the older
ones were backfilled later with the date of the backfill rather than of the release (every OTP 21.0.x release
is stamped 2020-09-25). One GraphQL page per 100 tags, against ~570 REST calls.openvex.table from the openvex branch fills in the advisories whose CVE record does not describe
releases, and is the only source for the bundled-component assessments.tickets.json out of the patches cache, copied to
assets/otp-tickets.json and fetched separately by the page._versions would mean having _patches in place first, which is an ordering to get wrong under make -j
and a staleness to reason about on every build. As separate files each cache refreshes on its own and the
page joins the latest of both. Only the releases that introduce a ticket are recorded; everything ordered
above one of them contains it, which the page works out.openvex.table still lists CVE-2025-58050 against
the bundled PCRE2 as under_investigation while the same CVE already has a fixed advisory against erts.
The page suppresses the assessment for that id and major, or it would report one vulnerability as both
open and unassessed.latest - 3, per SECURITY.md. It also decides which releases are expanded by
default, so the page opens on what still receives updates and follows the policy on its own.27.3.4.16 and 26.2.5.21 both carry it and both are recent.Generating needs Erlang and a few hundred GitHub API calls, neither of which the Netlify build has, so the
result is cached in the _versions branch the same way _patches is, and refreshed by
update-gh-cache.yaml. Two differences from _patches, both because of that constraint: a production build
never regenerates (JEKYLL_ENV=production, as docs does) so a stale cache cannot fail the site build, and
make versions builds into a temporary directory and swaps, so a failed run cannot leave the cache empty.
assets/otp-versions.json is copied out of the cache rather than symlinked into it as
_data/releases.json is: jekyll follows a symlinked directory but copies a symlinked file as the link
itself, which dangles in _site. The page's class names are partly built at runtime, so /^otpv-/ and
/^sev-/ are on the purgecss safelist.
The other caches are refreshed by a push to master or by erlang/otp dispatching
update-gh-cache.yaml, which is enough for them: they change when a release
happens, and a release is what does the dispatching. The version data is not like
that. Most of what the page says about an advisory arrives after the release
that fixed it -- the advisory is published later, and the CVE record behind it is
enriched with a score, a CWE and its affected ranges later still. Neither event
can start a workflow: GitHub Actions has no repository-advisory trigger, and the
CVE records come from MITRE, which gives no signal at all.
So update-versions-cache.yaml polls, every six hours. It builds only this cache
-- installing erlang alone, since a full asdf install would compile ruby from
source four times a day for nothing -- and then answers two questions separately.
The branch is redeployed when anything differs, including the marker file that
records which generator built it; the site is only rebuilt when
otp-versions.json itself differs, so a quiet week does not rebuild erlang.org
twenty-eight times to publish nothing.
Both workflows force-push the cache branches, so they share one concurrency
group named for those branches rather than taking one each. Without it a poll and
a full cache update can overlap, and the one that finishes second overwrites the
other -- normally harmless, since the same inputs give a byte-identical file, but
not if an advisory is published in between. The full update supersedes a running
poll, which has deployed nothing until its last two steps; a poll queues behind a
running full update and then usually finds nothing left to do.
Adding a trigger on the openvex branch in erlang/otp would cut the latency for
that one source, but not for the others, and openvex is the fallback rather than
the index. The poll is what actually closes the gap.
make test runs both suites; each can be run on its own while working.
npm test — the version scheme, in otp-version-scheme.test.ts. It is kept apart from the page in
otp-version-scheme.ts so the rules can be exercised without a browser: node --experimental-strip-types
runs the TypeScript directly, so there is nothing to install and no build step between the source and the
test. The cases are the ones in the Version Scheme documentation, so the page can be checked against the
document it claims to implement.make -C _scripts test — the generator, in create-versions_tests.erl. What it pins down is the reading
of the sources rather than the fetching: a CVE record that bounds versions it knows nothing about as
unknown, a CWE description that repeats its own id, the two shapes an affected range comes in, an
openvex purl with a doubled slash. Each of those has been wrong at least once. The tests live in test/
so that editing them does not change VERSIONS_DEPS and force the cache to be regenerated.This is placed under docs.
The latest documentation for each release since OTP-17 is downloaded+flattened and put into the docs folder.
The documentation is not built from scratch but rather fetched from github releases or erlang.org.
The documentation in docs/doc is modified to have the algolia search functionality inserted into it.
We have an agreement with algolia that they run a scraper that goes through our documentation and provides search results from that. We use a customized [Algolia Crawler] that crawls the documentation at www.erlang.org/doc once every week.
For the search widget we use docsearch v3, which is a small react widget.
At the moment there is (as far as I know) no good way for a anybody else to optimize the search results as the crawler and index config is inside my (@garazdawi) account and not available outside. So if you want to attempt to make the search better results, you should contact me and we'll have to work together to improve things.
Getting good results from the search is hard, so maybe we should
implement a way to make sure that lists:map is recognized as a module
and function. However, our react skills are not there yet so this will
have to do for now.
You need to have the following tools installed to build the erlang.org site:
Most likely others versions of these tools will work, but they have not been tested.
If you want to be sure that you use the correct version of the dependencies you can either use the devcontainer or asdf.
To install and use asdf follow their Getting started guide. On Linux using bash in a nutshell you do this:
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.15.0
echo ". $HOME/.asdf/asdf.sh" >> ~/.bashrc
echo ". $HOME/.asdf/completions/asdf.bash" >> ~/.bashrc
. ~/.bashrc
asdf plugin add erlang
asdf plugin add nodejs
asdf plugin add ruby
If you have any issues please refer to the asdf documentation.
erlang.org uses a combination of CSS Grid layout and Bootstrap 5 Grid layout. The goal is to use CSS Grid for all responsive layouts and then use Bootstrap Grid for all the non-responsive things.
Using CSS Grid to do the responsive layout instead of Bootstrap
removes a lot of extra divs and order classes that are needed
otherwise. However, it removes the layout from the html, so it
sometimes becomes less obvious what is going on.
The HTML for a normal page looks something like this:
<body>
<header class="container header">
<nav></nav>
</header>
<div class="container body">
<aside class="sidebar"></aside>
<main class="main">
<div class="top"></div>
<div class="content"></div>
</main>
</div>
<footer class="container footer"></footer>
</body>
In the above the container class is part of bootstrap and is used for
styling and then we use CSS grid to place the content of the body and
main classes responsively:
@include media-breakpoint-up(lg) {
.body {
display: grid;
/* 2 columns on > lg screens */
grid-template-columns: 1fr auto;
}
.main {
display: grid;
}
}
@include media-breakpoint-down(lg) {
.body {
display: grid;
/* Hide the sidebar on small screens */
.sidebar {
display: none;
}
}
.main {
display: grid;
}
}
Bootstrap 5 comes with a lot of css entities built in. You should have a look around in the bootstrap docs to see what you can use.
If you want to change the color of a specific component there is a list of the sass variable that you need to change in the specific page. For example if you want to change the font-size in badges you can lookup the variable here: https://getbootstrap.com/docs/5.0/components/badge/#sass. And then set $badge-font-size: 0.80em in _variables.scss.
A full list of all the variables can be found in node_modules/bootstrap/scss/_variables.scss.
You can of course also create your own styles, but we try to stay with the bootstrap styles as much as possible.
/docs
/community
HTML
38.2%
PostScript
16.0%
TypeScript
12.1%
Erlang
11.6%
SCSS
7.2%
Shell
6.8%
JavaScript
4.7%
Perl
1.5%
Makefile
1.1%