A personal website powered by Astro and ATProto

leaflets!

+231
package-lock.json
··· 11 11 "@astrojs/check": "^0.9.4", 12 12 "@atproto/api": "^0.16.2", 13 13 "@atproto/xrpc": "^0.7.1", 14 + "@nulfrost/leaflet-loader-astro": "^1.1.0", 14 15 "@tailwindcss/typography": "^0.5.16", 15 16 "@tailwindcss/vite": "^4.1.11", 16 17 "@types/node": "^24.2.0", ··· 177 178 "yaml": "^2.5.0" 178 179 } 179 180 }, 181 + "node_modules/@atcute/client": { 182 + "version": "4.0.3", 183 + "resolved": "https://registry.npmjs.org/@atcute/client/-/client-4.0.3.tgz", 184 + "integrity": "sha512-RIOZWFVLca/HiPAAUDqQPOdOreCxTbL5cb+WUf5yqQOKIu5yEAP3eksinmlLmgIrlr5qVOE7brazUUzaskFCfw==", 185 + "license": "MIT", 186 + "dependencies": { 187 + "@atcute/identity": "^1.0.2", 188 + "@atcute/lexicons": "^1.0.3" 189 + } 190 + }, 191 + "node_modules/@atcute/identity": { 192 + "version": "1.0.3", 193 + "resolved": "https://registry.npmjs.org/@atcute/identity/-/identity-1.0.3.tgz", 194 + "integrity": "sha512-mNMxbKHFGys03A8JXKk0KfMBzdd0vrYMzZZWjpw1nYTs0+ea6bo5S1hwqVUZxHdo1gFHSe/t63jxQIF4yL9aKw==", 195 + "license": "0BSD", 196 + "dependencies": { 197 + "@atcute/lexicons": "^1.0.4", 198 + "@badrap/valita": "^0.4.5" 199 + } 200 + }, 201 + "node_modules/@atcute/lexicons": { 202 + "version": "1.1.0", 203 + "resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-1.1.0.tgz", 204 + "integrity": "sha512-LFqwnria78xLYb62Ri/+WwQpUTgZp2DuyolNGIIOV1dpiKhFFFh//nscHMA6IExFLQRqWDs3tTjy7zv0h3sf1Q==", 205 + "license": "0BSD", 206 + "dependencies": { 207 + "esm-env": "^1.2.2" 208 + } 209 + }, 180 210 "node_modules/@atproto/api": { 181 211 "version": "0.16.2", 182 212 "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.16.2.tgz", ··· 334 364 }, 335 365 "engines": { 336 366 "node": ">=6.9.0" 367 + } 368 + }, 369 + "node_modules/@badrap/valita": { 370 + "version": "0.4.6", 371 + "resolved": "https://registry.npmjs.org/@badrap/valita/-/valita-0.4.6.tgz", 372 + "integrity": "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==", 373 + "license": "MIT", 374 + "engines": { 375 + "node": ">= 18" 337 376 } 338 377 }, 339 378 "node_modules/@capsizecss/unpack": { ··· 1269 1308 }, 1270 1309 "engines": { 1271 1310 "node": ">= 8" 1311 + } 1312 + }, 1313 + "node_modules/@nulfrost/leaflet-loader-astro": { 1314 + "version": "1.1.0", 1315 + "resolved": "https://registry.npmjs.org/@nulfrost/leaflet-loader-astro/-/leaflet-loader-astro-1.1.0.tgz", 1316 + "integrity": "sha512-A6ONOmds3/3pVFfa+YdpC5YMfOF1shvczAOnSWfVtUYz3bl3NRz26KieUrGW+26iVPgUtHPRLQOfygImrLrhYw==", 1317 + "license": "MIT", 1318 + "dependencies": { 1319 + "@atcute/client": "^4.0.3", 1320 + "@atcute/lexicons": "^1.1.0", 1321 + "@atproto/api": "^0.16.2", 1322 + "katex": "^0.16.22", 1323 + "sanitize-html": "^2.17.0" 1272 1324 } 1273 1325 }, 1274 1326 "node_modules/@oslojs/encoding": { ··· 2850 2902 "url": "https://github.com/sponsors/wooorm" 2851 2903 } 2852 2904 }, 2905 + "node_modules/deepmerge": { 2906 + "version": "4.3.1", 2907 + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", 2908 + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", 2909 + "license": "MIT", 2910 + "engines": { 2911 + "node": ">=0.10.0" 2912 + } 2913 + }, 2853 2914 "node_modules/defu": { 2854 2915 "version": "6.1.4", 2855 2916 "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", ··· 2932 2993 "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", 2933 2994 "license": "MIT" 2934 2995 }, 2996 + "node_modules/dom-serializer": { 2997 + "version": "2.0.0", 2998 + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", 2999 + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 3000 + "license": "MIT", 3001 + "dependencies": { 3002 + "domelementtype": "^2.3.0", 3003 + "domhandler": "^5.0.2", 3004 + "entities": "^4.2.0" 3005 + }, 3006 + "funding": { 3007 + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" 3008 + } 3009 + }, 3010 + "node_modules/dom-serializer/node_modules/entities": { 3011 + "version": "4.5.0", 3012 + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 3013 + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", 3014 + "license": "BSD-2-Clause", 3015 + "engines": { 3016 + "node": ">=0.12" 3017 + }, 3018 + "funding": { 3019 + "url": "https://github.com/fb55/entities?sponsor=1" 3020 + } 3021 + }, 3022 + "node_modules/domelementtype": { 3023 + "version": "2.3.0", 3024 + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 3025 + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", 3026 + "funding": [ 3027 + { 3028 + "type": "github", 3029 + "url": "https://github.com/sponsors/fb55" 3030 + } 3031 + ], 3032 + "license": "BSD-2-Clause" 3033 + }, 3034 + "node_modules/domhandler": { 3035 + "version": "5.0.3", 3036 + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", 3037 + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 3038 + "license": "BSD-2-Clause", 3039 + "dependencies": { 3040 + "domelementtype": "^2.3.0" 3041 + }, 3042 + "engines": { 3043 + "node": ">= 4" 3044 + }, 3045 + "funding": { 3046 + "url": "https://github.com/fb55/domhandler?sponsor=1" 3047 + } 3048 + }, 3049 + "node_modules/domutils": { 3050 + "version": "3.2.2", 3051 + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", 3052 + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", 3053 + "license": "BSD-2-Clause", 3054 + "dependencies": { 3055 + "dom-serializer": "^2.0.0", 3056 + "domelementtype": "^2.3.0", 3057 + "domhandler": "^5.0.3" 3058 + }, 3059 + "funding": { 3060 + "url": "https://github.com/fb55/domutils?sponsor=1" 3061 + } 3062 + }, 2935 3063 "node_modules/dotenv": { 2936 3064 "version": "17.2.1", 2937 3065 "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", ··· 3067 3195 "funding": { 3068 3196 "url": "https://github.com/sponsors/sindresorhus" 3069 3197 } 3198 + }, 3199 + "node_modules/esm-env": { 3200 + "version": "1.2.2", 3201 + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", 3202 + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", 3203 + "license": "MIT" 3070 3204 }, 3071 3205 "node_modules/estree-walker": { 3072 3206 "version": "3.0.3", ··· 3505 3639 "url": "https://github.com/sponsors/wooorm" 3506 3640 } 3507 3641 }, 3642 + "node_modules/htmlparser2": { 3643 + "version": "8.0.2", 3644 + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", 3645 + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", 3646 + "funding": [ 3647 + "https://github.com/fb55/htmlparser2?sponsor=1", 3648 + { 3649 + "type": "github", 3650 + "url": "https://github.com/sponsors/fb55" 3651 + } 3652 + ], 3653 + "license": "MIT", 3654 + "dependencies": { 3655 + "domelementtype": "^2.3.0", 3656 + "domhandler": "^5.0.3", 3657 + "domutils": "^3.0.1", 3658 + "entities": "^4.4.0" 3659 + } 3660 + }, 3661 + "node_modules/htmlparser2/node_modules/entities": { 3662 + "version": "4.5.0", 3663 + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 3664 + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", 3665 + "license": "BSD-2-Clause", 3666 + "engines": { 3667 + "node": ">=0.12" 3668 + }, 3669 + "funding": { 3670 + "url": "https://github.com/fb55/entities?sponsor=1" 3671 + } 3672 + }, 3508 3673 "node_modules/http-cache-semantics": { 3509 3674 "version": "4.2.0", 3510 3675 "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", ··· 3621 3786 "url": "https://github.com/sponsors/sindresorhus" 3622 3787 } 3623 3788 }, 3789 + "node_modules/is-plain-object": { 3790 + "version": "5.0.0", 3791 + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", 3792 + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", 3793 + "license": "MIT", 3794 + "engines": { 3795 + "node": ">=0.10.0" 3796 + } 3797 + }, 3624 3798 "node_modules/is-wsl": { 3625 3799 "version": "3.1.0", 3626 3800 "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", ··· 3674 3848 "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", 3675 3849 "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==", 3676 3850 "license": "MIT" 3851 + }, 3852 + "node_modules/katex": { 3853 + "version": "0.16.22", 3854 + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", 3855 + "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", 3856 + "funding": [ 3857 + "https://opencollective.com/katex", 3858 + "https://github.com/sponsors/katex" 3859 + ], 3860 + "license": "MIT", 3861 + "dependencies": { 3862 + "commander": "^8.3.0" 3863 + }, 3864 + "bin": { 3865 + "katex": "cli.js" 3866 + } 3867 + }, 3868 + "node_modules/katex/node_modules/commander": { 3869 + "version": "8.3.0", 3870 + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", 3871 + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", 3872 + "license": "MIT", 3873 + "engines": { 3874 + "node": ">= 12" 3875 + } 3677 3876 }, 3678 3877 "node_modules/kleur": { 3679 3878 "version": "4.1.5", ··· 5089 5288 "url": "https://github.com/sponsors/wooorm" 5090 5289 } 5091 5290 }, 5291 + "node_modules/parse-srcset": { 5292 + "version": "1.0.2", 5293 + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", 5294 + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", 5295 + "license": "MIT" 5296 + }, 5092 5297 "node_modules/parse5": { 5093 5298 "version": "7.3.0", 5094 5299 "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", ··· 5598 5803 "license": "MIT", 5599 5804 "dependencies": { 5600 5805 "queue-microtask": "^1.2.2" 5806 + } 5807 + }, 5808 + "node_modules/sanitize-html": { 5809 + "version": "2.17.0", 5810 + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz", 5811 + "integrity": "sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==", 5812 + "license": "MIT", 5813 + "dependencies": { 5814 + "deepmerge": "^4.2.2", 5815 + "escape-string-regexp": "^4.0.0", 5816 + "htmlparser2": "^8.0.0", 5817 + "is-plain-object": "^5.0.0", 5818 + "parse-srcset": "^1.0.2", 5819 + "postcss": "^8.3.11" 5820 + } 5821 + }, 5822 + "node_modules/sanitize-html/node_modules/escape-string-regexp": { 5823 + "version": "4.0.0", 5824 + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 5825 + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 5826 + "license": "MIT", 5827 + "engines": { 5828 + "node": ">=10" 5829 + }, 5830 + "funding": { 5831 + "url": "https://github.com/sponsors/sindresorhus" 5601 5832 } 5602 5833 }, 5603 5834 "node_modules/semver": {
+1
package.json
··· 14 14 "@astrojs/check": "^0.9.4", 15 15 "@atproto/api": "^0.16.2", 16 16 "@atproto/xrpc": "^0.7.1", 17 + "@nulfrost/leaflet-loader-astro": "^1.1.0", 17 18 "@tailwindcss/typography": "^0.5.16", 18 19 "@tailwindcss/vite": "^4.1.11", 19 20 "@types/node": "^24.2.0",
+13
src/content.config.ts
··· 1 + import { defineCollection, z } from "astro:content"; 2 + import { leafletStaticLoader } from "@nulfrost/leaflet-loader-astro"; 3 + import { loadConfig } from "./lib/config/site"; 4 + 5 + const config = loadConfig(); 6 + 7 + const documents = defineCollection({ 8 + loader: leafletStaticLoader({ 9 + repo: config.atproto.did || config.atproto.handle || "did:plc:example" 10 + }), 11 + }); 12 + 13 + export const collections = { documents };
+1
src/pages/index.astro
··· 20 20 <a href="/" class="text-blue-600 dark:text-blue-400 hover:underline">Home</a> 21 21 <a href="/blog" class="text-blue-600 dark:text-blue-400 hover:underline">Blog</a> 22 22 <a href="/galleries" class="text-blue-600 dark:text-blue-400 hover:underline">Galleries</a> 23 + <a href="/leaflets" class="text-blue-600 dark:text-blue-400 hover:underline">Leaflets</a> 23 24 </nav> 24 25 </header> 25 26
+101
src/pages/leaflets.astro
··· 1 + --- 2 + import Layout from '../layouts/Layout.astro'; 3 + import { getCollection } from "astro:content"; 4 + import { loadConfig } from '../lib/config/site'; 5 + 6 + const config = loadConfig(); 7 + const documents = await getCollection("documents"); 8 + 9 + // Sort documents by published date (newest first) 10 + const sortedDocuments = documents.sort((a, b) => { 11 + const dateA = a.data.publishedAt ? new Date(a.data.publishedAt).getTime() : 0; 12 + const dateB = b.data.publishedAt ? new Date(b.data.publishedAt).getTime() : 0; 13 + return dateB - dateA; 14 + }); 15 + --- 16 + 17 + <Layout title="Leaflet Documents"> 18 + <div class="container mx-auto px-4 py-8"> 19 + <header class="text-center mb-12"> 20 + <h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4"> 21 + Leaflet Documents 22 + </h1> 23 + <p class="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto"> 24 + A collection of my leaflet.pub documents 25 + </p> 26 + </header> 27 + 28 + <main class="max-w-4xl mx-auto"> 29 + {config.atproto.handle && config.atproto.handle !== 'your-handle-here' ? ( 30 + sortedDocuments.length > 0 ? ( 31 + <div class="space-y-6"> 32 + {sortedDocuments.map((document) => ( 33 + <article class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6"> 34 + <header class="mb-4"> 35 + <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2"> 36 + <a href={`/leaflets/${document.id}`} class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors"> 37 + {document.data.title} 38 + </a> 39 + </h2> 40 + 41 + {document.data.description && ( 42 + <p class="text-gray-600 dark:text-gray-400 mb-3"> 43 + {document.data.description} 44 + </p> 45 + )} 46 + 47 + <div class="text-sm text-gray-500 dark:text-gray-400"> 48 + {document.data.publishedAt && ( 49 + <span class="mr-4"> 50 + Published: {new Date(document.data.publishedAt).toLocaleDateString('en-US', { 51 + year: 'numeric', 52 + month: 'long', 53 + day: 'numeric', 54 + })} 55 + </span> 56 + )} 57 + {document.data.author && ( 58 + <span>Author: {document.data.author}</span> 59 + )} 60 + </div> 61 + </header> 62 + </article> 63 + ))} 64 + </div> 65 + ) : ( 66 + <div class="text-center py-12"> 67 + <div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-8"> 68 + <h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-4"> 69 + No Leaflet Documents Found 70 + </h3> 71 + <p class="text-gray-600 dark:text-gray-400 mb-4"> 72 + No leaflet.pub documents were found for your account. 73 + </p> 74 + <p class="text-sm text-gray-500 dark:text-gray-500"> 75 + Make sure you have created documents using leaflet.pub and they are properly indexed. 76 + </p> 77 + </div> 78 + </div> 79 + ) 80 + ) : ( 81 + <div class="text-center py-12"> 82 + <div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-8"> 83 + <h3 class="text-xl font-semibold text-yellow-800 dark:text-yellow-200 mb-4"> 84 + Configuration Required 85 + </h3> 86 + <p class="text-yellow-700 dark:text-yellow-300 mb-4"> 87 + To display your Leaflet documents, please configure your Bluesky handle in the environment variables. 88 + </p> 89 + <div class="text-sm text-yellow-600 dark:text-yellow-400"> 90 + <p class="mb-2">Create a <code class="bg-yellow-100 dark:bg-yellow-800 px-1 rounded">.env</code> file with:</p> 91 + <pre class="bg-yellow-100 dark:bg-yellow-800 p-3 rounded text-xs overflow-x-auto"> 92 + ATPROTO_HANDLE=your-handle.bsky.social 93 + SITE_TITLE=Your Site Title 94 + SITE_AUTHOR=Your Name</pre> 95 + </div> 96 + </div> 97 + </div> 98 + )} 99 + </main> 100 + </div> 101 + </Layout>
+63
src/pages/leaflets/[leaflet].astro
··· 1 + --- 2 + import Layout from '../../layouts/Layout.astro'; 3 + import { getCollection, getEntry, render } from "astro:content"; 4 + 5 + export async function getStaticPaths() { 6 + const documents = await getCollection("documents"); 7 + return documents.map((document) => ({ 8 + params: { leaflet: document.id }, 9 + props: document, 10 + })); 11 + } 12 + 13 + const document = await getEntry("documents", Astro.params.leaflet); 14 + 15 + if (!document) { 16 + throw new Error(`Document with id "${Astro.params.leaflet}" not found`); 17 + } 18 + 19 + const { Content } = await render(document); 20 + --- 21 + 22 + <Layout title={document.data.title}> 23 + <div class="container mx-auto px-4 py-8"> 24 + <header class="mb-8"> 25 + <nav class="mb-6"> 26 + <a href="/leaflets" class="text-blue-600 dark:text-blue-400 hover:underline"> 27 + ← Back to Leaflets 28 + </a> 29 + </nav> 30 + 31 + <h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4"> 32 + {document.data.title} 33 + </h1> 34 + 35 + {document.data.description && ( 36 + <p class="text-xl text-gray-600 dark:text-gray-400 mb-6"> 37 + {document.data.description} 38 + </p> 39 + )} 40 + 41 + <div class="text-sm text-gray-500 dark:text-gray-400 mb-6"> 42 + {document.data.publishedAt && ( 43 + <span class="mr-4"> 44 + Published: {new Date(document.data.publishedAt).toLocaleDateString('en-US', { 45 + year: 'numeric', 46 + month: 'long', 47 + day: 'numeric', 48 + })} 49 + </span> 50 + )} 51 + {document.data.author && ( 52 + <span>Author: {document.data.author}</span> 53 + )} 54 + </div> 55 + </header> 56 + 57 + <main class="max-w-4xl mx-auto"> 58 + <article class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-8"> 59 + <Content /> 60 + </article> 61 + </main> 62 + </div> 63 + </Layout>