+85
-48
README.md
+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
+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
-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
src/components/content/GrainImageGallery.astro
+14
-14
src/components/content/WhitewindBlogPost.astro
+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
+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
+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
+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>