diff --git a/README.md b/README.md
index bbc39eed756e6af2e53904dc5815f5ea21150517..b43faffb69978d1327961b245e848af29a2f1fa5 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,8 @@ To check out docs and examples, visit [gitlab.schukai.com/oss/bob](https://gitla
 ## Installation
 
 ```bash
-wget -O ~/.local/bin/bob http://download.schukai.com/tools/bob/bob-$( uname -s | tr [:upper:] [:lower:])-$(echo `uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/`) && chmod u+x ~/.local/bin/bob
+wget -O ~/.local/bin/bob \
+          http://download.schukai.com/tools/bob/bob-$( uname -s | tr [:upper:] [:lower:])-$(echo `uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/`) && chmod u+x ~/.local/bin/bob
 ```
 
 ### Nix/Flake Support
@@ -24,7 +25,10 @@ using the [nix package manager](https://nixos.org/).
 #### Prepare
 
 ```bash
-bob template prepare --input ./templates/ --output ./output/ --data-file ./data.yaml
+bob template prepare \
+           --input ./templates/    \ 
+           --output ./output/      \
+           --data-file ./data.yaml \
 ```
 
 This will create files in the `./output/` directory with all parsed templates from `./templates/` directory.
@@ -59,6 +63,25 @@ For example, the Monster datatable headers `data-monster-head` are extracted.
     ...
 ``` 
 
+If you want to add Javascript translations, a consecutive ID is assigned if not 
+specified separately. This is not ideal for several reasons. It is better to assign 
+your own ID. To be independent of HTML, JavaScript and CSS selectors, there is a 
+separate attribute `data-bob-reference` for this.
+
+```html
+<script type="application/json" 
+        data-monster-role="translations" 
+        data-bob-reference="my-translation">
+{
+     "key1": "value1",
+     "key2": "value2",
+     "key3": {
+          "one": "value3",
+          "two": "value4"
+     }
+}
+```
+
 
 #### HTML
 
@@ -161,7 +184,8 @@ sync:
         - title
 ```
 
-With the above specification, the `head` node from `./source.html` will be synced to all files in `./` except `./source.html`.
+With the above specification, the `head` node from `./source.html` will 
+be synced to all files in `./` except `./source.html`.
 Furthermore, the `title` node will be kept.
 
 Relative paths are relative to the specification file. Absolute paths are absolute, obviously.
diff --git a/devenv.nix b/devenv.nix
index 7892c37d46234c732f02172885d179b1d37f500e..b5bfeca8dc573ce238fb04d2cd1e9946a8625182 100644
--- a/devenv.nix
+++ b/devenv.nix
@@ -33,6 +33,7 @@
     httpie
     netcat
     memcached
+    treefmt
     fd    
   ];
 
diff --git a/examples/example1/pages/en.yaml b/examples/example1/pages/en.yaml
deleted file mode 100755
index f49c00bad86635af2eba4486b860b0d12c336fc6..0000000000000000000000000000000000000000
--- a/examples/example1/pages/en.yaml
+++ /dev/null
@@ -1,249 +0,0 @@
-test.html:
-    export: en/test.html
-    lang: en
-    title: Bad Request!!!
-    meta:
-        author: schukai GmbH
-        description: The request was malformed or invalid.??
-    images:
-        - id: tickyesdata-image-gi-4013311193
-          source: |-
-            data:image/gif;base64,R0lGODdhEAAQAMwAAPj7+FmhUYjNfGuxYYDJdYTIeanOpT+DOTuANXi/bGOrWj6CONzv2sPjv2Cm
-              V1unU4zPgI/Sg6DJnJ3ImTh8Mtbs00aNP1CZSGy0YqLEn47RgXW8amasW7XWsmmvX2iuXiwAAAAAEAAQAAAFVyAgjmRpnihqGCkpDQ
-              PbGkNUOFk6DZqgHCNGg2T4QAQBoIiRSAwBE4VA4FACKgkB5NGReASFZEmxsQ0whPDi9BiACYQAInXhwOUtgCUQoORFCGt/g4QAIQA7
-          alt: tick
-          title: "yes"
-    anchors:
-        - id: yes-a-html
-          href: /a.html
-          hreflang: ""
-          title: "Yes"
-        - id: QPCI6WO0
-          href: ""
-          hreflang: ""
-          title: ""
-    text:
-        - text: Bad Request
-          id: bad-request
-        - text: The request was incorrect, the server could not process it.
-          id: the-request-was-inco-2640993422
-        - text: Try submitting your
-          id: try-submitting-your
-        - text: request
-          id: request
-        - text: again (sometimes this helps).
-          id: again-sometimes-this-2086042570
-        - text: Contact support if you continue to have problems.
-          id: contact-support-if-y-3404332025
-        - text: |-
-            If you received this message as a result of a request to the server API, then check the structure
-                            against the documentation.
-          id: if-you-received-this-423958995
-        - text: 'You   can   try    the following steps:'
-          id: you-can-try-the-foll-3363859033
-        - text: OID
-          id: id1000
-        - text: date
-          id: id1001
-        - text: username
-          id: id1002
-        - text: customer
-          id: id1003
-        - text: zipcode
-          id: id1004
-        - text: city
-          id: id1005
-        - text: country
-          id: id1006
-        - text: street
-          id: id1007
-        - text: order state
-          id: id1008
-        - text: workflow state
-          id: id1009
-        - text: total
-          id: id1010
-        - text: company
-          id: id1011
-        - text: channel order number
-          id: id1012
-        - text: RESUBMISSIONDATE!!
-          id: id1013
-        - text: payment type
-          id: id1014
-        - text: ','
-          id: id1015
-    translations: []
-    modifications:
-        remove: []
-        add: []
-        attributes: []
-test1.html:
-    export: de/test1.html
-    lang: de
-    title: TESTx
-    meta:
-        author: schukai GmbH
-        description: The request was malformed or invalid.
-    images:
-        - id: tickyesdata-image-gi-4013311193
-          source: |-
-            data:image/gif;base64,R0lGODdhEAAQAMwAAPj7+FmhUYjNfGuxYYDJdYTIeanOpT+DOTuANXi/bGOrWj6CONzv2sPjv2Cm
-              V1unU4zPgI/Sg6DJnJ3ImTh8Mtbs00aNP1CZSGy0YqLEn47RgXW8amasW7XWsmmvX2iuXiwAAAAAEAAQAAAFVyAgjmRpnihqGCkpDQ
-              PbGkNUOFk6DZqgHCNGg2T4QAQBoIiRSAwBE4VA4FACKgkB5NGReASFZEmxsQ0whPDi9BiACYQAInXhwOUtgCUQoORFCGt/g4QAIQA7
-          alt: tick
-          title: "yes"
-    anchors:
-        - id: test-link-test-html
-          href: /test.html
-          hreflang: ""
-          title: test-link
-        - id: yes-a-html
-          href: /a.html
-          hreflang: ""
-          title: "Yes"
-    text:
-        - text: test-link
-          id: test-link
-        - text: Bad xxxx
-          id: bad-xxxx
-        - text: The request was incorrect, the server could not process it.
-          id: the-request-was-inco-2640993422
-        - text: Try submitting your
-          id: try-submitting-your
-        - text: request
-          id: request
-        - text: again (sometimes this helps).
-          id: again-sometimes-this-2086042570
-        - text: Contact support if you continue to have problems.
-          id: contact-support-if-y-3404332025
-        - text: |-
-            If you received this message as a result of a request to the server API, then check the structure
-                            against the documentation.
-          id: if-you-received-this-423958995
-        - text: 'You   can   try    the following steps:'
-          id: you-can-try-the-foll-3363859033
-    translations:
-        - id: ""
-          type: application/json
-          translations:
-            key1: value1
-            key2: value2
-            key3:
-                one: value3
-                two: value4
-    modifications:
-        remove: []
-        add: []
-        attributes: []
-test2.html:
-    export: en/test2.html
-    lang: en
-    title: TEST2
-    meta:
-        author: sch2ukai GmbH
-        description: The 2request was malformed or invalid.
-    images:
-        - id: tickyesdata-image-gi-4013311193
-          source: |-
-            data:image/gif;base64,R0lGODdhEAAQAMwAAPj7+FmhUYjNfGuxYYDJdYTIeanOpT+DOTuANXi/bGOrWj6CONzv2sPjv2Cm
-              V1unU4zPgI/Sg6DJnJ3ImTh8Mtbs00aNP1CZSGy0YqLEn47RgXW8amasW7XWsmmvX2iuXiwAAAAAEAAQAAAFVyAgjmRpnihqGCkpDQ
-              PbGkNUOFk6DZqgHCNGg2T4QAQBoIiRSAwBE4VA4FACKgkB5NGReASFZEmxsQ0whPDi9BiACYQAInXhwOUtgCUQoORFCGt/g4QAIQA7
-          alt: tick
-          title: "yes"
-    anchors:
-        - id: yes-a-html
-          href: /a.html
-          hreflang: ""
-          title: "Yes"
-    text:
-        - text: Bad xxxx
-          id: bad-xxxx
-        - text: The request was incorrect, the server could not process it.
-          id: the-request-was-inco-2640993422
-        - text: Try submitting your
-          id: try-submitting-your
-        - text: request
-          id: request
-        - text: again (sometimes this helps).
-          id: again-sometimes-this-2086042570
-        - text: Contact support if you continue to have problems.
-          id: contact-support-if-y-3404332025
-        - text: |-
-            If you received this message as a result of a request to the server API, then check the structure
-                            against the documentation.
-          id: if-you-received-this-423958995
-        - text: 'You   can   try    the following steps:'
-          id: you-can-try-the-foll-3363859033
-    translations: []
-    modifications:
-        remove: []
-        add: []
-        attributes: []
-test3.html:
-    export: en/test3.html
-    lang: en
-    title: TEST3
-    meta:
-        author: schukai3 GmbH
-        description: The 3request was malformed or invalid.
-    images:
-        - id: tickyesdata-image-gi-4013311193
-          source: |-
-            data:image/gif;base64,R0lGODdhEAAQAMwAAPj7+FmhUYjNfGuxYYDJdYTIeanOpT+DOTuANXi/bGOrWj6CONzv2sPjv2Cm
-              V1unU4zPgI/Sg6DJnJ3ImTh8Mtbs00aNP1CZSGy0YqLEn47RgXW8amasW7XWsmmvX2iuXiwAAAAAEAAQAAAFVyAgjmRpnihqGCkpDQ
-              PbGkNUOFk6DZqgHCNGg2T4QAQBoIiRSAwBE4VA4FACKgkB5NGReASFZEmxsQ0whPDi9BiACYQAInXhwOUtgCUQoORFCGt/g4QAIQA7
-          alt: tick
-          title: "yes"
-    anchors:
-        - id: yes-a-html
-          href: /a.html
-          hreflang: ""
-          title: "Yes"
-    text:
-        - text: Bad xxxx
-          id: bad-xxxx
-        - text: The request was incorrect, the server could not process it.
-          id: the-request-was-inco-2640993422
-        - text: Try submitting your
-          id: try-submitting-your
-        - text: request
-          id: request
-        - text: again (sometimes this helps).
-          id: again-sometimes-this-2086042570
-        - text: Contact support if you continue to have problems.
-          id: contact-support-if-y-3404332025
-        - text: |-
-            If you received this message as a result of a request to the server API, then check the structure
-                            against the documentation.
-          id: if-you-received-this-423958995
-        - text: 'You   can   try    the following steps:'
-          id: you-can-try-the-foll-3363859033
-    translations: []
-    modifications:
-        remove: []
-        add: []
-        attributes: []
-test4.html:
-    export: en/test4.html
-    lang: en
-    title: ""
-    meta: {}
-    images: []
-    anchors:
-        - id: a-html
-          href: a.html
-          hreflang: ""
-          title: ""
-    text:
-        - text: Das ist ein Text
-          id: das-ist-ein-text
-        - text: one-time password
-          id: one-time-password
-        - text: .
-          id: id1016
-    translations: []
-    modifications:
-        remove: []
-        add: []
-        attributes: []
diff --git a/examples/example2/config/snippet.yaml b/examples/example2/config/snippet.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..bb21f81000bdbae6dfdb8f1260594e3536451d84
--- /dev/null
+++ b/examples/example2/config/snippet.yaml
@@ -0,0 +1,13 @@
+snippet:
+  -
+    source: ../template/test.html
+    selector: 'monster-state'
+    destination: ../snippets/meta/container.html
+    attribute:
+      - selector: 'li'
+        name: 'data-state'
+        value: 'monster'
+    replacement:
+      -
+        selector: 'li>span'
+        content: '!!!!!!!!!!!!!!!!!!'
diff --git a/examples/example2/config/spec1.yaml b/examples/example2/config/spec1.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..588884439418acb1b7eb1b7c852d7e331cd125df
--- /dev/null
+++ b/examples/example2/config/spec1.yaml
@@ -0,0 +1,24 @@
+sync:
+
+  - source:
+      path: '../original/test1.html'
+      selector: head
+    destination:
+      path: '../original/'
+      exclude:
+        - ../original/test1.html
+      keep:
+        - title
+        - meta[name=description]
+        - meta[name=author]
+        
+#  - source:
+#      path: '../original/test1.html'
+#      selector: '.deco'
+#    destination:
+#      path: '../original/'
+#      exclude:
+#        - ../original/test1.html
+     
+
+     
diff --git a/examples/example2/pages/en.yaml b/examples/example2/pages/en.yaml
new file mode 100755
index 0000000000000000000000000000000000000000..7e477d0e3bb0536ff28b70fdd2f65f592e531e1c
--- /dev/null
+++ b/examples/example2/pages/en.yaml
@@ -0,0 +1,93 @@
+test.html:
+    export: en/test.html
+    lang: en
+    title: 333333
+    meta:
+        author: 1111
+        description: 22222
+    images:
+        - id: tickyesdata-image-gi-3753669760
+          source: |-
+            data:image/gif;base64,R0lGODdhEAAQAMwAAPj7+FmhUYjNfGuxYYDJdYTIeanOpT+DOTuANXi/bGOrWj6CONzv2sPjv2Cm
+              V1unU4zPgI/Sg6DJnJ3ImTh8Mtbs00aNP1CZSGy0YqLEn47RgXW8amasW7XWsmmvX2iuXiwAAAAAEAAQAAAFVyAgjmRpnihqGCkpDQ
+              PbGkNUOFk6DZqgHCNGg2T4QAQBoIiRSAwBE4VA4FACKgkB5NGReASFZEmxsQ0whPDi9BiACYQAInXhwOUtgCUQoORFCGt/g4QAIQA7
+          alt: tick
+          title: "yes"
+    anchors:
+        - id: mmmm
+          href: mmmm
+          hreflang: ""
+          title: ""
+        - id: yes-a-html
+          href: /a.html
+          hreflang: ""
+          title: "Yes"
+        - id: struct-a-data-monste-1746242989
+          href: ""
+          hreflang: ""
+          title: ""
+    text:
+        - text: Bad Request
+          id: bad-request
+        - text: The request was incorrect, the server could not process it.
+          id: the-request-was-inco-2640993422
+        - text: GURKE
+          id: gurke
+        - text: Try submitting your
+          id: try-submitting-your
+        - text: request
+          id: request
+        - text: again (sometimes this helps).
+          id: again-sometimes-this-2086042570
+        - text: Contact support if you continue to have problems.
+          id: contact-support-if-y-3404332025
+        - text: |-
+            If you received this message as a result of a request to the server API, then check the structure
+                                against the documentation.
+          id: if-you-received-this-423958995
+        - text: 'You can try the following steps:'
+          id: you-can-try-the-foll-3363859033
+        - text: OID
+          id: oid
+        - text: date
+          id: date
+        - text: username
+          id: username
+        - text: customer
+          id: customer
+        - text: zipcode
+          id: zipcode
+        - text: city
+          id: city
+        - text: country
+          id: country
+        - text: street
+          id: street
+        - text: order state
+          id: order-state
+        - text: workflow state
+          id: workflow-state
+        - text: total
+          id: total
+        - text: company
+          id: company
+        - text: channel order number
+          id: channel-order-number
+        - text: resubmissionDate
+          id: resubmission-date
+        - text: payment type
+          id: payment-type
+    translations:
+        - id: the-translation
+          type: application/json
+          translations:
+            key1: translation1
+            key2:
+                other: translation2
+            key3:
+                other: translation3
+            key5: translation4
+    modifications:
+        remove: []
+        add: []
+        attributes: []
diff --git a/examples/example2/snippets/meta/container.html b/examples/example2/snippets/meta/container.html
new file mode 100644
index 0000000000000000000000000000000000000000..2424762369803336a154c2d11af0f787e9c8df13
--- /dev/null
+++ b/examples/example2/snippets/meta/container.html
@@ -0,0 +1,27 @@
+<monster-state>
+    <img width="16" height="16" alt="tick" title="yes" src="data:image/gif;base64,R0lGODdhEAAQAMwAAPj7+FmhUYjNfGuxYYDJdYTIeanOpT+DOTuANXi/bGOrWj6CONzv2sPjv2Cm
+  V1unU4zPgI/Sg6DJnJ3ImTh8Mtbs00aNP1CZSGy0YqLEn47RgXW8amasW7XWsmmvX2iuXiwAAAAAEAAQAAAFVyAgjmRpnihqGCkpDQ
+  PbGkNUOFk6DZqgHCNGg2T4QAQBoIiRSAwBE4VA4FACKgkB5NGReASFZEmxsQ0whPDi9BiACYQAInXhwOUtgCUQoORFCGt/g4QAIQA7" id="tickyesdata-image-gi-4013311193" data-attributes="alt path:content.tickyesdata-image-gi-4013311193.alt,src path:content.tickyesdata-image-gi-4013311193.src,title path:content.tickyesdata-image-gi-4013311193.title"/>
+    <h1 class="deco"><span id="bad-request" data-replace-self="path:text.bad-request.text">Bad Request</span></h1>
+    <p><span id="the-request-was-inco-2640993422" data-replace-self="path:text.the-request-was-inco-2640993422.text">The request was incorrect, the server could not process it.
+    </span></p><p>
+</p><div class="infobox">
+    <div>
+
+        <ul>
+
+            <li data-state="monster">!!!!!!!!!!!!!!!!!!<a href="/a.html" title="Yes" id="yes-a-html" data-attributes="href path:anchors.yes-a-html.href,title path:anchors.yes-a-html.title,hreflang path:anchors.yes-a-html.hreflang">request</a> again (sometimes this helps).</li>
+            <li data-state="monster">!!!!!!!!!!!!!!!!!!</li>
+
+            <li data-state="monster">!!!!!!!!!!!!!!!!!!</li>
+
+
+        </ul>
+
+        <p><span id="you-can-try-the-foll-3363859033" data-replace-self="path:text.you-can-try-the-foll-3363859033.text">You   can   try    the following steps:</span></p>
+        
+    </div>
+</div>
+
+
+</monster-state>
\ No newline at end of file
diff --git a/examples/example2/template/test.html b/examples/example2/template/test.html
new file mode 100644
index 0000000000000000000000000000000000000000..8a4265b264e95287154510ffb592e02fb2397b4b
--- /dev/null
+++ b/examples/example2/template/test.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<html lang="en" data-attributes="lang path:lang">
+<head>
+
+    <style>
+        *:not(:defined) {
+            visibility: hidden;
+        }
+    </style>
+
+    <meta charset="utf-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1"/>
+
+    <meta name="robots" content="noindex"/>
+
+    <link rel="icon" type="image/x-icon" href="/asset/icon/favicon.svg"/>
+    <meta name="theme-color" content="#c10000"/>
+    <link rel="apple-touch-icon" sizes="180x180" href="/asset/icon/apple-touch-icon.png"/>
+    <link rel="icon" type="image/png" sizes="32x32" href="/asset/icon/favicon-32x32.png"/>
+    <link rel="icon" type="image/png" sizes="16x16" href="/asset/icon/favicon-16x16.png"/>
+    <link rel="mask-icon" href="/asset/icon/safari-pinned-tab.svg" color="#fd1d1d"/>
+    <meta name="msapplication-TileColor" content="#fd1d1d"/>
+    <meta name="msapplication-TileImage" content="/asset/icon/mstile-144x144.png"/>
+
+    <title data-replace="path:title">Bad Request</title>
+    <meta name="description" content="The request was malformed or invalid."
+          data-attributes="content path:meta.description"/>
+    <meta name="author" content="schukai GmbH" data-attributes="content path:meta.author"/>
+
+    <link rel="icon" href="/favicon.ico"/>
+    <link rel="icon" href="/favicon.svg" type="image/svg+xml"/>
+    <link rel="apple-touch-icon" href="/apple-touch-icon.png"/>
+
+    <script src="/script/main.mjs" type="module"></script>
+
+    <script type="application/json" data-monster-role="translations" data-bob-reference="the-translation">
+        {
+            "key1": "translation1",
+            "key2": {
+                "other": "translation2"
+            },
+            "key3": {
+                "other": "translation3"
+            },
+            "key5": "translation4"
+        }
+    </script>
+
+
+</head>
+
+<body>
+<header>
+    <div class="gradient"></div>
+</header>
+
+<monster-state>
+    <a href="mmmm"><img width="16" height="16" alt="tick" title="yes" src="data:image/gif;base64,R0lGODdhEAAQAMwAAPj7+FmhUYjNfGuxYYDJdYTIeanOpT+DOTuANXi/bGOrWj6CONzv2sPjv2Cm
+  V1unU4zPgI/Sg6DJnJ3ImTh8Mtbs00aNP1CZSGy0YqLEn47RgXW8amasW7XWsmmvX2iuXiwAAAAAEAAQAAAFVyAgjmRpnihqGCkpDQ
+  PbGkNUOFk6DZqgHCNGg2T4QAQBoIiRSAwBE4VA4FACKgkB5NGReASFZEmxsQ0whPDi9BiACYQAInXhwOUtgCUQoORFCGt/g4QAIQA7"
+         id="tickyesdata-image-gi-4013311193"
+         data-attributes="alt path:content.tickyesdata-image-gi-4013311193.alt,src path:content.tickyesdata-image-gi-4013311193.src,title path:content.tickyesdata-image-gi-4013311193.title"/></a>
+    <h1 class="deco">Bad Request</h1>
+    <p>The request was incorrect, the server could not process it.
+    </p>
+    <p>
+    </p>
+
+    <a href="mmmm">GURKE</a>
+    
+    <div class="infobox">
+        <div>
+
+            <ul>
+
+                <li>Try submitting your<a href="/a.html" title="Yes" id="yes-a-html"
+                                          data-attributes="href path:anchors.yes-a-html.href,title path:anchors.yes-a-html.title,hreflang path:anchors.yes-a-html.hreflang">request</a>
+                    again (sometimes this helps).
+                </li>
+                <li>Contact support if you continue to have problems.</li>
+
+                <li>If you received this message as a result of a request to the server API, then check the structure
+                    against the documentation.
+                </li>
+
+
+            </ul>
+
+            <p>You can try the following steps:</p>
+            <p>You can try the following steps:</p>
+
+        </div>
+    </div>
+
+
+</monster-state>
+
+
+<monster-datatable id="datatable-order-list" data-monster-datasource-selector="#orderListDatasource">
+    <template id="datatable-order-list-row">
+        <div data-monster-head="OID" data-monster-mode="fixed" data-monster-sortable="oid"
+             data-monster-grid-template="0.5fr"><a
+                data-monster-attributes="href path:datatable-order-list-row.oid | tostring | prefix:/app/commerce/order-detail?oid="
+                data-monster-replace="path:datatable-order-list-row.oid"></a></div>
+        <div data-monster-head="date" data-monster-sortable="orderDate"
+             data-monster-replace="path:datatable-order-list-row.orderDate | datetimeformat"></div>
+        <div data-monster-head="username" data-monster-mode="hidden"
+             data-monster-replace="path:datatable-order-list-row.userName"></div>
+        <div data-monster-head="customer">auto</div>
+        <div data-monster-head="zipcode"
+             data-monster-replace="path:datatable-order-list-row.deliveryAddressZipcode"></div>
+        <div data-monster-head="city" data-monster-replace="path:datatable-order-list-row.deliveryAddressCity"></div>
+        <div data-monster-head="city" data-monster-replace="path:datatable-order-list-row.deliveryAddressCity"></div>
+        <div data-monster-head="country"
+             data-monster-replace="path:datatable-order-list-row.deliveryAddressCountry | prefix:country_ | i18n"></div>
+        <div data-monster-head="street" data-monster-mode="hidden"
+             data-monster-replace="path:datatable-order-list-row.deliveryAddressAddress1"></div>
+        <div data-monster-head="order state" data-monster-align="end"></div>
+        <div data-monster-head="workflow state" data-monster-align="end"
+             data-monster-replace="path:datatable-order-list-row.workflowState"></div>
+        <div data-monster-head="total" data-monster-align="end"></div>
+        <div data-monster-head="company" data-monster-grid-template="0.8fr"
+             data-monster-replace="path:datatable-order-list-row.companySHID | tostring | prefix:&lt;img src=&#39;/alvine/upload/company/files/ | suffix:/shopicon.gif&#39;&gt;"></div>
+        <div data-monster-head="channel order number"
+             data-monster-replace="path:datatable-order-list-row.channelOrderID"></div>
+        <div data-monster-head="resubmissionDate" data-monster-mode="hidden"></div>
+        <div data-monster-head="payment type"
+             data-monster-replace="path:datatable-order-list-row.localStrings.paymentType"></div>
+    </template>
+</monster-datatable>
+
+
+</body>
+</html>
\ No newline at end of file
diff --git a/flake.nix b/flake.nix
index c2e4d488fc1be967dfe1d2fd6ab039213be8457a..22aa4fa81faec05ada91781d868fb9e34bb91579 100644
--- a/flake.nix
+++ b/flake.nix
@@ -28,23 +28,66 @@
       packages = forAllSystems (system:
         let
           pkgs = nixpkgsFor.${system};
-          hashes = import ./hashes.nix;
+
+          
+ projectDefinition = import ./project.nix;
+
+              projectHash =
+                if (self ? shortRev)
+                then self.shortRev
+                else "dirty";
+
+              lastModifiedDate =
+                self.lastModifiedDate or self.lastModified or "19700101";
+              sourcePath = ././source;       
+          
         in
         {
           bob = pkgs.buildGoModule {
-            name = "bob";
+            pname = projectDefinition.name;
+            version = projectDefinition.version;
             # In 'nix develop', we don't need a copy of the source tree
             # in the Nix store.
-            src = ././source;
-           vendorSha256 = hashes.hashValue;
+            src = sourcePath;
+           
+
+ tags = []; # add your tags here (eq  "netgo" "osusergo" "static_build")
+
+                ldflags = [
+                  "-X '${projectDefinition.modulePath}/internal/release.version=${projectDefinition.version}'"
+                  "-X '${projectDefinition.modulePath}/internal/release.commit=${projectHash}'"
+                  "-X '${projectDefinition.modulePath}/internal/release.name=${projectDefinition.name}'"
+                  "-X '${projectDefinition.modulePath}/internal/release.mnemonic=${projectDefinition.mnemonic}'"
+                  "-X '${projectDefinition.modulePath}/internal/release.buildDate=${lastModifiedDate}'"
+                ];
 
 
-            meta = with nixpkgs.legacyPackages.${system}.lib; {
-                description = "Bob: The HTML and HTML fragment builder";
-                homepage = "https://gitlab.schukai.com/oss/bob";
+              
+vendorHash = projectDefinition.vendorHash;
+                proxyVendor = true;
+
+                meta = with system.lib; {
+                  description = projectDefinition.description;
+                  homepage = "https://" + projectDefinition.modulePath;
                 license = licenses.mit;
-                maintainers = with maintainers; [ "schukai GmbH" ];
-              };    
+                  maintainers = with maintainers; ["schukai GmbH"];
+                };
+
+                buildInputs = [pkgs.bash];
+                nativeBuildInputs = with system; [pkgs.alejandra pkgs.shellcheck pkgs.shfmt];
+
+                doCheck = true;
+
+                checkPhase = ''
+                  cd ${sourcePath}
+                  if ! go test -v ./... 2>&1 | cat ; then
+                    echo "Test failed."
+                    exit 1
+                  fi
+
+                  echo "Test passed: '$out' is as expected."
+                '';              
+               
 
           };
         });
diff --git a/hashes.nix b/hashes.nix
deleted file mode 100644
index 2c9ec8b6db4162cb3aa5143562e5593bf87ca83f..0000000000000000000000000000000000000000
--- a/hashes.nix
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-    hashValue = "sha256-M4pZBhlt0CrRgAf9DxRUVNmA2sYxDYIccreOQy7qmrk=";
-}
diff --git a/project.nix b/project.nix
new file mode 100644
index 0000000000000000000000000000000000000000..a75540c7129d458294481d0ee7064a69b5b78052
--- /dev/null
+++ b/project.nix
@@ -0,0 +1,20 @@
+{
+  name = "Bob";
+  mnemonic = "bob";
+  description = "The HTML and HTML fragment builder.";
+  supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin"];
+  compileForSystems = ["linux/arm64" "linux/amd64" "darwin/amd64" "windows/amd64"];
+  modulePath = "gitlab.schukai.com/oss/bob";
+
+  k8s = {
+    loglevel = "info";
+    port = 80;
+    replicas = 1;
+  };
+
+  registry = "docker-registry.schukai.com:443";
+
+  setup = "done";
+  version = "0.1.0"; ## do not change this line; it will be updated automatically
+  vendorHash = "sha256-M4pZBhlt0CrRgAf9DxRUVNmA2sYxDYIccreOQy7qmrk=";
+}
\ No newline at end of file
diff --git a/source/command.go b/source/command.go
index 97fbc52d1f45bb4956d834cf05d31dbcec5deedd..a04d6d4f702cc4172036f8e041ff9fcd22dc509a 100644
--- a/source/command.go
+++ b/source/command.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"fmt"
+	"github.com/charmbracelet/log"
 	html2 "gitlab.schukai.com/oss/bob/html"
 	template2 "gitlab.schukai.com/oss/bob/template"
 	"gitlab.schukai.com/oss/bob/types"
