A personal website powered by Astro and ATProto

blog uses lex-cli types

Changed files
+350 -170
src
components
lib
components
config
pages
+85 -48
README.md
··· 1 - # ATProto Personal Website 1 + # ATproto Personal Website 2 2 3 - A personal website powered by ATProto, featuring real-time streaming, repository browsing, and type generation. 3 + A personal website built with Astro that uses your ATproto repository as a CMS. Features full type safety, real-time updates, and automatic component routing. 4 4 5 5 ## Features 6 6 7 - ### 🚀 Real-time Streaming 8 - - **Jetstream Test** (`/jetstream-test`): Real-time ATProto streaming with DID filtering 9 - - Uses the same jetstream endpoint as atptools for low-latency updates 10 - - Filters by your configured DID for personalized streaming 7 + - **Type-Safe Content**: Automatic TypeScript type generation from ATproto lexicon schemas 8 + - **Real-Time Updates**: Live content streaming via ATproto Jetstream 9 + - **Component Registry**: Type-safe mapping of lexicon types to Astro components 10 + - **Dynamic Routing**: Automatic component selection based on record types 11 + - **Blog Support**: Full blog post rendering with markdown support 12 + - **Gallery Display**: Image galleries with EXIF data and hover effects 13 + - **Pagination**: Fetch more than 100 records with cursor-based pagination 11 14 12 - ### 🌐 Repository Browsing 13 - - **ATProto Browser Test** (`/atproto-browser-test`): Browse any ATProto account's collections and records 14 - - Discover all collections in a repository 15 - - View records from specific collections 16 - - Similar functionality to atptools 15 + ## Quick Start 16 + 17 + 1. **Configure Environment**: 18 + ```bash 19 + cp env.example .env 20 + # Edit .env with your ATproto handle and DID 21 + ``` 22 + 23 + 2. **Install Dependencies**: 24 + ```bash 25 + npm install 26 + ``` 27 + 28 + 3. **Add Lexicon Schemas**: 29 + - Place JSON lexicon schemas in `src/lexicons/` 30 + - Update `src/lib/config/site.ts` with your lexicon sources 31 + - Run `npm run gen:types` to generate TypeScript types 17 32 18 - ### 📝 Type Generation 19 - - **Lexicon Generator Test** (`/lexicon-generator-test`): Generate TypeScript types for all lexicons in your repository 20 - - Automatically discovers all lexicon types from your configured account 21 - - Generates proper TypeScript interfaces and helper functions 22 - - Copy to clipboard or download as `.ts` file 33 + 4. **Create Components**: 34 + - Create Astro components in `src/components/content/` 35 + - Register them in `src/lib/components/registry.ts` 36 + - Use `ContentDisplay.astro` for automatic routing 23 37 24 - ### 🖼️ Image Galleries 25 - - **Image Galleries** (`/galleries`): View grain.social image galleries and photo collections 38 + 5. **Start Development**: 39 + ```bash 40 + npm run dev 41 + ``` 26 42 27 - ## Configuration 43 + ## Lexicon Integration 28 44 29 - The site is configured to use your ATProto account: 45 + The system provides full type safety for ATproto lexicons: 30 46 31 - - **Handle**: `tynanpurdy.com` 32 - - **DID**: `did:plc:6ayddqghxhciedbaofoxkcbs` 33 - - **PDS**: `https://bsky.social` 47 + 1. **Schema Files**: JSON lexicon definitions in `src/lexicons/` 48 + 2. **Type Generation**: Automatic TypeScript type generation 49 + 3. **Component Registry**: Type-safe mapping of lexicon types to components 50 + 4. **Content Display**: Dynamic component routing 34 51 35 - ## Development 52 + See [LEXICON_INTEGRATION.md](./LEXICON_INTEGRATION.md) for detailed instructions. 36 53 37 - ```bash 38 - npm install 39 - npm run dev 40 - ``` 54 + ## Available Scripts 41 55 42 - Visit `http://localhost:4324` to see the site. 56 + - `npm run dev` - Start development server 57 + - `npm run build` - Build for production 58 + - `npm run preview` - Preview production build 59 + - `npm run discover` - Discover collections from your repo 60 + - `npm run gen:types` - Generate TypeScript types from lexicon schemas 43 61 44 62 ## Project Structure 45 63 46 64 ``` 47 65 src/ 48 - ├── lib/atproto/ 49 - │ ├── atproto-browser.ts # Repository browsing functionality 50 - │ ├── jetstream-client.ts # Real-time streaming client 51 - │ └── client.ts # Basic ATProto client 52 - ├── pages/ 53 - │ ├── index.astro # Homepage with navigation 54 - │ ├── jetstream-test.astro # Real-time streaming test 55 - │ ├── atproto-browser-test.astro # Repository browsing test 56 - │ ├── lexicon-generator-test.astro # Type generation test 57 - │ └── galleries.astro # Image galleries 58 - └── components/ 59 - └── content/ # Content display components 66 + ├── components/content/ # Content display components 67 + ├── lib/ 68 + │ ├── atproto/ # ATproto client and utilities 69 + │ ├── components/ # Component registry 70 + │ ├── config/ # Site configuration 71 + │ ├── generated/ # Generated TypeScript types 72 + │ ├── services/ # Content services 73 + │ └── types/ # Type definitions 74 + ├── lexicons/ # Lexicon schema files 75 + └── pages/ # Astro pages 60 76 ``` 61 77 62 - ## Technologies 78 + ## Configuration 63 79 64 - - **Astro**: Web framework 65 - - **ATProto API**: For repository access and streaming 66 - - **TypeScript**: For type safety 67 - - **Tailwind CSS**: For styling 80 + The system is configured via environment variables and `src/lib/config/site.ts`: 81 + 82 + - `ATPROTO_HANDLE` - Your Bluesky handle 83 + - `ATPROTO_DID` - Your DID (optional, auto-resolved) 84 + - `SITE_TITLE` - Site title 85 + - `SITE_DESCRIPTION` - Site description 86 + - `SITE_AUTHOR` - Site author 87 + 88 + ## Adding New Content Types 89 + 90 + 1. Create a lexicon schema in `src/lexicons/` 91 + 2. Add it to `lexiconSources` in site config 92 + 3. Run `npm run gen:types` 93 + 4. Create a component in `src/components/content/` 94 + 5. Register it in `src/lib/components/registry.ts` 95 + 96 + The system will automatically route records to your components with full type safety. 68 97 69 - ## Inspired By 98 + ## Development 70 99 71 - This project takes inspiration from [atptools](https://github.com/espeon/atptools) for repository browsing and jetstream streaming approaches. 100 + - Debug mode shows component routing information 101 + - Generic fallback for unknown record types 102 + - Real-time updates via Jetstream 103 + - Type-safe component registry 104 + - Automatic type generation from schemas 105 + 106 + ## License 107 + 108 + MIT
+163 -2
package-lock.json
··· 20 20 "tailwindcss": "^4.1.11", 21 21 "tsx": "^4.19.2", 22 22 "typescript": "^5.9.2" 23 + }, 24 + "devDependencies": { 25 + "@atproto/lex-cli": "^0.9.1" 23 26 } 24 27 }, 25 28 "node_modules/@ampproject/remapping": { ··· 200 203 "multiformats": "^9.9.0", 201 204 "uint8arrays": "3.0.0", 202 205 "zod": "^3.23.8" 206 + } 207 + }, 208 + "node_modules/@atproto/lex-cli": { 209 + "version": "0.9.1", 210 + "resolved": "https://registry.npmjs.org/@atproto/lex-cli/-/lex-cli-0.9.1.tgz", 211 + "integrity": "sha512-ftcUZd8rElHeUJq6pTcQkURnTEe7woCF4I1NK3j5GpT/itacEZtcppabjy5o2aUsbktZsALj3ch3xm7ZZ+Zp0w==", 212 + "dev": true, 213 + "license": "MIT", 214 + "dependencies": { 215 + "@atproto/lexicon": "^0.4.12", 216 + "@atproto/syntax": "^0.4.0", 217 + "chalk": "^4.1.2", 218 + "commander": "^9.4.0", 219 + "prettier": "^3.2.5", 220 + "ts-morph": "^24.0.0", 221 + "yesno": "^0.4.0", 222 + "zod": "^3.23.8" 223 + }, 224 + "bin": { 225 + "lex": "dist/index.js" 226 + }, 227 + "engines": { 228 + "node": ">=18.7.0" 229 + } 230 + }, 231 + "node_modules/@atproto/lex-cli/node_modules/ansi-styles": { 232 + "version": "4.3.0", 233 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 234 + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 235 + "dev": true, 236 + "license": "MIT", 237 + "dependencies": { 238 + "color-convert": "^2.0.1" 239 + }, 240 + "engines": { 241 + "node": ">=8" 242 + }, 243 + "funding": { 244 + "url": "https://github.com/chalk/ansi-styles?sponsor=1" 245 + } 246 + }, 247 + "node_modules/@atproto/lex-cli/node_modules/chalk": { 248 + "version": "4.1.2", 249 + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 250 + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 251 + "dev": true, 252 + "license": "MIT", 253 + "dependencies": { 254 + "ansi-styles": "^4.1.0", 255 + "supports-color": "^7.1.0" 256 + }, 257 + "engines": { 258 + "node": ">=10" 259 + }, 260 + "funding": { 261 + "url": "https://github.com/chalk/chalk?sponsor=1" 203 262 } 204 263 }, 205 264 "node_modules/@atproto/lexicon": { ··· 1859 1918 "vite": "^5.2.0 || ^6 || ^7" 1860 1919 } 1861 1920 }, 1921 + "node_modules/@ts-morph/common": { 1922 + "version": "0.25.0", 1923 + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.25.0.tgz", 1924 + "integrity": "sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg==", 1925 + "dev": true, 1926 + "license": "MIT", 1927 + "dependencies": { 1928 + "minimatch": "^9.0.4", 1929 + "path-browserify": "^1.0.1", 1930 + "tinyglobby": "^0.2.9" 1931 + } 1932 + }, 1862 1933 "node_modules/@types/debug": { 1863 1934 "version": "4.1.12", 1864 1935 "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", ··· 2289 2360 "url": "https://github.com/sponsors/wooorm" 2290 2361 } 2291 2362 }, 2363 + "node_modules/balanced-match": { 2364 + "version": "1.0.2", 2365 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 2366 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 2367 + "dev": true, 2368 + "license": "MIT" 2369 + }, 2292 2370 "node_modules/base-64": { 2293 2371 "version": "1.0.0", 2294 2372 "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", ··· 2357 2435 "url": "https://github.com/sponsors/sindresorhus" 2358 2436 } 2359 2437 }, 2438 + "node_modules/brace-expansion": { 2439 + "version": "2.0.2", 2440 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", 2441 + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", 2442 + "dev": true, 2443 + "license": "MIT", 2444 + "dependencies": { 2445 + "balanced-match": "^1.0.0" 2446 + } 2447 + }, 2360 2448 "node_modules/braces": { 2361 2449 "version": "3.0.3", 2362 2450 "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", ··· 2598 2686 "node": ">=6" 2599 2687 } 2600 2688 }, 2689 + "node_modules/code-block-writer": { 2690 + "version": "13.0.3", 2691 + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", 2692 + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", 2693 + "dev": true, 2694 + "license": "MIT" 2695 + }, 2601 2696 "node_modules/color": { 2602 2697 "version": "4.2.3", 2603 2698 "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", ··· 2649 2744 "funding": { 2650 2745 "type": "github", 2651 2746 "url": "https://github.com/sponsors/wooorm" 2747 + } 2748 + }, 2749 + "node_modules/commander": { 2750 + "version": "9.5.0", 2751 + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", 2752 + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", 2753 + "dev": true, 2754 + "license": "MIT", 2755 + "engines": { 2756 + "node": "^12.20.0 || >=14" 2652 2757 } 2653 2758 }, 2654 2759 "node_modules/common-ancestor-path": { ··· 3185 3290 "radix3": "^1.1.2", 3186 3291 "ufo": "^1.6.1", 3187 3292 "uncrypto": "^0.1.3" 3293 + } 3294 + }, 3295 + "node_modules/has-flag": { 3296 + "version": "4.0.0", 3297 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 3298 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 3299 + "dev": true, 3300 + "license": "MIT", 3301 + "engines": { 3302 + "node": ">=8" 3188 3303 } 3189 3304 }, 3190 3305 "node_modules/hast-util-from-html": { ··· 4707 4822 "url": "https://github.com/sponsors/jonschlinkert" 4708 4823 } 4709 4824 }, 4825 + "node_modules/minimatch": { 4826 + "version": "9.0.5", 4827 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", 4828 + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", 4829 + "dev": true, 4830 + "license": "ISC", 4831 + "dependencies": { 4832 + "brace-expansion": "^2.0.1" 4833 + }, 4834 + "engines": { 4835 + "node": ">=16 || 14 >=14.17" 4836 + }, 4837 + "funding": { 4838 + "url": "https://github.com/sponsors/isaacs" 4839 + } 4840 + }, 4710 4841 "node_modules/minipass": { 4711 4842 "version": "7.1.2", 4712 4843 "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", ··· 5039 5170 "version": "3.6.2", 5040 5171 "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", 5041 5172 "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", 5173 + "devOptional": true, 5042 5174 "license": "MIT", 5043 - "optional": true, 5044 - "peer": true, 5045 5175 "bin": { 5046 5176 "prettier": "bin/prettier.cjs" 5047 5177 }, ··· 5631 5761 "url": "https://github.com/chalk/strip-ansi?sponsor=1" 5632 5762 } 5633 5763 }, 5764 + "node_modules/supports-color": { 5765 + "version": "7.2.0", 5766 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 5767 + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 5768 + "dev": true, 5769 + "license": "MIT", 5770 + "dependencies": { 5771 + "has-flag": "^4.0.0" 5772 + }, 5773 + "engines": { 5774 + "node": ">=8" 5775 + } 5776 + }, 5634 5777 "node_modules/tailwindcss": { 5635 5778 "version": "4.1.11", 5636 5779 "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", ··· 5736 5879 "funding": { 5737 5880 "type": "github", 5738 5881 "url": "https://github.com/sponsors/wooorm" 5882 + } 5883 + }, 5884 + "node_modules/ts-morph": { 5885 + "version": "24.0.0", 5886 + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-24.0.0.tgz", 5887 + "integrity": "sha512-2OAOg/Ob5yx9Et7ZX4CvTCc0UFoZHwLEJ+dpDPSUi5TgwwlTlX47w+iFRrEwzUZwYACjq83cgjS/Da50Ga37uw==", 5888 + "dev": true, 5889 + "license": "MIT", 5890 + "dependencies": { 5891 + "@ts-morph/common": "~0.25.0", 5892 + "code-block-writer": "^13.0.3" 5739 5893 } 5740 5894 }, 5741 5895 "node_modules/tsconfck": { ··· 6761 6915 "engines": { 6762 6916 "node": ">=8" 6763 6917 } 6918 + }, 6919 + "node_modules/yesno": { 6920 + "version": "0.4.0", 6921 + "resolved": "https://registry.npmjs.org/yesno/-/yesno-0.4.0.tgz", 6922 + "integrity": "sha512-tdBxmHvbXPBKYIg81bMCB7bVeDmHkRzk5rVJyYYXurwKkHq/MCd8rz4HSJUP7hW0H2NlXiq8IFiWvYKEHhlotA==", 6923 + "dev": true, 6924 + "license": "BSD" 6764 6925 }, 6765 6926 "node_modules/yocto-queue": { 6766 6927 "version": "1.2.1",
+6 -1
package.json
··· 6 6 "dev": "astro dev", 7 7 "build": "astro build", 8 8 "preview": "astro preview", 9 - "astro": "astro" 9 + "astro": "astro", 10 + "discover": "tsx scripts/discover-collections.ts", 11 + "gen:types": "tsx scripts/generate-types.ts" 10 12 }, 11 13 "dependencies": { 12 14 "@astrojs/check": "^0.9.4", ··· 21 23 "tailwindcss": "^4.1.11", 22 24 "tsx": "^4.19.2", 23 25 "typescript": "^5.9.2" 26 + }, 27 + "devDependencies": { 28 + "@atproto/lex-cli": "^0.9.1" 24 29 } 25 30 }
+1
src/components/content/GrainImageGallery.astro
··· 1 1 --- 2 2 import type { GrainImageGallery } from '../../lib/types/atproto'; 3 + import type { SocialGrainGallery } from '../../lib/generated/social-grain-gallery'; 3 4 4 5 interface Props { 5 6 gallery: GrainImageGallery;
+14 -14
src/components/content/WhitewindBlogPost.astro
··· 1 1 --- 2 - import type { WhitewindBlogPost } from '../../lib/types/atproto'; 3 - import { marked } from 'marked'; 2 + import type { ComWhtwndBlogEntryRecord } from '../../lib/generated/com-whtwnd-blog-entry'; 4 3 5 4 interface Props { 6 - post: WhitewindBlogPost; 5 + record: ComWhtwndBlogEntryRecord; 7 6 showTags?: boolean; 8 7 showTimestamp?: boolean; 9 8 } 10 9 11 - const { post, showTags = true, showTimestamp = true } = Astro.props; 10 + const { record, showTags = true, showTimestamp = true } = Astro.props; 12 11 13 12 const formatDate = (dateString: string) => { 14 13 return new Date(dateString).toLocaleDateString('en-US', { ··· 18 17 }); 19 18 }; 20 19 21 - // Prefer explicit publishedAt; fall back to createdAt or indexedAt if present 22 - const publishedRaw = (post as any).publishedAt || (post as any).createdAt || (post as any).indexedAt || null; 23 - const publishedSafe = publishedRaw && !isNaN(new Date(publishedRaw).getTime()) ? publishedRaw : null; 20 + const published = record.createdAt; 21 + const isValidDate = published ? !isNaN(new Date(published).getTime()) : false; 24 22 --- 25 23 26 24 <article class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6 mb-6"> 27 25 <header class="mb-4"> 28 26 <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2"> 29 - {post.title} 27 + {record.title} 30 28 </h2> 31 29 32 - {showTimestamp && publishedSafe && ( 30 + {showTimestamp && isValidDate && ( 33 31 <div class="text-sm text-gray-500 dark:text-gray-400 mb-3"> 34 - Published on {formatDate(publishedSafe)} 32 + Published on {formatDate(published!)} 35 33 </div> 36 34 )} 37 35 38 - {showTags && post.tags && post.tags.length > 0 && ( 36 + {showTags && record.tags && record.tags.length > 0 && ( 39 37 <div class="flex flex-wrap gap-2 mb-4"> 40 - {post.tags.map((tag) => ( 38 + {record.tags.map((tag) => ( 41 39 <span class="px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 text-xs rounded-full"> 42 40 #{tag} 43 41 </span> ··· 46 44 )} 47 45 </header> 48 46 49 - <div class="prose prose-white dark:prose-invert max-w-none"> 50 - <Fragment set:html={await marked(post.content || '')} /> 47 + <div class="prose prose-gray dark:prose-invert max-w-none"> 48 + <div class="text-gray-700 dark:text-gray-300 leading-relaxed"> 49 + {record.content} 50 + </div> 51 51 </div> 52 52 </article>
+47 -47
src/lib/components/registry.ts
··· 1 - import type { ContentComponent } from '../types/atproto'; 1 + import type { GeneratedLexiconUnion, GeneratedLexiconTypeMap } from '../generated/lexicon-types'; 2 2 3 - // Component registry for type-safe content rendering 4 - class ComponentRegistry { 5 - private components = new Map<string, ContentComponent>(); 3 + // Type-safe component registry 4 + export interface ComponentRegistryEntry<T = any> { 5 + component: string; 6 + props?: T; 7 + } 6 8 7 - // Register a component for a specific content type 8 - register(type: string, component: any, props?: Record<string, any>): void { 9 - this.components.set(type, { 10 - type, 11 - component, 12 - props, 13 - }); 14 - } 9 + export type ComponentRegistry = { 10 + [K in keyof GeneratedLexiconTypeMap]?: ComponentRegistryEntry; 11 + } & { 12 + [key: string]: ComponentRegistryEntry; // Fallback for unknown types 13 + }; 15 14 16 - // Get a component for a specific content type 17 - get(type: string): ContentComponent | undefined { 18 - return this.components.get(type); 19 - } 20 - 21 - // Check if a component exists for a content type 22 - has(type: string): boolean { 23 - return this.components.has(type); 24 - } 25 - 26 - // Get all registered component types 27 - getRegisteredTypes(): string[] { 28 - return Array.from(this.components.keys()); 29 - } 15 + // Default registry - add your components here 16 + export const registry: ComponentRegistry = { 17 + 'ComWhtwndBlogEntry': { 18 + component: 'WhitewindBlogPost', 19 + props: {} 20 + }, 21 + // Add more mappings as you create components 22 + // 'ComExampleRecord': { 23 + // component: 'ExampleComponent', 24 + // props: {} 25 + // } 26 + }; 30 27 31 - // Clear all registered components 32 - clear(): void { 33 - this.components.clear(); 34 - } 28 + // Type-safe component lookup 29 + export function getComponentInfo<T extends keyof GeneratedLexiconTypeMap>( 30 + $type: T 31 + ): ComponentRegistryEntry | null { 32 + return registry[$type] || null; 35 33 } 36 34 37 - // Global component registry instance 38 - export const componentRegistry = new ComponentRegistry(); 39 - 40 - // Type-safe component registration helper 41 - export function registerComponent( 42 - type: string, 43 - component: any, 44 - props?: Record<string, any> 35 + // Helper to register a new component 36 + export function registerComponent<T extends keyof GeneratedLexiconTypeMap>( 37 + $type: T, 38 + component: string, 39 + props?: any 45 40 ): void { 46 - componentRegistry.register(type, component, props); 41 + registry[$type] = { component, props }; 47 42 } 48 43 49 - // Type-safe component retrieval helper 50 - export function getComponent(type: string): ContentComponent | undefined { 51 - return componentRegistry.get(type); 52 - } 53 - 54 - // Check if content type has a registered component 55 - export function hasComponent(type: string): boolean { 56 - return componentRegistry.has(type); 44 + // Auto-assignment for unknown types (fallback) 45 + export function autoAssignComponent($type: string): ComponentRegistryEntry { 46 + // Convert NSID to component name 47 + const parts = $type.split('.'); 48 + const componentName = parts[parts.length - 1] 49 + .split('-') 50 + .map(part => part.charAt(0).toUpperCase() + part.slice(1)) 51 + .join(''); 52 + 53 + return { 54 + component: componentName, 55 + props: {} 56 + }; 57 57 }
+12
src/lib/config/site.ts
··· 33 33 content: { 34 34 defaultFeedLimit: number; 35 35 cacheTTL: number; // in milliseconds 36 + collections: string[]; 37 + maxRecords: number; 36 38 }; 39 + lexiconSources: Record<string, string>; // NSID -> local schema file path 37 40 } 38 41 39 42 // Default configuration with static handle ··· 58 61 content: { 59 62 defaultFeedLimit: 20, 60 63 cacheTTL: 5 * 60 * 1000, // 5 minutes 64 + collections: ['app.bsky.feed.post', 'com.whtwnd.blog.entry'], 65 + maxRecords: 500, 66 + }, 67 + lexiconSources: { 68 + 'com.whtwnd.blog.entry': './src/lexicons/com.whtwnd.blog.entry.json', 69 + // Add more NSIDs -> schema file mappings here 61 70 }, 62 71 }; 63 72 ··· 89 98 content: { 90 99 defaultFeedLimit: parseInt(process.env.CONTENT_DEFAULT_FEED_LIMIT || '20'), 91 100 cacheTTL: parseInt(process.env.CONTENT_CACHE_TTL || '300000'), 101 + collections: defaultConfig.content.collections, // Keep default collections 102 + maxRecords: parseInt(process.env.CONTENT_MAX_RECORDS || '500'), 92 103 }, 104 + lexiconSources: defaultConfig.lexiconSources, // Keep default lexicon sources 93 105 }; 94 106 }
+22 -58
src/pages/index.astro
··· 6 6 const config = loadConfig(); 7 7 --- 8 8 9 - <Layout title="Home Page"> 9 + <Layout title="Home"> 10 10 <div class="container mx-auto px-4 py-8"> 11 - <header class="text-center mb-12"> 11 + <header class="mb-8"> 12 12 <h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4"> 13 - Welcome to {config.site.title || 'Tynanverse'} 13 + Welcome to {config.site.title} 14 14 </h1> 15 - <p class="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto"> 16 - {config.site.description || 'A personal website powered by ATproto, made by Tynan'} 15 + <p class="text-xl text-gray-600 dark:text-gray-300 mb-6"> 16 + {config.site.description} 17 17 </p> 18 + <nav class="flex space-x-4"> 19 + <a href="/" class="text-blue-600 dark:text-blue-400 hover:underline">Home</a> 20 + <a href="/blog" class="text-blue-600 dark:text-blue-400 hover:underline">Blog</a> 21 + <a href="/galleries" class="text-blue-600 dark:text-blue-400 hover:underline">Galleries</a> 22 + </nav> 18 23 </header> 19 24 20 - <main class="max-w-4xl mx-auto space-y-12"> 21 - <!-- My Posts Feed --> 22 - {config.atproto.handle && config.atproto.handle !== 'your-handle-here' ? ( 23 - <> 24 - <section> 25 - <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6"> 26 - My Posts 27 - </h2> 28 - <ContentFeed 29 - limit={10} 30 - showAuthor={false} 31 - showTimestamp={true} 32 - live={true} 33 - /> 34 - </section> 35 - 36 - 37 - 38 - <section> 39 - <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6"> 40 - Explore More 41 - </h2> 42 - <div class="grid md:grid-cols-2 gap-6"> 43 - <a href="/galleries" class="block p-6 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:shadow-md transition-shadow"> 44 - <h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2"> 45 - Image Galleries 46 - </h3> 47 - <p class="text-gray-600 dark:text-gray-400"> 48 - View my grain.social image galleries and photo collections. 49 - </p> 50 - </a> 51 - </div> 52 - </section> 53 - </> 54 - ) : ( 55 - <section> 56 - <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6"> 57 - Configuration Required 58 - </h2> 59 - <div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-6"> 60 - <p class="text-yellow-800 dark:text-yellow-200 mb-4"> 61 - To display your posts, please configure your Bluesky handle in the environment variables. 62 - </p> 63 - <div class="text-sm text-yellow-700 dark:text-yellow-300"> 64 - <p class="mb-2">Create a <code class="bg-yellow-100 dark:bg-yellow-800 px-1 rounded">.env</code> file with:</p> 65 - <pre class="bg-yellow-100 dark:bg-yellow-800 p-3 rounded text-xs overflow-x-auto"> 66 - ATPROTO_HANDLE=your-handle.bsky.social 67 - SITE_TITLE=Your Site Title 68 - SITE_AUTHOR=Your Name</pre> 69 - </div> 70 - </div> 71 - </section> 72 - )} 25 + <main> 26 + <section class="mb-8"> 27 + <h2 class="text-2xl font-semibold text-gray-900 dark:text-white mb-4"> 28 + Latest Posts 29 + </h2> 30 + <ContentFeed 31 + limit={10} 32 + showAuthor={false} 33 + showTimestamp={true} 34 + live={true} 35 + /> 36 + </section> 73 37 </main> 74 38 </div> 75 39 </Layout>