diff --git a/mdsvex.config.js b/mdsvex.config.js deleted file mode 100644 index 5576a98..0000000 --- a/mdsvex.config.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - extensions: ['.md'] -}; diff --git a/package-lock.json b/package-lock.json index 1ec73c8..44d084e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "eslint": "^9.21.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-svelte": "^3.0.0", - "mdsvex": "^0.12.3", + "mdsvex": "^0.12.6", "mdsvex-relative-images": "^1.0.3", "postcss": "^8.5.3", "prettier": "^3.5.2", @@ -521,9 +521,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -535,10 +535,20 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -549,9 +559,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", - "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -573,13 +583,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", - "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -593,13 +606,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", - "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.12.0", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { @@ -1159,6 +1172,13 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@svelte-put/dragscroll": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@svelte-put/dragscroll/-/dragscroll-4.0.0.tgz", @@ -1193,9 +1213,9 @@ } }, "node_modules/@sveltejs/adapter-static": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.8.tgz", - "integrity": "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.9.tgz", + "integrity": "sha512-aytHXcMi7lb9ljsWUzXYQ0p5X1z9oWud2olu/EpmH7aCu4m84h7QLvb5Wp+CFirKcwoNnYvYWhyP/L8Vh1ztdw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1203,25 +1223,26 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.22.2", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.22.2.tgz", - "integrity": "sha512-2MvEpSYabUrsJAoq5qCOBGAlkICjfjunrnLcx3YAk2XV7TvAIhomlKsAgR4H/4uns5rAfYmj7Wet5KRtc8dPIg==", + "version": "2.43.5", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.43.5.tgz", + "integrity": "sha512-44Mm5csR4mesKx2Eyhtk8UVrLJ4c04BT2wMTfYGKJMOkUqpHP5KLL2DPV0hXUA4t4+T3ZYe0aBygd42lVYv2cA==", "dev": true, "license": "MIT", + "peer": true, "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.1.0", + "devalue": "^5.3.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": "^2.6.0", - "sirv": "^3.0.0", - "vitefu": "^1.0.6" + "sirv": "^3.0.0" }, "bin": { "svelte-kit": "svelte-kit.js" @@ -1230,9 +1251,15 @@ "node": ">=18.13" }, "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", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } } }, "node_modules/@sveltejs/vite-plugin-svelte": { @@ -1264,7 +1291,6 @@ "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.3.7" }, @@ -1552,12 +1578,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/node": { "version": "22.13.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz", "integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.20.0" } @@ -1575,6 +1612,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1751,6 +1789,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -1934,7 +1973,6 @@ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -1953,9 +1991,9 @@ } }, "node_modules/devalue": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", - "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.3.2.tgz", + "integrity": "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==", "dev": true, "license": "MIT" }, @@ -2045,19 +2083,21 @@ } }, "node_modules/eslint": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz", - "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.2", - "@eslint/core": "^0.12.0", - "@eslint/eslintrc": "^3.3.0", - "@eslint/js": "9.21.0", - "@eslint/plugin-kit": "^0.2.7", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.31.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -2068,9 +2108,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2168,9 +2208,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2185,9 +2225,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2205,15 +2245,15 @@ "license": "MIT" }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3005,15 +3045,17 @@ } }, "node_modules/mdsvex": { - "version": "0.12.3", - "resolved": "https://registry.npmjs.org/mdsvex/-/mdsvex-0.12.3.tgz", - "integrity": "sha512-C/uIJamjNo5PHHnR3JHqsBPoLcfUBpzRmAEB6FLMXI/s7XHOceswjDMKqSPEW2WHmYpKm0taZ3U20GSyhMridA==", + "version": "0.12.6", + "resolved": "https://registry.npmjs.org/mdsvex/-/mdsvex-0.12.6.tgz", + "integrity": "sha512-pupx2gzWh3hDtm/iDW4WuCpljmyHbHi34r7ktOqpPGvyiM4MyfNgdJ3qMizXdgCErmvYC9Nn/qyjePy+4ss9Wg==", "dev": true, "license": "MIT", "dependencies": { + "@types/mdast": "^4.0.4", "@types/unist": "^2.0.3", "prism-svelte": "^0.4.7", "prismjs": "^1.17.1", + "unist-util-visit": "^2.0.1", "vfile-message": "^2.0.4" }, "peerDependencies": { @@ -3031,6 +3073,48 @@ "unist-util-visit": "^3.1.0" } }, + "node_modules/mdsvex/node_modules/unist-util-is": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdsvex/node_modules/unist-util-visit": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", + "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0", + "unist-util-visit-parents": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdsvex/node_modules/unist-util-visit-parents": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", + "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3257,6 +3341,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3284,6 +3369,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -3424,6 +3510,7 @@ "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -3440,6 +3527,7 @@ "integrity": "sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" @@ -3799,6 +3887,7 @@ "integrity": "sha512-2Mo/AfObaw9zuD0u1JJ7sOVzRCGcpETEyDkLbtkcctWpCMCIyT0iz83xD8JT29SR7O4SgswuPRIDYReYF/607A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -3951,7 +4040,8 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.9.tgz", "integrity": "sha512-12laZu+fv1ONDRoNR9ipTOpUD7RN9essRVkX36sjxuRUInpN7hIiHN4lBd/SIFjbISvnXzp8h/hXzmU8SQQYhw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.2.1", @@ -4029,6 +4119,7 @@ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4167,11 +4258,12 @@ } }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", + "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -4326,21 +4418,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index e206e05..ee4f7bc 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "eslint": "^9.21.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-svelte": "^3.0.0", - "mdsvex": "^0.12.3", + "mdsvex": "^0.12.6", "mdsvex-relative-images": "^1.0.3", "postcss": "^8.5.3", "prettier": "^3.5.2", diff --git a/src/app.css b/src/app.css index 06dd7c0..b80ef25 100644 --- a/src/app.css +++ b/src/app.css @@ -26,11 +26,15 @@ src: url('/fonts/Disket-Mono-Bold.ttf'); } -.no-x-scroll { - max-width: 100%; - overflow-x: hidden; -} +.hero { + @apply bg-[url('/common/background-day.webp')] dark:bg-[url('/common/background-night.webp')] bg-cover bg-fixed text-slate-50; + text-shadow: 0 0 15px rgba(0, 0, 0, 0.25); -.hero-background { - @apply bg-[url('/common/background-day.webp')] dark:bg-[url('/common/background-night.webp')] bg-cover bg-fixed; + h1 { + @apply font-disket text-4xl font-bold sm:text-6xl md:text-8xl; + } + + h2 { + @apply font-sans text-xl font-bold sm:text-2xl md:text-4xl; + } } \ No newline at end of file diff --git a/src/app.d.ts b/src/app.d.ts index 1243e75..9dfaddb 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,5 +1,3 @@ -import type { Member } from '$lib/types/Member'; - declare global { namespace App { // interface Error {} @@ -20,17 +18,25 @@ declare global { type MdsvexResolver = () => Promise; + type BlogPostType = 'article' | 'update' | 'event'; + interface BlogPost { slug: string; + type?: BlogPostType; title: string; - thumbnail: string; - date: string; + thumbnail?: string; + thumbnailAlt?: string; + date?: string; + dateChanged?: string; + dateEventFrom?: string; + dateEventTo?: string; description: string; publisher: string; published?: boolean; - member?: Member; + projects?: string[]; + authors?: string | string[]; } } } -export {}; +export { }; diff --git a/src/app.html b/src/app.html index bd03bd5..862cd64 100644 --- a/src/app.html +++ b/src/app.html @@ -5,7 +5,7 @@ - + diff --git a/src/blogs/hello_world.md b/src/blogs/hello_world.md new file mode 100644 index 0000000..b00c051 --- /dev/null +++ b/src/blogs/hello_world.md @@ -0,0 +1,166 @@ +--- +title: 'Наш первый блог' +thumbnail: 'wave.png' +thumbnailAlt: 'Смайлик с махающей рукой' +date: '2025-10-22' +description: 'Немного о том, как мы делали наш сайт' +type: 'article' +authors: 'rndtrash' +--- + +# ПРИВЕТ ! + +Добро пожаловать на первый блог-пост нашего нового сайта! +На момент написания мы ведём работу над кучей проектов, и ввиду общей занятости [народа](/team), +работа идёт медленно, да и посты писать особо некому. + +Так давайте расскажем хотя бы про этот сайт! + +## Вводные данные + +Сайт написан на TypeScript с применением фреймворка [SvelteKit](https://svelte.dev). Некоторые отзываются о нём, мягко говоря, не лестно, +но нас Svelte подкупил тем, что все страницы, и даже ручки API, можно предварительно отрендерить по файлам, получив полностью статичный сайт! +А фреймворк [Tailwind CSS](https://tailwindcss.com) даёт нам удобные классы и неплохие дефолтные стили с цветовой палитрой. Пока у нас нет +_полноценных_ веб-дизайнеров, довольствуемся их встроенной палитрой. + +Это решение нам досталось в наследство от сайта [Small Fish](https://smallfi.sh). Впрочем, как и половина их кодовой базы: +для быстрого старта с разрешения команды мы воспользовались [их исходниками](https://github.com/Small-Fish-Dev/small-fish-dev.github.io). +Но вы не подумайте, что сайт слизан подчистую! Как минимум в процессе обновления версий фреймворков пришлось провести небольшой рефакторинг, +а потом ещё пошли наши прочие хотелки (об этом дальше), и можно смело сказать, что спустя +[сотню коммитов](https://git.teasanctuary.ru/TeaSanctuary/teasanctuary.ru/commits/branch/master), +из общего остались только фреймворки и сама структура сайта. + +В общем-то, главная причина почему мы взяли их сайт за основу, а не написали вручную — это удобная **система блогов**. + +## Собственно, блоги + +Конечно, в нашей небольшой команде абсолютно все компьютерно грамотные ([вы можете это исправить!](https://teasanctuary.ru/discord)), и никому не составит +труда настрочить текст вот так, просто HTML-ом. Но как же хочется писать блог в удобном WYSIWYG-редакторе, или хотя бы в простом формате! + +На помощь приходит библиотека [mdsvex](https://mdsvex.pngwn.io/). Она по сути превращает `.md` файлы в Svelte-компоненты, которые можно потом +встроить куда угодно. Здорово! + +Только надо решить парочку проблем. Во-первых, все элементы, вроде ссылок или картинок, переводятся в обычные HTML5-объекты. А у нас есть свои собственные, +особые ссылки ([вот такие!](https://хамяк.рф)). Благо, mdsvex позволяет просто использовать Svelte-компоненты, если правильно настроить авто-импортирование. +_Но это же не удобно!_ Это ведь надо знать, что есть _такие_ ссылки, а есть _не_ такие. Плюс, это будет выглядеть уродливо в тех же WYSIWYG-редакторах по +типу [Obsidian](https://obsidian.md). + +Решилось это с помощью системы layout-ов внутри mdsvex: можно задавать шаблоны, которые будут натянуты вокруг преобразованного Markdown-текста. Тут же +можно экспортировать особый объект, где к названию каждого стандартного HTML-элемента можно поставить ссылку на свой собственный класс. Таким образом, +каждый `...` превращается в `...`, без лишних телодвижений писателя блога. + +Отлично, у нас есть отдельные блоги, теперь хочется собрать их все вместе, и желательно рассортировать по дате. И вот тут всплывает вторая проблема: +система Git, которой мы пользуемся, просто не хранит дату создания и изменения файла! Надо это всё записать куда-то в метаданные. Самое простое решение: +присобачить к каждому Markdown-блогу по JSON-у. Но есть решение ещё лучше, оно +называется [frontmatter YAML](https://docs.github.com/ru/contributing/writing-for-github-docs/using-yaml-frontmatter) — де-факто стандарт встраивания +любых метаданных в файл Markdown. + +Здесь мы пишем дату в формате ISO, ну и на сдачу можно добавить кучу других данных. За примером далеко идти не надо: вот вам заголовок этой же заметки: + +```md +--- +title: 'Наш первый блог' +thumbnail: 'wave.png' +thumbnailAlt: 'Смайлик с махающей рукой' +date: '2025-10-22' +description: 'Немного о том, как мы делали наш сайт' +type: 'article' +authors: 'rndtrash' +--- +``` + +Тут вам и дата, и описание, и даже красивая миниатюра! И сюда можно вставить вообще всё, что поддерживает YAML, в том числе списки и булевы переменные. +Зачем? Ну-у-у... Потом разберёмся🙂 + +Важно то, что теперь с помощью `import.meta.glob('/src/blogs/*.md')` можно просто пройтись по всем постам, выдернуть метаданные и отсортировать их по дате. + +Эта система нам пригодилась в ещё одной вещи, которую, казалось бы, давно забыли, а зря! + +## RSS-лента + +Да, мы сами в шоке, но даже после становления фактической олигополии интернета Google-Twitter-ЧёЕщёТам, RSS-ленты продолжали использоваться для подкастов. +А с недавней волной популярности децентрализации ([Mastodon](https://joinmastodon.org/ru), [BlueSky](https://bsky.social), [NeoCities](https://neocities.org/) и прочие), +в моду вернулись маленькие бложики. _Да, мы знаем, что они никуда и не уходили, можете не писать гневные комментарии. Ах, да, их некуда писать. Ой._ + +RSS в этом плане нам очень удобен, во-первых, потому, что это просто XML-файл, и его можно просто куда-то положить, а во-вторых, на него могут подписаться не только люди, +но и машины. В нашем случае это Discord-бот [FeedCord](https://github.com/Qolors/FeedCord/). Ура, можно не городить свой воркфлоу, дёргающий [веб-хуки](https://discord.com/developers/docs/resources/webhook)! + +Поскольку SvelteKit — это fullstack-фреймворк, на нём же напишем простую ручку [rss.xml](https://teasanctuary.ru/blog/rss.xml). Это будет простая функция `GET()`, +возвращающая текст, ручками слепленный в что-то приблизительно похожее на XML. Но погодите, наш сайт же статичный, как мы будем вызывать эту JS-функцию? + +_Внимание, фокус!_ Мы берём обычный файл `src/routes/blog/rss.xml/+server.ts`, и добавляем в него волшебную строчку: + +```ts +export const prerender = true; +``` + +При сборке SvelteKit видит, что страницу можно сгенерировать заранее, выполняет весь этот код (а там то же самое, что в странице со списком блог-постов, но очевидно, в формате XML), +и в папке `build/` создаёт файл `rss.xml`. _Та-да!_ + +Конечно, это отвратительный хак, но мне честно не хотелось добавлять этап постобработки, у нас и так там стоит [генератор sitemap.xml](https://github.com/bartholomej/svelte-sitemap). +Да, ещё одна забытая технология. Надеюсь, хоть так [чёртовы ЖиПиТи-боты](https://git.teasanctuary.ru/TeaSanctuary/teasanctuary.ru/src/branch/master/static/robots.txt) перестанут +вслепую тыкаться по нашим сайтам, особенно по нашему инстансу Forgejo. + +## Деплой + +Сайт Small Fish, упомянутый в самом начале, располагается на GitHub Pages: CI/CD хук собирает сайт, запаковывает его в zip-архив и публикует на своём домене `.github.io`, +который ещё файлом `CNAME` можно поменять на абсолютно любой. + +Мы же хотим оставаться независимыми, в конце концов, зачем мы платим за серваки? Вот Nginx в контейнере, вот SFTP, закидывай и всё! + +Так и было, когда наш сайт был проще, _намного проще_ (умоляю вас, **не смотрите** в Web-архив!!!), но потом захотелось CD как у больших дядь. +На это есть [Forgejo Actions](https://forgejo.org/docs/latest/user/actions/reference/), местный аналог GitHub Actions. Пишешь скрипт, пушишь коммит, +всё будет сделано за тебя. Так, стоп, а почему не работает? Стоп, надо ещё хостить свой раннер? + +Да уж, совсем забыл упомянуть... На нашем основном сервере с доменом [teasanctuary.ru](https://teasanctuary.ru) всего **один гигабайт ОЗУ**. **Один**. Сюда еле влез Forgejo +с несколькими сервисами для нашего внутреннего пользования, а тут ещё надо запускать NodeJs, который запросто выжрет остаток и не подавится. + +Как быть?.. + +Помощь пришла откуда не ждали. Я, **rndtrash**, некоторое время назад переехал на другую квартиру, и пока я разбирал вещи, среди ящичков моему взору предстала ма-а-аленькая чёрная +коробочка с логотипом _некоторого пчелиного оператора_. Как позже выяснилось, это Smart TV-приставка, которую провайдер просто отказался забирать назад, ну а хозяевам квартиры она +просто не нужна. + +Меня накрыло в пот, и спустя один запрос в поисковике, один тред на 4PDA, один звонок владельцам квартиры и тридцать минут беготни в магазин электроники, +я понял, что передо мной лежал халявный сервер на Linux с ARMv8 процессором на 1.5 ГГц с одним гигабайтом оперативки. + +**ДЖЕКПОТ!!! 🎰🎰🎰** + +Дальше дело за малым: я просверлил несколько вентиляционных отверстий, чтобы коробка совсем не прокоптилась (вы бы видели местный охлад...), разрегистрировал её в нашем Forgejo, +и проработал план действий. Ввиду простоты сайта, наш [deploy-скрипт](https://git.teasanctuary.ru/TeaSanctuary/teasanctuary.ru/src/branch/master/.forgejo/workflows/deploy.yaml) +предельно прост: + +1. Ставим Ноду, клонируем репозиторий, устанавливаем Npm-зависимости +2. Кешируем зависимости, потому что это всё непотребство работает от _моего_ Интернета +3. Собираем сайт командой `npm run build` и формируем артефакт — архив с сайтом +4. Достаём SSH-ключ к основному серверу из секретного хранилища и регистрируем в SSH-агенте. Этот ключ имеет доступ только к особому пользователю, + который в свою очередь имеет доступ только к особой папке только для сайта +5. С помощью `rsync`, полностью заменяем содержимое удалённой папки на содержимое артефакта, сделанного на этапе **3**. + Это сделано на случай, если в репозитории случайно оказалось что-то непубличное👻 + +В этом плане есть только один прокол, причём очень важный. + +## SSL + +Тот самый заветный зелёный замочек у нас был с самого начала, но в какой-то момент он пропал. Покопавшись в логах [Certbot](https://certbot.eff.org/), мы с ужасом обнаружили, +что с переездом всего ПО в Podman-контейнер, он вот уже месяц как не мог запустить свой собственный сервер для сертификации. + +Вообще, процесс получения сертификата [Let's Encrypt](https://letsencrypt.org/) происходит вот так: некий бот раз в два-три месяца запускает свой собственный сервер на порту 80, +и с его помощью раздаёт особый файл-ключ по адресу `домен/.well-known/acme-challenge/случайные-буквы`. Главный сервер Let's Encrypt стучится по этому адресу, тем самым удостоверяется, +что к нему обратился действительно владелец домена, а не случайный прохожий, и наконец, регистрирует обновлённый файл сертификата. + +У нас опять всё не как у людей, поэтому мы сделали вот как: Certbot имеет особый режим, где он просто кладёт нужный файл, а дальше системный администратор как-то сам решает, каким +образом поднять 80 порт. Мы не можем закинуть этот файл прямо в папку сайта, потому что наш deploy-скрипт полностью уничтожает постороннее содержимое, поэтому мы сделали отдельную +папку, а Nginx в свою очередь сконфигурировали так, что все обращения по пути `/.well-known/acme-challenge` идут мимо папки сайта. + +## Финал + +Вот столько трудностей нам пришлось преодолеть, чтобы довести сайт хотя бы до такого состояния. Если что, на момент написания работает только главная страница, и собственно блоги. +Остаётся надеяться, что написание остальных страниц не составит особых усилий. + +Если вам понравился наш блог, то подписывайтесь на наш [RSS-фид](https://teasanctuary.ru/blog/rss.xml), и обязательно загляните в +[наше сообщество в Discord](https://teasanctuary.ru/discord). + +Ещё увидимся! + +- Команда Tea Sanctuary, 2025 diff --git a/src/blogs/test_unpublished.md b/src/blogs/test_unpublished.md new file mode 100644 index 0000000..4691141 --- /dev/null +++ b/src/blogs/test_unpublished.md @@ -0,0 +1,13 @@ +--- +title: 'Тестовый блог' +date: +dateChanged: '2025-09-29' +description: 'Немного о самом сайте' +projects: ['ts-hldm'] +--- + +# ПРИВЕТ ! + +Добро пожаловать на наш первый блог-пост! + + diff --git a/src/lib/components/BlogCard.svelte b/src/lib/components/BlogCard.svelte new file mode 100644 index 0000000..b40213d --- /dev/null +++ b/src/lib/components/BlogCard.svelte @@ -0,0 +1,158 @@ + + + + + +
+ {#if post.thumbnail} + {post.thumbnailAlt + {/if} + {#if isPostFresh} +
+ {#if isPostUpdated} + ОБНОВЛЕНО + {:else} + НОВОЕ + {/if} +
+ {/if} +
+
+
+ + {#if post.dateChanged} + + {/if} +
+ + + {blogPostTypeToString(type)} + +
+
+ +

{post.title}

+ + {#if post.description} +

{post.description}

+ {/if} +
+
+ + diff --git a/src/lib/components/DateWidget.svelte b/src/lib/components/DateWidget.svelte new file mode 100644 index 0000000..69ab90d --- /dev/null +++ b/src/lib/components/DateWidget.svelte @@ -0,0 +1,32 @@ + + +
+ + + {dateString + ? new Date(dateString).toLocaleString(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric' + }) + : 'Не опубликован!'} + +
diff --git a/src/lib/components/HoverIcon.svelte b/src/lib/components/HoverIcon.svelte index 7fbff3c..bd0e374 100644 --- a/src/lib/components/HoverIcon.svelte +++ b/src/lib/components/HoverIcon.svelte @@ -4,29 +4,39 @@ let className: string = ''; export { className as class }; export let src: string | null = null; - export let text: string; + export let alt: string | undefined = undefined; export let size: number = 32; export let black: boolean = false; - let isUrl; - $: isUrl = src?.startsWith('/') || src?.startsWith('http'); + function isUrl(src?: string) { + return src?.startsWith('/') || src?.startsWith('http'); + } -
{#if src} - {#if isUrl} - {text} + {#if isUrl(src)} + {:else} {/if} {:else} - {text} + {alt ?? 'Без иконки'} {/if} -
+ diff --git a/src/lib/components/IconBlock.svelte b/src/lib/components/IconBlock.svelte new file mode 100644 index 0000000..1d4922a --- /dev/null +++ b/src/lib/components/IconBlock.svelte @@ -0,0 +1,18 @@ + + +
+
+ + {caption} +
+
+ +
+
diff --git a/src/lib/components/Img.svelte b/src/lib/components/Img.svelte new file mode 100644 index 0000000..fd7de17 --- /dev/null +++ b/src/lib/components/Img.svelte @@ -0,0 +1,20 @@ + + +
+ {alt + {#if caption} +

{caption}

+ {/if} +
diff --git a/src/lib/components/InfoBlock.svelte b/src/lib/components/InfoBlock.svelte new file mode 100644 index 0000000..7635b91 --- /dev/null +++ b/src/lib/components/InfoBlock.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/src/lib/components/MdsvexLayout.svelte b/src/lib/components/MdsvexLayout.svelte new file mode 100644 index 0000000..061e2b2 --- /dev/null +++ b/src/lib/components/MdsvexLayout.svelte @@ -0,0 +1,11 @@ + + + + +{@render children()} diff --git a/src/lib/components/NavBar.svelte b/src/lib/components/NavBar.svelte index 3316ee0..84e5596 100644 --- a/src/lib/components/NavBar.svelte +++ b/src/lib/components/NavBar.svelte @@ -20,7 +20,7 @@ diff --git a/src/lib/components/SocialButton.svelte b/src/lib/components/SocialButton.svelte index f12be28..b18df8b 100644 --- a/src/lib/components/SocialButton.svelte +++ b/src/lib/components/SocialButton.svelte @@ -14,7 +14,7 @@ target={isLinkLocal(href) ? '_self' : '_blank'} >
- +
= 40rem', false); - + - + diff --git a/src/lib/components/WarningBlock.svelte b/src/lib/components/WarningBlock.svelte new file mode 100644 index 0000000..50c9cf5 --- /dev/null +++ b/src/lib/components/WarningBlock.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/src/lib/util/Blogs.ts b/src/lib/util/Blogs.ts new file mode 100644 index 0000000..5878e26 --- /dev/null +++ b/src/lib/util/Blogs.ts @@ -0,0 +1,81 @@ +export const THUMBNAIL_DEFAULT = "https://teasanctuary.ru/common/background-day.webp"; +export const BLOG_POST_FRESHNESS_MILLIS = 3 * 24 * 60 * 60 * 1000; // 3 дня + +export type PostComparer = (a: App.BlogPost, b: App.BlogPost) => number; + +export const sortPostsByPostDate: PostComparer = (a, b) => new Date(b.date!).valueOf() - new Date(a.date!).valueOf(); + +function laterDate(a: string, b?: string): number { + const dateA = new Date(a).valueOf(); + if (!b) return dateA; + + const dateB = new Date(b).valueOf(); + return Math.max(dateA, dateB); +} + +export const sortPostsByPostAndUpdateDate: PostComparer = (a, b) => laterDate(b.date!, b.dateChanged) - laterDate(a.date!, a.dateChanged); + +export async function fetchPostsSorted(postComparer?: PostComparer) { + const allPosts = await fetchPosts(); + + const sortedPosts = allPosts + // Для списка постов оставляем только те, у которых объявлена дата публикации + .filter((a) => !!a.date) + .sort(postComparer ?? sortPostsByPostDate); + + return sortedPosts; +}; + +// rndtrash: пришлось заменить `path.parse`, так как на стороне клиента его больше не реализуют +function getFilenameFromPath(path: string) { + const file = path.split("/").pop(); + const dot = file?.lastIndexOf('.') ?? -1; + return dot === -1 ? file : file?.substring(0, file.lastIndexOf('.')); +} + +export async function fetchPosts() { + const allPostFiles = import.meta.glob('/src/blogs/*.md'); + const iterablePostFiles = Object.entries(allPostFiles); + + const allPosts: App.BlogPost[] = await Promise.all( + iterablePostFiles.map(async ([filePath, resolver]) => { + const { metadata }: any = await resolver(); + const name = getFilenameFromPath(filePath); + + return { + slug: name, + ...metadata + }; + }) + ); + + return allPosts; +}; + +export function resolveBlogPath(slug?: string, src?: string) { + if (!src) return null; + + return src.startsWith('http://') || src.startsWith('https://') + ? src + : `/blog/${slug}/${src}`; +} + +const bptToString: Record = { + 'article': 'Заметка', + 'event': 'Событие', + 'update': 'Обновление' +}; + +const bptToIcon: Record = { + 'article': 'material-symbols:article', + 'event': 'material-symbols:event', + 'update': 'material-symbols:autorenew' +}; + +export function blogPostTypeToString(type: App.BlogPostType): string { + return bptToString[type] ?? bptToString['article']; +} + +export function blogPostTypeToIcon(type: App.BlogPostType): string { + return bptToIcon[type] ?? bptToIcon['article']; +} diff --git a/src/lib/util/LinkResolver.ts b/src/lib/util/LinkResolver.ts index 9d1570e..9ada1da 100644 --- a/src/lib/util/LinkResolver.ts +++ b/src/lib/util/LinkResolver.ts @@ -1,3 +1,4 @@ +// TODO: чашки на иконках выглядят страшненько, пока что пусть лучше будет голая ссылка const icons: Record = { none: 'material-symbols:link', 'steamcommunity.com': 'simple-icons:steam', @@ -6,17 +7,19 @@ const icons: Record = { 'github.com': 'simple-icons:github', 'youtube.com': 'simple-icons:youtube', 'itch.io': 'simple-icons:itchdotio', + 'discord.com': 'simple-icons:discord', 'discord.gg': 'simple-icons:discord', 'gamebanana.com': 'simple-icons:gamebanana', 'bsky.app': 'simple-icons:bluesky', 'bsky.social': 'simple-icons:bluesky', // https://хамяк.рф 'xn--80auf8a2c.xn--p1ai': 'fluent-emoji-high-contrast:hamster', - 'teasanctuary.ru': '/icons/tea-sanctuary-white.svg', + // 'teasanctuary.ru': '/icons/tea-sanctuary-white.svg', 'hl.teasanctuary.ru': '/icons/half-life.svg', 'git.teasanctuary.ru': 'devicon-plain:git', - localhost: '/icons/tea-sanctuary-white.svg', - email: 'material-symbols:alternate-email' + // localhost: '/icons/tea-sanctuary-white.svg', + email: 'material-symbols:alternate-email', + rss: 'material-symbols:rss-feed' }; // Особые случаи, когда одним доменом второго уровня не ограничишься (например, randomtrash.itch.io) @@ -32,6 +35,10 @@ const specialResolvers: Record string> = { if (prefix[0] === "git") { return 'git.teasanctuary.ru'; } + // Особый случай: приглашение в Discord + if (url.pathname.startsWith('/discord')) { + return 'discord.gg'; + } return 'teasanctuary.ru'; }, // Игнорируем имя пользователя @@ -44,24 +51,26 @@ function getIconFromUrl(url: URL): string | undefined { const href = url.href; if (href.startsWith('mailto:')) return 'email'; + if (href.endsWith('/rss.xml') || href.endsWith('/atom.rss') || href.endsWith('.atom')) + return 'rss'; const hostname = url.hostname; const secondLevel = hostname.match(/(([A-Za-z0-9\-])+\.([A-Za-z0-9\-])+)$/)?.at(0) ?? ''; if (specialResolvers[secondLevel]) - return icons[specialResolvers[secondLevel](url)]; + return specialResolvers[secondLevel](url); - return icons[hostname]; + return hostname; } export function tryGetIcon(link: string): string { let url: URL; try { - url = new URL(link); + url = new URL(link, document.baseURI); } catch { return icons['none']; } - return getIconFromUrl(url) ?? icons['none']; + return icons[getIconFromUrl(url) ?? ''] ?? icons['none']; } /** diff --git a/src/pages/index.md b/src/pages/index.md new file mode 100644 index 0000000..e206d50 --- /dev/null +++ b/src/pages/index.md @@ -0,0 +1,39 @@ +# Кто мы? + +__Tea Sanctuary__ — это в первую очередь коллектив друзей, разрабатывающих +проекты для души, для всеобщего пользования и даже на заказ. С +__8 июля 2017 года__ мы ведём публичную деятельность в сфере разработки ПО и развлечений. + +__Tea Sanctuary__ — это также и сообщество единомышленников. Любовь к добротным +видеоиграм и пассивная агрессия к вычислительной технике у нас в крови. Когда-то сообщество +было закрытым и насчитывало около 50 участников, но впоследствии мы решили его расширить. +Станьте частью коллектива! + +# Что делаем? + +Наша главная страсть — это, конечно, видеоигры. Мы часто участвуем в так +называемых "гейм джемах" — конкурсах на разработку игр. Наши игры вы можете +оценить здесь: [RandomTrash](https://randomtrash.itch.io) +[FriendlyWithMeat](https://friendlywithmeat.itch.io). +Также мы ведём работу над нашим первым полноценным игровым проектом. +Следите за новостями в нашем [сообществе](https://teasanctuary.ru/discord)! + +Отдельные участники нашего коллектива занимаются модификацией существующих игр, +добавляя в них новый контент. Например, __MegaZerg__ создаёт оригинальные карты +для такой бессмертной классики, как __Counter-Strike 1.6__ и __Half-Life Deathmatch__, +и выкладывает их на ресурс GameBanana: [kemist](https://gamebanana.com/members/2971042) + +Мы размещаем игровые сервера, как постоянные, так и временные для различных событий. +Например, у нас есть сервер [Tea Sanctuary HLDM](https://hl.teasanctuary.ru), +где вы можете ознакомиться с новыми картами от всего сообщества Half-Life. + +Не одними играми едины, за нашими плечами есть несколько прикладных программ, +созданных под заказ. Про них ничего особо рассказать не можем, но если вам надо +что-нибудь сделать — пишите нам! + +# Как с вами связаться? + +Общие вопросы можно задавать в [сообществе Tea Sanctuary](https://teasanctuary.ru/discord). +Там же можете написать личное сообщение администраторам. + +Наши соцсети и почту для более важных обращений можно найти на странице [Контакты](/contact). diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte index 0f5e7ab..58bd3e5 100644 --- a/src/routes/+error.svelte +++ b/src/routes/+error.svelte @@ -7,7 +7,7 @@
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 4b33d76..fafc4f0 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,6 +1,7 @@ - - - - - Tea Sanctuary + {#if page.data.title !== undefined} + {page.data.title} — Tea Sanctuary + + {:else} + Tea Sanctuary + + {/if} + + -
+
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index fab3d9e..da806ec 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,16 +1,18 @@ Tea Sanctuary -
+
@@ -57,19 +59,12 @@ >
-
+

TEA

SANCTUARY

-
Сообщество @@ -87,69 +82,61 @@
-
-
-
-

Кто мы?

-
- Tea Sanctuary — это в первую очередь коллектив друзей, разрабатывающих проекты - для души, для всеобщего пользования и даже на заказ. С 8 июля 2017 года мы ведём публичную - деятельность в сфере разработки ПО и развлечений. + +
+

ПОСЛЕДНИЕ ПОСТЫ

+ +
+ {#each page.data.posts as post, i} +
+
-
-
- Tea Sanctuary — это также и сообщество единомышленников. Любовь к добротным видеоиграм - и пассивная агрессия к вычислительной технике у нас в крови. Когда-то сообщество было закрытым - и насчитывало около 50 участников, но впоследствии мы решили его расширить. Станьте частью коллектива! -
-
-
-

Что делаем?

-
- Наша главная страсть — это, конечно, видеоигры. Мы часто участвуем в так называемых - "гейм джемах" — конкурсах на разработку игр. Наши игры вы можете оценить здесь: - RandomTrash - FriendlyWithMeat. - Также мы ведём работу над нашим первым полноценным игровым проектом. - Следите за новостями в нашем - сообществе! -
-
-
- Отдельные участники нашего коллектива занимаются модификацией существующих игр, добавляя в - них новый контент. Например, MegaZerg создаёт оригинальные карты для такой - бессмертной классики, как Counter-Strike 1.6, и выкладывает их на ресурс GameBanana: - kemist -
-
-
- Мы размещаем игровые сервера, как постоянные, так и временные для различных событий. - Например, у нас есть сервер - Tea Sanctuary HLDM, где - вы можете ознакомиться с новыми картами от всего сообщества Half-Life. -
-
-
- Не одними играми едины, за нашими плечами есть несколько прикладных программ, созданных под - заказ. Про них ничего особо рассказать не можем, но если вам надо что-нибудь сделать — - пишите нам! -
-
-
-

Как с вами связаться?

-
- TODO: контакты. -
-
+ {/each}
+
+
+ +
+
+ \ No newline at end of file + + section > h2 { + @apply text-xl sm:text-3xl; + } + diff --git a/src/routes/+page.ts b/src/routes/+page.ts new file mode 100644 index 0000000..1c87cc4 --- /dev/null +++ b/src/routes/+page.ts @@ -0,0 +1,17 @@ +import { fetchPostsSorted, sortPostsByPostAndUpdateDate } from "$src/lib/util/Blogs"; + +const LATEST_POSTS_COUNT = 3; + +export async function load() { + let md: any + try { + md = await import("$src/pages/index.md"); + } catch (ex) { + throw "Не удалось найти текст для главной страницы"; + } + console.log("TEST", md.default); + return { + content: md.default, + posts: (await fetchPostsSorted(sortPostsByPostAndUpdateDate)).slice(0, LATEST_POSTS_COUNT) + }; +} \ No newline at end of file diff --git a/src/routes/blog/+page.server.ts b/src/routes/blog/+page.server.ts new file mode 100644 index 0000000..c5653b5 --- /dev/null +++ b/src/routes/blog/+page.server.ts @@ -0,0 +1,5 @@ +import { fetchPostsSorted } from "$src/lib/util/Blogs"; + +export async function load() { + return { title: "Блог", description: "Новости и заметки проектов Tea Sanctuary", posts: await fetchPostsSorted() }; +} \ No newline at end of file diff --git a/src/routes/blog/+page.svelte b/src/routes/blog/+page.svelte index f61e340..870fcc9 100644 --- a/src/routes/blog/+page.svelte +++ b/src/routes/blog/+page.svelte @@ -1,14 +1,28 @@ - - Блог — Tea Sanctuary - - -
+
@@ -20,6 +34,26 @@
+ + Подпишитесь на нашу RSS ленту, чтобы не + пропускать новые посты! + + +
+ {#each groupedPosts.entries() as [monthYear, postsInMonthYear]} +

+ {monthYear} +

+
+ {#each postsInMonthYear as post, i} + + {/each} +
+ {/each} +
+ \ No newline at end of file + @import '$src/app.css'; + diff --git a/src/routes/blog/[slug]/+page.svelte b/src/routes/blog/[slug]/+page.svelte new file mode 100644 index 0000000..8c64572 --- /dev/null +++ b/src/routes/blog/[slug]/+page.svelte @@ -0,0 +1,100 @@ + + + + + + + + +
+
+
+

{data.blogPost.title}

+ {#if data.blogPost.description} +

{data.blogPost.description}

+ {/if} +
+
+
+ +
+ + {#if data.blogPost.dateChanged} + + {/if} +
+ + + {blogPostTypeToString(type)} + +
+ {#each authors as author} + + + + + {author} + + + {/each} +
+ +{#if page.data.blogPost.projects?.length > 0} + +

В данной заметке упоминаются наши проекты:

+
    + {#each page.data.blogPost.projects as project} +
  • {project}
  • + {/each} +
+
+{/if} + +
+ +
+ + diff --git a/src/routes/blog/[slug]/+page.ts b/src/routes/blog/[slug]/+page.ts new file mode 100644 index 0000000..46b041c --- /dev/null +++ b/src/routes/blog/[slug]/+page.ts @@ -0,0 +1,23 @@ +import { resolveBlogPath, THUMBNAIL_DEFAULT } from "$src/lib/util/Blogs.js"; +import { error } from "@sveltejs/kit"; + +export async function load({ params }) { + let post: any + try { + post = await import(`$src/blogs/${params.slug}.md`); + } catch (ex) { + error(404); + } + const blogPost: App.BlogPost = post.metadata; + const thumbnail = resolveBlogPath(params.slug, blogPost.thumbnail ?? THUMBNAIL_DEFAULT); + + return { + title: `${blogPost.title} — Блог`, + description: blogPost.description, + thumbnail: thumbnail, + content: post.default, + blogPost: { + ...blogPost + } + }; +}; diff --git a/src/routes/blog/rss.xml/+server.ts b/src/routes/blog/rss.xml/+server.ts new file mode 100644 index 0000000..59429cf --- /dev/null +++ b/src/routes/blog/rss.xml/+server.ts @@ -0,0 +1,66 @@ +import { fetchPostsSorted, resolveBlogPath } from "$src/lib/util/Blogs"; + +export const prerender = true; + +const feedUpdated = new Date(); + +function escapeXml(unsafe: string): string { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function makeThumbnail(post: App.BlogPost): string { + if (!post.thumbnail) return ''; + + const alt = !!post.thumbnailAlt ? ` alt="${escapeXml(post.thumbnailAlt)}"` : ''; + + return `

`; +} + +function makeAuthors(post: App.BlogPost): string { + const authors = + (post.authors == null + ? [] + : typeof post.authors === 'string' + ? [post.authors] + : post.authors) + .map(a => escapeXml(a)); + + if (authors.length === 0) return ''; + + let authorsString = authors[0]; + if (authors.length > 1) { + const lastAuthor = authors.pop(); + authorsString = `${authors.join(', ')} и ${lastAuthor}`; + } + + return `\n${authorsString}`; +} + +export async function GET({ setHeaders }) { + setHeaders({ + 'Cache-Control': 'max-age=0, s-maxage=3600', + 'Content-Type': 'application/rss+xml', + }); + const posts = await fetchPostsSorted(); + return new Response(String(` + + +Блог Tea Sanctuary +https://teasanctuary.ru/blog +1800 +${feedUpdated.toUTCString()} +${posts.map((post) => ` +${escapeXml(post.title)} +${makeAuthors(post)} +https://teasanctuary.ru/blog/${post.slug} +https://teasanctuary.ru/blog/${post.slug} +${(new Date(post.date!)).toUTCString()} +`).join("\n")} + +`)) +} \ No newline at end of file diff --git a/src/routes/contact/+page.svelte b/src/routes/contact/+page.svelte index 9be477d..e1aa212 100644 --- a/src/routes/contact/+page.svelte +++ b/src/routes/contact/+page.svelte @@ -1,14 +1,9 @@ - - Контакты — Tea Sanctuary - - -
+
@@ -20,6 +15,35 @@
+Страница находится в разработке! + +
+
+
+ На данный момент вы можете связаться с администрацией сайта и участниками команды через + + нашу гильдию в Discord + : +
+ +
+ Вы также можете ознакомиться с социальными сетями каждого отдельного участника команды + на странице Команда. +
+
+
+ \ No newline at end of file + @import '$src/app.css'; + diff --git a/src/routes/contact/+page.ts b/src/routes/contact/+page.ts new file mode 100644 index 0000000..2d33459 --- /dev/null +++ b/src/routes/contact/+page.ts @@ -0,0 +1,3 @@ +export async function load() { + return { title: "Контакты" }; +} \ No newline at end of file diff --git a/src/routes/projects/+page.svelte b/src/routes/projects/+page.svelte index 692a7cc..fa92579 100644 --- a/src/routes/projects/+page.svelte +++ b/src/routes/projects/+page.svelte @@ -1,14 +1,8 @@ - - Проекты — Tea Sanctuary - - -
+
@@ -20,6 +14,8 @@
+Страница находится в разработке! + \ No newline at end of file + @import '$src/app.css'; + diff --git a/src/routes/projects/+page.ts b/src/routes/projects/+page.ts new file mode 100644 index 0000000..efd7981 --- /dev/null +++ b/src/routes/projects/+page.ts @@ -0,0 +1,3 @@ +export async function load() { + return { title: "Проекты" }; +} \ No newline at end of file diff --git a/src/routes/team/+page.svelte b/src/routes/team/+page.svelte index 94b4472..155b80b 100644 --- a/src/routes/team/+page.svelte +++ b/src/routes/team/+page.svelte @@ -1,14 +1,8 @@ - - Команда — Tea Sanctuary - - -
+
@@ -20,6 +14,8 @@
+Страница находится в разработке! + \ No newline at end of file + @import '$src/app.css'; + diff --git a/src/routes/team/+page.ts b/src/routes/team/+page.ts new file mode 100644 index 0000000..d0b3bdc --- /dev/null +++ b/src/routes/team/+page.ts @@ -0,0 +1,3 @@ +export async function load() { + return { title: "Команда" }; +} \ No newline at end of file diff --git a/static/blog/hello_world/wave.png b/static/blog/hello_world/wave.png new file mode 100644 index 0000000..5eac6a0 Binary files /dev/null and b/static/blog/hello_world/wave.png differ diff --git a/static/blog/test_unpublished/wasd_perelesoq_game_jam_2025.png b/static/blog/test_unpublished/wasd_perelesoq_game_jam_2025.png new file mode 100644 index 0000000..dd59dc2 Binary files /dev/null and b/static/blog/test_unpublished/wasd_perelesoq_game_jam_2025.png differ diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..6d27e04 Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/robots.txt b/static/robots.txt index d49c9b4..fbbfa75 100644 --- a/static/robots.txt +++ b/static/robots.txt @@ -6,6 +6,8 @@ User-agent: AhrefsBot Disallow: / User-agent: MJ12bot Disallow: / +User-agent: GPTBot +Disallow: / User-agent: * Allow: / diff --git a/svelte.config.js b/svelte.config.js index ebf0dc8..2ea3e94 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,8 +1,13 @@ import adapter from '@sveltejs/adapter-static'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { mdsvex } from 'mdsvex'; -import mdsvexConfig from './mdsvex.config.js'; import autoImport from 'sveltekit-autoimport'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +// https://flaviocopes.com/fix-dirname-not-defined-es-module-scope/ +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); /** @type {import('@sveltejs/kit').Config} */ const config = { @@ -33,8 +38,11 @@ const config = { }, extensions: ['.svelte', '.md'], preprocess: [ - vitePreprocess(), - mdsvex(mdsvexConfig), + vitePreprocess({ script: true }), + mdsvex({ + extensions: ['.md'], + layout: join(__dirname, "./src/lib/components/MdsvexLayout.svelte") + }), autoImport({ include: ['**/*.(svelte|md)'], components: ['./src/lib/components/', { name: './src' }] diff --git a/tailwind.config.ts b/tailwind.config.ts index 93dc09d..1954591 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,45 +1,12 @@ -import plugin from 'tailwindcss/plugin'; - /** @type {import('tailwindcss').Config} */ export default { content: ['./src/**/*.{html,js,svelte,ts}'], theme: { - // colors: { - // white: '#FFFFFF', - // transparentblue: '#2447f779', - // blue: '#2446f7', - // black: '#000000', - // transparentblack1: '#000000BB', - // transparentblack0: '#00000011', - // darkblue: '#091856', - // navyblue: '#0f2898', - // gray: '#e2e2e2', - // lightblue: '#0092ff', - // transparent: 'transparent' - // }, extend: { fontFamily: { sans: ['Lineyka', 'sans-serif'], disket: ['Disket Mono', 'monospace'], }, - // height: { - // // 64 px for navbar - // screen: 'calc(100vh - 64px)' - // }, - // dropShadow: { - // md: '0px 0px 2px #091856', - // hover: '0px 6px 2px #091856' - // }, - // backgroundImage: { - // pixel: "url('/common/pixel-overlay.png')", - // 'pixel-dark': "url('/common/pixel-overlay-dark.png')", - // 'pixel-white': "url('/common/pixel-overlay-white.png')", - // 'pixel-large': "url('/common/pixel-overlay-large.png')" - // }, - // backgroundSize: { - // pixel: '7px', - // 'pixel-lg': '14px' - // }, typography: { DEFAULT: { css: {