Compare commits

..

2 commits

Author SHA1 Message Date
7265184fc5 WIP 2025-02-11 13:15:58 +01:00
2386a56036 Add dev secret template to README 2025-02-11 13:15:43 +01:00
13 changed files with 421 additions and 584 deletions

View file

@ -16,3 +16,14 @@ Ready to run in production? Please [check our deployment guides](https://hexdocs
* Docs: https://hexdocs.pm/phoenix * Docs: https://hexdocs.pm/phoenix
* Forum: https://elixirforum.com/c/phoenix-forum * Forum: https://elixirforum.com/c/phoenix-forum
* Source: https://github.com/phoenixframework/phoenix * Source: https://github.com/phoenixframework/phoenix
## Dev secret template
```
import Config
directus_url = ""
directus_token = ""
config :mse25, :directus, base_url: directus_url, token: directus_token
```

View file

@ -1,580 +1,222 @@
/*
Main CSS file for madr.se
If you have any questions regarding the CSS, feel free
to contact me: yttan at madr dot se
Table of contents, 7-1 inspired
1. Base
2. Components
3. Layout
4. Pages
5. Themes
6. Vendors
7. Shame
*/
/* === 1. Base === */
/*
Normalize and resets.
Only properties, element and attribute selectors are allowed in this
section.
All dynamic values that should change according to user preferences
(dark or light color mode, reduced motion etc) and agent abilities
(small handheld screen, big desktop screen) are handled by properties
when appliable.
This is to avoid redeclarating CSS rules.
*/
:root {
/* colors, dark mode default */
--color: hsl(0 0 90%);
--bgcolor: hsl(180 75% 6%);
--tree-item-accent-color: dimgrey;
--panel-bg-color: hsla(0 0 50% / 0.16);
--monospace-color: springgreen;
--monospace-color-inline: seagreen;
--a-color: gold;
/* typography, mobile first */
--base-font-size: 1.33em;
--page-title-font-size: 2em;
--tree-font-size: 0.85em;
--section-heading-lv2-font-size: 1.5em;
--section-heading-lv3-font-size: 1.2em;
--section-heading-lv4-font-size: 1em;
--system-serif-fonts: Cambria, Cochin, Georgia, Times, "Times New Roman",
serif;
--system-sansserif-fonts: apple-system, system-ui, BlinkMacSystemFont,
Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif;
--monospace-fonts: "JetBrains mono", monaco, menlo, meslo, "Courier New",
Courier, monospace;
/* whitespace */
--gap-sm: 0.5em;
--gap-md: 1em;
--gap-lg: 3em;
/* transitions */
--animation-duration: 0.5s;
/* dimensions and aspect ratios */
--map-ratio: 1;
@media (min-width: 666px) {
--base-font-size: 1.66em;
--tree-font-size: 0.67em;
}
}
html {
color: var(--color);
background-color: var(--bgcolor);
font: normal var(--base-font-size) / 1.5 var(--system-sansserif-fonts);
}
body { body {
background: #222;
margin: 0; margin: 0;
background-image: linear-gradient(
> footer > p { 175deg,
margin-top: var(--gap-lg); #212223,
color: #666; #222 350px,
font-size: 0.66em; #302928 345px,
} #282828
} );
font-family:
a { system-ui,
color: var(--a-color); -apple-system,
} BlinkMacSystemFont,
"Segoe UI",
input, Roboto,
button { Oxygen,
font-size: 1.2em; Ubuntu,
padding: 0.25em; Cantarell,
} "Open Sans",
"Helvetica Neue",
h1, sans-serif;
h2,
h3 {
font-family: var(--system-serif-fonts);
} }
h1 { h1 {
margin: 0.5em 0; font-size: 1.75em;
line-height: 0.95; letter-spacing: -0.066em;
font-size: var(--page-title-font-size);
@media (min-width: 666px) {
text-transform: lowercase;
margin-top: 0.5em;
margin-bottom: 0.5em;
text-align: right;
line-height: 0.9;
color: var(--tree-item-accent-color);
font-weight: normal;
}
}
h2,
h3 {
margin-top: 2em;
line-height: 1.1;
} }
h2 { h2 {
font-size: var(--section-heading-lv2-font-size); font-size: 1.33em;
border-bottom: 3px solid var(--panel-bg-color);
} }
h3 { p,
font-size: var(--section-heading-lv3-font-size); li {
line-height: 1.66em;
} }
h3,
h4 { h4 {
font-size: var(--section-heading-lv4-font-size); font-size: 1em;
}
main {
margin: 0 0 0 13em;
max-width: 37em;
}
.cards {
display: flex;
flex-direction: column;
gap: 1.33em;
}
.card {
background-color: #fff;
border: 2px solid #444;
padding: 3em;
border-radius: 9px;
box-shadow: 0 0 5px #000;
:first-child {
margin-top: 0;
}
:last-child {
margin-bottom: 0;
}
&.collapsed {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
} }
pre { pre {
margin: 2em 0; padding: 0.5em;
background-color: #022; background-color: #1e2025;
color: var(--monospace-color); color: #96df71;
position: relative;
margin: 1em 2em;
overflow-y: auto; overflow-y: auto;
padding: 0.66em;
box-shadow: 4px 4px 0 var(--panel-bg-color);
position: relative;
line-height: 1.2;
font-size: 0.8em;
> button { button {
font-size: 0.75em;
position: absolute; position: absolute;
top: 0.25em; top: 2px;
right: 0.25em; right: 2px;
} }
} }
code {
font-family: var(--monospace-fonts);
&.inline {
color: var(--monospace-color-inline);
background: #f3f3f3;
font-size: 0.9em;
}
}
section {
position: relative;
& > h2 {
background: var(--bgcolor);
color: var(--color);
padding: 0.5em 0.25em;
border-bottom: 0;
}
}
img {
max-width: 100%;
display: block;
height: auto;
}
ul,
ol {
clear: left;
}
p {
margin: 1em 0;
}
article {
line-height: 1.33;
}
figure {
margin: 0;
}
figcaption {
text-align: center;
margin-top: 0.5em;
}
table {
width: 100%;
}
td,
th {
background-color: var(--background-color-l);
padding: 0.25em;
font-size: 0.8em;
border: 1px solid rgb(128, 128, 128, 0.5);
}
th {
background-color: var(--background-color-ll);
text-transform: uppercase;
color: var(--em-color);
font-weight: normal;
}
li {
color: var(--em-color);
margin: 0.25em 0;
}
li:first-child {
margin-top: 0;
}
li:last-child {
margin-bottom: 0;
}
li::marker {
color: var(--link-color);
}
blockquote {
color: var(--em-color);
font-size: 1.2em;
line-height: 1.2;
font-style: italic;
border-left: 5px solid var(--background-color-l);
margin: 1em 1em 1em 0;
padding-left: 1em;
}
blockquote p::after,
blockquote p::before {
content: '"';
}
/* === /Base === */
/* === 2. Components === */
/*
Use kebab case named classes to identify components, and nesting
to group subcomponents.
Element selectors are preferred as subcomponents, due to the simple
nature of this site. As a general rule though, classes are the most
versatile.
*/
.home-search,
.profiles {
font-size: var(--tree-font-size);
}
.sr-only { .sr-only {
position: absolute; position: absolute;
left: -999em; width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
} }
.flx { .breadcrumbs {
display: flex; border-radius: 8px 8px 0 0;
justify-content: space-between; padding: 1em;
align-items: center; > .trail > span::after {
} content: "/";
}
background: #080808;
background-image: linear-gradient(
#080808 0,
#080808 50%,
#161616 50%,
#080808 100%
);
color: #fff;
.sticky { a {
position: sticky; color: #fff;
top: 0;
}
.interactive-map {
aspect-ratio: var(--map-ratio);
}
.home-h1 {
font-size: 1.33em;
}
.list-link {
&::after {
content: " →";
} }
} }
.feed-link { .head {
&::after { color: #fff;
content: " ↗"; background-color: #345;
padding: 1em 2em;
border-radius: 0 0 8px 8px;
margin-bottom: 1.5em;
:first-child {
margin-top: 0;
}
a {
color: #fff;
} }
} }
.skiplink { .skiplink {
position: absolute; position: absolute;
top: -5em; top: -1.75em;
transition: top var(--animation-duration) ease-out; left: 1em;
padding: 0.25em 0.5em; background-color: #ff0;
color: #000;
text-decoration: none;
padding: 0.25em;
transition: top 0.25s ease-out;
&:focus { &:focus {
top: 1em; top: 1em;
} }
} }
.tree { nav {
list-style: none;
margin: 0;
padding: 0;
display: flex; display: flex;
flex-direction: column; margin: 2em 0 1em;
gap: 0.66em;
font-size: var(--tree-font-size);
> li { > ul {
text-align: center; list-style: none;
display: grid;
grid-template-columns: 50px 1fr auto;
align-items: center;
margin: 0;
gap: 0.5em;
padding: 0.75em;
min-height: 50px;
background-color: rgba(128, 128, 128, 0.1);
border: 1px solid rgba(192, 192, 192, 0.1);
&:focus-within {
background-color: rgba(128, 128, 128, 0.25);
}
> small {
opacity: 0.66;
font-family: var(--monospace-fonts);
font-size: 0.66em;
}
}
> .article {
--tree-item-accent-color: rebeccapurple;
}
> .album {
--tree-item-accent-color: goldenrod;
}
> .link {
--tree-item-accent-color: honeydew;
}
> .events {
--tree-item-accent-color: firebrick;
}
a {
color: var(--color);
text-decoration: none;
flex: 1;
&:hover,
&:focus {
text-decoration: underline;
}
}
}
.landing {
display: flex;
flex-direction: column;
align-items: center;
padding: 2em 0;
box-sizing: border-box;
gap: 1.66em;
}
.breadcrumbs {
display: block;
margin: var(--gap-sm) 0;
padding: var(--gap-sm);
border: 1px solid rgb(128, 128, 128, 0.25);
background-color: var(--panel-bg-color);
border-radius: 0;
> span {
display: inline;
&:after {
content: " /";
}
}
a {
color: var(--color);
}
}
.months {
grid-auto-flow: rows;
display: grid;
list-style: none;
grid-template-columns: repeat(3, 1fr);
gap: 0.5em;
padding-left: 0;
margin: 2.75em 0;
> li {
margin: 0;
}
}
.article {
> div > p:first-child::first-letter {
@media (min-width: 500px) {
float: left;
font-size: 7em;
border: 8px double hsl(0 0 50%);
padding: 0.1em;
margin-right: 0.066em;
font-style: normal;
font-family: var(--system-serif-fonts);
}
}
> footer {
font-style: italic;
font-size: 0.8em;
text-align: right;
> p {
margin-top: 2.75em;
margin-bottom: 0;
}
}
}
.articles {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3em;
grid-auto-rows: 1fr;
> * {
display: flex;
flex-direction: column;
justify-content: flex-end;
border-top: 6px solid red;
&:focus-within {
background-color: #333;
}
}
time {
border-top: 1px solid crimson;
padding: 0.25em 0.5em;
}
}
.links {
> :is(h2, h3) {
margin-top: 3em;
}
}
.brutal-legend {
display: flex;
gap: 1em;
flex-direction: row-reverse;
> p {
flex: 1;
margin: 0;
}
> img {
aspect-ratio: 1;
}
}
.profiles {
display: flex;
gap: 1.66em;
list-style: none;
margin: 0;
> li {
margin: 0; margin: 0;
padding: 0; padding: 0;
display: flex;
gap: 1.5em;
background: #111;
padding: 0.33em 1.5em 0.25em;
border-radius: 0 1.33em 1.33em 0;
border: 2px solid #333;
border-left-width: 0;
padding-left: 13em;
}
a {
text-decoration: none;
color: #fff;
line-height: 1;
} }
} }
/* === /Components === */ .anders {
/* === 3. Layout === */ img {
/* position: fixed;
Containers and wrappers for components. top: 1em;
Only class selectors allowed, with the following element selectors as left: 1em;
exceptions: aside, body, footer, header, main and nav. aspect-ratio: 1;
*/ border-radius: 50%;
body { width: 175px;
margin: 0 auto;
max-width: 33em;
box-sizing: border-box;
min-height: 100vh;
padding: 0 0.5em;
}
/* === /Layout === */
/* === 4. Pages === */
/*
Styles that should only apply to certain pages.
*/
/* === /Pages === */
/* === 5. Themes === */
/*
Styles to create user-customized themes.
This section adapts the design to the following user preferences: &:hover {
Color theme, reduced motion animation: 1s ease-out 0s infinite alternate burst;
*/ }
@media (prefers-color-scheme: light) { }
:root {
--color: #000; img + img {
--bgcolor: #fff; top: 12.5em;
--a-color: blue; left: 5em;
width: 125px;
}
img + img + img {
top: 18.5em;
left: 0.5em;
width: 90px;
} }
} }
@media (prefers-reduced-motion) { .bookmarks {
:root { display: flex;
--animation-duration: 0; flex-direction: column;
gap: 2em;
}
@keyframes burst {
0% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(0.8);
} }
} }
@media (min-width: 800px) {
:root {
--map-ratio: 3 / 2;
}
}
@media (min-width: 1000px) {
:root {
--page-title-font-size: 4em;
}
}
/* === /Themes === */
/* === 6. Vendors === */
/*
Styles belonging to third-party components.
*/
.footnotes-list {
color: var(--aside-color);
font-size: 80%;
}
/* === /Vendors === */
/* === 7. Shame === */
/*
Styles necessary for specifity issues and for cutting corners
(breaking the rules in short terms in waiting for an opportunity
to rewrite or fix a problem for good).
madr.se has no reason to feel ashamed. Yet.
*/
/* === /Shame === */

