Add the curated Learning sources (#6)

Fifteen English Learning sources join ByteByteGo in the catalog: System
Design One, The Pragmatic Engineer (blog), Martin Fowler, Julia Evans,
Dan Luu, The Cloudflare Blog, Netflix Technology Blog, Engineering at
Meta, GitHub Blog: Engineering, Latent Space, Import AI, Interconnects,
Ahead of AI and Lilian Weng (on by default), and InfoQ (off).

Tests pin the Learning catalog, check that existing installs get the
new on-by-default Sources and that the Learn tab shows their posts from
the last 14 days. A live test times a full refresh. The README gets a
Learn feature bullet and a Learning sources table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
CedricandClaude Opus 5.5 committed 2026-10-16 15:50:50 +02:00
1 parent a6b8d8b537
commit b634118ed8
6 files changed
+263 -2

No files matched your search

+33
View File
@@ -8,6 +8,9 @@ or write a daily briefing. It's meant to replace the n8n → Discord workflow.
- **Today**: all articles from the enabled sources in the last 12/24/48 h, newest
first. Filter by language or source, pull to refresh.
- **Learn**: articles from the enabled Learning sources (engineering blogs,
system design explainers, AI research write-ups) from the last 14 days, newest
first. Learning sources aren't part of the Briefing.
- **Reading in the app**: tapping an article opens a clean **reader view** (text,
headings, lists, quotes, images) without ads or cookie banners. The globe icon
switches to the **original page** in an in-app WebView, for paywalls or sites
@@ -180,6 +183,36 @@ page. Hacker News and Lobsters link to articles all over the web, so reader view
depends on the linked site. Techmeme links to its own summary page, which only
has the teaser; the original page links on to the full story.
## Learning sources
Evergreen reading for building skills: research, engineering depth and system
design rather than product news. The Learn tab shows their posts from the last
14 days. All are English; ★ marks the ones enabled by default.
| | Source | Feed | Focus |
|---|---|---|---|
| 🇬🇧 ★ | ByteByteGo | `blog.bytebytego.com/feed` | System design explained with diagrams |
| 🇬🇧 ★ | System Design One | `newsletter.systemdesign.one/feed` | System design case studies of real systems |
| 🇬🇧 ★ | The Pragmatic Engineer (blog) | `blog.pragmaticengineer.com/rss/` | How big tech and startups build software (free posts) |
| 🇬🇧 ★ | Martin Fowler | `martinfowler.com/feed.atom` | Software architecture, refactoring, delivery |
| 🇬🇧 ★ | Julia Evans | `jvns.ca/atom.xml` | Systems, networking and Unix tools, explained plainly |
| 🇬🇧 ★ | Dan Luu | `danluu.com/atom.xml` | Long, data-driven essays on computers and engineering |
| 🇬🇧 ★ | The Cloudflare Blog | `blog.cloudflare.com/rss/` | Networking, security, incident write-ups |
| 🇬🇧 ★ | Netflix Technology Blog | `netflixtechblog.com/feed` | Distributed systems, data, streaming at scale |
| 🇬🇧 ★ | Engineering at Meta | `engineering.fb.com/feed/` | Infrastructure, AI, production engineering |
| 🇬🇧 ★ | GitHub Blog: Engineering | `github.blog/engineering/feed/` | How GitHub builds and runs GitHub |
| 🇬🇧 ★ | Latent Space | `www.latent.space/feed` | AI engineering: interviews and deep dives |
| 🇬🇧 ★ | Import AI | `importai.substack.com/feed` | Weekly AI research digest by Jack Clark |
| 🇬🇧 ★ | Interconnects | `www.interconnects.ai/feed` | Language model training and research |
| 🇬🇧 ★ | Ahead of AI | `magazine.sebastianraschka.com/feed` | LLM research explained by Sebastian Raschka |
| 🇬🇧 ★ | Lilian Weng | `lilianweng.github.io/index.xml` | Thorough surveys of machine learning topics |
| 🇬🇧 | InfoQ | `feed.infoq.com/` | Architecture and engineering talks, high volume |
Left out on purpose: The Pragmatic Engineer newsletter (mostly paid), One Useful
Thing (applied AI), vendor blogs (OpenAI, DeepMind, Hugging Face) and Chip Huyen
(inactive since January 2025). Some feeds are long (Dan Luu ~130 posts, Lilian
Weng ~50); the 14-day window keeps the list short.
## Project layout
```
+149
View File
@@ -212,6 +212,155 @@ const defaultSources = <NewsSource>[
description: 'System design explained with diagrams',
enabledByDefault: true,
),
NewsSource(
id: 'systemdesignone',
name: 'System Design One',
feedUrl: 'https://newsletter.systemdesign.one/feed',
homepage: 'https://newsletter.systemdesign.one',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'System design case studies of real systems',
enabledByDefault: true,
),
NewsSource(
id: 'pragmaticengineer-blog',
name: 'The Pragmatic Engineer (blog)',
feedUrl: 'https://blog.pragmaticengineer.com/rss/',
homepage: 'https://blog.pragmaticengineer.com',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'How big tech and startups build software (free posts)',
enabledByDefault: true,
),
NewsSource(
id: 'martinfowler',
name: 'Martin Fowler',
feedUrl: 'https://martinfowler.com/feed.atom',
homepage: 'https://martinfowler.com',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Software architecture, refactoring, delivery practices',
enabledByDefault: true,
),
NewsSource(
id: 'juliaevans',
name: 'Julia Evans',
feedUrl: 'https://jvns.ca/atom.xml',
homepage: 'https://jvns.ca',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Systems, networking and Unix tools, explained plainly',
enabledByDefault: true,
),
NewsSource(
id: 'danluu',
name: 'Dan Luu',
feedUrl: 'https://danluu.com/atom.xml',
homepage: 'https://danluu.com',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Long, data-driven essays on computers and engineering',
enabledByDefault: true,
),
NewsSource(
id: 'cloudflare-blog',
name: 'The Cloudflare Blog',
feedUrl: 'https://blog.cloudflare.com/rss/',
homepage: 'https://blog.cloudflare.com',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Networking, security and incident write-ups in depth',
enabledByDefault: true,
),
NewsSource(
id: 'netflix-techblog',
name: 'Netflix Technology Blog',
feedUrl: 'https://netflixtechblog.com/feed',
homepage: 'https://netflixtechblog.com',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Distributed systems, data and streaming at scale',
enabledByDefault: true,
),
NewsSource(
id: 'meta-engineering',
name: 'Engineering at Meta',
feedUrl: 'https://engineering.fb.com/feed/',
homepage: 'https://engineering.fb.com',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Infrastructure, AI and production engineering at Meta',
enabledByDefault: true,
),
NewsSource(
id: 'github-engineering',
name: 'GitHub Blog: Engineering',
feedUrl: 'https://github.blog/engineering/feed/',
homepage: 'https://github.blog/engineering/',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'How GitHub builds and runs GitHub',
enabledByDefault: true,
),
NewsSource(
id: 'latent-space',
name: 'Latent Space',
feedUrl: 'https://www.latent.space/feed',
homepage: 'https://www.latent.space',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'AI engineering: interviews and deep dives',
enabledByDefault: true,
),
NewsSource(
id: 'import-ai',
name: 'Import AI',
feedUrl: 'https://importai.substack.com/feed',
homepage: 'https://importai.substack.com',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Weekly AI research digest by Jack Clark',
enabledByDefault: true,
),
NewsSource(
id: 'interconnects',
name: 'Interconnects',
feedUrl: 'https://www.interconnects.ai/feed',
homepage: 'https://www.interconnects.ai',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Language model training and research, analysed',
enabledByDefault: true,
),
NewsSource(
id: 'ahead-of-ai',
name: 'Ahead of AI',
feedUrl: 'https://magazine.sebastianraschka.com/feed',
homepage: 'https://magazine.sebastianraschka.com',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'LLM research explained, by Sebastian Raschka',
enabledByDefault: true,
),
NewsSource(
id: 'lilianweng',
name: 'Lilian Weng',
feedUrl: 'https://lilianweng.github.io/index.xml',
homepage: 'https://lilianweng.github.io',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Thorough surveys of machine learning topics',
enabledByDefault: true,
),
NewsSource(
id: 'infoq',
name: 'InfoQ',
feedUrl: 'https://feed.infoq.com/',
homepage: 'https://www.infoq.com',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Software architecture and engineering talks, high volume',
),
];
/// The curated sources before the app remembered which ones it had seen.
+34
View File
@@ -43,6 +43,40 @@ void main() {
expect(ids, isNot(contains('engadget')));
});
test('the Learning sources are the curated set, all English, with the agreed defaults', () {
final learning = {
for (final s in defaultSources)
if (s.kind == SourceKind.learning) s.name: (s.feedUrl, s.language, s.enabledByDefault),
};
const en = SourceLanguage.en;
expect(learning, {
'ByteByteGo': ('https://blog.bytebytego.com/feed', en, true),
'System Design One': ('https://newsletter.systemdesign.one/feed', en, true),
'The Pragmatic Engineer (blog)': ('https://blog.pragmaticengineer.com/rss/', en, true),
'Martin Fowler': ('https://martinfowler.com/feed.atom', en, true),
'Julia Evans': ('https://jvns.ca/atom.xml', en, true),
'Dan Luu': ('https://danluu.com/atom.xml', en, true),
'The Cloudflare Blog': ('https://blog.cloudflare.com/rss/', en, true),
'Netflix Technology Blog': ('https://netflixtechblog.com/feed', en, true),
'Engineering at Meta': ('https://engineering.fb.com/feed/', en, true),
'GitHub Blog: Engineering': ('https://github.blog/engineering/feed/', en, true),
'Latent Space': ('https://www.latent.space/feed', en, true),
'Import AI': ('https://importai.substack.com/feed', en, true),
'Interconnects': ('https://www.interconnects.ai/feed', en, true),
'Ahead of AI': ('https://magazine.sebastianraschka.com/feed', en, true),
'Lilian Weng': ('https://lilianweng.github.io/index.xml', en, true),
'InfoQ': ('https://feed.infoq.com/', en, false),
});
});
test('every Learning source says what it is for and links its homepage', () {
for (final s in defaultSources.where((s) => s.kind == SourceKind.learning)) {
expect(s.description, isNotEmpty, reason: s.name);
expect(Uri.parse(s.homepage).isScheme('https'), isTrue, reason: s.name);
}
});
test('every Source has its own id', () {
final ids = defaultSources.map((s) => s.id).toList();
+17
View File
@@ -2,10 +2,14 @@
import 'dart:io';
import 'package:daily_tech_news/data/default_sources.dart';
import 'package:daily_tech_news/models/news_source.dart';
import 'package:daily_tech_news/services/article_extractor.dart';
import 'package:daily_tech_news/services/consent_service.dart';
import 'package:daily_tech_news/services/feed_service.dart';
import 'package:daily_tech_news/services/storage.dart';
import 'package:daily_tech_news/state/app_state.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
final live = Platform.environment['LIVE_FEEDS'] == '1';
@@ -29,6 +33,19 @@ void main() {
expect(failures, isEmpty);
}, skip: live ? false : 'set LIVE_FEEDS=1 to run', timeout: const Timeout(Duration(minutes: 3)));
test('a full refresh with all on-by-default Sources stays fast', () async {
SharedPreferences.setMockInitialValues({});
final state = AppState(await Storage.open(), feedService: FeedService());
final clock = Stopwatch()..start();
await state.init();
clock.stop();
// ignore: avoid_print
print('${state.enabledSources.length} Sources, ${state.articles.length} articles '
'(${state.articlesOf(SourceKind.learning).length} Learning) in '
'${clock.elapsedMilliseconds} ms; errors: ${state.feedErrors}');
expect(clock.elapsed, lessThan(const Duration(seconds: 15)));
}, skip: live ? false : 'set LIVE_FEEDS=1 to run', timeout: const Timeout(Duration(minutes: 1)));
test('reader view extracts the article body', () async {
final service = FeedService();
final extractor = ArticleExtractor(consent: ConsentService());
+20
View File
@@ -135,4 +135,24 @@ void main() {
expect(bytebytego.language, SourceLanguage.en);
expect(bytebytego.enabledByDefault, isTrue);
});
test('a fresh install shows posts of the last 14 days from the curated Learning sources',
() async {
final learning = defaultSources.where((s) => s.kind == SourceKind.learning);
final state = await launchApp(defaultSources, feeds: {
for (final s in learning)
s.feedUrl: [('${s.name}: recent', const Duration(days: 13)),
('${s.name}: old', const Duration(days: 15))],
});
expect(_titles(state.articlesOf(SourceKind.learning)).toSet(), {
'ByteByteGo: recent', 'System Design One: recent',
'The Pragmatic Engineer (blog): recent', 'Martin Fowler: recent', 'Julia Evans: recent',
'Dan Luu: recent', 'The Cloudflare Blog: recent', 'Netflix Technology Blog: recent',
'Engineering at Meta: recent', 'GitHub Blog: Engineering: recent', 'Latent Space: recent',
'Import AI: recent', 'Interconnects: recent', 'Ahead of AI: recent',
'Lilian Weng: recent',
// InfoQ is off by default.
});
});
}
+10 -2
View File
@@ -23,6 +23,13 @@ NewsSource _source(String id, {bool onByDefault = false}) => NewsSource(
enabledByDefault: onByDefault,
);
/// The curated Learning sources that are on by default (all but InfoQ).
const _learningOnByDefault = {
'bytebytego', 'systemdesignone', 'pragmaticengineer-blog', 'martinfowler', 'juliaevans',
'danluu', 'cloudflare-blog', 'netflix-techblog', 'meta-engineering', 'github-engineering',
'latent-space', 'import-ai', 'interconnects', 'ahead-of-ai', 'lilianweng',
};
/// Starts the app the way a launch does, against whatever is in preferences.
Future<AppState> _launch(List<NewsSource> curatedSources) async {
final state =
@@ -87,7 +94,7 @@ void main() {
expect(state.enabledSourceIds, {
'heise', 'verge', 'mit-tr', 'new-source',
// Added to the catalog later, on by default.
'techmeme', 'lobsters', 'the-decoder', 'simonwillison', 'bytebytego',
'techmeme', 'lobsters', 'the-decoder', 'simonwillison', ..._learningOnByDefault,
});
});
@@ -102,8 +109,9 @@ void main() {
expect(state.enabledSourceIds, {
'heise', 'golem', 't3n', 'verge',
'techmeme', 'lobsters', 'the-decoder', 'simonwillison', 'bytebytego',
'techmeme', 'lobsters', 'the-decoder', 'simonwillison', ..._learningOnByDefault,
});
expect(state.enabledSources.map((s) => s.id), isNot(contains('wired')));
expect(state.enabledSourceIds, isNot(contains('infoq')));
});
}