@@ -15,7 +16,6 @@ import (
 type Definition struct {
 	Help struct {
 	} `command:"help" call:"PrintHelp" description:"Prints this help message"`
-	Verbose  bool `short:"v" long:"verbose" description:"Show verbose debug information"`
 	Template struct {
 		Prepare struct {
 			Input    string `short:"i" long:"input" description:"Directory with html files to prepare" required:"true"`
@@ -106,9 +106,10 @@ func (d *Definition) PrepareTemplate(s *xflags.Settings[Definition]) {
 			}
 		}
 
-		if d.Verbose {
-			fmt.Printf("Loaded data file %s\n", i)
-		}
+		log.Info("Loaded data file " + i)
+
+		// reset all content entries
+		storage.ResetContent()
 	}
 
 	toDelete := []string{}
@@ -131,9 +132,7 @@ func (d *Definition) PrepareTemplate(s *xflags.Settings[Definition]) {
 			return nil
 		}
 
-		if d.Verbose {
-			fmt.Printf("Prepare %s\n", path)
-		}
+		log.Info("Prepare " + path)
 
 		key, err := template2.PrepareHtmlFile(path, d.Template.Prepare.Output, storage)
 		if err != nil {
@@ -152,9 +151,7 @@ func (d *Definition) PrepareTemplate(s *xflags.Settings[Definition]) {
 
 	for _, v := range toDelete {
 
-		if d.Verbose {
-			fmt.Printf("Delete %s\n", v)
-		}
+		log.Info("Delete " + v)
 
 		delete(storage, v)
 	}
@@ -175,9 +172,7 @@ func (d *Definition) PrepareTemplate(s *xflags.Settings[Definition]) {
 			s.AddError(err)
 		}
 
-		if d.Verbose {
-			fmt.Printf("Saved data file %s\n", o)
-		}
+		log.Info("Saved data file " + o)
 
 	}
 
diff --git a/source/go.mod b/source/go.mod
index 61dd0d07f11391d1ebcf5b46c628c616e249190f..4796b7138a928fdbeb7a9610ea893672ac87085b 100644
--- a/source/go.mod
+++ b/source/go.mod
@@ -12,7 +12,19 @@ require (
 )
 
 require (
+	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+	github.com/charmbracelet/lipgloss v0.9.1 // indirect
+	github.com/charmbracelet/log v0.3.1 // indirect
+	github.com/go-logfmt/logfmt v0.6.0 // indirect
+	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+	github.com/mattn/go-isatty v0.0.18 // indirect
+	github.com/mattn/go-runewidth v0.0.15 // indirect
+	github.com/muesli/reflow v0.3.0 // indirect
+	github.com/muesli/termenv v0.15.2 // indirect
+	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/volker-schukai/tokenizer v1.0.0 // indirect
 	gitlab.schukai.com/oss/libraries/go/utilities/data.git v0.2.0 // indirect
 	gitlab.schukai.com/oss/libraries/go/utilities/pathfinder v0.9.2 // indirect
+	golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
+	golang.org/x/sys v0.16.0 // indirect
 )
diff --git a/source/go.sum b/source/go.sum
index 3775078da48514fbb1980e1637f506e01480ba82..b91bd4533bdd9ea8d1dfcb7a83470805f8e70aea 100644
--- a/source/go.sum
+++ b/source/go.sum
@@ -2,10 +2,32 @@ github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x0
 github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
 github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
 github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
+github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
+github.com/charmbracelet/log v0.3.1 h1:TjuY4OBNbxmHWSwO3tosgqs5I3biyY8sQPny/eCMTYw=
+github.com/charmbracelet/log v0.3.1/go.mod h1:OR4E1hutLsax3ZKpXbgUqPtTjQfrh1pG3zwHGWuuq8g=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
+github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
+github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
+github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
+github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
@@ -44,6 +66,8 @@ golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
 golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
 golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -71,7 +95,10 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
diff --git a/source/html/cut_test.go b/source/html/cut_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..4d603d44e31ed4bd4b5dd1f20295a693aeb4acef
--- /dev/null
+++ b/source/html/cut_test.go
@@ -0,0 +1,159 @@
+package html
+
+import (
+	"bytes"
+	"github.com/andybalholm/cascadia"
+	"gitlab.schukai.com/oss/bob/types"
+	"golang.org/x/net/html"
+	"gopkg.in/yaml.v3"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+// Helper function to create a temporary file with content
+func createTempFile(content string) (string, error) {
+	tmpfile, err := ioutil.TempFile("", "example.*.html")
+	if err != nil {
+		return "", err
+	}
+
+	if _, err := tmpfile.Write([]byte(content)); err != nil {
+		return "", err
+	}
+
+	if err := tmpfile.Close(); err != nil {
+		return "", err
+	}
+
+	return tmpfile.Name(), nil
+}
+
+// TestCutHtml tests the CutHtml function
+func TestCutHtml(t *testing.T) {
+	// Create temporary directory
+	tempDir, err := ioutil.TempDir("", "testcuthtml")
+	if err != nil {
+		t.Fatalf("Failed to create temp dir: %v", err)
+	}
+	defer os.RemoveAll(tempDir)
+
+	// Create a temporary source HTML file
+	sourceHTML := `<div><p class="content">Original content</p></div>`
+	sourcePath := filepath.Join(tempDir, "source.html")
+	if err := ioutil.WriteFile(sourcePath, []byte(sourceHTML), 0644); err != nil {
+		t.Fatalf("Failed to write source HTML file: %v", err)
+	}
+
+	// Create a SnippetsSpecification in YAML format
+	spec := types.SnippetsSpecification{
+		Snippets: []types.Snippet{
+			{
+				Source:      sourcePath,
+				Destination: sourcePath,
+				Replacement: []types.ContentReplacement{
+					{
+						Selector: ".content",
+						Content:  `<p class="content">Replaced content</p>`,
+					},
+				},
+			},
+		},
+	}
+	specBytes, err := yaml.Marshal(spec)
+	if err != nil {
+		t.Fatalf("Failed to marshal YAML: %v", err)
+	}
+
+	// Write the YAML to a temporary file
+	specPath := filepath.Join(tempDir, "spec.yaml")
+	if err := ioutil.WriteFile(specPath, specBytes, 0644); err != nil {
+		t.Fatalf("Failed to write spec YAML file: %v", err)
+	}
+
+	// Run the CutHtml function
+	if err := CutHtml(specPath); err != nil {
+		t.Fatalf("CutHtml failed: %v", err)
+	}
+
+	// Verify the result
+	modifiedHTML, err := ioutil.ReadFile(sourcePath)
+	if err != nil {
+		t.Fatalf("Failed to read modified HTML file: %v", err)
+	}
+
+	expectedHTML := `<div><p class="content">Replaced content</p></div>`
+	if string(modifiedHTML) != expectedHTML {
+		t.Errorf("Expected HTML to be '%v', but got '%v'", expectedHTML, string(modifiedHTML))
+	}
+}
+
+// TestSetAttributes tests the setAttributes function
+func TestSetAttributes(t *testing.T) {
+	// Example HTML node
+	rawHTML := `<div><p class="old-class">Hello</p></div>`
+	node, _ := html.Parse(strings.NewReader(rawHTML))
+	attrs := []types.Attributes{{Selector: "p", Name: "class", Value: "new-class"}}
+
+	// Perform the attribute setting
+	err := setAttributes(node, attrs)
+	if err != nil {
+		t.Errorf("setAttributes failed: %v", err)
+	}
+
+	// Check if the attribute was set correctly
+	query, _ := cascadia.Compile("p")
+	pNode := query.MatchFirst(node)
+	if pNode == nil {
+		t.Errorf("p node not found")
+	} else if pNode.Attr[0].Val != "new-class" {
+		t.Errorf("Attribute not set correctly, got: %s, want: new-class", pNode.Attr[0].Val)
+	}
+}
+
+// TestRemoveAttribute tests the removeAttribute function
+func TestRemoveAttribute(t *testing.T) {
+	// Example attributes
+	attrs := []html.Attribute{{Key: "class", Val: "old-class"}, {Key: "id", Val: "test-id"}}
+
+	// Remove the 'class' attribute
+	updatedAttrs := removeAttribute(attrs, "class")
+
+	// Check if the 'class' attribute is removed
+	for _, attr := range updatedAttrs {
+		if attr.Key == "class" {
+			t.Errorf("Attribute 'class' was not removed")
+		}
+	}
+}
+
+func TestReplaceNodes(t *testing.T) {
+	// Example HTML node
+	rawHTML := `<div><p class="target">Old Content</p><p class="untouched">Don't touch this</p></div>`
+	node, _ := html.Parse(strings.NewReader(rawHTML))
+	replacements := []types.ContentReplacement{
+		{Selector: ".target", Content: "<p>New Content</p>"},
+	}
+
+	// Perform the replacement
+	replacedNode, err := replaceNodes(node, replacements)
+	if err != nil {
+		t.Fatalf("replaceNodes failed: %v", err)
+	}
+
+	// Convert the node back to HTML for easy verification
+	var buf bytes.Buffer
+	html.Render(&buf, replacedNode)
+
+	replacedHTML := buf.String()
+
+	// Expected HTML after replacement
+	expectedHTML := `<html><head></head><body><div><p>New Content</p><p class="untouched">Don't touch this</p></div></body></html>`
+
+	// Verify the replacement
+	if replacedHTML != expectedHTML {
+		t.Errorf("Expected HTML to be '%v', but got '%v'", expectedHTML, replacedHTML)
+	}
+}
diff --git a/source/html/generate.go b/source/html/generate.go
index 40c579600618cfc71caf8cd97a4c98b194f057d3..394c4cd8cebf4af9cf9b1358cfc747c8f88cf14a 100644
--- a/source/html/generate.go
+++ b/source/html/generate.go
@@ -3,6 +3,7 @@ package html
 import (
 	"encoding/json"
 	"github.com/andybalholm/cascadia"
+	"github.com/charmbracelet/log"
 	"gitlab.schukai.com/oss/bob/types"
 	"gitlab.schukai.com/oss/libraries/go/markup/html/engine"
 	"golang.org/x/net/html"
@@ -28,8 +29,10 @@ func GenerateFiles(dataPath, templates, out string) error {
 
 	for name, page := range storage {
 		p := path.Join(templatesDir, name)
+		log.Info("Generating with template " + p)
 		generatedHtml, err := Generate(page, p)
 		if err != nil {
+			log.Error("Error " + err.Error() + " while generating " + p)
 			return err
 		}
 
@@ -37,11 +40,15 @@ func GenerateFiles(dataPath, templates, out string) error {
 
 		dir := path.Dir(outFile)
 		if err := os.MkdirAll(dir, 0755); err != nil {
+
+			log.Error("Error " + err.Error() + " while creating directory " + dir)
 			return err
 		}
 
+		log.Info("Writing " + outFile)
 		err = os.WriteFile(outFile, []byte(generatedHtml), 0644)
 		if err != nil {
+			log.Error("Error " + err.Error() + " while writing " + outFile)
 			return err
 		}
 
@@ -55,10 +62,17 @@ func Generate(data *types.PageData, name string) (string, error) {
 	dataset := make(map[any]any)
 	dataset["lang"] = data.Lang
 	dataset["title"] = data.Title
+	if data.Title == "" {
+		log.Warn("No title set for " + name)
+	}
+
+	log.Info("Generating " + data.Export + " with template " + name)
+
 	dataset["meta"] = make(map[any]any)
 	for k, v := range data.Meta {
 		dataset["meta"].(map[any]any)[k] = v
 	}
+
 	dataset["images"] = make(map[any]any, 0)
 	for _, v := range data.Images {
 		dataset["images"].(map[any]any)[v.Id] = map[any]any{
@@ -112,11 +126,19 @@ func Generate(data *types.PageData, name string) (string, error) {
 
 func doModifications(page *types.PageData, from string) (string, error) {
 
+	log.Info("Modifications for " + page.Export)
+
 	node, err := html.Parse(strings.NewReader(from))
 	if err != nil {
 		return "", err
 	}
 
+	if len(page.Modifications.Remove) == 0 {
+		log.Info("No remove modifications set for " + page.Export)
+	} else {
+		log.Infof("There are %d remove modifications set for %s", len(page.Modifications.Remove), page.Export)
+	}
+
 	for _, m := range page.Modifications.Remove {
 
 		selector, err := cascadia.Parse(m)
@@ -129,7 +151,12 @@ func doModifications(page *types.PageData, from string) (string, error) {
 				n.Parent.RemoveChild(n)
 			}
 		}
+	}
 
+	if len(page.Modifications.Add) == 0 {
+		log.Info("No add modifications set for " + page.Export)
+	} else {
+		log.Infof("There are %d add modifications set for %s", len(page.Modifications.Add), page.Export)
 	}
 
 	for _, m := range page.Modifications.Add {
@@ -153,6 +180,12 @@ func doModifications(page *types.PageData, from string) (string, error) {
 		}
 	}
 
+	if len(page.Modifications.SetAttribute) == 0 {
+		log.Info("No set attribute modifications set for " + page.Export)
+	} else {
+		log.Infof("There are %d set attribute modifications set for %s", len(page.Modifications.SetAttribute), page.Export)
+	}
+
 	for _, m := range page.Modifications.SetAttribute {
 		selector, err := cascadia.Parse(m.Selector)
 		if err != nil {
@@ -170,6 +203,8 @@ func doModifications(page *types.PageData, from string) (string, error) {
 					Val: m.Value,
 				})
 
+				log.Info("Set attribute " + m.Name + " to " + m.Value + " for " + page.Export)
+
 				n.Attr = attr
 			}
 		}
diff --git a/source/template/prepare.go b/source/template/prepare.go
index aa75cc21464b5cd0c39e4bf596b03510042e7816..b5d09495103e8074e943225941b17da073b2b7ec 100644
--- a/source/template/prepare.go
+++ b/source/template/prepare.go
@@ -1,15 +1,18 @@
 package template
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"github.com/andybalholm/cascadia"
+	"github.com/charmbracelet/log"
 	"gitlab.schukai.com/oss/bob/constants"
 	"gitlab.schukai.com/oss/bob/types"
 	"gitlab.schukai.com/oss/bob/util"
 	"golang.org/x/net/html"
 	"golang.org/x/net/html/atom"
 	"path"
+	"regexp"
 	"strings"
 )
 
@@ -32,11 +35,6 @@ func removeAttribute(attrs []html.Attribute, key string) []html.Attribute {
 	return result
 }
 
-//func setDataAttribute(node *html.Node, name, attribute, instruction string) {
-//	node.Attr = removeAttribute(node.Attr, name)
-//	node.Attr = append(node.Attr, html.Attribute{Key: name, Val: attribute + " " + instruction})
-//}
-
 func setDataAttributesAttribute(node *html.Node, name, attribute, instruction string) {
 
 	value := util.GetAttribute(node.Attr, attributeAttributes)
@@ -164,27 +162,65 @@ func prepareAnchors(node *html.Node, storage *types.PageData) {
 		return
 	}
 
-	copyOfAnchors := make([]types.Anchor, len(storage.Anchors))
-	copy(copyOfAnchors, storage.Anchors)
-	storage.Anchors = []types.Anchor{}
-
 	for _, n := range list {
 
+		logContent := buildLogContent(n, "")
+		log.Info("Found a node: " + logContent)
+
 		title := util.GetAttribute(n.Attr, "title")
 		hreflang := util.GetAttribute(n.Attr, "hreflang")
 		href := util.GetAttribute(n.Attr, "href")
 
-		id := util.GetOrCreateReference(n, title+hreflang+href)
+		k := title + hreflang + href
+		if k == "" {
+			log.Warn("The anchor '" + logContent + "' has no title, hreflang and href attribute! Will use the content of the anchor as key.")
+			k = logContent
+		}
+
+		id := util.GetOrCreateReference(n, k)
 		if id == "" {
-			id, _ = util.RandomString(8)
-			n.Attr = removeAttribute(n.Attr, constants.DataBobReferenceAttributeKey)
-			n.Attr = append(n.Attr, html.Attribute{Key: constants.DataBobReferenceAttributeKey, Val: id})
+			id = util.GetNextId()
+		}
+
+		// check if id is already in use
+		found := false
+		sameContent := false
+		for _, t := range storage.Anchors {
+			if t.Id == id {
+				// check if the content is the same
+				if t.Title == title && t.HrefLang == hreflang && t.Href == href {
+					sameContent = true
+					continue
+				}
+				found = true
+				break
+			}
+		}
+
+		if sameContent {
+			log.Info("The ID " + id + " for the anchor '" + logContent + "' is already in use and the content is the same! Will use this ID for the anchor.")
+		} else if found {
+			originalId := id
+			id = util.GetNextId()
+			log.Warn("We want to use the ID " + originalId + " for the anchor '" + logContent + "', but this ID is already in use! " +
+				"This could be a problem if you want to use the anchor in a translation. " +
+				"Please change the anchor or add the attribute '" + constants.DataBobReferenceAttributeKey + "' to the tag. " +
+				"Will use now use ID: " + id + " instead.")
 		}
 
 		setDataAttributesAttribute(n, attributeAttributes, "href", "path:anchors."+id+".href")
 		setDataAttributesAttribute(n, attributeAttributes, "title", "path:anchors."+id+".title")
 		setDataAttributesAttribute(n, attributeAttributes, "hreflang", "path:anchors."+id+".hreflang")
 
+		n.Attr = removeAttribute(n.Attr, constants.DataBobReferenceAttributeKey)
+		n.Attr = append(n.Attr, html.Attribute{Key: constants.DataBobReferenceAttributeKey, Val: id})
+
+		if sameContent {
+			continue
+		}
+
+		log.Info("Added anchor: " + id + " to storage")
+
 		storage.Anchors = append(storage.Anchors, types.Anchor{
 			Id:       id,
 			Title:    title,
@@ -194,20 +230,6 @@ func prepareAnchors(node *html.Node, storage *types.PageData) {
 
 	}
 
-	for _, anchor := range copyOfAnchors {
-		foundIndex := -1
-		for index, i := range storage.Images {
-			if i.Id == anchor.Id {
-				foundIndex = index
-				break
-			}
-		}
-
-		if foundIndex > -1 {
-			storage.Anchors[foundIndex] = anchor
-		}
-	}
-
 }
 
 func prepareImages(node *html.Node, storage *types.PageData) {
@@ -222,27 +244,57 @@ func prepareImages(node *html.Node, storage *types.PageData) {
 		return
 	}
 
-	copyOfImages := make([]types.Image, len(storage.Images))
-	copy(copyOfImages, storage.Images)
-	storage.Images = []types.Image{}
-
 	for _, n := range list {
 
+		logContent := buildLogContent(n, "")
+		log.Info("Found img node: " + logContent)
+
 		alt := util.GetAttribute(n.Attr, "alt")
 		title := util.GetAttribute(n.Attr, "title")
 		source := util.GetAttribute(n.Attr, "src")
 
 		id := util.GetOrCreateReference(n, alt+title+source)
 		if id == "" {
-			id, _ = util.RandomString(8)
-			n.Attr = removeAttribute(n.Attr, constants.DataBobReferenceAttributeKey)
-			n.Attr = append(n.Attr, html.Attribute{Key: constants.DataBobReferenceAttributeKey, Val: id})
+			id = util.GetNextId()
 		}
 
+		// check if id is already in use
+		found := false
+		sameContent := false
+		for _, t := range storage.Images {
+			if t.Id == id {
+				// check if the content is the same
+				if t.Alt == alt && t.Title == title && t.Source == source {
+					sameContent = true
+					continue
+				}
+				found = true
+				break
+			}
+		}
+
+		if sameContent {
+			log.Info("The ID " + id + " for the image '" + logContent + "' is already in use and the content is the same! Will use this ID for the image.")
+		} else if found {
+			originalId := id
+			id = util.GetNextId()
+			log.Warn("We want to use the ID " + originalId + " for the image '" + logContent + "', but this ID is already in use! " +
+				"This could be a problem if you want to use the image in a translation. " +
+				"Please change the image or add the attribute '" + constants.DataBobReferenceAttributeKey + "' to the tag. " +
+				"Will use now use ID: " + id + " instead.")
+		}
+
+		n.Attr = removeAttribute(n.Attr, constants.DataBobReferenceAttributeKey)
+		n.Attr = append(n.Attr, html.Attribute{Key: constants.DataBobReferenceAttributeKey, Val: id})
+
 		setDataAttributesAttribute(n, attributeAttributes, "src", "path:content."+id+".src")
 		setDataAttributesAttribute(n, attributeAttributes, "alt", "path:content."+id+".alt")
 		setDataAttributesAttribute(n, attributeAttributes, "title", "path:content."+id+".title")
 
+		if sameContent {
+			continue
+		}
+
 		storage.Images = append(storage.Images, types.Image{
 			Id:     id,
 			Alt:    alt,
@@ -252,24 +304,10 @@ func prepareImages(node *html.Node, storage *types.PageData) {
 
 	}
 
-	for _, image := range copyOfImages {
-		foundIndex := -1
-		for index, i := range storage.Images {
-			if i.Id == image.Id {
-				foundIndex = index
-				break
-			}
-		}
-
-		if foundIndex > -1 {
-			storage.Images[foundIndex] = image
-		}
-	}
-
 }
 
 func prepareTranslationJson(node *html.Node, storage *types.PageData) {
-	selector, err := cascadia.Parse("script[data-monster-role=translation]")
+	selector, err := cascadia.Parse("script[data-monster-role=translations]")
 	if err != nil {
 		return
 	}
@@ -279,14 +317,29 @@ func prepareTranslationJson(node *html.Node, storage *types.PageData) {
 		return
 	}
 
-	copyOfTranslations := make([]types.Translations, len(storage.Translations))
-	copy(copyOfTranslations, storage.Translations)
-	storage.Translations = []types.Translations{}
-
 	for _, n := range list {
 
 		id := util.GetAttribute(n.Attr, constants.DataBobReferenceAttributeKey)
-		typ := util.GetAttribute(n.Attr, "type")
+		if id == "" {
+			id = util.GetNextId()
+		}
+
+		log.Info("Found json translation: " + id)
+
+		found := false
+		for _, t := range storage.Translations {
+			if t.Id == id {
+				found = true
+				break
+			}
+		}
+
+		if found {
+			log.Info("The ID " + id + " for the json translation is already in use! If this is not intended, please change the ID in the HTML file. Will use ID: " + id)
+			continue
+		}
+
+		typ_ := util.GetAttribute(n.Attr, "type")
 
 		n.Attr = removeAttribute(n.Attr, attributeReplace)
 		n.Attr = append(n.Attr, html.Attribute{Key: attributeReplace, Val: "path:translations." + id + ".content"})
@@ -306,28 +359,15 @@ func prepareTranslationJson(node *html.Node, storage *types.PageData) {
 			fmt.Println(err)
 		}
 
+		log.Info("Added json translation: " + id + " (" + typ_ + ") to storage")
 		storage.Translations = append(storage.Translations, types.Translations{
 			Id:        id,
-			Type:      typ,
+			Type:      typ_,
 			KeyValues: t,
 		})
 
 	}
 
-	for _, translation := range copyOfTranslations {
-		foundIndex := -1
-		for index, t := range storage.Translations {
-			if t.Id == translation.Id {
-				foundIndex = index
-				break
-			}
-		}
-
-		if foundIndex > -1 {
-			storage.Translations[foundIndex] = translation
-		}
-	}
-
 }
 
 func prepareTextNodes(node *html.Node, storage *types.PageData) {
@@ -342,26 +382,8 @@ func prepareTextNodes(node *html.Node, storage *types.PageData) {
 		return
 	}
 
-	copyOfTextNodes := make([]types.Text, len(storage.Text))
-	copy(copyOfTextNodes, storage.Text)
-	storage.Text = []types.Text{}
-
 	runNodes(body, storage)
 
-	for _, text := range copyOfTextNodes {
-		foundIndex := -1
-		for index, t := range storage.Text {
-			if t.Id == text.Id {
-				foundIndex = index
-				break
-			}
-		}
-
-		if foundIndex > -1 {
-			storage.Text[foundIndex] = text
-		}
-	}
-
 }
 
 func runNodes(n *html.Node, storage *types.PageData) {
@@ -378,15 +400,67 @@ func runNodes(n *html.Node, storage *types.PageData) {
 
 }
 
+func renderNode(n *html.Node) string {
+	var buf bytes.Buffer
+	html.Render(&buf, n)
+	return buf.String()
+}
+
+func removeDuplicateWhitespaces(str string) string {
+	re := regexp.MustCompile(`\s+`)
+	return re.ReplaceAllString(str, " ")
+}
+
 func handleTextNode(n *html.Node, storage *types.PageData) {
 	content := strings.TrimSpace(n.Data)
 	if content == "" {
 		return
 	}
 
+	logContent := buildLogContent(n.Parent, content)
+	log.Info("Found text node: " + logContent)
+
+	if n.Parent != nil {
+		if n.Parent.Type == html.ElementNode {
+			if util.HasAttribute(n.Parent.Attr, "data-monster-head") {
+				return
+			}
+		}
+	}
+
 	id, err := util.BuildTextKey(content)
 	if err != nil || id == "" {
 		id = util.GetNextId()
+		log.Warn("Could not build text key for: '" + logContent + "', using ID: " + id + " instead.")
+	} else {
+		log.Info("Create text key: " + id)
+	}
+
+	// check if id is already in use
+	found := false
+	sameContent := false
+	for _, t := range storage.Text {
+		if t.Id == id {
+			// check if the content is the same
+			if t.Text == content {
+				sameContent = true
+				continue
+			}
+			found = true
+			break
+		}
+	}
+
+	if sameContent {
+		log.Info("The ID " + id + " for the text '" + logContent + "' is already in use and the content is the same! Will use this ID for the text.")
+	} else if found {
+		originalId := id
+		id = util.GetNextId()
+		log.Warn("We want to use the ID " + originalId + " for the text '" + logContent +
+			"', but this ID is already in use! " +
+			"This could be a problem if you want to use the text in a translation. " +
+			"Please change the text or the ID in the HTML file. " +
+			"Will use now use ID: " + id + " instead.")
 	}
 
 	parent := n.Parent
@@ -404,17 +478,42 @@ func handleTextNode(n *html.Node, storage *types.PageData) {
 			},
 		},
 	}
+
 	parent.InsertBefore(span, n)
 	parent.RemoveChild(n)
 
 	span.AppendChild(n)
 
+	if sameContent {
+		return
+	}
+
+	log.Info("Added text: " + id + " (" + logContent + ") to storage")
 	storage.Text = append(storage.Text, types.Text{
 		Id:   id,
 		Text: content,
 	})
 }
 
+func buildLogContent(n *html.Node, content string) string {
+	logContent := ""
+	if len(content) == 0 {
+		logContent = "Struct: " + renderNode(n)
+	} else if len(content) < 10 {
+		logContent = "Parent struct for (" + content + "): " + renderNode(n)
+
+	} else {
+		logContent = content
+	}
+
+	if len(logContent) > 100 {
+		logContent = logContent[:100] + "..."
+	}
+
+	logContent = removeDuplicateWhitespaces(logContent)
+	return logContent
+}
+
 func checkNodes(n *html.Node, storage *types.PageData) {
 	if n.Parent != nil {
 		if n.Parent.Type == html.ElementNode {
@@ -450,30 +549,57 @@ func checkMonsterDatatableHead(n *html.Node, storage *types.PageData) {
 	for _, div := range list {
 
 		head := util.GetAttribute(div.Attr, "data-monster-head")
+		logContent := buildLogContent(div, head)
 
-		id := util.GetAttribute(div.Attr, constants.DataBobReferenceAttributeKey)
+		id := util.GetOrCreateReference(div, head)
 		if id == "" {
+			id = util.GetNextId()
+			log.Warn("Could not build key for: '" + logContent + "', using ID: " + id + " instead.")
+		} else {
+			log.Info("Create key: " + id)
+		}
 
-			headID, _ := util.BuildTextKey(head)
-			if headID == "" {
-				id = util.GetNextId()
-			} else {
-				id = headID
+		// check if id is already in use
+		found := false
+		sameContent := false
+		for _, t := range storage.Text {
+			if t.Id == id {
+				// check if the content is the same
+				if t.Text == head {
+					sameContent = true
+					continue
+				}
+				found = true
+				break
 			}
-			div.Attr = append(div.Attr, html.Attribute{Key: constants.DataBobReferenceAttributeKey, Val: id})
+		}
+
+		if sameContent {
+			log.Info("The ID " + id + " for the head '" + logContent + "' is already in use and the content is the same! Will use this ID for the head.")
+		} else if found {
+			originalId := id
+			id = util.GetNextId()
+			log.Warn("We want to use the ID " + originalId + " for the head '" + logContent +
+				"', but this ID is already in use! " +
+				"This could be a problem if you want to use the head in a translation. " +
+				"Please change the head or add the attribute '" + constants.DataBobReferenceAttributeKey + "' to the tag. " +
+				"Will use now use ID: " + id + " instead.")
 
 		}
 
 		div.Attr = removeAttribute(div.Attr, "data-attributes")
 		div.Attr = append(div.Attr, html.Attribute{Key: "data-attributes", Val: "data-monster-head path:text." + id + ".text"})
 
+		if sameContent {
+			continue
+		}
+
+		log.Info("Added head: " + id + " (" + logContent + ") to storage")
 		storage.Text = append(storage.Text, types.Text{
 			Id:   id,
 			Text: head,
 		})
-
 	}
-
 }
 
 func PrepareHtmlFile(from, to string, storage types.PageDataStorage) (string, error) {
diff --git a/source/types/page-data.go b/source/types/page-data.go
index c5c7166012c28bb4e674a79e0aede058fb4eee7b..7260de7c510e4f7d23fe5348cd0ca6ac91f29e08 100644
--- a/source/types/page-data.go
+++ b/source/types/page-data.go
@@ -68,3 +68,20 @@ func NewPageDataStorage() PageDataStorage {
 	return storage
 
 }
+
+func (p *PageDataStorage) ResetContent() {
+	for _, pageData := range *p {
+		pageData.ResetContent()
+	}
+}
+
+func (p *PageData) ResetContent() {
+
+	p.Title = ""
+	p.Meta = make(map[string]string)
+	p.Images = make([]Image, 0)
+	p.Anchors = make([]Anchor, 0)
+	p.Text = make([]Text, 0)
+	p.Translations = make([]Translations, 0)
+
+}
diff --git a/source/util/html.go b/source/util/html.go
index d206b967618415d1f1384f24e327e70b7ab3fe7d..fabf0eb621778681d055d36a83d98580989f8835 100644
--- a/source/util/html.go
+++ b/source/util/html.go
@@ -20,6 +20,16 @@ func LoadHtml(p string) (*html.Node, error) {
 
 }
 
+func HasAttribute(attrs []html.Attribute, key string) bool {
+	for _, attr := range attrs {
+		if attr.Key == key {
+			return true
+		}
+	}
+
+	return false
+}
+
 func GetAttribute(attrs []html.Attribute, key string) string {
 	for _, attr := range attrs {
 		if attr.Key == key {
diff --git a/source/util/id.go b/source/util/id.go
index 74a36ab4102d89049bf0b64687e486d1585e5ae5..6c5a6d12ee9218404be26744c03a6cd4c6e4ea4c 100644
--- a/source/util/id.go
+++ b/source/util/id.go
@@ -67,6 +67,10 @@ func BuildTextKey(txt string) (string, error) {
 	}
 
 	txt = strings.TrimSpace(txt)
+
+	re := regexp.MustCompile(`([a-z])([A-Z])`)
+	txt = re.ReplaceAllString(txt, "${1}-${2}")
+
 	txt = strings.ToLower(txt)
 
 	txt = strings.ReplaceAll(txt, keyDelimiter, tempStringMask)
diff --git a/source/util/id_test.go b/source/util/id_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..229fb4d490e543b9f10c4d57bf606f4002f82399
--- /dev/null
+++ b/source/util/id_test.go
@@ -0,0 +1,93 @@
+package util
+
+import (
+	"regexp"
+	"strconv"
+	"testing"
+)
+
+func TestNewSecret(t *testing.T) {
+	secret, err := NewSecret()
+	if err != nil {
+		t.Errorf("Error creating new secret: %v", err)
+	}
+	if len(secret) == 0 {
+		t.Error("Generated secret is empty")
+	}
+}
+
+func TestNewSecretWithLength(t *testing.T) {
+	length := 10
+	secret, err := NewSecretWithLength(length)
+	if err != nil {
+		t.Errorf("Error creating new secret with length %d: %v", length, err)
+	}
+	if len(secret) != length {
+		t.Errorf("Generated secret length expected to be %d, got %d", length, len(secret))
+	}
+}
+
+func TestRandomString(t *testing.T) {
+	length := 10
+	str, err := RandomString(length)
+	if err != nil {
+		t.Errorf("Error generating random string: %v", err)
+	}
+	if len(str) != length {
+		t.Errorf("Random string length expected to be %d, got %d", length, len(str))
+	}
+}
+
+func TestCryptID(t *testing.T) {
+	input := "testString"
+	length := 8
+	encrypted, err := cryptID(input, length)
+	if err != nil {
+		t.Errorf("Error encrypting string: %v", err)
+	}
+	if len(encrypted) != length {
+		t.Errorf("Encrypted string length expected to be %d, got %d", length, len(encrypted))
+	}
+}
+
+func TestHashString(t *testing.T) {
+	input := "testString"
+	hashed := hashString(input)
+	if _, err := strconv.Atoi(hashed); err != nil {
+		t.Errorf("Hashed string is not a valid number: %s", hashed)
+	}
+}
+
+func TestBuildTextKey(t *testing.T) {
+	tests := []struct {
+		input         string
+		expectedRegex string
+	}{
+		{"Test Key", "^test-key$"},
+		{"", "^$"},
+		{"ThisIsALongTextKeyThatExceedsTheLimit", "^this-is-along-text-k-[0-9]+$"},
+		{"MixedCaseInput", "^mixed-case-input$"},
+		{"With Spaces", "^with-spaces$"},
+		{"123Numbers456", "^123numbers456$"},
+		{"Special#Characters!", "^special-characters$"},
+		{"Short", "^short$"},
+		{"LongerThanTwentyCharactersInputString", "^longer-than-twenty-c-[0-9]+$"},
+		{"-StartsInDash", "^starts-in-dash$"},
+		{"EndsInDash-", "^ends-in-dash$"},
+		{"With--Multiple--Dashes", "^with-multiple-dashes$"},
+		{"EndsInDash-", "^ends-in-dash$"},
+		{"-StartsInDash", "^starts-in-dash$"},
+		{"Emojis🚀AreCool", "^emojis-are-cool$"},
+	}
+
+	for _, test := range tests {
+		output, err := BuildTextKey(test.input)
+		if err != nil {
+			t.Errorf("Error building text key for input '%s': %v", test.input, err)
+		}
+		matched, _ := regexp.MatchString(test.expectedRegex, output)
+		if !matched {
+			t.Errorf("Output '%s' does not match expected regex '%s' for input '%s'", output, test.expectedRegex, test.input)
+		}
+	}
+}
diff --git a/treefmt.toml b/treefmt.toml
new file mode 100644
index 0000000000000000000000000000000000000000..968b9bf3830426c33c6a9a4012e1e7d073ecf421
--- /dev/null
+++ b/treefmt.toml
@@ -0,0 +1,34 @@
+# One CLI to format the code tree - https://github.com/numtide/treefmt
+
+[formatter.nix]
+command = "nixfmt"
+options = []
+includes = [ "*.nix" ]
+excludes = []
+[formatter.go]
+command = "gofmt"
+options = []
+includes = [ "*.go" ]
+excludes = []
+[formatter.shell]
+command = "shfmt"
+options = [ "-i", "2", "-ci" ]
+includes = [ "*.sh" ]
+excludes = []
+[formatter.yaml]
+command = "yq"
+options = [ "eval", "-P" ]
+includes = [ "*.yaml", "*.yml" ]
+excludes = []
+[formatter.json]
+command = "jq"
+options = [ "." ]
+includes = [ "*.json" ]
+excludes = []
+[formatter.toml]
+command = "tomlfmt"
+options = []
+includes = [ "*.toml" ]
+excludes = []
+
+