فهرست منبع

Installed SSR.

Iñigo Valentin 10 ماه پیش
والد
کامیت
63373df348

+ 6 - 1
frontend/angular.json

@@ -32,7 +32,12 @@
             ],
             "styles": [
               "src/styles.scss"
-            ]
+            ],
+            "server": "src/main.server.ts",
+            "outputMode": "server",
+            "ssr": {
+              "entry": "src/server.ts"
+            }
           },
           "configurations": {
             "production": {

+ 162 - 83
frontend/package-lock.json

@@ -14,9 +14,12 @@
         "@angular/core": "^20.3.7",
         "@angular/forms": "^20.3.7",
         "@angular/platform-browser": "^20.3.7",
+        "@angular/platform-server": "^20.3.7",
         "@angular/router": "^20.3.7",
+        "@angular/ssr": "^20.3.7",
         "@ngx-translate/core": "^17.0.0",
         "@ngx-translate/http-loader": "^17.0.0",
+        "express": "^5.1.0",
         "rxjs": "~7.8.0",
         "tslib": "^2.3.0",
         "zone.js": "~0.15.0"
@@ -26,7 +29,9 @@
         "@angular/cli": "^20.3.7",
         "@angular/compiler-cli": "^20.3.7",
         "@angular/localize": "^20.3.7",
+        "@types/express": "^5.0.1",
         "@types/jasmine": "~5.1.0",
+        "@types/node": "^20.17.19",
         "jasmine-core": "~5.9.0",
         "karma": "~6.4.0",
         "karma-chrome-launcher": "~3.2.0",
@@ -580,6 +585,25 @@
         }
       }
     },
+    "node_modules/@angular/platform-server": {
+      "version": "20.3.7",
+      "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-20.3.7.tgz",
+      "integrity": "sha512-ADqOwqeUpTkp97SUpNO4jZ0o9Du7oBpi0mqzLx/c1dQYgL5hKAZYpa7bpG/edn2nSMHXwQAaGw7t+MTmU7elxQ==",
+      "dependencies": {
+        "tslib": "^2.3.0",
+        "xhr2": "^0.2.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "@angular/common": "20.3.7",
+        "@angular/compiler": "20.3.7",
+        "@angular/core": "20.3.7",
+        "@angular/platform-browser": "20.3.7",
+        "rxjs": "^6.5.3 || ^7.4.0"
+      }
+    },
     "node_modules/@angular/router": {
       "version": "20.3.7",
       "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.7.tgz",
@@ -597,6 +621,25 @@
         "rxjs": "^6.5.3 || ^7.4.0"
       }
     },
+    "node_modules/@angular/ssr": {
+      "version": "20.3.7",
+      "resolved": "https://registry.npmjs.org/@angular/ssr/-/ssr-20.3.7.tgz",
+      "integrity": "sha512-munpdYSgU2jDdRb5xujmaAXEpnOhFVLNO0ghqPugeDOH8ejeum3oBsHkdWK1w6rhj1PmX32/Q0DlAiuY4VlXCQ==",
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "peerDependencies": {
+        "@angular/common": "^20.0.0",
+        "@angular/core": "^20.0.0",
+        "@angular/platform-server": "^20.0.0",
+        "@angular/router": "^20.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@angular/platform-server": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@babel/code-frame": {
       "version": "7.27.1",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -3390,6 +3433,25 @@
         "@babel/types": "^7.28.2"
       }
     },
+    "node_modules/@types/body-parser": {
+      "version": "1.19.6",
+      "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+      "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+      "dev": true,
+      "dependencies": {
+        "@types/connect": "*",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/connect": {
+      "version": "3.4.38",
+      "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+      "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/cors": {
       "version": "2.8.19",
       "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
@@ -3405,19 +3467,96 @@
       "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
       "dev": true
     },
+    "node_modules/@types/express": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz",
+      "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
+      "dev": true,
+      "dependencies": {
+        "@types/body-parser": "*",
+        "@types/express-serve-static-core": "^5.0.0",
+        "@types/serve-static": "*"
+      }
+    },
+    "node_modules/@types/express-serve-static-core": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz",
+      "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*",
+        "@types/qs": "*",
+        "@types/range-parser": "*",
+        "@types/send": "*"
+      }
+    },
+    "node_modules/@types/http-errors": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+      "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+      "dev": true
+    },
     "node_modules/@types/jasmine": {
       "version": "5.1.10",
       "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.10.tgz",
       "integrity": "sha512-ctILpOSFD58PTyGwr400GadefIHPqdWQ8VlZI9D65USOWR/aIw8dkDAw17KPCu/Wn+e+r9BWpPtmuB5T/pP8ng==",
       "dev": true
     },
