diff --git a/bun.lock b/bun.lock
index 0aeb042..ff4c31b 100644
--- a/bun.lock
+++ b/bun.lock
@@ -20,21 +20,21 @@
"shiki": "^3.22.0",
},
"devDependencies": {
- "@playwright/test": "^1.58.1",
+ "@playwright/test": "^1.58.2",
"@sveltejs/adapter-static": "^3.0.10",
- "@sveltejs/enhanced-img": "^0.9.3",
- "@sveltejs/kit": "^2.50.2",
+ "@sveltejs/enhanced-img": "^0.10.2",
+ "@sveltejs/kit": "^2.52.0",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/svelte": "^5.3.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
- "eslint-plugin-svelte": "^3.14.0",
- "jsdom": "^28.0.0",
+ "eslint-plugin-svelte": "^3.15.0",
+ "jsdom": "^28.1.0",
"mdsvex": "^0.12.6",
"prettier": "^3.8.1",
"sass": "^1.97.3",
- "svelte": "^5.49.1",
+ "svelte": "^5.51.2",
"unplugin-icons": "^23.0.1",
"vite": "^7.3.1",
"vitest": "^4.0.18",
@@ -50,9 +50,9 @@
"@antfu/utils": ["@antfu/utils@8.1.1", "", {}, "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ=="],
- "@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.1.1", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "lru-cache": "^11.2.4" } }, "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ=="],
+ "@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.1.2", "", { "dependencies": { "@csstools/css-calc": "^3.0.0", "@csstools/css-color-parser": "^4.0.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.2.5" } }, "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg=="],
- "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@6.7.6", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.4" } }, "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg=="],
+ "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@6.8.1", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.6" } }, "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ=="],
"@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
@@ -72,6 +72,8 @@
"@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.1", "", {}, "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw=="],
+ "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
+
"@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.0.3", "", { "dependencies": { "@chevrotain/gast": "11.0.3", "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ=="],
"@chevrotain/gast": ["@chevrotain/gast@11.0.3", "", { "dependencies": { "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q=="],
@@ -82,17 +84,17 @@
"@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="],
- "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="],
+ "@csstools/color-helpers": ["@csstools/color-helpers@6.0.1", "", {}, "sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ=="],
- "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="],
+ "@csstools/css-calc": ["@csstools/css-calc@3.1.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ=="],
- "@csstools/css-color-parser": ["@csstools/css-color-parser@3.1.0", "", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA=="],
+ "@csstools/css-color-parser": ["@csstools/css-color-parser@4.0.1", "", { "dependencies": { "@csstools/color-helpers": "^6.0.1", "@csstools/css-calc": "^3.0.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-vYwO15eRBEkeF6xjAno/KQ61HacNhfQuuU/eGwH67DplL0zD5ZixUa563phQvUelA07yDczIXdtmYojCphKJcw=="],
- "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="],
+ "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="],
"@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.0.26", "", {}, "sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA=="],
- "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
+ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
"@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
@@ -266,7 +268,7 @@
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
- "@playwright/test": ["@playwright/test@1.58.1", "", { "dependencies": { "playwright": "1.58.1" }, "bin": { "playwright": "cli.js" } }, "sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w=="],
+ "@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="],
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
@@ -336,9 +338,9 @@
"@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="],
- "@sveltejs/enhanced-img": ["@sveltejs/enhanced-img@0.9.3", "", { "dependencies": { "magic-string": "^0.30.5", "sharp": "^0.34.1", "svelte-parse-markup": "^0.1.5", "vite-imagetools": "^9.0.2", "zimmerframe": "^1.1.2" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0", "svelte": "^5.0.0", "vite": "^6.3.0 || >=7.0.0" } }, "sha512-9T+j4/jcFUqdSjUuN1KrsTo8mJhckm2WCO1waf184kG/uf8rfCn5LSVR71KPxaklBJo1YOAEQDUaqdApZ/UpPw=="],
+ "@sveltejs/enhanced-img": ["@sveltejs/enhanced-img@0.10.2", "", { "dependencies": { "magic-string": "^0.30.5", "sharp": "^0.34.1", "svelte-parse-markup": "^0.1.5", "vite-imagetools": "^9.0.3", "zimmerframe": "^1.1.2" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0", "svelte": "^5.0.0", "vite": "^6.3.0 || >=7.0.0" } }, "sha512-HcIX7KFaLe+3ZD+GcMIlOGKODO8zb8p6I5tY8aoM9tz4GwueGyn9gILyTWZHqXYgg7PXto++ELB/q68wC9j4qw=="],
- "@sveltejs/kit": ["@sveltejs/kit@2.50.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-875hTUkEbz+MyJIxWbQjfMaekqdmEKUUfR7JyKcpfMRZqcGyrO9Gd+iS1D/Dx8LpE5FEtutWGOtlAh4ReSAiOA=="],
+ "@sveltejs/kit": ["@sveltejs/kit@2.52.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-zG+HmJuSF7eC0e7xt2htlOcEMAdEtlVdb7+gAr+ef08EhtwUsjLxcAwBgUCJY3/5p08OVOxVZti91WfXeuLvsg=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
@@ -542,7 +544,7 @@
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
- "cssstyle": ["cssstyle@5.3.7", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", "css-tree": "^3.1.0", "lru-cache": "^11.2.4" } }, "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ=="],
+ "cssstyle": ["cssstyle@6.0.1", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.2", "@csstools/css-syntax-patches-for-csstree": "^1.0.26", "css-tree": "^3.1.0", "lru-cache": "^11.2.5" } }, "sha512-IoJs7La+oFp/AB033wBStxNOJt4+9hHMxsXUPANcoXL2b3W4DZKghlJ2cI/eyeRZIQ9ysvYEorVhjrcYctWbog=="],
"cytoscape": ["cytoscape@3.32.0", "", {}, "sha512-5JHBC9n75kz5851jeklCPmZWcg3hUe6sjqJvyk3+hVqFaKcHwHgxsjeN1yLmggoUc6STbtm9/NQyabQehfjvWQ=="],
@@ -662,7 +664,7 @@
"eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="],
- "eslint-plugin-svelte": ["eslint-plugin-svelte@3.14.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.6.1", "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", "globals": "^16.0.0", "known-css-properties": "^0.37.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", "svelte-eslint-parser": "^1.4.0" }, "peerDependencies": { "eslint": "^8.57.1 || ^9.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-Isw0GvaMm0yHxAj71edAdGFh28ufYs+6rk2KlbbZphnqZAzrH3Se3t12IFh2H9+1F/jlDhBBL4oiOJmLqmYX0g=="],
+ "eslint-plugin-svelte": ["eslint-plugin-svelte@3.15.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.6.1", "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", "globals": "^16.0.0", "known-css-properties": "^0.37.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", "svelte-eslint-parser": "^1.4.0" }, "peerDependencies": { "eslint": "^8.57.1 || ^9.0.0 || ^10.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-QKB7zqfuB8aChOfBTComgDptMf2yxiJx7FE04nneCmtQzgTHvY8UJkuh8J2Rz7KB9FFV9aTHX6r7rdYGvG8T9Q=="],
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
@@ -800,7 +802,7 @@
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
- "jsdom": ["jsdom@28.0.0", "", { "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.7.6", "@exodus/bytes": "^1.11.0", "cssstyle": "^5.3.7", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "undici": "^7.20.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA=="],
+ "jsdom": ["jsdom@28.1.0", "", { "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.8.1", "@bramus/specificity": "^2.4.2", "@exodus/bytes": "^1.11.0", "cssstyle": "^6.0.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "undici": "^7.21.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
@@ -842,7 +844,7 @@
"lottie-web": ["lottie-web@5.13.0", "", {}, "sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ=="],
- "lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
+ "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
@@ -998,9 +1000,9 @@
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
- "playwright": ["playwright@1.58.1", "", { "dependencies": { "playwright-core": "1.58.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ=="],
+ "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
- "playwright-core": ["playwright-core@1.58.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg=="],
+ "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="],
"points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
@@ -1118,7 +1120,7 @@
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
- "svelte": ["svelte@5.49.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.2", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-jj95WnbKbXsXXngYj28a4zx8jeZx50CN/J4r0CEeax2pbfdsETv/J1K8V9Hbu3DCXnpHz5qAikICuxEooi7eNQ=="],
+ "svelte": ["svelte@5.51.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.2", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-AqApqNOxVS97V4Ko9UHTHeSuDJrwauJhZpLDs1gYD8Jk48ntCSWD7NxKje+fnGn5Ja1O3u2FzQZHPdifQjXe3w=="],
"svelte-eslint-parser": ["svelte-eslint-parser@1.4.0", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-fjPzOfipR5S7gQ/JvI9r2H8y9gMGXO3JtmrylHLLyahEMquXI0lrebcjT+9/hNgDej0H7abTyox5HpHmW1PSWA=="],
@@ -1158,7 +1160,7 @@
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
- "undici": ["undici@7.20.0", "", {}, "sha512-MJZrkjyd7DeC+uPZh+5/YaMDxFiiEEaDgbUSVMXayofAkDWF1088CDo+2RPg7B1BuS1qf1vgNE7xqwPxE0DuSQ=="],
+ "undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
@@ -1192,7 +1194,7 @@
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
- "vite-imagetools": ["vite-imagetools@9.0.2", "", { "dependencies": { "@rollup/pluginutils": "^5.0.5", "imagetools-core": "^9.1.0", "sharp": "^0.34.1" } }, "sha512-FV5DXw4swU81t+g8JOLT+T7gKuBOXuVsZ0WGhi7y0R182+GfBYkcf6V9/T0Nweu/vn1X0DA2p5ePMnaGZlRl1A=="],
+ "vite-imagetools": ["vite-imagetools@9.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.5", "imagetools-core": "^9.1.0", "sharp": "^0.34.1" } }, "sha512-FwjApRNZyN+RucPW9Z9kf0dyzyi3r3zlDfrTnzHXNaYpmT3pZ5w//d6QkApy1iypbDm+3fq+Gwfv+PYA4j4uYw=="],
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
diff --git a/package.json b/package.json
index 1858e85..5aef297 100644
--- a/package.json
+++ b/package.json
@@ -16,21 +16,21 @@
"test:unit": "vitest run"
},
"devDependencies": {
- "@playwright/test": "^1.58.1",
+ "@playwright/test": "^1.58.2",
"@sveltejs/adapter-static": "^3.0.10",
- "@sveltejs/enhanced-img": "^0.9.3",
- "@sveltejs/kit": "^2.50.2",
+ "@sveltejs/enhanced-img": "^0.10.2",
+ "@sveltejs/kit": "^2.52.0",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/svelte": "^5.3.1",
- "eslint": "^9.39.2",
+ "eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
- "eslint-plugin-svelte": "^3.14.0",
- "jsdom": "^28.0.0",
+ "eslint-plugin-svelte": "^3.15.0",
+ "jsdom": "^28.1.0",
"mdsvex": "^0.12.6",
"prettier": "^3.8.1",
"sass": "^1.97.3",
- "svelte": "^5.49.1",
+ "svelte": "^5.51.2",
"unplugin-icons": "^23.0.1",
"vite": "^7.3.1",
"vitest": "^4.0.18"
diff --git a/src/app.scss b/src/app.scss
index f060bfa..bc1aad4 100644
--- a/src/app.scss
+++ b/src/app.scss
@@ -287,6 +287,8 @@ div.description {
// Internal navigation
a.nav {
position: relative;
+ color: var(--txt);
+ text-decoration: none;
.arrow {
position: absolute;
top: -0.02em;
@@ -294,12 +296,17 @@ a.nav {
transition: $transition-base;
transform-origin: 50% 53%;
left: -0.2em;
+ display: inline-block;
+ width: auto;
+ text-align: left;
}
.slash {
display: inline-block;
transition: $transition-base;
transform: scale(1, 1) rotate(0deg);
line-height: 0;
+ width: auto;
+ text-align: left;
}
&:hover {
.arrow {
@@ -314,13 +321,18 @@ a.nav {
// External links
a.external {
position: relative;
+ color: var(--txt-2);
+ text-decoration: none;
.arrow {
display: inline-block;
margin-left: 0.4ch;
transition: $transition-fast;
color: var(--txt-2);
+ width: auto;
+ text-align: left;
}
&:hover {
+ color: var(--txt);
text-decoration: underline;
.arrow {
transform: translateY(-0.4ch) translateX(-0.2ch) scale(1, 1) rotate(-30deg);
@@ -330,12 +342,16 @@ a.external {
// Internal links
a.link {
+ color: var(--txt);
+ text-decoration: none;
.arrow {
display: inline-block;
margin-left: 0.5ch;
opacity: 0;
transform: translateX(-0.6em);
transition: $transition-fast;
+ width: auto;
+ text-align: left;
}
.slash {
display: inline-block;
@@ -343,6 +359,8 @@ a.link {
transform: scale(1, 1) rotate(0deg);
line-height: 0;
opacity: 0;
+ width: 1ch;
+ text-align: center;
}
&:hover {
.arrow {
@@ -413,6 +431,8 @@ figure {
&:hover {
&::after {
content: '#';
+ margin-left: 0.3ch;
+ color: var(--txt-3);
}
}
}
diff --git a/src/lib/components/Image.svelte b/src/lib/components/Image.svelte
index e355a48..d3a5eee 100644
--- a/src/lib/components/Image.svelte
+++ b/src/lib/components/Image.svelte
@@ -6,62 +6,118 @@
let loaded = false;
- async function importImage(image) {
- if (!image || typeof image !== 'string') {
- return null;
+ const lazyPictures = import.meta.glob(
+ `/src/content/**/*.{avif,gif,heif,jpeg,jpg,png,tiff,webp}`,
+ {
+ import: 'default',
+ query: {
+ enhanced: true,
+ w: '2400;2000;1600;1200;800;400'
+ }
}
+ );
- const pictures = import.meta.glob(`/src/content/**/*.{avif,gif,heif,jpeg,jpg,png,tiff,webp}`, {
+ const eagerPictures = import.meta.glob(
+ `/src/content/**/*.{avif,gif,heif,jpeg,jpg,png,tiff,webp}`,
+ {
+ eager: true,
import: 'default',
query: {
enhanced: true,
w: '2400;2000;1600;1200;800;400'
}
- });
+ }
+ );
+
+ const isSSR = import.meta.env.SSR;
- // Simple exact filename match
- for (const [path, src] of Object.entries(pictures)) {
+ function findModule(modules, image) {
+ if (!image || typeof image !== 'string') {
+ return null;
+ }
+
+ // Normalize path for relative references
+ const normalizedImage = image.replace(/^\.\//, '').toLowerCase();
+
+ for (const [path, src] of Object.entries(modules)) {
+ const normalizedPath = path.toLowerCase();
const fileName = path.split('/').pop();
- if (fileName === image) {
- try {
- return await src();
- } catch (error) {
- console.error('Error loading image:', error);
- return null;
- }
+ const normalizedFileName = fileName?.toLowerCase();
+
+ if (
+ normalizedFileName === normalizedImage ||
+ normalizedPath.endsWith(`/${normalizedImage}`)
+ ) {
+ return src;
}
}
return null;
}
+ async function importImage(image) {
+ const module = findModule(lazyPictures, image);
+ if (!module) return null;
+
+ try {
+ return typeof module === 'function' ? await module() : module;
+ } catch (error) {
+ console.error('Error loading image:', error);
+ return null;
+ }
+ }
+
+ function resolveImageSync(image) {
+ const module = findModule(eagerPictures, image);
+ if (!module) return null;
+ return typeof module === 'function' ? null : module;
+ }
+
+ $: resolvedImage = isSSR ? resolveImageSync(image) : null;
+
function handleLoad() {
loaded = true;
}
- {#await importImage(image)}
- Loading...
- {:then src}
- {#if src}
-
-
+ {#if isSSR}
+ {#if resolvedImage}
+
+
{:else}
- Image not found: {image}
+ {alt || 'Image unavailable'}
{/if}
- {:catch error}
- Error loading image: {error.message}
- {/await}
+ {:else}
+ {#await importImage(image)}
+ Loading...
+ {:then src}
+ {#if src}
+
+
+
+ {:else}
+ {alt || 'Image unavailable'}
+ {/if}
+ {:catch error}
+ {alt || 'Image unavailable'}
+ {/await}
+ {/if}
diff --git a/src/lib/components/PageHead.svelte b/src/lib/components/PageHead.svelte
index 10d88d2..30f9415 100644
--- a/src/lib/components/PageHead.svelte
+++ b/src/lib/components/PageHead.svelte
@@ -4,32 +4,61 @@
export let title;
export let description;
export let type = 'website';
- export let image = {};
+ export let image = null;
+ export let jsonLd = null;
const hostname = 'https://techquests.dev';
+ const siteName = 'Tech Quests';
+ const authorName = 'Andre Nogueira';
- $: canonicalUrl = $page.url.href.replace(/^http:\/\/[^/]+/, hostname);
+ $: canonicalUrl = `${hostname}${$page.url.pathname}`;
+ $: hasImage = Boolean(image?.img?.src);
+ $: twitterCard = hasImage ? 'summary_large_image' : 'summary';
{title}
-
+
+
+
+
+
+
-
+
- {#if image}
+ {#if hasImage}
-
-
+ {#if image.img.w && image.img.h}
+
+
+ {/if}
+ {:else}
+
+
+
+
+
+
+ {/if}
+ {#if jsonLd}
+
{/if}
diff --git a/src/lib/utils/markdown.js b/src/lib/utils/markdown.js
index 5203e5d..3612f45 100644
--- a/src/lib/utils/markdown.js
+++ b/src/lib/utils/markdown.js
@@ -53,24 +53,90 @@ export function markdownToHtml(markdown) {
return '';
}
+ const cleanedMarkdown = markdown
+ .replace(/
@@ -48,15 +69,56 @@
>aanogueira@protonmail.com->
-
-
-
-
Contact form
-
@@ -78,8 +140,33 @@
form {
display: grid;
- grid-template-columns: auto auto;
- gap: $spacing-sm;
+ grid-template-columns: 1fr 1fr;
+ gap: $spacing-sm $spacing-md;
+ }
+
+ .field {
+ display: grid;
+ gap: $spacing-xs;
+ }
+
+ .section-title {
+ margin-top: $spacing-4xl;
+ }
+
+ .cli-only {
+ display: none;
+ }
+
+ .field.full {
+ grid-column: 1 / -1;
+ }
+
+ .field-label {
+ font-size: $font-xs;
+ font-family: $font-family-mono;
+ color: var(--txt-2);
+ display: grid;
+ gap: $spacing-xs;
}
input[type='text'],
diff --git a/src/routes/projects/+page.svelte b/src/routes/projects/+page.svelte
index e864e4a..e3054c0 100644
--- a/src/routes/projects/+page.svelte
+++ b/src/routes/projects/+page.svelte
@@ -10,14 +10,21 @@
@@ -38,12 +45,30 @@
max-width: 100%;
}
+ a.link {
+ display: grid;
+ grid-template-areas:
+ 'thumb'
+ 'title'
+ 'description';
+ gap: $spacing-sm;
+ }
+
+ .thumb {
+ grid-area: thumb;
+ }
+
h2 {
+ grid-area: title;
margin: $spacing-sm 0 $spacing-smd 0;
color: var(--txt);
font-size: $font-lg;
}
+ .description {
+ grid-area: description;
+ }
+
@media (max-width: $breakpoint-tablet) {
.posts {
grid-template-columns: auto;
diff --git a/src/routes/projects/[slug]/+page.js b/src/routes/projects/[slug]/+page.js
index 836db8f..9f77d37 100644
--- a/src/routes/projects/[slug]/+page.js
+++ b/src/routes/projects/[slug]/+page.js
@@ -1,4 +1,5 @@
import { nameFromPath, importOgImage } from '$lib/js/posts.js';
+import { buildProjectJsonLd, buildImageObject } from '$lib/utils/structured-data.js';
import { error } from '@sveltejs/kit';
import { dev } from '$app/environment';
@@ -20,8 +21,25 @@ export async function load({ params }) {
throw error(404, 'Project Not Found');
}
- let imagePath = match.path.split('/').slice(0, -1).join('/') + '/' + post.metadata.images[0];
- let image = await importOgImage(imagePath);
+ let image = null;
+ if (post.metadata.images?.length) {
+ let imagePath = match.path.split('/').slice(0, -1).join('/') + '/' + post.metadata.images[0];
+ image = await importOgImage(imagePath);
+ }
+
+ const imageObjects = post.metadata.images?.length
+ ? (
+ await Promise.all(
+ post.metadata.images.map(async (item) => {
+ const imagePath = match.path.split('/').slice(0, -1).join('/') + '/' + item;
+ const imageModule = await importOgImage(imagePath);
+ return buildImageObject(imageModule);
+ })
+ )
+ ).filter(Boolean)
+ : [];
+
+ const jsonLd = buildProjectJsonLd({ ...post.metadata, slug: params.slug }, imageObjects);
return {
post,
@@ -29,7 +47,8 @@ export async function load({ params }) {
title: post.metadata.name,
description: post.metadata.description,
type: 'article',
- image
+ image,
+ jsonLd
}
};
}
diff --git a/src/routes/projects/[slug]/+page.svelte b/src/routes/projects/[slug]/+page.svelte
index 0a76cf1..70dbb60 100644
--- a/src/routes/projects/[slug]/+page.svelte
+++ b/src/routes/projects/[slug]/+page.svelte
@@ -58,7 +58,7 @@
-
+
@@ -79,6 +79,19 @@
a {
font-family: $font-family-mono;
font-size: $font-md;
+ color: var(--txt-2);
+ text-decoration: underline;
+ text-decoration-color: var(--txt-3);
+ text-decoration-thickness: 1px;
+ text-underline-offset: 0.12em;
+ transition:
+ color 0.2s,
+ text-decoration-color 0.2s;
+ &:hover,
+ &:focus-visible {
+ color: var(--txt);
+ text-decoration-color: var(--txt);
+ }
}
.row {
diff --git a/src/routes/projects/rss.xml/+server.js b/src/routes/projects/rss.xml/+server.js
index 5072934..06e750c 100644
--- a/src/routes/projects/rss.xml/+server.js
+++ b/src/routes/projects/rss.xml/+server.js
@@ -1,5 +1,6 @@
import { getPosts, importOgImage } from '$lib/js/posts.js';
import { escapeXml, getMarkdownContent, generateRssXml } from '$lib/utils/rss.js';
+import { convertHtmlToPlainText, truncateHtmlForDescription } from '$lib/utils/markdown.js';
export const prerender = true;
@@ -44,37 +45,10 @@ export async function GET() {
content += `${escapeXml(project.description)}
`;
}
- // Add metadata information
- content +=
- '
';
-
- if (project.description) {
- content += `
Summary: ${escapeXml(project.description)}
`;
- }
-
- if (project.date) {
- content += `
Published: ${new Date(project.date).toDateString()}
`;
- }
-
- if (project.tags?.length > 0) {
- content += `
Tags: ${project.tags.map((tag) => escapeXml(tag)).join(', ')}
`;
- }
-
- // Add project-specific metadata
- if (project.icon) {
- content += `
Icon: ${escapeXml(project.icon)}
`;
+ if (!content) {
+ content = project.description || 'No content available';
}
- if (project.website) {
- content += `
Website: ${escapeXml(project.website)}
`;
- }
-
- if (project.github) {
- content += `
GitHub: ${escapeXml(project.github)}
`;
- }
-
- content += '
';
-
return content || 'No content available';
}
@@ -131,11 +105,14 @@ export async function GET() {
const feedEmail = 'aanogueira@protonmail.com';
// Generate RSS items
- const rssItems = projectsWithImages.map(
- (project) => ` -
+ const rssItems = projectsWithImages.map((project) => {
+ const body = project.content || project.description || 'No content available';
+ const plainText = convertHtmlToPlainText(body);
+ const summary = truncateHtmlForDescription(plainText, 220);
+ return `
-
${escapeXml(project.name || project.title || 'Untitled')}
- ${escapeXml(project.description || 'No description available')}
-
+ ${escapeXml(summary || project.description || 'No description available')}
+
${feedBaseUrl}/projects/${project.slug}
${feedBaseUrl}/projects/${project.slug}
${new Date(project.date || new Date()).toUTCString()}
@@ -144,8 +121,8 @@ export async function GET() {
${project.website ? `${escapeXml(project.website)}` : ''}
${project.github ? `GitHub Repository` : ''}
${project.imageData ? `` : ''}
-
`
- );
+ `;
+ });
// Generate RSS XML using shared utility
const rss = generateRssXml({
diff --git a/src/routes/rss.xml/+server.js b/src/routes/rss.xml/+server.js
index 0ab1306..7188202 100644
--- a/src/routes/rss.xml/+server.js
+++ b/src/routes/rss.xml/+server.js
@@ -1,5 +1,6 @@
import { getPosts, importOgImage } from '$lib/js/posts.js';
import { escapeXml, getMarkdownContent, generateRssItem, generateRssXml } from '$lib/utils/rss.js';
+import { convertHtmlToPlainText, truncateHtmlForDescription } from '$lib/utils/markdown.js';
export const prerender = true;
@@ -38,54 +39,23 @@ export async function GET() {
const markdownContent = await getMarkdownContent(pathKey, contentModules);
if (markdownContent?.trim()) {
const { markdownToHtml } = await import('$lib/utils/markdown.js');
- let htmlContent = await markdownToHtml(markdownContent);
+ const htmlContent = await markdownToHtml(markdownContent);
- content += htmlContent + '
';
+ content += htmlContent;
}
} catch (error) {
console.warn('Failed to process markdown content for:', item.slug, error);
if (item.description) {
- content += `${escapeXml(item.description)}
`;
+ content += `${escapeXml(item.description)}
`;
}
}
}
// Fallback to description if no content was found
if (!content && item.description) {
- content += `${escapeXml(item.description)}
`;
+ content += `${item.description}
`;
}
- // Add metadata information
- content +=
- '';
-
- if (item.description) {
- content += `
Summary: ${escapeXml(item.description)}
`;
- }
-
- if (item.date) {
- content += `
Published: ${new Date(item.date).toDateString()}
`;
- }
-
- if (item.tags?.length > 0) {
- content += `
Tags: ${item.tags.map((tag) => escapeXml(tag)).join(', ')}
`;
- }
-
- // Add project-specific metadata
- if (item.icon) {
- content += `
Icon: ${escapeXml(item.icon)}
`;
- }
-
- if (item.website) {
- content += `
Website: ${escapeXml(item.website)}
`;
- }
-
- if (item.github) {
- content += `
GitHub: ${escapeXml(item.github)}
`;
- }
-
- content += '
';
-
return content || 'No content available';
}
@@ -178,7 +148,7 @@ export async function GET() {
]);
// Sort by date (newest first), with projects treated as recent
- const sortedContent = allContent.sort((a, b) => b.sortDate - a.sortDate).slice(0, 25); // Latest 25 items
+ const sortedContent = allContent.sort((a, b) => b.sortDate - a.sortDate).slice(0, 50); // Latest 50 items
// Define feed metadata
const baseUrl = 'https://techquests.dev';
@@ -188,9 +158,21 @@ export async function GET() {
const feedEmail = 'aanogueira@protonmail.com';
// Generate RSS XML using shared utility
- const rssItems = sortedContent.map((item) =>
- generateRssItem(item, baseUrl, feedAuthor, feedEmail)
- );
+ const rssItems = sortedContent.map((item) => {
+ const body = item.content || item.description || 'No content available';
+ const plainText = convertHtmlToPlainText(body);
+ const summary = truncateHtmlForDescription(plainText, 220);
+
+ return generateRssItem(
+ {
+ ...item,
+ description: summary || item.description
+ },
+ baseUrl,
+ feedAuthor,
+ feedEmail
+ );
+ });
const rss = generateRssXml({
title: feedTitle,
diff --git a/src/routes/sitemap.xml/+server.js b/src/routes/sitemap.xml/+server.js
index ba43040..2726b83 100644
--- a/src/routes/sitemap.xml/+server.js
+++ b/src/routes/sitemap.xml/+server.js
@@ -7,6 +7,10 @@ export async function GET() {
const modules = import.meta.glob('/src/content/blog/*/*.md');
const posts = await getPosts(modules);
+ // Get all projects
+ const projectModules = import.meta.glob('/src/content/projects/*/*.md');
+ const projects = await getPosts(projectModules);
+
// Define the base URL
const baseUrl = 'https://techquests.dev';
@@ -44,6 +48,17 @@ export async function GET() {
`
)
.join('')}
+ ${projects
+ .map(
+ (project) => `
+
+ ${baseUrl}/projects/${project.slug}
+ ${new Date(project.date || new Date()).toISOString().split('T')[0]}
+ yearly
+ 0.6
+ `
+ )
+ .join('')}
`;
return new Response(sitemap, {
diff --git a/static/robots.txt b/static/robots.txt
index 4d21601..14ba526 100644
--- a/static/robots.txt
+++ b/static/robots.txt
@@ -1,11 +1,6 @@
User-agent: *
Allow: /
-# Disallow common areas that shouldn't be crawled
-Disallow: /api/
-Disallow: /_app/version.json
-Disallow: /*.json$
-
# Specific bot optimizations
User-agent: GPTBot
Disallow: /
@@ -35,6 +30,3 @@ Sitemap: https://techquests.dev/sitemap.xml
Allow: /rss.xml
Allow: /blog/rss.xml
Allow: /projects/rss.xml
-
-# Common crawl delay for good citizenship
-Crawl-delay: 1