WIP: Add content adapter and basic user profile layout

This commit is contained in:
a 2024-11-23 19:09:04 -06:00
parent 08fc095ddd
commit b99f7d2c2c
10 changed files with 572 additions and 0 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
.hugo_build.lock
assets/mastodon-archive/*.json
assets/mastodon-archive/avatar.*
assets/mastodon-archive/header.*

1
README.md Normal file
View file

@ -0,0 +1 @@
# hugo-content-adapter-mastodon

View file

@ -0,0 +1,166 @@
{{ with resources.GetMatch "mastodon-archive/avatar.*" }}
{{ $avatar_content := dict "mediaType" .MediaType.Type "value" .Content }}
{{ $avatar_resource := dict
"path" (printf "avatar.%s" (index .MediaType.Suffixes 0))
"content" $avatar_content
}}
{{ $.AddResource $avatar_resource }}
{{ end }}
{{ with resources.GetMatch "mastodon-archive/header.*" }}
{{ $header_content := dict "mediaType" .MediaType.Type "value" .Content }}
{{ $header_resource := dict
"path" (printf "header.%s" (index .MediaType.Suffixes 0))
"content" $header_content
}}
{{ $.AddResource $header_resource }}
{{ end }}
{{ with unmarshal (resources.Get "mastodon-archive/actor.json") }}
{{ $account_page := dict
"path" "."
"kind" "section"
"title" .name
"summary" (.summary | plainify)
"content" (dict
"mediaType" "text/html"
"value" ""
)
"type" "mastodon"
"layout" "user"
"params" (dict
"mastodon" (dict
"_model" "Account"
"username" .preferredUsername
"created_at" (.published | time)
"note" .summary
"display_name" .name
)
)
}}
{{ $.AddPage $account_page }}
{{ end }}
{{ with unmarshal (resources.Get "mastodon-archive/outbox.json") }}
{{ $total := .totalItems }}
{{ $activities := last 1000 .orderedItems }}
{{ range $activities }}
{{ $published := .published | time }}
{{/* Extract database ID to use as a path later */}}
{{ $status_id :=
.id
| strings.TrimPrefix .actor
| strings.TrimPrefix "/statuses/"
| strings.TrimSuffix "/activity"
}}
{{/* Calculate Mastodon-style visibility scope */}}
{{ $followers := printf "%s/followers" .actor }}
{{ $visibility := "direct" }}
{{ if or
(in .to "https://www.w3.org/ns/activitystreams#Public")
(in .to "as:Public")
(in .to "Public")
}}
{{ $visibility = "public" }}
{{ else if or
(in .cc "https://www.w3.org/ns/activitystreams#Public")
(in .cc "as:Public")
(in .cc "Public")
}}
{{ $visibility = "unlisted" }}
{{ else if or
(in .to $followers)
(in .cc $followers)
}}
{{ $visibility = "private" }}
{{ end }}
{{/* Convert Create activities into a status */}}
{{ if eq .type "Create" }}
{{/* Initialize Hugo Page and add it */}}
{{ $page := dict
"path" $status_id
"content" (dict
"mediaType" "text/html"
"value" .object.content
)
"dates" (dict
"date" $published
)
"type" "mastodon"
"layout" "status"
"params" (dict
"mastodon" (dict
"_model" "Status"
"_post_type" "original"
"id" $status_id
"visibility" $visibility
"spoiler_text" .object.summary
)
)
}}
{{ $.AddPage $page }}
{{ else if eq .type "Announce" }}
{{ $page := dict
"path" $status_id
"dates" (dict
"date" $published
)
"type" "mastodon"
"layout" "status"
"params" (dict
"mastodon" (dict
"_model" "Status"
"_post_type" "reblog"
"_reblog_of_uri" .object
)
)
}}
{{ $.AddPage $page }}
{{ else }}
{{ $page := dict
"path" $status_id
"dates" (dict
"date" $published
)
"params" (dict
"mastodon" (dict
"_model" ""
"_post_type" "unknown"
)
)
}}
{{ $.AddPage $page }}
{{ end }}
{{/* Add the activity as a resource */}}
{{ $resource := dict
"path" (printf "%s/activity" $status_id)
"content" (dict
"mediaType" "application/json"
"value" (jsonify .)
)
}}
{{ $.AddResource $resource }}
{{ end }}
{{ end }}

View file

