A personal website powered by Astro and ATProto
1# Lexicon Integration Guide
2
3This guide explains how to add support for new ATproto lexicons in your Astro website. The system provides full type safety and automatic component routing.
4
5## Overview
6
7The lexicon integration system consists of:
8
91. **Schema Files**: JSON lexicon definitions in `src/lexicons/`
102. **Type Generation**: Automatic TypeScript type generation from schemas
113. **Component Registry**: Type-safe mapping of lexicon types to Astro components
124. **Content Display**: Dynamic component routing based on record types
13
14## Step-by-Step Guide
15
16### 1. Add Lexicon Schema
17
18Create a JSON schema file in `src/lexicons/` following the ATproto lexicon specification:
19
20```json
21// src/lexicons/com.example.myrecord.json
22{
23 "lexicon": 1,
24 "id": "com.example.myrecord",
25 "description": "My custom record type",
26 "defs": {
27 "main": {
28 "type": "record",
29 "key": "tid",
30 "record": {
31 "type": "object",
32 "required": ["title", "content"],
33 "properties": {
34 "title": {
35 "type": "string",
36 "description": "The title of the record"
37 },
38 "content": {
39 "type": "string",
40 "description": "The content of the record"
41 },
42 "tags": {
43 "type": "array",
44 "items": {
45 "type": "string"
46 },
47 "description": "Tags for the record"
48 }
49 }
50 }
51 }
52 }
53}
54```
55
56### 2. Update Site Configuration
57
58Add the lexicon to your site configuration in `src/lib/config/site.ts`:
59
60```typescript
61export const defaultConfig: SiteConfig = {
62 // ... existing config
63 lexiconSources: {
64 'com.whtwnd.blog.entry': './src/lexicons/com.whtwnd.blog.entry.json',
65 'com.example.myrecord': './src/lexicons/com.example.myrecord.json', // Add your new lexicon
66 },
67};
68```
69
70### 3. Generate TypeScript Types
71
72Run the type generation script:
73
74```bash
75npm run gen:types
76```
77
78This will create:
79- `src/lib/generated/com-example-myrecord.ts` - Individual type definitions
80- `src/lib/generated/lexicon-types.ts` - Union types and type maps
81
82### 4. Create Your Component
83
84Create an Astro component to display your record type. **Components receive the typed record value directly**:
85
86```astro
87---
88// src/components/content/MyRecordDisplay.astro
89import type { ComExampleMyrecord } from '../../lib/generated/com-example-myrecord';
90
91interface Props {
92 record: ComExampleMyrecord['value']; // Typed record value, not generic AtprotoRecord
93 showAuthor?: boolean;
94 showTimestamp?: boolean;
95}
96
97const { record, showAuthor = true, showTimestamp = true } = Astro.props;
98
99// The record is already typed - no casting needed!
100---
101
102<div class="my-record-display">
103 <h2 class="text-xl font-bold">{record.title}</h2>
104 <p class="text-gray-600">{record.content}</p>
105
106 {record.tags && record.tags.length > 0 && (
107 <div class="flex flex-wrap gap-2 mt-3">
108 {record.tags.map((tag: string) => (
109 <span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm">
110 {tag}
111 </span>
112 ))}
113 </div>
114 )}
115</div>
116```
117
118### 5. Register Your Component
119
120Add your component to the registry in `src/lib/components/registry.ts`:
121
122```typescript
123export const registry: ComponentRegistry = {
124 'ComWhtwndBlogEntry': {
125 component: 'WhitewindBlogPost',
126 props: {}
127 },
128 'ComExampleMyrecord': { // Add your new type
129 component: 'MyRecordDisplay',
130 props: {}
131 },
132 // ... other components
133};
134```
135
136### 6. Use Your Component
137
138Your component will now be automatically used when displaying records of your type:
139
140```astro
141---
142import ContentDisplay from '../../components/content/ContentDisplay.astro';
143import type { AtprotoRecord } from '../../lib/atproto/atproto-browser';
144
145const records: AtprotoRecord[] = await fetchRecords();
146---
147
148{records.map(record => (
149 <ContentDisplay record={record} showAuthor={true} showTimestamp={true} />
150))}
151```
152
153## Type Safety Features
154
155### Generated Types
156
157The system generates strongly typed interfaces:
158
159```typescript
160// Generated from your schema
161export interface ComExampleMyrecordRecord {
162 title: string;
163 content: string;
164 tags?: string[];
165}
166
167export interface ComExampleMyrecord {
168 $type: 'com.example.myrecord';
169 value: ComExampleMyrecordRecord;
170}
171```
172
173### Direct Type Access
174
175Components receive the typed record value directly, not the generic `AtprotoRecord`:
176
177```typescript
178// ✅ Good - Direct typed access
179interface Props {
180 record: ComExampleMyrecord['value']; // Typed record value
181}
182
183// ❌ Avoid - Generic casting
184interface Props {
185 record: AtprotoRecord; // Generic record
186}
187const myRecord = record.value as ComExampleMyrecord['value']; // Casting needed
188```
189
190### Component Registry Types
191
192The registry provides type-safe component lookup:
193
194```typescript
195// Type-safe component lookup
196const componentInfo = getComponentInfo('ComExampleMyrecord');
197// componentInfo.component will be 'MyRecordDisplay'
198// componentInfo.props will be typed correctly
199```
200
201### Automatic Fallbacks
202
203If no component is registered for a type, the system:
204
2051. Tries to auto-assign a component name based on the NSID
2062. Falls back to `GenericContentDisplay.astro` for unknown types
2073. Shows debug information in development mode
208
209## Advanced Usage
210
211### Custom Props
212
213You can pass custom props to your components:
214
215```typescript
216export const registry: ComponentRegistry = {
217 'ComExampleMyrecord': {
218 component: 'MyRecordDisplay',
219 props: {
220 showTags: true,
221 maxTags: 5
222 }
223 },
224};
225```
226
227### Multiple Record Types
228
229Support multiple record types in one component:
230
231```astro
232---
233// Handle multiple types in one component
234const recordType = record?.$type;
235
236if (recordType === 'com.example.type1') {
237 // Handle type 1 with typed access
238 const type1Record = record as ComExampleType1['value'];
239} else if (recordType === 'com.example.type2') {
240 // Handle type 2 with typed access
241 const type2Record = record as ComExampleType2['value'];
242}
243---
244```
245
246### Dynamic Component Loading
247
248The system dynamically imports components and passes typed data:
249
250```typescript
251// This happens automatically in ContentDisplay.astro
252const Component = await import(`../../components/content/${componentInfo.component}.astro`);
253// Component receives record.value (typed) instead of full AtprotoRecord
254```
255
256## Troubleshooting
257
258### Type Generation Issues
259
260If type generation fails:
261
2621. Check your JSON schema syntax
2632. Ensure the schema has a `main` record definition
2643. Verify all required fields are properly defined
265
266### Component Not Found
267
268If your component isn't being used:
269
2701. Check the registry mapping in `src/lib/components/registry.ts`
2712. Verify the component file exists in `src/components/content/`
2723. Check the component name matches the registry entry
273
274### Type Errors
275
276If you get TypeScript errors:
277
2781. Regenerate types: `npm run gen:types`
2792. Check that your component uses the correct generated types
2803. Verify your component receives `RecordType['value']` not `AtprotoRecord`
281
282## Best Practices
283
2841. **Schema Design**: Follow ATproto lexicon conventions
2852. **Type Safety**: Always use generated types in components
2863. **Direct Access**: Components receive typed data directly, no casting needed
2874. **Component Naming**: Use descriptive component names
2885. **Error Handling**: Provide fallbacks for missing data
2896. **Development**: Use debug mode to troubleshoot issues
290
291## Example: Complete Integration
292
293Here's a complete example adding support for a photo gallery lexicon:
294
2951. **Schema**: `src/lexicons/com.example.gallery.json`
2962. **Config**: Add to `lexiconSources`
2973. **Types**: Run `npm run gen:types`
2984. **Component**: Create `GalleryDisplay.astro` with typed props
2995. **Registry**: Add mapping in `registry.ts`
3006. **Usage**: Use `ContentDisplay` component
301
302The system will automatically route gallery records to your `GalleryDisplay` component with full type safety and direct typed access.