Week 24 of 2025

Development log of Esc Collective website

23 items
  1. Update the readme
  2. Link to git-rebase from the readme
  3. Added solutions Proton Mail and Tuta Mail, and finalized solution Mailfence
  4. Implement single feature pages
  5. Use a component (aka macro) for features bullets
  6. DRY on the solution card component
  7. add microsoft 365 mail as an email provider
  8. Add solution Transip Email
  9. Do not push alternatives to supported solutions
  10. Do not print a heading when no alternatives
  11. Add OpenMoji icos, a stylesheet and a script to generate it
  12. Use icons for features
  13. Implement the /features/ index page
  14. Write a few feature descriptions with some icons
  15. Improve the features index page
  16. Make feature cards links to feature pages
  17. Make solution cards links
  18. Add solution gmail
  19. Display list of features on every solution's page
  20. Extended description for some features.
  21. Update existing and added new features. Realigned features in the email category.
  22. Added feature just-email, realigned some of the other features and added icons.
  23. Added solutions Odoo Community and Odoo

Update the readme

On by Tad Lispy

Separate development setup (simpler) from publishing (more complex).

Describe git rebase operation.

Describe the publish script.

Describe branch previews.

index 25a36fd..dc2028e 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,42 @@
-This repository contains sources to build escco.eu website: content, templates, assets. Use Zola to generate a website from it and push it to 
+This repository is one of the two involved in escco.eu website. It contains sources to build escco.eu website: content, templates, assets. Use Zola to generate a website from it and push the generated files (from the `public/` directory) to https://codeberg.org/escco/pages/.
=
=
=# Development
=
-Start by cloning this repository. Then from within clone the pages  using the following command:
+To install and keep track of all dependencies automatically you need [Nix package manager](https://nixos.org/) and [direnv](https://direnv.net/).
+
+Start by cloning this repository.
+
+You should be able to run `zola serve` and get a working website at <http//localhost:1111/>.
+
+Changing content should be automatically reflected in the website (hot reloading).
+  
+# Collaboration
+
+If you are working on a feature, create a branch. For example:
+
+``` shell
+git switch --create awesome-services-list
+```
+
+If you are a member of the collective, share your work with the rest of us (see publishing) and discuss it in the Club. You don't need to create a PR. If you are an outside contributor, please create a pull request.
+
+It's advised to work together with another person (peer programming). If both of you are contributting to the same branch, you may run into conflicts. We deal with them by using a git feature called "rebasing". The easiest way is to invoke the following command:
+
+``` shell
+git pull --rebase
+```
+
+This command will fetch the latest contributions from your colleagues and if needed rewrite your local contributions as if they were written after. Do it every time you start working after a break (e.g. in the morning). This way your local repository will only contain changes additional to what others did (a linear, non-diverging histories). Follow it with `git push` and your version will be on top. Occasionally you may need to manually resolve conflicting changes. It's not difficult, but if you are new to this kind of thing, as a colleague to walk you through it.
+
+  
+# Publishing
+
+Pushing your contributions to this repository will not automatically update the website (not yet, eventually we will setup a continuous delivery system for that). For now, you need to take some manual steps to deploy a new version.
+
+## Setup
+
+Within _this_ repository clone the _other_ one (pages)  using the following command:
=
=``` shell
=git --work-tree public clone ssh://git@codeberg.org/escco/pages.git pages.git
@@ -15,39 +48,40 @@ This should create two sub-directories:
=  - `pages.git` containing git history and configuration
=  
=
-> Why? Because Zola will wipe out everything in public/, including .git/ if it were there. So we need to keep git directory outside of public.
+> Why not clone directly to the `public/` directory? Because Zola will wipe out everything in `public/`, including `.git/` if it were there. So we need to keep `.git/` directory outside of `public/`.
=
=This is the directory structure you should have now:
=
=``` shellsession
=escco.eu/
-  ./config.toml
+  ./config.toml  <-- global configuration of the website
=  ./content      <-- markdown files
-  ./flake.lock
-  ./flake.nix
+  ./flake.lock   <-- pins down versions of dependencies (like Zola; do not edit by hand)
+  ./flake.nix    <-- declaration of development environment
=  ./pages.git    <-- git history of public
=  ./public       <-- generated web content to be published
-  ./README.md
+  ./README.md    <-- this document
=  ./sass         <-- stylesheets
-  ./static
+  ./static       <-- content like images, scripts, 3rd party CSS or JS
=  ./templates    <-- reusable html templates
=```
=
-`
+## The publish script
=
-Use `zola serve` command to preview the website on your local machine.
+There is a script called `./publish.nu` that automates the publishing process. Once you are happy with your changes, call in like that:
=
-Use `zola build` to write production ready files to `public/`.
+``` shell
+nu publish.nu
+```
=
-Change directory to `pages.git/`, commit and push to publish the website.
+It should:
+
+  1. Make sure you don't have any uncommitted changes
+  2. Build the site with production optimizations
+  3. Present you with a list of changes (commits since last time site was published)
+  4. Publish the site to the branch preview matching your branch (see below)
+  
=
-Inside `pages.git` you can use branches and other features of Git. Just remember that `pages.git/` and `public/` are a separate repository from this one. If it feels too complicated, talk to Tad - he will explain or try find a way to make it simpler.
+## Branch preview
=
-> NOTE: The `zola serve` command, when stopped, will remove `public/`. If you are getting weird errors from git, like
-> 
-> ``` shellsession
-> $ git status
-> fatal: this operation must be run in a work tree
-> ```
-> 
-> inside pages repository, check if it exists. If not, just run `zola build` again to re-create it.
+If you are publishing the `main` branch, the changes will be applied to the website at <https://escco.eu/>. Other branches will be published at `https://escco.eu/@branch` where `branch` is the name of your branch. This allows us to experiment with different ideas and present them to the collective for discussion. Don't be affraid to take initiative and show us what you have in mind. It's often more efficient than having long discussions about ideas, that might not be feasible. The only risk is that your work will not be approved, but most likely it will lead to some learning.

On by Tad Lispy

index dc2028e..2f24b63 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ git switch --create awesome-services-list
=
=If you are a member of the collective, share your work with the rest of us (see publishing) and discuss it in the Club. You don't need to create a PR. If you are an outside contributor, please create a pull request.
=
-It's advised to work together with another person (peer programming). If both of you are contributting to the same branch, you may run into conflicts. We deal with them by using a git feature called "rebasing". The easiest way is to invoke the following command:
+It's advised to work together with another person (peer programming). If both of you are contributting to the same branch, you may run into conflicts. We deal with them by using [a git feature called "rebasing"](https://git-scm.com/docs/git-rebase). The easiest way is to invoke the following command:
=
=``` shell
=git pull --rebase

Added solutions Proton Mail and Tuta Mail, and finalized solution Mailfence

On by jnuytten

index ca766b4..4598b8d 100644
--- a/content/solutions/mailfence/index.md
+++ b/content/solutions/mailfence/index.md
@@ -5,18 +5,23 @@ taxonomies:
=  solution_providers: [ "ContactOffice Group sa" ]
=  solution_categories: [ "email provider" ]
=  features: 
-    - "rich text formatting"
-    - "scripting"
-    - "libre"
-    - "local"
-  esc_support: [ "training", "consulting", "integration" ]
+    - "open standards: imap, smtp"
+    - "encryption with OpenPGP"
+    - "calendar"
+    - "free plan"
+  esc_support: [ "consulting", "integration" ]
=---
=
-Mailfence is a solution for email, including calendar
+Mailfence is a secure and private email solution providing calendar, chat, documents and advanced user management.
=
=<!-- more -->
=
-Esc Collective offers commercial support for business switching to Mailfence in form of consulting, training and integration. We can help set up the Mailfence environment, take care of DNS settings including DKIM and DMARC, and help migrate existing email from your current provider.
-We believe Mailfence is a viable solution because it -- customer base, age, mature product 
+Esc Collective offers commercial support for business switching to Mailfence in form of consulting and integration. We can help set up the Mailfence environment, take care of DNS settings including DKIM and DMARC, and help migrate existing email from your current provider. But maybe most important of all, Esc Collective can help you find the email provider which meets your functional and business requirements.
+
+What makes Mailfence great is that it supports open standards for email synchronisation such as IMAP and SMTP which gives you the freedom to choose your own email client (e.g. Thunderbird). Nevertheless Mailfence does have a great browser app as well as apps for Android and iOS.
+
+Based in Belgium they are fully compliant with GDPR, and the focus of Mailfence has always been on privacy and security. There is no tracking, and even their browser client fully supports email encryption, digital signatures with OpenPGP.
+
+Functionalities include everything a small and medium sized business needs including user management, shared mailboxes, group calendars and so on. However, they have a free plan and Mailfence would still be great for small teams, families and individual users.
=
=Home page: <https://mailfence.com/>
new file mode 100644
index 0000000..1f7844e
--- /dev/null
+++ b/content/solutions/proton-mail/index.md
@@ -0,0 +1,28 @@
+---
+title: Proton Mail
+  
+taxonomies:
+  solution_providers: [ "Proton AG" ]
+  solution_categories: [ "email provider" ]
+  features: 
+    - "encryption"
+    - "calendar"
+    - "free plan"
+  esc_support: [ "consulting", "integration" ]
+---
+
+Proton Mail is an email solution focused on privacy and freedom. Their email solution includes calendar, and Proton also has solutions for documents, VPN and password management.
+
+<!-- more -->
+
+Esc Collective offers commercial support for business switching to Proton Mail in form of consulting and integration. We can help set up the Proton Mail environment, take care of DNS settings including DKIM and DMARC, and help migrate existing email from your current provider. But maybe most important of all, Esc Collective can help you find the email provider which meets your functional and business requirements.
+
+Based in Switzerland Proton Mail is governed by strict privacy regulations. They have a history of contesting court orders to disclose user information, and share information about this on [their website](https://proton.me/legal/transparency). Note that under Swiss law Proton can outright reject any such requests from foreign authorities, and more recently stated clearly that pending changes to Swiss law [will make them leave the country](https://www.tomsguide.com/computing/vpns/proton-vpn-boss-compares-switzerland-to-russia-and-claims-it-could-leave-the-country-over-proposed-law).
+
+What makes Proton Mail great is the company's focus on privacy and freedom. Their email service is developed to use encryption by default. Zero knowledge cloud storage ensures that even if Proton is ever obliged to disclose user data this is only limited to metadata.
+
+Proton requires the installation of their Mail Bridge to allow synchronization with other mail clients on a computer, this is not supported on mobile devices. Most users will therefore choose to use the browser app or the Proton email clients, which are available on Android, iOS, MS Windows and macOS. There is a Proton Mail client for Linux but it is still in beta.
+
+Proton Mail will work for most businesses. Having to rely on the Mail Bridge when not using Proton's email clients is a drawback, and some functionalities are missing such as shared mailboxes. But aside of that it is a great privacy and security focused email provider, with a large customer base and a proven history of reliability.
+
+Home page: <https://proton.me/>
new file mode 100644
index 0000000..4dba12c
--- /dev/null
+++ b/content/solutions/tuta-mail/index.md
@@ -0,0 +1,28 @@
+---
+title: Tuta Mail
+  
+taxonomies:
+  solution_providers: [ "Tutao HmbH" ]
+  solution_categories: [ "email provider" ]
+  features: 
+    - "encryption"
+    - "calendar"
+    - "free plan"
+  esc_support: [ "consulting", "integration" ]
+---
+
+Tuta Mail is an email solution focused on security through quantum-safe encryption. Their service includes a calendar and many business grade functionalities.
+
+<!-- more -->
+
+Esc Collective offers commercial support for business switching to Tuta Mail in form of consulting and integration. We can help set up the Tuta Mail environment, take care of DNS settings including DKIM and DMARC, and help migrate existing email from your current provider. But maybe most important of all, Esc Collective can help you find the email provider which meets your functional and business requirements.
+
+Tuta is based in Germany, and therefore ensures full GDPR compliance for both their private and business customers. They do not support IMAP, POP or other open email retrieval protocols so you cannot use email clients such as Thunderbird or Outlook. Tuta offers their own mail clients in the browser or installed on Android, iOS, Linux, Windows and MacOS.
+
+What makes Tuta Mail great is the company's focus on security through encryption, in a simple to use interface. Their email service is developed to use end-to-end encryption by default (with a limitation, see below). This ensures that even if Tuta is ever obliged to disclose user data this is only limited to metadata.
+
+Tuta goes very far in encrypting data stored on their servers and for emails sent in between mailboxes hosted on Tuta Mail - everything is encrypted except for email addresses and dates. However, because encryption is not performed with an open standard such as OpenPGP, any email traffic outside of the Tuta Mail network is only encrypted when it arrives in the network and not during transfer.
+
+Tuta Mail will work for most businesses. They offer multi-user support, as well as specific functionalities such as shared mailboxes. 
+
+Home page: <https://tuta.com/>

Implement single feature pages

On by Tad Lispy

Now features link to pages at /features/xyz. Those pages contain a list of solutions with a given feature, and optionally custom text. The text comes from special documents at content/solutions/features. Currently there is only one as a sample - free plan. Those feature documents are not rendered (they don't produce pages themselves), and only serve as extra data for the taxonomy pages. This is based on a tip from the Zola forum:

https://zola.discourse.group/t/can-we-add-extra-data-to-taxonomyterm/770/2?u=tad-lispy

In the future I want to use those documents to provide more data, e.g. an icons for each feature.

new file mode 100644
index 0000000..7f2bf71
--- /dev/null
+++ b/content/solutions/features/_index.md
@@ -0,0 +1,8 @@
+---
+title: Features definitions
+page_template: page.html
+render: false
+---
+
+These are features of digital solutions we review.
+
new file mode 100644
index 0000000..834b612
--- /dev/null
+++ b/content/solutions/features/free-plan.md
@@ -0,0 +1,7 @@
+---
+render: false
+---
+
+A free plan means that you can use a service in your business (usually in a limited capability) without any financial obligations.
+
+We only mark services with this feature if the free plan is unlimited in time (so not only a time trial) and actually useful for businesses.
new file mode 100644
index 0000000..b37b27f
--- /dev/null
+++ b/templates/features/single.html
@@ -0,0 +1,53 @@
+{# This is a template for a single feature #}
+
+{% extends "base.html" %}
+
+{% block main_content %}
+
+{% set feature_slug = term.name | slugify %}
+{% set feature_path = "solutions/features/" ~ feature_slug ~ ".md" %}
+{# TODO: Consider if a feature page should be required
+
+    As it is now, they can be skipped. But maybe we want to
+    ensure that every feature is defined? At the very least it
+    would prevent typos.
+#}
+{% set feature_data = load_data (path=feature_path, required=false) %}
+{% if feature_data %}
+  {% set feature_page = get_page(path=feature_path) %}
+  {{ feature_page.content | safe }}
+{% endif %}
+
+<p>Solutions with the <em>{{ term.name }}</em> feature:</p>
+
+{% set categories = get_taxonomy(kind="solution_categories") %}
+{% for category in categories.items %}
+  {% set_global solutions = [] %}
+  {% for solution in category.pages %}
+    {% if solution.taxonomies.features is containing (term.name) %}
+      {% set_global solutions = solutions | concat (with=solution) %}
+    {% endif %}
+  {% endfor %}
+  {% if solutions %}
+    <h2>{{ category.name | title }}</h2>
+    <section class="card-grid">
+      {% for solution in solutions %}
+      {# TODO: Dry with solution.html - a macro? #}
+        <article>
+            <h3>{{ solution.title }}</h3>
+            <p>{{ solution.summary | safe | striptags }}</p>
+            <ul>
+              {% for feature in solution.taxonomies.features %}
+              {% set feature_slug = feature | slugify %}
+              <li>
+                  <a href="{{ get_url(path="/features/" ~ feature_slug) }}">{{ feature }}</a>
+              </li>
+              {% endfor %}
+            </ul>
+            <p style="margin-top: auto"><a href="{{ get_url(path=solution.path) }}">Learn more</a></p>
+        </article>
+      {% endfor %}
+    </section>
+  {% endif %}
+{% endfor %}
+{% endblock main_content %}
index c0c552c..6482e75 100644
--- a/templates/solution.html
+++ b/templates/solution.html
@@ -11,7 +11,7 @@ Solution
=
=Do you use {{ page.title }} in your business? We can help you switch to one of the viable alternatives.
=
-
+{# TODO: Do not print the heading if there are no alternatives. Print something else. #}
={% for category in page.taxonomies.solution_categories %}
=  <h2>Alternatives in the {{ category }} category:</h2>
=
@@ -32,7 +32,10 @@ Do you use {{ page.title }} in your business? We can help you switch to one of t
=          <p>{{ alternative.summary | safe | striptags }}</p>
=          <ul>
=              {% for feature in alternative.taxonomies.features %}
-              <li>{{ feature }}</li>
+              {% set feature_slug = feature | slugify %}
+              <li>
+                  <a href="{{ get_url(path="/features/" ~ feature_slug) }}">{{ feature }}</a>
+              </li>
=              {% endfor %}
=          </ul>
=          <p style="margin-top: auto"><a href="{{ get_url(path=alternative.path) }}">Learn more</a></p>
index 3994c55..1f6a8c8 100644
--- a/templates/solutions.html
+++ b/templates/solutions.html
@@ -1,7 +1,5 @@
={% extends "base.html" %}
=
-Solution
-
={% block main_content %}
=  {{ section.content | safe }}
=
@@ -16,9 +14,12 @@ Solution
=            <h3>{{ solution.title }}</h3>
=            <p>{{ solution.summary | safe | striptags }}</p>
=            <ul>
-                {% for feature in solution.taxonomies.features %}
-                <li>{{ feature }}</li>
-                {% endfor %}
+              {% for feature in solution.taxonomies.features %}
+              {% set feature_slug = feature | slugify %}
+              <li>
+                  <a href="{{ get_url(path="/features/" ~ feature_slug) }}">{{ feature }}</a>
+              </li>
+              {% endfor %}
=            </ul>
=            <p style="margin-top: auto"><a href="{{ get_url(path=solution.path) }}">Learn more</a></p>
=        </article>

Use a component (aka macro) for features bullets

On by Tad Lispy

The bullets are rendered in three templates as of now:

There will be more in the future, and the bullets will evolve (icons, tooltips, who knows what else). The bullets themselves are part of bigger blocks: solution card within a solutions card grid. Those two also should be re-implemented as components.

new file mode 100644
index 0000000..e34a337
--- /dev/null
+++ b/templates/components.html
@@ -0,0 +1,8 @@
+{# These are reusable components (macros) #}
+
+{% macro feature_bullet(feature) %}
+{% set feature_slug = feature | slugify %}
+<li>
+    <a href="{{ get_url(path="/features/" ~ feature_slug) }}">{{ feature }}</a>
+</li>
+{% endmacro feature_bullet %}
index b37b27f..731b828 100644
--- a/templates/features/single.html
+++ b/templates/features/single.html
@@ -2,6 +2,8 @@
=
={% extends "base.html" %}
=
+{% import "components.html" as components %}
+
={% block main_content %}
=
={% set feature_slug = term.name | slugify %}
@@ -38,10 +40,7 @@
=            <p>{{ solution.summary | safe | striptags }}</p>
=            <ul>
=              {% for feature in solution.taxonomies.features %}
-              {% set feature_slug = feature | slugify %}
-              <li>
-                  <a href="{{ get_url(path="/features/" ~ feature_slug) }}">{{ feature }}</a>
-              </li>
+              {{ components::feature_bullet(feature=feature) }}
=              {% endfor %}
=            </ul>
=            <p style="margin-top: auto"><a href="{{ get_url(path=solution.path) }}">Learn more</a></p>
index 6482e75..c164f1d 100644
--- a/templates/solution.html
+++ b/templates/solution.html
@@ -1,6 +1,8 @@
+{# This is a template for a single solution page #}
+
={% extends "base.html" %}
=
-Solution
+{% import "components.html" as components %}
=
={% block main_content %}
={{ page.content | safe }}
@@ -32,10 +34,7 @@ Do you use {{ page.title }} in your business? We can help you switch to one of t
=          <p>{{ alternative.summary | safe | striptags }}</p>
=          <ul>
=              {% for feature in alternative.taxonomies.features %}
-              {% set feature_slug = feature | slugify %}
-              <li>
-                  <a href="{{ get_url(path="/features/" ~ feature_slug) }}">{{ feature }}</a>
-              </li>
+              {{ components::feature_bullet(feature=feature) }}
=              {% endfor %}
=          </ul>
=          <p style="margin-top: auto"><a href="{{ get_url(path=alternative.path) }}">Learn more</a></p>
index 1f6a8c8..ed2d441 100644
--- a/templates/solutions.html
+++ b/templates/solutions.html
@@ -1,5 +1,9 @@
+{# This is a template for the /solutions/ index page #}
+
={% extends "base.html" %}
=
+{% import "components.html" as components %}
+
={% block main_content %}
=  {{ section.content | safe }}
=
@@ -15,10 +19,7 @@
=            <p>{{ solution.summary | safe | striptags }}</p>
=            <ul>
=              {% for feature in solution.taxonomies.features %}
-              {% set feature_slug = feature | slugify %}
-              <li>
-                  <a href="{{ get_url(path="/features/" ~ feature_slug) }}">{{ feature }}</a>
-              </li>
+              {{ components::feature_bullet(feature=feature) }}
=              {% endfor %}
=            </ul>
=            <p style="margin-top: auto"><a href="{{ get_url(path=solution.path) }}">Learn more</a></p>

DRY on the solution card component

On by Tad Lispy

Don't Repeat Yourself.

index e34a337..3bc698d 100644
--- a/templates/components.html
+++ b/templates/components.html
@@ -2,7 +2,20 @@
=
={% macro feature_bullet(feature) %}
={% set feature_slug = feature | slugify %}
-<li>
+  <li>
=    <a href="{{ get_url(path="/features/" ~ feature_slug) }}">{{ feature }}</a>
-</li>
+  </li>
={% endmacro feature_bullet %}
+
+{% macro solution_card(solution) %}
+  <article>
+      <h3>{{ solution.title }}</h3>
+      <p>{{ solution.summary | safe | striptags }}</p>
+      <ul>
+          {% for feature in solution.taxonomies.features %}
+          {{ self::feature_bullet(feature=feature) }}
+          {% endfor %}
+      </ul>
+      <p style="margin-top: auto"><a href="{{ get_url(path=solution.path) }}">Learn more</a></p>
+  </article>
+{% endmacro solution_card %}
index 731b828..3cd5390 100644
--- a/templates/features/single.html
+++ b/templates/features/single.html
@@ -34,17 +34,7 @@
=    <h2>{{ category.name | title }}</h2>
=    <section class="card-grid">
=      {% for solution in solutions %}
-      {# TODO: Dry with solution.html - a macro? #}
-        <article>
-            <h3>{{ solution.title }}</h3>
-            <p>{{ solution.summary | safe | striptags }}</p>
-            <ul>
-              {% for feature in solution.taxonomies.features %}
-              {{ components::feature_bullet(feature=feature) }}
-              {% endfor %}
-            </ul>
-            <p style="margin-top: auto"><a href="{{ get_url(path=solution.path) }}">Learn more</a></p>
-        </article>
+        {{ components::solution_card(solution=solution) }}
=      {% endfor %}
=    </section>
=  {% endif %}
index c164f1d..5042efe 100644
--- a/templates/solution.html
+++ b/templates/solution.html
@@ -29,16 +29,7 @@ Do you use {{ page.title }} in your business? We can help you switch to one of t
=
=  <section class="card-grid">
=      {% for alternative in alternatives %}
-      <article>
-          <h3>{{ alternative.title }}</h3>
-          <p>{{ alternative.summary | safe | striptags }}</p>
-          <ul>
-              {% for feature in alternative.taxonomies.features %}
-              {{ components::feature_bullet(feature=feature) }}
-              {% endfor %}
-          </ul>
-          <p style="margin-top: auto"><a href="{{ get_url(path=alternative.path) }}">Learn more</a></p>
-      </article>
+        {{ components::solution_card(solution=alternative) }}
=      {% endfor %}
=  </section>
=  {% endfor %}
index ed2d441..1deb8ac 100644
--- a/templates/solutions.html
+++ b/templates/solutions.html
@@ -14,16 +14,7 @@
=    <section class="card-grid">
=      {% for solution in category.pages %}
=      {# TODO: Dry with solution.html - a macro? #}
-        <article>
-            <h3>{{ solution.title }}</h3>
-            <p>{{ solution.summary | safe | striptags }}</p>
-            <ul>
-              {% for feature in solution.taxonomies.features %}
-              {{ components::feature_bullet(feature=feature) }}
-              {% endfor %}
-            </ul>
-            <p style="margin-top: auto"><a href="{{ get_url(path=solution.path) }}">Learn more</a></p>
-        </article>
+        {{ components::solution_card(solution=solution) }}
=      {% endfor %}
=    </section>
=  {% endfor %}

add microsoft 365 mail as an email provider

On by jnuytten

new file mode 100644
index 0000000..6dd0278
--- /dev/null
+++ b/content/solutions/m365-mail/index.md
@@ -0,0 +1,25 @@
+---
+title: M365 Mail
+  
+taxonomies:
+  solution_providers: [ "Microsoft" ]
+  solution_categories: [ "email provider" ]
+  features: 
+    - "part of M365 Office suite"
+    - "enterprise focus"
+    - "US jurisdiction"
+---
+
+Microsoft offers email for business as part of its Microsoft 365 line of services.
+
+<!-- more -->
+
+Esc Collective recommends against using Microsoft 365 for email in your business, for multiple reasons:
+- Microsoft is based in the US, and is obliged to follow any court orders or presidential executive orders. Also if that involves violating international law, exposing your personal data or company secrets, or stopping business critical services without prior notice. This was painfully demonstrated when [Microsoft cut its email services to a staff member of the International Criminal Court](https://apnews.com/article/icc-trump-sanctions-karim-khan-court-a4b4c02751ab84c09718b1b95cbd5db3) after the US president sanctioned the ICC due to an ongoing procedure against Israeli politicians.
+- Big tech companies such as Microsoft do not respect EU regulations such as those for privacy - the General Data Protection Regulation (GDPR). While European politicians still pretend the [EU-US data privacy framework (DPF)](https://ec.europa.eu/commission/presscorner/detail/en/qanda_23_3752) is in place, anyone reading the news about US politics concludes the DPF is defunct. This means any services provided by US companies, even when hosted in Europe, are no longer compliant with GDPR.
+- Microsoft has built its business on vendor lock-in, and does not hesitate to use its monopolist position to raise prices or discontinue functionalities critical for some businesses.
+- Microsoft has [strong ties to the increasingly hostile regime of Donald Trump](https://www.cnbc.com/2025/01/09/microsoft-contributes-1-million-to-trumps-inauguration-fund.html)
+
+While Microsoft 365 offers a very complete set of services and sports "enterprise grade security", properly managing a M365 tenant and keeping it secure is a hard task for small and medium sized companies. Microsoft products are built with enterprises in mind, their architecture and administrative tools are not made for companies with limited IT staff and budgets. Also the listed threats on privacy, business continuity and business ethics far outweigh the advantages the Microsoft solutions might offer.
+
+Home page: <https://www.office.com/>

Add solution Transip Email

On by jnuytten

new file mode 100644
index 0000000..3e7bc0c
--- /dev/null
+++ b/content/solutions/transip-mail/index.md
@@ -0,0 +1,25 @@
+---
+title: Transip E-mail
+  
+taxonomies:
+  solution_providers: [ "Transip" ]
+  solution_categories: [ "email provider" ]
+  features: 
+    - "open standards: imap, smtp"
+    - "just email"
+  esc_support: [ "consulting", "integration" ]
+---
+
+Transip E-mail is a great solution for those businesses that only want to send email and don't need all the extra's.
+
+<!-- more -->
+
+Esc Collective offers commercial support for business switching to Transip E-mail in form of consulting and integration. We can take care of DNS settings including DKIM and DMARC, and help migrate existing email from your current provider. But maybe most important of all, Esc Collective can help you find the email provider which meets your functional and business requirements.
+
+Transip E-mail might be just what your business needs: a simple mailbox on your own custom domain. You can use the service from their webclient or using IMAP / SMTP from any client on your own device, for example Thunderbird. Their product does not include an encryption functionality but OpenPGP can be implemented locally in a client software to provide end-to-end encryption.
+
+This solution for email is not expensive. It should be prefered if your business requirement is limited to email and you don't need other functions such as calendar and shared mailboxes.
+
+Transip is a company from the Netherlands, data is stored in their datacenters in the Netherlands. They have an excellent reputation, respecting data privacy and ensuring security. We also have positive experience with their support.
+
+Home page: <https://www.transip.nl/email-hosting/>

Do not push alternatives to supported solutions

On by Tad Lispy

Use different text for supported vs not supported solutions.

index 3bc698d..c4ba2c3 100644
--- a/templates/components.html
+++ b/templates/components.html
@@ -19,3 +19,11 @@
=      <p style="margin-top: auto"><a href="{{ get_url(path=solution.path) }}">Learn more</a></p>
=  </article>
={% endmacro solution_card %}
+
+{% macro natural_list (items) %}
+{# Produce a natural sounding list, like "foo, bar and baz" #}
+  {%- for item in items -%}
+    {%- if loop.first %} {% elif loop.last %} and {% else %}, {% endif %}
+    {{ item | safe }}
+  {%- endfor -%}
+{% endmacro natural_list %}
index 5042efe..6c9c832 100644
--- a/templates/solution.html
+++ b/templates/solution.html
@@ -11,11 +11,19 @@
=  set alternatives = []
=%}
=
-Do you use {{ page.title }} in your business? We can help you switch to one of the viable alternatives.
+<p>
+  Do you use {{ page.title }} in your business?
+  {% if page.taxonomies.esc_support %}
+  Esc Collective offers support in form of {{ components::natural_list (items=page.taxonomies.esc_support) }}. 
+  {% else %}
+  We can help you switch to one of the viable alternatives.
+  {% endif %}
+</p>
+
=
={# TODO: Do not print the heading if there are no alternatives. Print something else. #}
={% for category in page.taxonomies.solution_categories %}
-  <h2>Alternatives in the {{ category }} category:</h2>
+  <h2>Alternatives to {{ page.title }} in the {{ category }} category:</h2>
=
=  {% set term = get_taxonomy_term(kind="solution_categories", term=category) %}
=  {% set_global alternatives = [] %}

Do not print a heading when no alternatives

On by Tad Lispy

index 6c9c832..9913136 100644
--- a/templates/solution.html
+++ b/templates/solution.html
@@ -23,8 +23,6 @@
=
={# TODO: Do not print the heading if there are no alternatives. Print something else. #}
={% for category in page.taxonomies.solution_categories %}
-  <h2>Alternatives to {{ page.title }} in the {{ category }} category:</h2>
-
=  {% set term = get_taxonomy_term(kind="solution_categories", term=category) %}
=  {% set_global alternatives = [] %}
=  {% for solution in term.pages %}
@@ -34,6 +32,8 @@
=    {% endif %}
=  {% endfor %}
=
+  {% if not alternatives %}{% continue %}{% endif %}
+  <h2>Alternatives to {{ page.title }} in the {{ category }} category:</h2>
=
=  <section class="card-grid">
=      {% for alternative in alternatives %}

Add OpenMoji icos, a stylesheet and a script to generate it

On by Tad Lispy

new file mode 100644
index 0000000..d5c9154
--- /dev/null
+++ b/openmoji.csv
@@ -0,0 +1,4293 @@
+emoji,hexcode,group,subgroups,annotation,tags,openmoji_tags,openmoji_author,openmoji_date,skintone,skintone_combination,skintone_base_emoji,skintone_base_hexcode,unicode,order
+😀,1F600,smileys-emotion,face-smiling,grinning face,"cheerful, cheery, face, grin, grinning, happy, laugh, nice, smile, smiling, teeth",,Emily Jäger,2018-04-18,,,,,1,1
+😃,1F603,smileys-emotion,face-smiling,grinning face with big eyes,"awesome, big, eyes, face, grin, grinning, happy, mouth, open, smile, smiling, teeth, yay",,Emily Jäger,2018-04-18,,,,,0.6,2
+😄,1F604,smileys-emotion,face-smiling,grinning face with smiling eyes,"eye, eyes, face, grin, grinning, happy, laugh, lol, mouth, open, smile, smiling",,Emily Jäger,2018-04-18,,,,,0.6,3
+😁,1F601,smileys-emotion,face-smiling,beaming face with smiling eyes,"beaming, eye, eyes, face, grin, grinning, happy, nice, smile, smiling, teeth",,Emily Jäger,2018-04-18,,,,,0.6,4
+😆,1F606,smileys-emotion,face-smiling,grinning squinting face,"closed, eyes, face, grinning, haha, hahaha, happy, laugh, lol, mouth, open, rofl, smile, smiling, squinting",,Emily Jäger,2018-04-18,,,,,0.6,5
+😅,1F605,smileys-emotion,face-smiling,grinning face with sweat,"cold, dejected, excited, face, grinning, mouth, nervous, open, smile, smiling, stress, stressed, sweat",,Emily Jäger,2018-04-18,,,,,0.6,6
+🤣,1F923,smileys-emotion,face-smiling,rolling on the floor laughing,"crying, face, floor, funny, haha, happy, hehe, hilarious, joy, laugh, lmao, lol, rofl, roflmao, rolling, tear",,Emily Jäger,2018-04-18,,,,,3,7
+😂,1F602,smileys-emotion,face-smiling,face with tears of joy,"crying, face, feels, funny, haha, happy, hehe, hilarious, joy, laugh, lmao, lol, rofl, roflmao, tear",,Emily Jäger,2018-04-18,,,,,0.6,8
+🙂,1F642,smileys-emotion,face-smiling,slightly smiling face,"face, happy, slightly, smile, smiling",,Emily Jäger,2018-04-18,,,,,1,9
+🙃,1F643,smileys-emotion,face-smiling,upside-down face,"face, hehe, smile, upside-down",,Emily Jäger,2018-04-18,,,,,1,10
+🫠,1FAE0,smileys-emotion,face-smiling,melting face,"disappear, dissolve, embarrassed, face, haha, heat, hot, liquid, lol, melt, melting, sarcasm, sarcastic",,Liz Bravo,2022-02-10,,,,,14,11
+😉,1F609,smileys-emotion,face-smiling,winking face,"face, flirt, heartbreaker, sexy, slide, tease, wink, winking, winks",,Emily Jäger,2018-04-18,,,,,0.6,12
+😊,1F60A,smileys-emotion,face-smiling,smiling face with smiling eyes,"blush, eye, eyes, face, glad, satisfied, smile, smiling",,Emily Jäger,2018-04-18,,,,,0.6,13
+😇,1F607,smileys-emotion,face-smiling,smiling face with halo,"angel, angelic, angels, blessed, face, fairy, fairytale, fantasy, halo, happy, innocent, peaceful, smile, smiling, spirit, tale",,Emily Jäger,2018-04-18,,,,,1,14
+🥰,1F970,smileys-emotion,face-affection,smiling face with hearts,"3, adore, crush, face, heart, hearts, ily, love, romance, smile, smiling, you",,Antonia Wagner,2019-05-03,,,,,11,15
+😍,1F60D,smileys-emotion,face-affection,smiling face with heart-eyes,"143, bae, eye, face, feels, heart-eyes, hearts, ily, kisses, love, romance, romantic, smile, xoxo",,Emily Jäger,2018-04-18,,,,,0.6,16
+🤩,1F929,smileys-emotion,face-affection,star-struck,"excited, eyes, face, grinning, smile, star, starry-eyed, wow",,Emily Jäger,2018-04-18,,,,,5,17
+😘,1F618,smileys-emotion,face-affection,face blowing a kiss,"adorbs, bae, blowing, face, flirt, heart, ily, kiss, love, lover, miss, muah, romantic, smooch, xoxo, you",,Emily Jäger,2018-04-18,,,,,0.6,18
+😗,1F617,smileys-emotion,face-affection,kissing face,"143, date, dating, face, flirt, ily, kiss, love, smooch, smooches, xoxo, you",,Emily Jäger,2018-04-18,,,,,1,19
+☺️,263A,smileys-emotion,face-affection,smiling face,"face, happy, outlined, relaxed, smile, smiling",,Emily Jäger,2018-04-18,,,,,0.6,21
+😚,1F61A,smileys-emotion,face-affection,kissing face with closed eyes,"143, bae, blush, closed, date, dating, eye, eyes, face, flirt, ily, kisses, kissing, smooches, xoxo",,Emily Jäger,2018-04-18,,,,,0.6,22
+😙,1F619,smileys-emotion,face-affection,kissing face with smiling eyes,"143, closed, date, dating, eye, eyes, face, flirt, ily, kiss, kisses, kissing, love, night, smile, smiling",,Emily Jäger,2018-04-18,,,,,1,23
+🥲,1F972,smileys-emotion,face-affection,smiling face with tear,"face, glad, grateful, happy, joy, pain, proud, relieved, smile, smiley, smiling, tear, touched",,Liz Bravo,2020-03-07,,,,,13,24
+😋,1F60B,smileys-emotion,face-tongue,face savoring food,"delicious, eat, face, food, full, hungry, savor, smile, smiling, tasty, um, yum, yummy",,Emily Jäger,2018-04-18,,,,,0.6,25
+😛,1F61B,smileys-emotion,face-tongue,face with tongue,"awesome, cool, face, nice, party, stuck-out, sweet, tongue",,Mariella Steeb,2018-04-18,,,,,1,26
+😜,1F61C,smileys-emotion,face-tongue,winking face with tongue,"crazy, epic, eye, face, funny, joke, loopy, nutty, party, stuck-out, tongue, wacky, weirdo, wink, winking, yolo",,Mariella Steeb,2018-04-18,,,,,0.6,27
+🤪,1F92A,smileys-emotion,face-tongue,zany face,"crazy, eye, eyes, face, goofy, large, small, zany",,Mariella Steeb,2018-04-18,,,,,5,28

And so on for the next 4265 lines. I wrote the openmoji.nu Nushell script to produce a nice CSS stylesheet.

# Export a CSS stylesheet mapping classes to OpenMoji hexcodes
#
# The required dataset and icons can be downloaded from their repository at
# https://github.com/hfg-gmuend/openmoji/blob/15.1.0/data/openmoji.csv
#
def "main export-css" [
  --output: path = "static/openmoji-color-icons.css", # Where to write the CSS
  --data: path = "openmoji.csv"                       # Where is the data.csv from OpenMoji
  --icons: path = "./openmoji-svg-color"              # Whare are the icons, relative to the CSS file
] {
    print $"exporting ($data) to ($output)"
    open $data --raw
    | from csv --no-infer
    | select hexcode annotation
    | update annotation { |row| $row.annotation | str kebab-case  }
    | each { |row| $".om-($row.annotation) { background-image: url\('($icons)/($row.hexcode).svg'\) }" }
    | save --force $output
}

def main [] {
  print "Try --help to learn about sub-commands"
}

Here is the abbreviated result, stored at /static/openmoji-color-icons.css.

.om-grinning-face { background-image: url('./openmoji-svg-color/1F600.svg') }
.om-grinning-face-with-big-eyes { background-image: url('./openmoji-svg-color/1F603.svg') }
.om-grinning-face-with-smiling-eyes { background-image: url('./openmoji-svg-color/1F604.svg') }
.om-beaming-face-with-smiling-eyes { background-image: url('./openmoji-svg-color/1F601.svg') }
.om-grinning-squinting-face { background-image: url('./openmoji-svg-color/1F606.svg') }
.om-grinning-face-with-sweat { background-image: url('./openmoji-svg-color/1F605.svg') }
.om-rolling-on-the-floor-laughing { background-image: url('./openmoji-svg-color/1F923.svg') }
.om-face-with-tears-of-joy { background-image: url('./openmoji-svg-color/1F602.svg') }
.om-slightly-smiling-face { background-image: url('./openmoji-svg-color/1F642.svg') }
.om-upside-down-face { background-image: url('./openmoji-svg-color/1F643.svg') }
.om-melting-face { background-image: url('./openmoji-svg-color/1FAE0.svg') }
.om-winking-face { background-image: url('./openmoji-svg-color/1F609.svg') }
.om-smiling-face-with-smiling-eyes { background-image: url('./openmoji-svg-color/1F60A.svg') }
.om-smiling-face-with-halo { background-image: url('./openmoji-svg-color/1F607.svg') }
.om-smiling-face-with-hearts { background-image: url('./openmoji-svg-color/1F970.svg') }
.om-smiling-face-with-heart-eyes { background-image: url('./openmoji-svg-color/1F60D.svg') }
.om-star-struck { background-image: url('./openmoji-svg-color/1F929.svg') }
.om-face-blowing-a-kiss { background-image: url('./openmoji-svg-color/1F618.svg') }
.om-kissing-face { background-image: url('./openmoji-svg-color/1F617.svg') }
.om-smiling-face { background-image: url('./openmoji-svg-color/263A.svg') }
.om-kissing-face-with-closed-eyes { background-image: url('./openmoji-svg-color/1F61A.svg') }
.om-kissing-face-with-smiling-eyes { background-image: url('./openmoji-svg-color/1F619.svg') }
.om-smiling-face-with-tear { background-image: url('./openmoji-svg-color/1F972.svg') }
.om-face-savoring-food { background-image: url('./openmoji-svg-color/1F60B.svg') }
.om-face-with-tongue { background-image: url('./openmoji-svg-color/1F61B.svg') }
.om-winking-face-with-tongue { background-image: url('./openmoji-svg-color/1F61C.svg') }
.om-zany-face { background-image: url('./openmoji-svg-color/1F92A.svg') }
.om-squinting-face-with-tongue { background-image: url('./openmoji-svg-color/1F61D.svg') }

/* and so on ... */

And a way we use it in our stylesheet.

@charset "UTF-8";

/*
Stylesheet adapted from openmoji-awesome <https://github.com/gromin/openmoji-awesome>
The graphics are licensed under the CC-BY 4.0: https://openmoji.org/
Preamble from https://ellekasai.github.io/twemoji-awesome/,
licensed under MIT: https://ellekasai.mit-license.org/
*/

.om {
    --om-path: calc("./openmoji-svg-color/" + var(--om-hexcode) + ".svg");
    background-image: url("./openmoji-svg-color/"var(--om-hexcode)".svg");
    display: inline-block;
    height: 1em;
    width: 1em;
    margin: 0 .05em 0 .1em;
    vertical-align: -0.1em;
    background-repeat: no-repeat;
    background-position: center center;
    background-size: 1em 1em;
}

/* When applied to a list item, use it as a bullet */
li.om {
    display: list-item;
    list-style: none;
    width: unset;
    margin-left: -1.1em;
    padding-left: 1.1em;
    height: initial;
    background-size: 0.9em;
    background-position-y: 0.1em;
    background-position-x: 0.0em;
}

.om-lg {
    height: 1.33em;
    width: 1.33em;
    margin: 0 0.0665em 0 0.133em;
    vertical-align: -0.15em;
    background-size: 1.33em 1.33em;
}

.om-bg {
    height: 1.5em;
    width: 1.5em;
    margin: 0 0.075em 0 0.15em;
    vertical-align: -0.2em;
    background-size: 1.5em 1.5em;
}

.om-2x {
    height: 2em;
    width: 2em;
    margin: 0 0.1em 0 0.2em;
    vertical-align: -0.7em;
    background-size: 2em 2em;
}

.om-3x {
    height: 3em;
    width: 3em;
    margin: 0 0.15em 0 0.3em;
    vertical-align: -1.1em;
    background-size: 3em 3em;
}

.om-4x {
    height: 4em;
    width: 4em;
    margin: 0 0.2em 0 0.4em;
    vertical-align: -1.5em;
    background-size: 4em 4em;
}

.om-5x {
    height: 5em;
    width: 5em;
    margin: 0 0.25em 0 0.5em;
    vertical-align: -1.6em;
    background-size: 5em 5em;
}
index 133b51b..d6119b1 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -338,6 +338,8 @@
=        }
=      }
=    </style>
+    <link rel="stylesheet" href="{{ get_url(path="/openmoji-preamble.css") }}" type="text/css" />
+    <link rel="stylesheet" href="{{ get_url(path="/openmoji-color-icons.css") }}" type="text/css" />
=  </head>
=
=  <body>

Use icons for features

On by Tad Lispy

As bullet points and in a feature page heading (bigger).

The icons come from the extra.moji metadata of a feature page. If there is no page, a black star is used for the bullet point and there is no heading (as there is no content it would describe).

index 834b612..be76f86 100644
--- a/content/solutions/features/free-plan.md
+++ b/content/solutions/features/free-plan.md
@@ -1,5 +1,7 @@
=---
=render: false
+extra:
+  moji: "wrapped gift"
=---
=
=A free plan means that you can use a service in your business (usually in a limited capability) without any financial obligations.
new file mode 100644
index 0000000..3c87c67
--- /dev/null
+++ b/content/solutions/features/trump-supporters.md
@@ -0,0 +1,11 @@
+---
+render: false
+extra:
+  moji: "trump"
+---
+
+The company behind this solution or some members of it's leadership actively support Donald Trump's regime in USA (financially or through actions beyond what is strictly required by law).
+
+<!-- more -->
+
+The current regime in USA is authoritarian and hostile toward Europe. We believe that supporting companies that align with it is dangerous to our societies.
index 6f9f835..f6ac974 100644
--- a/content/solutions/ms-word/index.md
+++ b/content/solutions/ms-word/index.md
@@ -10,7 +10,7 @@ taxonomies:
=    - "proprietary"
=    - "USA based"
=    - "big tech"
-    - "MAGA"
+    - "Trump supporters"
=---
=
=Microsoft Word is a word processor offered by Microsoft.
index d6119b1..e9a935b 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -191,12 +191,16 @@
=        height: 6rem;
=      }
=
+      .subtitle {
+        text-align: end;
+      }
+
=      p {
=        hyphens: auto;
=        line-height: 1.6em;
=      }
=
-      main > p:first-child {
+      main > p:first-of-type {
=        font-size: 150%;
=      }
=
index c4ba2c3..e7be6e5 100644
--- a/templates/components.html
+++ b/templates/components.html
@@ -2,14 +2,31 @@
=
={% macro feature_bullet(feature) %}
={% set feature_slug = feature | slugify %}
-  <li>
+{% set feature_moji = "black star" %}
+
+{% set feature_path = "solutions/features/" ~ feature_slug ~ ".md" %}
+{# TODO: Consider if a feature page should be required
+
+    As it is now, they can be skipped. But maybe we want to
+    ensure that every feature is defined? At the very least it
+    would prevent typos.
+#}
+{% set feature_data = load_data (path=feature_path, required=false) %}
+{% if feature_data %}
+  {% set feature_page = get_page(path=feature_path) %}
+  {% if feature_page.extra.moji %}
+  {% set feature_moji = feature_page.extra.moji %}
+  {% endif %}
+{% endif %}
+
+  <li class="om om-{{ feature_moji | slugify }}">
=    <a href="{{ get_url(path="/features/" ~ feature_slug) }}">{{ feature }}</a>
=  </li>
={% endmacro feature_bullet %}
=
-{% macro solution_card(solution) %}
+{% macro solution_card(solution, level=4) %}
=  <article>
-      <h3>{{ solution.title }}</h3>
+      <h{{ level }}>{{ solution.title }}</h{{ level }}>
=      <p>{{ solution.summary | safe | striptags }}</p>
=      <ul>
=          {% for feature in solution.taxonomies.features %}
@@ -27,3 +44,10 @@
=    {{ item | safe }}
=  {%- endfor -%}
={% endmacro natural_list %}
+
+
+{% macro openmoji (moji, size="1x") %}
+{# An icon from https://openmoji.org/library/ #}
+  <i class="om om-{{ moji | slugify }} om-{{ size }}">
+  </i>
+{% endmacro openmoji %}
index 3cd5390..f87f027 100644
--- a/templates/features/single.html
+++ b/templates/features/single.html
@@ -17,6 +17,7 @@
={% set feature_data = load_data (path=feature_path, required=false) %}
={% if feature_data %}
=  {% set feature_page = get_page(path=feature_path) %}
+  <h2 class="subtitle">About the <em>{{ term.name }}</em> feature {{ components::openmoji(moji=feature_page.extra.moji, size="2x") }}</h2>
=  {{ feature_page.content | safe }}
={% endif %}
=
@@ -31,7 +32,7 @@
=    {% endif %}
=  {% endfor %}
=  {% if solutions %}
-    <h2>{{ category.name | title }}</h2>
+    <h3>{{ category.name | title }}</h3>
=    <section class="card-grid">
=      {% for solution in solutions %}
=        {{ components::solution_card(solution=solution) }}

Implement the /features/ index page

On by Tad Lispy

It lists all the features in big cards. Each feature shows an icon, description (if available) and a list of solutions.

index be76f86..13f3a53 100644
--- a/content/solutions/features/free-plan.md
+++ b/content/solutions/features/free-plan.md
@@ -6,4 +6,6 @@ extra:
=
=A free plan means that you can use a service in your business (usually in a limited capability) without any financial obligations.
=
+<!-- more -->
+
=We only mark services with this feature if the free plan is unlimited in time (so not only a time trial) and actually useful for businesses.
index e9a935b..e362729 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -211,30 +211,33 @@
=        gap: 2em 2em;
=        margin: 4rem 0;
=
-
-        &> article {
-          background-color: white;
-          border-radius: 8px;
-          box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
-          padding: 1em;
-          display: flex;
-          flex-direction: column;
-
-          h2 {
+        article {
+          /* There shouldn't be more than one level of headings inside a card within a grid */
+          h2, h3, h4, h5, h6 {
=            font-size: 1em;
=            font-weight: bold;
=          }
-
-          p {
-            margin-top: 1em;
-            font-size: 1rem;
-            text-align: start;
-            text-align: justify;
-            hyphens: auto;
-          }
=        }
+
=      }
=
+      article {
+        background-color: white;
+        border-radius: 8px;
+        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+        padding: 1em;
+        display: flex;
+        flex-direction: column;
+        margin-bottom: 2rem;
+
+        p {
+          margin-top: 1em;
+          font-size: 1rem;
+          text-align: start;
+          text-align: justify;
+          hyphens: auto;
+        }
+      }
=
=      /* UTILITY CLASSES */
=
new file mode 100644
index 0000000..2a94c48
--- /dev/null
+++ b/templates/features/list.html
@@ -0,0 +1,39 @@
+{% extends "base.html" %}
+
+{% import "components.html" as components %}
+
+{% block main_content %}
+  {% for term in terms %}
+    <article>
+      {% set feature_slug = term.name | slugify %}
+      {% set feature_path = "solutions/features/" ~ feature_slug ~ ".md" %}
+      {# TODO: Consider if a feature page should be required
+
+          As it is now, they can be skipped. But maybe we want to
+          ensure that every feature is defined? At the very least it
+          would prevent typos.
+      #}
+      {% set feature_data = load_data (path=feature_path, required=false) %}
+      {% if feature_data %}
+        {% set feature_page = get_page(path=feature_path) %}
+
+        <h2 style="text-transform: capitalize">
+          {{ components::openmoji(moji=feature_page.extra.moji, size="2x") }}
+          {{ term.name }}
+        </h2>
+        {{ feature_page.summary | safe }}
+      {% else %}
+        <h2 style="text-transform: capitalize">
+          {{ components::openmoji(moji="black star", size="2x") }}
+          {{ term.name }}
+        </h2>
+        <p>This feature doesn't have a description yet.</p>
+      {% endif %}
+
+      <p>
+        Solutions with the <em>{{ term.name }}</em> feature:
+        {{ components::natural_list(items=term.pages | map (attribute="title")) }}
+      </p>
+    </article>
+  {% endfor %}
+{% endblock main_content %}

Write a few feature descriptions with some icons

On by Tad Lispy

Just to see if the whole thing works. The content is dummy.

new file mode 100644
index 0000000..0b633c0
--- /dev/null
+++ b/content/solutions/features/big-tech.md
@@ -0,0 +1,9 @@
+---
+extra:
+  moji: "hal 9000"
+render: false
+---
+
+Big tech bad. Inspiration to write low. See <https://en.wikipedia.org/wiki/Big_Tech>.
+
+<!-- more -->
new file mode 100644
index 0000000..a2e2e29
--- /dev/null
+++ b/content/solutions/features/calendar.md
@@ -0,0 +1,9 @@
+---
+extra:
+  moji: "spiral calendar"
+render: false
+---
+
+Calendar good, I guess.
+
+<!-- more -->
new file mode 100644
index 0000000..f56d6cd
--- /dev/null
+++ b/content/solutions/features/encryption.md
@@ -0,0 +1,9 @@
+---
+extra:
+  moji: "locked"
+render: false
+---
+
+Encryption is nice.
+
+<!-- more -->
new file mode 100644
index 0000000..247f3d9
--- /dev/null
+++ b/content/solutions/features/end-to-end-encryption.md
@@ -0,0 +1,9 @@
+---
+extra:
+  moji: "locked with key"
+render: false
+---
+
+E2E encryption is very nice, but can get clunky.
+
+<!-- more -->
new file mode 100644
index 0000000..9086e8f
--- /dev/null
+++ b/content/solutions/features/local.md
@@ -0,0 +1,9 @@
+---
+extra:
+  moji: "desktop computer"
+render: false
+---
+
+This program runs locally on desktop computers and doesn't require an internet connection.
+
+<!-- more -->
new file mode 100644
index 0000000..d69b15b
--- /dev/null
+++ b/content/solutions/features/scripting.md
@@ -0,0 +1,9 @@
+---
+extra:
+  moji: "fairy"
+render: false
+---
+
+You can extend the functionality of this solution via scripts, i.e. programs that run inside the main application.
+
+<!-- more -->
new file mode 100644
index 0000000..f3a885d
--- /dev/null
+++ b/content/solutions/features/self-hosting.md
@@ -0,0 +1,9 @@
+---
+extra:
+  moji: "mage"
+render: false
+---
+
+This service can be run on your own servers, which usually gives you much more control over your business data. Too hard? We can help.
+
+<!-- more -->
new file mode 100644
index 0000000..835ccd5
--- /dev/null
+++ b/content/solutions/features/us-jurisdiction.md
@@ -0,0 +1,9 @@
+---
+extra:
+  moji: "flag: United States"
+render: false
+---
+
+Stepping into the lions den?
+
+<!-- more -->
index 2a94c48..2446940 100644
--- a/templates/features/list.html
+++ b/templates/features/list.html
@@ -28,6 +28,9 @@
=          {{ term.name }}
=        </h2>
=        <p>This feature doesn't have a description yet.</p>
+        {% if config.mode == "serve" %}
+        <p><small>Create a file at <code>content/{{ feature_path }}</code> and describe it.</small></p>
+        {% endif %}
=      {% endif %}
=
=      <p>

Improve the features index page

On by Tad Lispy

Content and template.

index 7f2bf71..f8b3470 100644
--- a/content/solutions/features/_index.md
+++ b/content/solutions/features/_index.md
@@ -1,8 +1,10 @@
=---
-title: Features definitions
-page_template: page.html
+# The content of this page is used in /features/ index page.
+# See /templates/features/list.html template
=render: false
=---
=
-These are features of digital solutions we review.
+We review different digital solution and annotate them with _features_ - aspects relevant to their use in a European <abbr title="Small and Medium Enterprises">SMEs</abbr>.
+
+The features are considered from this perspective. Below is a list of all the features we define, together with an explanation and digital solutions they apply to.
=
index 2446940..fea5cab 100644
--- a/templates/features/list.html
+++ b/templates/features/list.html
@@ -3,9 +3,12 @@
={% import "components.html" as components %}
=
={% block main_content %}
-  {% for term in terms %}
+  {% set section = get_section(path="solutions/features/_index.md") %}
+  {{ section.content | safe }}
+
+  {% for feature in terms %}
=    <article>
-      {% set feature_slug = term.name | slugify %}
+      {% set feature_slug = feature.name | slugify %}
=      {% set feature_path = "solutions/features/" ~ feature_slug ~ ".md" %}
=      {# TODO: Consider if a feature page should be required
=
@@ -19,13 +22,13 @@
=
=        <h2 style="text-transform: capitalize">
=          {{ components::openmoji(moji=feature_page.extra.moji, size="2x") }}
-          {{ term.name }}
+          {{ feature.name }}
=        </h2>
=        {{ feature_page.summary | safe }}
=      {% else %}
=        <h2 style="text-transform: capitalize">
=          {{ components::openmoji(moji="black star", size="2x") }}
-          {{ term.name }}
+          {{ feature.name }}
=        </h2>
=        <p>This feature doesn't have a description yet.</p>
=        {% if config.mode == "serve" %}
@@ -34,8 +37,8 @@
=      {% endif %}
=
=      <p>
-        Solutions with the <em>{{ term.name }}</em> feature:
-        {{ components::natural_list(items=term.pages | map (attribute="title")) }}
+        Solutions with the <em>{{ feature.name }}</em> feature:
+        {{ components::natural_list(items=feature.pages | map (attribute="title")) }}
=      </p>
=    </article>
=  {% endfor %}

On by Tad Lispy

Required some wizardry to strip interactive tags. According to HTML spec interactive elements (like links and buttons) can't be nested.

index e362729..e498304 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -239,6 +239,12 @@
=        }
=      }
=
+      a:has(> article) {
+        display: block;
+        text-decoration: none;
+        color: inherit
+      }
+
=      /* UTILITY CLASSES */
=
=      .full-bleed-background {
index e7be6e5..c29303c 100644
--- a/templates/components.html
+++ b/templates/components.html
@@ -51,3 +51,23 @@
=  <i class="om om-{{ moji | slugify }} om-{{ size }}">
=  </i>
={% endmacro openmoji %}
+
+{% macro strip_interactive (html, tags="a button") %}
+{# Remove interactive tags (links and buttons).
+   Useful if content needs to be wrapped in a link.
+#}
+  {% set_global output = html %}
+  {% for tag in tags | split (pat=" ") %}
+    {% for following_character in [ " ", "\n", ">" ] %}
+      {% set from = "<" ~ tag ~ following_character %}
+      {% set class = "stripped-" ~ tag %}
+      {% set to = "<span class='" ~ class ~ "'" ~ following_character %}
+      {% set_global output =
+        output
+        | replace(from=from, to=to)
+        | replace(from="</" ~ tag ~ ">", to="</span>")
+      %}
+    {% endfor %}
+  {% endfor %}
+  {{ output | safe }}
+{% endmacro strip_interactive %}
index fea5cab..3d56b2e 100644
--- a/templates/features/list.html
+++ b/templates/features/list.html
@@ -7,39 +7,55 @@
=  {{ section.content | safe }}
=
=  {% for feature in terms %}
-    <article>
-      {% set feature_slug = feature.name | slugify %}
-      {% set feature_path = "solutions/features/" ~ feature_slug ~ ".md" %}
-      {# TODO: Consider if a feature page should be required
-
-          As it is now, they can be skipped. But maybe we want to
-          ensure that every feature is defined? At the very least it
-          would prevent typos.
-      #}
-      {% set feature_data = load_data (path=feature_path, required=false) %}
-      {% if feature_data %}
-        {% set feature_page = get_page(path=feature_path) %}
-
-        <h2 style="text-transform: capitalize">
-          {{ components::openmoji(moji=feature_page.extra.moji, size="2x") }}
-          {{ feature.name }}
-        </h2>
-        {{ feature_page.summary | safe }}
-      {% else %}
-        <h2 style="text-transform: capitalize">
-          {{ components::openmoji(moji="black star", size="2x") }}
-          {{ feature.name }}
-        </h2>
-        <p>This feature doesn't have a description yet.</p>
-        {% if config.mode == "serve" %}
-        <p><small>Create a file at <code>content/{{ feature_path }}</code> and describe it.</small></p>
-        {% endif %}
-      {% endif %}
+    <a
+      href="{{ get_url (path=feature.path) }}"
+      title="Read more about the {{ feature.name }} feature"
+    >
+      <article>
+        {% set feature_slug = feature.name | slugify %}
+        {% set feature_path = "solutions/features/" ~ feature_slug ~ ".md" %}
+        {# TODO: Consider if a feature page should be required
+
+            As it is now, they can be skipped. But maybe we want to
+            ensure that every feature is defined? At the very least it
+            would prevent typos.
+        #}
+        {% set feature_data = load_data (path=feature_path, required=false) %}
+        {% if feature_data %}
+          {% set feature_page = get_page(path=feature_path) %}
=
-      <p>
-        Solutions with the <em>{{ feature.name }}</em> feature:
-        {{ components::natural_list(items=feature.pages | map (attribute="title")) }}
-      </p>
-    </article>
+          {{ self::feature_heading(text=feature.name, moji=feature_page.extra.moji) }}
+          <div>
+            {{ components::strip_interactive (html=feature_page.summary) | safe }}
+            {{ self::solutions_list (feature=feature) }}
+          </div>
+        {% else %}
+          {{ self::feature_heading(text=feature.name) }}
+          <div>
+            <p>This feature doesn't have a description yet.</p>
+            {% if config.mode == "serve" %}
+            <p><small>Create a file at <code>content/{{ feature_path }}</code> and describe it.</small></p>
+            {% endif %}
+            {{ self::solutions_list (feature=feature) }}
+          </div>
+        {% endif %}
+      </article>
+    </a>
=  {% endfor %}
={% endblock main_content %}
+
+
+{% macro feature_heading (text, moji="black star") %}
+  <h2 style="text-transform: capitalize">
+    {{ components::openmoji(moji=moji, size="2x") }}
+    {{ text }}
+  </h2>
+{% endmacro feature_heading %}
+
+{% macro solutions_list (feature) %}
+  <p>
+    Solutions with the <em>{{ feature.name }}</em> feature:
+    {{ components::natural_list(items=feature.pages | map (attribute="title")) }}
+  </p>
+{% endmacro feature_heading %}
+

On by Tad Lispy

Also implement a subtle raising animation for hovered card-links.

Also fix some weird sizing / whitespace issues.

index e498304..26335a1 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -229,6 +229,7 @@
=        display: flex;
=        flex-direction: column;
=        margin-bottom: 2rem;
+        transition: box-shadow 250ms;
=
=        p {
=          margin-top: 1em;
@@ -242,7 +243,19 @@
=      a:has(> article) {
=        display: block;
=        text-decoration: none;
-        color: inherit
+        color: inherit;
+
+        article {
+          height: 100%;
+          margin: 0;
+          box-sizing: border-box;
+        }
+
+        &:hover, &:focus {
+          article {
+            box-shadow: 0 10px 12px rgba(0, 0, 0, 0.1);
+          }
+        }
=      }
=
=      /* UTILITY CLASSES */
index c29303c..c93e9ee 100644
--- a/templates/components.html
+++ b/templates/components.html
@@ -1,6 +1,6 @@
={# These are reusable components (macros) #}
=
-{% macro feature_bullet(feature) %}
+{% macro feature_bullet(feature, link=true) %}
={% set feature_slug = feature | slugify %}
={% set feature_moji = "black star" %}
=
@@ -20,21 +20,28 @@
={% endif %}
=
=  <li class="om om-{{ feature_moji | slugify }}">
-    <a href="{{ get_url(path="/features/" ~ feature_slug) }}">{{ feature }}</a>
+    {% if link %}
+    <a href="{{ get_url(path="/features/" ~ feature_slug) }}">
+    {% endif %}
+        {{ feature }}
+    {% if link %}
+    </a>
+    {% endif %}
=  </li>
={% endmacro feature_bullet %}
=
={% macro solution_card(solution, level=4) %}
-  <article>
+  <a href="{{ get_url(path=solution.path) }}">
+    <article>
=      <h{{ level }}>{{ solution.title }}</h{{ level }}>
-      <p>{{ solution.summary | safe | striptags }}</p>
-      <ul>
-          {% for feature in solution.taxonomies.features %}
-          {{ self::feature_bullet(feature=feature) }}
-          {% endfor %}
+      {{ self::strip_interactive (html=solution.summary) }}
+      <ul style="margin-top: auto">
+        {% for feature in solution.taxonomies.features %}
+        {{ self::feature_bullet(feature=feature, link=false) }}
+        {% endfor %}
=      </ul>
-      <p style="margin-top: auto"><a href="{{ get_url(path=solution.path) }}">Learn more</a></p>
-  </article>
+    </article>
+  </a>
={% endmacro solution_card %}
=
={% macro natural_list (items) %}
index 3d56b2e..93aaa79 100644
--- a/templates/features/list.html
+++ b/templates/features/list.html
@@ -10,6 +10,7 @@
=    <a
=      href="{{ get_url (path=feature.path) }}"
=      title="Read more about the {{ feature.name }} feature"
+      style="margin-bottom: 2em"
=    >
=      <article>
=        {% set feature_slug = feature.name | slugify %}

Add solution gmail

On by jnuytten

new file mode 100644
index 0000000..0a0bac5
--- /dev/null
+++ b/content/solutions/gmail/index.md
@@ -0,0 +1,26 @@
+---
+title: Gmail for business
+  
+taxonomies:
+  solution_providers: [ "Alphabet" ]
+  solution_categories: [ "email provider" ]
+  features: 
+    - "part of Google Workspace suite"
+    - "US jurisdiction"
+    - "Active supporter of the anti-DEI movement"
+
+---
+
+Google offers email for business as part of its Google Workspace solution.
+
+<!-- more -->
+
+Esc Collective recommends against using Gmail for email in your business, for multiple reasons:
+- Google is based in the US, and is obliged to follow any court orders or presidential executive orders. Also if that involves violating international law, exposing your personal data or company secrets, or stopping business critical services without prior notice.
+- Big tech companies such as Google do not respect EU regulations such as those for privacy - the General Data Protection Regulation (GDPR). While European politicians still pretend the [EU-US data privacy framework (DPF)](https://ec.europa.eu/commission/presscorner/detail/en/qanda_23_3752) is in place, anyone reading the news about US politics concludes the DPF is defunct. This means any services provided by US companies, even when hosted in Europe, are no longer compliant with GDPR.
+- Google has built their business on your data, respect of privacy or (other) companies secrets have never been their concern.
+- Google has [strong ties to the increasingly hostile regime of Donald Trump](https://www.cnbc.com/2025/01/09/google-donates-1-million-to-trumps-inauguration-fund.html) and was one of the big tech companies that already early in 2025 ditched all their DEI initiatives.
+
+While Google offers a very complete set of services and offers high standards in security, providing access to your company data to this provider is a major concern seen their disrespect for privacy.
+
+Home page: <https://www.google.com/>

Display list of features on every solution's page

On by Tad Lispy

index c93e9ee..8bdc03d 100644
--- a/templates/components.html
+++ b/templates/components.html
@@ -78,3 +78,58 @@
=  {% endfor %}
=  {{ output | safe }}
={% endmacro strip_interactive %}
+
+
+{% macro features_cards(features, level=2) %}
+  {% for feature in features %}
+    <a
+      href="{{ get_url (path=feature.path) }}"
+      title="Read more about the {{ feature.name }} feature"
+      style="margin-bottom: 2em"
+    >
+      <article>
+        {% set feature_slug = feature.name | slugify %}
+        {% set feature_path = "solutions/features/" ~ feature_slug ~ ".md" %}
+        {# TODO: Consider if a feature page should be required
+
+            As it is now, they can be skipped. But maybe we want to
+            ensure that every feature is defined? At the very least it
+            would prevent typos.
+        #}
+        {% set feature_data = load_data (path=feature_path, required=false) %}
+        {% if feature_data %}
+          {% set feature_page = get_page(path=feature_path) %}
+
+          {{ self::feature_heading(text=feature.name, moji=feature_page.extra.moji, level=level) }}
+          <div>
+            {{ components::strip_interactive (html=feature_page.summary) | safe }}
+            {{ self::solutions_list (feature=feature) }}
+          </div>
+        {% else %}
+          {{ self::feature_heading(text=feature.name, level=level) }}
+          <div>
+            <p>This feature doesn't have a description yet.</p>
+            {% if config.mode == "serve" %}
+            <p><small>Create a file at <code>content/{{ feature_path }}</code> and describe it.</small></p>
+            {% endif %}
+            {{ self::solutions_list (feature=feature) }}
+          </div>
+        {% endif %}
+      </article>
+    </a>
+  {% endfor %}
+{% endmacro features_cards %}
+
+{% macro feature_heading (text, moji="black star", level=2) %}
+  <h{{ level }} style="text-transform: capitalize">
+    {{ components::openmoji(moji=moji, size="2x") }}
+    {{ text }}
+  </h{{ level }}>
+{% endmacro feature_heading %}
+
+{% macro solutions_list (feature) %}
+  <p>
+    Solutions with the <em>{{ feature.name }}</em> feature:
+    {{ components::natural_list(items=feature.pages | map (attribute="title")) }}
+  </p>
+{% endmacro feature_heading %}
index 93aaa79..1044c51 100644
--- a/templates/features/list.html
+++ b/templates/features/list.html
@@ -6,57 +6,7 @@
=  {% set section = get_section(path="solutions/features/_index.md") %}
=  {{ section.content | safe }}
=
-  {% for feature in terms %}
-    <a
-      href="{{ get_url (path=feature.path) }}"
-      title="Read more about the {{ feature.name }} feature"
-      style="margin-bottom: 2em"
-    >
-      <article>
-        {% set feature_slug = feature.name | slugify %}
-        {% set feature_path = "solutions/features/" ~ feature_slug ~ ".md" %}
-        {# TODO: Consider if a feature page should be required
-
-            As it is now, they can be skipped. But maybe we want to
-            ensure that every feature is defined? At the very least it
-            would prevent typos.
-        #}
-        {% set feature_data = load_data (path=feature_path, required=false) %}
-        {% if feature_data %}
-          {% set feature_page = get_page(path=feature_path) %}
-
-          {{ self::feature_heading(text=feature.name, moji=feature_page.extra.moji) }}
-          <div>
-            {{ components::strip_interactive (html=feature_page.summary) | safe }}
-            {{ self::solutions_list (feature=feature) }}
-          </div>
-        {% else %}
-          {{ self::feature_heading(text=feature.name) }}
-          <div>
-            <p>This feature doesn't have a description yet.</p>
-            {% if config.mode == "serve" %}
-            <p><small>Create a file at <code>content/{{ feature_path }}</code> and describe it.</small></p>
-            {% endif %}
-            {{ self::solutions_list (feature=feature) }}
-          </div>
-        {% endif %}
-      </article>
-    </a>
-  {% endfor %}
+  {{ components::features_cards (features=terms) }}
={% endblock main_content %}
=
=
-{% macro feature_heading (text, moji="black star") %}
-  <h2 style="text-transform: capitalize">
-    {{ components::openmoji(moji=moji, size="2x") }}
-    {{ text }}
-  </h2>
-{% endmacro feature_heading %}
-
-{% macro solutions_list (feature) %}
-  <p>
-    Solutions with the <em>{{ feature.name }}</em> feature:
-    {{ components::natural_list(items=feature.pages | map (attribute="title")) }}
-  </p>
-{% endmacro feature_heading %}
-
index 9913136..7f0320d 100644
--- a/templates/solution.html
+++ b/templates/solution.html
@@ -7,20 +7,32 @@
={% block main_content %}
={{ page.content | safe }}
=
-{%
-  set alternatives = []
-%}
-
=<p>
=  Do you use {{ page.title }} in your business?
=  {% if page.taxonomies.esc_support %}
-  Esc Collective offers support in form of {{ components::natural_list (items=page.taxonomies.esc_support) }}. 
+  Esc Collective offers support in form of {{ components::natural_list (items=page.taxonomies.esc_support) }}.
=  {% else %}
=  We can help you switch to one of the viable alternatives.
=  {% endif %}
=</p>
=
=
+{% set_global features = []  %}
+{% for feature in get_taxonomy (kind="features") | get (key="items") %}
+  {% if page.taxonomies.features is containing (feature.name) %}
+    {% set_global features = features | concat (with=feature) %}
+  {% endif %}
+{% endfor %}
+
+{% if features %}
+<h2>Features of {{ page.title }}</h2>
+{{ components::features_cards (features=features, level=3) }}
+{% endif %}
+
+{%
+  set alternatives = []
+%}
+
={# TODO: Do not print the heading if there are no alternatives. Print something else. #}
={% for category in page.taxonomies.solution_categories %}
=  {% set term = get_taxonomy_term(kind="solution_categories", term=category) %}

Extended description for some features.

On by Víctor Andrade

Added some explainatory text for some features missing proper description:

index a2e2e29..b0e7960 100644
--- a/content/solutions/features/calendar.md
+++ b/content/solutions/features/calendar.md
@@ -4,6 +4,6 @@ extra:
=render: false
=---
=
-Calendar good, I guess.
+Calendar functionality enables efficient scheduling and time management for teams and organizations. Essential for European businesses that need to coordinate across different time zones and maintain compliance with local working hour regulations.
=
=<!-- more -->
index f56d6cd..773fe24 100644
--- a/content/solutions/features/encryption.md
+++ b/content/solutions/features/encryption.md
@@ -4,6 +4,6 @@ extra:
=render: false
=---
=
-Encryption is nice.
+Data encryption ensures that sensitive information remains secure and protected from unauthorized access. This feature implements industry-standard encryption protocols to safeguard data both in transit and at rest, helping organizations comply with GDPR and other European data protection regulations. Essential for maintaining data privacy and security in today's digital landscape.
=
=<!-- more -->
index 9086e8f..88518b1 100644
--- a/content/solutions/features/local.md
+++ b/content/solutions/features/local.md
@@ -4,6 +4,6 @@ extra:
=render: false
=---
=
-This program runs locally on desktop computers and doesn't require an internet connection.
+This program runs locally on desktop computers and doesn't require an internet connection. This approach enhances data sovereignty, reduces latency, and ensures continuous operation even during network disruptions. Particularly valuable for organizations that need to maintain full control over their data and operations while complying with local data residency requirements.
=
=<!-- more -->
index d69b15b..087ca92 100644
--- a/content/solutions/features/scripting.md
+++ b/content/solutions/features/scripting.md
@@ -4,6 +4,6 @@ extra:
=render: false
=---
=
-You can extend the functionality of this solution via scripts, i.e. programs that run inside the main application.
+You can extend the functionality of this solution via scripts, i.e. programs that run inside the main application. This enables automation of repetitive tasks, integration with other systems, and creation of custom workflows tailored to specific business needs. Particularly useful for SMEs that require flexible solutions that can adapt to their unique operational requirements and regulatory compliance needs.
=
=<!-- more -->

Update existing and added new features. Realigned features in the email category.

On by jnuytten

index 773fe24..4687f16 100644
--- a/content/solutions/features/encryption.md
+++ b/content/solutions/features/encryption.md
@@ -4,6 +4,7 @@ extra:
=render: false
=---
=
-Data encryption ensures that sensitive information remains secure and protected from unauthorized access. This feature implements industry-standard encryption protocols to safeguard data both in transit and at rest, helping organizations comply with GDPR and other European data protection regulations. Essential for maintaining data privacy and security in today's digital landscape.
+Encryption ensures that sensitive information remains secure and protected from unauthorized access. This feature implements protocols to safeguard data while it is being sent over the internet or when it is saved on a server. When an email service or a cloud storage solution provides encryption, this means that only users having access to a digital key can read the data.
+Encryption is essential for maintaining data privacy and security in today's digital landscape, and it is a hard requirement of the EU GDPR (privacy) regulations.
=
=<!-- more -->
new file mode 100644
index 0000000..1161c3c
--- /dev/null
+++ b/content/solutions/features/eu-jurisdiction.md
@@ -0,0 +1,9 @@
+---
+extra:
+  moji: "flag: European Union"
+render: false
+---
+
+Companies which have their headquarters in the European Union fall under jurisdiction of the EU and the member state they are in. They are obliged to follow EU law, EU court orders and of course the law of the particular member state they are in. The EU has one of the strictest privacy regulations. Also many checks and balances are in place which make EU regulations and policies slow to change, but this ensures that extremes can be stopped very effectively.
+
+<!-- more -->
new file mode 100644
index 0000000..c2a0a41
--- /dev/null
+++ b/content/solutions/features/imap.md
@@ -0,0 +1,10 @@
+---
+extra:
+  moji: ""
+render: false
+---
+
+IMAP stands for Internet Message Access Protocol. This is a communication protocol used to synchronize email in between a server and one (or multiple) clients such as a laptop or a smartphone.
+IMAP is an open standard, which means that email services using IMAP support basically any email client you would like to use.
+
+<!-- more -->
new file mode 100644
index 0000000..e182317
--- /dev/null
+++ b/content/solutions/features/smtp.md
@@ -0,0 +1,10 @@
+---
+extra:
+  moji: ""
+render: false
+---
+
+SMTP stands for Simple Mail Transfer Protocol. This is a communication protocol used to send email over the internet. 
+SMTP is an open standard, which means that email services using SMTP support basically any email client you would like to use.
+
+<!-- more -->
index 3c87c67..ce93c85 100644
--- a/content/solutions/features/trump-supporters.md
+++ b/content/solutions/features/trump-supporters.md
@@ -4,8 +4,8 @@ extra:
=  moji: "trump"
=---
=
-The company behind this solution or some members of it's leadership actively support Donald Trump's regime in USA (financially or through actions beyond what is strictly required by law).
+The company behind this solution or some members of it's leadership actively support Donald Trump's regime in USA, financially or through actions beyond what is strictly required by law.
=
=<!-- more -->
=
-The current regime in USA is authoritarian and hostile toward Europe. We believe that supporting companies that align with it is dangerous to our societies.
+The current regime in USA is authoritarian and hostile toward Europe. We believe that supporting companies that align with it are dangerous to our societies.
index 835ccd5..7d9f9e0 100644
--- a/content/solutions/features/us-jurisdiction.md
+++ b/content/solutions/features/us-jurisdiction.md
@@ -4,6 +4,6 @@ extra:
=render: false
=---
=
-Stepping into the lions den?
+Companies which have their headquarters in the United States fall under US jurisdiction, in other words they are obliged to follow US law, US court orders and even US presidential orders. This creates a major risk for non-US companies and individuals if democratic, human rights or privacy values are no longer respected by the US.
=
=<!-- more -->
index 0a0bac5..fb95bbd 100644
--- a/content/solutions/gmail/index.md
+++ b/content/solutions/gmail/index.md
@@ -5,9 +5,13 @@ taxonomies:
=  solution_providers: [ "Alphabet" ]
=  solution_categories: [ "email provider" ]
=  features: 
-    - "part of Google Workspace suite"
+    - "Big Tech"
=    - "US jurisdiction"
-    - "Active supporter of the anti-DEI movement"
+    - "Trump Supporters"
+    - "Calendar"
+    - "Free Plan"
+    - "SMTP"
+    - "IMAP"
=
=---
=
index 6dd0278..2c3a57f 100644
--- a/content/solutions/m365-mail/index.md
+++ b/content/solutions/m365-mail/index.md
@@ -5,9 +5,12 @@ taxonomies:
=  solution_providers: [ "Microsoft" ]
=  solution_categories: [ "email provider" ]
=  features: 
-    - "part of M365 Office suite"
-    - "enterprise focus"
+    - "Big Tech"
=    - "US jurisdiction"
+    - "Trump supporters"
+    - "Calendar"
+    - "SMTP"
+    - "IMAP"
=---
=
=Microsoft offers email for business as part of its Microsoft 365 line of services.
index 4598b8d..87ef451 100644
--- a/content/solutions/mailfence/index.md
+++ b/content/solutions/mailfence/index.md
@@ -5,10 +5,12 @@ taxonomies:
=  solution_providers: [ "ContactOffice Group sa" ]
=  solution_categories: [ "email provider" ]
=  features: 
-    - "open standards: imap, smtp"
-    - "encryption with OpenPGP"
-    - "calendar"
-    - "free plan"
+    - "EU jurisdiction"
+    - "Encryption"
+    - "Calendar"
+    - "Free plan"
+    - "IMAP"
+    - "SMTP"
=  esc_support: [ "consulting", "integration" ]
=---
=
index 1f7844e..dc561fd 100644
--- a/content/solutions/proton-mail/index.md
+++ b/content/solutions/proton-mail/index.md
@@ -5,9 +5,10 @@ taxonomies:
=  solution_providers: [ "Proton AG" ]
=  solution_categories: [ "email provider" ]
=  features: 
-    - "encryption"
-    - "calendar"
-    - "free plan"
+    - "Switzerland jurisdiction"
+    - "Encryption"
+    - "Calendar"
+    - "Free plan"
=  esc_support: [ "consulting", "integration" ]
=---
=
index 3e7bc0c..bbdb990 100644
--- a/content/solutions/transip-mail/index.md
+++ b/content/solutions/transip-mail/index.md
@@ -5,8 +5,10 @@ taxonomies:
=  solution_providers: [ "Transip" ]
=  solution_categories: [ "email provider" ]
=  features: 
-    - "open standards: imap, smtp"
-    - "just email"
+    - "EU jurisdiction"
+    - "IMAP"
+    - "SMTP"
+    - "Just email"
=  esc_support: [ "consulting", "integration" ]
=---
=
index 4dba12c..b532dad 100644
--- a/content/solutions/tuta-mail/index.md
+++ b/content/solutions/tuta-mail/index.md
@@ -5,9 +5,10 @@ taxonomies:
=  solution_providers: [ "Tutao HmbH" ]
=  solution_categories: [ "email provider" ]
=  features: 
-    - "encryption"
-    - "calendar"
-    - "free plan"
+    - "EU jurisdiction"
+    - "Encryption"
+    - "Calendar"
+    - "Free plan"
=  esc_support: [ "consulting", "integration" ]
=---
=

Added feature just-email, realigned some of the other features and added icons.

On by jnuytten

similarity index 100%
rename from content/solutions/features/eu-jurisdiction.md
rename to content/solutions/features/eu-based.md
index c2a0a41..5592ac9 100644
--- a/content/solutions/features/imap.md
+++ b/content/solutions/features/imap.md
@@ -1,6 +1,6 @@
=---
=extra:
-  moji: ""
+  moji: "envelope-with-arrow"
=render: false
=---
=
new file mode 100644
index 0000000..7477b58
--- /dev/null
+++ b/content/solutions/features/just-email.md
@@ -0,0 +1,10 @@
+---
+extra:
+  moji: "e-mail"
+render: false
+---
+
+With "just email" we mean that this service offers a cheap, reliable email solution without much bells and whistles. For many companies and individuals "just a mailbox" is all they need: no calendar, no shared mailbox, no fine grained access management, etc.
+If email is all you need, you can save a lot of money migrating from Gmail or M365 to any of the solutions with this feature.
+
+<!-- more -->
index e182317..77270f7 100644
--- a/content/solutions/features/smtp.md
+++ b/content/solutions/features/smtp.md
@@ -1,6 +1,6 @@
=---
=extra:
-  moji: ""
+  moji: "outbox-tray"
=render: false
=---
=
new file mode 100644
index 0000000..6a04119
--- /dev/null
+++ b/content/solutions/features/switzerland-based.md
@@ -0,0 +1,9 @@
+---
+extra:
+  moji: "flag: Switzerland"
+render: false
+---
+
+Companies which have their headquarters in Switzerland fall under the jurisdiction of this country. They are obliged to follow Swiss law and Swiss court orders. Switzerland has currently very strict privacy regulations, and it has a history of neutrality which goes back centuries. ESC advices that Switzerland is a very safe country to store your data.
+
+<!-- more -->
similarity index 100%
rename from content/solutions/features/us-jurisdiction.md
rename to content/solutions/features/us-based.md

Added solutions Odoo Community and Odoo

On by jnuytten

index fb95bbd..22ae84b 100644
--- a/content/solutions/gmail/index.md
+++ b/content/solutions/gmail/index.md
@@ -6,7 +6,7 @@ taxonomies:
=  solution_categories: [ "email provider" ]
=  features: 
=    - "Big Tech"
-    - "US jurisdiction"
+    - "US based"
=    - "Trump Supporters"
=    - "Calendar"
=    - "Free Plan"
index 2c3a57f..bfa3e77 100644
--- a/content/solutions/m365-mail/index.md
+++ b/content/solutions/m365-mail/index.md
@@ -6,7 +6,7 @@ taxonomies:
=  solution_categories: [ "email provider" ]
=  features: 
=    - "Big Tech"
-    - "US jurisdiction"
+    - "US based"
=    - "Trump supporters"
=    - "Calendar"
=    - "SMTP"
index 87ef451..6f73db5 100644
--- a/content/solutions/mailfence/index.md
+++ b/content/solutions/mailfence/index.md
@@ -5,7 +5,7 @@ taxonomies:
=  solution_providers: [ "ContactOffice Group sa" ]
=  solution_categories: [ "email provider" ]
=  features: 
-    - "EU jurisdiction"
+    - "EU based"
=    - "Encryption"
=    - "Calendar"
=    - "Free plan"
index f6ac974..192e882 100644
--- a/content/solutions/ms-word/index.md
+++ b/content/solutions/ms-word/index.md
@@ -8,7 +8,7 @@ taxonomies:
=    - "scripting"
=    - "local"
=    - "proprietary"
-    - "USA based"
+    - "US based"
=    - "big tech"
=    - "Trump supporters"
=---
new file mode 100644
index 0000000..0dea999
--- /dev/null
+++ b/content/solutions/odoo-community/index.md
@@ -0,0 +1,24 @@
+---
+title: Odoo Community
+  
+taxonomies:
+  solution_providers: [ "Odoo Community Association" ]
+  solution_categories: [ "ERP" ]
+  features: 
+    - "Switzerland based"
+    - "Open source"
+    - "Self hosting"
+  esc_support: [ "consulting", "integration" ]
+---
+
+Odoo Community is an open-source ERP system which can be self-hosted. It provides many applications including contact management, invoicing, CRM, HRM and more.
+
+<!-- more -->
+
+Esc Collective offers commercial support for business switching to Odoo Community in form of consulting and integration. We can help set up Odoo Community on a virtual server, and get you started with this great open source ERP. Please note that we do not develop any custom functionalities and custom integrations, we can however help you to identify your needs and identify a partner to develop needed customisations.
+
+Odoo Community is open source, and released under the LGPLv3 license. It features a list of standard applications but it has an app store with additional free and paid apps.
+
+Please note that to have the latest features, more extensive functionalities such as full accounting, and support from Odoo (the company) you must switch to Odoo Enterprise. We recommend Odoo Community only to small organisations with limited functional requirements.
+
+Home page: <https://odoo-community.org/>
new file mode 100644
index 0000000..6ef9d19
--- /dev/null
+++ b/content/solutions/odoo/index.md
@@ -0,0 +1,31 @@
+---
+title: Odoo
+  
+taxonomies:
+  solution_providers: [ "Odoo SA" ]
+  solution_categories: [ "ERP" ]
+  features: 
+    - "EU based"
+    - "Self hosting"
+    - "Cloud hosting"
+    - "Free plan"
+  esc_support: [ "consulting" ]
+---
+
+Odoo is an ERP system providing many applications including contact management, invoicing, accounting, CRM, HRM and more.
+
+<!-- more -->
+
+Esc Collective offers commercial support for business switching to Odoo in form of consulting. We can help you decide whether Odoo is the righ ERP for your company, we can evaluate the type of licenses and installation that is suitable for you, and we can help you to configure Odoo to meet your company requirements. Since we are not an official partner, we can give independent advice to ensure your Odoo implementation is cost effective.
+Please note that we do not develop any custom functionalities and custom integrations, we can however help you to identify your needs and identify a partner to develop needed customisations.
+
+The main difference with Odoo Community edition is that in this commercial (enterprise) edition you get more applications, for example accounting, and much more extensive functionalities in general.
+
+Odoo has been demonstrated to work for companies from all sizes: going from a single user up to enterprise implementations of over 300.000 users. Major companies such as KPMG have switched most of their clients to use Odoo for accounting.
+This ERP can be self-hosted, but this is only recommended for very large organizations. Odoo and third party providers offer shared and dedicated hosting solutions.
+
+Odoo has a free plan in which you can use one of their applications, with an unlimited number of users for always. Also they offer a trial version for 30 days during which you can test all the features.
+
+Odoo is a very big company with divisions all over the world. This certainly has disadvantages, but on the other hand the functionalities offered on this platform are so extensive that they are impossible to develop by a small local company or an open source initiative. On the bright side their sales team and their technical support team is compared to US big tech very responsive, and there is a big eco system of system integrators, software development companies and freelancers offering paid services in Odoo.
+
+Home page: <https://odoo.com>
index dc561fd..3203ad9 100644
--- a/content/solutions/proton-mail/index.md
+++ b/content/solutions/proton-mail/index.md
@@ -5,7 +5,7 @@ taxonomies:
=  solution_providers: [ "Proton AG" ]
=  solution_categories: [ "email provider" ]
=  features: 
-    - "Switzerland jurisdiction"
+    - "Switzerland based"
=    - "Encryption"
=    - "Calendar"
=    - "Free plan"
index bbdb990..367a4db 100644
--- a/content/solutions/transip-mail/index.md
+++ b/content/solutions/transip-mail/index.md
@@ -5,7 +5,7 @@ taxonomies:
=  solution_providers: [ "Transip" ]
=  solution_categories: [ "email provider" ]
=  features: 
-    - "EU jurisdiction"
+    - "EU based"
=    - "IMAP"
=    - "SMTP"
=    - "Just email"
index b532dad..0663f65 100644
--- a/content/solutions/tuta-mail/index.md
+++ b/content/solutions/tuta-mail/index.md
@@ -5,7 +5,7 @@ taxonomies:
=  solution_providers: [ "Tutao HmbH" ]
=  solution_categories: [ "email provider" ]
=  features: 
-    - "EU jurisdiction"
+    - "EU based"
=    - "Encryption"
=    - "Calendar"
=    - "Free plan"