View file

@ -42,6 +42,31 @@ defmodule Mse25.Directus do
get("/articles?" <> params) get("/articles?" <> params)
end end
def get_note(slug) do
get_item(:notes, slug)
end
def get_notes!(options \\ []) do
params =
[
"fields=" <>
Enum.join(
[
"id",
"contents",
"images",
"date_created",
"location"
],
","
)
]
|> annual?(:notes, options)
|> query_params_string(options, :notes)
get("/notes?" <> params)
end
def get_album(externalId) do def get_album(externalId) do
case get_item( case get_item(
:albums, :albums,
@ -198,6 +223,13 @@ defmodule Mse25.Directus do
end end
end end
defp get_item(:notes, externalId, fields) do
case get("/notes?fields=" <> fields <> "&filter[id][_eq]=" <> externalId) do
[] -> {:not_found, externalId}
[item | _] -> {:ok, item}
end
end
defp get_item(collection, slug, fields) do defp get_item(collection, slug, fields) do
case get( case get(
"/" <> to_string(collection) <> "?fields=" <> fields <> "&filter[slug][_eq]=" <> slug "/" <> to_string(collection) <> "?fields=" <> fields <> "&filter[slug][_eq]=" <> slug

View file

@ -1,26 +1,36 @@
<a href="#content" class="skiplink">Hoppa till innehållet</a> <a href="#content" class="skiplink">Hoppa till innehållet</a>
<nav> <nav>
<span class="sr-only">Du är här:</span> <ul>
<span class="breadcrumbs" itemscope itemtype="https://schema.org/BreadcrumbList"> <li><a href="/webblogg">Webblogg</a></li>
<span itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"> <li><a href="/delningar">Länkar</a></li>
<a href="/" rel="home"> <li><a href="/anteckningar">Anteckningar</a></li>
<span itemprop="name">madr.se</span> <li><a href="/evenemang">Evenemang</a></li>
</a> <li><a href="/om">Om</a></li>
<meta itemprop="position" content="1" /> </ul>
</span>
<%= for {index, {parent_slug, parent_name}} <- breadcrumbs(@breadcrumbs) do %>
<span class="sr-only">&gt;</span>
<span itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<a href={parent_slug}>
<span itemprop="name"><%= parent_name %></span>
</a>
<meta itemprop="position" content={index} />
</span>
<% end %>
</span>
</nav> </nav>
<main id="content"> <main id="content">
<div class="breadcrumbs">
<span class="sr-only">Du är här:</span>
<span class="trail" itemscope itemtype="https://schema.org/BreadcrumbList">
<span itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<a href="/" rel="home">
<span itemprop="name">madr.se</span>
</a>
<meta itemprop="position" content="1" />
</span>
<%= for {index, {parent_slug, parent_name}} <- breadcrumbs(@breadcrumbs) do %>
<span class="sr-only">&gt;</span>
<span itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<a href={parent_slug}>
<span itemprop="name"><%= parent_name %></span>
</a>
<meta itemprop="position" content={index} />
</span>
<% end %>
</span>
</div>
<%= @inner_content %> <%= @inner_content %>
</main> </main>
<%= if show_footer?(assigns) do %> <%= if show_footer?(assigns) do %>

View file

@ -41,6 +41,13 @@ defmodule Mse25Web.ItemController do
end end
end end
defp fetch([_year, album_id], :note) do
case Directus.get_note(album_id) do
{:ok, response} -> {:ok, :note, response}
not_found -> not_found
end
end
defp fetch([year, "brutal-legend-" <> external_id]) do defp fetch([year, "brutal-legend-" <> external_id]) do
fetch([year, external_id], :album) fetch([year, external_id], :album)
end end
@ -174,6 +181,21 @@ defmodule Mse25Web.ItemController do
] ]
end end
defp assigns(:note, %{
"contents" => text,
"images" => images,
"date_created" => published_at
}) do
year = String.slice(published_at, 0..3)
[
text: Earmark.as_html!(text),
breadcrumbs: [{"anteckningar", "Anteckningar"}, {year, year, ""}],
date_created: String.slice(published_at, 0..9),
images: images
]
end
defp assigns(:album, %{ defp assigns(:album, %{
"year" => year, "year" => year,
"album" => album, "album" => album,

View file

@ -1,4 +1,4 @@
<article class="article" vocab="https://schema.org/" typeof="Article"> <article class="collapsed article card" vocab="https://schema.org/" typeof="Article">
<h1 property="name"><%= @heading %></h1> <h1 property="name"><%= @heading %></h1>
<div property="articleBody"> <div property="articleBody">

View file

@ -0,0 +1,10 @@
<article class="collapsed article card" vocab="https://schema.org/" typeof="Article">
<h1 property="name articleBody"><%= raw(@contents) %></h1>
<footer>
<p>
Publicerad <time property="datePublished"><%= @published_at %></time>
av <span property="publisher">Anders Englöf Ytterström</span>
</p>
</footer>
</article>

View file

@ -1,4 +1,4 @@
<article class="article" vocab="https://schema.org/" typeof="Article"> <article class="card article" vocab="https://schema.org/" typeof="Article">
<h1 property="name"><%= @heading %></h1> <h1 property="name"><%= @heading %></h1>
<div property="articleBody"> <div property="articleBody">

View file

@ -68,6 +68,26 @@ defmodule Mse25Web.PageController do
) )
end end
def notes(conn, params) do
{notes, page_title} =
case params do
%{"q" => query_string} ->
{Directus.get_notes!(limit: @almost_infinity, query: query_string),
"Anteckningar: \"#{query_string}\""}
_ ->
{Directus.get_notes!(limit: @almost_infinity), "Anteckningar"}
end
render(conn, :notes,
page_title: page_title,
breadcrumbs: [],
notes: group_by_creation_date(notes),
q: params["q"],
nosearch?: params["q"] == nil or params["q"] == ""
)
end
def events(conn, params) do def events(conn, params) do
{_, %{"title" => title, "contents" => contents}} = Directus.get_page("evenemang") {_, %{"title" => title, "contents" => contents}} = Directus.get_page("evenemang")
@ -93,7 +113,7 @@ defmodule Mse25Web.PageController do
end end
def links(conn, _params) do def links(conn, _params) do
links = Directus.get_links!(limit: @almost_infinity) |> group_by_date links = Directus.get_links!(limit: @almost_infinity) |> group_by_pub_date
render(conn, :links, render(conn, :links,
page_title: "Delningar", page_title: "Delningar",
@ -109,10 +129,17 @@ defmodule Mse25Web.PageController do
|> Enum.sort(fn {a, _a}, {b, _b} -> b < a end) |> Enum.sort(fn {a, _a}, {b, _b} -> b < a end)
end end
defp group_by_date(items) do defp group_by_pub_date(items) do
items items
|> Enum.group_by(fn %{"pubDate" => pub_date} -> pub_date end) |> Enum.group_by(fn %{"pubDate" => pub_date} -> pub_date end)
|> Map.to_list() |> Map.to_list()
|> Enum.sort(fn {a, _a}, {b, _b} -> b < a end) |> Enum.sort(fn {a, _a}, {b, _b} -> b < a end)
end end
defp group_by_creation_date(items) do
items
|> Enum.group_by(fn %{"date_created" => pub_date} -> String.slice(pub_date, 0..9) end)
|> Map.to_list()
|> Enum.sort(fn {a, _a}, {b, _b} -> b < a end)
end
end end

View file

@ -3,4 +3,8 @@ defmodule Mse25Web.PageHTML do
import Mse25.EventHelpers import Mse25.EventHelpers
embed_templates "page_html/*" embed_templates "page_html/*"
defp fancy_timestamp(datestr) do
datestr |> IO.inspect()
end
end end

View file

@ -1,41 +1,45 @@
<h1> <div class="head">
<%= @page_title %> <h1>
</h1> <%= @page_title %>
<p> </h1>
Inlägg skrivna sedan 2006.
<%= if @nosearch? do %>
Gå direkt till:
<% end %>
</p>
<ul class="months">
<%= for {year, articles} <- @articles do %>
<li>
<a href={"#y" <> year}><%= year %></a> (<%= Enum.count(articles) %>)
</li>
<% end %>
</ul>
<form method="get" action="/webblogg">
<p> <p>
Inlägg skrivna sedan 2006.
<%= if @nosearch? do %> <%= if @nosearch? do %>
Eller Gå direkt till:
<% end %> <% end %>
<label for="q">sök innehåll</label>:
<input type="search" value={@q} name="q" id="q" size="7" />
<button>Sök</button>
</p> </p>
</form> <ul class="months">
<%= for {year, articles} <- @articles do %> <%= for {year, articles} <- @articles do %>
<section id={"y" <> year}> <li>
<h2 class="sticky"><%= year %></h2> <a href={"#y" <> year}><%= year %></a> (<%= Enum.count(articles) %>)
<div class="articles"> </li>
<%= for article <- articles do %> <% end %>
<article class="article" vocab="https://schema.org/" typeof="Article"> </ul>
<h2 property="name"> <form method="get" action="/webblogg">
<a href={"/" <> article["slug"]}><%= article["title"] %></a> <p>
</h2> <%= if @nosearch? do %>
<time><%= article["pubDate"] %></time> Eller
</article>
<% end %> <% end %>
</div> <label for="q">sök innehåll</label>:
</section> <input type="search" value={@q} name="q" id="q" size="7" />
<% end %> <button>Sök</button>
</p>
</form>
</div>
<div class="cards">
<%= for {year, articles} <- @articles do %>
<section class="card" id={"y" <> year}>
<h2 class="sticky"><%= year %></h2>
<div class="articles">
<%= for article <- articles do %>
<article class="article" vocab="https://schema.org/" typeof="Article">
<h2 property="name">
<a href={"/" <> article["slug"]}><%= article["title"] %></a>
</h2>
<time><%= article["pubDate"] %></time>
</article>
<% end %>
</div>
</section>
<% end %>
</div>

View file

@ -0,0 +1,74 @@
<div class="head">
<h1>Anteckningar</h1>
<p>Blandade tankar, oftast på engelska. Replikerat på Mastodon.</p>
</div>
<div class="cards">
<%= for {date, links} <- @notes do %>
<section class="card" id={"d" <> date}>
<h2>
<%= date
|> Date.from_iso8601!()
|> Calendar.strftime(
"%A, %d %B %Y",
month_names: fn m ->
Enum.at(
[
"januari",
"februari",
"mars",
"april",
"maj",
"juni",
"juli",
"augusti",
"september",
"oktober",
"november",
"december"
],
m - 1
)
end,
day_of_week_names: fn d ->
Enum.at(
[
"måndag",
"tisdag",
"onsdag",
"torsdag",
"fredag",
"lördag",
"söndag"
],
d - 1
)
end
)
|> String.replace(~r/ 0/, " ") %>
</h2>
<div class="bookmarks">
<%= for link <- links do %>
<article vocab="https://schema.org/" typeof="WebContent Review" class="bookmark">
<h3>
<div property="reviewBody">
<%= link["contents"] |> Earmark.as_html!() |> raw %>
</div>
</h3>
<footer>
<p>
Posted on
<a class="permalink" href={"/notes/" <> to_string(link["id"])} title="Permalänk">
<%= fancy_timestamp(link["date_created"]) %>
</a>
at
<a href="https://www.openstreetmap.org/#map=15/50.82806/-0.12861">
pos
</a>
</p>
</footer>
</article>
<% end %>
</div>
</section>
<% end %>
</div>

View file

@ -31,6 +31,7 @@ defmodule Mse25Web.Router do
get "/evenemang", PageController, :events get "/evenemang", PageController, :events
get "/webblogg", PageController, :articles get "/webblogg", PageController, :articles
get "/delningar", PageController, :links get "/delningar", PageController, :links
get "/anteckningar", PageController, :notes
get "/sok", PageController, :search get "/sok", PageController, :search
get "/prenumerera.xml", FeedController, :feed get "/prenumerera.xml", FeedController, :feed