+    "node_modules/@types/mime": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+      "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+      "dev": true
+    },
     "node_modules/@types/node": {
-      "version": "24.7.2",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz",
-      "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==",
+      "version": "20.19.23",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz",
+      "integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==",
       "dev": true,
       "dependencies": {
-        "undici-types": "~7.14.0"
+        "undici-types": "~6.21.0"
+      }
+    },
+    "node_modules/@types/qs": {
+      "version": "6.14.0",
+      "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
+      "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
+      "dev": true
+    },
+    "node_modules/@types/range-parser": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+      "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+      "dev": true
+    },
+    "node_modules/@types/send": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz",
+      "integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/serve-static": {
+      "version": "1.15.9",
+      "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz",
+      "integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==",
+      "dev": true,
+      "dependencies": {
+        "@types/http-errors": "*",
+        "@types/node": "*",
+        "@types/send": "<1"
+      }
+    },
+    "node_modules/@types/serve-static/node_modules/@types/send": {
+      "version": "0.17.5",
+      "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
+      "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
+      "dev": true,
+      "dependencies": {
+        "@types/mime": "^1",
+        "@types/node": "*"
       }
     },
     "node_modules/@vitejs/plugin-basic-ssl": {
@@ -3451,7 +3590,6 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
       "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
-      "dev": true,
       "dependencies": {
         "mime-types": "^3.0.0",
         "negotiator": "^1.0.0"
@@ -3650,7 +3788,6 @@
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
       "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
-      "dev": true,
       "dependencies": {
         "bytes": "^3.1.2",
         "content-type": "^1.0.5",
@@ -3670,7 +3807,6 @@
       "version": "0.6.3",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
       "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
-      "dev": true,
       "dependencies": {
         "safer-buffer": ">= 2.1.2 < 3.0.0"
       },
@@ -3749,7 +3885,6 @@
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
       "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
-      "dev": true,
       "engines": {
         "node": ">= 0.8"
       }
@@ -3865,7 +4000,6 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
       "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
-      "dev": true,
       "dependencies": {
         "es-errors": "^1.3.0",
         "function-bind": "^1.1.2"
@@ -3878,7 +4012,6 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
       "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
-      "dev": true,
       "dependencies": {
         "call-bind-apply-helpers": "^1.0.2",
         "get-intrinsic": "^1.3.0"
@@ -4147,7 +4280,6 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
       "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
-      "dev": true,
       "dependencies": {
         "safe-buffer": "5.2.1"
       },
@@ -4159,7 +4291,6 @@
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
       "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
-      "dev": true,
       "engines": {
         "node": ">= 0.6"
       }
@@ -4174,7 +4305,6 @@
       "version": "0.7.2",
       "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
       "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
-      "dev": true,
       "engines": {
         "node": ">= 0.6"
       }
@@ -4183,7 +4313,6 @@
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
       "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
-      "dev": true,
       "engines": {
         "node": ">=6.6.0"
       }
@@ -4262,7 +4391,6 @@
       "version": "4.4.3",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
       "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
-      "dev": true,
       "dependencies": {
         "ms": "^2.1.3"
       },
@@ -4279,7 +4407,6 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
       "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
-      "dev": true,
       "engines": {
         "node": ">= 0.8"
       }
@@ -4381,7 +4508,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
       "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
-      "dev": true,
       "dependencies": {
         "call-bind-apply-helpers": "^1.0.1",
         "es-errors": "^1.3.0",
@@ -4400,8 +4526,7 @@
     "node_modules/ee-first": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
-      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
-      "dev": true
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
     },
     "node_modules/electron-to-chromium": {
       "version": "1.5.237",
@@ -4419,7 +4544,6 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
       "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
-      "dev": true,
       "engines": {
         "node": ">= 0.8"
       }
@@ -4594,7 +4718,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
       "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
-      "dev": true,
       "engines": {
         "node": ">= 0.4"
       }
@@ -4603,7 +4726,6 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
       "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
-      "dev": true,
       "engines": {
         "node": ">= 0.4"
       }
@@ -4612,7 +4734,6 @@
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
       "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
-      "dev": true,
       "dependencies": {
         "es-errors": "^1.3.0"
       },
@@ -4673,14 +4794,12 @@
     "node_modules/escape-html": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
-      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
-      "dev": true
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
     },
     "node_modules/etag": {
       "version": "1.8.1",
       "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
       "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
-      "dev": true,
       "engines": {
         "node": ">= 0.6"
       }
@@ -4722,7 +4841,6 @@
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
       "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
-      "dev": true,
       "dependencies": {
         "accepts": "^2.0.0",
         "body-parser": "^2.2.0",
@@ -4842,7 +4960,6 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
       "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
-      "dev": true,
       "dependencies": {
         "debug": "^4.4.0",
         "encodeurl": "^2.0.0",
@@ -4901,7 +5018,6 @@
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
       "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
-      "dev": true,
       "engines": {
         "node": ">= 0.6"
       }
@@ -4910,7 +5026,6 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
       "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
-      "dev": true,
       "engines": {
         "node": ">= 0.8"
       }
@@ -4965,7 +5080,6 @@
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
       "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
-      "dev": true,
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
@@ -5004,7 +5118,6 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
       "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
-      "dev": true,
       "dependencies": {
         "call-bind-apply-helpers": "^1.0.2",
         "es-define-property": "^1.0.1",
@@ -5028,7 +5141,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
       "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
-      "dev": true,
       "dependencies": {
         "dunder-proto": "^1.0.1",
         "es-object-atoms": "^1.0.0"
@@ -5080,7 +5192,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
       "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
-      "dev": true,
       "engines": {
         "node": ">= 0.4"
       },
@@ -5107,7 +5218,6 @@
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
       "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
-      "dev": true,
       "engines": {
         "node": ">= 0.4"
       },
@@ -5134,7 +5244,6 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
       "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
-      "dev": true,
       "dependencies": {
         "function-bind": "^1.1.2"
       },
@@ -5210,7 +5319,6 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
       "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
-      "dev": true,
       "dependencies": {
         "depd": "2.0.0",
         "inherits": "2.0.4",
@@ -5226,7 +5334,6 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
       "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
-      "dev": true,
       "engines": {
         "node": ">= 0.8"
       }
@@ -5275,7 +5382,6 @@
       "version": "0.7.0",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
       "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
-      "dev": true,
       "dependencies": {
         "safer-buffer": ">= 2.1.2 < 3.0.0"
       },
@@ -5343,8 +5449,7 @@
     "node_modules/inherits": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
-      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
-      "dev": true
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
     },
     "node_modules/ini": {
       "version": "5.0.0",
@@ -5368,7 +5473,6 @@
       "version": "1.9.1",
       "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
       "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
-      "dev": true,
       "engines": {
         "node": ">= 0.10"
       }
@@ -5457,8 +5561,7 @@
     "node_modules/is-promise": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
-      "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
-      "dev": true
+      "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="
     },
     "node_modules/is-regex": {
       "version": "1.2.1",
@@ -6350,7 +6453,6 @@
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
       "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
-      "dev": true,
       "engines": {
         "node": ">= 0.4"
       }
@@ -6359,7 +6461,6 @@
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
       "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
-      "dev": true,
       "engines": {
         "node": ">= 0.8"
       }
@@ -6368,7 +6469,6 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
       "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
-      "dev": true,
       "engines": {
         "node": ">=18"
       },
@@ -6419,7 +6519,6 @@
       "version": "1.54.0",
       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
       "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
-      "dev": true,
       "engines": {
         "node": ">= 0.6"
       }
@@ -6428,7 +6527,6 @@
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
       "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
-      "dev": true,
       "dependencies": {
         "mime-db": "^1.54.0"
       },
@@ -6633,8 +6731,7 @@
     "node_modules/ms": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
-      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
-      "dev": true
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
     },
     "node_modules/msgpackr": {
       "version": "1.11.5",
@@ -6699,7 +6796,6 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
       "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
-      "dev": true,
       "engines": {
         "node": ">= 0.6"
       }
@@ -7024,7 +7120,6 @@
       "version": "1.13.4",
       "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
       "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
-      "dev": true,
       "engines": {
         "node": ">= 0.4"
       },
@@ -7036,7 +7131,6 @@
       "version": "2.4.1",
       "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
       "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
-      "dev": true,
       "dependencies": {
         "ee-first": "1.1.1"
       },
@@ -7048,7 +7142,6 @@
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
       "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
-      "dev": true,
       "dependencies": {
         "wrappy": "1"
       }
@@ -7246,7 +7339,6 @@
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
       "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
-      "dev": true,
       "engines": {
         "node": ">= 0.8"
       }
@@ -7301,7 +7393,6 @@
       "version": "8.3.0",
       "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
       "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
-      "dev": true,
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/express"
@@ -7406,7 +7497,6 @@
       "version": "2.0.7",
       "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
       "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
-      "dev": true,
       "dependencies": {
         "forwarded": "0.2.0",
         "ipaddr.js": "1.9.1"
@@ -7434,7 +7524,6 @@
       "version": "6.14.0",
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
       "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
-      "dev": true,
       "dependencies": {
         "side-channel": "^1.1.0"
       },
@@ -7449,7 +7538,6 @@
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
       "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
-      "dev": true,
       "engines": {
         "node": ">= 0.6"
       }
@@ -7458,7 +7546,6 @@
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
       "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
-      "dev": true,
       "dependencies": {
         "bytes": "3.1.2",
         "http-errors": "2.0.0",
@@ -7624,7 +7711,6 @@
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
       "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
-      "dev": true,
       "dependencies": {
         "debug": "^4.4.0",
         "depd": "^2.0.0",
@@ -7648,7 +7734,6 @@
       "version": "5.2.1",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
       "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
-      "dev": true,
       "funding": [
         {
           "type": "github",
@@ -7684,8 +7769,7 @@
     "node_modules/safer-buffer": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
-      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
-      "dev": true
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
     },
     "node_modules/sass": {
       "version": "1.90.0",
@@ -7723,7 +7807,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
       "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
-      "dev": true,
       "dependencies": {
         "debug": "^4.3.5",
         "encodeurl": "^2.0.0",
@@ -7745,7 +7828,6 @@
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
       "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
-      "dev": true,
       "dependencies": {
         "encodeurl": "^2.0.0",
         "escape-html": "^1.0.3",
@@ -7759,8 +7841,7 @@
     "node_modules/setprototypeof": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
-      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
-      "dev": true
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
     },
     "node_modules/shebang-command": {
       "version": "2.0.0",
@@ -7787,7 +7868,6 @@
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
       "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
-      "dev": true,
       "dependencies": {
         "es-errors": "^1.3.0",
         "object-inspect": "^1.13.3",
@@ -7806,7 +7886,6 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
       "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
-      "dev": true,
       "dependencies": {
         "es-errors": "^1.3.0",
         "object-inspect": "^1.13.3"
@@ -7822,7 +7901,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
       "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
-      "dev": true,
       "dependencies": {
         "call-bound": "^1.0.2",
         "es-errors": "^1.3.0",
@@ -7840,7 +7918,6 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
       "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
-      "dev": true,
       "dependencies": {
         "call-bound": "^1.0.2",
         "es-errors": "^1.3.0",
@@ -8158,7 +8235,6 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
       "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
-      "dev": true,
       "engines": {
         "node": ">= 0.8"
       }
@@ -8452,7 +8528,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
       "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
-      "dev": true,
       "engines": {
         "node": ">=0.6"
       }
@@ -8480,7 +8555,6 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
       "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
-      "dev": true,
       "dependencies": {
         "content-type": "^1.0.5",
         "media-typer": "^1.1.0",
@@ -8530,9 +8604,9 @@
       }
     },
     "node_modules/undici-types": {
-      "version": "7.14.0",
-      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
-      "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+      "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
       "dev": true
     },
     "node_modules/unique-filename": {
@@ -8572,7 +8646,6 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
       "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
-      "dev": true,
       "engines": {
         "node": ">= 0.8"
       }
@@ -8657,7 +8730,6 @@
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
       "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
-      "dev": true,
       "engines": {
         "node": ">= 0.8"
       }
@@ -8961,8 +9033,7 @@
     "node_modules/wrappy": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
-      "dev": true
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
     },
     "node_modules/ws": {
       "version": "8.17.1",
@@ -8985,6 +9056,14 @@
         }
       }
     },
+    "node_modules/xhr2": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz",
+      "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/y18n": {
       "version": "5.0.8",
       "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

+ 9 - 3
frontend/package.json

@@ -6,7 +6,7 @@
   "sourceSite": "Github",
   "sourceURL": "https://github.com/InigoValentin/leather.inigovalentin.com",
   "license": "GPLv3",
-  "languages":{
+  "languages": {
     "available": "es|en|eu",
     "default": "es"
   },
@@ -15,7 +15,8 @@
     "start": "ng serve",
     "build": "ng build",
     "watch": "ng build --watch --configuration development",
-    "test": "ng test"
+    "test": "ng test",
+    "serve:ssr:leather-frontend": "node dist/leather-frontend/server/server.mjs"
   },
   "prettier": {
     "printWidth": 100,
@@ -36,9 +37,12 @@
     "@angular/core": "^20.3.7",
     "@angular/forms": "^20.3.7",
     "@angular/platform-browser": "^20.3.7",
+    "@angular/platform-server": "^20.3.7",
     "@angular/router": "^20.3.7",
+    "@angular/ssr": "^20.3.7",
     "@ngx-translate/core": "^17.0.0",
     "@ngx-translate/http-loader": "^17.0.0",
+    "express": "^5.1.0",
     "rxjs": "~7.8.0",
     "tslib": "^2.3.0",
     "zone.js": "~0.15.0"
@@ -48,7 +52,9 @@
     "@angular/cli": "^20.3.7",
     "@angular/compiler-cli": "^20.3.7",
     "@angular/localize": "^20.3.7",
+    "@types/express": "^5.0.1",
     "@types/jasmine": "~5.1.0",
+    "@types/node": "^20.17.19",
     "jasmine-core": "~5.9.0",
     "karma": "~6.4.0",
     "karma-chrome-launcher": "~3.2.0",
@@ -57,4 +63,4 @@
     "karma-jasmine-html-reporter": "~2.1.0",
     "typescript": "~5.9.2"
   }
-}
+}

+ 12 - 0
frontend/src/app/app.config.server.ts

@@ -0,0 +1,12 @@
+import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
+import { provideServerRendering, withRoutes } from '@angular/ssr';
+import { appConfig } from './app.config';
+import { serverRoutes } from './app.routes.server';
+
+const serverConfig: ApplicationConfig = {
+  providers: [
+    provideServerRendering(withRoutes(serverRoutes))
+  ]
+};
+
+export const config = mergeApplicationConfig(appConfig, serverConfig);

+ 2 - 1
frontend/src/app/app.config.ts

@@ -5,6 +5,7 @@ import { provideRouter } from '@angular/router';
 import { HttpClient, provideHttpClient } from '@angular/common/http';
 
 import { routes } from './app.routes';
+import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
 
 
 export const appConfig: ApplicationConfig = {
@@ -16,6 +17,6 @@ export const appConfig: ApplicationConfig = {
     provideTranslateService({
       loader: provideTranslateHttpLoader({prefix: './i18n/', suffix: '.json' }),
       fallbackLang: 'en', lang: 'en'
-    })
+    }), provideClientHydration(withEventReplay())
   ]
 };

+ 8 - 0
frontend/src/app/app.routes.server.ts

@@ -0,0 +1,8 @@
+import { RenderMode, ServerRoute } from '@angular/ssr';
+
+export const serverRoutes: ServerRoute[] = [
+  {
+    path: '**',
+    renderMode: RenderMode.Server
+  }
+];

+ 11 - 7
frontend/src/app/app.ts

@@ -32,21 +32,24 @@ export class App {
         
         let available: String[] = this.languages.available.split("|");
         // If the language is set as a request parameter and its valid, use it.
-        const queryLang = new URLSearchParams(window.location.search).get('lang');
+        // TODO: Disabled in SSR
+        /*const queryLang = new URLSearchParams(window.location.search).get('lang');
         console.log("QUERY LANG: " + queryLang);
         if (queryLang && available.indexOf(queryLang.substring(0, 2).toLowerCase()) != -1)
-            return queryLang.substring(0, 2).toLowerCase();
+            return queryLang.substring(0, 2).toLowerCase();*/
 
         // If the language has been previously set and a valid one is its in local storage, done.
-        if (available.indexOf("" + localStorage.getItem('language')) != -1)
-            return "" + localStorage.getItem('language');
+        // TODO: Disabled in SSR
+        //if (available.indexOf("" + localStorage.getItem('language')) != -1)
+        //    return "" + localStorage.getItem('language');
         
         // If the language is not set, loop the browser accepted languages.
         // When there is a match with the app available languages, return it.
-        for (let l of navigator.languages){
+        // TODO: Disabled in SSR
+        /*for (let l of navigator.languages){
             if ( l.length >= 2 && available.indexOf(l.substring(0, 2).toLowerCase()) != -1)
                 return l.substring(0, 2).toLowerCase();
-        }
+        }*/
         
         // If everything else failed, return the default language.
         return this.languages.default;
@@ -59,7 +62,8 @@ export class App {
         // Detect the best language.
         const lang: string = this.selectLanguage()
         this.translate.use(lang);
-        localStorage.setItem('language', lang)
+        // TODO: Disabled in SSR
+        //localStorage.setItem('language', lang)
         
         // Set meta tags
         this.metaService.addTag({ property: 'author', author });

+ 4 - 2
frontend/src/app/footer/footer.ts

@@ -20,7 +20,8 @@ export class Footer{
         { code: 'eu', name: 'Euskara' },
     ];
 
-    currentLanguage = localStorage.getItem('language');
+    // TODO: Disabled in SSR
+    currentLanguage = 'es';//localStorage.getItem('language');
     
     private translate = inject(TranslateService)
     
@@ -29,7 +30,8 @@ export class Footer{
     
     switchLanguage(languageCode: string): void {
         this.currentLanguage = languageCode;
-        localStorage.setItem('language', languageCode);
+        // TODO: Disabled in SSR
+        //localStorage.setItem('language', languageCode);
         this.translate.use(languageCode);
         window.location.reload();
     }

+ 17 - 11
frontend/src/app/home/home.ts

@@ -1,4 +1,5 @@
-import { Component, OnInit, inject } from '@angular/core';
+import { Component, OnInit, inject, PLATFORM_ID } from '@angular/core';
+import { isPlatformBrowser, isPlatformServer } from '@angular/common';
 import { Meta, Title } from '@angular/platform-browser';
 import { TranslateService, _ } from '@ngx-translate/core';
 import { ProjectService } from '../service/project-service';
@@ -17,22 +18,19 @@ export class Home implements OnInit {
     utilService: UtilService;
     
     private translate = inject(TranslateService)
+    private platform = inject(PLATFORM_ID)
 
     constructor(
       private projectService: ProjectService, private profileService: ProfileService,
       private titleService: Title, private metaService: Meta
     ){
-        this.utilService = new UtilService()
-        this.translate.use("" + localStorage.getItem("language"));
-    }
-
-    ngOnInit(): void {
-        this.projectService.getProjects(1).subscribe(data => { this.projects = data; });
-        this.profileService.getProfile().subscribe(data => { this.profile = data; });
-        this.startInterval();
-        this.translate.use("" + localStorage.getItem("language"));
+        this.utilService = new UtilService();
+        
         // Set meta tags
-        const url: string = window.location.protocol + "//" + window.location.host;
+        console.log("TEST");
+        //const url: string = this.document.location.protocol + "//" + this.document.location.host;
+        const url = "TODO";
+        
         this.metaService.addTag({ property: 'canonical', content: url });
         this.metaService.addTag({ property: 'og:url', content: url });
         this.metaService.addTag({ property: 'og:image', content: url + '/img/logo/leather.png' });
@@ -46,11 +44,19 @@ export class Home implements OnInit {
             this.metaService.addTag({ property: 'description', content: res});
         });
     }
+
+    ngOnInit(): void {
+        this.projectService.getProjects(1).subscribe(data => { this.projects = data; });
+        this.profileService.getProfile().subscribe(data => { this.profile = data; });
+        this.startInterval();
+        
+    }
   
     private delay(ms: number){ return new Promise(resolve => setTimeout(resolve, ms)); }
   
     startInterval() {
         this.intervalId = setInterval(() => {
+            if (!isPlatformBrowser(this.platform)) return;
             const id: number = this.projects[Math.floor(Math.random() * this.projects.length)].id;
             this.projectService.getProjectRandomImage(id.toString()).subscribe(async image => {
                 var img: HTMLImageElement

+ 1 - 1
frontend/src/app/service/profile-service.ts

@@ -11,6 +11,6 @@ export class ProfileService {
   constructor(private http: HttpClient) { }
 
   getProfile(): Observable<ProfileModel[]> {
-    return this.http.get<ProfileModel[]>(this.apiUrl + "?lang=" + localStorage.getItem('language'));
+    return this.http.get<ProfileModel[]>(this.apiUrl + "?lang=" + 'es'/* TODO: localStorage.getItem('language')*/);
   }
 }

+ 3 - 3
frontend/src/app/service/project-service.ts

@@ -16,19 +16,19 @@ export class ProjectService {
     if (images == true || parseInt(images) >= 0)
         paramImages = "&images=" + images;
     return this.http.get<ProjectModel[]>(
-      this.apiUrl + "?lang=" + localStorage.getItem('language') + paramImages
+      this.apiUrl + "?lang=" + 'es'/* TODO: localStorage.getItem('language')*/ + paramImages
     );
   }
 
   getProject(id: string): Observable<ProjectModel> {
     return this.http.get<ProjectModel>(
-      `${this.apiUrl}/${id}` + "?lang=" + localStorage.getItem('language')
+      `${this.apiUrl}/${id}` + "?lang=" + 'es'/* TODO: localStorage.getItem('language')*/
     );
   }
   
   getProjectRandomImage(id: string): Observable<ProjectImageModel> {
     return this.http.get<ProjectImageModel>(
-      `${this.apiUrl}/${id}/images/random` + "?lang=" + localStorage.getItem('language')
+      `${this.apiUrl}/${id}/images/random` + "?lang=" + 'es'/* TODO: localStorage.getItem('language')*/
     );
   }
   

+ 8 - 0
frontend/src/main.server.ts

@@ -0,0 +1,8 @@
+import { BootstrapContext, bootstrapApplication } from '@angular/platform-browser';
+import { App } from './app/app';
+import { config } from './app/app.config.server';
+
+const bootstrap = (context: BootstrapContext) =>
+    bootstrapApplication(App, config, context);
+
+export default bootstrap;

+ 68 - 0
frontend/src/server.ts

@@ -0,0 +1,68 @@
+import {
+  AngularNodeAppEngine,
+  createNodeRequestHandler,
+  isMainModule,
+  writeResponseToNodeResponse,
+} from '@angular/ssr/node';
+import express from 'express';
+import { join } from 'node:path';
+
+const browserDistFolder = join(import.meta.dirname, '../browser');
+
+const app = express();
+const angularApp = new AngularNodeAppEngine();
+
+/**
+ * Example Express Rest API endpoints can be defined here.
+ * Uncomment and define endpoints as necessary.
+ *
+ * Example:
+ * ```ts
+ * app.get('/api/{*splat}', (req, res) => {
+ *   // Handle API request
+ * });
+ * ```
+ */
+
+/**
+ * Serve static files from /browser
+ */
+app.use(
+  express.static(browserDistFolder, {
+    maxAge: '1y',
+    index: false,
+    redirect: false,
+  }),
+);
+
+/**
+ * Handle all other requests by rendering the Angular application.
+ */
+app.use((req, res, next) => {
+  angularApp
+    .handle(req)
+    .then((response) =>
+      response ? writeResponseToNodeResponse(response, res) : next(),
+    )
+    .catch(next);
+});
+
+/**
+ * Start the server if this module is the main entry point, or it is ran via PM2.
+ * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
+ */
+if (isMainModule(import.meta.url) || process.env['pm_id']) {
+  const port = process.env['PORT'] || 4000;
+  app.listen(port, (error) => {
+    if (error) {
+      throw error;
+    }
+
+    console.log(`Node Express server listening on http://localhost:${port}`);
+  });
+}
+
+/**
+ * Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions.
+ */
+export const reqHandler = createNodeRequestHandler(app);

+ 2 - 1
frontend/tsconfig.app.json

@@ -3,7 +3,8 @@
   "compilerOptions": {
     "outDir": "./out-tsc/app",
     "types": [
-      "@angular/localize"
+      "@angular/localize",
+      "node"
     ]
   },
   "include": [