diff --git a/.gitignore b/.gitignore
index 91128673..48323c98 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,20 @@
**/node_modules/
**/.vitepress/cache/
**/.vitepress/dist/
-articles.json
*.dbg.s
.idea/
+
+.env
+
+# Terraform state files
+terraform.tfstate*
+
+# Terraform plugins dir
+.terraform
+
+# Temporary terraform fs lock file
+.terraform.tfstate.lock.info
+
+# Terraform variables usually contain sensitive information
+terraform.tfvars
+terraform.tfvars.json
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bdba88e9..08726e3e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1 +1 @@
-See the [changelog](https://elastio.github.io/bon/changelog) page on the website for details.
+See the [changelog](https://bon-rs.com/changelog) page on the website for details.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index cf8ad5f3..b717a91c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1 +1 @@
-See the [contributing](https://elastio.github.io/bon/guide/internal/contributing) page on the website for details.
+See the [contributing](https://bon-rs.com/guide/internal/contributing) page on the website for details.
diff --git a/README.md b/README.md
index 31395e86..799b6108 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
-
+
@@ -34,7 +34,7 @@
`bon` is a Rust crate for generating compile-time-checked builders for functions and structs. It also provides idiomatic partial application with optional and named parameters for functions and methods.
-Visit the [guide for a complete overview of the crate](https://elastio.github.io/bon/guide/overview).
+Visit the [guide for a complete overview of the crate](https://bon-rs.com/guide/overview).
## Quick examples
@@ -133,7 +133,7 @@ assert_eq!(user.level, Some(24));
assert!(user.is_admin);
```
-See [the guide](https://elastio.github.io/bon/guide/overview) for the rest.
+See [the guide](https://bon-rs.com/guide/overview) for the rest.
---
diff --git a/bon-cli/Cargo.toml b/bon-cli/Cargo.toml
index 49f43c38..8b757f33 100644
--- a/bon-cli/Cargo.toml
+++ b/bon-cli/Cargo.toml
@@ -5,7 +5,7 @@ version = "0.1.0"
description = "Dev tool for working with the `bon` crate"
edition = "2021"
-homepage = "https://elastio.github.io/bon/"
+homepage = "https://bon-rs.com/"
license = "MIT OR Apache-2.0"
repository = "https://github.com/elastio/bon"
diff --git a/bon-macros/Cargo.toml b/bon-macros/Cargo.toml
index 6f9b6c50..5f8e88da 100644
--- a/bon-macros/Cargo.toml
+++ b/bon-macros/Cargo.toml
@@ -8,7 +8,7 @@ detail of the `bon` crate
"""
edition = "2021"
-homepage = "https://elastio.github.io/bon/"
+homepage = "https://bon-rs.com/"
license = "MIT OR Apache-2.0"
repository = "https://github.com/elastio/bon"
diff --git a/bon-macros/src/builder/builder_gen/mod.rs b/bon-macros/src/builder/builder_gen/mod.rs
index b2e7fdf1..db1b5b11 100644
--- a/bon-macros/src/builder/builder_gen/mod.rs
+++ b/bon-macros/src/builder/builder_gen/mod.rs
@@ -353,7 +353,7 @@ impl BuilderGenCtx {
// The fields can't be hidden using Rust's privacy syntax.
// The details about this are described in the blog post:
- // https://elastio.github.io/bon/blog/the-weird-of-function-local-types-in-rust.
+ // https://bon-rs.com/blog/the-weird-of-function-local-types-in-rust.
//
// We could use `#[cfg(not(rust_analyzer))]` to hide the private fields in IDE.
// However, RA would then not be able to type-check the generated code, which
diff --git a/bon-macros/src/builder/builder_gen/models.rs b/bon-macros/src/builder/builder_gen/models.rs
index 0626a7a9..d2d1f82e 100644
--- a/bon-macros/src/builder/builder_gen/models.rs
+++ b/bon-macros/src/builder/builder_gen/models.rs
@@ -168,7 +168,7 @@ pub(crate) struct BuilderGenCtx {
///
/// This is an unfortunate workaround due to the limitations of defining the
/// builder type inside of a nested module. See more details on this problem in
-///
+///
pub(super) struct PrivateIdentsPool {
pub(super) phantom: syn::Ident,
pub(super) receiver: syn::Ident,
diff --git a/bon-macros/src/lib.rs b/bon-macros/src/lib.rs
index fa92e4ab..21bdfbc9 100644
--- a/bon-macros/src/lib.rs
+++ b/bon-macros/src/lib.rs
@@ -99,8 +99,8 @@ mod tests;
/// or setting the same field twice will be reported as compile-time errors.
///
/// See the full documentation for more details:
-/// - [Guide](https://elastio.github.io/bon/guide/overview)
-/// - [Attributes reference](https://elastio.github.io/bon/reference/builder)
+/// - [Guide](https://bon-rs.com/guide/overview)
+/// - [Attributes reference](https://bon-rs.com/reference/builder)
#[proc_macro_attribute]
pub fn builder(
params: proc_macro::TokenStream,
@@ -142,8 +142,8 @@ pub fn builder(
/// or setting the same field twice will be reported as compile-time errors.
///
/// See the full documentation for more details:
-/// - [Guide](https://elastio.github.io/bon/guide/overview)
-/// - [Attributes reference](https://elastio.github.io/bon/reference/builder)
+/// - [Guide](https://bon-rs.com/guide/overview)
+/// - [Attributes reference](https://bon-rs.com/reference/builder)
#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive_builder(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
builder::generate_from_derive(item.into()).into()
@@ -203,7 +203,7 @@ pub fn derive_builder(item: proc_macro::TokenStream) -> proc_macro::TokenStream
/// or setting the same field twice will be reported as compile-time errors.
///
/// For details on this macro including the reason why it's needed see
-/// [this paragraph in the overview](https://elastio.github.io/bon/guide/overview#builder-for-an-associated-method).
+/// [this paragraph in the overview](https://bon-rs.com/guide/overview#builder-for-an-associated-method).
///
/// [`builder`]: macro@builder
#[proc_macro_attribute]
diff --git a/bon/Cargo.toml b/bon/Cargo.toml
index d3c73d9a..e124004a 100644
--- a/bon/Cargo.toml
+++ b/bon/Cargo.toml
@@ -14,7 +14,7 @@ categories = [
keywords = ["builder", "macro", "derive", "constructor", "setter"]
edition = "2021"
-homepage = "https://elastio.github.io/bon/"
+homepage = "https://bon-rs.com/"
license = "MIT OR Apache-2.0"
repository = "https://github.com/elastio/bon"
diff --git a/bon/src/__/ide.rs b/bon/src/__/ide.rs
index 7d06a6f1..8babac5e 100644
--- a/bon/src/__/ide.rs
+++ b/bon/src/__/ide.rs
@@ -8,87 +8,87 @@
pub mod builder_top_level {
use super::*;
- /// See the docs at
+ /// See the docs at
pub const builder_type: Option = None;
pub mod builder_type {
use super::*;
- /// See the docs at
+ /// See the docs at
pub const name: Identifier = Identifier;
- /// See the docs at
+ /// See the docs at
pub const vis: VisibilityString = VisibilityString;
- /// See the docs at
+ /// See the docs at
pub const doc: DocComments = DocComments;
}
- /// See the docs at
+ /// See the docs at
pub const finish_fn: Option = None;
- /// See the docs at
+ /// See the docs at
pub mod finish_fn {
use super::*;
- /// See the docs at
+ /// See the docs at
pub const name: Identifier = Identifier;
- /// See the docs at
+ /// See the docs at
pub const vis: VisibilityString = VisibilityString;
- /// See the docs at
+ /// See the docs at
pub const doc: DocComments = DocComments;
}
- /// See the docs at
+ /// See the docs at
pub const start_fn: Option = None;
- /// See the docs at
+ /// See the docs at
pub mod start_fn {
use super::*;
- /// See the docs at
+ /// See the docs at
pub const name: Identifier = Identifier;
- /// See the docs at
+ /// See the docs at
pub const vis: VisibilityString = VisibilityString;
- /// See the docs at
+ /// See the docs at
pub const doc: DocComments = DocComments;
}
- /// See the docs at
+ /// See the docs at
pub const state_mod: Option = None;
- /// See the docs at
+ /// See the docs at
pub mod state_mod {
use super::*;
- /// See the docs at
+ /// See the docs at
pub const name: Identifier = Identifier;
- /// See the docs at
+ /// See the docs at
pub const vis: VisibilityString = VisibilityString;
- /// See the docs at
+ /// See the docs at
pub const doc: DocComments = DocComments;
}
- /// See the docs at
+ /// See the docs at
pub mod on {
use super::*;
- /// See the docs at
+ /// See the docs at
pub const into: Flag = Flag;
}
- /// See the docs at
+ /// See the docs at
pub mod derive {
- /// See the docs at
+ /// See the docs at
pub use core::fmt::Debug;
- /// See the docs at
+ /// See the docs at
pub use core::clone::Clone;
}
@@ -96,7 +96,7 @@ pub mod builder_top_level {
/// It's hinted with an underscore due to the limitations of the current
/// completions limitation. This will be fixed in the future.
///
- /// See the docs at
+ /// See the docs at
pub const crate_: Option = None;
}
diff --git a/bon/src/lib.rs b/bon/src/lib.rs
index bfc2a9ed..36f6a57c 100644
--- a/bon/src/lib.rs
+++ b/bon/src/lib.rs
@@ -1,6 +1,6 @@
#![doc(
- html_logo_url = "https://elastio.github.io/bon/bon-logo-thumb.png",
- html_favicon_url = "https://elastio.github.io/bon/bon-logo-medium.png"
+ html_logo_url = "https://bon-rs.com/bon-logo-thumb.png",
+ html_favicon_url = "https://bon-rs.com/bon-logo-medium.png"
)]
#![doc = include_str!("../README.md")]
#![cfg_attr(not(feature = "std"), no_std)]
diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts
index 21790290..0f28f111 100644
--- a/website/.vitepress/config.mts
+++ b/website/.vitepress/config.mts
@@ -2,8 +2,6 @@ import { defineConfig } from "vitepress";
import { abbr } from "@mdit/plugin-abbr";
import * as v1 from "../v1/config.mjs";
-const base = "/bon/";
-
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "Bon",
@@ -24,13 +22,16 @@ export default defineConfig({
},
},
- base,
-
head: [
- ["link", { rel: "icon", href: `${base}bon-logo-thumb.png` }],
+ ["link", { rel: "icon", href: `bon-logo-thumb.png` }],
+ ["meta", { property: "og:image", content: `bon-logo-thumb.png` }],
[
- "meta",
- { property: "og:image", content: `${base}bon-logo-thumb.png` },
+ "script",
+ {
+ defer: "",
+ src: "https://umami.bon-rs.com/script.js",
+ "data-website-id": "10c1ad05-7a6e-49ee-8633-5f8f75de4ab9",
+ },
],
],
@@ -134,7 +135,7 @@ export default defineConfig({
{
text: "Shared Configuration",
link: "/guide/patterns/shared-configuration",
- }
+ },
],
},
{
diff --git a/website/README.md b/website/README.md
index cdebba6b..98ebadd3 100644
--- a/website/README.md
+++ b/website/README.md
@@ -1,6 +1,6 @@
# bon website
-This folder contains the source code and markdown files that comprise the [bon website](https://elastio.github.io/bon/).
+This folder contains the source code and markdown files that comprise the [bon website](https://bon-rs.com/).
The website is built using [VitePress](https://vitepress.dev/). It's a simple and elegant framework for static websites that hides a lot of complexity from you. You don't need to be a TypeScript expert let alone a Vue expert to get around this directory.
diff --git a/website/changelog.md b/website/changelog.md
index 03d829e8..27894415 100644
--- a/website/changelog.md
+++ b/website/changelog.md
@@ -38,16 +38,17 @@ All the breaking changes are very unlikely to actually break your code that was
### Fixed
-- Fixed `#[cfg/cfg_attr()]` not being expanded when used on function arguments with doc comments or other attributes.
+- Fix `#[cfg/cfg_attr()]` not being expanded when used on function arguments with doc comments or other attributes.
### Other
-- Added graceful internal panic handling. If some `bon` macro panics due to an internal bug, the macro will try to still generate a fallback for IDEs to still provide intellisense ([#145](https://github.com/elastio/bon/pull/145))
+- Add graceful internal panic handling. If some `bon` macro panics due to an internal bug, the macro will try to still generate a fallback for IDEs to still provide intellisense ([#145](https://github.com/elastio/bon/pull/145))
+- Switch from `elastio.github.io/bon` to a custom domain `bon-rs.com` ([#158](https://github.com/elastio/bon/pull/158))
## [2.3.0](https://github.com/elastio/bon/compare/v2.2.1...v2.3.0) - 2024-09-14
-See the [blog post for this release](https://elastio.github.io/bon/blog/bon-builder-v2-3-release) that describes some of the most notable changes in detail.
+See the [blog post for this release](https://bon-rs.com/blog/bon-builder-v2-3-release) that describes some of the most notable changes in detail.
### Added
@@ -67,13 +68,13 @@ See the [blog post for this release](https://elastio.github.io/bon/blog/bon-buil
## [2.2.0](https://github.com/elastio/bon/compare/v2.1.1...v2.2.0) - 2024-09-08
-See the [blog post for this release](https://elastio.github.io/bon/blog/bon-builder-v2-2-release) that describes some of the most notable changes in detail.
+See the [blog post for this release](https://bon-rs.com/blog/bon-builder-v2-2-release) that describes some of the most notable changes in detail.
### Changed
-- The `#[bon::builder]` attribute was deprecated on structs. The new [`#[derive(bon::Builder)]`](https://elastio.github.io/bon/reference/builder) should be used to derive a builder from a struct. Starting with `bon` 2.3 (next minor release) all usages of `#[bon::builder]` on structs will generate deprecation warnings. ([#99](https://github.com/elastio/bon/pull/99)).
+- The `#[bon::builder]` attribute was deprecated on structs. The new [`#[derive(bon::Builder)]`](https://bon-rs.com/reference/builder) should be used to derive a builder from a struct. Starting with `bon` 2.3 (next minor release) all usages of `#[bon::builder]` on structs will generate deprecation warnings. ([#99](https://github.com/elastio/bon/pull/99)).
- There is a CLI to assist in migrating to the new syntax. See the [release blog post](https://elastio.github.io/bon/blog/bon-builder-v2-2-release#derive-builder-syntax-for-structs) for details about that.
+ There is a CLI to assist in migrating to the new syntax. See the [release blog post](https://bon-rs.com/blog/bon-builder-v2-2-release#derive-builder-syntax-for-structs) for details about that.
### Added
@@ -100,7 +101,7 @@ See the [blog post for this release](https://elastio.github.io/bon/blog/bon-buil
## [2.1.0](https://github.com/elastio/bon/compare/v2.0.1...v2.1.0) - 2024-09-01
-See the [blog post for this release](https://elastio.github.io/bon/blog/bon-builder-v2-1-release) that describes some of the most notable changes in detail.
+See the [blog post for this release](https://bon-rs.com/blog/bon-builder-v2-1-release) that describes some of the most notable changes in detail.
### Added
@@ -125,8 +126,8 @@ See the [blog post for this release](https://elastio.github.io/bon/blog/bon-buil
### Docs
-- Add a new section ["`None` literals inference"](https://elastio.github.io/bon/guide/patterns/into-conversions-in-depth#none-literals-inference) to docs for "Into Conversions In-Depth"
-- Fix the docs about the comparison of Into conversions on the ["Alternatives"](http://elastio.github.io/bon/guide/alternatives) page that were not updated during the v2 release
+- Add a new section ["`None` literals inference"](https://bon-rs.com/guide/patterns/into-conversions-in-depth#none-literals-inference) to docs for "Into Conversions In-Depth"
+- Fix the docs about the comparison of Into conversions on the ["Alternatives"](http://bon-rs.com/guide/alternatives) page that were not updated during the v2 release
### Fixed
@@ -139,7 +140,7 @@ See the [blog post for this release](https://elastio.github.io/bon/blog/bon-buil
## [2.0.0](https://github.com/elastio/bon/compare/v1.2.1...v2.0.0) - 2024-08-26
-See the [blog post](https://elastio.github.io/bon/blog/bon-builder-generator-v2-release) for details.
+See the [blog post](https://bon-rs.com/blog/bon-builder-generator-v2-release) for details.
## [1.2.1](https://github.com/elastio/bon/compare/v1.2.0...v1.2.1) - 2024-08-12
@@ -216,4 +217,4 @@ See the [blog post](https://elastio.github.io/bon/blog/bon-builder-generator-v2-
### Added
-- Initial release 🎉. See the [`bon` crate overview for details](https://elastio.github.io/bon/guide/overview).
+- Initial release 🎉. See the [`bon` crate overview for details](https://bon-rs.com/guide/overview).
diff --git a/website/infra/.terraform.lock.hcl b/website/infra/.terraform.lock.hcl
new file mode 100644
index 00000000..50ec017a
--- /dev/null
+++ b/website/infra/.terraform.lock.hcl
@@ -0,0 +1,63 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/hashicorp/cloudinit" {
+ version = "2.3.5"
+ constraints = "~> 2.3.5"
+ hashes = [
+ "h1:HCoabXm6NQwCivl1q24+l9VUufc2mFqNeulsQBA9iFg=",
+ "zh:17c20574de8eb925b0091c9b6a4d859e9d6e399cd890b44cfbc028f4f312ac7a",
+ "zh:348664d9a900f7baf7b091cf94d657e4c968b240d31d9e162086724e6afc19d5",
+ "zh:5a876a468ffabff0299f8348e719cb704daf81a4867f8c6892f3c3c4add2c755",
+ "zh:6ef97ee4c8c6a69a3d36746ba5c857cf4f4d78f32aa3d0e1ce68f2ece6a5dba5",
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+ "zh:8283e5a785e3c518a440f6ac6e7cc4fc07fe266bf34974246f4e2ef05762feda",
+ "zh:a44eb5077950168b571b7eb65491246c00f45409110f0f172cc3a7605f19dba9",
+ "zh:aa0806cbff72b49c1b389c0b8e6904586e5259c08dabb7cb5040418568146530",
+ "zh:bec4613c3beaad9a7be7ca99cdb2852073f782355b272892e6ee97a22856aec1",
+ "zh:d7fe368577b6c8d1ae44c751ed42246754c10305c7f001cc0109833e95aa107d",
+ "zh:df2409fc6a364b1f0a0f8a9cd8a86e61e80307996979ce3790243c4ce88f2915",
+ "zh:ed3c263396ff1f4d29639cc43339b655235acf4d06296a7c120a80e4e0fd6409",
+ ]
+}
+
+provider "registry.terraform.io/hashicorp/null" {
+ version = "3.2.3"
+ hashes = [
+ "h1:+AnORRgFbRO6qqcfaQyeX80W0eX3VmjadjnUFUJTiXo=",
+ "zh:22d062e5278d872fe7aed834f5577ba0a5afe34a3bdac2b81f828d8d3e6706d2",
+ "zh:23dead00493ad863729495dc212fd6c29b8293e707b055ce5ba21ee453ce552d",
+ "zh:28299accf21763ca1ca144d8f660688d7c2ad0b105b7202554ca60b02a3856d3",
+ "zh:55c9e8a9ac25a7652df8c51a8a9a422bd67d784061b1de2dc9fe6c3cb4e77f2f",
+ "zh:756586535d11698a216291c06b9ed8a5cc6a4ec43eee1ee09ecd5c6a9e297ac1",
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+ "zh:9d5eea62fdb587eeb96a8c4d782459f4e6b73baeece4d04b4a40e44faaee9301",
+ "zh:a6355f596a3fb8fc85c2fb054ab14e722991533f87f928e7169a486462c74670",
+ "zh:b5a65a789cff4ada58a5baffc76cb9767dc26ec6b45c00d2ec8b1b027f6db4ed",
+ "zh:db5ab669cf11d0e9f81dc380a6fdfcac437aea3d69109c7aef1a5426639d2d65",
+ "zh:de655d251c470197bcbb5ac45d289595295acb8f829f6c781d4a75c8c8b7c7dd",
+ "zh:f5c68199f2e6076bce92a12230434782bf768103a427e9bb9abee99b116af7b5",
+ ]
+}
+
+provider "registry.terraform.io/hetznercloud/hcloud" {
+ version = "1.48.1"
+ constraints = "~> 1.48.1"
+ hashes = [
+ "h1:fa9fxdSV9DG+HDcXyRbcGfb6Dk94SBP3TamHb1yOYiI=",
+ "zh:086cce10cb005f25f85183c59e639d6675e91e919934c80f660ca1cc4b9bc09b",
+ "zh:111d185707168b90c7ed3d245b522b2bd508f0bd4275496a1acdc9c0adaa85f2",
+ "zh:1acba3f30150282d283c46cd7ce25e9afb8b027fd2f594d41de9131d25a42b27",
+ "zh:1f8858aa81f93d52550502a11c7ea4e9370316ab098f6b75a09ffe75da6129ee",
+ "zh:20e01e6e6f99f57b3c1ef2a9de5d617c0139d3f3934eeb5e6c5976ae8b831a48",
+ "zh:2a8489a586a7bdadc42bbc9e3cb7b9deaefdf8020e3f2caba2678877d5d64d52",
+ "zh:31d8017529b0429bc9e873ec5d358ab9b75af2ba0ae24f21abcd4d09f36b7ee9",
+ "zh:407b4d7f1407e7e4a51b6f4dcdb0c7fbf81f2f1e25a7275f34054009419125a2",
+ "zh:42cf7cf867d199054713d4e6060e4b578eff16f0f537e9aaa5fd990c3eab8bc6",
+ "zh:460ac856ff952c5d41525949b93cfb7ee642f900594eff965494f11999d7496b",
+ "zh:d09e527d23f62564c82bc24e286cf2cb8cb0ed6cdc6f4c66adf2145cfa62adac",
+ "zh:d465356710444ac70dea4883252efc429b73e79fc6dc94f075662b838476680e",
+ "zh:d476c8eca307e30a20eed54c0735b062a6f3066b4ac63eebecd38ab8f40c16f4",
+ "zh:e0e9b2f6d5e28dbd01fa1ec3147aa88062d6223c5146532a3dcd1d3bb827e1e9",
+ ]
+}
diff --git a/website/infra/README.md b/website/infra/README.md
new file mode 100644
index 00000000..e919ebd6
--- /dev/null
+++ b/website/infra/README.md
@@ -0,0 +1,5 @@
+# umami backend
+
+This directory contains the deployment code for our [umami](https://umami.is/) backend used for collecting anonymous statics about the usage of our documentation website. The code for this lives here in the open for the sake of transparency and sharing (in case if you want to self-host your own umami instance on Hetzner).
+
+It is a simple Hetzner VPS that runs a docker-compose cluster with the umami service and Postgres. The data is stored on a separate volume. The server is allocated a static IPv4.
diff --git a/website/infra/bootstrap/data-volume.service b/website/infra/bootstrap/data-volume.service
new file mode 100644
index 00000000..7f2fee46
--- /dev/null
+++ b/website/infra/bootstrap/data-volume.service
@@ -0,0 +1,20 @@
+[Unit]
+Description=Data volume initialization
+
+# This unit is generated automatically by `systemd` using the builtin generator
+# that reads the configurations from `/etc/fstab`
+BindsTo=mnt-master.mount
+After=mnt-master.mount
+
+[Service]
+Type=oneshot
+User=${server_os_user}
+RemainAfterExit=yes
+WorkingDirectory=/var/app
+EnvironmentFile=${env_file_path}
+
+ExecStart=/var/app/data-volume.sh
+
+[Install]
+RequiredBy=docker.service
+Before=docker.service
diff --git a/website/infra/bootstrap/data-volume.sh b/website/infra/bootstrap/data-volume.sh
new file mode 100755
index 00000000..1e60f1e9
--- /dev/null
+++ b/website/infra/bootstrap/data-volume.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+sudo mkdir -p $DATA_VOLUME_PATH/docker
diff --git a/website/infra/bootstrap/docker-compose.sh b/website/infra/bootstrap/docker-compose.sh
new file mode 100644
index 00000000..7c4139a0
--- /dev/null
+++ b/website/infra/bootstrap/docker-compose.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+echo "Running: docker compose $@"
+
+CURRENT_UID=$(id -u):$(id -g) docker compose $@
diff --git a/website/infra/bootstrap/docker-daemon.json b/website/infra/bootstrap/docker-daemon.json
new file mode 100644
index 00000000..fed5ce5a
--- /dev/null
+++ b/website/infra/bootstrap/docker-daemon.json
@@ -0,0 +1,8 @@
+{
+ "data-root": "${data_volume_path}/docker",
+ "log-driver": "local",
+ "log-opts": {
+ "mode": "non-blocking",
+ "max-buffer-size": "5m"
+ }
+}
diff --git a/website/infra/bootstrap/umami.service b/website/infra/bootstrap/umami.service
new file mode 100644
index 00000000..d7f979b8
--- /dev/null
+++ b/website/infra/bootstrap/umami.service
@@ -0,0 +1,19 @@
+[Unit]
+Description=Umami Service (docker compose)
+
+BindsTo=docker.service
+After=docker.service
+
+[Service]
+Type=oneshot
+User=${server_os_user}
+RemainAfterExit=yes
+WorkingDirectory=/var/app
+EnvironmentFile=${env_file_path}
+
+ExecStartPre=-/var/app/docker-compose.sh rm
+ExecStart=/var/app/docker-compose.sh up --detach --no-build --wait
+ExecStop=/var/app/docker-compose.sh stop --timeout 60
+
+[Install]
+WantedBy=multi-user.target
diff --git a/website/infra/bootstrap/user_data.yml b/website/infra/bootstrap/user_data.yml
new file mode 100644
index 00000000..c961b8a2
--- /dev/null
+++ b/website/infra/bootstrap/user_data.yml
@@ -0,0 +1,77 @@
+#cloud-config
+
+disk_setup:
+ ${data_volume_device}:
+ table_type: gpt
+ layout: true
+ overwrite: false
+
+mounts:
+ - - ${data_volume_device}
+ - ${data_volume_path}
+ - ${data_volume_fs}
+ - defaults,noauto,x-systemd.growfs,x-systemd.makefs,x-systemd.device-timeout=10min
+ - "0"
+ - "0"
+
+groups: [docker]
+
+users:
+ - default
+ - name: ${server_os_user}
+ lock_passwd: true
+ shell: /bin/bash
+ ssh_authorized_keys: ["${ssh_public_key}"]
+ groups: docker
+ sudo: ALL=(ALL) NOPASSWD:ALL
+
+package_update: true
+package_upgrade: true
+package_reboot_if_required: true
+packages:
+ - apt-transport-https
+ - ca-certificates
+ - curl
+ - gnupg
+
+write_files:
+ %{~ for path, file in files ~}
+ - encoding: gzip+base64
+ content: ${file.content}
+ owner: '${server_os_user}:${server_os_user}'
+ path: ${path}
+ permissions: '${file.perms}'
+ defer: true
+ %{~ endfor ~}
+
+runcmd:
+ - |
+ log_content() {
+ echo "Contents of $1:"
+ cat $1 || echo "The file is absent at path $1"
+ }
+ log_content /var/run/reboot-required
+ log_content /var/run/reboot-required.pkgs
+
+ - ip addr add ${server_ip} dev eth0
+
+ - systemctl enable --now data-volume.service
+
+ - echo 'Installing docker...'
+
+ # Installs docker and docker-compose on the server
+ # Based on instructions from https://docs.docker.com/engine/install/ubuntu/
+ # and several github gists from here and there
+
+ - export DOCKER_GPG=/etc/apt/keyrings/docker.gpg
+ - export DOCKER_URL=https://download.docker.com/linux/ubuntu
+ - mkdir -p /etc/apt/keyrings
+ - curl --retry 5 --retry-connrefused -fsSL $DOCKER_URL/gpg | gpg --dearmor -o $DOCKER_GPG
+ - 'echo "deb [arch=$(dpkg --print-architecture) signed-by=$DOCKER_GPG] $DOCKER_URL $(lsb_release -cs) stable"
+ | tee /etc/apt/sources.list.d/docker.list > /dev/null'
+ - apt-get update -y
+ - apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
+ - systemctl enable --now docker.service
+
+ # Enable the systemd service responsible for managing Docker Compose services
+ - systemctl enable --now umami.service
diff --git a/website/infra/cloud_init.tf b/website/infra/cloud_init.tf
new file mode 100644
index 00000000..05a628b1
--- /dev/null
+++ b/website/infra/cloud_init.tf
@@ -0,0 +1,75 @@
+locals {
+ data_volume_path = "/mnt/master"
+ data_volume_fs = "ext4"
+ pg_data = "${local.data_volume_path}/data/postgres"
+ env_file_path = "/var/app/.env"
+ bootstrap = "${path.module}/bootstrap"
+
+ template_files = {
+ "umami.service" = "/etc/systemd/system/umami.service"
+ "data-volume.service" = "/etc/systemd/system/data-volume.service"
+ "docker-daemon.json" = "/etc/docker/daemon.json"
+ }
+ data_files = merge(
+ {
+ "/var/app/docker-compose.yml" = file("${path.module}/docker-compose.yml")
+ (local.env_file_path) = join("\n", [for k, v in local.env_vars : "${k}=${v}"])
+ },
+ {
+ for source, target in local.template_files :
+ target => templatefile("${local.bootstrap}/${source}", local.template_vars)
+ }
+ )
+
+ exec_files = {
+ for file in fileset(local.bootstrap, "*.sh") :
+ "/var/app/${file}" => file("${local.bootstrap}/${file}")
+ }
+
+ files_by_perms = {
+ "0444" = local.data_files
+ "0555" = local.exec_files
+ }
+
+ template_vars = {
+ env_file_path = local.env_file_path
+ server_os_user = local.server_os_user
+ server_ip = hcloud_floating_ip.master.ip_address
+
+ ssh_public_key = chomp(file("~/.ssh/id_rsa.pub"))
+
+ data_volume_device = hcloud_volume.master.linux_device
+ data_volume_path = local.data_volume_path
+ data_volume_fs = local.data_volume_fs
+ }
+
+ env_vars = {
+ PG_PASSWORD = var.pg_password
+ PG_DATA = local.pg_data
+ DATA_VOLUME_PATH = local.data_volume_path
+ UMAMI_APP_SECRET = var.umami_app_secret
+ }
+}
+
+data "cloudinit_config" "master" {
+ part {
+ content = templatefile(
+ "${path.module}/bootstrap/user_data.yml",
+ merge(
+ local.template_vars,
+ {
+ files = merge(
+ flatten([
+ for perms, files in local.files_by_perms : [
+ for path, content in files : {
+ (path) = { content = base64gzip(content), perms = perms }
+ }
+ ]
+ ])
+ ...
+ )
+ }
+ )
+ )
+ }
+}
diff --git a/website/infra/docker-compose.yml b/website/infra/docker-compose.yml
new file mode 100644
index 00000000..ae2e3b35
--- /dev/null
+++ b/website/infra/docker-compose.yml
@@ -0,0 +1,45 @@
+services:
+ umami:
+ image: docker.umami.is/umami-software/umami:postgresql-latest
+ ports:
+ - 80:3000
+
+ env_file: .env
+ environment:
+ DATABASE_URL: postgresql://umami:${PG_PASSWORD}@postgres:5432/umami
+ DATABASE_TYPE: postgresql
+ APP_SECRET: ${UMAMI_APP_SECRET}
+
+ networks: [postgres]
+ depends_on:
+ postgres:
+ condition: service_healthy
+
+ restart: always
+ healthcheck:
+ test: [CMD, curl, http://localhost:3000/api/heartbeat]
+ interval: 10s
+ timeout: 10s
+ retries: 5
+
+ postgres:
+ image: postgres:17
+ environment:
+ POSTGRES_USER: umami
+ POSTGRES_DB: umami
+ POSTGRES_PASSWORD: ${PG_PASSWORD}
+
+ volumes: [postgres:/var/lib/postgresql/data]
+ networks: [postgres]
+
+ healthcheck:
+ test: [CMD-SHELL, "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+networks:
+ postgres:
+
+volumes:
+ postgres:
diff --git a/website/infra/main.tf b/website/infra/main.tf
new file mode 100644
index 00000000..70f1e4c8
--- /dev/null
+++ b/website/infra/main.tf
@@ -0,0 +1,113 @@
+locals {
+ location = "fsn1"
+
+ hostname = "hetzner-master"
+
+ # XXX: using the name `admin` for the user is a bad idea. It does seem to work
+ # fine on Hetzner. However, when using Oracle Cloud, it was found that `admin`
+ # user name causes the server to be inaccessible via SSH. The supposition is
+ # that there is a conflict with the `admin` group name already present in
+ # the used Oracle Ubuntu AMI.
+ server_os_user = "master"
+}
+
+resource "hcloud_floating_ip" "master" {
+ type = "ipv4"
+ home_location = local.location
+}
+
+resource "hcloud_floating_ip_assignment" "master" {
+ floating_ip_id = hcloud_floating_ip.master.id
+ server_id = hcloud_server.master.id
+}
+
+resource "hcloud_server" "master" {
+ name = local.hostname
+ image = "ubuntu-24.04"
+ server_type = "cax21"
+ location = local.location
+ user_data = data.cloudinit_config.master.rendered
+ firewall_ids = [hcloud_firewall.master.id]
+
+ public_net {
+ ipv4_enabled = true
+ ipv6_enabled = true
+ }
+}
+
+resource "hcloud_volume" "master" {
+ name = "master"
+ size = 50
+ location = local.location
+}
+
+resource "hcloud_volume_attachment" "master" {
+ server_id = hcloud_server.master.id
+ volume_id = hcloud_volume.master.id
+
+ # Automount doesn't work if server's cloud-init script contains `runcmd` module
+ #
+ # instead we use systemd mount unit via fstab
+ automount = false
+}
+
+# HACK: we need to gracefully shutdown our systemd service with the database
+# docker container before the data volume is detached. This null resource
+# depends on the volume attachment resource, so the remote-exec provisioner
+# teardown script will be run before the attachment is destroyed.
+#
+# Unfortunately, it's not possible to do this with `systemd`. The volume detach
+# sequence is undocumented in Hetzner docs. One would expect that all `systemd`
+# services dependent upon the volume's mount are stopped before the volume
+# is detached but this isn't true.
+#
+# The reality is cruel. It was experimentally found that the volume is
+# detached abruptly. Therefore the database doesn't have time to flush its data
+# to disk, which means potential corruption or even data loss.
+resource "terraform_data" "teardown" {
+ triggers_replace = [
+ hcloud_volume_attachment.master.id
+ ]
+
+ input = {
+ server_ip = hcloud_server.master.ipv4_address
+ server_os_user = local.server_os_user
+ }
+
+
+ provisioner "remote-exec" {
+ when = destroy
+
+ inline = [
+ <<-SCRIPT
+ #!/usr/bin/env bash
+ set -euo pipefail
+ sudo systemctl stop umami.service
+ SCRIPT
+ ]
+
+ connection {
+ host = self.input.server_ip
+ user = self.input.server_os_user
+ }
+ }
+}
+
+resource "hcloud_firewall" "master" {
+ name = "allow-inbound"
+ rule {
+ direction = "in"
+ protocol = "tcp"
+ port = "22"
+ source_ips = var.allowed_ssh_ips
+ }
+ rule {
+ direction = "in"
+ protocol = "tcp"
+ port = "80"
+ source_ips = [
+ "0.0.0.0/0",
+ "::/0"
+ ]
+ }
+}
diff --git a/website/infra/outputs.tf b/website/infra/outputs.tf
new file mode 100644
index 00000000..8943d7ad
--- /dev/null
+++ b/website/infra/outputs.tf
@@ -0,0 +1,15 @@
+output "server_ip" {
+ value = hcloud_floating_ip.master.ip_address
+}
+
+output "server_status" {
+ value = hcloud_server.master.status
+}
+
+output "data_volume_path" {
+ value = local.data_volume_path
+}
+
+output "server_os_user" {
+ value = local.server_os_user
+}
diff --git a/website/infra/providers.tf b/website/infra/providers.tf
new file mode 100644
index 00000000..67f91754
--- /dev/null
+++ b/website/infra/providers.tf
@@ -0,0 +1,20 @@
+provider "hcloud" {
+ token = var.hcloud_token
+}
+
+terraform {
+ # Make sure to keep it in sync with the version requirement on CI
+ required_version = ">= 1.9"
+
+ required_providers {
+ hcloud = {
+ source = "hetznercloud/hcloud"
+ version = "~> 1.48.1"
+ }
+
+ cloudinit = {
+ source = "hashicorp/cloudinit"
+ version = "~> 2.3.5"
+ }
+ }
+}
diff --git a/website/infra/ssh.sh b/website/infra/ssh.sh
new file mode 100755
index 00000000..47cf8c43
--- /dev/null
+++ b/website/infra/ssh.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+tf_output=$(terraform output -json)
+
+os_user=$(echo "$tf_output" | jq -r '.server_os_user.value')
+ip=$(echo "$tf_output" | jq -r '.server_ip.value')
+
+echo "> ssh $os_user@$ip"
+
+ssh -t "$os_user@$ip"
diff --git a/website/infra/variables.tf b/website/infra/variables.tf
new file mode 100644
index 00000000..e2ac9a35
--- /dev/null
+++ b/website/infra/variables.tf
@@ -0,0 +1,23 @@
+variable "allowed_ssh_ips" {
+ nullable = false
+ type = list(string)
+ sensitive = true
+}
+
+variable "pg_password" {
+ nullable = false
+ type = string
+ sensitive = true
+}
+
+variable "umami_app_secret" {
+ nullable = false
+ type = string
+ sensitive = true
+}
+
+variable "hcloud_token" {
+ nullable = false
+ type = string
+ sensitive = true
+}
diff --git a/website/public/CNAME b/website/public/CNAME
new file mode 100644
index 00000000..c5dc03e1
--- /dev/null
+++ b/website/public/CNAME
@@ -0,0 +1 @@
+bon-rs.com