your personal website on atproto - mirror
blento.app
1# External data sources for sections
2
3Allow sections to be auto-filled from external data (e.g. Grain gallery photos, standard.site blog posts) instead of manually curated cards.
4
5## Goals
6
7- Sections can pull items from external sources (ATProto collections, web APIs, etc.) without the user manually creating each card.
8- Extension path mirrors the existing card `loadData` pattern so there's one consistent idea in the codebase.
9- A section can always fall back to manual content (same UX as today).
10- Multiple sections on a page can use different sources; a single page can mix sourced and manual sections.
11
12## Proposed API
13
14### SectionDefinition additions
15
16```ts
17type SectionDefinition = {
18 // existing...
19
20 // Optional external data fetch. Result stored in WebsiteData.additionalData[section.id].
21 loadData?: (section: SectionRecord, ctx: { did; handle; cache? }) => Promise<unknown>;
22 loadDataServer?: (
23 section: SectionRecord,
24 ctx: { did; handle; cache?; env?; platform? }
25 ) => Promise<unknown>;
26};
27```
28
29Mirrors the card-level `loadData`/`loadDataServer`. Reuses the same cache service + KV infrastructure.
30
31### sectionData shape
32
33Each section that supports sourcing stores its source config in `sectionData`:
34
35```ts
36// Manual (default)
37sectionData: { source: { provider: 'manual' }, ...otherStuff }
38
39// Sourced from Grain
40sectionData: {
41 source: { provider: 'grain-gallery', galleryUri: 'at://did:.../xyz.grain.photo.gallery/abc' }
42}
43
44// Sourced from standard.site blog
45sectionData: {
46 source: { provider: 'standard-site-blog', did: 'did:...', limit: 10 }
47}
48```
49
50### Source registration
51
52Sources are pluggable modules that can be used by one or more section types:
53
54```ts
55// src/lib/sections/sources/types.ts
56type SectionSource = {
57 id: string; // e.g. 'grain-gallery'
58 name: string; // user-facing name
59 supportedSections: string[]; // e.g. ['gallery']
60 configComponent: Component<{
61 config: any;
62 onchange: (next: any) => void;
63 }>;
64 loadData?: (config: any, ctx: any) => Promise<unknown>;
65 loadDataServer?: (config: any, ctx: any) => Promise<unknown>;
66};
67```
68
69Registered in a central `AllSectionSources` array like cards/sections are. Section types query the list filtered by `supportedSections`.
70
71Example: `src/lib/sections/sources/grain-gallery.ts` registers a source with `supportedSections: ['gallery']` and a `loadDataServer` that fetches photos via the Grain lexicon.
72
73### Rendering flow
74
75The section's `contentComponent` receives:
76
77- `items` (manual cards assigned to the section, as today)
78- `externalData` (from `additionalData[section.id]`)
79
80If `sectionData.source.provider !== 'manual'`, the section renders from `externalData`; otherwise it renders `items`. Optionally, a section could combine both (e.g. pinned manual items + auto-pulled items).
81
82External items are "synthetic": they look like `Item`s at the render layer (enough for `BaseCard` / `Card` to render them) but they have no PDS record and no `sectionId`. They reuse existing card types — a Grain photo renders as a synthetic `image` card, a blog post as a synthetic `link` card. No new card components needed.
83
84### Editing UX
85
86- Section settings popover/drawer gets a **Source** dropdown listing compatible sources (derived from `AllSectionSources` filtered to the current section type)
87- When the source is non-manual, the inline card-editing UI is replaced with the source's `configComponent` (e.g. "Paste Grain gallery URI")
88- Switching source back to "Manual" restores the manual editing UX (and the existing cards resurface)
89- The source's config is persisted in `sectionData.source`
90
91## Implementation order
92
931. Add `loadData` / `loadDataServer` to `SectionDefinition` and wire into `load.ts` (server-side fan-out like cards already do).
942. Add the sources registry (`src/lib/sections/sources/index.ts`).
953. Teach `contentComponent` / `editingContentComponent` to branch on `sectionData.source.provider`.
964. Build one real source end-to-end: **Grain gallery → Gallery section** (infrastructure already exists in `PhotoGalleryCard`).
975. Follow with: **standard.site blog → Row section** (requires a blog-post card first, or a synthetic link card with richer metadata).
98
99## Open questions
100
101- Should sources be able to return card-type-specific `cardData`, or should they always return a normalised shape? Probably card-type-specific for flexibility.
102- Pagination: do sources need to know about "load more" or infinite scroll? For now assume batch-fetch (limit in config).
103- Caching: per-section cache key by `section.id + section.updatedAt`? Or by source config hash so multiple sections with the same source share cache? Probably the latter.
104- Mixing sourced + manual: start with mutually exclusive (source OR manual), add mixing later if needed.
105- Mobile UX for the Source dropdown and config UI: same treatment as the card editing popovers.