@ -0,0 +1,72 @@
{{ $actor_data := dict }}
{{ $actor_url := "https://mastodon.social/users/trwnh.json" }}
{{ with resources.GetRemote $actor_url }}
{{ with .Err }}
{{ errorf "Unable to get remote resource %s: %s" $actor_url . }}
{{ else }}
{{ $actor_data = . | unmarshal }}
{{ end }}
{{ else }}
{{ errorf "Unable to get remote resource %s" $actor_url }}
{{ end }}
{{ with $actor_data }}
{{ $username := .preferredUsername }}
{{/* Fetch and save avatar */}}
{{ $avatar_url := .icon.url }}
{{ with resources.GetRemote $avatar_url }}
{{ with .Err }}
{{ warnf "Unable to get avatar from %s with error: %s" $avatar_url . }}
{{ else }}
{{ $avatar_content := dict "mediaType" .MediaType.Type "value" .Content }}
{{ $avatar_resource := dict
"path" (printf "avatar.%s" (index .MediaType.Suffixes 0))
"content" $avatar_content
}}
{{ $.AddResource $avatar_resource }}
{{ end }}
{{ end }}
{{/* Fetch and save header */}}
{{ $header_url := .image.url }}
{{ with resources.GetRemote $header_url }}
{{ with .Err }}
{{ warnf "Unable to get header from %s with error: %s" $avatar_url . }}
{{ else }}
{{ $header_content := dict "mediaType" .MediaType.Type "value" .Content }}
{{ $header_resource := dict
"path" (printf "header.%s" (index .MediaType.Suffixes 0))
"content" $header_content
}}
{{ $.AddResource $header_resource }}
{{ end }}
{{ end }}
{{/* Add account page */}}
{{ $account_page := dict
"path" "."
"kind" "section"
"title" .name
"summary" (.summary | plainify)
"content" (dict
"mediaType" "text/html"
"value" ""
)
"type" "mastodon"
"layout" "user"
"params" (dict
"mastodon" (dict
"_model" "Account"
"username" .preferredUsername
"created_at" (.published | time)
"note" .summary
"display_name" .name
)
)
}}
{{ $.AddPage $account_page }}
{{ end }}

7
hugo.toml Normal file
View file

@ -0,0 +1,7 @@
baseURL = 'https://example.org/'
languageCode = 'en-us'
title = 'My New Hugo Site'
[caches.getresource]
dir = ':resourceDir/_cache/:project'
maxage = -1

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}}</title>
{{ block "head" . }}
{{ end }}
</head>
{{ block "body" . }}
<body>
</body>
{{ end }}
</html>

View file

@ -0,0 +1,38 @@
{{ define "head" }}
<style>
</style>
{{ end }}
{{ define "body" }}
<body>
<main>
<h1>Mastodon/Status view</h1>
<h2>Status</h2>
{{ with .Params.mastodon.spoiler_text }}
<p>{{.}}</p>
{{ end }}
{{ with .Content }}
<div class="status__content">{{.}}</div>
{{ end }}
<h2>Status params</h2>
<dl>
{{ range $k, $v := .Params.mastodon }}
<dt>{{$k}}</dt>
<dd>{{$v}}</dd>
{{ end }}
</dl>
<h2>Activity</h2>
{{ with .Resources.Get "activity" }}
<pre>{{.Content | unmarshal | jsonify (dict "indent" " ") }}</pre>
{{ end }}
</main>
</body>
{{ end }}

View file

@ -0,0 +1,28 @@
{{ define "head" }}
<style>
</style>
{{ end }}
{{ define "body" }}
<body>
<main>
<h1>Mastodon/Statuses view</h1>
<ul>
{{ range where .RegularPages.ByDate "Params.mastodon.visibility" "public" }}
<li>
{{ with .Params.mastodon._reblog_of_uri }}
<div>Reblogged {{ . }}</div>
{{ else }}
{{ with .Params.mastodon.spoiler_text }}
<p style="border-inline: thick solid gold; background: #ddd">{{.}}</p>
{{ end }}
<div>{{.Content}}</div>
{{ end }}
<a href="{{.Permalink}}">{{.Permalink}}</a>
</li>
{{ end }}
</ul>
</main>
</body>
{{ end }}

226
layouts/mastodon/user.html Normal file
View file

@ -0,0 +1,226 @@
{{ define "head" }}
<style>
:root {
--color-primary-bg: #181821;
--color-primary-text: white;
--color-primary-text-faded: rgb(156, 156, 201);
--color-primary-accent: rgb(140, 141, 255);
--color-primary-link: var(--color-primary-accent);
--color-primary-link-visited: var(--color-primary-link);
--color-secondary-bg: #20202c;
--color-outline: rgb(49, 49, 68);
--radius: 10px;
--size-avatar: 100px;
--size-padding: calc(var(--spacing-1u, 10px) * 2);
--spacing-1u: 10px;
line-height: 1.6;
}
a {
word-break: break-all;
}
a:link {
color: var(--color-primary-link);
}
a:visited {
color: var(--color-primary-link-visited);
}
body {
margin: 0;
padding: 0;
background-color: var(--color-secondary-bg);
}
@media (min-width: 610px) {
main {
padding-block: var(--spacing-1u, 10px);
}
}
.main-title {
display: none;
}
.account, .statuses {
max-width: 600px;
margin-inline: auto;
background-color: var(--color-primary-bg);
color: var(--color-primary-text);
}
.account {
}
.account-prepend {
background-color: var(--color-primary-bg);
color: rgb(140, 141, 255);
margin: 0;
padding: 1rem;
padding-inline: var(--size-padding, 20px);
border-color: var(--color-outline);
border-style: solid;
border-width: 1px;
font-size: 1rem;
}
.account-images {
position: relative;
}
.account-header {
display: block;
width: 100%;
height: 150px;
object-fit: cover;
background-color: var(--color-primary-accent);
}
.account-avatar {
display: block;
width: var(--size-avatar, 100px);
height: var(--size-avatar, 100px);
background-color: var(--color-primary-bg);
border-color: var(--color-outline);
border-style: solid;
border-width: 1px;
border-radius: var(--radius, 10px);
position: absolute;
inset-inline-start: var(--size-padding, 20px);
inset-block-end: calc(var(--size-avatar, 100px) / 2 * -1);
}
.account-info {
padding-inline: var(--size-padding, 20px);
padding-block-start: calc(var(--size-avatar, 100px) / 2);
padding-block-end: var(--size-padding, 20px)
}
.account-display_name {
font-weight: bolder;
font-size: 1.5rem;
line-height: 2;
margin: 0;
}
.account-bio {
}
.account-bio *:first-child {
margin-block-start: 0;
}
.account-bio *:last-child {
margin-block-end: 0;
}
.account-created_at {
margin: 0;
margin-block-start: var(--size-padding, 20px);
color: var(--color-primary-text-faded);
}
.statuses {
}
.statuses-prepend {
margin: 0;
padding: 1rem;
padding-inline: var(--size-padding, 20px);
font-size: 1rem;
color: rgb(140, 141, 255);
border-color: var(--color-outline);
border-style: solid;
border-width: 1px;
border-inline: 0;
}
.statuses-empty-hint {
margin: 0;
padding: var(--size-padding, 20px);
}
.statuses > ul, .statuses > ul > li {
display: contents;
}
.status {
border-color: var(--color-outline);
border-style: solid;
border-width: 1px;
border-inline: 0;
border-block-start: 0;
padding: var(--size-padding, 20px);
box-sizing: border-box;
}
.statuses > ul > li:last-child .status {
border-block-end: 0;
}
.status-header {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: 1fr 1fr;
column-gap: var(--spacing-1u, 10px);
place-items: center start;
}
.status-avatar {
grid-column: 1;
grid-row: 1 / span 2;
width: 50px;
height: 50px;
}
.status-author {
margin: 0;
grid-column: 2;
grid-row: 1;
line-height: 25px;
}
.status-meta {
margin: 0;
grid-column: 2;
grid-row: 2;
line-height: 25px;
}
.status-prepend {
}
.status-cw {
background-color: hsl(50,100%,50%,10%);
padding: 0.5rem 1rem;
border-inline: thick solid goldenrod;
}
.status-body {
}
@media (min-width: 480px) {
.status-cw, .status-body {
margin-left: 60px;
}
}
</style>
{{ end }}
{{ define "body" }}
<body>
<main class="user-profile h-feed">
<h1 class="main-title">Mastodon/User view</h1>
<section class="account h-card">
<h2 class="account-prepend">Account</h2>
<div class="account-images">
{{ $header := .Resources.GetMatch "header.*" }}
{{ with $header }}
<img class="account-header" src="{{.Permalink}}" alt="" width="320">
{{ else }}
<div class="account-header"></div>
{{ end }}
{{ $avatar := .Resources.GetMatch "avatar.*" }}
{{ with $avatar }}
<img class="account-avatar u-logo u-photo" src="{{.Permalink}}" alt="" width="120" height="120">
{{ end }}
</div>
<div class="account-info">
<p class="account-display_name p-name">{{ .Params.mastodon.display_name }}</p>
<div class="account-bio e-summary">{{ .Params.mastodon.note | safeHTML }}</div>
<p class="account-created_at">Created <time datetime="{{.Params.mastodon.created_at.Format "2006-01-02"}}" class="dt-published">{{.Params.mastodon.created_at.Format "January 2, 2006"}}</time></p>
</div>
</section>
<section class="statuses">
<h2 class="statuses-prepend">Statuses</h2>
{{ with where .RegularPages "Params.mastodon.visibility" "public" }}
<ul>
{{ range . }}
<li>
{{ partial "mastodon/status.html" (dict "status_ctx" . "account_ctx" $) }}
</li>
{{ end }}
</ul>
{{ else }}
<p class="statuses-empty-hint">No statuses</p>
{{ end }}
</section>
</main>
</body>
{{ end }}

View file

@ -0,0 +1,18 @@
{{ $status := .status_ctx }}
{{ $account := .account_ctx }}
{{ $avatar := $account.Resources.GetMatch "avatar.*" }}
<article class="status h-entry">
<header class="status-header h-card">
<img src="{{ $avatar.Permalink }}" alt="" width="50" height="50" class="status-avatar u-logo u-photo">
<p class="status-author p-name">{{ $account.Params.mastodon.display_name }}</p>
<p class="status-meta"><a href="{{$status.Permalink}}" class="status-permalink"><time datetime="{{ $status.Date.UTC.Format "2006-01-02T15:04:05Z" }}" class="status-created_at">{{ $status.Date.Format "Jan 02, 2006 - 3:04 PM MST"}}</time></a></p>
</header>
<section class="status-prepend">
{{ with $status.Params.mastodon.spoiler_text }}
<p class="status-cw">{{ . }}</p>
{{ end }}
</section>
<section class="status-body">
{{ $status.Content }}
</section>
</article>