my website at ewancroft.uk

add cssnano for CSS optimization and implement performance monitoring

- Added `cssnano` to `.cspell.json`, `package.json`, and `package-lock.json` for CSS optimization.
- Updated `postcss.config.js` to conditionally include `cssnano` in production.
- Enhanced performance monitoring in `+layout.svelte` to log metrics after page load.
- Introduced a debugging utility system for structured logging and performance tracking.
- Implemented dynamic imports for markdown processing and utility functions to optimize loading.
- Added service worker for caching and offline support.
- Created sitemap and robots.txt for better SEO.

remove function configuration from vercel.json to streamline deployment settings

ewancroft.uk b89a0170 440ad0a9

verified
+1
.cspell.json
··· 16 16 "Centralised", 17 17 "colour", 18 18 "CRSV", 19 + "cssnano", 19 20 "customised", 20 21 "dbaeumer", 21 22 "Decentralised",
+829
package-lock.json
··· 37 37 "@types/node": "^22.10.1", 38 38 "@types/sanitize-html": "^2.13.0", 39 39 "autoprefixer": "^10.4.21", 40 + "cssnano": "^7.1.0", 40 41 "eslint": "^9.7.0", 41 42 "eslint-plugin-svelte": "^2.36.0", 42 43 "globals": "^15.0.0", ··· 2600 2601 "file-uri-to-path": "1.0.0" 2601 2602 } 2602 2603 }, 2604 + "node_modules/boolbase": { 2605 + "version": "1.0.0", 2606 + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 2607 + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", 2608 + "dev": true, 2609 + "license": "ISC" 2610 + }, 2603 2611 "node_modules/brace-expansion": { 2604 2612 "version": "1.1.11", 2605 2613 "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", ··· 2684 2692 "license": "MIT", 2685 2693 "funding": { 2686 2694 "url": "https://github.com/sponsors/ljharb" 2695 + } 2696 + }, 2697 + "node_modules/caniuse-api": { 2698 + "version": "3.0.0", 2699 + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", 2700 + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", 2701 + "dev": true, 2702 + "license": "MIT", 2703 + "dependencies": { 2704 + "browserslist": "^4.0.0", 2705 + "caniuse-lite": "^1.0.0", 2706 + "lodash.memoize": "^4.1.2", 2707 + "lodash.uniq": "^4.5.0" 2687 2708 } 2688 2709 }, 2689 2710 "node_modules/caniuse-lite": { ··· 2828 2849 "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 2829 2850 "license": "MIT" 2830 2851 }, 2852 + "node_modules/colord": { 2853 + "version": "2.9.3", 2854 + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", 2855 + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", 2856 + "dev": true, 2857 + "license": "MIT" 2858 + }, 2831 2859 "node_modules/comma-separated-tokens": { 2832 2860 "version": "2.0.3", 2833 2861 "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", ··· 2911 2939 "node": ">=4" 2912 2940 } 2913 2941 }, 2942 + "node_modules/css-declaration-sorter": { 2943 + "version": "7.2.0", 2944 + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", 2945 + "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", 2946 + "dev": true, 2947 + "license": "ISC", 2948 + "engines": { 2949 + "node": "^14 || ^16 || >=18" 2950 + }, 2951 + "peerDependencies": { 2952 + "postcss": "^8.0.9" 2953 + } 2954 + }, 2914 2955 "node_modules/css-gradient-parser": { 2915 2956 "version": "0.0.16", 2916 2957 "resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.16.tgz", ··· 2920 2961 "node": ">=16" 2921 2962 } 2922 2963 }, 2964 + "node_modules/css-select": { 2965 + "version": "5.2.2", 2966 + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", 2967 + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", 2968 + "dev": true, 2969 + "license": "BSD-2-Clause", 2970 + "dependencies": { 2971 + "boolbase": "^1.0.0", 2972 + "css-what": "^6.1.0", 2973 + "domhandler": "^5.0.2", 2974 + "domutils": "^3.0.1", 2975 + "nth-check": "^2.0.1" 2976 + }, 2977 + "funding": { 2978 + "url": "https://github.com/sponsors/fb55" 2979 + } 2980 + }, 2923 2981 "node_modules/css-to-react-native": { 2924 2982 "version": "3.2.0", 2925 2983 "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", ··· 2931 2989 "postcss-value-parser": "^4.0.2" 2932 2990 } 2933 2991 }, 2992 + "node_modules/css-tree": { 2993 + "version": "3.1.0", 2994 + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", 2995 + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", 2996 + "dev": true, 2997 + "license": "MIT", 2998 + "dependencies": { 2999 + "mdn-data": "2.12.2", 3000 + "source-map-js": "^1.0.1" 3001 + }, 3002 + "engines": { 3003 + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" 3004 + } 3005 + }, 3006 + "node_modules/css-what": { 3007 + "version": "6.2.2", 3008 + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", 3009 + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", 3010 + "dev": true, 3011 + "license": "BSD-2-Clause", 3012 + "engines": { 3013 + "node": ">= 6" 3014 + }, 3015 + "funding": { 3016 + "url": "https://github.com/sponsors/fb55" 3017 + } 3018 + }, 2934 3019 "node_modules/cssesc": { 2935 3020 "version": "3.0.0", 2936 3021 "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", ··· 2943 3028 "node": ">=4" 2944 3029 } 2945 3030 }, 3031 + "node_modules/cssnano": { 3032 + "version": "7.1.0", 3033 + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.0.tgz", 3034 + "integrity": "sha512-Pu3rlKkd0ZtlCUzBrKL1Z4YmhKppjC1H9jo7u1o4qaKqyhvixFgu5qLyNIAOjSTg9DjVPtUqdROq2EfpVMEe+w==", 3035 + "dev": true, 3036 + "license": "MIT", 3037 + "dependencies": { 3038 + "cssnano-preset-default": "^7.0.8", 3039 + "lilconfig": "^3.1.3" 3040 + }, 3041 + "engines": { 3042 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 3043 + }, 3044 + "funding": { 3045 + "type": "opencollective", 3046 + "url": "https://opencollective.com/cssnano" 3047 + }, 3048 + "peerDependencies": { 3049 + "postcss": "^8.4.32" 3050 + } 3051 + }, 3052 + "node_modules/cssnano-preset-default": { 3053 + "version": "7.0.8", 3054 + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.8.tgz", 3055 + "integrity": "sha512-d+3R2qwrUV3g4LEMOjnndognKirBZISylDZAF/TPeCWVjEwlXS2e4eN4ICkoobRe7pD3H6lltinKVyS1AJhdjQ==", 3056 + "dev": true, 3057 + "license": "MIT", 3058 + "dependencies": { 3059 + "browserslist": "^4.25.1", 3060 + "css-declaration-sorter": "^7.2.0", 3061 + "cssnano-utils": "^5.0.1", 3062 + "postcss-calc": "^10.1.1", 3063 + "postcss-colormin": "^7.0.4", 3064 + "postcss-convert-values": "^7.0.6", 3065 + "postcss-discard-comments": "^7.0.4", 3066 + "postcss-discard-duplicates": "^7.0.2", 3067 + "postcss-discard-empty": "^7.0.1", 3068 + "postcss-discard-overridden": "^7.0.1", 3069 + "postcss-merge-longhand": "^7.0.5", 3070 + "postcss-merge-rules": "^7.0.6", 3071 + "postcss-minify-font-values": "^7.0.1", 3072 + "postcss-minify-gradients": "^7.0.1", 3073 + "postcss-minify-params": "^7.0.4", 3074 + "postcss-minify-selectors": "^7.0.5", 3075 + "postcss-normalize-charset": "^7.0.1", 3076 + "postcss-normalize-display-values": "^7.0.1", 3077 + "postcss-normalize-positions": "^7.0.1", 3078 + "postcss-normalize-repeat-style": "^7.0.1", 3079 + "postcss-normalize-string": "^7.0.1", 3080 + "postcss-normalize-timing-functions": "^7.0.1", 3081 + "postcss-normalize-unicode": "^7.0.4", 3082 + "postcss-normalize-url": "^7.0.1", 3083 + "postcss-normalize-whitespace": "^7.0.1", 3084 + "postcss-ordered-values": "^7.0.2", 3085 + "postcss-reduce-initial": "^7.0.4", 3086 + "postcss-reduce-transforms": "^7.0.1", 3087 + "postcss-svgo": "^7.1.0", 3088 + "postcss-unique-selectors": "^7.0.4" 3089 + }, 3090 + "engines": { 3091 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 3092 + }, 3093 + "peerDependencies": { 3094 + "postcss": "^8.4.32" 3095 + } 3096 + }, 3097 + "node_modules/cssnano-utils": { 3098 + "version": "5.0.1", 3099 + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.1.tgz", 3100 + "integrity": "sha512-ZIP71eQgG9JwjVZsTPSqhc6GHgEr53uJ7tK5///VfyWj6Xp2DBmixWHqJgPno+PqATzn48pL42ww9x5SSGmhZg==", 3101 + "dev": true, 3102 + "license": "MIT", 3103 + "engines": { 3104 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 3105 + }, 3106 + "peerDependencies": { 3107 + "postcss": "^8.4.32" 3108 + } 3109 + }, 3110 + "node_modules/cssnano/node_modules/lilconfig": { 3111 + "version": "3.1.3", 3112 + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", 3113 + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", 3114 + "dev": true, 3115 + "license": "MIT", 3116 + "engines": { 3117 + "node": ">=14" 3118 + }, 3119 + "funding": { 3120 + "url": "https://github.com/sponsors/antonk52" 3121 + } 3122 + }, 3123 + "node_modules/csso": { 3124 + "version": "5.0.5", 3125 + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", 3126 + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", 3127 + "dev": true, 3128 + "license": "MIT", 3129 + "dependencies": { 3130 + "css-tree": "~2.2.0" 3131 + }, 3132 + "engines": { 3133 + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", 3134 + "npm": ">=7.0.0" 3135 + } 3136 + }, 3137 + "node_modules/csso/node_modules/css-tree": { 3138 + "version": "2.2.1", 3139 + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", 3140 + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", 3141 + "dev": true, 3142 + "license": "MIT", 3143 + "dependencies": { 3144 + "mdn-data": "2.0.28", 3145 + "source-map-js": "^1.0.1" 3146 + }, 3147 + "engines": { 3148 + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", 3149 + "npm": ">=7.0.0" 3150 + } 3151 + }, 3152 + "node_modules/csso/node_modules/mdn-data": { 3153 + "version": "2.0.28", 3154 + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", 3155 + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", 3156 + "dev": true, 3157 + "license": "CC0-1.0" 3158 + }, 2946 3159 "node_modules/debug": { 2947 3160 "version": "4.3.7", 2948 3161 "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", ··· 4420 4633 "dev": true, 4421 4634 "license": "MIT" 4422 4635 }, 4636 + "node_modules/lodash.memoize": { 4637 + "version": "4.1.2", 4638 + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", 4639 + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", 4640 + "dev": true, 4641 + "license": "MIT" 4642 + }, 4423 4643 "node_modules/lodash.merge": { 4424 4644 "version": "4.6.2", 4425 4645 "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 4426 4646 "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 4647 + "dev": true, 4648 + "license": "MIT" 4649 + }, 4650 + "node_modules/lodash.uniq": { 4651 + "version": "4.5.0", 4652 + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", 4653 + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", 4427 4654 "dev": true, 4428 4655 "license": "MIT" 4429 4656 }, ··· 4733 4960 "type": "opencollective", 4734 4961 "url": "https://opencollective.com/unified" 4735 4962 } 4963 + }, 4964 + "node_modules/mdn-data": { 4965 + "version": "2.12.2", 4966 + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", 4967 + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", 4968 + "dev": true, 4969 + "license": "CC0-1.0" 4736 4970 }, 4737 4971 "node_modules/merge2": { 4738 4972 "version": "1.4.1", ··· 5569 5803 "node": ">=0.10.0" 5570 5804 } 5571 5805 }, 5806 + "node_modules/nth-check": { 5807 + "version": "2.1.1", 5808 + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", 5809 + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", 5810 + "dev": true, 5811 + "license": "BSD-2-Clause", 5812 + "dependencies": { 5813 + "boolbase": "^1.0.0" 5814 + }, 5815 + "funding": { 5816 + "url": "https://github.com/fb55/nth-check?sponsor=1" 5817 + } 5818 + }, 5572 5819 "node_modules/object-assign": { 5573 5820 "version": "4.1.1", 5574 5821 "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", ··· 5802 6049 "node": "^10 || ^12 || >=14" 5803 6050 } 5804 6051 }, 6052 + "node_modules/postcss-calc": { 6053 + "version": "10.1.1", 6054 + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.1.1.tgz", 6055 + "integrity": "sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==", 6056 + "dev": true, 6057 + "license": "MIT", 6058 + "dependencies": { 6059 + "postcss-selector-parser": "^7.0.0", 6060 + "postcss-value-parser": "^4.2.0" 6061 + }, 6062 + "engines": { 6063 + "node": "^18.12 || ^20.9 || >=22.0" 6064 + }, 6065 + "peerDependencies": { 6066 + "postcss": "^8.4.38" 6067 + } 6068 + }, 6069 + "node_modules/postcss-calc/node_modules/postcss-selector-parser": { 6070 + "version": "7.1.0", 6071 + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", 6072 + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", 6073 + "dev": true, 6074 + "license": "MIT", 6075 + "dependencies": { 6076 + "cssesc": "^3.0.0", 6077 + "util-deprecate": "^1.0.2" 6078 + }, 6079 + "engines": { 6080 + "node": ">=4" 6081 + } 6082 + }, 6083 + "node_modules/postcss-colormin": { 6084 + "version": "7.0.4", 6085 + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.4.tgz", 6086 + "integrity": "sha512-ziQuVzQZBROpKpfeDwmrG+Vvlr0YWmY/ZAk99XD+mGEBuEojoFekL41NCsdhyNUtZI7DPOoIWIR7vQQK9xwluw==", 6087 + "dev": true, 6088 + "license": "MIT", 6089 + "dependencies": { 6090 + "browserslist": "^4.25.1", 6091 + "caniuse-api": "^3.0.0", 6092 + "colord": "^2.9.3", 6093 + "postcss-value-parser": "^4.2.0" 6094 + }, 6095 + "engines": { 6096 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6097 + }, 6098 + "peerDependencies": { 6099 + "postcss": "^8.4.32" 6100 + } 6101 + }, 6102 + "node_modules/postcss-convert-values": { 6103 + "version": "7.0.6", 6104 + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.6.tgz", 6105 + "integrity": "sha512-MD/eb39Mr60hvgrqpXsgbiqluawYg/8K4nKsqRsuDX9f+xN1j6awZCUv/5tLH8ak3vYp/EMXwdcnXvfZYiejCQ==", 6106 + "dev": true, 6107 + "license": "MIT", 6108 + "dependencies": { 6109 + "browserslist": "^4.25.1", 6110 + "postcss-value-parser": "^4.2.0" 6111 + }, 6112 + "engines": { 6113 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6114 + }, 6115 + "peerDependencies": { 6116 + "postcss": "^8.4.32" 6117 + } 6118 + }, 6119 + "node_modules/postcss-discard-comments": { 6120 + "version": "7.0.4", 6121 + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.4.tgz", 6122 + "integrity": "sha512-6tCUoql/ipWwKtVP/xYiFf1U9QgJ0PUvxN7pTcsQ8Ns3Fnwq1pU5D5s1MhT/XySeLq6GXNvn37U46Ded0TckWg==", 6123 + "dev": true, 6124 + "license": "MIT", 6125 + "dependencies": { 6126 + "postcss-selector-parser": "^7.1.0" 6127 + }, 6128 + "engines": { 6129 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6130 + }, 6131 + "peerDependencies": { 6132 + "postcss": "^8.4.32" 6133 + } 6134 + }, 6135 + "node_modules/postcss-discard-comments/node_modules/postcss-selector-parser": { 6136 + "version": "7.1.0", 6137 + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", 6138 + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", 6139 + "dev": true, 6140 + "license": "MIT", 6141 + "dependencies": { 6142 + "cssesc": "^3.0.0", 6143 + "util-deprecate": "^1.0.2" 6144 + }, 6145 + "engines": { 6146 + "node": ">=4" 6147 + } 6148 + }, 6149 + "node_modules/postcss-discard-duplicates": { 6150 + "version": "7.0.2", 6151 + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.2.tgz", 6152 + "integrity": "sha512-eTonaQvPZ/3i1ASDHOKkYwAybiM45zFIc7KXils4mQmHLqIswXD9XNOKEVxtTFnsmwYzF66u4LMgSr0abDlh5w==", 6153 + "dev": true, 6154 + "license": "MIT", 6155 + "engines": { 6156 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6157 + }, 6158 + "peerDependencies": { 6159 + "postcss": "^8.4.32" 6160 + } 6161 + }, 6162 + "node_modules/postcss-discard-empty": { 6163 + "version": "7.0.1", 6164 + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.1.tgz", 6165 + "integrity": "sha512-cFrJKZvcg/uxB6Ijr4l6qmn3pXQBna9zyrPC+sK0zjbkDUZew+6xDltSF7OeB7rAtzaaMVYSdbod+sZOCWnMOg==", 6166 + "dev": true, 6167 + "license": "MIT", 6168 + "engines": { 6169 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6170 + }, 6171 + "peerDependencies": { 6172 + "postcss": "^8.4.32" 6173 + } 6174 + }, 6175 + "node_modules/postcss-discard-overridden": { 6176 + "version": "7.0.1", 6177 + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.1.tgz", 6178 + "integrity": "sha512-7c3MMjjSZ/qYrx3uc1940GSOzN1Iqjtlqe8uoSg+qdVPYyRb0TILSqqmtlSFuE4mTDECwsm397Ya7iXGzfF7lg==", 6179 + "dev": true, 6180 + "license": "MIT", 6181 + "engines": { 6182 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6183 + }, 6184 + "peerDependencies": { 6185 + "postcss": "^8.4.32" 6186 + } 6187 + }, 5805 6188 "node_modules/postcss-import": { 5806 6189 "version": "16.1.1", 5807 6190 "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-16.1.1.tgz", ··· 5869 6252 } 5870 6253 } 5871 6254 }, 6255 + "node_modules/postcss-merge-longhand": { 6256 + "version": "7.0.5", 6257 + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.5.tgz", 6258 + "integrity": "sha512-Kpu5v4Ys6QI59FxmxtNB/iHUVDn9Y9sYw66D6+SZoIk4QTz1prC4aYkhIESu+ieG1iylod1f8MILMs1Em3mmIw==", 6259 + "dev": true, 6260 + "license": "MIT", 6261 + "dependencies": { 6262 + "postcss-value-parser": "^4.2.0", 6263 + "stylehacks": "^7.0.5" 6264 + }, 6265 + "engines": { 6266 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6267 + }, 6268 + "peerDependencies": { 6269 + "postcss": "^8.4.32" 6270 + } 6271 + }, 6272 + "node_modules/postcss-merge-rules": { 6273 + "version": "7.0.6", 6274 + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.6.tgz", 6275 + "integrity": "sha512-2jIPT4Tzs8K87tvgCpSukRQ2jjd+hH6Bb8rEEOUDmmhOeTcqDg5fEFK8uKIu+Pvc3//sm3Uu6FRqfyv7YF7+BQ==", 6276 + "dev": true, 6277 + "license": "MIT", 6278 + "dependencies": { 6279 + "browserslist": "^4.25.1", 6280 + "caniuse-api": "^3.0.0", 6281 + "cssnano-utils": "^5.0.1", 6282 + "postcss-selector-parser": "^7.1.0" 6283 + }, 6284 + "engines": { 6285 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6286 + }, 6287 + "peerDependencies": { 6288 + "postcss": "^8.4.32" 6289 + } 6290 + }, 6291 + "node_modules/postcss-merge-rules/node_modules/postcss-selector-parser": { 6292 + "version": "7.1.0", 6293 + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", 6294 + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", 6295 + "dev": true, 6296 + "license": "MIT", 6297 + "dependencies": { 6298 + "cssesc": "^3.0.0", 6299 + "util-deprecate": "^1.0.2" 6300 + }, 6301 + "engines": { 6302 + "node": ">=4" 6303 + } 6304 + }, 6305 + "node_modules/postcss-minify-font-values": { 6306 + "version": "7.0.1", 6307 + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.1.tgz", 6308 + "integrity": "sha512-2m1uiuJeTplll+tq4ENOQSzB8LRnSUChBv7oSyFLsJRtUgAAJGP6LLz0/8lkinTgxrmJSPOEhgY1bMXOQ4ZXhQ==", 6309 + "dev": true, 6310 + "license": "MIT", 6311 + "dependencies": { 6312 + "postcss-value-parser": "^4.2.0" 6313 + }, 6314 + "engines": { 6315 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6316 + }, 6317 + "peerDependencies": { 6318 + "postcss": "^8.4.32" 6319 + } 6320 + }, 6321 + "node_modules/postcss-minify-gradients": { 6322 + "version": "7.0.1", 6323 + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.1.tgz", 6324 + "integrity": "sha512-X9JjaysZJwlqNkJbUDgOclyG3jZEpAMOfof6PUZjPnPrePnPG62pS17CjdM32uT1Uq1jFvNSff9l7kNbmMSL2A==", 6325 + "dev": true, 6326 + "license": "MIT", 6327 + "dependencies": { 6328 + "colord": "^2.9.3", 6329 + "cssnano-utils": "^5.0.1", 6330 + "postcss-value-parser": "^4.2.0" 6331 + }, 6332 + "engines": { 6333 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6334 + }, 6335 + "peerDependencies": { 6336 + "postcss": "^8.4.32" 6337 + } 6338 + }, 6339 + "node_modules/postcss-minify-params": { 6340 + "version": "7.0.4", 6341 + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.4.tgz", 6342 + "integrity": "sha512-3OqqUddfH8c2e7M35W6zIwv7jssM/3miF9cbCSb1iJiWvtguQjlxZGIHK9JRmc8XAKmE2PFGtHSM7g/VcW97sw==", 6343 + "dev": true, 6344 + "license": "MIT", 6345 + "dependencies": { 6346 + "browserslist": "^4.25.1", 6347 + "cssnano-utils": "^5.0.1", 6348 + "postcss-value-parser": "^4.2.0" 6349 + }, 6350 + "engines": { 6351 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6352 + }, 6353 + "peerDependencies": { 6354 + "postcss": "^8.4.32" 6355 + } 6356 + }, 6357 + "node_modules/postcss-minify-selectors": { 6358 + "version": "7.0.5", 6359 + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.5.tgz", 6360 + "integrity": "sha512-x2/IvofHcdIrAm9Q+p06ZD1h6FPcQ32WtCRVodJLDR+WMn8EVHI1kvLxZuGKz/9EY5nAmI6lIQIrpo4tBy5+ug==", 6361 + "dev": true, 6362 + "license": "MIT", 6363 + "dependencies": { 6364 + "cssesc": "^3.0.0", 6365 + "postcss-selector-parser": "^7.1.0" 6366 + }, 6367 + "engines": { 6368 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6369 + }, 6370 + "peerDependencies": { 6371 + "postcss": "^8.4.32" 6372 + } 6373 + }, 6374 + "node_modules/postcss-minify-selectors/node_modules/postcss-selector-parser": { 6375 + "version": "7.1.0", 6376 + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", 6377 + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", 6378 + "dev": true, 6379 + "license": "MIT", 6380 + "dependencies": { 6381 + "cssesc": "^3.0.0", 6382 + "util-deprecate": "^1.0.2" 6383 + }, 6384 + "engines": { 6385 + "node": ">=4" 6386 + } 6387 + }, 5872 6388 "node_modules/postcss-nested": { 5873 6389 "version": "6.2.0", 5874 6390 "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", ··· 5895 6411 "postcss": "^8.2.14" 5896 6412 } 5897 6413 }, 6414 + "node_modules/postcss-normalize-charset": { 6415 + "version": "7.0.1", 6416 + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.1.tgz", 6417 + "integrity": "sha512-sn413ofhSQHlZFae//m9FTOfkmiZ+YQXsbosqOWRiVQncU2BA3daX3n0VF3cG6rGLSFVc5Di/yns0dFfh8NFgQ==", 6418 + "dev": true, 6419 + "license": "MIT", 6420 + "engines": { 6421 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6422 + }, 6423 + "peerDependencies": { 6424 + "postcss": "^8.4.32" 6425 + } 6426 + }, 6427 + "node_modules/postcss-normalize-display-values": { 6428 + "version": "7.0.1", 6429 + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.1.tgz", 6430 + "integrity": "sha512-E5nnB26XjSYz/mGITm6JgiDpAbVuAkzXwLzRZtts19jHDUBFxZ0BkXAehy0uimrOjYJbocby4FVswA/5noOxrQ==", 6431 + "dev": true, 6432 + "license": "MIT", 6433 + "dependencies": { 6434 + "postcss-value-parser": "^4.2.0" 6435 + }, 6436 + "engines": { 6437 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6438 + }, 6439 + "peerDependencies": { 6440 + "postcss": "^8.4.32" 6441 + } 6442 + }, 6443 + "node_modules/postcss-normalize-positions": { 6444 + "version": "7.0.1", 6445 + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.1.tgz", 6446 + "integrity": "sha512-pB/SzrIP2l50ZIYu+yQZyMNmnAcwyYb9R1fVWPRxm4zcUFCY2ign7rcntGFuMXDdd9L2pPNUgoODDk91PzRZuQ==", 6447 + "dev": true, 6448 + "license": "MIT", 6449 + "dependencies": { 6450 + "postcss-value-parser": "^4.2.0" 6451 + }, 6452 + "engines": { 6453 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6454 + }, 6455 + "peerDependencies": { 6456 + "postcss": "^8.4.32" 6457 + } 6458 + }, 6459 + "node_modules/postcss-normalize-repeat-style": { 6460 + "version": "7.0.1", 6461 + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.1.tgz", 6462 + "integrity": "sha512-NsSQJ8zj8TIDiF0ig44Byo3Jk9e4gNt9x2VIlJudnQQ5DhWAHJPF4Tr1ITwyHio2BUi/I6Iv0HRO7beHYOloYQ==", 6463 + "dev": true, 6464 + "license": "MIT", 6465 + "dependencies": { 6466 + "postcss-value-parser": "^4.2.0" 6467 + }, 6468 + "engines": { 6469 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6470 + }, 6471 + "peerDependencies": { 6472 + "postcss": "^8.4.32" 6473 + } 6474 + }, 6475 + "node_modules/postcss-normalize-string": { 6476 + "version": "7.0.1", 6477 + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.1.tgz", 6478 + "integrity": "sha512-QByrI7hAhsoze992kpbMlJSbZ8FuCEc1OT9EFbZ6HldXNpsdpZr+YXC5di3UEv0+jeZlHbZcoCADgb7a+lPmmQ==", 6479 + "dev": true, 6480 + "license": "MIT", 6481 + "dependencies": { 6482 + "postcss-value-parser": "^4.2.0" 6483 + }, 6484 + "engines": { 6485 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6486 + }, 6487 + "peerDependencies": { 6488 + "postcss": "^8.4.32" 6489 + } 6490 + }, 6491 + "node_modules/postcss-normalize-timing-functions": { 6492 + "version": "7.0.1", 6493 + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.1.tgz", 6494 + "integrity": "sha512-bHifyuuSNdKKsnNJ0s8fmfLMlvsQwYVxIoUBnowIVl2ZAdrkYQNGVB4RxjfpvkMjipqvbz0u7feBZybkl/6NJg==", 6495 + "dev": true, 6496 + "license": "MIT", 6497 + "dependencies": { 6498 + "postcss-value-parser": "^4.2.0" 6499 + }, 6500 + "engines": { 6501 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6502 + }, 6503 + "peerDependencies": { 6504 + "postcss": "^8.4.32" 6505 + } 6506 + }, 6507 + "node_modules/postcss-normalize-unicode": { 6508 + "version": "7.0.4", 6509 + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.4.tgz", 6510 + "integrity": "sha512-LvIURTi1sQoZqj8mEIE8R15yvM+OhbR1avynMtI9bUzj5gGKR/gfZFd8O7VMj0QgJaIFzxDwxGl/ASMYAkqO8g==", 6511 + "dev": true, 6512 + "license": "MIT", 6513 + "dependencies": { 6514 + "browserslist": "^4.25.1", 6515 + "postcss-value-parser": "^4.2.0" 6516 + }, 6517 + "engines": { 6518 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6519 + }, 6520 + "peerDependencies": { 6521 + "postcss": "^8.4.32" 6522 + } 6523 + }, 6524 + "node_modules/postcss-normalize-url": { 6525 + "version": "7.0.1", 6526 + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.1.tgz", 6527 + "integrity": "sha512-sUcD2cWtyK1AOL/82Fwy1aIVm/wwj5SdZkgZ3QiUzSzQQofrbq15jWJ3BA7Z+yVRwamCjJgZJN0I9IS7c6tgeQ==", 6528 + "dev": true, 6529 + "license": "MIT", 6530 + "dependencies": { 6531 + "postcss-value-parser": "^4.2.0" 6532 + }, 6533 + "engines": { 6534 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6535 + }, 6536 + "peerDependencies": { 6537 + "postcss": "^8.4.32" 6538 + } 6539 + }, 6540 + "node_modules/postcss-normalize-whitespace": { 6541 + "version": "7.0.1", 6542 + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.1.tgz", 6543 + "integrity": "sha512-vsbgFHMFQrJBJKrUFJNZ2pgBeBkC2IvvoHjz1to0/0Xk7sII24T0qFOiJzG6Fu3zJoq/0yI4rKWi7WhApW+EFA==", 6544 + "dev": true, 6545 + "license": "MIT", 6546 + "dependencies": { 6547 + "postcss-value-parser": "^4.2.0" 6548 + }, 6549 + "engines": { 6550 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6551 + }, 6552 + "peerDependencies": { 6553 + "postcss": "^8.4.32" 6554 + } 6555 + }, 6556 + "node_modules/postcss-ordered-values": { 6557 + "version": "7.0.2", 6558 + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.2.tgz", 6559 + "integrity": "sha512-AMJjt1ECBffF7CEON/Y0rekRLS6KsePU6PRP08UqYW4UGFRnTXNrByUzYK1h8AC7UWTZdQ9O3Oq9kFIhm0SFEw==", 6560 + "dev": true, 6561 + "license": "MIT", 6562 + "dependencies": { 6563 + "cssnano-utils": "^5.0.1", 6564 + "postcss-value-parser": "^4.2.0" 6565 + }, 6566 + "engines": { 6567 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6568 + }, 6569 + "peerDependencies": { 6570 + "postcss": "^8.4.32" 6571 + } 6572 + }, 6573 + "node_modules/postcss-reduce-initial": { 6574 + "version": "7.0.4", 6575 + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.4.tgz", 6576 + "integrity": "sha512-rdIC9IlMBn7zJo6puim58Xd++0HdbvHeHaPgXsimMfG1ijC5A9ULvNLSE0rUKVJOvNMcwewW4Ga21ngyJjY/+Q==", 6577 + "dev": true, 6578 + "license": "MIT", 6579 + "dependencies": { 6580 + "browserslist": "^4.25.1", 6581 + "caniuse-api": "^3.0.0" 6582 + }, 6583 + "engines": { 6584 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6585 + }, 6586 + "peerDependencies": { 6587 + "postcss": "^8.4.32" 6588 + } 6589 + }, 6590 + "node_modules/postcss-reduce-transforms": { 6591 + "version": "7.0.1", 6592 + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.1.tgz", 6593 + "integrity": "sha512-MhyEbfrm+Mlp/36hvZ9mT9DaO7dbncU0CvWI8V93LRkY6IYlu38OPg3FObnuKTUxJ4qA8HpurdQOo5CyqqO76g==", 6594 + "dev": true, 6595 + "license": "MIT", 6596 + "dependencies": { 6597 + "postcss-value-parser": "^4.2.0" 6598 + }, 6599 + "engines": { 6600 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6601 + }, 6602 + "peerDependencies": { 6603 + "postcss": "^8.4.32" 6604 + } 6605 + }, 5898 6606 "node_modules/postcss-safe-parser": { 5899 6607 "version": "6.0.0", 5900 6608 "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", ··· 5952 6660 "node": ">=4" 5953 6661 } 5954 6662 }, 6663 + "node_modules/postcss-svgo": { 6664 + "version": "7.1.0", 6665 + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.1.0.tgz", 6666 + "integrity": "sha512-KnAlfmhtoLz6IuU3Sij2ycusNs4jPW+QoFE5kuuUOK8awR6tMxZQrs5Ey3BUz7nFCzT3eqyFgqkyrHiaU2xx3w==", 6667 + "dev": true, 6668 + "license": "MIT", 6669 + "dependencies": { 6670 + "postcss-value-parser": "^4.2.0", 6671 + "svgo": "^4.0.0" 6672 + }, 6673 + "engines": { 6674 + "node": "^18.12.0 || ^20.9.0 || >= 18" 6675 + }, 6676 + "peerDependencies": { 6677 + "postcss": "^8.4.32" 6678 + } 6679 + }, 6680 + "node_modules/postcss-unique-selectors": { 6681 + "version": "7.0.4", 6682 + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.4.tgz", 6683 + "integrity": "sha512-pmlZjsmEAG7cHd7uK3ZiNSW6otSZ13RHuZ/4cDN/bVglS5EpF2r2oxY99SuOHa8m7AWoBCelTS3JPpzsIs8skQ==", 6684 + "dev": true, 6685 + "license": "MIT", 6686 + "dependencies": { 6687 + "postcss-selector-parser": "^7.1.0" 6688 + }, 6689 + "engines": { 6690 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 6691 + }, 6692 + "peerDependencies": { 6693 + "postcss": "^8.4.32" 6694 + } 6695 + }, 6696 + "node_modules/postcss-unique-selectors/node_modules/postcss-selector-parser": { 6697 + "version": "7.1.0", 6698 + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", 6699 + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", 6700 + "dev": true, 6701 + "license": "MIT", 6702 + "dependencies": { 6703 + "cssesc": "^3.0.0", 6704 + "util-deprecate": "^1.0.2" 6705 + }, 6706 + "engines": { 6707 + "node": ">=4" 6708 + } 6709 + }, 5955 6710 "node_modules/postcss-value-parser": { 5956 6711 "version": "4.2.0", 5957 6712 "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", ··· 6431 7186 "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", 6432 7187 "license": "MIT" 6433 7188 }, 7189 + "node_modules/sax": { 7190 + "version": "1.4.1", 7191 + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", 7192 + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", 7193 + "dev": true, 7194 + "license": "ISC" 7195 + }, 6434 7196 "node_modules/semver": { 6435 7197 "version": "7.6.3", 6436 7198 "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", ··· 6670 7432 "url": "https://github.com/sponsors/sindresorhus" 6671 7433 } 6672 7434 }, 7435 + "node_modules/stylehacks": { 7436 + "version": "7.0.6", 7437 + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.6.tgz", 7438 + "integrity": "sha512-iitguKivmsueOmTO0wmxURXBP8uqOO+zikLGZ7Mm9e/94R4w5T999Js2taS/KBOnQ/wdC3jN3vNSrkGDrlnqQg==", 7439 + "dev": true, 7440 + "license": "MIT", 7441 + "dependencies": { 7442 + "browserslist": "^4.25.1", 7443 + "postcss-selector-parser": "^7.1.0" 7444 + }, 7445 + "engines": { 7446 + "node": "^18.12.0 || ^20.9.0 || >=22.0" 7447 + }, 7448 + "peerDependencies": { 7449 + "postcss": "^8.4.32" 7450 + } 7451 + }, 7452 + "node_modules/stylehacks/node_modules/postcss-selector-parser": { 7453 + "version": "7.1.0", 7454 + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", 7455 + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", 7456 + "dev": true, 7457 + "license": "MIT", 7458 + "dependencies": { 7459 + "cssesc": "^3.0.0", 7460 + "util-deprecate": "^1.0.2" 7461 + }, 7462 + "engines": { 7463 + "node": ">=4" 7464 + } 7465 + }, 6673 7466 "node_modules/sucrase": { 6674 7467 "version": "3.35.0", 6675 7468 "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", ··· 6842 7635 }, 6843 7636 "funding": { 6844 7637 "url": "https://opencollective.com/eslint" 7638 + } 7639 + }, 7640 + "node_modules/svgo": { 7641 + "version": "4.0.0", 7642 + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", 7643 + "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==", 7644 + "dev": true, 7645 + "license": "MIT", 7646 + "dependencies": { 7647 + "commander": "^11.1.0", 7648 + "css-select": "^5.1.0", 7649 + "css-tree": "^3.0.1", 7650 + "css-what": "^6.1.0", 7651 + "csso": "^5.0.5", 7652 + "picocolors": "^1.1.1", 7653 + "sax": "^1.4.1" 7654 + }, 7655 + "bin": { 7656 + "svgo": "bin/svgo.js" 7657 + }, 7658 + "engines": { 7659 + "node": ">=16" 7660 + }, 7661 + "funding": { 7662 + "type": "opencollective", 7663 + "url": "https://opencollective.com/svgo" 7664 + } 7665 + }, 7666 + "node_modules/svgo/node_modules/commander": { 7667 + "version": "11.1.0", 7668 + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", 7669 + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", 7670 + "dev": true, 7671 + "license": "MIT", 7672 + "engines": { 7673 + "node": ">=16" 6845 7674 } 6846 7675 }, 6847 7676 "node_modules/tailwindcss": {
+1
package.json
··· 19 19 "@types/node": "^22.10.1", 20 20 "@types/sanitize-html": "^2.13.0", 21 21 "autoprefixer": "^10.4.21", 22 + "cssnano": "^7.1.0", 22 23 "eslint": "^9.7.0", 23 24 "eslint-plugin-svelte": "^2.36.0", 24 25 "globals": "^15.0.0",
+4 -3
postcss.config.js
··· 3 3 'postcss-import': {}, 4 4 'tailwindcss/nesting': {}, 5 5 tailwindcss: {}, 6 - autoprefixer: {} 7 - } 8 - }; 6 + autoprefixer: {}, 7 + ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}) 8 + }, 9 + }
+87
src/app.html
··· 15 15 content="%sveltekit.assets%/favicon/ms-icon-144x144.png" 16 16 /> 17 17 18 + <!-- Preload critical fonts --> 19 + <link 20 + rel="preload" 21 + href="%sveltekit.assets%/fonts/ArrowType-Recursive-1.085/Recursive_Web/woff2_variable/Recursive_VF_1.085.woff2" 22 + as="font" 23 + type="font/woff2" 24 + crossorigin 25 + /> 26 + 18 27 <!-- THEME LOADER - MUST BE FIRST SCRIPT --> 19 28 <script src="%sveltekit.assets%/scripts/themeLoader.js"></script> 29 + 30 + <!-- Service Worker Registration --> 31 + <script> 32 + // Enhanced debugging for service worker registration 33 + console.log('🔍 Service Worker registration script loaded'); 34 + console.log('📍 Current location:', window.location.href); 35 + console.log('🌐 Protocol:', window.location.protocol); 36 + console.log('🏠 Hostname:', window.location.hostname); 37 + 38 + if ('serviceWorker' in navigator) { 39 + console.log('✅ Service Worker API supported'); 40 + 41 + // Check for existing service workers 42 + navigator.serviceWorker.getRegistrations().then(registrations => { 43 + console.log('📋 Existing service worker registrations:', registrations.length); 44 + registrations.forEach((registration, index) => { 45 + console.log(` ${index + 1}. Scope: ${registration.scope}, Active: ${!!registration.active}`); 46 + }); 47 + }); 48 + 49 + window.addEventListener('load', () => { 50 + console.log('🚀 Page loaded, attempting to register service worker at /scripts/sw.js'); 51 + 52 + // Test if the service worker file is accessible 53 + fetch('/scripts/sw.js') 54 + .then(response => { 55 + console.log('✅ Service worker file accessible:', { 56 + status: response.status, 57 + statusText: response.statusText, 58 + url: response.url 59 + }); 60 + }) 61 + .catch(error => { 62 + console.error('❌ Service worker file not accessible:', error); 63 + }); 64 + 65 + navigator.serviceWorker.register('/scripts/sw.js') 66 + .then((registration) => { 67 + console.log('✅ Service Worker registered successfully:', { 68 + scope: registration.scope, 69 + active: !!registration.active, 70 + waiting: !!registration.waiting, 71 + installing: !!registration.installing 72 + }); 73 + 74 + // Listen for updates 75 + registration.addEventListener('updatefound', () => { 76 + console.log('🔄 Service Worker update found'); 77 + const newWorker = registration.installing; 78 + 79 + newWorker.addEventListener('statechange', () => { 80 + if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { 81 + console.log('🆕 New Service Worker installed and ready'); 82 + } 83 + }); 84 + }); 85 + }) 86 + .catch((registrationError) => { 87 + console.error('❌ Service Worker registration failed:', { 88 + error: registrationError.message, 89 + name: registrationError.name, 90 + stack: registrationError.stack 91 + }); 92 + 93 + // Log additional debugging info 94 + console.log('🔍 Debug info:', { 95 + location: window.location.href, 96 + userAgent: navigator.userAgent, 97 + serviceWorkerSupported: 'serviceWorker' in navigator, 98 + https: window.location.protocol === 'https:', 99 + localhost: window.location.hostname === 'localhost' 100 + }); 101 + }); 102 + }); 103 + } else { 104 + console.warn('⚠️ Service Worker not supported in this browser'); 105 + } 106 + </script> 20 107 21 108 <!-- Favicon and Icons --> 22 109 <link rel="icon" href="%sveltekit.assets%/favicon/favicon-32x32.png" />
+24 -4
src/lib/components/profile/profile.ts
··· 4 4 5 5 export async function safeFetch(url: string, fetch: typeof globalThis.fetch) { 6 6 try { 7 - const response = await fetch(url); 8 - if (!response.ok) 9 - throw new Error(`HTTP error! status: ${response.status}, statusText: ${response.statusText}`); 10 - return await response.json(); 7 + // Add timeout to prevent hanging requests 8 + const controller = new AbortController(); 9 + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout 10 + 11 + try { 12 + const response = await fetch(url, { 13 + signal: controller.signal, 14 + headers: { 15 + 'Accept': 'application/json' 16 + } 17 + }); 18 + 19 + clearTimeout(timeoutId); 20 + 21 + if (!response.ok) 22 + throw new Error(`HTTP error! status: ${response.status}, statusText: ${response.statusText}`); 23 + return await response.json(); 24 + } catch (error) { 25 + clearTimeout(timeoutId); 26 + if (error.name === 'AbortError') { 27 + throw new Error(`Request timed out for ${url}`); 28 + } 29 + throw error; 30 + } 11 31 } catch (error: unknown) { 12 32 // Catch network errors (e.g., connection refused, timeout) 13 33 console.error(`Network error fetching ${url}:`, error);
+8 -3
src/lib/css/app.css
··· 1 1 @import "$css/variables.css"; 2 2 @import "$css/animations.css"; 3 3 4 - /* KaTeX CSS for rendering mathematical expressions */ 5 - @import url('https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css'); 6 - 7 4 /* Minimalist flat styles with gentle dark pastel green theme */ 8 5 @tailwind base; 9 6 @tailwind components; ··· 15 12 font-weight: 300 800; 16 13 font-stretch: 100%; 17 14 font-display: swap; 15 + font-preload: true; 18 16 } 17 + 18 + /* KaTeX CSS - will be loaded dynamically when needed */ 19 + /* @import url('https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css'); */ 19 20 20 21 @layer base { 21 22 ··· 48 49 font-family: "Recursive", sans-serif; 49 50 font-variation-settings: "MONO" 0, "CASL" 0, "wght" 300, "slnt" 0, 50 51 "CRSV" 0.5; 52 + /* Optimize font rendering */ 53 + -webkit-font-smoothing: antialiased; 54 + -moz-osx-font-smoothing: grayscale; 55 + text-rendering: optimizeLegibility; 51 56 } 52 57 53 58 /* This is to ensure KaTeX renders correctly.
+146 -381
src/lib/server/ogImage.ts
··· 1 - import satori from 'satori'; 2 - import { Resvg } from '@resvg/resvg-js'; 3 1 import { dev } from '$app/environment'; 4 - 5 - // Font URLs for production (served from static folder) 6 - const FONT_BASE_URL = '/fonts/ArrowType-Recursive-1.085/Recursive_Desktop/separate_statics/TTF'; 7 - const FONT_FILES = { 8 - regular: 'RecursiveSansCslSt-Regular.ttf', 9 - bold: 'RecursiveSansCslSt-Bold.ttf', 10 - italic: 'RecursiveSansCslSt-Italic.ttf' 11 - }; 12 - 13 - // Preload fonts (cache in memory for performance) 14 - let fontCache: { regular?: Buffer; bold?: Buffer; italic?: Buffer } = {}; 15 - 16 - async function loadSingleFont(fileName: string, baseUrl?: string): Promise<Buffer> { 17 - try { 18 - if (dev) { 19 - // In development, try to load from filesystem first 20 - const fs = await import('fs/promises'); 21 - const path = await import('path'); 22 - const fontPath = path.resolve(`static${FONT_BASE_URL}/${fileName}`); 23 - console.log(`Loading font from filesystem: ${fontPath}`); 24 - return await fs.readFile(fontPath); 25 - } else { 26 - // In production, fetch from the served static files 27 - const fontUrl = `${baseUrl || ''}${FONT_BASE_URL}/${fileName}`; 28 - console.log(`Fetching font from URL: ${fontUrl}`); 29 - 30 - const response = await fetch(fontUrl); 31 - if (!response.ok) { 32 - throw new Error(`Failed to fetch font: ${response.status} ${response.statusText}`); 33 - } 34 - 35 - const arrayBuffer = await response.arrayBuffer(); 36 - return Buffer.from(arrayBuffer); 37 - } 38 - } catch (error) { 39 - console.error(`Failed to load font ${fileName}:`, error); 40 - throw error; 41 - } 42 - } 2 + import { createFileDebugger } from '../utils/debug.js'; 43 3 44 - async function loadFonts(baseUrl?: string) { 45 - try { 46 - // Load fonts if not already cached 47 - if (!fontCache.regular) { 48 - fontCache.regular = await loadSingleFont(FONT_FILES.regular, baseUrl); 49 - } 50 - if (!fontCache.bold) { 51 - fontCache.bold = await loadSingleFont(FONT_FILES.bold, baseUrl); 52 - } 53 - if (!fontCache.italic) { 54 - fontCache.italic = await loadSingleFont(FONT_FILES.italic, baseUrl); 55 - } 56 - 57 - // Defensive: throw if any font is missing 58 - if (!fontCache.regular || !fontCache.bold || !fontCache.italic) { 59 - throw new Error('Failed to load all required font files for OG image'); 60 - } 61 - 62 - return fontCache as { regular: Buffer; bold: Buffer; italic: Buffer }; 63 - } catch (error) { 64 - console.error('Font loading error:', error); 65 - throw error; 66 - } 67 - } 4 + const debug = createFileDebugger('ogImage.ts'); 68 5 69 6 export interface OgImageOptions { 70 7 title: string; ··· 82 19 } 83 20 84 21 /** 85 - * Calculate optimal font size based on text length and available space 22 + * Generate OG image with dynamic sizing and layout 23 + * @param options OgImageOptions 24 + * @param baseUrl Optional base URL for font loading 25 + * @returns Promise<Buffer> PNG image buffer 86 26 */ 87 - function calculateTitleFontSize(title: string, maxWidth: number = 1000): number { 88 - const baseSize = 64; 89 - const charThreshold = 45; 90 - const minSize = 36; 91 - 92 - if (title.length <= charThreshold) { 93 - return baseSize; 94 - } 95 - 96 - const scaleFactor = Math.max(0.5, 1 - ((title.length - charThreshold) * 0.012)); 97 - return Math.max(minSize, Math.floor(baseSize * scaleFactor)); 98 - } 99 - 100 - /** 101 - * Calculate optimal subtitle font size 102 - */ 103 - function calculateSubtitleFontSize(subtitle: string, titleSize: number): number { 104 - const baseRatio = 0.57; 105 - const charThreshold = 80; 106 - const minSize = 20; 107 - 108 - let size = Math.floor(titleSize * baseRatio); 27 + export async function generateOgImage(options: OgImageOptions, baseUrl?: string): Promise<Buffer> { 28 + debug.enter('generateOgImage', { 29 + title: options.title?.substring(0, 50) + (options.title?.length > 50 ? '...' : ''), 30 + hasSubtitle: !!options.subtitle, 31 + hasAuthor: !!options.author, 32 + hasExtraMeta: !!options.extraMeta?.length, 33 + baseUrl: baseUrl || 'none' 34 + }); 109 35 110 - if (subtitle.length > charThreshold) { 111 - const scaleFactor = Math.max(0.7, 1 - ((subtitle.length - charThreshold) * 0.01)); 112 - size = Math.floor(size * scaleFactor); 113 - } 36 + const timer = debug.time('generateOgImage'); 114 37 115 - return Math.max(minSize, size); 116 - } 38 + try { 39 + debug.info('Starting OG image generation', { 40 + titleLength: options.title?.length, 41 + subtitleLength: options.subtitle?.length, 42 + authorName: options.author?.name, 43 + extraMetaCount: options.extraMeta?.length || 0 44 + }); 117 45 118 - /** 119 - * Estimate content height to ensure it fits 120 - */ 121 - function estimateContentHeight(options: OgImageOptions): { 122 - titleHeight: number; 123 - subtitleHeight: number; 124 - metaHeight: number; 125 - authorHeight: number; 126 - totalContentHeight: number; 127 - } { 128 - const titleFontSize = calculateTitleFontSize(options.title); 129 - const titleLines = Math.ceil(options.title.length / 40); 130 - const titleHeight = titleLines * titleFontSize * 1.25 + 32; 131 - 132 - let subtitleHeight = 0; 133 - if (options.subtitle) { 134 - const subtitleFontSize = calculateSubtitleFontSize(options.subtitle, titleFontSize); 135 - const subtitleLines = Math.ceil(options.subtitle.length / 60); 136 - subtitleHeight = subtitleLines * subtitleFontSize * 1.2 + 24; 137 - } 138 - 139 - let metaHeight = 0; 140 - if (options.metaLine || (options.extraMeta && options.extraMeta.length > 0)) { 141 - metaHeight = 32 + 24; 46 + // Dynamically import the actual implementation to avoid build-time analysis 47 + debug.debug('Attempting to import OG image implementation'); 48 + const { generateOgImageImpl } = await import('./ogImageImpl'); 49 + 50 + debug.info('Successfully imported OG image implementation, calling main function'); 51 + const result = await generateOgImageImpl(options, baseUrl); 52 + 53 + debug.info('OG image generation completed successfully', { 54 + resultType: typeof result, 55 + resultLength: result?.length, 56 + resultIsBuffer: Buffer.isBuffer(result) 57 + }); 58 + 59 + timer(); 60 + debug.exit('generateOgImage', { 61 + success: true, 62 + resultType: typeof result, 63 + resultLength: result?.length 64 + }); 65 + 66 + return result; 67 + } catch (error) { 68 + debug.errorWithContext('Failed to generate OG image with main implementation', error as Error, { 69 + function: 'generateOgImage', 70 + options: { 71 + title: options.title?.substring(0, 100), 72 + hasSubtitle: !!options.subtitle, 73 + hasAuthor: !!options.author 74 + } 75 + }); 76 + 77 + debug.info('Falling back to SVG-based OG image generation'); 78 + 79 + try { 80 + // Fallback: create a simple SVG-based OG image without external dependencies 81 + const fallbackSvg = createFallbackSvg(options); 82 + const svgBuffer = Buffer.from(fallbackSvg, 'utf-8'); 83 + 84 + debug.info('Fallback SVG generation successful', { 85 + svgLength: fallbackSvg.length, 86 + bufferLength: svgBuffer.length, 87 + fallbackType: 'svg' 88 + }); 89 + 90 + timer(); 91 + debug.exit('generateOgImage', { 92 + success: true, 93 + fallback: true, 94 + resultType: 'svg', 95 + resultLength: svgBuffer.length 96 + }); 97 + 98 + // For now, return SVG as-is. In production, you might want to convert this to PNG 99 + // or handle the error differently 100 + return svgBuffer; 101 + } catch (fallbackError) { 102 + debug.errorWithContext('Fallback SVG generation also failed', fallbackError as Error, { 103 + function: 'generateOgImage', 104 + fallback: true 105 + }); 106 + 107 + timer(); 108 + debug.exit('generateOgImage', { 109 + success: false, 110 + fallback: true, 111 + error: fallbackError 112 + }); 113 + 114 + throw fallbackError; 115 + } 142 116 } 143 - 144 - let authorHeight = options.author ? 120 : 28; 145 - 146 - return { 147 - titleHeight, 148 - subtitleHeight, 149 - metaHeight, 150 - authorHeight, 151 - totalContentHeight: titleHeight + subtitleHeight + metaHeight + authorHeight 152 - }; 153 117 } 154 118 155 119 /** 156 - * Generates a PNG buffer for an OG image with unified styling. 157 - * @param options OgImageOptions 158 - * @param baseUrl Base URL for fetching fonts in production (e.g., 'https://ewancroft.uk') 159 - * @returns Buffer (PNG) 120 + * Create a fallback SVG-based OG image when the main generator fails 160 121 */ 161 - export async function generateOgImage(options: OgImageOptions, baseUrl?: string): Promise<Buffer> { 162 - const fonts = await loadFonts(baseUrl); 122 + function createFallbackSvg(options: OgImageOptions): string { 123 + debug.enter('createFallbackSvg', { 124 + title: options.title?.substring(0, 30), 125 + hasSubtitle: !!options.subtitle 126 + }); 163 127 164 - // Calculate optimal sizing 165 - const titleFontSize = calculateTitleFontSize(options.title); 166 - const subtitleFontSize = options.subtitle ? calculateSubtitleFontSize(options.subtitle, titleFontSize) : 0; 167 - 168 - // Estimate if content will fit and adjust if needed 169 - const contentEstimate = estimateContentHeight(options); 170 - const availableHeight = 630 - 96; 171 - 172 - let titleMarginBottom = 32; 173 - let subtitleMarginBottom = 24; 174 - let metaMarginBottom = 24; 175 - 176 - if (contentEstimate.totalContentHeight > availableHeight) { 177 - const compressionRatio = availableHeight / contentEstimate.totalContentHeight; 178 - titleMarginBottom = Math.max(8, Math.floor(titleMarginBottom * compressionRatio)); 179 - subtitleMarginBottom = Math.max(6, Math.floor(subtitleMarginBottom * compressionRatio)); 180 - metaMarginBottom = Math.max(8, Math.floor(metaMarginBottom * compressionRatio)); 181 - } 182 - 183 - // Layout children 184 - let children: any[] = []; 185 - 186 - // Title with dynamic sizing 187 - children.push({ 188 - type: 'h1', 189 - props: { 190 - style: { 191 - fontSize: `${titleFontSize}px`, 192 - fontWeight: 700, 193 - fontStyle: 'normal', 194 - margin: `0 0 ${titleMarginBottom}px 0`, 195 - textAlign: 'center', 196 - lineHeight: 1.25, 197 - maxWidth: '1000px', 198 - overflow: 'hidden', 199 - display: 'flex', 200 - alignItems: 'center', 201 - justifyContent: 'center', 202 - textWrap: 'balance', 203 - hyphens: 'auto', 204 - paddingLeft: '48px', 205 - paddingRight: '48px', 206 - }, 207 - children: options.title, 208 - }, 209 - }); 210 - 211 - // Subtitle/description with dynamic sizing 212 - if (options.subtitle) { 213 - children.push({ 214 - type: 'div', 215 - props: { 216 - style: { 217 - fontSize: `${subtitleFontSize}px`, 218 - fontWeight: 400, 219 - margin: `0 0 ${subtitleMarginBottom}px 0`, 220 - textAlign: 'center', 221 - opacity: 0.8, 222 - maxWidth: '900px', 223 - lineHeight: 1.3, 224 - textWrap: 'balance', 225 - paddingLeft: '48px', 226 - paddingRight: '48px', 227 - }, 228 - children: options.subtitle, 229 - }, 128 + try { 129 + const title = options.title || 'OG Image'; 130 + const subtitle = options.subtitle || ''; 131 + 132 + debug.debug('Creating fallback SVG', { 133 + title, 134 + subtitle, 135 + titleLength: title.length, 136 + subtitleLength: subtitle.length 137 + }); 138 + 139 + const svg = `<?xml version="1.0" encoding="UTF-8"?> 140 + <svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg"> 141 + <defs> 142 + <style> 143 + .title { font-family: Arial, sans-serif; font-size: 48px; font-weight: bold; fill: #ffffff; } 144 + .subtitle { font-family: Arial, sans-serif; font-size: 24px; fill: #8fd0a0; } 145 + </style> 146 + </defs> 147 + <rect width="1200" height="630" fill="#0f1a0f"/> 148 + <text x="600" y="300" text-anchor="middle" class="title">${title}</text> 149 + ${subtitle ? `<text x="600" y="350" text-anchor="middle" class="subtitle">${subtitle}</text>` : ''} 150 + </svg>`; 151 + 152 + debug.debug('Fallback SVG created successfully', { 153 + svgLength: svg.length, 154 + hasSubtitle: !!subtitle 155 + }); 156 + 157 + debug.exit('createFallbackSvg', { 158 + success: true, 159 + svgLength: svg.length 230 160 }); 231 - } 232 - 233 - // Meta line (e.g., reading time, word count) 234 - if (options.metaLine || (options.extraMeta && options.extraMeta.length > 0)) { 235 - children.push({ 236 - type: 'div', 237 - props: { 238 - style: { 239 - fontSize: '24px', 240 - opacity: 0.75, 241 - margin: `0 0 ${metaMarginBottom}px 0`, 242 - display: 'flex', 243 - flexDirection: 'row', 244 - alignItems: 'center', 245 - justifyContent: 'center', 246 - flexWrap: 'wrap', 247 - gap: '8px', 248 - }, 249 - children: options.metaLine || options.extraMeta?.join(' • '), 250 - }, 161 + 162 + return svg; 163 + } catch (error) { 164 + debug.errorWithContext('Failed to create fallback SVG', error as Error, { 165 + function: 'createFallbackSvg' 251 166 }); 252 - } 253 - 254 - // Custom children (for advanced layouts) 255 - if (options.customChildren) { 256 - children.push(options.customChildren); 257 - } 258 - 259 - // Decorative line (optional, for index pages) - only if no author 260 - if (!options.author && !options.customChildren) { 261 - children.push({ 262 - type: 'div', 263 - props: { 264 - style: { 265 - width: '120px', 266 - height: '4px', 267 - background: '#2d4839', 268 - borderRadius: '2px', 269 - margin: '0 auto', 270 - flexShrink: 0, 271 - }, 272 - }, 167 + 168 + debug.exit('createFallbackSvg', { 169 + success: false, 170 + error: error 273 171 }); 172 + 173 + // Return a minimal error SVG if even the fallback fails 174 + return `<?xml version="1.0" encoding="UTF-8"?> 175 + <svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg"> 176 + <rect width="1200" height="630" fill="#ff0000"/> 177 + <text x="600" y="315" text-anchor="middle" font-family="Arial" font-size="24" fill="white">OG Image Generation Failed</text> 178 + </svg>`; 274 179 } 275 - 276 - // Create main container with efficient spacing 277 - const mainContainer = { 278 - type: 'div', 279 - props: { 280 - style: { 281 - width: '1200px', 282 - height: '630px', 283 - display: 'flex', 284 - flexDirection: 'column', 285 - alignItems: 'center', 286 - justifyContent: 'space-between', 287 - background: '#121c17', 288 - color: '#d8e8d8', 289 - fontFamily: 'Recursive', 290 - position: 'relative', 291 - padding: options.author ? '60px 0 48px 0' : '48px 0', 292 - boxSizing: 'border-box', 293 - }, 294 - children: [ 295 - // Main content container - takes up most space 296 - { 297 - type: 'div', 298 - props: { 299 - style: { 300 - display: 'flex', 301 - flexDirection: 'column', 302 - alignItems: 'center', 303 - justifyContent: 'center', 304 - width: '100%', 305 - flex: '1', 306 - }, 307 - children, 308 - }, 309 - }, 310 - // Author info at bottom 311 - options.author && { 312 - type: 'div', 313 - props: { 314 - style: { 315 - width: '100%', 316 - display: 'flex', 317 - flexDirection: 'row', 318 - alignItems: 'center', 319 - justifyContent: 'center', 320 - gap: '18px', 321 - textAlign: 'center', 322 - opacity: 0.85, 323 - flexShrink: 0, 324 - }, 325 - children: [ 326 - options.author.avatar && { 327 - type: 'img', 328 - props: { 329 - src: options.author.avatar, 330 - width: 64, 331 - height: 64, 332 - style: { 333 - borderRadius: '50%', 334 - border: '3px solid #2d4839', 335 - boxShadow: '0 2px 12px rgba(0,0,0,0.18)', 336 - background: '#1e2c23', 337 - flexShrink: 0, 338 - }, 339 - }, 340 - }, 341 - { 342 - type: 'div', 343 - props: { 344 - style: { 345 - display: 'flex', 346 - flexDirection: 'column', 347 - alignItems: 'flex-start', 348 - gap: '2px', 349 - }, 350 - children: [ 351 - { 352 - type: 'span', 353 - props: { 354 - style: { 355 - fontSize: '22px', 356 - fontWeight: 700, 357 - fontStyle: 'normal', 358 - lineHeight: 1.1, 359 - }, 360 - children: options.author.name, 361 - }, 362 - }, 363 - options.author.handle && { 364 - type: 'span', 365 - props: { 366 - style: { 367 - fontSize: '16px', 368 - color: '#8fd0a0', 369 - fontWeight: 400, 370 - fontStyle: 'italic', 371 - }, 372 - children: '@' + options.author.handle, 373 - }, 374 - }, 375 - ].filter(Boolean), 376 - }, 377 - }, 378 - ].filter(Boolean), 379 - }, 380 - }, 381 - ].filter(Boolean), 382 - }, 383 - }; 384 - 385 - // Compose SVG 386 - const svg = await satori(mainContainer, { 387 - width: 1200, 388 - height: 630, 389 - fonts: [ 390 - { 391 - name: 'Recursive', 392 - data: fonts.regular, 393 - weight: 400, 394 - style: 'normal', 395 - }, 396 - { 397 - name: 'Recursive', 398 - data: fonts.bold, 399 - weight: 700, 400 - style: 'normal', 401 - }, 402 - { 403 - name: 'Recursive', 404 - data: fonts.italic, 405 - weight: 400, 406 - style: 'italic', 407 - }, 408 - ], 409 - }); 410 - 411 - // Convert SVG to PNG 412 - const resvg = new Resvg(svg, { fitTo: { mode: 'width', value: 1200 } }); 413 - const png = resvg.render(); 414 - return png.asPng(); 415 180 }
+629
src/lib/server/ogImageImpl.ts
··· 1 + import { dev } from '$app/environment'; 2 + import { createFileDebugger } from '../utils/debug.js'; 3 + 4 + const debug = createFileDebugger('ogImageImpl.ts'); 5 + 6 + // Font URLs for production (served from static folder) 7 + const FONT_BASE_URL = '/fonts/ArrowType-Recursive-1.085/Recursive_Desktop/separate_statics/TTF'; 8 + const FONT_FILES = { 9 + regular: 'RecursiveSansCslSt-Regular.ttf', 10 + bold: 'RecursiveSansCslSt-Bold.ttf', 11 + italic: 'RecursiveSansCslSt-Italic.ttf' 12 + }; 13 + 14 + // Preload fonts (cache in memory for performance) 15 + let fontCache: { regular?: Buffer; bold?: Buffer; italic?: Buffer } = {}; 16 + 17 + async function loadSingleFont(fileName: string, baseUrl?: string): Promise<Buffer> { 18 + debug.enter('loadSingleFont', { fileName, baseUrl: baseUrl || 'none' }); 19 + 20 + try { 21 + if (dev) { 22 + // In development, try to load from filesystem first 23 + debug.debug('Loading font from filesystem in development mode'); 24 + const fs = await import('fs/promises'); 25 + const path = await import('path'); 26 + const fontPath = path.resolve(`static${FONT_BASE_URL}/${fileName}`); 27 + 28 + debug.debug('Font filesystem path resolved', { fontPath }); 29 + console.log(`Loading font from filesystem: ${fontPath}`); 30 + 31 + const fontBuffer = await fs.readFile(fontPath); 32 + debug.debug('Font loaded successfully from filesystem', { 33 + fileName, 34 + bufferLength: fontBuffer.length 35 + }); 36 + 37 + debug.exit('loadSingleFont', { 38 + success: true, 39 + source: 'filesystem', 40 + bufferLength: fontBuffer.length 41 + }); 42 + 43 + return fontBuffer; 44 + } else { 45 + // In production, fetch from the served static files 46 + debug.debug('Loading font from URL in production mode'); 47 + const fontUrl = `${baseUrl || ''}${FONT_BASE_URL}/${fileName}`; 48 + 49 + debug.debug('Font URL constructed', { fontUrl }); 50 + console.log(`Fetching font from URL: ${fontUrl}`); 51 + 52 + const response = await fetch(fontUrl); 53 + if (!response.ok) { 54 + const error = new Error(`Failed to fetch font: ${response.status} ${response.statusText}`); 55 + debug.error('Font fetch failed', { 56 + fileName, 57 + fontUrl, 58 + status: response.status, 59 + statusText: response.statusText 60 + }); 61 + throw error; 62 + } 63 + 64 + const arrayBuffer = await response.arrayBuffer(); 65 + const fontBuffer = Buffer.from(arrayBuffer); 66 + 67 + debug.debug('Font loaded successfully from URL', { 68 + fileName, 69 + fontUrl, 70 + arrayBufferLength: arrayBuffer.byteLength, 71 + bufferLength: fontBuffer.length 72 + }); 73 + 74 + debug.exit('loadSingleFont', { 75 + success: true, 76 + source: 'url', 77 + bufferLength: fontBuffer.length 78 + }); 79 + 80 + return fontBuffer; 81 + } 82 + } catch (error) { 83 + debug.errorWithContext(`Failed to load font ${fileName}`, error as Error, { 84 + function: 'loadSingleFont', 85 + fileName, 86 + baseUrl, 87 + dev 88 + }); 89 + 90 + debug.exit('loadSingleFont', { 91 + success: false, 92 + fileName, 93 + error: error 94 + }); 95 + 96 + throw error; 97 + } 98 + } 99 + 100 + async function loadFonts(baseUrl?: string) { 101 + debug.enter('loadFonts', { baseUrl: baseUrl || 'none' }); 102 + const timer = debug.time('loadFonts'); 103 + 104 + try { 105 + debug.info('Starting font loading process', { 106 + baseUrl, 107 + cacheStatus: { 108 + regular: !!fontCache.regular, 109 + bold: !!fontCache.bold, 110 + italic: !!fontCache.italic 111 + } 112 + }); 113 + 114 + // Load fonts if not already cached 115 + if (!fontCache.regular) { 116 + debug.debug('Loading regular font'); 117 + fontCache.regular = await loadSingleFont(FONT_FILES.regular, baseUrl); 118 + } else { 119 + debug.debug('Regular font already cached'); 120 + } 121 + 122 + if (!fontCache.bold) { 123 + debug.debug('Loading bold font'); 124 + fontCache.bold = await loadSingleFont(FONT_FILES.bold, baseUrl); 125 + } else { 126 + debug.debug('Bold font already cached'); 127 + } 128 + 129 + if (!fontCache.italic) { 130 + debug.debug('Loading italic font'); 131 + fontCache.italic = await loadSingleFont(FONT_FILES.italic, baseUrl); 132 + } else { 133 + debug.debug('Italic font already cached'); 134 + } 135 + 136 + // Defensive: throw if any font is missing 137 + if (!fontCache.regular || !fontCache.bold || !fontCache.italic) { 138 + const error = new Error('Failed to load all required font files for OG image'); 139 + debug.error('Font validation failed', { 140 + regular: !!fontCache.regular, 141 + bold: !!fontCache.bold, 142 + italic: !!fontCache.italic 143 + }); 144 + throw error; 145 + } 146 + 147 + debug.info('All fonts loaded successfully', { 148 + regularLength: fontCache.regular?.length, 149 + boldLength: fontCache.bold?.length, 150 + italicLength: fontCache.italic?.length 151 + }); 152 + 153 + timer(); 154 + debug.exit('loadFonts', { 155 + success: true, 156 + fontCount: 3, 157 + totalSize: (fontCache.regular?.length || 0) + (fontCache.bold?.length || 0) + (fontCache.italic?.length || 0) 158 + }); 159 + 160 + return fontCache as { regular: Buffer; bold: Buffer; italic: Buffer }; 161 + } catch (error) { 162 + debug.errorWithContext('Font loading error', error as Error, { 163 + function: 'loadFonts', 164 + baseUrl 165 + }); 166 + 167 + timer(); 168 + debug.exit('loadFonts', { 169 + success: false, 170 + error: error 171 + }); 172 + 173 + throw error; 174 + } 175 + } 176 + 177 + export interface OgImageOptions { 178 + title: string; 179 + subtitle?: string; 180 + metaLine?: string; 181 + author?: { 182 + name: string; 183 + handle?: string; 184 + avatar?: string; 185 + }; 186 + // For blog posts: wordCount, readingTime, date, etc. 187 + extraMeta?: string[]; 188 + // For custom layouts 189 + customChildren?: any; 190 + } 191 + 192 + /** 193 + * Calculate optimal font size based on text length and available space 194 + */ 195 + function calculateTitleFontSize(title: string, maxWidth: number = 1000): number { 196 + debug.enter('calculateTitleFontSize', { titleLength: title.length, maxWidth }); 197 + 198 + const baseSize = 64; 199 + const charThreshold = 45; 200 + const minSize = 36; 201 + 202 + let fontSize: number; 203 + 204 + if (title.length <= charThreshold) { 205 + fontSize = baseSize; 206 + debug.debug('Title within threshold, using base size', { 207 + titleLength: title.length, 208 + charThreshold, 209 + fontSize 210 + }); 211 + } else { 212 + const scaleFactor = Math.max(0.5, 1 - ((title.length - charThreshold) * 0.012)); 213 + fontSize = Math.max(minSize, Math.floor(baseSize * scaleFactor)); 214 + debug.debug('Title exceeds threshold, calculating scaled size', { 215 + titleLength: title.length, 216 + charThreshold, 217 + scaleFactor, 218 + fontSize 219 + }); 220 + } 221 + 222 + debug.exit('calculateTitleFontSize', { 223 + success: true, 224 + titleLength: title.length, 225 + fontSize 226 + }); 227 + 228 + return fontSize; 229 + } 230 + 231 + /** 232 + * Calculate subtitle font size based on title size 233 + */ 234 + function calculateSubtitleFontSize(subtitle: string, titleSize: number): number { 235 + debug.enter('calculateSubtitleFontSize', { subtitleLength: subtitle.length, titleSize }); 236 + 237 + const baseRatio = 0.6; 238 + const minSize = 24; 239 + const maxSize = 48; 240 + 241 + const calculatedSize = Math.floor(titleSize * baseRatio); 242 + const fontSize = Math.max(minSize, Math.min(maxSize, calculatedSize)); 243 + 244 + debug.debug('Subtitle font size calculated', { 245 + subtitleLength: subtitle.length, 246 + titleSize, 247 + baseRatio, 248 + calculatedSize, 249 + fontSize, 250 + constrained: fontSize !== calculatedSize 251 + }); 252 + 253 + debug.exit('calculateSubtitleFontSize', { 254 + success: true, 255 + fontSize 256 + }); 257 + 258 + return fontSize; 259 + } 260 + 261 + /** 262 + * Estimate content height for proper vertical centering 263 + */ 264 + function estimateContentHeight(options: OgImageOptions): { 265 + titleHeight: number; 266 + subtitleHeight: number; 267 + metaHeight: number; 268 + authorHeight: number; 269 + totalContentHeight: number; 270 + } { 271 + debug.enter('estimateContentHeight', { 272 + title: options.title?.substring(0, 30), 273 + hasSubtitle: !!options.subtitle, 274 + hasMeta: !!(options.metaLine || options.extraMeta?.length), 275 + hasAuthor: !!options.author 276 + }); 277 + 278 + const titleSize = calculateTitleFontSize(options.title); 279 + const titleHeight = titleSize * 1.2; // Line height factor 280 + 281 + let subtitleHeight = 0; 282 + if (options.subtitle) { 283 + const subtitleSize = calculateSubtitleFontSize(options.subtitle, titleSize); 284 + subtitleHeight = subtitleSize * 1.3 + 16; // Line height + margin 285 + } 286 + 287 + let metaHeight = 0; 288 + if (options.metaLine || options.extraMeta?.length) { 289 + metaHeight = 32 + 8; // Base height + margin 290 + } 291 + 292 + let authorHeight = 0; 293 + if (options.author) { 294 + authorHeight = 80 + 24; // Avatar + name + margins 295 + } 296 + 297 + const totalContentHeight = titleHeight + subtitleHeight + metaHeight + authorHeight; 298 + 299 + debug.debug('Content height estimation completed', { 300 + titleSize, 301 + titleHeight, 302 + subtitleHeight, 303 + metaHeight, 304 + authorHeight, 305 + totalContentHeight 306 + }); 307 + 308 + debug.exit('estimateContentHeight', { 309 + success: true, 310 + totalContentHeight 311 + }); 312 + 313 + return { 314 + titleHeight, 315 + subtitleHeight, 316 + metaHeight, 317 + authorHeight, 318 + totalContentHeight 319 + }; 320 + } 321 + 322 + /** 323 + * Generate OG image with dynamic sizing and layout 324 + * @param options OgImageOptions 325 + * @param baseUrl Optional base URL for font loading 326 + * @returns Promise<Buffer> PNG image buffer 327 + */ 328 + export async function generateOgImageImpl(options: OgImageOptions, baseUrl?: string): Promise<Buffer> { 329 + debug.enter('generateOgImageImpl', { 330 + title: options.title?.substring(0, 50) + (options.title?.length > 50 ? '...' : ''), 331 + hasSubtitle: !!options.subtitle, 332 + hasAuthor: !!options.author, 333 + baseUrl: baseUrl || 'none' 334 + }); 335 + 336 + const totalTimer = debug.time('generateOgImageImpl'); 337 + 338 + try { 339 + debug.info('Starting OG image implementation', { 340 + titleLength: options.title?.length, 341 + subtitleLength: options.subtitle?.length, 342 + extraMetaCount: options.extraMeta?.length || 0 343 + }); 344 + 345 + // Load dependencies only when this function is called 346 + debug.debug('Loading external dependencies'); 347 + const depTimer = debug.time('loadDependencies'); 348 + 349 + const [satori, { Resvg }] = await Promise.all([ 350 + import('satori').then(module => { 351 + debug.debug('Satori module loaded', { hasDefault: !!module.default }); 352 + return module.default; 353 + }), 354 + import('@resvg/resvg-js').then(module => { 355 + debug.debug('Resvg module loaded', { hasResvg: !!module.Resvg }); 356 + return module; 357 + }) 358 + ]); 359 + 360 + depTimer(); 361 + debug.info('External dependencies loaded successfully'); 362 + 363 + // Load fonts 364 + debug.debug('Loading fonts'); 365 + const fonts = await loadFonts(baseUrl); 366 + 367 + // Calculate layout 368 + debug.debug('Calculating content layout'); 369 + const contentHeight = estimateContentHeight(options); 370 + 371 + // Calculate vertical positioning for centering 372 + const totalHeight = 630; 373 + const startY = Math.max(80, (totalHeight - contentHeight.totalContentHeight) / 2); 374 + 375 + debug.debug('Layout calculations completed', { 376 + totalHeight, 377 + contentHeight: contentHeight.totalContentHeight, 378 + startY 379 + }); 380 + 381 + // Build the main container with dynamic positioning 382 + debug.debug('Building SVG container structure'); 383 + const mainContainer = { 384 + type: 'div', 385 + props: { 386 + style: { 387 + display: 'flex', 388 + flexDirection: 'column', 389 + width: '100%', 390 + height: '100%', 391 + backgroundColor: '#0f1a0f', 392 + color: '#e8f5e8', 393 + fontFamily: 'Recursive', 394 + padding: '80px', 395 + boxSizing: 'border-box', 396 + position: 'relative', 397 + }, 398 + children: [ 399 + // Title 400 + { 401 + type: 'h1', 402 + props: { 403 + style: { 404 + fontSize: `${calculateTitleFontSize(options.title)}px`, 405 + fontWeight: 700, 406 + lineHeight: 1.1, 407 + margin: '0 0 16px 0', 408 + textAlign: 'center', 409 + color: '#ffffff', 410 + textShadow: '0 2px 8px rgba(0,0,0,0.3)', 411 + }, 412 + children: options.title, 413 + }, 414 + }, 415 + // Subtitle 416 + options.subtitle && { 417 + type: 'h2', 418 + props: { 419 + style: { 420 + fontSize: `${calculateSubtitleFontSize(options.subtitle, calculateTitleFontSize(options.title))}px`, 421 + fontWeight: 400, 422 + lineHeight: 1.3, 423 + margin: '0 0 24px 0', 424 + textAlign: 'center', 425 + color: '#8fd0a0', 426 + opacity: 0.9, 427 + }, 428 + children: options.subtitle, 429 + }, 430 + }, 431 + // Meta information 432 + (options.metaLine || options.extraMeta?.length) && { 433 + type: 'div', 434 + props: { 435 + style: { 436 + display: 'flex', 437 + flexDirection: 'column', 438 + alignItems: 'center', 439 + gap: '8px', 440 + margin: '0 0 32px 0', 441 + opacity: 0.8, 442 + }, 443 + children: [ 444 + options.metaLine && { 445 + type: 'p', 446 + props: { 447 + style: { 448 + fontSize: '18px', 449 + margin: '0', 450 + color: '#a8d5b8', 451 + fontWeight: 500, 452 + }, 453 + children: options.metaLine, 454 + }, 455 + }, 456 + ...(options.extraMeta?.map(meta => ({ 457 + type: 'p', 458 + props: { 459 + style: { 460 + fontSize: '16px', 461 + margin: '0', 462 + color: '#8fd0a0', 463 + fontWeight: 400, 464 + }, 465 + children: meta, 466 + }, 467 + })) || []), 468 + ].filter(Boolean), 469 + }, 470 + }, 471 + // Author section 472 + options.author && { 473 + type: 'div', 474 + props: { 475 + style: { 476 + display: 'flex', 477 + flexDirection: 'row', 478 + alignItems: 'center', 479 + justifyContent: 'center', 480 + gap: '18px', 481 + textAlign: 'center', 482 + opacity: 0.85, 483 + flexShrink: 0, 484 + }, 485 + children: [ 486 + options.author.avatar && { 487 + type: 'img', 488 + props: { 489 + src: options.author.avatar, 490 + width: 64, 491 + height: 64, 492 + style: { 493 + borderRadius: '50%', 494 + border: '3px solid #2d4839', 495 + boxShadow: '0 2px 12px rgba(0,0,0,0.18)', 496 + background: '#1e2c23', 497 + flexShrink: 0, 498 + }, 499 + }, 500 + }, 501 + { 502 + type: 'div', 503 + props: { 504 + style: { 505 + display: 'flex', 506 + flexDirection: 'column', 507 + alignItems: 'flex-start', 508 + gap: '2px', 509 + }, 510 + children: [ 511 + { 512 + type: 'span', 513 + props: { 514 + style: { 515 + fontSize: '22px', 516 + fontWeight: 700, 517 + fontStyle: 'normal', 518 + lineHeight: 1.1, 519 + }, 520 + children: options.author.name, 521 + }, 522 + }, 523 + options.author.handle && { 524 + type: 'span', 525 + props: { 526 + style: { 527 + fontSize: '16px', 528 + color: '#8fd0a0', 529 + fontWeight: 400, 530 + fontStyle: 'italic', 531 + }, 532 + children: '@' + options.author.handle, 533 + }, 534 + }, 535 + ].filter(Boolean), 536 + }, 537 + }, 538 + ].filter(Boolean), 539 + }, 540 + }, 541 + ].filter(Boolean), 542 + }, 543 + }; 544 + 545 + debug.debug('SVG container structure built', { 546 + hasTitle: true, 547 + hasSubtitle: !!options.subtitle, 548 + hasMeta: !!(options.metaLine || options.extraMeta?.length), 549 + hasAuthor: !!options.author 550 + }); 551 + 552 + // Compose SVG 553 + debug.debug('Composing SVG with Satori'); 554 + const svgTimer = debug.time('satoriSvgComposition'); 555 + 556 + const svg = await satori(mainContainer, { 557 + width: 1200, 558 + height: 630, 559 + fonts: [ 560 + { 561 + name: 'Recursive', 562 + data: fonts.regular, 563 + weight: 400, 564 + style: 'normal', 565 + }, 566 + { 567 + name: 'Recursive', 568 + data: fonts.bold, 569 + weight: 700, 570 + style: 'normal', 571 + }, 572 + { 573 + name: 'Recursive', 574 + data: fonts.italic, 575 + weight: 400, 576 + style: 'italic', 577 + }, 578 + ], 579 + }); 580 + 581 + svgTimer(); 582 + debug.debug('SVG composition completed', { svgLength: svg.length }); 583 + 584 + // Convert SVG to PNG 585 + debug.debug('Converting SVG to PNG with Resvg'); 586 + const pngTimer = debug.time('svgToPngConversion'); 587 + 588 + const resvg = new Resvg(svg, { fitTo: { mode: 'width', value: 1200 } }); 589 + const png = resvg.render(); 590 + const pngBuffer = png.asPng(); 591 + 592 + pngTimer(); 593 + debug.debug('PNG conversion completed', { 594 + pngBufferLength: pngBuffer.length, 595 + isBuffer: Buffer.isBuffer(pngBuffer) 596 + }); 597 + 598 + totalTimer(); 599 + debug.info('OG image generation completed successfully', { 600 + finalBufferLength: pngBuffer.length, 601 + isBuffer: Buffer.isBuffer(pngBuffer) 602 + }); 603 + 604 + debug.exit('generateOgImageImpl', { 605 + success: true, 606 + resultType: typeof pngBuffer, 607 + resultLength: pngBuffer?.length 608 + }); 609 + 610 + return pngBuffer; 611 + } catch (error) { 612 + debug.errorWithContext('Failed to generate OG image', error as Error, { 613 + function: 'generateOgImageImpl', 614 + options: { 615 + title: options.title?.substring(0, 100), 616 + hasSubtitle: !!options.subtitle, 617 + hasAuthor: !!options.author 618 + } 619 + }); 620 + 621 + totalTimer(); 622 + debug.exit('generateOgImageImpl', { 623 + success: false, 624 + error: error 625 + }); 626 + 627 + throw error; 628 + } 629 + }
+105 -45
src/lib/services/blogService.ts
··· 89 89 async function loadAllPages(fetch: typeof window.fetch): Promise<any[]> { 90 90 let allRecords: any[] = []; 91 91 let cursor: string | undefined | null = undefined; 92 + let pageCount = 0; 93 + const MAX_PAGES = 10; // Prevent infinite loops 92 94 93 95 do { 94 96 // Construct request URL with optional cursor ··· 99 101 url.searchParams.set("cursor", cursor); 100 102 } 101 103 102 - const res = await fetch(url.toString()); 103 - if (!res.ok) throw new Error(`Failed to fetch page: ${res.status}`); 104 - const body = await res.json(); 104 + // Add timeout to fetch request 105 + const controller = new AbortController(); 106 + const timeoutId = setTimeout(() => controller.abort(), 8000); // 8 second timeout 105 107 106 - // Append new records 107 - if (body.records && Array.isArray(body.records)) { 108 - allRecords = allRecords.concat(body.records); 108 + try { 109 + const res = await fetch(url.toString(), { 110 + signal: controller.signal, 111 + headers: { 112 + 'Accept': 'application/json' 113 + } 114 + }); 115 + 116 + clearTimeout(timeoutId); 117 + 118 + if (!res.ok) throw new Error(`Failed to fetch page: ${res.status}`); 119 + const body = await res.json(); 120 + 121 + // Append new records 122 + if (body.records && Array.isArray(body.records)) { 123 + allRecords = allRecords.concat(body.records); 124 + } 125 + 126 + // Update cursor for next page 127 + cursor = body.cursor ?? null; 128 + pageCount++; 129 + 130 + // Safety check to prevent infinite loops 131 + if (pageCount >= MAX_PAGES) { 132 + console.warn(`Reached maximum page limit (${MAX_PAGES}), stopping pagination`); 133 + break; 134 + } 135 + } catch (error) { 136 + clearTimeout(timeoutId); 137 + if (error instanceof Error && error.name === 'AbortError') { 138 + console.warn('Request timed out, returning partial results'); 139 + break; 140 + } 141 + throw error; 109 142 } 110 - 111 - // Update cursor for next page 112 - cursor = body.cursor ?? null; 113 143 } while (cursor); 114 144 115 145 return allRecords; ··· 120 150 */ 121 151 export async function loadAllPosts(fetch: typeof window.fetch): Promise<BlogServiceResult> { 122 152 try { 123 - // Load profile once 124 - if (profile === undefined) { 125 - profile = await getProfile(fetch); 126 - } 153 + // Add overall timeout for the entire operation 154 + const controller = new AbortController(); 155 + const timeoutId = setTimeout(() => controller.abort(), 25000); // 25 second total timeout 156 + 157 + try { 158 + // Load profile once 159 + if (profile === undefined) { 160 + profile = await getProfile(fetch); 161 + } 127 162 128 - // Always fetch fresh data for blog posts 129 - const records = await loadAllPages(fetch); 163 + // Always fetch fresh data for blog posts 164 + const records = await loadAllPages(fetch); 130 165 131 - const mdposts: Map<string, MarkdownPost> = new Map(); 132 - for (const data of records) { 133 - const processed = processRecord(data); 134 - if (processed) { 135 - mdposts.set(processed.rkey, processed); 166 + const mdposts: Map<string, MarkdownPost> = new Map(); 167 + for (const data of records) { 168 + const processed = processRecord(data); 169 + if (processed) { 170 + mdposts.set(processed.rkey, processed); 171 + } 136 172 } 137 - } 138 173 139 - // Convert markdown posts to full post format 140 - allPosts = await parse(mdposts); 174 + // Convert markdown posts to full post format 175 + allPosts = await parse(mdposts); 141 176 142 - // Sort posts chronologically (newest first) 143 - sortedPosts = Array.from(allPosts.values()).sort( 144 - (a, b) => b.createdAt.getTime() - a.createdAt.getTime() 145 - ); 177 + // Sort posts chronologically (newest first) 178 + sortedPosts = Array.from(allPosts.values()).sort( 179 + (a, b) => b.createdAt.getTime() - a.createdAt.getTime() 180 + ); 146 181 147 - // Assign reverse post numbers 148 - const total = sortedPosts.length; 149 - sortedPosts.forEach((post, index) => { 150 - post.postNumber = total - index; 151 - }); 182 + // Assign reverse post numbers 183 + const total = sortedPosts.length; 184 + sortedPosts.forEach((post, index) => { 185 + post.postNumber = total - index; 186 + }); 152 187 153 - return { 154 - posts: allPosts, 155 - profile, 156 - sortedPosts, 157 - getPost: (rkey: string) => allPosts?.get(rkey) ?? null, 158 - getAdjacentPosts: (rkey: string) => { 159 - const idx = sortedPosts.findIndex(p => p.rkey === rkey); 160 - return { 161 - previous: idx > 0 ? sortedPosts[idx - 1] : null, 162 - next: idx < sortedPosts.length - 1 ? sortedPosts[idx + 1] : null, 163 - }; 164 - }, 165 - }; 188 + clearTimeout(timeoutId); 189 + 190 + return { 191 + posts: allPosts, 192 + profile, 193 + sortedPosts, 194 + getPost: (rkey: string) => allPosts?.get(rkey) ?? null, 195 + getAdjacentPosts: (rkey: string) => { 196 + const idx = sortedPosts.findIndex(p => p.rkey === rkey); 197 + return { 198 + previous: idx > 0 ? sortedPosts[idx - 1] : null, 199 + next: idx < sortedPosts.length - 1 ? sortedPosts[idx + 1] : null, 200 + }; 201 + }, 202 + }; 203 + } catch (error) { 204 + clearTimeout(timeoutId); 205 + if (error instanceof Error && error.name === 'AbortError') { 206 + console.warn('Blog loading timed out, returning cached data if available'); 207 + // Return cached data if available, otherwise empty result 208 + if (allPosts && sortedPosts.length > 0) { 209 + return { 210 + posts: allPosts, 211 + profile: profile || ({} as Profile), 212 + sortedPosts, 213 + getPost: (rkey: string) => allPosts!.get(rkey) ?? null, 214 + getAdjacentPosts: (rkey: string) => { 215 + const idx = sortedPosts.findIndex(p => p.rkey === rkey); 216 + return { 217 + previous: idx > 0 ? sortedPosts[idx - 1] : null, 218 + next: idx < sortedPosts.length - 1 ? sortedPosts[idx + 1] : null, 219 + }; 220 + }, 221 + }; 222 + } 223 + } 224 + throw error; 225 + } 166 226 } catch (err) { 167 227 console.error("Error in loadAllPosts:", err); 168 228 return {
+296
src/lib/utils/README.md
··· 1 + # Debugging Utility System 2 + 3 + This directory contains a comprehensive debugging utility system that provides structured logging, performance monitoring, and debugging capabilities across your application. 4 + 5 + ## Features 6 + 7 + - **Multi-level logging**: error, warn, info, debug, trace 8 + - **Performance timing**: Built-in timing for functions and operations 9 + - **Context tracking**: File, function, and component-specific debugging 10 + - **Environment awareness**: Automatic detection of development/production modes 11 + - **Memory monitoring**: Track memory usage when available 12 + - **Structured output**: Consistent formatting with emojis and timestamps 13 + - **Error context**: Rich error logging with stack traces 14 + - **Circular reference handling**: Safe object serialization 15 + 16 + ## Quick Start 17 + 18 + ### Basic Usage 19 + 20 + ```typescript 21 + import { debug, quickDebug } from '$lib/utils/debug.js'; 22 + 23 + // Simple logging 24 + debug.info('Application started'); 25 + debug.warn('Deprecated feature used'); 26 + debug.error('Something went wrong'); 27 + 28 + // Quick debugging 29 + quickDebug.log('Quick message'); 30 + quickDebug.error('Quick error'); 31 + ``` 32 + 33 + ### File-Specific Debugging 34 + 35 + ```typescript 36 + import { createFileDebugger } from '$lib/utils/debug.js'; 37 + 38 + const debug = createFileDebugger('myFile.ts'); 39 + 40 + debug.enter('myFunction', { param1: 'value' }); 41 + debug.debug('Processing data...'); 42 + debug.exit('myFunction', { result: 'success' }); 43 + ``` 44 + 45 + ### Component Debugging 46 + 47 + ```typescript 48 + import { createComponentDebugger } from '$lib/utils/debug.js'; 49 + 50 + const debug = createComponentDebugger('MyComponent'); 51 + 52 + debug.lifecycle('MyComponent', 'mounted', { props: componentProps }); 53 + debug.state('MyComponent', 'userData', { oldValue, newValue }); 54 + debug.interaction('click', 'submitButton', { formData }); 55 + ``` 56 + 57 + ### Performance Timing 58 + 59 + ```typescript 60 + const timer = debug.time('expensiveOperation'); 61 + // ... do work ... 62 + timer(); // Logs the duration 63 + ``` 64 + 65 + ### Error Handling 66 + 67 + ```typescript 68 + try { 69 + // ... risky operation ... 70 + } catch (error) { 71 + debug.errorWithContext('Operation failed', error, { 72 + function: 'myFunction', 73 + additionalContext: 'extra info' 74 + }); 75 + } 76 + ``` 77 + 78 + ## Configuration 79 + 80 + The debugger automatically configures itself based on environment variables: 81 + 82 + - `NODE_ENV=development` - Enables debugging 83 + - `DEBUG=true` - Forces debugging on in production 84 + - `DEBUG_LEVEL` - Set minimum log level (error, warn, info, debug, trace) 85 + 86 + ### Custom Configuration 87 + 88 + ```typescript 89 + import { Debugger } from '$lib/utils/debug.js'; 90 + 91 + const customDebug = new Debugger({ 92 + enabled: true, 93 + level: 'debug', 94 + prefix: '[CUSTOM]', 95 + includeTimestamp: true, 96 + includeStack: true, 97 + maxDepth: 5 98 + }); 99 + ``` 100 + 101 + ## Log Levels 102 + 103 + 1. **error** ❌ - Critical errors that need immediate attention 104 + 2. **warn** ⚠️ - Warnings about potential issues 105 + 3. **info** ℹ️ - General information about application state 106 + 4. **debug** 🐛 - Detailed debugging information 107 + 5. **trace** 🔍 - Most verbose logging for deep debugging 108 + 109 + ## Best Practices 110 + 111 + ### 1. Use Appropriate Log Levels 112 + 113 + ```typescript 114 + // Good 115 + debug.error('Database connection failed', error); 116 + debug.warn('API rate limit approaching'); 117 + debug.info('User logged in successfully'); 118 + debug.debug('Processing request data'); 119 + debug.trace('Entering inner loop iteration'); 120 + 121 + // Avoid 122 + debug.debug('CRITICAL ERROR: System down'); // Should be error 123 + debug.info('Debug: variable value is 42'); // Should be debug 124 + ``` 125 + 126 + ### 2. Provide Context 127 + 128 + ```typescript 129 + // Good 130 + debug.error('Failed to save user', error, { 131 + userId: user.id, 132 + operation: 'save', 133 + timestamp: new Date().toISOString() 134 + }); 135 + 136 + // Avoid 137 + debug.error('Failed to save user'); // No context 138 + ``` 139 + 140 + ### 3. Use Function Entry/Exit 141 + 142 + ```typescript 143 + async function processUser(userId: string) { 144 + debug.enter('processUser', { userId }); 145 + 146 + try { 147 + const result = await doWork(userId); 148 + debug.exit('processUser', { success: true, result }); 149 + return result; 150 + } catch (error) { 151 + debug.exit('processUser', { success: false, error }); 152 + throw error; 153 + } 154 + } 155 + ``` 156 + 157 + ### 4. Performance Monitoring 158 + 159 + ```typescript 160 + async function fetchData() { 161 + const timer = debug.time('fetchData'); 162 + const memory = debug.memory('before fetch'); 163 + 164 + try { 165 + const data = await api.get('/data'); 166 + memory('after fetch'); 167 + timer(); 168 + return data; 169 + } catch (error) { 170 + timer(); 171 + throw error; 172 + } 173 + } 174 + ``` 175 + 176 + ## Examples 177 + 178 + ### API Route Debugging 179 + 180 + ```typescript 181 + // src/routes/api/users/+server.ts 182 + import { createFileDebugger } from '$lib/utils/debug.js'; 183 + 184 + const debug = createFileDebugger('api/users/+server.ts'); 185 + 186 + export const GET = async ({ url }) => { 187 + debug.enter('GET', { url: url.toString() }); 188 + 189 + try { 190 + const users = await getUsers(); 191 + debug.info('Users retrieved successfully', { count: users.length }); 192 + 193 + debug.exit('GET', { success: true, userCount: users.length }); 194 + return json(users); 195 + } catch (error) { 196 + debug.errorWithContext('Failed to get users', error, { function: 'GET' }); 197 + debug.exit('GET', { success: false, error }); 198 + throw error; 199 + } 200 + }; 201 + ``` 202 + 203 + ### Component Debugging 204 + 205 + ```typescript 206 + // MyComponent.svelte 207 + <script> 208 + import { createComponentDebugger } from '$lib/utils/debug.js'; 209 + import { onMount } from 'svelte'; 210 + 211 + const debug = createComponentDebugger('MyComponent'); 212 + 213 + export let data; 214 + 215 + onMount(() => { 216 + debug.lifecycle('MyComponent', 'mounted', { hasData: !!data }); 217 + }); 218 + 219 + $effect(() => { 220 + debug.state('MyComponent', 'data', { data }); 221 + }); 222 + </script> 223 + ``` 224 + 225 + ### Service Layer Debugging 226 + 227 + ```typescript 228 + // userService.ts 229 + import { createFileDebugger } from '$lib/utils/debug.js'; 230 + 231 + const debug = createFileDebugger('userService.ts'); 232 + 233 + export class UserService { 234 + async createUser(userData: UserData) { 235 + debug.enter('createUser', { email: userData.email }); 236 + const timer = debug.time('createUser'); 237 + 238 + try { 239 + const user = await this.db.users.create(userData); 240 + debug.info('User created successfully', { userId: user.id }); 241 + 242 + timer(); 243 + debug.exit('createUser', { success: true, userId: user.id }); 244 + return user; 245 + } catch (error) { 246 + debug.errorWithContext('Failed to create user', error, { 247 + function: 'createUser', 248 + userData: { email: userData.email } 249 + }); 250 + 251 + timer(); 252 + debug.exit('createUser', { success: false, error }); 253 + throw error; 254 + } 255 + } 256 + } 257 + ``` 258 + 259 + ## Environment Variables 260 + 261 + | Variable | Default | Description | 262 + |----------|---------|-------------| 263 + | `NODE_ENV` | - | Set to 'development' to enable debugging | 264 + | `DEBUG` | false | Force enable debugging | 265 + | `DEBUG_LEVEL` | 'info' | Minimum log level | 266 + | `DEBUG_PREFIX` | '[DEBUG]' | Custom log prefix | 267 + | `DEBUG_TIMESTAMP` | true | Include timestamps | 268 + | `DEBUG_STACK` | false | Include stack traces | 269 + 270 + ## Production Considerations 271 + 272 + - Debugging is automatically disabled in production unless `DEBUG=true` 273 + - Performance impact is minimal when disabled 274 + - Sensitive data should never be logged 275 + - Consider log aggregation services for production debugging 276 + 277 + ## Troubleshooting 278 + 279 + ### Debugging Not Working 280 + 281 + 1. Check `NODE_ENV` is set to 'development' 282 + 2. Verify `DEBUG=true` if in production 283 + 3. Ensure debug utility is imported correctly 284 + 4. Check browser console for any errors 285 + 286 + ### Too Much Logging 287 + 288 + 1. Increase log level: `DEBUG_LEVEL=warn` 289 + 2. Use more specific debuggers for targeted logging 290 + 3. Disable specific debuggers in production 291 + 292 + ### Performance Issues 293 + 294 + 1. Use `debug.time()` to identify slow operations 295 + 2. Monitor memory usage with `debug.memory()` 296 + 3. Consider reducing log level in performance-critical sections
+317
src/lib/utils/debug.ts
··· 1 + /** 2 + * Comprehensive debugging utility for development and production 3 + */ 4 + 5 + export interface DebugOptions { 6 + enabled?: boolean; 7 + level?: 'error' | 'warn' | 'info' | 'debug' | 'trace'; 8 + prefix?: string; 9 + includeTimestamp?: boolean; 10 + includeStack?: boolean; 11 + maxDepth?: number; 12 + } 13 + 14 + export interface DebugContext { 15 + file: string; 16 + function: string; 17 + line?: number; 18 + component?: string; 19 + userAgent?: string; 20 + timestamp: string; 21 + } 22 + 23 + class Debugger { 24 + private options: DebugOptions; 25 + private context: Partial<DebugContext> = {}; 26 + 27 + constructor(options: DebugOptions = {}) { 28 + this.options = { 29 + enabled: process.env.NODE_ENV === 'development' || process.env.DEBUG === 'true', 30 + level: 'info', 31 + prefix: '[DEBUG]', 32 + includeTimestamp: true, 33 + includeStack: false, 34 + maxDepth: 3, 35 + ...options 36 + }; 37 + } 38 + 39 + /** 40 + * Set context for all subsequent debug calls 41 + */ 42 + setContext(context: Partial<DebugContext>): void { 43 + this.context = { ...this.context, ...context }; 44 + } 45 + 46 + /** 47 + * Create a new debugger instance with specific context 48 + */ 49 + withContext(context: Partial<DebugContext>): Debugger { 50 + const newDebugger = new Debugger(this.options); 51 + newDebugger.setContext({ ...this.context, ...context }); 52 + return newDebugger; 53 + } 54 + 55 + /** 56 + * Log error messages 57 + */ 58 + error(message: string, data?: any, error?: Error): void { 59 + if (!this.shouldLog('error')) return; 60 + this.log('error', message, data, error); 61 + } 62 + 63 + /** 64 + * Log warning messages 65 + */ 66 + warn(message: string, data?: any): void { 67 + if (!this.shouldLog('warn')) return; 68 + this.log('warn', message, data); 69 + } 70 + 71 + /** 72 + * Log info messages 73 + */ 74 + info(message: string, data?: any): void { 75 + if (!this.shouldLog('info')) return; 76 + this.log('info', message, data); 77 + } 78 + 79 + /** 80 + * Log debug messages 81 + */ 82 + debug(message: string, data?: any): void { 83 + if (!this.shouldLog('debug')) return; 84 + this.log('debug', message, data); 85 + } 86 + 87 + /** 88 + * Log trace messages (most verbose) 89 + */ 90 + trace(message: string, data?: any): void { 91 + if (!this.shouldLog('trace')) return; 92 + this.log('trace', message, data); 93 + } 94 + 95 + /** 96 + * Log function entry 97 + */ 98 + enter(functionName: string, params?: any): void { 99 + if (!this.shouldLog('debug')) return; 100 + this.debug(`→ Entering ${functionName}`, params); 101 + } 102 + 103 + /** 104 + * Log function exit 105 + */ 106 + exit(functionName: string, result?: any): void { 107 + if (!this.shouldLog('debug')) return; 108 + this.debug(`← Exiting ${functionName}`, result); 109 + } 110 + 111 + /** 112 + * Log performance timing 113 + */ 114 + time(label: string): () => void { 115 + if (!this.shouldLog('debug')) return () => {}; 116 + 117 + const start = performance.now(); 118 + this.debug(`⏱️ Starting timer: ${label}`); 119 + 120 + return () => { 121 + const duration = performance.now() - start; 122 + this.debug(`⏱️ Timer ${label}: ${duration.toFixed(2)}ms`); 123 + }; 124 + } 125 + 126 + /** 127 + * Log memory usage (if available) 128 + */ 129 + memory(label?: string): void { 130 + if (!this.shouldLog('debug')) return; 131 + 132 + if (typeof performance !== 'undefined' && 'memory' in performance) { 133 + const mem = (performance as any).memory; 134 + this.debug(`💾 Memory ${label || 'usage'}:`, { 135 + used: `${(mem.usedJSHeapSize / 1024 / 1024).toFixed(2)}MB`, 136 + total: `${(mem.totalJSHeapSize / 1024 / 1024).toFixed(2)}MB`, 137 + limit: `${(mem.jsHeapSizeLimit / 1024 / 1024).toFixed(2)}MB` 138 + }); 139 + } 140 + } 141 + 142 + /** 143 + * Log API calls 144 + */ 145 + api(method: string, url: string, data?: any, response?: any): void { 146 + if (!this.shouldLog('debug')) return; 147 + this.debug(`🌐 API ${method} ${url}`, { request: data, response }); 148 + } 149 + 150 + /** 151 + * Log component lifecycle events 152 + */ 153 + lifecycle(component: string, event: string, data?: any): void { 154 + if (!this.shouldLog('debug')) return; 155 + this.debug(`🔄 ${component} ${event}`, data); 156 + } 157 + 158 + /** 159 + * Log state changes 160 + */ 161 + state(component: string, oldState: any, newState: any): void { 162 + if (!this.shouldLog('debug')) return; 163 + this.debug(`📊 ${component} state change`, { from: oldState, to: newState }); 164 + } 165 + 166 + /** 167 + * Log user interactions 168 + */ 169 + interaction(action: string, target: string, data?: any): void { 170 + if (!this.shouldLog('debug')) return; 171 + this.debug(`👆 User interaction: ${action} on ${target}`, data); 172 + } 173 + 174 + /** 175 + * Log errors with full context 176 + */ 177 + errorWithContext(message: string, error: Error, context?: any): void { 178 + if (!this.shouldLog('error')) return; 179 + 180 + this.error(message, { 181 + ...context, 182 + error: { 183 + name: error.name, 184 + message: error.message, 185 + stack: this.options.includeStack ? error.stack : undefined 186 + } 187 + }); 188 + } 189 + 190 + /** 191 + * Check if logging should occur for given level 192 + */ 193 + private shouldLog(level: string): boolean { 194 + if (!this.options.enabled) return false; 195 + 196 + const levels = ['error', 'warn', 'info', 'debug', 'trace']; 197 + const currentLevelIndex = levels.indexOf(this.options.level || 'info'); 198 + const messageLevelIndex = levels.indexOf(level); 199 + 200 + return messageLevelIndex <= currentLevelIndex; 201 + } 202 + 203 + /** 204 + * Format and output log message 205 + */ 206 + private log(level: string, message: string, data?: any, error?: Error): void { 207 + const timestamp = this.options.includeTimestamp ? new Date().toISOString() : ''; 208 + const prefix = this.options.prefix || '[DEBUG]'; 209 + const levelEmoji = this.getLevelEmoji(level); 210 + 211 + let output = `${prefix} ${levelEmoji} ${level.toUpperCase()}`; 212 + 213 + if (timestamp) { 214 + output += ` [${timestamp}]`; 215 + } 216 + 217 + if (this.context.file) { 218 + output += ` [${this.context.file}`; 219 + if (this.context.function) output += `:${this.context.function}`; 220 + if (this.context.line) output += `:${this.context.line}`; 221 + output += ']'; 222 + } 223 + 224 + output += ` ${message}`; 225 + 226 + // Console output based on level 227 + switch (level) { 228 + case 'error': 229 + console.error(output, data || ''); 230 + if (error) console.error(error); 231 + break; 232 + case 'warn': 233 + console.warn(output, data || ''); 234 + break; 235 + case 'info': 236 + console.info(output, data || ''); 237 + break; 238 + case 'debug': 239 + case 'trace': 240 + console.log(output, data || ''); 241 + break; 242 + } 243 + } 244 + 245 + /** 246 + * Get emoji for log level 247 + */ 248 + private getLevelEmoji(level: string): string { 249 + switch (level) { 250 + case 'error': return '❌'; 251 + case 'warn': return '⚠️'; 252 + case 'info': return 'ℹ️'; 253 + case 'debug': return '🐛'; 254 + case 'trace': return '🔍'; 255 + default: return '📝'; 256 + } 257 + } 258 + 259 + /** 260 + * Safely stringify objects with circular reference handling 261 + */ 262 + private safeStringify(obj: any, depth: number = 0): string { 263 + if (depth > (this.options.maxDepth || 3)) return '[Max Depth Reached]'; 264 + if (obj === null) return 'null'; 265 + if (typeof obj === 'undefined') return 'undefined'; 266 + if (typeof obj === 'string') return obj; 267 + if (typeof obj === 'number' || typeof obj === 'boolean') return String(obj); 268 + 269 + try { 270 + if (obj instanceof Error) { 271 + return `Error: ${obj.message}`; 272 + } 273 + 274 + if (Array.isArray(obj)) { 275 + return `[${obj.map(item => this.safeStringify(item, depth + 1)).join(', ')}]`; 276 + } 277 + 278 + if (typeof obj === 'object') { 279 + const entries = Object.entries(obj).map(([key, value]) => 280 + `${key}: ${this.safeStringify(value, depth + 1)}` 281 + ); 282 + return `{${entries.join(', ')}}`; 283 + } 284 + 285 + return String(obj); 286 + } catch (error) { 287 + return '[Circular Reference or Unstringifiable]'; 288 + } 289 + } 290 + } 291 + 292 + // Create default debugger instance 293 + export const debug = new Debugger(); 294 + 295 + // Create specialized debuggers for common use cases 296 + export const createFileDebugger = (file: string) => debug.withContext({ file }); 297 + export const createComponentDebugger = (component: string) => debug.withContext({ component }); 298 + export const createFunctionDebugger = (file: string, functionName: string) => 299 + debug.withContext({ file, function: functionName }); 300 + 301 + // Export the Debugger class for custom instances 302 + export { Debugger }; 303 + 304 + // Utility functions for quick debugging 305 + export const quickDebug = { 306 + log: (message: string, data?: any) => debug.debug(message, data), 307 + error: (message: string, error?: Error) => debug.error(message, undefined, error), 308 + warn: (message: string, data?: any) => debug.warn(message, data), 309 + info: (message: string, data?: any) => debug.info(message, data), 310 + trace: (message: string, data?: any) => debug.trace(message, data) 311 + }; 312 + 313 + // Environment detection helpers 314 + export const isDevelopment = () => process.env.NODE_ENV === 'development'; 315 + export const isProduction = () => process.env.NODE_ENV === 'production'; 316 + export const isTest = () => process.env.NODE_ENV === 'test'; 317 + export const isDebugEnabled = () => process.env.DEBUG === 'true';
+274
src/lib/utils/dynamicImports.ts
··· 1 + /** 2 + * Dynamic import utilities for code splitting and reducing initial bundle size 3 + */ 4 + 5 + import { createFileDebugger } from './debug.js'; 6 + 7 + const debug = createFileDebugger('dynamicImports.ts'); 8 + 9 + // Lazy load markdown processing libraries 10 + export const loadMarkdownProcessor = async () => { 11 + debug.enter('loadMarkdownProcessor'); 12 + const timer = debug.time('loadMarkdownProcessor'); 13 + 14 + try { 15 + debug.info('Loading markdown processing libraries...'); 16 + 17 + const [ 18 + { unified }, 19 + { remark }, 20 + remarkGfm, 21 + remarkMath, 22 + remarkEmoji, 23 + remarkBreaks, 24 + remarkRehype, 25 + rehypeStringify, 26 + rehypeRaw, 27 + rehypeSanitize, 28 + rehypeHighlight, 29 + rehypeKatex, 30 + rehypeSlug, 31 + rehypeAutolinkHeadings 32 + ] = await Promise.all([ 33 + import('unified').then(module => { 34 + debug.debug('Loaded unified module', { hasUnified: !!module.unified }); 35 + return module; 36 + }), 37 + import('remark').then(module => { 38 + debug.debug('Loaded remark module', { hasRemark: !!module.remark }); 39 + return module; 40 + }), 41 + import('remark-gfm').then(module => { 42 + debug.debug('Loaded remark-gfm module', { hasDefault: !!module.default }); 43 + return module.default; 44 + }), 45 + import('remark-math').then(module => { 46 + debug.debug('Loaded remark-math module', { hasDefault: !!module.default }); 47 + return module.default; 48 + }), 49 + import('remark-emoji').then(module => { 50 + debug.debug('Loaded remark-emoji module', { hasDefault: !!module.default }); 51 + return module.default; 52 + }), 53 + import('remark-breaks').then(module => { 54 + debug.debug('Loaded remark-breaks module', { hasDefault: !!module.default }); 55 + return module.default; 56 + }), 57 + import('remark-rehype').then(module => { 58 + debug.debug('Loaded remark-rehype module', { hasDefault: !!module.default }); 59 + return module.default; 60 + }), 61 + import('rehype-stringify').then(module => { 62 + debug.debug('Loaded rehype-stringify module', { hasDefault: !!module.default }); 63 + return module.default; 64 + }), 65 + import('rehype-raw').then(module => { 66 + debug.debug('Loaded rehype-raw module', { hasDefault: !!module.default }); 67 + return module.default; 68 + }), 69 + import('rehype-sanitize').then(module => { 70 + debug.debug('Loaded rehype-sanitize module', { hasDefault: !!module.default }); 71 + return module.default; 72 + }), 73 + import('rehype-highlight').then(module => { 74 + debug.debug('Loaded rehype-highlight module', { hasDefault: !!module.default }); 75 + return module.default; 76 + }), 77 + import('rehype-katex').then(module => { 78 + debug.debug('Loaded rehype-katex module', { hasDefault: !!module.default }); 79 + return module.default; 80 + }), 81 + import('rehype-slug').then(module => { 82 + debug.debug('Loaded rehype-slug module', { hasDefault: !!module.default }); 83 + return module.default; 84 + }), 85 + import('rehype-autolink-headings').then(module => { 86 + debug.debug('Loaded rehype-autolink-headings module', { hasDefault: !!module.default }); 87 + return module.default; 88 + }) 89 + ]); 90 + 91 + const result = { 92 + unified, 93 + remark, 94 + remarkGfm, 95 + remarkMath, 96 + remarkEmoji, 97 + remarkBreaks, 98 + remarkRehype, 99 + rehypeStringify, 100 + rehypeRaw, 101 + rehypeSanitize, 102 + rehypeHighlight, 103 + rehypeKatex, 104 + rehypeSlug, 105 + rehypeAutolinkHeadings 106 + }; 107 + 108 + debug.info('Successfully loaded all markdown processing libraries', { 109 + loadedModules: Object.keys(result).filter(key => result[key as keyof typeof result]) 110 + }); 111 + 112 + timer(); 113 + debug.exit('loadMarkdownProcessor', { success: true, moduleCount: Object.keys(result).length }); 114 + return result; 115 + } catch (error) { 116 + debug.errorWithContext('Failed to load markdown processing libraries', error as Error, { 117 + function: 'loadMarkdownProcessor' 118 + }); 119 + timer(); 120 + debug.exit('loadMarkdownProcessor', { success: false, error: error }); 121 + throw error; 122 + } 123 + }; 124 + 125 + // Lazy load OG image generation libraries 126 + export const loadOGImageGenerator = async () => { 127 + debug.enter('loadOGImageGenerator'); 128 + const timer = debug.time('loadOGImageGenerator'); 129 + 130 + try { 131 + debug.info('Loading OG image generation libraries...'); 132 + 133 + const [satori] = await Promise.all([ 134 + import('satori').then(module => { 135 + debug.debug('Loaded satori module', { hasDefault: !!module.default }); 136 + return module.default; 137 + }) 138 + ]); 139 + 140 + const result = { satori }; 141 + 142 + debug.info('Successfully loaded OG image generation libraries', { 143 + loadedModules: Object.keys(result) 144 + }); 145 + 146 + timer(); 147 + debug.exit('loadOGImageGenerator', { success: true, moduleCount: Object.keys(result).length }); 148 + return result; 149 + } catch (error) { 150 + debug.errorWithContext('Failed to load OG image generation libraries', error as Error, { 151 + function: 'loadOGImageGenerator' 152 + }); 153 + timer(); 154 + debug.exit('loadOGImageGenerator', { success: false, error: error }); 155 + throw error; 156 + } 157 + }; 158 + 159 + // Lazy load utility libraries 160 + export const loadUtilities = async () => { 161 + debug.enter('loadUtilities'); 162 + const timer = debug.time('loadUtilities'); 163 + 164 + try { 165 + debug.info('Loading utility libraries...'); 166 + 167 + const [sanitizeHtml] = await Promise.all([ 168 + import('sanitize-html').then(module => { 169 + debug.debug('Loaded sanitize-html module', { hasDefault: !!module.default }); 170 + return module.default; 171 + }) 172 + ]); 173 + 174 + const result = { sanitizeHtml }; 175 + 176 + debug.info('Successfully loaded utility libraries', { 177 + loadedModules: Object.keys(result) 178 + }); 179 + 180 + timer(); 181 + debug.exit('loadUtilities', { success: true, moduleCount: Object.keys(result).length }); 182 + return result; 183 + } catch (error) { 184 + debug.errorWithContext('Failed to load utility libraries', error as Error, { 185 + function: 'loadUtilities' 186 + }); 187 + timer(); 188 + debug.exit('loadUtilities', { success: false, error: error }); 189 + throw error; 190 + } 191 + }; 192 + 193 + // Preload critical dependencies when needed 194 + export const preloadDependencies = async (type: 'markdown' | 'og' | 'utilities') => { 195 + debug.enter('preloadDependencies', { type }); 196 + const timer = debug.time(`preloadDependencies:${type}`); 197 + 198 + try { 199 + debug.info(`Preloading dependencies for type: ${type}`); 200 + 201 + let result; 202 + switch (type) { 203 + case 'markdown': 204 + result = await loadMarkdownProcessor(); 205 + break; 206 + case 'og': 207 + result = await loadOGImageGenerator(); 208 + break; 209 + case 'utilities': 210 + result = await loadUtilities(); 211 + break; 212 + default: 213 + const error = new Error(`Unknown dependency type: ${type}`); 214 + debug.error(`Unknown dependency type: ${type}`); 215 + throw error; 216 + } 217 + 218 + debug.info(`Successfully preloaded dependencies for type: ${type}`, { 219 + type, 220 + resultKeys: Object.keys(result || {}) 221 + }); 222 + 223 + timer(); 224 + debug.exit('preloadDependencies', { success: true, type, resultKeys: Object.keys(result || {}) }); 225 + return result; 226 + } catch (error) { 227 + debug.errorWithContext(`Failed to preload dependencies for type: ${type}`, error as Error, { 228 + function: 'preloadDependencies', 229 + type 230 + }); 231 + timer(); 232 + debug.exit('preloadDependencies', { success: false, type, error: error }); 233 + throw error; 234 + } 235 + }; 236 + 237 + // Check if dependencies are already loaded 238 + export const isDependencyLoaded = (type: string): boolean => { 239 + debug.enter('isDependencyLoaded', { type }); 240 + 241 + let result = false; 242 + try { 243 + switch (type) { 244 + case 'markdown': 245 + result = typeof window !== 'undefined' && 'unified' in window; 246 + break; 247 + case 'og': 248 + result = typeof window !== 'undefined' && 'satori' in window; 249 + break; 250 + case 'utilities': 251 + result = typeof window !== 'undefined' && 'sanitizeHtml' in window; 252 + break; 253 + default: 254 + debug.warn(`Unknown dependency type: ${type}`); 255 + result = false; 256 + } 257 + 258 + debug.debug(`Dependency ${type} loaded status: ${result}`, { 259 + type, 260 + isWindow: typeof window !== 'undefined', 261 + result 262 + }); 263 + 264 + debug.exit('isDependencyLoaded', { type, result }); 265 + return result; 266 + } catch (error) { 267 + debug.errorWithContext(`Error checking if dependency ${type} is loaded`, error as Error, { 268 + function: 'isDependencyLoaded', 269 + type 270 + }); 271 + debug.exit('isDependencyLoaded', { type, result: false, error: error }); 272 + return false; 273 + } 274 + };
+10
src/lib/utils/index.ts
··· 1 + // Export all utility functions 2 + export * from './cache'; 3 + export * from './formatters'; 4 + export * from './milestones'; 5 + export * from './tally'; 6 + export * from './textProcessor'; 7 + export * from './xml'; 8 + export * from './mathLoader'; 9 + export * from './performance'; 10 + export * from './dynamicImports';
+66
src/lib/utils/mathLoader.ts
··· 1 + /** 2 + * Dynamic KaTeX CSS loader for math content 3 + * Only loads KaTeX styles when needed, improving initial page load performance 4 + */ 5 + 6 + let katexLoaded = false; 7 + 8 + export async function loadKaTeX(): Promise<void> { 9 + if (katexLoaded) return; 10 + 11 + try { 12 + // Check if KaTeX CSS is already loaded 13 + if (document.querySelector('link[href*="katex"]')) { 14 + katexLoaded = true; 15 + return; 16 + } 17 + 18 + // Dynamically load KaTeX CSS 19 + const link = document.createElement('link'); 20 + link.rel = 'stylesheet'; 21 + link.href = 'https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css'; 22 + link.crossOrigin = 'anonymous'; 23 + 24 + // Add loading promise 25 + const loadPromise = new Promise<void>((resolve, reject) => { 26 + link.onload = () => { 27 + katexLoaded = true; 28 + resolve(); 29 + }; 30 + link.onerror = () => reject(new Error('Failed to load KaTeX CSS')); 31 + }); 32 + 33 + // Append to head 34 + document.head.appendChild(link); 35 + 36 + // Wait for load to complete 37 + await loadPromise; 38 + } catch (error) { 39 + console.warn('Failed to load KaTeX CSS:', error); 40 + // Fallback: try to load from a different CDN or local file 41 + } 42 + } 43 + 44 + export function isKaTeXLoaded(): boolean { 45 + return katexLoaded; 46 + } 47 + 48 + /** 49 + * Check if content contains math expressions and load KaTeX if needed 50 + */ 51 + export async function loadKaTeXIfNeeded(content: string): Promise<void> { 52 + // Simple regex to detect potential math content 53 + const mathPatterns = [ 54 + /\$\$[\s\S]*?\$\$/, // Display math 55 + /\$[^$\n]*\$/, // Inline math 56 + /\\\([\s\S]*?\\\)/, // LaTeX inline 57 + /\\\[[\s\S]*?\\\]/, // LaTeX display 58 + /\\begin\{.*?\}[\s\S]*?\\end\{.*?\}/, // LaTeX environments 59 + ]; 60 + 61 + const hasMath = mathPatterns.some(pattern => pattern.test(content)); 62 + 63 + if (hasMath) { 64 + await loadKaTeX(); 65 + } 66 + }
+167
src/lib/utils/performance.ts
··· 1 + /** 2 + * Performance monitoring utilities for tracking Core Web Vitals and performance metrics 3 + */ 4 + 5 + export interface PerformanceMetrics { 6 + fcp: number | null; 7 + lcp: number | null; 8 + fid: number | null; 9 + cls: number | null; 10 + ttfb: number | null; 11 + fcpScore: string | null; 12 + lcpScore: string | null; 13 + fidScore: string | null; 14 + clsScore: string | null; 15 + } 16 + 17 + /** 18 + * Get performance score based on metric value and thresholds 19 + */ 20 + function getPerformanceScore(metric: number, thresholds: { good: number; needsImprovement: number }): string { 21 + if (metric <= thresholds.good) return 'good'; 22 + if (metric <= thresholds.needsImprovement) return 'needs-improvement'; 23 + return 'poor'; 24 + } 25 + 26 + /** 27 + * Calculate First Contentful Paint score 28 + */ 29 + function getFCPScore(fcp: number): string { 30 + return getPerformanceScore(fcp, { good: 1800, needsImprovement: 3000 }); 31 + } 32 + 33 + /** 34 + * Calculate Largest Contentful Paint score 35 + */ 36 + function getLCPScore(lcp: number): string { 37 + return getPerformanceScore(lcp, { good: 2500, needsImprovement: 4000 }); 38 + } 39 + 40 + /** 41 + * Calculate First Input Delay score 42 + */ 43 + function getFIDScore(fid: number): string { 44 + return getPerformanceScore(fid, { good: 100, needsImprovement: 300 }); 45 + } 46 + 47 + /** 48 + * Calculate Cumulative Layout Shift score 49 + */ 50 + function getCLSScore(cls: number): string { 51 + return getPerformanceScore(cls, { good: 0.1, needsImprovement: 0.25 }); 52 + } 53 + 54 + /** 55 + * Measure Core Web Vitals and other performance metrics 56 + */ 57 + export function measurePerformance(): Promise<PerformanceMetrics> { 58 + return new Promise((resolve) => { 59 + const metrics: PerformanceMetrics = { 60 + fcp: null, 61 + lcp: null, 62 + fid: null, 63 + cls: null, 64 + ttfb: null, 65 + fcpScore: null, 66 + lcpScore: null, 67 + fidScore: null, 68 + clsScore: null 69 + }; 70 + 71 + // Measure TTFB (Time to First Byte) 72 + const navigationEntry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; 73 + if (navigationEntry) { 74 + metrics.ttfb = navigationEntry.responseStart - navigationEntry.requestStart; 75 + } 76 + 77 + // Measure FCP (First Contentful Paint) 78 + const fcpObserver = new PerformanceObserver((list) => { 79 + const entries = list.getEntries(); 80 + const fcpEntry = entries.find(entry => entry.name === 'first-contentful-paint') as PerformanceEntry; 81 + if (fcpEntry) { 82 + metrics.fcp = fcpEntry.startTime; 83 + metrics.fcpScore = getFCPScore(metrics.fcp); 84 + } 85 + }); 86 + fcpObserver.observe({ entryTypes: ['paint'] }); 87 + 88 + // Measure LCP (Largest Contentful Paint) 89 + const lcpObserver = new PerformanceObserver((list) => { 90 + const entries = list.getEntries(); 91 + const lastEntry = entries[entries.length - 1] as PerformanceEntry; 92 + if (lastEntry) { 93 + metrics.lcp = lastEntry.startTime; 94 + metrics.lcpScore = getLCPScore(metrics.lcp); 95 + } 96 + }); 97 + lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] }); 98 + 99 + // Measure FID (First Input Delay) 100 + const fidObserver = new PerformanceObserver((list) => { 101 + const entries = list.getEntries(); 102 + const fidEntry = entries[0] as PerformanceEntry; 103 + if (fidEntry) { 104 + metrics.fid = fidEntry.processingStart - fidEntry.startTime; 105 + metrics.fidScore = getFIDScore(metrics.fid); 106 + } 107 + }); 108 + fidObserver.observe({ entryTypes: ['first-input'] }); 109 + 110 + // Measure CLS (Cumulative Layout Shift) 111 + let clsValue = 0; 112 + const clsObserver = new PerformanceObserver((list) => { 113 + for (const entry of list.getEntries()) { 114 + if (!(entry as any).hadRecentInput) { 115 + clsValue += (entry as any).value; 116 + } 117 + } 118 + metrics.cls = clsValue; 119 + metrics.clsScore = getCLSScore(metrics.cls); 120 + }); 121 + clsObserver.observe({ entryTypes: ['layout-shift'] }); 122 + 123 + // Wait for all metrics to be collected 124 + setTimeout(() => { 125 + fcpObserver.disconnect(); 126 + lcpObserver.disconnect(); 127 + fidObserver.disconnect(); 128 + clsObserver.disconnect(); 129 + resolve(metrics); 130 + }, 5000); // Wait up to 5 seconds for metrics 131 + }); 132 + } 133 + 134 + /** 135 + * Log performance metrics to console 136 + */ 137 + export function logPerformanceMetrics(metrics: PerformanceMetrics): void { 138 + console.group('🚀 Performance Metrics'); 139 + console.log(`FCP: ${metrics.fcp?.toFixed(2)}ms (${metrics.fcpScore})`); 140 + console.log(`LCP: ${metrics.lcp?.toFixed(2)}ms (${metrics.lcpScore})`); 141 + console.log(`FID: ${metrics.fid?.toFixed(2)}ms (${metrics.fidScore})`); 142 + console.log(`CLS: ${metrics.cls?.toFixed(3)} (${metrics.clsScore})`); 143 + console.log(`TTFB: ${metrics.ttfb?.toFixed(2)}ms`); 144 + console.groupEnd(); 145 + } 146 + 147 + /** 148 + * Send performance metrics to analytics (if configured) 149 + */ 150 + export function sendPerformanceMetrics(metrics: PerformanceMetrics, endpoint?: string): void { 151 + if (!endpoint) return; 152 + 153 + fetch(endpoint, { 154 + method: 'POST', 155 + headers: { 156 + 'Content-Type': 'application/json', 157 + }, 158 + body: JSON.stringify({ 159 + timestamp: Date.now(), 160 + url: window.location.href, 161 + userAgent: navigator.userAgent, 162 + ...metrics 163 + }) 164 + }).catch(error => { 165 + console.warn('Failed to send performance metrics:', error); 166 + }); 167 + }
+18
src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 2 import "$css/app.css"; 3 3 import { getStores } from "$app/stores"; 4 + import { onMount } from "svelte"; 4 5 const { page } = getStores(); 5 6 import Profile from "$components/profile/Profile.svelte"; 6 7 import { Footer, HeaderMain } from "$components/layout"; 7 8 import { NoScriptMessage } from "$components/shared"; 9 + import { measurePerformance, logPerformanceMetrics } from "$utils/performance"; 8 10 9 11 let { data, children } = $props(); 10 12 ··· 14 16 ); 15 17 const isHomePage = $derived($page.route.id === "/"); 16 18 const isBlogIndex = $derived($page.route.id === "/blog"); 19 + 20 + // Performance monitoring 21 + onMount(() => { 22 + // Measure performance after page load 23 + setTimeout(async () => { 24 + try { 25 + const metrics = await measurePerformance(); 26 + logPerformanceMetrics(metrics); 27 + 28 + // Send metrics to analytics if configured 29 + // sendPerformanceMetrics(metrics, '/api/analytics/performance'); 30 + } catch (error) { 31 + console.warn('Performance measurement failed:', error); 32 + } 33 + }, 1000); 34 + }); 17 35 </script> 18 36 19 37 <!-- NoScript fallback -->
+36
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { onMount } from "svelte"; 3 3 import { getStores } from "$app/stores"; 4 + import { createComponentDebugger } from "$lib/utils/debug.js"; 5 + 4 6 const { page } = getStores(); 5 7 import { DynamicLinks, LatestBlogPost } from "$components/layout/main"; 8 + 9 + // Create debugger for this component 10 + const debug = createComponentDebugger('MainPage'); 6 11 7 12 let { data } = $props(); 8 13 ··· 10 15 let localeLoaded = $state(false); 11 16 12 17 onMount(() => { 18 + debug.lifecycle('MainPage', 'mounted', { 19 + hasData: !!data, 20 + dataKeys: data ? Object.keys(data) : [], 21 + latestPostsCount: data?.latestPosts?.length || 0, 22 + dynamicLinksCount: data?.dynamicLinks ? 'hasData' : 'noData' 23 + }); 24 + 13 25 // Set a brief timeout to ensure the browser has time to determine locale 14 26 setTimeout(() => { 15 27 localeLoaded = true; 28 + debug.debug('Locale loaded state updated', { localeLoaded: true }); 16 29 }, 10); 17 30 }); 31 + 32 + // Debug data changes 33 + $effect(() => { 34 + if (data) { 35 + debug.state('MainPage', 'data', { 36 + latestPostsCount: data.latestPosts?.length || 0, 37 + dynamicLinksType: typeof data.dynamicLinks, 38 + hasLatestPosts: !!data.latestPosts?.length, 39 + hasDynamicLinks: !!data.dynamicLinks 40 + }); 41 + } 42 + }); 43 + 44 + // Debug locale changes 45 + $effect(() => { 46 + debug.state('MainPage', 'localeLoaded', { localeLoaded }); 47 + }); 18 48 </script> 19 49 20 50 <svelte:head> ··· 55 85 <!-- Latest Blog Post section (only show if we have posts) --> 56 86 {#if data.latestPosts && data.latestPosts.length > 0} 57 87 <LatestBlogPost posts={data.latestPosts} {localeLoaded} /> 88 + {:else} 89 + {#if data.latestPosts !== undefined} 90 + <div class="text-center py-8 text-gray-500"> 91 + No blog posts available 92 + </div> 93 + {/if} 58 94 {/if} 59 95 60 96 <DynamicLinks data={data.dynamicLinks} />
+70 -11
src/routes/api/og/main.png/+server.ts
··· 1 1 import { generateOgImage } from '$lib/server/ogImage'; 2 2 import { dev } from '$app/environment'; 3 + import { createFileDebugger } from '$lib/utils/debug.js'; 4 + 5 + const debug = createFileDebugger('api/og/main.png/+server.ts'); 3 6 4 7 export const GET = async ({ url }) => { 5 - const baseUrl = dev ? url.origin : 'https://ewancroft.uk'; 8 + debug.enter('GET', { 9 + url: url.toString(), 10 + userAgent: url.searchParams.get('user-agent') || 'unknown', 11 + referer: url.searchParams.get('referer') || 'none' 12 + }); 6 13 7 - const pngBuffer = await generateOgImage({ 8 - title: "Ewan's Corner", 9 - subtitle: 'personal site, blog, and digital garden', 10 - }, baseUrl); 14 + const timer = debug.time('mainOgImageGeneration'); 11 15 12 - return new Response(new Uint8Array(pngBuffer), { 13 - headers: { 14 - 'Content-Type': 'image/png', 15 - 'Cache-Control': 'public, max-age=86400', 16 - }, 17 - }); 16 + try { 17 + debug.info('Processing main OG image request', { 18 + dev, 19 + urlOrigin: url.origin, 20 + baseUrl: dev ? url.origin : 'https://ewancroft.uk' 21 + }); 22 + 23 + const baseUrl = dev ? url.origin : 'https://ewancroft.uk'; 24 + 25 + debug.debug('Calling generateOgImage with main site options'); 26 + const pngBuffer = await generateOgImage({ 27 + title: "Ewan's Corner", 28 + subtitle: 'personal site, blog, and digital garden', 29 + }, baseUrl); 30 + 31 + debug.info('OG image generated successfully', { 32 + bufferLength: pngBuffer?.length, 33 + isBuffer: Buffer.isBuffer(pngBuffer), 34 + contentType: 'image/png' 35 + }); 36 + 37 + const response = new Response(new Uint8Array(pngBuffer), { 38 + headers: { 39 + 'Content-Type': 'image/png', 40 + 'Cache-Control': 'public, max-age=86400', 41 + }, 42 + }); 43 + 44 + debug.debug('Response created successfully', { 45 + status: response.status, 46 + headers: Object.fromEntries(response.headers.entries()) 47 + }); 48 + 49 + timer(); 50 + debug.exit('GET', { 51 + success: true, 52 + responseStatus: response.status, 53 + bufferLength: pngBuffer?.length 54 + }); 55 + 56 + return response; 57 + } catch (error) { 58 + debug.errorWithContext('Failed to generate main OG image', error as Error, { 59 + function: 'GET', 60 + url: url.toString() 61 + }); 62 + 63 + timer(); 64 + debug.exit('GET', { 65 + success: false, 66 + error: error 67 + }); 68 + 69 + // Return an error response 70 + return new Response('Failed to generate OG image', { 71 + status: 500, 72 + headers: { 73 + 'Content-Type': 'text/plain', 74 + }, 75 + }); 76 + } 18 77 };
+5
static/robots.txt
··· 1 + User-agent: * 2 + Allow: / 3 + 4 + # Sitemap 5 + Sitemap: https://ewancroft.uk/sitemap.xml
+124
static/scripts/sw.js
··· 1 + // Service Worker for website performance optimization 2 + const CACHE_NAME = 'website-cache-v1'; 3 + const STATIC_CACHE = 'static-cache-v1'; 4 + const DYNAMIC_CACHE = 'dynamic-cache-v1'; 5 + 6 + // Files to cache immediately 7 + const STATIC_FILES = [ 8 + '/', 9 + '/fonts/ArrowType-Recursive-1.085/Recursive_Web/woff2_variable/Recursive_VF_1.085.woff2', 10 + '/scripts/themeLoader.js', 11 + '/favicon/favicon-32x32.png', 12 + '/favicon/apple-touch-icon.png' 13 + ]; 14 + 15 + // Install event - cache static files 16 + self.addEventListener('install', (event) => { 17 + event.waitUntil( 18 + caches.open(STATIC_CACHE) 19 + .then((cache) => { 20 + console.log('Caching static files'); 21 + return cache.addAll(STATIC_FILES); 22 + }) 23 + .catch((error) => { 24 + console.log('Failed to cache static files:', error); 25 + }) 26 + ); 27 + }); 28 + 29 + // Activate event - clean up old caches 30 + self.addEventListener('activate', (event) => { 31 + event.waitUntil( 32 + caches.keys().then((cacheNames) => { 33 + return Promise.all( 34 + cacheNames.map((cacheName) => { 35 + if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) { 36 + console.log('Deleting old cache:', cacheName); 37 + return caches.delete(cacheName); 38 + } 39 + }) 40 + ); 41 + }) 42 + ); 43 + }); 44 + 45 + // Fetch event - serve from cache when possible 46 + self.addEventListener('fetch', (event) => { 47 + const { request } = event; 48 + const url = new URL(request.url); 49 + 50 + // Skip non-GET requests 51 + if (request.method !== 'GET') { 52 + return; 53 + } 54 + 55 + // Skip non-HTTP requests 56 + if (!url.protocol.startsWith('http')) { 57 + return; 58 + } 59 + 60 + // Handle different types of requests 61 + if (request.destination === 'font') { 62 + // Cache fonts aggressively 63 + event.respondWith(cacheFirst(request, STATIC_CACHE)); 64 + } else if (request.destination === 'image') { 65 + // Cache images with network fallback 66 + event.respondWith(cacheFirst(request, DYNAMIC_CACHE)); 67 + } else if (request.destination === 'script' || request.destination === 'style') { 68 + // Cache scripts and styles 69 + event.respondWith(cacheFirst(request, STATIC_CACHE)); 70 + } else { 71 + // For other requests, try network first 72 + event.respondWith(networkFirst(request, DYNAMIC_CACHE)); 73 + } 74 + }); 75 + 76 + // Cache first strategy - check cache first, fallback to network 77 + async function cacheFirst(request, cacheName) { 78 + const cachedResponse = await caches.match(request); 79 + if (cachedResponse) { 80 + return cachedResponse; 81 + } 82 + 83 + try { 84 + const networkResponse = await fetch(request); 85 + if (networkResponse.ok) { 86 + const cache = await caches.open(cacheName); 87 + cache.put(request, networkResponse.clone()); 88 + } 89 + return networkResponse; 90 + } catch (error) { 91 + // Return a fallback response if available 92 + return new Response('Network error', { status: 408 }); 93 + } 94 + } 95 + 96 + // Network first strategy - try network first, fallback to cache 97 + async function networkFirst(request, cacheName) { 98 + try { 99 + const networkResponse = await fetch(request); 100 + if (networkResponse.ok) { 101 + const cache = await caches.open(cacheName); 102 + cache.put(request, networkResponse.clone()); 103 + } 104 + return networkResponse; 105 + } catch (error) { 106 + const cachedResponse = await caches.match(request); 107 + if (cachedResponse) { 108 + return cachedResponse; 109 + } 110 + return new Response('Network error', { status: 408 }); 111 + } 112 + } 113 + 114 + // Background sync for offline actions 115 + self.addEventListener('sync', (event) => { 116 + if (event.tag === 'background-sync') { 117 + event.waitUntil(doBackgroundSync()); 118 + } 119 + }); 120 + 121 + async function doBackgroundSync() { 122 + // Handle any background sync tasks 123 + console.log('Background sync triggered'); 124 + }
+17 -9
static/scripts/themeLoader.js
··· 33 33 COLOR: "color-theme", 34 34 }; 35 35 36 + // Cache DOM element for better performance 37 + const docElement = document.documentElement; 38 + const themeClassList = docElement.classList; 39 + 36 40 /** 37 41 * Applies theme classes to the document element 38 42 */ 39 43 function applyTheme(isDarkMode, themeId) { 40 - // Remove all existing theme classes 41 - document.documentElement.classList.remove("light"); 42 - THEMES.forEach((theme) => { 43 - if (theme.id !== "default") { 44 - document.documentElement.classList.remove(theme.id); 45 - } 46 - }); 44 + // Remove all existing theme classes efficiently 45 + themeClassList.remove("light"); 46 + 47 + // Only remove non-default themes to reduce DOM operations 48 + if (themeId !== "default") { 49 + THEMES.forEach((theme) => { 50 + if (theme.id !== "default") { 51 + themeClassList.remove(theme.id); 52 + } 53 + }); 54 + } 47 55 48 56 // Apply light mode class if needed 49 57 if (!isDarkMode) { 50 - document.documentElement.classList.add("light"); 58 + themeClassList.add("light"); 51 59 } 52 60 53 61 // Apply color theme class if not default 54 62 if (themeId !== "default") { 55 - document.documentElement.classList.add(themeId); 63 + themeClassList.add(themeId); 56 64 } 57 65 } 58 66
+21
static/sitemap.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> 3 + <url> 4 + <loc>https://ewancroft.uk/</loc> 5 + <lastmod>2025-01-11</lastmod> 6 + <changefreq>weekly</changefreq> 7 + <priority>1.0</priority> 8 + </url> 9 + <url> 10 + <loc>https://ewancroft.uk/blog</loc> 11 + <lastmod>2025-01-11</lastmod> 12 + <changefreq>daily</changefreq> 13 + <priority>0.8</priority> 14 + </url> 15 + <url> 16 + <loc>https://ewancroft.uk/now</loc> 17 + <lastmod>2025-01-11</lastmod> 18 + <changefreq>monthly</changefreq> 19 + <priority>0.6</priority> 20 + </url> 21 + </urlset>
+3 -1
svelte.config.js
··· 11 11 // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 12 12 // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 13 13 // See https://svelte.dev/docs/kit/adapters for more information about adapters. 14 - adapter: adapter(), 14 + adapter: adapter({ 15 + runtime: 'nodejs20.x' 16 + }), 15 17 prerender: { 16 18 entries: ['*'], 17 19 origin: 'https://ewancroft.uk'
+24
vercel.json
··· 1 + { 2 + "routes": [ 3 + { 4 + "src": "/favicon.ico", 5 + "dest": "/static/favicon/favicon-32x32.png" 6 + }, 7 + { 8 + "src": "/apple-touch-icon.png", 9 + "dest": "/static/favicon/apple-touch-icon.png" 10 + }, 11 + { 12 + "src": "/apple-touch-icon-precomposed.png", 13 + "dest": "/static/favicon/apple-touch-icon.png" 14 + }, 15 + { 16 + "src": "/robots.txt", 17 + "dest": "/static/robots.txt" 18 + }, 19 + { 20 + "src": "/sitemap.xml", 21 + "dest": "/static/sitemap.xml" 22 + } 23 + ] 24 + }
+46
vite.config.ts
··· 8 8 }, 9 9 build: { 10 10 target: "esnext", 11 + // Increase chunk size warning limit while we optimize 12 + chunkSizeWarningLimit: 800, 13 + // Enable source maps for debugging (disable in production if needed) 14 + sourcemap: false, 15 + // Optimize chunk splitting 16 + rollupOptions: { 17 + output: { 18 + // Better chunk naming for caching 19 + chunkFileNames: (chunkInfo) => { 20 + const facadeModuleId = chunkInfo.facadeModuleId ? chunkInfo.facadeModuleId.split('/').pop() : 'chunk'; 21 + return `js/${facadeModuleId}-[hash].js`; 22 + }, 23 + // Optimize asset naming 24 + assetFileNames: (assetInfo) => { 25 + if (!assetInfo.name) return 'assets/[name]-[hash].[ext]'; 26 + 27 + const info = assetInfo.name.split('.'); 28 + const ext = info[info.length - 1]; 29 + if (/\.(css)$/.test(assetInfo.name)) { 30 + return `css/[name]-[hash].${ext}`; 31 + } 32 + if (/\.(png|jpe?g|gif|svg|webp|avif)$/.test(assetInfo.name)) { 33 + return `images/[name]-[hash].${ext}`; 34 + } 35 + if (/\.(woff2?|eot|ttf|otf)$/.test(assetInfo.name)) { 36 + return `fonts/[name]-[hash].${ext}`; 37 + } 38 + return `assets/[name]-[hash].${ext}`; 39 + } 40 + } 41 + } 11 42 }, 43 + // Optimize dependencies 44 + optimizeDeps: { 45 + include: ['svelte'], 46 + exclude: ['@resvg/resvg-js'] // Exclude heavy dependencies from pre-bundling 47 + }, 48 + // Server optimizations for development 49 + server: { 50 + hmr: { 51 + overlay: false 52 + } 53 + }, 54 + // Define environment variables for build optimization 55 + define: { 56 + __BUILD_TIME__: JSON.stringify(new Date().toISOString()) 57 + } 12 58 });