-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcode.gs
More file actions
460 lines (401 loc) · 21.6 KB
/
Copy pathcode.gs
File metadata and controls
460 lines (401 loc) · 21.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
"use strict";
// https://developers.google.com/apps-script/guides/services/quotas: max 9kB/property value, max 500kB/property store
// rsseverything.com (stephenslab.top) has limits that make it unusable for VRT news
const contentTypeGASGzip = "application/x-gzip";
const thisPublishedURL = "https://script.google.com/macros/s/.../exec";
const URLVRTBinnenland = "https://www.vrt.be/vrtnws/nl/rubrieken/binnenland/";
const URLScienceAndCulture = "https://scienceandculture.com/latest/";
const VRTBinnenlandSectionsToIgnore = [
"overzicht",
"defensie"
];
function trackLengthOfPropertyStore() {
const allProps = PropertiesService.getScriptProperties().getProperties();
let exceeds = Object.entries(allProps).reduce((tot, [k, v]) => {if(v.length >= 9*1024*0.94) tot += k+" nearly >9kB\n"; return tot;}, "");
if(Object.entries(allProps).reduce((tot, [k, v]) => tot+k.length+v.length, 0) > 500*1024*0.96)
exceeds += "total store nearly >500kB";
if(exceeds !== "")
throw exceeds;
}
function doGet(event) {
switch(event?.parameters?.source?.[0]) {
case "VRT":
if(!event?.parameters?.section?.[0])
return ContentService.createTextOutput("No section provided");
return ContentService.createTextOutput(getVRTBinnenland(event.parameters.section[0])).setMimeType(ContentService.MimeType.XML);
case "ScienceAndCulture":
return ContentService.createTextOutput(getScienceAndCulture()).setMimeType(ContentService.MimeType.XML);
}
if(event?.parameters?.source?.[0].startsWith("test"))
return ContentService.createTextOutput(testSource).setMimeType(ContentService.MimeType.XML);
return ContentService.createTextOutput("No known source");
}
/*** Science And Culture ***
* Saved as ScienceAndCulture: [<UTC times>];
* ScienceAndCulture_<UTC time>: [<articles as objects>]
*/
function getScienceAndCultureArticles() {
const response = UrlFetchApp.fetch(
URLScienceAndCulture, {
followRedirects: true
}
);
const html = response.getContentText();
let mainSection = html.match(/<main.*<\/main>/s)?.[0];
mainSection = cleanAndparseHtml(mainSection);
let aWithTitle;
const articles = mainSection.getRootElement().getChildren("article")
for(let i=0; i < articles.length; ++i) {
aWithTitle = getElementsByTagAndAttribute(articles[i], "a", "title")?.[0];
articles[i] = {
'text': aWithTitle?.getText()||"Bug 0",
'link': aWithTitle?.getAttribute("href").getValue()||("Bug "+Date.now()),
'imglink': getElementsByTagAndAttribute(articles[i], "img")[0]?.getAttribute("src").getValue()||"none",
'description': getElementsByTagAndAttribute(articles[i], "center", "class", "article-center")[0]?.getText().trim()
};
if(!articles[i].link.startsWith("http"))
articles[i].link = `https://${articles[i].link.includes("scienceandculture.com") ? "" : "www.scienceandculture.com/"}${articles[i].link.slice(articles[i].link.startsWith("/") ? 1 : 0)}`;
if(!articles[i].imglink.startsWith("http"))
articles[i].imglink = `https://${articles[i].imglink.includes("scienceandculture.com") ? "" : "www.scienceandculture.com/"}${articles[i].imglink.slice(articles[i].imglink.startsWith("/") ? 1 : 0)}`;
}
return articles;
}
function saveScienceAndCultureArticles() {
saveArticles("ScienceAndCulture", getScienceAndCultureArticles());
}
function getScienceAndCulture() {
return makeRSSXMLFromData({
'source': "ScienceAndCulture",
'title': "Science And Culture",
'URL': URLScienceAndCulture,
'description': "Science And Culture Today | Discovering Design in Nature",
'language': "en-US",
'articles': getArticles("ScienceAndCulture")
});
}
/* sections of VRTBinnenland */
/* To be run weekly, since these sections probably don't change. */
function getVRTBinnenlandSections() {
const response = UrlFetchApp.fetch(URLVRTBinnenland, {
followRedirects: true
});
let html = response.getContentText();
let mainSection = html.match(/<main.*<\/main>/s)?.[0];
mainSection = cleanAndparseHtml(mainSection);
// Get all "sections" of Binnenland (politiek, ...)
let sections = getElementsByTagAndAttribute(mainSection, "div", "data-sentry-source-file", "Item.tsx")
.reduce((tot, elt) => Object.assign(tot, {[elt.getText().toLowerCase()]: elt.getParentElement().getAttribute("href").getValue()}),
{}
);
sections = Object.fromEntries(Object.entries(sections).map(([section, link]) => {
link = link.split("/");
return [section, URLVRTBinnenland.split(link[1])[0] + link.slice(1).join("/")];
})
.filter(([section, link]) => (link !== URLVRTBinnenland)&&(!VRTBinnenlandSectionsToIgnore.some(sectionToIgnore => sectionToIgnore === section)))
);
saveProperty(PropertiesService.getScriptProperties(), "VRTBinnenlandSections", JSON.stringify(sections));
}
/*** VRTBinnenland ***
* Saved as VRTBinnenland: [<UTC times>];
* VRTBinnenland_<section>_<UTC time>: [<articles as objects>]
*/
function getVRTBinnenlandArticles() {
const sections = JSON.parse( // ophalen van props duurt 40s
getSavedProperties(PropertiesService.getScriptProperties())["VRTBinnenlandSections"]
);
const sectionEntries = Object.entries(sections);
let i;
const toFetch = [];
for(i=0; i < sectionEntries.length; ++i) {
toFetch.push({
url: sectionEntries[i][1],
followRedirects: true
});
}
//Logger.log("fetching");
const responses = UrlFetchApp.fetchAll(toFetch);
//Logger.log("processing");
const articles = {};
let mainSection, elts, j, text, k, indexLastArticle;
for(i=0; i < responses.length; ++i) {
//Logger.log(i);
articles[sectionEntries[i][0]] = [];
mainSection = responses[i].getContentText().match(/<main.*<\/main>/s)?.[0];
mainSection = cleanAndparseHtml(mainSection);
//++ 1 min per mainSection => splitsen per section
elts = getElementsByTagAndAttribute(mainSection, "div", "data-sentry-component", "EBAGuard");
for(j=0; j < elts.length; ++j) {
text = getElementsByTagAndAttribute(elts[j], "span");
for(k=0; k < text.length; ++k)
text[k] = text[k].getText()+"<br>";
indexLastArticle = articles[sectionEntries[i][0]].push({
'text': text,
'link': getElementsByTagAndAttribute(elt, "a")[0].getAttribute("href").getValue(),
'imglink': getElementsByTagAndAttribute(elt, "img")[0]?.getAttribute("src").getValue(),
'description': text
})-1;
if(!articles[sectionEntries[i][0]][indexLastArticle].link.startsWith("http"))
articles[sectionEntries[i][0]][indexLastArticle].link = `https://${articles[sectionEntries[i][0]][indexLastArticle].link.includes("vrt.be") ? "" : "www.vrt.be/"}${articles[sectionEntries[i][0]][indexLastArticle].link.slice(articles[sectionEntries[i][0]][indexLastArticle].link.startsWith("/") ? 1 : 0)}`;
if(!articles[sectionEntries[i][0]][indexLastArticle].imglink.startsWith("http"))
articles[sectionEntries[i][0]][indexLastArticle].imglink = `https://${articles[sectionEntries[i][0]][indexLastArticle].imglink.includes("vrt.be") ? "" : "www.vrt.be/"}${articles[sectionEntries[i][0]][indexLastArticle].imglink.slice(articles[sectionEntries[i][0]][indexLastArticle].imglink.startsWith("/") ? 1 : 0)}`;
}
}
//Logger.log("end processing. saving...");
return articles;
}
function saveVRTBinnenlandArticles() {
const entries = Object.entries(getVRTBinnenlandArticles());
for(let i=0; i < entries.length; ++i)
saveArticles("VRTBinnenland_"+entries[i][0], entries[i][1]);
}
function getVRTBinnenland(section) {
return makeRSSXMLFromData({
'source': `VRT&section=${section}`,
'title': `VRT Binnenland ${section}`,
'URL': URLVRTBinnenland+section.toLowerCase()+"/",
'description': `VRT nws Binnenland, ${section}`,
'language': "nl-BE",
'articles': getArticles("VRTBinnenland_"+section)
});
}
/*** HELPERS ***/
function getSavedProperties(propertyStore) {
const savedProperties = propertyStore.getProperties();
const keys = Object.keys(savedProperties);
for(let i=0; i < keys.length; ++i) {
savedProperties[key] = Utilities.ungzip(
Utilities.newBlob(
Utilities.base64Decode(savedProperties[key]),
contentTypeGASGzip
)
).getDataAsString();
}
return savedProperties;
}
function saveProperty(propertyStore, key, value) {
propertyStore.setProperty(key, Utilities.base64Encode(
Utilities.gzip(
Utilities.newBlob(
value,
'application/json'
)
).getBytes()
));
}
function saveArticles(source, articles) {
const scriptProperties = PropertiesService.getScriptProperties();
const savedProperties = getSavedProperties(scriptProperties); /*scriptProperties.getProperties();*/
const UTCTimesForSource = JSON.parse(savedProperties[source]||"[]").sort((a, b) => b-a);
let fourAndAHalfWeeksAgo = new Date();
fourAndAHalfWeeksAgo.setDate(fourAndAHalfWeeksAgo.getDate() - 4.5*7);
fourAndAHalfWeeksAgo.setHours(fourAndAHalfWeeksAgo.getHours(), 0, 0, 0);
fourAndAHalfWeeksAgo = fourAndAHalfWeeksAgo.getTime();
// Delete properties that are too old and get the timestamps that are not too old
const newUTCTimesForSource = [];
for(let i=0; i < UTCTimesForSource.length; ++i)
UTCTimesForSource[i] <= fourAndAHalfWeeksAgo ? scriptProperties.deleteProperty(source+"_"+UTCTimesForSource[i]) : newUTCTimesForSource.push(UTCTimesForSource[i]);
// Remove articles for which there already was an article saved
let oldArticles, linksOfArticlesToRemove = new Set();
for(let i=0, j=0; i < UTCTimesForSource.length; ++i) {
oldArticles = JSON.parse(savedProperties[source+"_"+UTCTimesForSource[i]]||"[]");
for(j=0; j < oldArticles.length; ++j)
linksOfArticlesToRemove.add(oldArticles[j].link);
}
articles = articles.filter(article => !linksOfArticlesToRemove.has(article.link));
// Save new articles, when needed
if(articles.length === 0)
return;
const now = new Date();
now.setHours(now.getHours(), 0, 0, 0);
saveProperty(scriptProperties, source+"_"+now.getTime(), JSON.stringify(articles));
saveProperty(scriptProperties, source, JSON.stringify(newUTCTimesForSource.concat(now.getTime())));
}
function getArticles(source) {
const savedProperties = getSavedProperties(PropertiesService.getScriptProperties());
const UTCTimesForSource = JSON.parse(savedProperties[source]||"[]");
const result = [];
for(let i=0; i < UTCTimesForSource.length; ++i)
result.push(JSON.parse(savedProperties[source+"_"+UTCTimesForSource[i]]));
return result;
}
function makeRSSXMLFromData(data) {
const articles = "";
for(let i=0; i < data.articles.length; ++i) {
articles += `
\t\t\t<item>
\t\t\t\t<description>${data.articles[i].description}</description>
\t\t\t\t${data.articles[i].imglink ? `<enclosure url="${data.articles[i].imglink./*replace(/&/g, "&")*/split("?")[0]}" type="image/jpeg" length="10" />` : ""}
\t\t\t\t<link>${data.articles[i].link}</link>
\t\t\t\t<guid>${data.articles[i].link}</guid>
\t\t\t\t<title>${data.articles[i].text}</title>
\t\t\t</item>
`;
}
return `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
\t<channel>
\t\t<atom:link href="${thisPublishedURL}?source=${data.source}" rel="self" type="application/rss+xml" />
\t\t<title>${data.title}</title>
\t\t<link>${data.URL}</link>
\t\t<description>${data.description}</description>
\t\t<language>${data.language}</language>
\t\t<generator>Google Apps Script: https://script.google.com/home/projects/.../edit</generator>
\t\t${articles}
\t</channel>
</rss>`;
}
function cleanAndparseHtml(html) {
html = html.replace(new RegExp(
// to clean
"(<script.*?<\/script>"+"|"
+"<svg.*?<\/svg>"+"|"
+"<iframe.*?<\/iframe>"+"|"
+"<style.*?<\/style>"+"|"
+'\sstyle="[^"]+)'+"|"
// to make xml compatible
+"(<(?!(img|link|meta|br|input)-src)(?=link|meta|br|input|img)[^>]*)(?<!\/)>"+"|" // unclosed tags
/*+"(<(script|style)[^>]*>)"+"|" // force script/style content into cdata
+"(<\/(script|style)[^>]*>)"+"|" // force script/style content into cdata*/
+"&(?!amp;)"+"|" // change & to &
+'(\sdata-length=)([^>"]+)([">])', // specific for ScienceAndCulture
"sig"), (match, toClean, unclosedTags, /*openScriptStyleTag, closeScriptStyleTag,*/ _, dataLength1, dataLength2, dataLength3) => {
if(toClean)
return "";
if(unclosedTags)
return unclosedTags+"/>"; // close unclosed tags
/*if(openScriptStyleTag)
return openScriptStyleTag+"<![CDATA["; // force script / style content into cdata
if(closeScriptStyleTag)
return "]]>"+closeScriptStyleTag; // force script / style content into cdata*/
if(match === "&")
return "&"; // change & to &
if(dataLength1)
return `${dataLength1}"${dataLength2}"${dataLength3}`; // specific for ScienceAndCulture
}
);
// now it should work
return XmlService.parse(html);
}
function getElementsByTagAndAttribute(element, tag, attribute, value) {
// inspired by https://sites.google.com/site/scriptsexamples/learn-by-example/parsing-html
let data = [];
let descendants = element.getDescendants();
descendants.push(element);
if(tag)
tag = tag.toLowerCase();
if(attribute)
attribute = attribute.toLowerCase();
for(let i=0, elt, targetAttributes; i < descendants.length; ++i) {
elt = descendants[i].asElement?.();
if(elt == null)
continue;
if(tag && (elt.getName().toLowerCase() !== tag))
continue;
if((attribute === undefined)&&(value === undefined)) {
data.push(elt);
continue;
}
if(attribute === undefined)
targetAttributes = elt.getAttributes();
else
targetAttributes = [elt.getAttribute(attribute)];
if(value === undefined) {
if(targetAttributes.some(targetAttribute => targetAttribute))
data.push(elt);
continue;
}
if(targetAttributes.some(targetAttribute => targetAttribute?.getValue() === value))
data.push(elt);
}
return data;
}
const testSource = `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<atom:link href="https://script.google.com/macros/s/.../exec?source=VRT&section=samenleving" rel="self" type="application/rss+xml" />
<title>VRT Binnenland samenleving</title>
<link>https://www.vrt.be/vrtnws/nl/rubrieken/binnenland/samenleving/</link>
<description>VRT nws Binnenland, samenleving</description>
<language>nl-BE</language>
<generator>Google Apps Script: https://script.google.com/home/projects/.../edit</generator>
<item>
<description>samenleving: De Lokerse Koewachtenaars kunnen al in oktober naar de stembus: "Kiezen we voor Lokeren of Stekene?"</description>
<enclosure url="https://images.vrt.be/vrtnws_web/2025/08/29/e5be26c2-92c5-41c7-8f59-169a87a61b47.jpg" type="image/jpeg" length="10" />
<link>https://www.vrt.be/vrtnws/nl/2025/08/29/de-lokerse-koewachtenaren-kunnen-half-oktober-weer-naar-de-stemb/</link>
<guid>https://www.vrt.be/vrtnws/nl/2025/08/29/de-lokerse-koewachtenaren-kunnen-half-oktober-weer-naar-de-stemb/</guid>
<title>De Lokerse Koewachtenaars kunnen al in oktober naar de stembus: "Kiezen we voor Lokeren of Stekene?"</title>
</item>
<item>
<description>samenleving: Inzameling winterjassen op markt in Herentals: "Mensen gaan zelfs jassen kopen om te doneren" </description>
<enclosure url="https://images.vrt.be/vrtnws_web/2025/08/29/edf6c882-fad3-430d-942c-e889b6bf9aa1.jpg" type="image/jpeg" length="10" />
<link>https://www.vrt.be/vrtnws/nl/2025/08/29/inzameling-winterjassen-op-markt-in-herentals-er-zijn-zelfs-me/</link>
<guid>https://www.vrt.be/vrtnws/nl/2025/08/29/inzameling-winterjassen-op-markt-in-herentals-er-zijn-zelfs-me/</guid>
<title>Inzameling winterjassen op markt in Herentals: "Mensen gaan zelfs jassen kopen om te doneren" </title>
</item>
<item>
<description>samenleving: Na dood van baby in Lede: experten roepen op om tijdens
vakanties extra alert te zijn voor kindermishandeling</description>
<enclosure url="https://images.vrt.be/vrtnws_web/2019/10/14/d19eb875-ee82-11e9-abcc-02b7b76bf47f.jpg" type="image/jpeg" length="10" />
<link>https://www.vrt.be/vrtnws/nl/2025/08/26/lede-baby-kindermishandeling-experten-vlaams-expertisecentrum-va/</link>
<guid>https://www.vrt.be/vrtnws/nl/2025/08/26/lede-baby-kindermishandeling-experten-vlaams-expertisecentrum-va/</guid>
<title>Na dood van baby in Lede: experten roepen op om tijdens
vakanties extra alert te zijn voor kindermishandeling</title>
</item>
<item>
<description>samenleving: Na moord op 17-jarige Lisa stijgt vraag naar zelfverdedigingssprays, maar in België zijn ze verboden</description>
<enclosure url="https://images.vrt.be/vrtnws_web/2025/08/25/24be86e1-b292-48b7-bc63-219fae860655.jpg" type="image/jpeg" length="10" />
<link>https://www.vrt.be/vrtnws/nl/2025/08/25/na-moord-op-17-jarige-lisa-stijgt-vraag-naar-zelfverdedigingsspr/</link>
<guid>https://www.vrt.be/vrtnws/nl/2025/08/25/na-moord-op-17-jarige-lisa-stijgt-vraag-naar-zelfverdedigingsspr/</guid>
<title>Na moord op 17-jarige Lisa stijgt vraag naar zelfverdedigingssprays, maar in België zijn ze verboden</title>
</item>
<item>
<description>samenleving: Advocaat ouders Julie Van Espen na moord op Lisa: "Jonge vrouw staat symbool voor onschuld, kwetsbaarheid en toekomst"</description>
<enclosure url="https://images.vrt.be/vrtnws_web/2025/08/25/6ea0f222-f0f8-41d6-bab7-62abcf8d9f6c.jpg" type="image/jpeg" length="10" />
<link>https://www.vrt.be/vrtnws/nl/2025/08/25/advocaat-ouders-julie-van-espen-over-lisa-verontwaardiging-polit/</link>
<guid>https://www.vrt.be/vrtnws/nl/2025/08/25/advocaat-ouders-julie-van-espen-over-lisa-verontwaardiging-polit/</guid>
<title>Advocaat ouders Julie Van Espen na moord op Lisa: "Jonge vrouw staat symbool voor onschuld, kwetsbaarheid en toekomst"</title>
</item>
<item>
<description>samenleving: Openheid, zichtbaarheid of klassieke muziek: hoe openbare ruimte beter kan zodat (ook) vrouwen zich veilig voelen</description>
<enclosure url="https://images.vrt.be/vrtnws_web/2025/08/24/f7bf4d76-1479-44f0-b5f8-f8d1de88ee49.jpg" type="image/jpeg" length="10" />
<link>https://www.vrt.be/vrtnws/nl/2025/08/23/vrouwelijke-perspectief/</link>
<guid>https://www.vrt.be/vrtnws/nl/2025/08/23/vrouwelijke-perspectief/</guid>
<title>Openheid, zichtbaarheid of klassieke muziek: hoe openbare ruimte beter kan zodat (ook) vrouwen zich veilig voelen</title>
</item>
<item>
<description>samenleving: "Het leven is proberen, ik vind mijn weg wel": Noor uit Afghanistan en Yara en Malek uit Syrië vluchtten 10 jaar geleden naar België</description>
<enclosure url="https://images.vrt.be/vrtnws_web/2025/08/23/32532bdf-1ab1-4902-b391-b6c991d54a1b.jpg" type="image/jpeg" length="10" />
<link>https://www.vrt.be/vrtnws/nl/2025/08/23/tien-jaar-in-belgie-3-vluchtelingen-blikken-terug-en-maken-de-b/</link>
<guid>https://www.vrt.be/vrtnws/nl/2025/08/23/tien-jaar-in-belgie-3-vluchtelingen-blikken-terug-en-maken-de-b/</guid>
<title>"Het leven is proberen, ik vind mijn weg wel": Noor uit Afghanistan en Yara en Malek uit Syrië vluchtten 10 jaar geleden naar België</title>
</item>
<item>
<description>samenleving: 10 jaar na asielcrisis: 39.000 aanvragen in België in 2015, helft vluchtelingen nu aan het werk</description>
<enclosure url="https://images.vrt.be/vrtnws_web/2025/07/31/5af73b64-61aa-41a7-aa2e-0eb2fb00af92.jpg" type="image/jpeg" length="10" />
<link>https://www.vrt.be/vrtnws/nl/2025/07/31/10-jaar-na-asielcrisis-2015-vluchtelingen-syrie-irak-afghanistan/</link>
<guid>https://www.vrt.be/vrtnws/nl/2025/07/31/10-jaar-na-asielcrisis-2015-vluchtelingen-syrie-irak-afghanistan/</guid>
<title>10 jaar na asielcrisis: 39.000 aanvragen in België in 2015, helft vluchtelingen nu aan het werk</title>
</item>
<item>
<description>samenleving: 143.000 mensen zonder werk vonden nieuwe job sinds start Vlaamse regering</description>
<enclosure url="https://images.vrt.be/vrtnws_web/2023/11/07/c716665b-7d7a-11ee-91d7-02b7b76bf47f.png" type="image/jpeg" length="10" />
<link>https://www.vrt.be/vrtnws/nl/2025/08/22/al-143-000-werkzoekenden-vonden-sinds-oktober-nieuwe-job-via-vda/</link>
<guid>https://www.vrt.be/vrtnws/nl/2025/08/22/al-143-000-werkzoekenden-vonden-sinds-oktober-nieuwe-job-via-vda/</guid>
<title>143.000 mensen zonder werk vonden nieuwe job sinds start Vlaamse regering</title>
</item>
<item>
<description>samenleving: Bijzondere traditie: Geraardsbergenaars wensen mekaar zondag 'een gelukkig jaar'</description>
<enclosure url="https://images.vrt.be/vrtnws_web/2025/08/22/fb63dc4e-1e71-4373-954c-ec715eedc6e6.jpg" type="image/jpeg" length="10" />
<link>https://www.vrt.be/vrtnws/nl/2025/08/22/bijzondere-traditie-geraardsbergenaars-wensen-mekaar-zondag-ee/</link>
<guid>https://www.vrt.be/vrtnws/nl/2025/08/22/bijzondere-traditie-geraardsbergenaars-wensen-mekaar-zondag-ee/</guid>
<title>Bijzondere traditie: Geraardsbergenaars wensen mekaar zondag 'een gelukkig jaar'</title>
</item>
<item>
<description>samenleving: 140 kinderen volgen gratis taalstage in Sint-Niklaas om Nederlands te versterken</description>
<enclosure url="https://images.vrt.be/vrtnws_web/2025/08/22/2053cb24-801e-4969-b7b4-c54c63cbc1e4.jpg" type="image/jpeg" length="10" />
<link>https://www.vrt.be/vrtnws/nl/2025/08/22/140-anderstalige-kinderen-volgen-taalklasjes-sint-niklaas/</link>
<guid>https://www.vrt.be/vrtnws/nl/2025/08/22/140-anderstalige-kinderen-volgen-taalklasjes-sint-niklaas/</guid>
<title>140 kinderen volgen gratis taalstage in Sint-Niklaas om Nederlands te versterken</title>
</item>
</channel>
</rss>`;