Блоги #1
43 changed files with 1175 additions and 268 deletions
|
|
@ -1,3 +0,0 @@
|
|||
export default {
|
||||
extensions: ['.md']
|
||||
};
|
||||
227
package-lock.json
generated
227
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
14
src/app.css
14
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);
|
||||
|
||||
h1 {
|
||||
@apply font-disket text-4xl font-bold sm:text-6xl md:text-8xl;
|
||||
}
|
||||
|
||||
.hero-background {
|
||||
@apply bg-[url('/common/background-day.webp')] dark:bg-[url('/common/background-night.webp')] bg-cover bg-fixed;
|
||||
h2 {
|
||||
@apply font-sans text-xl font-bold sm:text-2xl md:text-4xl;
|
||||
}
|
||||
}
|
||||
16
src/app.d.ts
vendored
16
src/app.d.ts
vendored
|
|
@ -1,5 +1,3 @@
|
|||
import type { Member } from '$lib/types/Member';
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
|
|
@ -20,15 +18,23 @@ declare global {
|
|||
|
||||
type MdsvexResolver = () => Promise<MdsvexFile>;
|
||||
|
||||
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[];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://teasanctuary.ru" />
|
||||
<meta name="theme-color" content="#63A002" />
|
||||
<meta name="theme-color" content="#319668" />
|
||||
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%sveltekit.assets%/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="%sveltekit.assets%/favicon-16x16.png">
|
||||
|
|
|
|||
166
src/blogs/hello_world.md
Normal file
166
src/blogs/hello_world.md
Normal file
|
|
@ -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-элемента можно поставить ссылку на свой собственный класс. Таким образом,
|
||||
каждый `<a href="...">...</a>` превращается в `<SocialHyperlink href="...">...</SocialHyperlink>`, без лишних телодвижений писателя блога.
|
||||
|
||||
Отлично, у нас есть отдельные блоги, теперь хочется собрать их все вместе, и желательно рассортировать по дате. И вот тут всплывает вторая проблема:
|
||||
система 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
|
||||
13
src/blogs/test_unpublished.md
Normal file
13
src/blogs/test_unpublished.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
title: 'Тестовый блог'
|
||||
date:
|
||||
dateChanged: '2025-09-29'
|
||||
description: 'Немного о самом сайте'
|
||||
projects: ['ts-hldm']
|
||||
---
|
||||
|
||||
# ПРИВЕТ !
|
||||
|
||||
Добро пожаловать на наш первый блог-пост!
|
||||
|
||||
<Img src="./wasd_perelesoq_game_jam_2025.png" />
|
||||
158
src/lib/components/BlogCard.svelte
Normal file
158
src/lib/components/BlogCard.svelte
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
<script lang="ts" module>
|
||||
export enum BlogCardSize {
|
||||
Short,
|
||||
Long,
|
||||
Both
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
BLOG_POST_FRESHNESS_MILLIS,
|
||||
blogPostTypeToIcon,
|
||||
blogPostTypeToString
|
||||
} from '$lib/util/Blogs';
|
||||
import DateWidget from '$lib/components/DateWidget.svelte';
|
||||
import Icon from '@iconify/svelte';
|
||||
|
||||
export let post: App.BlogPost;
|
||||
export let size: BlogCardSize = BlogCardSize.Both;
|
||||
export let fullHeight = false;
|
||||
|
||||
const type: App.BlogPostType = post.type ?? 'article';
|
||||
const dateNow = new Date().valueOf();
|
||||
const isPostNew = dateNow - new Date(post.date!).valueOf() <= BLOG_POST_FRESHNESS_MILLIS;
|
||||
const isPostUpdated =
|
||||
post.dateChanged != null &&
|
||||
dateNow - new Date(post.dateChanged).valueOf() <= BLOG_POST_FRESHNESS_MILLIS;
|
||||
const isPostFresh = isPostNew || isPostUpdated;
|
||||
// TODO: rndtrash: события и их актуальность
|
||||
|
||||
/**
|
||||
* rndtrash: пришлось дублировать классы с модификатором и без, потому что Tailwind просто не понимает,
|
||||
* когда его классы склеивают из нескольких частей
|
||||
*/
|
||||
|
||||
/**
|
||||
* Возвращает список классов для полноразмерной плашки.
|
||||
*
|
||||
* Возвращает пустую строку, если плашка может быть только короткой
|
||||
* @param classes
|
||||
* @param modClasses
|
||||
*/
|
||||
function fullClass(classes: string, modClasses: string): string {
|
||||
if (size === BlogCardSize.Short) return '';
|
||||
return size === BlogCardSize.Both ? modClasses : classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает список классов для короткой карточки.
|
||||
*
|
||||
* Возвращает пустую строку, если плашка может быть только полноразмерной
|
||||
* @param classes
|
||||
* @param modClasses
|
||||
*/
|
||||
function shortClass(classes: string, modClasses?: string): string {
|
||||
if (size === BlogCardSize.Long) return '';
|
||||
return size === BlogCardSize.Both && modClasses ? modClasses : classes;
|
||||
}
|
||||
</script>
|
||||
|
||||
<a
|
||||
href="/blog/{post.slug}"
|
||||
class="blog-card
|
||||
{fullHeight ? 'min-h-full' : ''}
|
||||
{isPostUpdated ? 'updated' : isPostNew ? 'new' : ''}
|
||||
{shortClass('flex-col justify-baseline')}
|
||||
{fullClass('flex-row justify-stretch', 'sm:flex-row sm:justify-stretch')}"
|
||||
>
|
||||
<div
|
||||
class="relative w-full basis-auto overflow-hidden
|
||||
{shortClass('h-32 md:h-48')}
|
||||
{fullClass('h-auto basis-1/3', 'sm:h-auto sm:basis-1/3')}"
|
||||
>
|
||||
{#if post.thumbnail}
|
||||
<img
|
||||
class="thumbnail"
|
||||
src={`/blog/${post.slug}/${post.thumbnail}`}
|
||||
alt={post.thumbnailAlt ?? 'Миниатюра поста'}
|
||||
/>
|
||||
{/if}
|
||||
{#if isPostFresh}
|
||||
<div class="toast {fullClass('hidden', 'sm:hidden')}">
|
||||
{#if isPostUpdated}
|
||||
ОБНОВЛЕНО
|
||||
{:else}
|
||||
НОВОЕ
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex w-full flex-col justify-center p-4 break-words">
|
||||
<div class="flex flex-row flex-wrap justify-start gap-4 pb-2">
|
||||
<DateWidget
|
||||
class={post.dateChanged ? shortClass('hidden', 'not-sm:hidden') : ''}
|
||||
dateString={post.date}
|
||||
type="published"
|
||||
highlight={isPostNew && !isPostUpdated}
|
||||
/>
|
||||
{#if post.dateChanged}
|
||||
<DateWidget
|
||||
dateString={post.dateChanged}
|
||||
type="updated"
|
||||
highlight={isPostUpdated}
|
||||
/>
|
||||
{/if}
|
||||
<div class="flex items-center gap-2 p-1 text-lg font-bold">
|
||||
<Icon icon={blogPostTypeToIcon(type)} width={28} height={28} />
|
||||
<span class={shortClass('hidden', 'not-md:hidden')}>
|
||||
{blogPostTypeToString(type)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="text-3xl font-bold">{post.title}</h2>
|
||||
|
||||
{#if post.description}
|
||||
<p>{post.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<style>
|
||||
@import '$src/app.css';
|
||||
|
||||
.blog-card {
|
||||
@apply flex w-full max-w-5xl overflow-hidden rounded-lg bg-slate-100 text-slate-950 drop-shadow-xl transition-all hover:drop-shadow-2xl;
|
||||
|
||||
.toast {
|
||||
@apply absolute top-0 right-0 rounded-bl-lg p-2 font-bold text-slate-50;
|
||||
}
|
||||
|
||||
&.updated {
|
||||
box-shadow: 0 0 0 4px var(--color-purple-600);
|
||||
|
||||
.toast {
|
||||
@apply bg-purple-600;
|
||||
}
|
||||
}
|
||||
|
||||
&.new {
|
||||
box-shadow: 0 0 0 4px var(--color-amber-600);
|
||||
|
||||
.toast {
|
||||
@apply bg-amber-600;
|
||||
}
|
||||
}
|
||||
|
||||
img.thumbnail {
|
||||
@apply absolute h-full w-full object-cover transition-transform;
|
||||
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
&:hover img.thumbnail {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
32
src/lib/components/DateWidget.svelte
Normal file
32
src/lib/components/DateWidget.svelte
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<script lang="ts">
|
||||
import Icon from '@iconify/svelte';
|
||||
|
||||
let className: string = '';
|
||||
export { className as class };
|
||||
|
||||
export let dateString: string | undefined;
|
||||
export let type: 'published' | 'updated' = 'published';
|
||||
export let highlight = false;
|
||||
|
||||
const icon =
|
||||
type == 'published' ? 'material-symbols:calendar-today' : 'material-symbols:update';
|
||||
|
||||
const highlightClasses = (classes: string) => (highlight ? classes : '');
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex flex-nowrap items-center gap-2 p-1 text-lg font-bold {className} rounded-lg
|
||||
{highlightClasses(type == 'published' ? 'bg-amber-600' : 'bg-purple-600')}
|
||||
{highlightClasses('text-slate-50')}"
|
||||
>
|
||||
<Icon {icon} width={28} height={28} />
|
||||
<span class="text-nowrap">
|
||||
{dateString
|
||||
? new Date(dateString).toLocaleString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})
|
||||
: 'Не опубликован!'}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -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');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="{className} {black
|
||||
? 'fill-slate-950'
|
||||
: 'fill-slate-50'} text-sm uppercase transition-all"
|
||||
<span
|
||||
class="{className} {black ? 'fill-slate-950' : 'fill-slate-50'} hover-icon"
|
||||
style:width="{size}px"
|
||||
style:height="{size}px"
|
||||
>
|
||||
{#if src}
|
||||
{#if isUrl}
|
||||
<img {src} alt={text} width={size} height={size} />
|
||||
{#if isUrl(src)}
|
||||
<img {src} {alt} width={size} height={size} />
|
||||
{:else}
|
||||
<Icon width={size} height={size} icon={src} color={black ? '#020618' : '#f8fafc'} />
|
||||
{/if}
|
||||
{:else}
|
||||
{text}
|
||||
{alt ?? 'Без иконки'}
|
||||
{/if}
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<style>
|
||||
@import '$src/app.css';
|
||||
|
||||
.hover-icon {
|
||||
@apply block text-sm uppercase transition-all;
|
||||
|
||||
img {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
18
src/lib/components/IconBlock.svelte
Normal file
18
src/lib/components/IconBlock.svelte
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import Icon from '@iconify/svelte';
|
||||
|
||||
export let bgStrong: string;
|
||||
export let bgBleak: string;
|
||||
export let icon: string;
|
||||
export let caption: string;
|
||||
</script>
|
||||
|
||||
<section class="flex flex-col sm:flex-row">
|
||||
<div class="flex flex-row items-center gap-2 {bgStrong} p-2 text-slate-50">
|
||||
<Icon width={32} height={32} {icon} color={'#f8fafc'} />
|
||||
<span class="sm:hidden">{caption}</span>
|
||||
</div>
|
||||
<div class="{bgBleak} p-4 sm:grow">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
20
src/lib/components/Img.svelte
Normal file
20
src/lib/components/Img.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { resolveBlogPath } from '$lib/util/Blogs';
|
||||
|
||||
export let src: string;
|
||||
export let caption: string;
|
||||
export let alt: string = '';
|
||||
</script>
|
||||
|
||||
<div class="text-center">
|
||||
<img
|
||||
class="mx-auto mb-0 flex justify-center rounded-lg"
|
||||
class:caption-img={caption}
|
||||
src={resolveBlogPath(page.params.slug, src)}
|
||||
alt={alt ? alt : src}
|
||||
/>
|
||||
{#if caption}
|
||||
<p style="margin-top: 0px;">{caption}</p>
|
||||
{/if}
|
||||
</div>
|
||||
12
src/lib/components/InfoBlock.svelte
Normal file
12
src/lib/components/InfoBlock.svelte
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<script lang="ts">
|
||||
import IconBlock from './IconBlock.svelte';
|
||||
</script>
|
||||
|
||||
<IconBlock
|
||||
bgStrong="bg-blue-500"
|
||||
bgBleak="bg-blue-50"
|
||||
icon="material-symbols:info"
|
||||
caption="Обратите внимание"
|
||||
>
|
||||
<slot />
|
||||
</IconBlock>
|
||||
11
src/lib/components/MdsvexLayout.svelte
Normal file
11
src/lib/components/MdsvexLayout.svelte
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts" module>
|
||||
import SocialHyperlink from './SocialHyperlink.svelte';
|
||||
|
||||
export { SocialHyperlink as a };
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const { children, ...rest } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
<HoverIcon
|
||||
src={route.icon}
|
||||
class="text-sm uppercase"
|
||||
text={route.href}
|
||||
alt={route.label}
|
||||
size={32}
|
||||
black={!isActive(route.href)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
target={isLinkLocal(href) ? '_self' : '_blank'}
|
||||
>
|
||||
<div class="shrink-0 rounded-l-xl bg-slate-800 p-2">
|
||||
<HoverIcon src={customIcon ?? tryGetIcon(href)} class="text-sm uppercase" text={href} />
|
||||
<HoverIcon src={customIcon ?? tryGetIcon(href)} class="text-sm uppercase" />
|
||||
</div>
|
||||
<div
|
||||
class="flex shrink-0 grow flex-nowrap items-center justify-center rounded-r-xl bg-slate-100 p-2 text-2xl text-nowrap text-slate-950"
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@
|
|||
const sm = new MediaQuery('width >= 40rem', false);
|
||||
</script>
|
||||
|
||||
<a {href} class="{className} group inline-block" target={isLinkLocal(href) ? '_self' : '_blank'}>
|
||||
<a {href} class="{className} group inline-block no-underline" target={isLinkLocal(href) ? '_self' : '_blank'}>
|
||||
<span
|
||||
class="inline-block size-6 rounded-sm bg-emerald-800 p-0.5 align-bottom transition-all group-hover:scale-110 sm:size-8 sm:rounded-xl sm:p-1"
|
||||
>
|
||||
<HoverIcon src={customIcon ?? tryGetIcon(href)} text={href} size={sm.current ? 24 : 20} />
|
||||
<HoverIcon src={customIcon ?? tryGetIcon(href)} size={sm.current ? 24 : 20} />
|
||||
</span>
|
||||
<span class="text-emerald-900 underline">
|
||||
<slot />
|
||||
|
|
|
|||
12
src/lib/components/WarningBlock.svelte
Normal file
12
src/lib/components/WarningBlock.svelte
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<script lang="ts">
|
||||
import IconBlock from './IconBlock.svelte';
|
||||
</script>
|
||||
|
||||
<IconBlock
|
||||
bgStrong="bg-yellow-500"
|
||||
bgBleak="bg-yellow-50"
|
||||
icon="material-symbols:warning"
|
||||
caption="Внимание"
|
||||
>
|
||||
<slot />
|
||||
</IconBlock>
|
||||
81
src/lib/util/Blogs.ts
Normal file
81
src/lib/util/Blogs.ts
Normal file
|
|
@ -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<App.BlogPostType, string> = {
|
||||
'article': 'Заметка',
|
||||
'event': 'Событие',
|
||||
'update': 'Обновление'
|
||||
};
|
||||
|
||||
const bptToIcon: Record<App.BlogPostType, string> = {
|
||||
'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'];
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
// TODO: чашки на иконках выглядят страшненько, пока что пусть лучше будет голая ссылка
|
||||
const icons: Record<string, string> = {
|
||||
none: 'material-symbols:link',
|
||||
'steamcommunity.com': 'simple-icons:steam',
|
||||
|
|
@ -6,17 +7,19 @@ const icons: Record<string, string> = {
|
|||
'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, (url: URL) => 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'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
39
src/pages/index.md
Normal file
39
src/pages/index.md
Normal file
|
|
@ -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).
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
</svelte:head>
|
||||
|
||||
<section
|
||||
class="hero-background flex h-screen shrink-0 flex-col items-center justify-center gap-12 overflow-hidden p-4"
|
||||
class="hero flex h-screen shrink-0 flex-col items-center justify-center gap-12 overflow-hidden p-4"
|
||||
>
|
||||
<div class="flex flex-nowrap flex-col gap-5">
|
||||
<div class="flex flex-nowrap flex-col gap-3 items-center justify-center lg:flex-row lg:flex-nowrap">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import '$src/app.css';
|
||||
// import '../syntax-highlight.css'; // https://github.com/PrismJS/prism-themes
|
||||
import { page } from '$app/state';
|
||||
import '$src/syntax-highlight.css'; // https://github.com/PrismJS/prism-themes
|
||||
import NavBar from '$lib/components/NavBar.svelte';
|
||||
|
||||
const routes: App.Route[] = [
|
||||
|
|
@ -13,14 +14,21 @@
|
|||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<meta property="og:title" content="Tea Sanctuary" />
|
||||
<meta property="og:image" content="https://teasanctuary.ru/common/logo.png" />
|
||||
<meta property="og:description" content="Делаем вещи как можем." />
|
||||
|
||||
{#if page.data.title !== undefined}
|
||||
<title>{page.data.title} — Tea Sanctuary</title>
|
||||
<meta property="og:title" content="{page.data.title} — Tea Sanctuary" />
|
||||
{:else}
|
||||
<title>Tea Sanctuary</title>
|
||||
<meta property="og:title" content="Tea Sanctuary" />
|
||||
{/if}
|
||||
<meta
|
||||
property="og:image"
|
||||
content={page.data.thumbnail ?? 'https://teasanctuary.ru/common/logo.png'}
|
||||
/>
|
||||
<meta property="og:description" content={page.data.description ?? 'Делаем вещи как можем.'} />
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex h-screen w-screen flex-row portrait:flex-col-reverse">
|
||||
<div class="flex h-dvh w-dvw flex-row portrait:flex-col-reverse">
|
||||
<NavBar {routes} />
|
||||
<div class="flex grow-1 flex-col overflow-auto">
|
||||
<div class="relative grow-1">
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
<script lang="ts">
|
||||
import SocialButton from '$lib/components/SocialButton.svelte';
|
||||
import SocialHyperlink from '$lib/components/SocialHyperlink.svelte';
|
||||
import { PUBLIC_TS_DISCORD } from '$env/static/public';
|
||||
import BlogCard, { BlogCardSize } from '$src/lib/components/BlogCard.svelte';
|
||||
import { page } from '$app/state';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Tea Sanctuary</title>
|
||||
</svelte:head>
|
||||
|
||||
<section
|
||||
class="flex shrink-0 flex-col items-center justify-center gap-5 overflow-hidden p-4 hero-background"
|
||||
>
|
||||
<section class="hero flex shrink-0 flex-col items-center justify-center gap-5 overflow-hidden p-4">
|
||||
<div
|
||||
class="flex flex-col flex-nowrap items-center justify-center gap-3 lg:flex-row lg:flex-nowrap"
|
||||
>
|
||||
|
|
@ -57,19 +59,12 @@
|
|||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="font-disket text-center text-4xl font-bold text-slate-50 [text-shadow:_0_0_15px_rgba(0,0,0,0.25)] sm:text-6xl md:text-8xl lg:text-left"
|
||||
>
|
||||
<div class="text-center font-bold lg:text-left">
|
||||
<h1>TEA</h1>
|
||||
<h1>SANCTUARY</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-start gap-4">
|
||||
<!--
|
||||
<div class="flex flex-row gap-2 rounded-2xl bg-amber-950 p-2 text-center text-amber-50 ring-2 shadow">
|
||||
Сайт находится в разработке. Если эта плашка висит после 1.06.2025 - пинайте Ивана!
|
||||
</div>
|
||||
-->
|
||||
<div class="flex flex-row flex-wrap items-start justify-center gap-4">
|
||||
<SocialButton class="w-60 shrink-0" href={PUBLIC_TS_DISCORD}>Сообщество</SocialButton>
|
||||
<SocialButton class="w-60 shrink-0" href="https://github.com/TeaSanctuary/">
|
||||
|
|
@ -87,69 +82,61 @@
|
|||
</SocialButton>
|
||||
</div>
|
||||
</section>
|
||||
<section class="flex justify-center bg-slate-50 text-slate-950">
|
||||
<div
|
||||
class="flex w-5xl max-w-screen flex-col flex-nowrap gap-12 p-2 px-2 pt-12 pb-12 text-base sm:text-xl"
|
||||
|
||||
<section class="flex flex-col items-stretch bg-blue-900 pt-4 text-slate-50">
|
||||
<h1 class="text-center">ПОСЛЕДНИЕ ПОСТЫ</h1>
|
||||
|
||||
<div class="flex flex-row items-stretch justify-evenly gap-4 overflow-x-auto p-4">
|
||||
{#each page.data.posts as post, i}
|
||||
<div class="aspect-3/2 w-80 shrink-0">
|
||||
<BlogCard {post} size={BlogCardSize.Short} fullHeight />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<article
|
||||
class="prose
|
||||
sm:prose-xl
|
||||
prose-slate
|
||||
prose-code:break-words
|
||||
prose-pre:drop-shadow-md
|
||||
prose-headings:font-disket
|
||||
prose-headings:my-4
|
||||
prose-headings:font-bold
|
||||
prose-headings:text-slate-950
|
||||
prose-h1:text-2xl
|
||||
prose-h1:sm:text-4xl
|
||||
prose-h2:text-xl
|
||||
prose-h2:sm:text-3xl
|
||||
prose-p:text-justify
|
||||
prose-p:mt-0
|
||||
prose-p:mb-8
|
||||
bg-slate-50
|
||||
pt-8
|
||||
pb-4
|
||||
text-base
|
||||
text-slate-950
|
||||
px-2 sm:px-4 sm:text-xl"
|
||||
>
|
||||
<section id="who-are-we">
|
||||
<h1>Кто мы?</h1>
|
||||
<div class="text-justify">
|
||||
<b>Tea Sanctuary</b> — это в первую очередь коллектив друзей, разрабатывающих проекты
|
||||
для души, для всеобщего пользования и даже на заказ. С <b>8 июля 2017 года</b> мы ведём публичную
|
||||
деятельность в сфере разработки ПО и развлечений.
|
||||
</div>
|
||||
<br />
|
||||
<div class="text-justify">
|
||||
<b>Tea Sanctuary</b> — это также и сообщество единомышленников. Любовь к добротным видеоиграм
|
||||
и пассивная агрессия к вычислительной технике у нас в крови. Когда-то сообщество было закрытым
|
||||
и насчитывало около 50 участников, но впоследствии мы решили его расширить. Станьте частью коллектива!
|
||||
</div>
|
||||
</section>
|
||||
<section id="what-are-we-doing">
|
||||
<h1>Что делаем?</h1>
|
||||
<div class="text-justify">
|
||||
Наша главная страсть — это, конечно, видеоигры. Мы часто участвуем в так называемых
|
||||
"гейм джемах" — конкурсах на разработку игр. Наши игры вы можете оценить здесь:
|
||||
<SocialHyperlink href="https://randomtrash.itch.io">RandomTrash</SocialHyperlink>
|
||||
<SocialHyperlink href="https://friendlywithmeat.itch.io/">FriendlyWithMeat</SocialHyperlink>.
|
||||
Также мы ведём работу над нашим первым полноценным игровым проектом.
|
||||
Следите за новостями в нашем
|
||||
<SocialHyperlink href={PUBLIC_TS_DISCORD}>сообществе</SocialHyperlink>!
|
||||
</div>
|
||||
<br />
|
||||
<div class="text-justify">
|
||||
Отдельные участники нашего коллектива занимаются модификацией существующих игр, добавляя в
|
||||
них новый контент. Например, <b>MegaZerg</b> создаёт оригинальные карты для такой
|
||||
бессмертной классики, как Counter-Strike 1.6, и выкладывает их на ресурс GameBanana:
|
||||
<SocialHyperlink href="https://gamebanana.com/members/2971042">kemist</SocialHyperlink>
|
||||
</div>
|
||||
<br />
|
||||
<div class="text-justify">
|
||||
Мы размещаем игровые сервера, как постоянные, так и временные для различных событий.
|
||||
Например, у нас есть сервер
|
||||
<SocialHyperlink href="https://hl.teasanctuary.ru">Tea Sanctuary HLDM</SocialHyperlink>, где
|
||||
вы можете ознакомиться с новыми картами от всего сообщества Half-Life.
|
||||
</div>
|
||||
<br />
|
||||
<div class="text-justify">
|
||||
Не одними играми едины, за нашими плечами есть несколько прикладных программ, созданных под
|
||||
заказ. Про них ничего особо рассказать не можем, но если вам надо что-нибудь сделать —
|
||||
пишите нам!
|
||||
</div>
|
||||
</section>
|
||||
<section id="how-can-you-contact-us">
|
||||
<h1>Как с вами связаться?</h1>
|
||||
<div class="text-justify">
|
||||
TODO: <SocialHyperlink href="/contact">контакты</SocialHyperlink>.
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<section class="flex max-w-5xl flex-col flex-nowrap mx-auto">
|
||||
<svelte:component this={data.content} />
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
@import "$src/app.css";
|
||||
@import '$src/app.css';
|
||||
|
||||
section > h1,
|
||||
section > h2 {
|
||||
@apply font-disket mb-4 font-bold;
|
||||
}
|
||||
|
||||
section > h1 {
|
||||
@apply font-disket mb-4 text-2xl font-bold sm:text-4xl;
|
||||
@apply text-2xl sm:text-4xl;
|
||||
}
|
||||
|
||||
section > h2 {
|
||||
@apply text-xl sm:text-3xl;
|
||||
}
|
||||
</style>
|
||||
17
src/routes/+page.ts
Normal file
17
src/routes/+page.ts
Normal file
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
5
src/routes/blog/+page.server.ts
Normal file
5
src/routes/blog/+page.server.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { fetchPostsSorted } from "$src/lib/util/Blogs";
|
||||
|
||||
export async function load() {
|
||||
return { title: "Блог", description: "Новости и заметки проектов Tea Sanctuary", posts: await fetchPostsSorted() };
|
||||
}
|
||||
|
|
@ -1,14 +1,28 @@
|
|||
<script lang="ts">
|
||||
import SocialButton from '$lib/components/SocialButton.svelte';
|
||||
import { page } from '$app/state';
|
||||
import SocialHyperlink from '$src/lib/components/SocialHyperlink.svelte';
|
||||
import InfoBlock from '$src/lib/components/InfoBlock.svelte';
|
||||
import BlogCard from '$src/lib/components/BlogCard.svelte';
|
||||
|
||||
function groupPostsByMonthYear(posts: App.BlogPost[]) {
|
||||
const groupedPosts = new Map<string, App.BlogPost[]>();
|
||||
|
||||
posts.forEach((post) => {
|
||||
const key = new Date(post.date!).toLocaleString('default', {
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
if (!groupedPosts.has(key)) groupedPosts.set(key, []);
|
||||
groupedPosts.get(key)?.push(post);
|
||||
});
|
||||
|
||||
return groupedPosts;
|
||||
}
|
||||
|
||||
const groupedPosts = groupPostsByMonthYear(page.data.posts);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Блог — Tea Sanctuary</title>
|
||||
</svelte:head>
|
||||
|
||||
<section
|
||||
class="flex shrink-0 flex-col items-center justify-center gap-5 overflow-hidden p-4 hero-background"
|
||||
>
|
||||
<section class="hero flex shrink-0 flex-col items-center justify-center gap-5 overflow-hidden p-4">
|
||||
<div
|
||||
class="flex flex-col flex-nowrap items-center justify-center gap-3 lg:flex-row lg:flex-nowrap"
|
||||
>
|
||||
|
|
@ -20,6 +34,26 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<InfoBlock>
|
||||
Подпишитесь на нашу <SocialHyperlink href="/blog/rss.xml">RSS ленту</SocialHyperlink>, чтобы не
|
||||
пропускать новые посты!
|
||||
</InfoBlock>
|
||||
|
||||
<section class="flex flex-col items-stretch p-2 pb-8 md:p-4">
|
||||
{#each groupedPosts.entries() as [monthYear, postsInMonthYear]}
|
||||
<h1
|
||||
class="mt-10 mb-4 text-left text-2xl font-bold underline decoration-4 md:text-center md:text-4xl md:decoration-8"
|
||||
>
|
||||
{monthYear}
|
||||
</h1>
|
||||
<div class="flex flex-col flex-wrap items-center gap-4">
|
||||
{#each postsInMonthYear as post, i}
|
||||
<BlogCard {post} />
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
@import "$src/app.css";
|
||||
@import '$src/app.css';
|
||||
</style>
|
||||
100
src/routes/blog/[slug]/+page.svelte
Normal file
100
src/routes/blog/[slug]/+page.svelte
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import DateWidget from '$src/lib/components/DateWidget.svelte';
|
||||
import InfoBlock from '$src/lib/components/InfoBlock.svelte';
|
||||
import Icon from '@iconify/svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { blogPostTypeToIcon, blogPostTypeToString } from '$src/lib/util/Blogs';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const isPublic = !!data.blogPost.date;
|
||||
const authors =
|
||||
data.blogPost.authors == null
|
||||
? []
|
||||
: typeof data.blogPost.authors === 'string'
|
||||
? [data.blogPost.authors]
|
||||
: data.blogPost.authors;
|
||||
const type: App.BlogPostType = data.blogPost.type ?? 'article';
|
||||
</script>
|
||||
|
||||
<base target="_blank" />
|
||||
|
||||
<svelte:head>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
</svelte:head>
|
||||
|
||||
<section class="hero flex shrink-0 flex-col items-center justify-center gap-5 overflow-hidden p-4">
|
||||
<div
|
||||
class="flex flex-col flex-nowrap items-center justify-center gap-3 lg:flex-row lg:flex-nowrap"
|
||||
>
|
||||
<div class="text-left">
|
||||
<h1>{data.blogPost.title}</h1>
|
||||
{#if data.blogPost.description}
|
||||
<h2 class="text-gray">{data.blogPost.description}</h2>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="flex shrink-0 flex-col flex-wrap items-center justify-center p-2 font-bold {isPublic
|
||||
? 'bg-amber-50 text-slate-950'
|
||||
: 'bg-red-500 text-slate-50'} sm:flex-row sm:gap-x-5"
|
||||
>
|
||||
<DateWidget dateString={data.blogPost.date} type="published" />
|
||||
{#if data.blogPost.dateChanged}
|
||||
<DateWidget dateString={data.blogPost.dateChanged} type="updated" />
|
||||
{/if}
|
||||
<div class="flex items-center gap-2 p-1 text-lg font-bold">
|
||||
<Icon icon={blogPostTypeToIcon(type)} width={28} height={28} />
|
||||
<span>
|
||||
{blogPostTypeToString(type)}
|
||||
</span>
|
||||
</div>
|
||||
{#each authors as author}
|
||||
<!-- TODO: rndtrash: из-за 404 не даёт собрать сайт. href="/team/{author}" -->
|
||||
<a class="flex items-center gap-2 p-1 text-lg font-bold" href="#">
|
||||
<Icon icon="material-symbols:person" width={28} height={28} />
|
||||
<span class="underline">
|
||||
{author}
|
||||
</span>
|
||||
</a>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
{#if page.data.blogPost.projects?.length > 0}
|
||||
<InfoBlock>
|
||||
<p>В данной заметке упоминаются наши проекты:</p>
|
||||
<ul>
|
||||
{#each page.data.blogPost.projects as project}
|
||||
<li>{project}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</InfoBlock>
|
||||
{/if}
|
||||
|
||||
<article
|
||||
class="prose
|
||||
sm:prose-xl
|
||||
prose-slate
|
||||
prose-code:break-words
|
||||
prose-pre:drop-shadow-md
|
||||
prose-headings:font-disket
|
||||
prose-headings:mb-4
|
||||
prose-headings:font-bold prose-headings:text-slate-950 prose-h1:text-2xl
|
||||
prose-h1:sm:text-4xl prose-h2:text-xl
|
||||
prose-h2:sm:text-3xl prose-p:text-justify
|
||||
mx-auto
|
||||
w-5xl
|
||||
p-4
|
||||
text-base
|
||||
text-slate-950
|
||||
sm:text-xl lg:p-8"
|
||||
>
|
||||
<svelte:component this={data.content} />
|
||||
</article>
|
||||
|
||||
<style>
|
||||
@import '$src/app.css';
|
||||
</style>
|
||||
23
src/routes/blog/[slug]/+page.ts
Normal file
23
src/routes/blog/[slug]/+page.ts
Normal file
|
|
@ -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
|
||||
}
|
||||
};
|
||||
};
|
||||
66
src/routes/blog/rss.xml/+server.ts
Normal file
66
src/routes/blog/rss.xml/+server.ts
Normal file
|
|
@ -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, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function makeThumbnail(post: App.BlogPost): string {
|
||||
if (!post.thumbnail) return '';
|
||||
|
||||
const alt = !!post.thumbnailAlt ? ` alt="${escapeXml(post.thumbnailAlt)}"` : '';
|
||||
|
||||
return `<br><br><img src="https://teasanctuary.ru${resolveBlogPath(post.slug, post.thumbnail)}"${alt}>`;
|
||||
}
|
||||
|
||||
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<author>${authorsString}</author>`;
|
||||
}
|
||||
|
||||
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(`<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<rss xmlns:dc="https://purl.org/dc/elements/1.1/" xmlns:content="https://purl.org/rss/1.0/modules/content/" xmlns:atom="https://www.w3.org/2005/Atom" version="2.0">
|
||||
<channel>
|
||||
<title>Блог Tea Sanctuary</title>
|
||||
<link>https://teasanctuary.ru/blog</link>
|
||||
<ttl>1800</ttl>
|
||||
<updated>${feedUpdated.toUTCString()}</updated>
|
||||
${posts.map((post) => `<item>
|
||||
<title>${escapeXml(post.title)}</title>
|
||||
<description><![CDATA[${escapeXml(post.description)}${makeThumbnail(post)}]]></description>${makeAuthors(post)}
|
||||
<guid isPermaLink="true">https://teasanctuary.ru/blog/${post.slug}</guid>
|
||||
<link>https://teasanctuary.ru/blog/${post.slug}</link>
|
||||
<pubDate>${(new Date(post.date!)).toUTCString()}</pubDate>
|
||||
</item>`).join("\n")}
|
||||
</channel>
|
||||
</rss>`))
|
||||
}
|
||||
|
|
@ -1,14 +1,9 @@
|
|||
<script lang="ts">
|
||||
import SocialButton from '$lib/components/SocialButton.svelte';
|
||||
import WarningBlock from '$lib/components/WarningBlock.svelte';
|
||||
import SocialHyperlink from '$src/lib/components/SocialHyperlink.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Контакты — Tea Sanctuary</title>
|
||||
</svelte:head>
|
||||
|
||||
<section
|
||||
class="flex shrink-0 flex-col items-center justify-center gap-5 overflow-hidden p-4 hero-background"
|
||||
>
|
||||
<section class="hero flex shrink-0 flex-col items-center justify-center gap-5 overflow-hidden p-4">
|
||||
<div
|
||||
class="flex flex-col flex-nowrap items-center justify-center gap-3 lg:flex-row lg:flex-nowrap"
|
||||
>
|
||||
|
|
@ -20,6 +15,35 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<WarningBlock>Страница находится в разработке!</WarningBlock>
|
||||
|
||||
<section class="flex justify-center">
|
||||
<div
|
||||
class="flex w-5xl max-w-screen flex-col flex-nowrap gap-12 p-2 px-2 pt-12 pb-12 text-base sm:text-xl"
|
||||
>
|
||||
<section>
|
||||
На данный момент вы можете связаться с администрацией сайта и участниками команды через
|
||||
<SocialHyperlink href="https://teasanctuary.ru/discord">
|
||||
нашу гильдию в Discord
|
||||
</SocialHyperlink>:
|
||||
</section>
|
||||
<iframe
|
||||
src="https://discord.com/widget?id=1176141874390638662&theme=dark"
|
||||
title="Виджет Discord"
|
||||
width="350"
|
||||
height="350"
|
||||
class="mx-auto"
|
||||
allowtransparency
|
||||
frameborder="0"
|
||||
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"
|
||||
></iframe>
|
||||
<section>
|
||||
Вы также можете ознакомиться с социальными сетями каждого отдельного участника команды
|
||||
на странице <SocialHyperlink href="/team">Команда</SocialHyperlink>.
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
@import "$src/app.css";
|
||||
@import '$src/app.css';
|
||||
</style>
|
||||
3
src/routes/contact/+page.ts
Normal file
3
src/routes/contact/+page.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export async function load() {
|
||||
return { title: "Контакты" };
|
||||
}
|
||||
|
|
@ -1,14 +1,8 @@
|
|||
<script lang="ts">
|
||||
import SocialButton from '$lib/components/SocialButton.svelte';
|
||||
import WarningBlock from '$lib/components/WarningBlock.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Проекты — Tea Sanctuary</title>
|
||||
</svelte:head>
|
||||
|
||||
<section
|
||||
class="flex shrink-0 flex-col items-center justify-center gap-5 overflow-hidden p-4 hero-background"
|
||||
>
|
||||
<section class="hero flex shrink-0 flex-col items-center justify-center gap-5 overflow-hidden p-4">
|
||||
<div
|
||||
class="flex flex-col flex-nowrap items-center justify-center gap-3 lg:flex-row lg:flex-nowrap"
|
||||
>
|
||||
|
|
@ -20,6 +14,8 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<WarningBlock>Страница находится в разработке!</WarningBlock>
|
||||
|
||||
<style>
|
||||
@import "$src/app.css";
|
||||
@import '$src/app.css';
|
||||
</style>
|
||||
3
src/routes/projects/+page.ts
Normal file
3
src/routes/projects/+page.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export async function load() {
|
||||
return { title: "Проекты" };
|
||||
}
|
||||
|
|
@ -1,14 +1,8 @@
|
|||
<script lang="ts">
|
||||
import SocialButton from '$lib/components/SocialButton.svelte';
|
||||
import WarningBlock from '$lib/components/WarningBlock.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Команда — Tea Sanctuary</title>
|
||||
</svelte:head>
|
||||
|
||||
<section
|
||||
class="flex shrink-0 flex-col items-center justify-center gap-5 overflow-hidden p-4 hero-background"
|
||||
>
|
||||
<section class="hero flex shrink-0 flex-col items-center justify-center gap-5 overflow-hidden p-4">
|
||||
<div
|
||||
class="flex flex-col flex-nowrap items-center justify-center gap-3 lg:flex-row lg:flex-nowrap"
|
||||
>
|
||||
|
|
@ -20,6 +14,8 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<WarningBlock>Страница находится в разработке!</WarningBlock>
|
||||
|
||||
<style>
|
||||
@import "$src/app.css";
|
||||
@import '$src/app.css';
|
||||
</style>
|
||||
3
src/routes/team/+page.ts
Normal file
3
src/routes/team/+page.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export async function load() {
|
||||
return { title: "Команда" };
|
||||
}
|
||||
BIN
static/blog/hello_world/wave.png
Normal file
BIN
static/blog/hello_world/wave.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
static/blog/test_unpublished/wasd_perelesoq_game_jam_2025.png
Normal file
BIN
static/blog/test_unpublished/wasd_perelesoq_game_jam_2025.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 449 KiB |
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -6,6 +6,8 @@ User-agent: AhrefsBot
|
|||
Disallow: /
|
||||
User-agent: MJ12bot
|
||||
Disallow: /
|
||||
User-agent: GPTBot
|
||||
Disallow: /
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
|
|
|
|||
|
|
@ -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' }]
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue