Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
galaxyproject
GitHub Repository: galaxyproject/training-material
Path: blob/main/_plugins/feeds.rb
1677 views
1
# frozen_string_literal: true
2
3
require './_plugins/jekyll-topic-filter'
4
require './_plugins/gtn'
5
require './_plugins/util'
6
require 'json'
7
8
class DateTime
9
##
10
# Convert a given DateTime stamp roughly to an African/European lunch time
11
#
12
# Why that time? It is when the majority of our users are online and wanting to read the news.
13
#
14
# This is really only available in the feeds plugin, should not be assumed to be available elsewhere.
15
def to_euro_lunch
16
self.to_date.to_datetime + 0.6
17
end
18
end
19
20
TRACKING = "?utm_source=matrix&utm_medium=newsbot&utm_campaign=matrix-news"
21
22
PRIO = [
23
'news',
24
'events',
25
'learning-pathways',
26
'tutorials',
27
'slides',
28
'recordings',
29
'faqs',
30
'workflows',
31
'contributors',
32
'grants',
33
'organisations'
34
].map.with_index { |x, i| [x, i] }.to_h
35
36
def track(url)
37
if url =~ /utm_source/
38
url
39
elsif url.include? '#'
40
url.gsub(/#/, TRACKING + '#')
41
else
42
url + TRACKING
43
end
44
end
45
46
FEED_WIDGET_XSLT = Nokogiri::XSLT(File.read('feed-widget.xslt.xml'))
47
48
def serialise(site, feed_path, builder)
49
# The builder won't let you add a processing instruction, so we have to
50
# serialise it to a string and then parse it again. Ridiculous.
51
if ! Dir.exist?(File.dirname(feed_path))
52
FileUtils.mkdir_p(File.dirname(feed_path))
53
end
54
55
# First the 'default' with explanatory portion
56
finalised = Nokogiri::XML builder.to_xml
57
pi = Nokogiri::XML::ProcessingInstruction.new(
58
finalised, 'xml-stylesheet',
59
%(type="text/xml" href="#{site.config['url']}#{site.baseurl}/feed.xslt.xml")
60
)
61
finalised.root.add_previous_sibling pi
62
File.write(feed_path, finalised.to_xml)
63
64
# Then the widget-compatible version with a more minimal representation:
65
finalised = Nokogiri::XML builder.to_xml
66
pi = Nokogiri::XML::ProcessingInstruction.new(
67
finalised, 'xml-stylesheet',
68
%(type="text/xml" href="#{site.config['url']}#{site.baseurl}/feed-widget.xslt.xml")
69
)
70
finalised.root.add_previous_sibling pi
71
File.write(feed_path.gsub(/\.xml$/, '.w.xml'), finalised.to_xml)
72
73
# Write out HTML version since Safari doesn't support XSLT on XML. Rip.
74
File.write(feed_path.gsub(/\.xml$/, '.w.html'), FEED_WIDGET_XSLT.transform(finalised))
75
end
76
77
def markdownify(site, text)
78
site.find_converter_instance(
79
Jekyll::Converters::Markdown
80
).convert(text.to_s)
81
end
82
83
ICON_FOR = {
84
'contributors' => '🧑‍🏫',
85
'grants' => '💰',
86
'organisations' => '🏢',
87
'events' => '📅',
88
'tutorials' => '📚',
89
'slides' => '🖼️',
90
'news' => '📰',
91
'faqs' => '❓',
92
'workflows' => '🛠️',
93
'learning-pathways' => '🛤️',
94
'recordings' => '🎥',
95
}
96
97
def generate_opml(site, groups)
98
builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
99
# Set stylesheet
100
xml.opml(version: '2.0') do
101
xml.head do
102
xml.title('Galaxy Training Network')
103
xml.dateCreated(DateTime.now.rfc3339)
104
xml.dateModified(DateTime.now.rfc3339)
105
xml.ownerEmail('[email protected]')
106
end
107
xml.body do
108
groups.each do |group, items|
109
xml.outline(text: group) do
110
items.each do |item|
111
xml.outline(text: item[:title], type: 'rss', version: 'RSS', xmlUrl: item[:url], htmlUrl: item[:url])
112
end
113
end
114
end
115
end
116
end
117
end
118
119
opml_path = File.join(site.dest, 'feeds', 'gtn.opml')
120
finalised = Nokogiri::XML builder.to_xml
121
File.write(opml_path, finalised.to_xml)
122
end
123
124
def generate_topic_feeds(site, topic, bucket)
125
mats = bucket.select { |x| x[3].include?(topic) }
126
feed_path = File.join(site.dest, 'topics', topic, 'feed.xml')
127
Jekyll.logger.info "[GTN/Feeds] Generating feed for #{topic} (#{mats.length} items)"
128
129
builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
130
# Set stylesheet
131
xml.feed(xmlns: 'http://www.w3.org/2005/Atom') do
132
# Set generator also needs a URI attribute
133
xml.generator('Jekyll', uri: 'https://jekyllrb.com/')
134
xml.link(href: "#{site.config['url']}#{site.baseurl}/topics/#{topic}/feed.xml", rel: 'self')
135
xml.link(rel: 'alternate', href: "#{site.config['url']}#{site.baseurl}/topics/#{topic}/")
136
xml.updated(mats.first[0].rfc3339)
137
xml.id("#{site.config['url']}#{site.baseurl}/topics/#{topic}/feed.xml")
138
topic_title = site.data[topic]['title']
139
xml.title("#{topic_title}")
140
xml.subtitle("Recently added tutorials, slides, FAQs, and events in the #{topic} topic")
141
xml.logo("#{site.config['url']}#{site.baseurl}/assets/images/GTN-60px.png")
142
143
mats.each do |time, group, page, tags|
144
xml.entry do
145
xml.title(ICON_FOR[group] + " " + page.data['title'])
146
link = "#{site.config['url']}#{site.baseurl}#{page.url}"
147
xml.link(href: link)
148
# Our links are (mostly) stable
149
xml.id(link)
150
151
# This is a feed of only NEW tutorials, so we only include publication times.
152
# xml.published(Gtn::PublicationTimes.obtain_time(page.path).to_datetime.rfc3339)
153
xml.updated(time.rfc3339)
154
155
tags.uniq.each do |tag|
156
xml.category(term: tag)
157
end
158
159
if page.data.key? 'description'
160
xml.summary(page.data['description'])
161
else
162
md = page.content[0..page.content.index("\n")].strip
163
html = markdownify(site, md)
164
text = Nokogiri::HTML(html).text
165
xml.summary(text)
166
end
167
168
Gtn::Contributors.get_authors(page.data).each do |c|
169
xml.author do
170
xml.name(Gtn::Contributors.fetch_name(site, c, warn:false))
171
if c !~ / /
172
xml.uri("#{site.config['url']}#{site.baseurl}/hall-of-fame/#{c}/")
173
end
174
end
175
end
176
177
Gtn::Contributors.get_non_authors(page.data).each do |c|
178
xml.contributor do
179
xml.name(Gtn::Contributors.fetch_name(site, c, warn:false))
180
if c !~ / /
181
xml.uri("#{site.config['url']}#{site.baseurl}/hall-of-fame/#{c}/")
182
end
183
end
184
end
185
end
186
end
187
end
188
end
189
190
serialise(site, feed_path, builder)
191
end
192
193
def generate_tag_topic_feeds(_site)
194
# Any new materials in a topic with the equivalent tag
195
# Any new materials tagged with that tag
196
# Any news by tag
197
''
198
end
199
200
def group_bucket_by(bucket, group_by: 'day')
201
case group_by
202
when 'day'
203
bucket
204
.group_by { |x| x[0].strftime('%Y-%m-%d') }
205
.to_h { |_k, v| [v.map { |x| x[0] }.min, v] }
206
when 'week'
207
bucket
208
.group_by { |x| x[0].strftime('%Y-%W') }
209
.to_h { |_k, v| [v.map { |x| x[0] }.min, v] }
210
when 'month'
211
bucket
212
.group_by { |x| x[0].strftime('%Y-%m') }
213
.to_h { |_k, v| [v.map { |x| x[0] }.min, v] }
214
else
215
# Pretend this is an h
216
# bucket
217
# .map { |x| [x[0], x] }
218
# .to_h
219
bucket
220
.map.with_index { |x, i| [x[0] + i / 100000000.0, [x]] }
221
.to_h
222
# We add an artificial separator in the range of miliseconds to each file,
223
# should never grow more than 1s, likely, to ensure each of these are
224
# individual items. This is kludge-y, yeah, but downstream processing wants
225
# to group_by in places, and we don't want to trigger it collapsing there
226
# too.
227
end
228
end
229
230
def format_contents(xml, site, parts, title, group_by: 'day')
231
# output += '<div xmlns="http://www.w3.org/1999/xhtml">'
232
end
233
234
235
236
def generate_matrix_feed_itemized(site, mats, group_by: 'day', filter_by: nil)
237
filter_title = nil
238
if !filter_by.nil?
239
mats = mats.select { |x| x[3].include?(filter_by) }
240
filter_title = filter_by.gsub('-', ' ').capitalize
241
end
242
243
case group_by
244
when 'day'
245
# Reject anything that is today
246
mats = mats.reject { |x| x[0].strftime('%Y-%m-%d') == Date.today.strftime('%Y-%m-%d') }
247
when 'week'
248
mats = mats.reject { |x| x[0].strftime('%Y-%W') == Date.today.strftime('%Y-%W') }
249
when 'month'
250
mats = mats.reject { |x| x[0].strftime('%Y-%m') == Date.today.strftime('%Y-%m') }
251
end
252
253
bucket = group_bucket_by(mats, group_by: group_by)
254
lookup = {
255
'day' => 'Daily',
256
'week' => 'Weekly',
257
'month' => 'Monthly',
258
nil => 'All'
259
}
260
261
parts = [filter_by || 'matrix', group_by || 'all']
262
path = "feeds/#{parts.join('-')}.i.xml"
263
264
feed_path = File.join(site.dest, path)
265
Jekyll.logger.info '[GTN/Feeds] Generating matrix/i feed'
266
267
dir = File.dirname(feed_path)
268
FileUtils.mkdir_p(dir) unless File.directory?(dir)
269
270
# Group by days
271
builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
272
# Set stylesheet
273
xml.feed(xmlns: 'http://www.w3.org/2005/Atom') do
274
# Set generator also needs a URI attribute
275
xml.generator('Jekyll', uri: 'https://jekyllrb.com/')
276
xml.link(href: "#{site.config['url']}#{site.baseurl}/#{path}", rel: 'self')
277
xml.link(href: "#{site.config['url']}#{site.baseurl}/", rel: 'alternate')
278
# convert '2024-01-01' to date
279
xml.updated(DateTime.now.rfc3339)
280
xml.id("#{site.config['url']}#{site.baseurl}/#{path}")
281
title_parts = ["GTN", filter_title, lookup[group_by], "Updates"].compact
282
# title used for slack's 'bot name', so should be something useful.
283
xml.title(title_parts.join(' '))
284
xml.subtitle('The latest events, tutorials, slides, blog posts, FAQs, workflows, and contributors in the GTN.')
285
xml.logo("#{site.config['url']}#{site.baseurl}/assets/images/GTN-60px.png")
286
287
bucket.each do |bucket_date, parts|
288
parts.group_by { |x| x[1] }.sort_by { |x| PRIO[x[0]] }.each do |type, items|
289
if items.length.positive?
290
items.each do |date, type, page, tags|
291
# Entry per-item.
292
xml.entry do
293
294
# This is a feed of only NEW tutorials, so we only include publication times.
295
if group_by.nil?
296
xml.published(bucket_date.rfc3339)
297
xml.updated(bucket_date.rfc3339)
298
else
299
xml.published(bucket_date.to_euro_lunch.rfc3339)
300
xml.updated(bucket_date.to_euro_lunch.rfc3339)
301
end
302
303
href = "#{site.config['url']}#{site.config['baseurl']}#{page.url}"
304
305
xml.id(href)
306
xml.link(href: track(href))
307
308
tags.uniq.each do |tag|
309
xml.category(term: tag)
310
end
311
xml.category(term: "new #{page['layout']}")
312
313
if page.data.key?('description')
314
xml.summary(page.data['description'])
315
else
316
md = page.content[0..page.content.index("\n")].strip
317
html = markdownify(site, md)
318
text = Nokogiri::HTML(html).text
319
xml.summary(text)
320
end
321
322
prefix = type.gsub(/s$/, '').gsub(/-/, ' ').capitalize.gsub(/Faq/, 'FAQ').gsub(/New$/, 'Post')
323
title = "#{ICON_FOR[type]} New #{prefix}: #{page.data['title']}"
324
325
xml.title(title)
326
327
had_authors = false
328
Gtn::Contributors.get_authors(page.data).each do |c|
329
xml.author do
330
had_authors = true
331
xml.name(Gtn::Contributors.fetch_name(site, c, warn:false))
332
if c !~ / /
333
xml.uri("#{site.config['url']}#{site.baseurl}/hall-of-fame/#{c}/")
334
end
335
end
336
end
337
338
if !had_authors
339
xml.author do
340
xml.name('GTN')
341
xml.uri("#{site.config['url']}#{site.baseurl}/hall-of-fame/")
342
xml.email('[email protected]')
343
end
344
end
345
346
Gtn::Contributors.get_non_authors(page.data).each do |c|
347
xml.contributor do
348
xml.name(Gtn::Contributors.fetch_name(site, c, warn:false))
349
if c !~ / /
350
xml.uri("#{site.config['url']}#{site.baseurl}/hall-of-fame/#{c}/")
351
end
352
end
353
end
354
355
end
356
end
357
end
358
end
359
end
360
361
xml.author do
362
xml.name('GTN')
363
xml.uri("#{site.config['url']}#{site.baseurl}/hall-of-fame/")
364
xml.email('[email protected]')
365
end
366
end
367
end
368
369
serialise(site, feed_path, builder)
370
end
371
372
373
374
375
# Our old style matrix bot postsx
376
def generate_matrix_feed(site, mats, group_by: 'day', filter_by: nil)
377
# new materials (tut + sli)
378
# new grants/contributors/orgs
379
# new news posts(?)
380
filter_title = nil
381
if !filter_by.nil?
382
mats = mats.select { |x| x[3].include?(filter_by) }
383
filter_title = filter_by.gsub('-', ' ').capitalize
384
end
385
386
case group_by
387
when 'day'
388
# Reject anything that is today
389
mats = mats.reject { |x| x[0].strftime('%Y-%m-%d') == Date.today.strftime('%Y-%m-%d') }
390
when 'week'
391
mats = mats.reject { |x| x[0].strftime('%Y-%W') == Date.today.strftime('%Y-%W') }
392
when 'month'
393
mats = mats.reject { |x| x[0].strftime('%Y-%m') == Date.today.strftime('%Y-%m') }
394
end
395
396
bucket = group_bucket_by(mats, group_by: group_by)
397
lookup = {
398
'day' => 'Daily',
399
'week' => 'Weekly',
400
'month' => 'Monthly'
401
}
402
403
parts = [filter_by || 'matrix', group_by || 'all']
404
path = "feeds/#{parts.join('-')}.xml"
405
406
feed_path = File.join(site.dest, path)
407
Jekyll.logger.info '[GTN/Feeds] Generating matrix feed'
408
409
dir = File.dirname(feed_path)
410
FileUtils.mkdir_p(dir) unless File.directory?(dir)
411
412
# Group by days
413
414
builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
415
# Set stylesheet
416
xml.feed(xmlns: 'http://www.w3.org/2005/Atom') do
417
# Set generator also needs a URI attribute
418
xml.generator('Jekyll', uri: 'https://jekyllrb.com/')
419
xml.link(href: "#{site.config['url']}#{site.baseurl}/#{path}", rel: 'self')
420
xml.link(href: "#{site.config['url']}#{site.baseurl}/", rel: 'alternate')
421
# convert '2024-01-01' to date
422
xml.updated(DateTime.now.rfc3339)
423
xml.id("#{site.config['url']}#{site.baseurl}/#{path}")
424
title_parts = [filter_title, "#{lookup[group_by]} Updates"].compact
425
xml.title(title_parts.join(' — '))
426
xml.subtitle('The latest events, tutorials, slides, blog posts, FAQs, workflows, learning paths, recordings, and contributors in the GTN.')
427
xml.logo("#{site.config['url']}#{site.baseurl}/assets/images/GTN-60px.png")
428
429
bucket.each do |date, parts|
430
xml.entry do
431
case group_by
432
when 'day'
433
title = "#{date.strftime('%B %d, %Y')}"
434
when 'week'
435
title = "#{date.strftime('W%W, %Y')}"
436
when 'month'
437
title = "#{date.strftime('%B %Y')}"
438
end
439
xml.title(title)
440
# Our IDs should be stable
441
xml.id("#{site.config['url']}#{site.baseurl}/#{group_by}/#{date.strftime('%Y-%m-%d')}")
442
443
# This is a feed of only NEW tutorials, so we only include publication times.
444
xml.published(parts.map { |x| x[0] }.min.to_datetime.rfc3339)
445
xml.updated(parts.map { |x| x[0] }.max.to_datetime.rfc3339)
446
447
# xml.category(term: "new #{type}")
448
xml.content(type: 'xhtml') do
449
xml.div(xmlns: 'http://www.w3.org/1999/xhtml') do
450
# xml.h4 title
451
452
parts.group_by { |x| x[1] }.sort_by { |x| PRIO[x[0]] }.each do |type, items|
453
xml.h4 "#{ICON_FOR[type]} #{type.gsub(/-/, ' ').capitalize}"
454
if items.length.positive?
455
xml.ul do
456
items.each do |date, _type, page, _tags|
457
xml.li do
458
if page.is_a?(String)
459
href = track("#{site.config['url']}#{site.config['baseurl']}/hall-of-fame/#{page}/")
460
text = "@#{page}"
461
else
462
text = page.data['title']
463
href = track("#{site.config['url']}#{site.config['baseurl']}#{page.url}")
464
end
465
if group_by != 'day'
466
text += " (#{date.strftime('%B %d, %Y')})"
467
end
468
469
xml.a(text, href: href)
470
end
471
end
472
end
473
end
474
end
475
476
if group_by != 'day'
477
xml.small do
478
xml.span 'Powered by '
479
xml.a('GTN RSS Feeds', href: 'https://training.galaxyproject.org/training-material/news/2024/06/04/gtn-standards-rss.html')
480
end
481
end
482
end
483
end
484
485
xml.author do
486
xml.name('GTN')
487
xml.uri("#{site.config['url']}#{site.baseurl}/hall-of-fame/")
488
xml.email('[email protected]')
489
end
490
end
491
end
492
end
493
end
494
495
serialise(site, feed_path, builder)
496
end
497
498
def generate_event_feeds(site)
499
events = site.pages.select { |x| x['layout'] == 'event' || x['layout'] == 'event-external' }
500
feed_path = File.join(site.dest, 'events', 'feed.xml')
501
Jekyll.logger.info '[GTN/Feeds] Generating event feed'
502
503
# Pre-filering.
504
updated = events.map { |x| Gtn::PublicationTimes.obtain_time(x.path) }.max
505
506
events = events
507
.reject { |x| x.data.fetch('draft', '').to_s == 'true' }
508
.reject { |x| x.data['event_over'] == true } # Remove past events, prunes our feed nicely.
509
.sort_by { |page| Gtn::PublicationTimes.obtain_time(page.path) }
510
.reverse
511
512
if !events.empty?
513
Jekyll.logger.debug "Found #{events.length} events"
514
end
515
516
builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
517
# Set stylesheet
518
xml.feed(xmlns: 'http://www.w3.org/2005/Atom') do
519
# Set generator also needs a URI attribute
520
xml.generator('Jekyll', uri: 'https://jekyllrb.com/')
521
xml.link(href: "#{site.config['url']}#{site.baseurl}/events/feed.xml", rel: 'self')
522
xml.link(href: "#{site.config['url']}#{site.baseurl}/events/", rel: 'alternate')
523
xml.updated(updated.to_datetime.rfc3339)
524
xml.id("#{site.config['url']}#{site.baseurl}/events/feed.xml")
525
xml.title('Events')
526
xml.subtitle('Events in the Inter-Galactic Network')
527
xml.logo("#{site.config['url']}#{site.baseurl}/assets/images/GTN-60px.png")
528
529
events.each do |page|
530
xml.entry do
531
pdate = collapse_event_date_pretty(page.data)
532
xml.title("[#{pdate}] #{page.data['title']}")
533
link = "#{site.config['url']}#{site.baseurl}#{page.url}"
534
xml.link(href: link)
535
# Our links are stable
536
xml.id(link)
537
538
# This is a feed of only NEW tutorials, so we only include publication times.
539
# xml.published(Gtn::PublicationTimes.obtain_time(page.path).to_datetime.rfc3339)
540
xml.published(Gtn::PublicationTimes.obtain_time(page.path).to_datetime.rfc3339)
541
xml.updated(Gtn::PublicationTimes.obtain_time(page.path).to_datetime.rfc3339)
542
543
# TODO: find a better solution maybe with namespaces?
544
xml.category(term: "starts:#{page.data['date_start'].to_datetime.rfc3339}")
545
xml.category(term: "ends:#{(page.data['date_end'] || page.data['date_start']).to_datetime.rfc3339}")
546
xml.category(term: "days:#{page.data['duration']}")
547
if page.data['external']
548
xml.link(href: page.data['external'])
549
end
550
551
# xml.path(page.path)
552
xml.category(term: "new #{page['layout']}")
553
# xml.content(page.content, type: "html")
554
xml.summary(page.data['description'])
555
556
if page.data['location'] && page.data['location']['geo']
557
lat = page.data['location']['geo']['lat']
558
lon = page.data['location']['geo']['lon']
559
xml.georss('point', "#{lat} #{lon}")
560
end
561
562
Gtn::Contributors.get_organisers(page.data).each do |c|
563
xml.author do
564
xml.name(Gtn::Contributors.fetch_name(site, c, warn:false))
565
xml.uri("#{site.config['url']}#{site.baseurl}/hall-of-fame/#{c}/")
566
if page.data['contact_email']
567
xml.email(page.data['contact_email'])
568
end
569
end
570
end
571
572
Gtn::Contributors.get_instructors(page.data).each do |c|
573
xml.contributor do
574
xml.name(Gtn::Contributors.fetch_name(site, c, warn:false))
575
xml.uri("#{site.config['url']}#{site.baseurl}/hall-of-fame/#{c}/")
576
end
577
end
578
end
579
end
580
end
581
end
582
583
serialise(site, feed_path, builder)
584
end
585
586
# Basically like `PageWithoutAFile`
587
Jekyll::Hooks.register :site, :post_write do |site|
588
if Jekyll.env == 'production'
589
opml = {}
590
generate_event_feeds(site)
591
opml['GTN Events'] = [
592
{title: 'Events', url: "#{site.config['url']}#{site.baseurl}/events/feed.xml"}
593
]
594
595
bucket = Gtn::TopicFilter.all_date_sorted_resources(site)
596
bucket.freeze
597
598
opml['GTN Topics'] = []
599
opml['GTN Topics - Digests'] = []
600
Gtn::TopicFilter.list_topics(site).each do |topic|
601
generate_topic_feeds(site, topic, bucket)
602
opml['GTN Topics'] <<
603
{title: "#{topic} all changes", url: "#{site.config['url']}#{site.baseurl}/topic/feed.xml"}
604
605
generate_matrix_feed(site, bucket, group_by: 'month', filter_by: topic)
606
generate_matrix_feed_itemized(site, bucket, group_by: nil, filter_by: topic)
607
608
opml['GTN Topics - Digests'] <<
609
{title: "#{topic} monthly changes", url: "#{site.config['url']}#{site.baseurl}/feeds/#{topic}-month.xml"}
610
end
611
612
generate_matrix_feed(site, bucket, group_by: 'day')
613
generate_matrix_feed(site, bucket, group_by: 'week')
614
generate_matrix_feed(site, bucket, group_by: 'month')
615
616
generate_matrix_feed_itemized(site, bucket, group_by: nil)
617
generate_matrix_feed_itemized(site, bucket, group_by: 'day')
618
619
opml['GTN Digests'] = [
620
{title: "GTN Firehose", url: "#{site.config['url']}#{site.baseurl}/feeds/matrix.i.xml"},
621
{title: "GTN daily changes", url: "#{site.config['url']}#{site.baseurl}/feeds/matrix-daily.xml"},
622
{title: "GTN daily changes (itemized, one change per entry)", url: "#{site.config['url']}#{site.baseurl}/feeds/matrix-daily.i.xml"},
623
{title: "GTN weekly changes", url: "#{site.config['url']}#{site.baseurl}/feeds/matrix-weekly.xml"},
624
{title: "GTN monthly changes", url: "#{site.config['url']}#{site.baseurl}/feeds/matrix-monthly.xml"}
625
]
626
627
generate_opml(site, opml)
628
end
629
end
630
631