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.