From bf3e635a1c58b0f742da5670aee2b498bd446cae Mon Sep 17 00:00:00 2001
From: MassiveBox <box@massivebox.net>
Date: Wed, 26 Mar 2025 23:13:08 +0100
Subject: [PATCH] Add webdav upload script and docs

---
 .gitignore                          |   6 +-
 README.md                           |  19 +-
 package-lock.json                   | 299 ++++++++++++++++++++++++++--
 package.json                        |   7 +-
 src/components/Matomo.astro         |  13 +-
 src/utils/{joplin.js => joplin.mjs} |  22 +-
 src/utils/upload.mjs                |  96 +++++++++
 7 files changed, 426 insertions(+), 36 deletions(-)
 rename src/utils/{joplin.js => joplin.mjs} (94%)
 create mode 100644 src/utils/upload.mjs

diff --git a/.gitignore b/.gitignore
index ebc9a5a..2b8d69b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,5 +35,7 @@ pnpm-debug.log*
 !.yarn/versions
 .pnp.*
 
-# blog
-src/content/blog
+# blog, meta
+src/content/blog/*
+Matomo.astro
+src/pages/pages/*
diff --git a/README.md b/README.md
index 9f35789..f650606 100644
--- a/README.md
+++ b/README.md
@@ -20,13 +20,14 @@ The website will be built to the `./dist` folder.
 
 ## Blogging with Joplin
 
-You can use Joplin to write and manageyour blog posts, instead of just placing the Markdown files in the
+You can use Joplin to write and manage your blog posts, instead of just placing the Markdown files in the
 `src/content/blog` folder.
 
 1. Install [Joplin](https://joplinapp.org/)
 2. Start the Web Clipper Service: Tools > Options > Web Clipper > Start Web Clipper Service
 3. Run `npm run joplin`
 4. Allow the request for an API token on the Joplin app
+   - You can also place the Joplin token in an `.env` file in the format `JOPLIN_KEY=your_token`.
 
 The script will look for a notebook named `Blog`. It will then download all notes from that notebook, alongside their
 attachments, and place them in the `src/content/blog` folder.
@@ -46,6 +47,22 @@ In order, to have an ogImage (also known as article cover):
    ```
    Note the `.` (dot) instead of `:` (colon)!
 
+## Uploading to WebDav
+
+I have found that a quick and easy way to deploy the website is via WebDav and compatible software on the server side, like
+[this](https://github.com/mholt/caddy-webdav) plugin for Caddy.
+
+To simplify uploading to WebDav, I have created a script, which can be run with `npm run upload`.
+
+You have to set the following environment variables:
+- `WEBDAV_ENDPOINT`: The URL of the WebDav server, e.g. `https://example.com/webdav/`
+- `WEBDAV_USERNAME`: The username for the WebDav server
+- `WEBDAV_PASSWORD`: The password for the WebDav server
+- `WEBDAV_PATH`: The path to the folder on the server, e.g. `/public_html/` (default: `/`)
+
+The script will upload the `./dist` folder, so make sure you run `npm run build` before the upload script.  
+Note that running the script will clear the remote folder before uploading the files.
+
 ## License
 
 The original [Astro Paper](https://github.com/satnaing/astro-paper) is licensed under the MIT License, Copyright © 2025 
diff --git a/package-lock.json b/package-lock.json
index 37dd0cd..542f094 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,7 +25,8 @@
         "sass": "^1.77.2",
         "satori": "^0.10.11",
         "tailwindcss": "^3.4.1",
-        "typescript": "^5.4.5"
+        "typescript": "^5.4.5",
+        "webdav": "^5.8.0"
       },
       "devDependencies": {
         "@astrojs/react": "^3.0.9",
@@ -40,6 +41,7 @@
         "astro-eslint-parser": "^0.16.2",
         "commitizen": "^4.3.0",
         "cz-conventional-changelog": "^3.3.0",
+        "dotenv": "^16.4.7",
         "eslint": "^8.56.0",
         "eslint-plugin-astro": "^0.31.3",
         "husky": "^8.0.3",
@@ -739,6 +741,15 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@buttercup/fetch": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.2.1.tgz",
+      "integrity": "sha512-sCgECOx8wiqY8NN1xN22BqqKzXYIG2AicNLlakOAI4f0WgyLVUbAigMf8CZhBtJxdudTcB1gD5lciqi44jwJvg==",
+      "license": "MIT",
+      "optionalDependencies": {
+        "node-fetch": "^3.3.0"
+      }
+    },
     "node_modules/@commitlint/config-validator": {
       "version": "17.6.7",
       "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-17.6.7.tgz",
@@ -4236,6 +4247,12 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/byte-length": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/byte-length/-/byte-length-1.0.2.tgz",
+      "integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==",
+      "license": "MIT"
+    },
     "node_modules/cachedir": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz",
@@ -4365,6 +4382,15 @@
       "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
       "dev": true
     },
+    "node_modules/charenc": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
+      "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/cheerio": {
       "version": "1.0.0-rc.12",
       "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz",
@@ -4987,6 +5013,15 @@
         "node": ">= 8"
       }
     },
+    "node_modules/crypt": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
+      "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/css-background-parser": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz",
@@ -5120,6 +5155,15 @@
         "@commitlint/load": ">6.1.1"
       }
     },
+    "node_modules/data-uri-to-buffer": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+      "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
     "node_modules/debug": {
       "version": "4.3.4",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -5470,6 +5514,19 @@
         "tslib": "^2.0.3"
       }
     },
+    "node_modules/dotenv": {
+      "version": "16.4.7",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
+      "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
     "node_modules/dset": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.3.tgz",
@@ -6154,21 +6211,18 @@
       "dev": true
     },
     "node_modules/fast-xml-parser": {
-      "version": "4.2.7",
-      "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.7.tgz",
-      "integrity": "sha512-J8r6BriSLO1uj2miOk1NW0YVm8AGOOu3Si2HQp/cSmo6EA4m3fcwu2WKjJ4RK9wMLBtg69y1kS8baDiQBR41Ig==",
+      "version": "4.5.3",
+      "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
+      "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==",
       "funding": [
-        {
-          "type": "paypal",
-          "url": "https://paypal.me/naturalintelligence"
-        },
         {
           "type": "github",
           "url": "https://github.com/sponsors/NaturalIntelligence"
         }
       ],
+      "license": "MIT",
       "dependencies": {
-        "strnum": "^1.0.5"
+        "strnum": "^1.1.1"
       },
       "bin": {
         "fxparser": "src/cli/cli.js"
@@ -6190,6 +6244,29 @@
         "pend": "~1.2.0"
       }
     },
+    "node_modules/fetch-blob": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+      "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/jimmywarting"
+        },
+        {
+          "type": "paypal",
+          "url": "https://paypal.me/jimmywarting"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "node-domexception": "^1.0.0",
+        "web-streams-polyfill": "^3.0.3"
+      },
+      "engines": {
+        "node": "^12.20 || >= 14.13"
+      }
+    },
     "node_modules/fflate": {
       "version": "0.7.4",
       "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz",
@@ -6333,6 +6410,18 @@
         "node": ">=8"
       }
     },
+    "node_modules/formdata-polyfill": {
+      "version": "4.0.10",
+      "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+      "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+      "license": "MIT",
+      "dependencies": {
+        "fetch-blob": "^3.1.2"
+      },
+      "engines": {
+        "node": ">=12.20.0"
+      }
+    },
     "node_modules/fraction.js": {
       "version": "4.3.6",
       "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz",
@@ -6852,6 +6941,12 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/hot-patcher": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-2.0.1.tgz",
+      "integrity": "sha512-ECg1JFG0YzehicQaogenlcs2qg6WsXQsxtnbr1i696u5tLUjtJdQAh0u2g0Q5YV45f263Ta1GnUJsc8WIfJf4Q==",
+      "license": "MIT"
+    },
     "node_modules/html-escaper": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
@@ -7654,6 +7749,12 @@
       "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
       "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="
     },
+    "node_modules/layerr": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/layerr/-/layerr-3.0.0.tgz",
+      "integrity": "sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA==",
+      "license": "MIT"
+    },
     "node_modules/levn": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -8433,6 +8534,23 @@
         "node": ">= 18"
       }
     },
+    "node_modules/md5": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
+      "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "charenc": "0.0.2",
+        "crypt": "0.0.2",
+        "is-buffer": "~1.1.6"
+      }
+    },
+    "node_modules/md5/node_modules/is-buffer": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+      "license": "MIT"
+    },
     "node_modules/mdast-util-definitions": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz",
@@ -9505,6 +9623,12 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/nested-property": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/nested-property/-/nested-property-4.0.0.tgz",
+      "integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==",
+      "license": "MIT"
+    },
     "node_modules/nlcst-to-string": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-3.1.1.tgz",
@@ -9545,6 +9669,43 @@
       "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
       "optional": true
     },
+    "node_modules/node-domexception": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+      "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/jimmywarting"
+        },
+        {
+          "type": "github",
+          "url": "https://paypal.me/jimmywarting"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.5.0"
+      }
+    },
+    "node_modules/node-fetch": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+      "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+      "license": "MIT",
+      "dependencies": {
+        "data-uri-to-buffer": "^4.0.0",
+        "fetch-blob": "^3.1.4",
+        "formdata-polyfill": "^4.0.10"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/node-fetch"
+      }
+    },
     "node_modules/node-releases": {
       "version": "2.0.14",
       "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
@@ -10050,6 +10211,12 @@
       "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
       "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
     },
+    "node_modules/path-posix": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz",
+      "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==",
+      "license": "ISC"
+    },
     "node_modules/path-to-regexp": {
       "version": "6.2.1",
       "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
@@ -10690,6 +10857,12 @@
         "node": ">=6"
       }
     },
+    "node_modules/querystringify": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+      "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
+      "license": "MIT"
+    },
     "node_modules/queue-microtask": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -11033,6 +11206,12 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/requires-port": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+      "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+      "license": "MIT"
+    },
     "node_modules/resolve": {
       "version": "1.22.4",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz",
@@ -12265,9 +12444,16 @@
       }
     },
     "node_modules/strnum": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
-      "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
+      "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/NaturalIntelligence"
+        }
+      ],
+      "license": "MIT"
     },
     "node_modules/strtok3": {
       "version": "7.0.0",
@@ -13175,6 +13361,25 @@
         "punycode": "^2.1.0"
       }
     },
+    "node_modules/url-join": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz",
+      "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==",
+      "license": "MIT",
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      }
+    },
+    "node_modules/url-parse": {
+      "version": "1.5.10",
+      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
+      "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+      "license": "MIT",
+      "dependencies": {
+        "querystringify": "^2.1.1",
+        "requires-port": "^1.0.0"
+      }
+    },
     "node_modules/util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -13897,6 +14102,76 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/web-streams-polyfill": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+      "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/webdav": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/webdav/-/webdav-5.8.0.tgz",
+      "integrity": "sha512-iuFG7NamJ41Oshg4930iQgfIpRrUiatPWIekeznYgEf2EOraTRcDPTjy7gIOMtkdpKTaqPk1E68NO5PAGtJahA==",
+      "license": "MIT",
+      "dependencies": {
+        "@buttercup/fetch": "^0.2.1",
+        "base-64": "^1.0.0",
+        "byte-length": "^1.0.2",
+        "entities": "^6.0.0",
+        "fast-xml-parser": "^4.5.1",
+        "hot-patcher": "^2.0.1",
+        "layerr": "^3.0.0",
+        "md5": "^2.3.0",
+        "minimatch": "^9.0.5",
+        "nested-property": "^4.0.0",
+        "node-fetch": "^3.3.2",
+        "path-posix": "^1.0.0",
+        "url-join": "^5.0.0",
+        "url-parse": "^1.5.10"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/webdav/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/webdav/node_modules/entities": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
+      "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/webdav/node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/whatwg-encoding": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
diff --git a/package.json b/package.json
index 67286b0..101f668 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,8 @@
     "cz": "cz",
     "prepare": "husky install",
     "lint": "eslint .",
-    "joplin": "node src/utils/joplin.js"
+    "joplin": "node src/utils/joplin.mjs",
+    "upload": "node src/utils/upload.mjs"
   },
   "dependencies": {
     "@astrojs/check": "^0.7.0",
@@ -33,7 +34,8 @@
     "sass": "^1.77.2",
     "satori": "^0.10.11",
     "tailwindcss": "^3.4.1",
-    "typescript": "^5.4.5"
+    "typescript": "^5.4.5",
+    "webdav": "^5.8.0"
   },
   "devDependencies": {
     "@astrojs/react": "^3.0.9",
@@ -48,6 +50,7 @@
     "astro-eslint-parser": "^0.16.2",
     "commitizen": "^4.3.0",
     "cz-conventional-changelog": "^3.3.0",
+    "dotenv": "^16.4.7",
     "eslint": "^8.56.0",
     "eslint-plugin-astro": "^0.31.3",
     "husky": "^8.0.3",
diff --git a/src/components/Matomo.astro b/src/components/Matomo.astro
index a8f7e7c..8324dee 100644
--- a/src/components/Matomo.astro
+++ b/src/components/Matomo.astro
@@ -1,12 +1 @@
-<script is:inline>
-  var _paq = _paq || [];
-  _paq.push(["trackPageView"]), _paq.push(["enableLinkTracking"]),
-    function() {
-      _paq.push(["setTrackerUrl", "//stats.massive.box/sjprfvo.php"]);
-      _paq.push(["setSiteId", "1"]);
-      _paq.push(['disableAlwaysUseSendBeacon', 'true']);
-      var a = document, r = a.createElement("script"), s = a.getElementsByTagName("script")[0];
-      r.async = !0, r.defer = !0, r.src = "//stats.massive.box/fauqtkg.php", s.parentNode.insertBefore(r, s)
-    }();
-</script>
-<noscript><img src="//stats.massive.box/sjprfvo.php?dzp=1&opl=1" /></noscript>
\ No newline at end of file
+<script defer src="https://stats.massive.box/p.js" data-website-id="30344937-ffdf-4a6a-ba52-dc60a23aa4f2"></script>
\ No newline at end of file
diff --git a/src/utils/joplin.js b/src/utils/joplin.mjs
similarity index 94%
rename from src/utils/joplin.js
rename to src/utils/joplin.mjs
index 98d67a3..d157435 100644
--- a/src/utils/joplin.js
+++ b/src/utils/joplin.mjs
@@ -1,6 +1,12 @@
-const http = require('node:http');
-const fs = require('fs');
-const path = require('path');
+import dotenv from 'dotenv';
+dotenv.config();
+
+import http from 'node:http';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
 async function fetch(url, method = "GET") {
   return new Promise((resolve) => {
@@ -172,16 +178,18 @@ async function clearBlogFolder() {
 }
 
 async function main() {
-
   console.log("Finding JoplinClipperServer port...");
   let host = "http://localhost:" + await findPort();
-  let apiToken = await authenticate(host);
-  //let apiToken = "";
+
+  let apiToken = process.env.JOPLIN_KEY;
+  if (!apiToken) {
+    apiToken = await authenticate(host);
+  }
+  
   let notes = await getNotebookNotes(apiToken, host);
   await clearBlogFolder();
 
   for(let noteId of notes) {
-
     let noteBody = await getNoteBody(apiToken, host, noteId);
     let slug = await getNoteSlug(noteBody);
 
diff --git a/src/utils/upload.mjs b/src/utils/upload.mjs
new file mode 100644
index 0000000..76bec24
--- /dev/null
+++ b/src/utils/upload.mjs
@@ -0,0 +1,96 @@
+import dotenv from 'dotenv';
+dotenv.config();
+
+import { createClient } from 'webdav';
+import fs from 'fs';
+import path from 'path';
+
+const client = createClient(
+  process.env.WEBDAV_ENDPOINT,
+  {
+      username: process.env.WEBDAV_USERNAME,
+      password: process.env.WEBDAV_PASSWORD,
+      maxBodyLength: Infinity,
+      maxContentLength: Infinity,
+      headers: {
+          Accept: "*/*",
+          Connection: "keep-alive"
+      }
+  }
+);
+
+async function createDirectoryIfNotExist(remotePath) {
+    try {
+        const exists = await client.exists(remotePath);
+        if (!exists) {
+            await client.createDirectory(remotePath);
+        }
+    } catch (error) {
+        if (error.status !== 409 && error.status !== 405) {
+            throw error;
+        }
+    }
+}
+
+async function uploadDirectory(localPath, remoteBasePath) {
+    try {
+        const items = fs.readdirSync(localPath, { withFileTypes: true });
+
+        for (const item of items) {
+            const localItemPath = path.join(localPath, item.name);
+            const remoteItemPath = path.posix.join(remoteBasePath, item.name);
+
+            if (item.isDirectory()) {
+                // Create directory on WebDAV server
+                await createDirectoryIfNotExist(remoteItemPath);
+                console.log(`Verified directory: ${remoteItemPath}`);
+                await uploadDirectory(localItemPath, remoteItemPath);
+            } else {
+                // Upload file
+                console.log(`Uploading file: ${remoteItemPath}`);
+                const content = fs.readFileSync(localItemPath);
+                await client.putFileContents(remoteItemPath, content);
+            }
+        }
+    } catch (error) {
+        console.error(`Error processing ${localPath}:`, error.message);
+        throw error;
+    }
+}
+
+async function clearFolder(remotePath) {
+    try {
+        const items = await client.getDirectoryContents(remotePath);
+        for (const item of items) {
+            if (item.basename !== '..') {
+                const itemPath = path.posix.join(remotePath, item.basename);
+                await client.deleteFile(itemPath);
+            }
+        }
+    } catch (error) {
+        console.error('Error clearing root folder:', error.message);
+        throw error;
+    }
+}
+
+// Usage example
+async function main() {
+    const localDir = './dist';
+    let remoteDir = process.env.WEBDAV_REMOTE_DIR;
+    if(remoteDir === undefined) {
+        remoteDir = '/';
+    }
+
+    try {
+        // Clear root folder
+        await clearFolder(remoteDir);
+
+        // Upload new content
+        await uploadDirectory(localDir, remoteDir);
+        console.log('Directory upload complete');
+    } catch (error) {
+        console.error('Upload failed:', error.message);
+    }
+}
+
+main();