Lexicon Integration Guide#
This guide explains how to add support for new ATproto lexicons in your Astro website. The system provides full type safety and automatic component routing.
Overview#
The lexicon integration system consists of:
- Schema Files: JSON lexicon definitions in
src/lexicons/ - Type Generation: Automatic TypeScript type generation from schemas
- Component Registry: Type-safe mapping of lexicon types to Astro components
- Content Display: Dynamic component routing based on record types
Step-by-Step Guide#
1. Add Lexicon Schema#
Create a JSON schema file in src/lexicons/ following the ATproto lexicon specification:
// src/lexicons/com.example.myrecord.json
{
"lexicon": 1,
"id": "com.example.myrecord",
"description": "My custom record type",
"defs": {
"main": {
"type": "record",
"key": "tid",
"record": {
"type": "object",
"required": ["title", "content"],
"properties": {
"title": {
"type": "string",
"description": "The title of the record"
},
"content": {
"type": "string",
"description": "The content of the record"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tags for the record"
}
}
}
}
}
}
2. Update Site Configuration#
Add the lexicon to your site configuration in src/lib/config/site.ts:
export const defaultConfig: SiteConfig = {
// ... existing config
lexiconSources: {
'com.whtwnd.blog.entry': './src/lexicons/com.whtwnd.blog.entry.json',
'com.example.myrecord': './src/lexicons/com.example.myrecord.json', // Add your new lexicon
},
};
3. Generate TypeScript Types#
Run the type generation script:
npm run gen:types
This will create:
src/lib/generated/com-example-myrecord.ts- Individual type definitionssrc/lib/generated/lexicon-types.ts- Union types and type maps
4. Create Your Component#
Create an Astro component to display your record type. Components receive the typed record value directly:
---
// src/components/content/MyRecordDisplay.astro
import type { ComExampleMyrecord } from '../../lib/generated/com-example-myrecord';
interface Props {
record: ComExampleMyrecord['value']; // Typed record value, not generic AtprotoRecord
showAuthor?: boolean;
showTimestamp?: boolean;
}
const { record, showAuthor = true, showTimestamp = true } = Astro.props;
// The record is already typed - no casting needed!
---
<div class="my-record-display">
<h2 class="text-xl font-bold">{record.title}</h2>
<p class="text-gray-600">{record.content}</p>
{record.tags && record.tags.length > 0 && (
<div class="flex flex-wrap gap-2 mt-3">
{record.tags.map((tag: string) => (
<span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm">
{tag}
</span>
))}
</div>
)}
</div>
5. Register Your Component#
Add your component to the registry in src/lib/components/registry.ts:
export const registry: ComponentRegistry = {
'ComWhtwndBlogEntry': {
component: 'WhitewindBlogPost',
props: {}
},
'ComExampleMyrecord': { // Add your new type
component: 'MyRecordDisplay',
props: {}
},
// ... other components
};
6. Use Your Component#
Your component will now be automatically used when displaying records of your type:
---
import ContentDisplay from '../../components/content/ContentDisplay.astro';
import type { AtprotoRecord } from '../../lib/atproto/atproto-browser';
const records: AtprotoRecord[] = await fetchRecords();
---
{records.map(record => (
<ContentDisplay record={record} showAuthor={true} showTimestamp={true} />
))}
Type Safety Features#
Generated Types#
The system generates strongly typed interfaces:
// Generated from your schema
export interface ComExampleMyrecordRecord {
title: string;
content: string;
tags?: string[];
}
export interface ComExampleMyrecord {
$type: 'com.example.myrecord';
value: ComExampleMyrecordRecord;
}
Direct Type Access#
Components receive the typed record value directly, not the generic AtprotoRecord:
// ✅ Good - Direct typed access
interface Props {
record: ComExampleMyrecord['value']; // Typed record value
}
// ❌ Avoid - Generic casting
interface Props {
record: AtprotoRecord; // Generic record
}
const myRecord = record.value as ComExampleMyrecord['value']; // Casting needed
Component Registry Types#
The registry provides type-safe component lookup:
// Type-safe component lookup
const componentInfo = getComponentInfo('ComExampleMyrecord');
// componentInfo.component will be 'MyRecordDisplay'
// componentInfo.props will be typed correctly
Automatic Fallbacks#
If no component is registered for a type, the system:
- Tries to auto-assign a component name based on the NSID
- Falls back to
GenericContentDisplay.astrofor unknown types - Shows debug information in development mode
Advanced Usage#
Custom Props#
You can pass custom props to your components:
export const registry: ComponentRegistry = {
'ComExampleMyrecord': {
component: 'MyRecordDisplay',
props: {
showTags: true,
maxTags: 5
}
},
};
Multiple Record Types#
Support multiple record types in one component:
---
// Handle multiple types in one component
const recordType = record?.$type;
if (recordType === 'com.example.type1') {
// Handle type 1 with typed access
const type1Record = record as ComExampleType1['value'];
} else if (recordType === 'com.example.type2') {
// Handle type 2 with typed access
const type2Record = record as ComExampleType2['value'];
}
---
Dynamic Component Loading#
The system dynamically imports components and passes typed data:
// This happens automatically in ContentDisplay.astro
const Component = await import(`../../components/content/${componentInfo.component}.astro`);
// Component receives record.value (typed) instead of full AtprotoRecord
Troubleshooting#
Type Generation Issues#
If type generation fails:
- Check your JSON schema syntax
- Ensure the schema has a
mainrecord definition - Verify all required fields are properly defined
Component Not Found#
If your component isn't being used:
- Check the registry mapping in
src/lib/components/registry.ts - Verify the component file exists in
src/components/content/ - Check the component name matches the registry entry
Type Errors#
If you get TypeScript errors:
- Regenerate types:
npm run gen:types - Check that your component uses the correct generated types
- Verify your component receives
RecordType['value']notAtprotoRecord
Best Practices#
- Schema Design: Follow ATproto lexicon conventions
- Type Safety: Always use generated types in components
- Direct Access: Components receive typed data directly, no casting needed
- Component Naming: Use descriptive component names
- Error Handling: Provide fallbacks for missing data
- Development: Use debug mode to troubleshoot issues
Example: Complete Integration#
Here's a complete example adding support for a photo gallery lexicon:
- Schema:
src/lexicons/com.example.gallery.json - Config: Add to
lexiconSources - Types: Run
npm run gen:types - Component: Create
GalleryDisplay.astrowith typed props - Registry: Add mapping in
registry.ts - Usage: Use
ContentDisplaycomponent
The system will automatically route gallery records to your GalleryDisplay component with full type safety and direct typed access.