25/lib/mse25_web/controllers/feed_view.ex
Anders Englöf Ytterström 57e935ec00
Improve HTML for robot consumtion (#24)
* Fix icalendar validation errors

* Add RSS feed to documents

* Add stuff to meta: opengraph, canonical

* Add SEO robots meta elements

* Fix correct page titles

* Add more semantics to HTML

* Remove breadcrumbs from templates

* Render breadcrumbs in layout

Each controller should provide their own breadcrumb
trail as a list of tuples, where each tuple is the
pair of a slugified key and a human readable label.

Example:

[{"blog", "Webblogg"}]
[{"blog", "Webblogg"}, "2024", "2024"]

* Add CSS util class to show content only to screen readers

* Load interactive event map only on events page

* Decrease home logo size

* Use correct HTML element for time

* Improve Home page HTML semantics

* Add Person RFDa to footer

* Add RDFa to articles: annual, item, articles

* Enrich links semantics using RDFa

* Enrich Page semantics using RDFa

* Enrich Album semantics using RFDa

* Enrich Event semantics with RDFa
2024-10-16 15:40:53 +02:00

269 lines
8.1 KiB
Elixir

defmodule Mse25Web.FeedView do
use Mse25Web, :html
def rss(items, _host) do
~s"""
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:georss="http://www.georss.org/georss" xmlns:gml="http://www.opengis.net/gml">
<channel>
<title>madr.se</title>
<description>The online home of Anders Englöf Ytterström, a metalhead and musician living and working in Borlänge, Sweden.</description>
<language>sv</language>
<link>https://madr.se/</link>
<managingEditor>yttan@fastmail.se (Anders Englöf Ytterström)</managingEditor>
<webMaster>yttan@fastmail.se (Anders Englöf Ytterström)</webMaster>
<atom:link href="https://madr.se/prenumerera.xml" rel="self" type="application/rss+xml" />
#{Enum.map(items, &rss_item/1)}
</channel>
</rss>
"""
end
def calendar(upcoming) do
~s"""
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//https://madr.se//kommande-evenemang.ics//SE
CALSCALE:GREGORIAN
X-ORIGINAL-URL:https://madr.se
X-WR-CALDESC: Kommande evenemang, madr.se
METHOD:PUBLISH
REFRESH-INTERVAL;VALUE=DURATION:PT1H
X-Robots-Tag:noindex
X-PUBLISHED-TTL:PT1H
BEGIN:VTIMEZONE
TZID:CEST
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:20000630T000000
END:STANDARD
END:VTIMEZONE
#{upcoming |> Enum.map(fn %{id: id, title: title, created_at: created_at, starts_at: starts_at, ends_at: ends_at, longitude: longitude, latitude: latitude, lead: lead, venue: venue, region: region} -> ~s"""
BEGIN:VEVENT
UID:#{starts_at}.#{id}@madr.se
DTSTAMP:#{created_at}
CREATED:#{created_at}
LAST-MODIFIED:#{created_at}
DTSTART;TZID=CEST:#{starts_at}T060606
DTEND;TZID=CEST:#{ends_at}T060606
SUMMARY:#{title}
DESCRIPTION:#{lead}
LOCATION:#{venue}\, #{region}
GEO:#{latitude};#{longitude}
END:VEVENT
""" end) |> Enum.join("")}END:VCALENDAR
"""
end
def event_map(markers) do
~s"""
(function(g, document) {
"use strict";
const mapData = [
#{markers |> Enum.map(fn %{date: date, latitude: latitude, longitude: longitude, title: title, region: region, venue: venue} -> ~s"""
{
location: [#{longitude}, #{latitude}],
title: "#{title}",
date: "#{date}",
region: "#{region}",
venue: "#{venue}"
}
""" end) |> Enum.join(",")}
]
// insert Leaflet styles (<link>) to <head> and <script> to
// bottom of <body>, and initiate the map when the assets have
// loaded.
var install = function() {
var styles, leaflet;
styles = document.createElement("link");
styles.rel = "stylesheet";
styles.href = "https://unpkg.com/leaflet@1.6.0/dist/leaflet.css";
styles.integrity =
"sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==";
styles.crossOrigin = "";
document.head.appendChild(styles);
leaflet = document.createElement("script");
leaflet.src = "https://unpkg.com/leaflet@1.6.0/dist/leaflet.js";
leaflet.integrity =
"sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew==";
leaflet.crossOrigin = "";
leaflet.async = true;
leaflet.onload = function() {
init(mapData);
};
document.body.appendChild(leaflet);
};
// inititate the map.
// create markers for all events: one marker per venue. Display all
// events on each venue through a popup by clicking on the marker.
// set bounds to have all markers visible.
var init = function(mapData) {
var events,
markers = {},
L = g.L;
if (L) {
// create map and set initial bounds
events = L.map("leaflet", { zoomDelta: 0.25, zoomSnap: 0 }).fitBounds(
mapData.map(function(e) {
return e.location;
})
);
// use OpenStreetMap tile layer: it's free!
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png?{foo}", {
foo: "bar",
attribution:
'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>'
}).addTo(events);
// group events by venue
mapData.forEach(function(e) {
if (markers[e.venue]) {
markers[e.venue].events.push(e.date + " - " + e.title);
} else {
markers[e.venue] = {
region: e.region,
location: e.location,
events: [e.date + " - " + e.title]
};
}
});
// create markers
for (var m in markers) {
if (markers.hasOwnProperty(m)) {
L.marker(markers[m].location)
.addTo(events)
.bindPopup(
"<strong style='color: black'>" +
m +
", " +
markers[m].region +
"</strong><br>" +
markers[m].events.join("<br>")
);
}
}
}
};
install();
})(this, document);
"""
end
def pub_date(nil), do: ""
def pub_date(%{"pubDate" => date}), do: format_rfc822(date)
def pub_date(%{"started_at" => date}), do: format_rfc822(date)
def pub_date(%{"purchased_at" => date}), do: format_rfc822(date)
defp format_rfc822(date_time) when is_binary(date_time) do
date_time
|> Date.from_iso8601!()
|> format_rfc822()
end
defp format_rfc822(date_time), do: Calendar.strftime(date_time, "%a, %d %b %Y 06:06:06 +0200")
defp rss_item(%{
:t => :articles,
"title" => title,
"contents" => contents,
"slug" => slug,
"pubDate" => date
}) do
~s"""
<item>
<title>#{title}</title>
<link>https://madr.se/#{slug}</link>
<description>
<![CDATA[#{Earmark.as_html!(contents)}]]>
</description>
<pubDate>#{format_rfc822(date)}</pubDate>
<guid>https://madr.se/#{slug}</guid>
</item>
"""
end
defp rss_item(%{
:t => :events,
"title" => title,
"contents" => contents,
"slug" => slug,
"location" => %{
"position" => %{
"coordinates" => [lng, lat]
}
},
"started_at" => date
}) do
~s"""
<item>
<title>#{title}</title>
<link>https://madr.se/#{slug}</link>
<description>
<![CDATA[#{Earmark.as_html!(contents)}]]>
</description>
<pubDate>#{format_rfc822(date)}</pubDate>
<guid>https://madr.se/#{slug}</guid>
<georss:where>
<gml:Point>
<gml:pos>#{to_string(lng) <> " " <> to_string(lat)}</gml:pos>
</gml:Point>
</georss:where>
</item>
"""
end
defp rss_item(%{
:t => :links,
"title" => title,
"contents" => contents,
"slug" => slug,
"pubDate" => date,
"source" => link
}) do
~s"""
<item>
<title>#{title}</title>
<link>#{link}</link>
<description>
<![CDATA[#{Earmark.as_html!(contents)}]]>
</description>
<pubDate>#{format_rfc822(date)}</pubDate>
<guid>https://madr.se/#{slug}</guid>
</item>
"""
end
defp rss_item(%{
:t => :albums,
"summary" => summary,
"contents" => contents,
"purchased_at" => date,
"externalId" => id,
"purchase_year" => year
}) do
~s"""
<item>
<title>#{summary}</title>
<link>https://madr.se/#{year}/#{id}</link>
<description>
<![CDATA[#{Earmark.as_html!(contents)}]]>
</description>
<pubDate>#{format_rfc822(date)}</pubDate>
<guid>https://madr.se/#{year}/#{id}</guid>
</item>
"""
end
end