Your calm window into the Atmosphere. morgen.blue
rss atproto
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: port SPEC.md and skyreader lexicons from feat-auth

Bring the design spec and the four planned custom lexicons
(app.skyreader.feed.saved, .feed.subscription, .social.follow,
.social.share) back into the repo so Claude has the feature
orientation available during the Laravel rebuild. Nothing compiles
against these yet — they're reference material.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+588
+292
SPEC.md
··· 1 + # Morgenblau Spec 2 + 3 + > Single source of truth for product vision, content model, and guardrails. 4 + > Keep high-level. Update when core decisions change, not for every feature. 5 + 6 + --- 7 + 8 + <vision> 9 + 10 + ## What is Morgenblau? 11 + 12 + A calm content platform powered by RSS and ATProto. Not a classic RSS reader — a window into the Atmosphere that organizes content into finite daily digests instead of infinite feeds. 13 + 14 + **Core emotional promise:** Intentionality without deprivation. You still get the good stuff, but on your terms. 15 + 16 + **Target users:** People who want to consume content (blogs, microposts, videos, podcasts) without the anxiety of unread counts or the pull of endless scrolling. They value the open web and the ATProto ecosystem. 17 + 18 + **What makes it different:** 19 + 20 + - Daily digests instead of an unread inbox 21 + - Social layer via ATProto backlinks — RSS becomes interactive 22 + - Four first-class content types with dedicated UIs 23 + - The "editor of your own publication" identity — you curate sources, not manage subscriptions 24 + 25 + </vision> 26 + 27 + --- 28 + 29 + <modes> 30 + 31 + ## Three Modes (Product Roadmap) 32 + 33 + | Mode | Status | Description | 34 + | -------- | ------ | -------------------------------------------------------------- | 35 + | Consume | v1 | Daily digests, four content types, social layer, custom player | 36 + | Discover | Future | Find new sources via ATProto social graph, link extraction | 37 + | Create | Future | Post to Bluesky (and later long-form via standard.site) | 38 + 39 + **v1 is Consume only.** Discovery and Creation are future modes. 40 + 41 + </modes> 42 + 43 + --- 44 + 45 + <platform> 46 + 47 + ## Platform 48 + 49 + **v1 is web-only.** No native apps, no PWA. Browser-first experience. 50 + 51 + </platform> 52 + 53 + --- 54 + 55 + <authentication> 56 + 57 + ## Authentication 58 + 59 + **ATProto OAuth only.** Users must log in with an ATProto account. There is no anonymous or email-based access. This unlocks the social layer from day one. 60 + 61 + </authentication> 62 + 63 + --- 64 + 65 + <daily-digests> 66 + 67 + ## Daily Digests 68 + 69 + The core consumption model. Content is organized into daily editions rather than a continuous feed. 70 + 71 + ### Editions 72 + 73 + The app fetches feeds at set times throughout the day (morning, lunch, evening). Whatever is new since the last fetch goes into that edition. 74 + 75 + - **Morning edition** — fetched early morning 76 + - **Lunch edition** — fetched around midday 77 + - **Evening edition** — fetched in the evening 78 + 79 + Previous days show as complete daily digests (all editions merged). 80 + 81 + ### Empty Editions 82 + 83 + An empty edition is a feature, not a bug. Display a simple, calm message. No nudges, no guilt. Example: _"Nothing new this morning. Enjoy your coffee."_ 84 + 85 + ### History 86 + 87 + Rolling window of past digests (exact retention TBD, roughly 30 days). Older content fades away — reinforces the daily mindset. 88 + 89 + ### No Read Tracking 90 + 91 + No read state. No progress indicators. No "you've seen 8 of 12." Each edition simply exists. Content is not marked, dimmed, or tracked. 92 + 93 + </daily-digests> 94 + 95 + --- 96 + 97 + <content-types> 98 + 99 + ## Four Content Types 100 + 101 + All four are first-class citizens in v1, each with a UI optimized for its format. 102 + 103 + | Type | Description | Playback | 104 + | --------- | ---------------------------------- | ------------------------ | 105 + | Blogpost | Articles with titles and body text | In-app reader + link out | 106 + | Micropost | Short posts without a title | Inline in digest | 107 + | Video | YouTube, Vimeo, etc. | Custom player | 108 + | Podcast | Audio feeds | Custom player | 109 + 110 + ### Reading Mode 111 + 112 + In-app reader by default — fetch and render article content directly. Users can always open the original URL. Both options available. 113 + 114 + ### Media Playback 115 + 116 + Custom video and audio player UI that matches Morgenblau's design language. Not YouTube iframes or bare HTML audio elements. 117 + 118 + </content-types> 119 + 120 + --- 121 + 122 + <windows> 123 + 124 + ## Windows 125 + 126 + Windows are filtered views onto your content — different lenses to look through. 127 + 128 + ### Default Windows (Predefined) 129 + 130 + The app provides default windows based on content type (e.g., Blogposts, Videos, Podcasts, Microposts). 131 + 132 + ### Custom Windows (User-Created) 133 + 134 + Users can create their own windows with custom filter criteria — by tags, sources, content types, or combinations. 135 + 136 + ### Default Landing 137 + 138 + When a user opens Morgenblau, they land on **today's digest** — a unified view of the current day's content across all sources. Windows are available for filtering from there. 139 + 140 + </windows> 141 + 142 + --- 143 + 144 + <feed-sources> 145 + 146 + ## Feed Sources 147 + 148 + ### Adding Sources 149 + 150 + Users manually add RSS/Atom feed URLs. No auto-discovery in v1. Each subscription is stored as an `app.skyreader.feed.subscription` record in the user's ATProto repo. 151 + 152 + ### Organization 153 + 154 + Flat list of subscriptions. No folders or categories — Windows handle the filtering/viewing. 155 + 156 + ### Primary Sources 157 + 158 + Users can mark feeds as **primary sources**. These receive prominent placement in the digest — front-page treatment. 159 + 160 + </feed-sources> 161 + 162 + --- 163 + 164 + <social-layer> 165 + 166 + ## Social Layer (ATProto) 167 + 168 + The core differentiator. For each piece of content, the app checks for ATProto backlinks and displays social context alongside it. 169 + 170 + ### v1 Scope 171 + 172 + - **Read:** Show Bluesky likes, reposts, and reply threads found via backlinks 173 + - **Like:** Users can like content from within Morgenblau 174 + - **Follow:** In-app follows stored as `app.skyreader.social.follow` records (separate from Bluesky social graph follows) 175 + - No reposting, replying, or other interactions in v1 176 + 177 + ### UX Principle 178 + 179 + Social context is available but not forced. The reading experience comes first. Reactions are opt-in per article — shown only if the user wants to see them. 180 + 181 + </social-layer> 182 + 183 + --- 184 + 185 + <atproto-lexicons> 186 + 187 + ## ATProto Lexicons 188 + 189 + Morgenblau uses [Skyreader's](https://github.com/disnet/skyreader) lexicons (`app.skyreader.*`) for all user data stored in ATProto repos. This enables interoperability — data written by Morgenblau can be read by Skyreader and vice versa. 190 + 191 + Vendored lexicon schemas live in `lexicons/app/skyreader/`. 192 + 193 + | Feature | NSID | Schema | 194 + | ------------------ | --------------------------------- | ----------------------------------------------- | 195 + | Feed subscriptions | `app.skyreader.feed.subscription` | `lexicons/app/skyreader/feed/subscription.json` | 196 + | Saved articles | `app.skyreader.feed.saved` | `lexicons/app/skyreader/feed/saved.json` | 197 + | Shared articles | `app.skyreader.social.share` | `lexicons/app/skyreader/social/share.json` | 198 + | In-app follows | `app.skyreader.social.follow` | `lexicons/app/skyreader/social/follow.json` | 199 + 200 + </atproto-lexicons> 201 + 202 + --- 203 + 204 + <saving-sharing> 205 + 206 + ## Saving & Sharing 207 + 208 + Simple and minimal. 209 + 210 + - Users can **save** individual articles to a separate saved-items view — stored as `app.skyreader.feed.saved` records 211 + - Users can **share** articles with optional commentary — stored as `app.skyreader.social.share` records 212 + - No folders, tags, or organization for saved content — just a list 213 + 214 + </saving-sharing> 215 + 216 + --- 217 + 218 + <navigation> 219 + 220 + ## Navigation 221 + 222 + ### Day Navigation 223 + 224 + **Calendar strip** — horizontal strip of days. Tap a day to see its digest. 225 + 226 + ### Digest Layout 227 + 228 + **Vertical feed** — simple top-to-bottom scroll of cards. Clean and predictable. Primary sources may receive larger or more prominent cards. 229 + 230 + </navigation> 231 + 232 + --- 233 + 234 + <anti-features> 235 + 236 + ## Anti-Features 237 + 238 + Things Morgenblau will never do. 239 + 240 + ### Hard No 241 + 242 + - **No unread counts.** Never show unread badges, counts, or inbox-zero mechanics. This is the foundational design principle. 243 + 244 + ### Open for Future (Tasteful Only) 245 + 246 + - **Notifications** — could see optional edition-ready notifications ("Your morning edition is ready"), but never content-level push notifications 247 + - **Smart ranking** — could consider light curation in the future, but never engagement-based algorithmic sorting 248 + 249 + </anti-features> 250 + 251 + --- 252 + 253 + <brand> 254 + 255 + ## Brand 256 + 257 + ### Texture 258 + 259 + **Crisp morning.** Clear, sharp, awake — not warm and cozy. The terrace on a clear morning, not the candlelit cafe. 260 + 261 + - Clean sans-serifs 262 + - Cool blues 263 + - Precise transitions 264 + 265 + ### Core Metaphors 266 + 267 + - **The window** — something you choose to look through, then step away from. It never follows you around. What you see through it is finite and tied to today. 268 + - **The newspaper** — not the layout, but the feeling. A finite object with a clear start and end. A ritual, not a habit. 269 + 270 + ### Identity 271 + 272 + Users aren't "managing subscriptions." They're the **editor of their own daily publication** — choosing sources, deciding who gets the front page. 273 + 274 + </brand> 275 + 276 + --- 277 + 278 + <tech-stack> 279 + 280 + ## Tech Stack (Quick Reference) 281 + 282 + | Layer | Technology | 283 + | --------------- | ----------------------------------- | 284 + | Framework | React 19, TypeScript | 285 + | Router | TanStack React Router + React Start | 286 + | Styling | Tailwind CSS v4 | 287 + | Animation | Motion, Three.js (shaders) | 288 + | Components | Base UI | 289 + | Build | Vite | 290 + | Package Manager | bun | 291 + 292 + </tech-stack>
+74
lexicons/app/skyreader/feed/saved.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.skyreader.feed.saved", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A saved item extracted from a URL", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["url", "savedAt"], 12 + "properties": { 13 + "url": { 14 + "type": "string", 15 + "format": "uri", 16 + "maxLength": 2048, 17 + "description": "The original article URL" 18 + }, 19 + "title": { 20 + "type": "string", 21 + "maxLength": 1000, 22 + "description": "Extracted article title" 23 + }, 24 + "description": { 25 + "type": "string", 26 + "maxLength": 5000, 27 + "description": "Extracted article description" 28 + }, 29 + "fullContent": { 30 + "type": "string", 31 + "maxLength": 100000, 32 + "description": "Extracted HTML content (reserved for future selective backup)" 33 + }, 34 + "contentType": { 35 + "type": "string", 36 + "maxLength": 64, 37 + "description": "Content type of the saved item (e.g. webpage, pdf, epub)" 38 + }, 39 + "author": { 40 + "type": "string", 41 + "maxLength": 256, 42 + "description": "Extracted author name" 43 + }, 44 + "domain": { 45 + "type": "string", 46 + "maxLength": 256, 47 + "description": "Source domain" 48 + }, 49 + "image": { 50 + "type": "string", 51 + "format": "uri", 52 + "maxLength": 2048, 53 + "description": "Hero image URL" 54 + }, 55 + "publishedAt": { 56 + "type": "string", 57 + "format": "datetime", 58 + "description": "Original publish date" 59 + }, 60 + "savedAt": { 61 + "type": "string", 62 + "format": "datetime", 63 + "description": "When the user saved this item" 64 + }, 65 + "wordCount": { 66 + "type": "integer", 67 + "minimum": 0, 68 + "description": "Word count" 69 + } 70 + } 71 + } 72 + } 73 + } 74 + }
+93
lexicons/app/skyreader/feed/subscription.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.skyreader.feed.subscription", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A subscription to an RSS/Atom feed or AT Protocol content stream", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["createdAt"], 12 + "properties": { 13 + "feedUrl": { 14 + "type": "string", 15 + "format": "uri", 16 + "maxLength": 2048, 17 + "description": "The URL of the RSS/Atom feed. Required for RSS subscriptions." 18 + }, 19 + "title": { 20 + "type": "string", 21 + "maxLength": 512, 22 + "description": "User-provided or auto-detected feed title" 23 + }, 24 + "siteUrl": { 25 + "type": "string", 26 + "format": "uri", 27 + "maxLength": 2048, 28 + "description": "The main website URL associated with the feed" 29 + }, 30 + "category": { 31 + "type": "string", 32 + "maxLength": 128, 33 + "description": "User-defined category/folder for organization" 34 + }, 35 + "tags": { 36 + "type": "array", 37 + "maxLength": 10, 38 + "items": { 39 + "type": "string", 40 + "maxLength": 64 41 + }, 42 + "description": "User-defined tags for the subscription" 43 + }, 44 + "createdAt": { 45 + "type": "string", 46 + "format": "datetime" 47 + }, 48 + "updatedAt": { 49 + "type": "string", 50 + "format": "datetime" 51 + }, 52 + "source": { 53 + "type": "string", 54 + "maxLength": 64, 55 + "description": "Origin of this subscription (e.g., 'leaflet', 'opml', 'manual')" 56 + }, 57 + "externalRef": { 58 + "type": "string", 59 + "format": "at-uri", 60 + "maxLength": 2048, 61 + "description": "Reference to the external source record (e.g., Leaflet subscription at-uri)" 62 + }, 63 + "sourceType": { 64 + "type": "string", 65 + "maxLength": 64, 66 + "description": "Content source type: 'rss', 'atproto.shares', 'atproto.documents', 'atproto.collection'. Omitted means RSS." 67 + }, 68 + "subjectDid": { 69 + "type": "string", 70 + "maxLength": 2048, 71 + "description": "The AT Protocol account DID. Required for atproto.* source types." 72 + }, 73 + "collectionNsid": { 74 + "type": "string", 75 + "maxLength": 256, 76 + "description": "Collection NSID for atproto.collection source type (future extensibility)." 77 + }, 78 + "customTitle": { 79 + "type": "string", 80 + "maxLength": 512, 81 + "description": "User-set custom display title override" 82 + }, 83 + "customIconUrl": { 84 + "type": "string", 85 + "format": "uri", 86 + "maxLength": 2048, 87 + "description": "User-set custom icon URL override" 88 + } 89 + } 90 + } 91 + } 92 + } 93 + }
+26
lexicons/app/skyreader/social/follow.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.skyreader.social.follow", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "An in-app follow relationship to another Skyreader user", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["subject", "createdAt"], 12 + "properties": { 13 + "subject": { 14 + "type": "string", 15 + "format": "did", 16 + "description": "DID of the user being followed" 17 + }, 18 + "createdAt": { 19 + "type": "string", 20 + "format": "datetime" 21 + } 22 + } 23 + } 24 + } 25 + } 26 + }
+103
lexicons/app/skyreader/social/share.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.skyreader.social.share", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A shared article with optional commentary", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["itemUrl", "createdAt"], 12 + "properties": { 13 + "subscriptionUri": { 14 + "type": "string", 15 + "format": "at-uri", 16 + "description": "AT URI of the source subscription (optional)" 17 + }, 18 + "feedUrl": { 19 + "type": "string", 20 + "format": "uri", 21 + "maxLength": 2048, 22 + "description": "URL of the RSS feed this article came from" 23 + }, 24 + "itemUrl": { 25 + "type": "string", 26 + "format": "uri", 27 + "maxLength": 2048, 28 + "description": "The URL of the shared article" 29 + }, 30 + "itemTitle": { 31 + "type": "string", 32 + "maxLength": 512, 33 + "description": "Title of the shared article" 34 + }, 35 + "itemAuthor": { 36 + "type": "string", 37 + "maxLength": 256, 38 + "description": "Author of the article" 39 + }, 40 + "itemDescription": { 41 + "type": "string", 42 + "maxLength": 1000, 43 + "description": "Brief description/excerpt from the article" 44 + }, 45 + "content": { 46 + "type": "string", 47 + "maxLength": 100000, 48 + "description": "Article content (HTML)" 49 + }, 50 + "itemImage": { 51 + "type": "string", 52 + "format": "uri", 53 + "maxLength": 2048, 54 + "description": "Preview image URL for the article" 55 + }, 56 + "itemGuid": { 57 + "type": "string", 58 + "maxLength": 512, 59 + "description": "Unique identifier (guid) of the article from the feed" 60 + }, 61 + "itemPublishedAt": { 62 + "type": "string", 63 + "format": "datetime", 64 + "description": "Original publish date of the article" 65 + }, 66 + "note": { 67 + "type": "string", 68 + "maxLength": 3000, 69 + "description": "User's commentary or note about the share" 70 + }, 71 + "tags": { 72 + "type": "array", 73 + "maxLength": 5, 74 + "items": { 75 + "type": "string", 76 + "maxLength": 64 77 + } 78 + }, 79 + "createdAt": { 80 + "type": "string", 81 + "format": "datetime" 82 + }, 83 + "reshareOf": { 84 + "type": "object", 85 + "description": "Reference to the share being reshared", 86 + "properties": { 87 + "uri": { 88 + "type": "string", 89 + "format": "at-uri", 90 + "description": "AT URI of the share being reshared" 91 + }, 92 + "authorDid": { 93 + "type": "string", 94 + "description": "DID of the author of the original share" 95 + } 96 + }, 97 + "required": ["uri", "authorDid"] 98 + } 99 + } 100 + } 101 + } 102 + } 103 + }