Skip to main content

max / balanced_breakfast

4.3 KB · 189 lines History Blame Raw
1 // Dev.to article feed. Queries the public Forem API with optional tag
2 // filtering. Extracts reaction/comment counts and reading time from
3 // the JSON response. Renders cover images inline when present.
4
5 const DEVTO_API = "https://dev.to/api/articles";
6
7 fn id() {
8 "devto"
9 }
10
11 fn name() {
12 "Dev.to"
13 }
14
15 fn capabilities() {
16 #{
17 supports_pagination: true
18 }
19 }
20
21 fn config_schema() {
22 #{
23 description: "Fetch articles from Dev.to, a community of software developers.",
24 fields: [
25 #{
26 key: "tag",
27 label: "Tag filter",
28 field_type: "text",
29 description: "Filter by tag (e.g., rust, javascript, python). Leave blank for all.",
30 placeholder: "rust"
31 },
32 #{
33 key: "limit",
34 label: "Items per fetch",
35 field_type: "text",
36 description: "Number of articles to fetch (max 30)",
37 default_value: "20",
38 placeholder: "20"
39 }
40 ]
41 }
42 }
43
44 fn fetch(config, cursor) {
45 let limit = 20;
46 if config.limit != () {
47 let parsed = parse_int(config.limit);
48 if parsed != () && parsed > 0 {
49 if parsed > 30 {
50 limit = 30;
51 } else {
52 limit = parsed;
53 }
54 }
55 }
56
57 let page = 1;
58 if cursor != () {
59 let parsed = parse_int(cursor);
60 if parsed != () && parsed > 0 {
61 page = parsed;
62 }
63 }
64
65 let url = DEVTO_API + "?per_page=" + limit + "&page=" + page;
66
67 if config.tag != () && config.tag != "" {
68 url += "&tag=" + config.tag;
69 }
70
71 let articles = http_get_json(url);
72
73 if articles == () {
74 return #{ items: [], has_more: false };
75 }
76
77 let items = [];
78 for article in articles {
79 let item = parse_article(article);
80 if item != () {
81 items.push(item);
82 }
83 }
84
85 let has_more = items.len() >= limit;
86 let result = #{
87 items: items,
88 has_more: has_more
89 };
90
91 if has_more {
92 result.next_cursor = "" + (page + 1);
93 }
94
95 result
96 }
97
98 fn parse_article(article) {
99 if article.title == () {
100 return ();
101 }
102
103 let title = article.title;
104 let id = 0;
105 if article.id != () {
106 id = article.id;
107 }
108
109 let author = "Unknown";
110 if article.user != () {
111 if article.user.name != () {
112 author = article.user.name;
113 } else if article.user.username != () {
114 author = article.user.username;
115 }
116 }
117
118 let url = "";
119 if article.url != () {
120 url = article.url;
121 }
122
123 let description = "";
124 if article.description != () {
125 description = article.description;
126 }
127
128 let published = timestamp_now();
129 if article.published_at != () {
130 published = article.published_at;
131 }
132
133 let reactions = 0;
134 if article.positive_reactions_count != () {
135 reactions = article.positive_reactions_count;
136 }
137
138 let comments = 0;
139 if article.comments_count != () {
140 comments = article.comments_count;
141 }
142
143 let reading_time = 0;
144 if article.reading_time_minutes != () {
145 reading_time = article.reading_time_minutes;
146 }
147
148 // Build tags
149 let tags = ["devto"];
150 if article.tag_list != () {
151 for tag in article.tag_list {
152 tags.push(tag);
153 }
154 }
155
156 let secondary = "" + reactions + " reactions · " + comments + " comments";
157 if reading_time > 0 {
158 secondary += " · " + reading_time + " min read";
159 }
160
161 // Body: cover image + description
162 let body = "";
163 if article.cover_image != () && article.cover_image != "" {
164 body = `<img src="` + article.cover_image + `" alt="` + title + `" style="max-width:100%">` + "\n\n";
165 }
166 body += description;
167
168 #{
169 id: #{ source: "devto", item_id: "" + id },
170 bite: #{
171 author: author,
172 text: truncate(title, 100),
173 secondary: secondary,
174 indicator: "💻"
175 },
176 content: #{
177 title: title,
178 body: body,
179 url: url
180 },
181 meta: #{
182 source_name: "Dev.to",
183 published_at: published,
184 score: reactions,
185 tags: tags
186 }
187 }
188 }
189