Basic OpenGraph support (#267)
Creates an OpenGraph template include in base.html including the basic tags expected on all pages. Then allows any page to add additional expected tags via `context`. Currently, profiles and posts are enriched to show complete opengraph metadata, and render correctly in Discord. Note: This does not show posts in Slack like Twitter/Mastodon do. I believe this is due to Slack preferring oembed when present, which is a mastodon API endpoint we may need to create at some point.
This commit is contained in:
parent
dab8dd59a7
commit
b53504fe64
|
@ -838,6 +838,22 @@ class Post(StatorModel):
|
||||||
raise ValueError("Actor on delete does not match object")
|
raise ValueError("Actor on delete does not match object")
|
||||||
post.delete()
|
post.delete()
|
||||||
|
|
||||||
|
### OpenGraph API ###
|
||||||
|
|
||||||
|
def to_opengraph_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
"og:title": f"{self.author.name} (@{self.author.handle})",
|
||||||
|
"og:type": "article",
|
||||||
|
"og:published_time": (self.published or self.created).isoformat(),
|
||||||
|
"og:modified_time": (
|
||||||
|
self.edited or self.published or self.created
|
||||||
|
).isoformat(),
|
||||||
|
"og:description": (self.summary or self.safe_content_local()),
|
||||||
|
"og:image:url": self.author.local_icon_url().absolute,
|
||||||
|
"og:image:height": 85,
|
||||||
|
"og:image:width": 85,
|
||||||
|
}
|
||||||
|
|
||||||
### Mastodon API ###
|
### Mastodon API ###
|
||||||
|
|
||||||
def to_mastodon_json(self, interactions=None):
|
def to_mastodon_json(self, interactions=None):
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def dict_merge(base: dict, defaults: dict):
|
||||||
|
"""
|
||||||
|
Merges two input dictionaries, returning the merged result.
|
||||||
|
|
||||||
|
`input|dict_merge:defaults`
|
||||||
|
|
||||||
|
The defaults are overridden by any key present in the `input` dict.
|
||||||
|
"""
|
||||||
|
if not (isinstance(base, dict) or isinstance(defaults, dict)):
|
||||||
|
raise ValueError("Filter inputs must be dictionaries")
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
result.update(defaults)
|
||||||
|
result.update(base)
|
||||||
|
|
||||||
|
return result
|
|
@ -39,21 +39,28 @@ class Individual(TemplateView):
|
||||||
# Show normal page
|
# Show normal page
|
||||||
return super().get(request)
|
return super().get(request)
|
||||||
|
|
||||||
def get_context_data(self):
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
ancestors, descendants = PostService(self.post_obj).context(
|
ancestors, descendants = PostService(self.post_obj).context(
|
||||||
self.request.identity
|
self.request.identity
|
||||||
)
|
)
|
||||||
return {
|
|
||||||
"identity": self.identity,
|
context.update(
|
||||||
"post": self.post_obj,
|
{
|
||||||
"interactions": PostInteraction.get_post_interactions(
|
"identity": self.identity,
|
||||||
[self.post_obj] + ancestors + descendants,
|
"post": self.post_obj,
|
||||||
self.request.identity,
|
"interactions": PostInteraction.get_post_interactions(
|
||||||
),
|
[self.post_obj] + ancestors + descendants,
|
||||||
"link_original": True,
|
self.request.identity,
|
||||||
"ancestors": ancestors,
|
),
|
||||||
"descendants": descendants,
|
"link_original": True,
|
||||||
}
|
"ancestors": ancestors,
|
||||||
|
"descendants": descendants,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
def serve_object(self):
|
def serve_object(self):
|
||||||
# If this not a local post, redirect to its canonical URI
|
# If this not a local post, redirect to its canonical URI
|
||||||
|
|
|
@ -8,4 +8,10 @@ def config_context(request):
|
||||||
request.identity.config_identity if request.identity else None
|
request.identity.config_identity if request.identity else None
|
||||||
),
|
),
|
||||||
"top_section": request.path.strip("/").split("/")[0],
|
"top_section": request.path.strip("/").split("/")[0],
|
||||||
|
"opengraph_defaults": {
|
||||||
|
"og:site_name": Config.system.site_name,
|
||||||
|
"og:type": "website",
|
||||||
|
"og:title": Config.system.site_name,
|
||||||
|
"og:url": request.build_absolute_uri(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
{% load opengraph %}
|
||||||
|
{% with opengraph_merged=opengraph_local|dict_merge:opengraph_defaults %}
|
||||||
|
<!-- Begin OpenGraph tagging -->
|
||||||
|
{% for key, value in opengraph_merged.items %}
|
||||||
|
<meta content="{{ value|striptags }}" property="{{ key }}"/>
|
||||||
|
{% if key == "og:description" %}
|
||||||
|
{# Mastodon duplicates this one tag without the og: prefix. Not sure why #}
|
||||||
|
<meta content="{{ value|striptags }}" property="description"/>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% block opengraph_extra %}
|
||||||
|
{% endblock %}
|
||||||
|
<!-- End OpenGraph tagging -->
|
||||||
|
{% endwith %}
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
{% block title %}Post by {{ post.author.html_name_or_handle }}{% endblock %}
|
{% block title %}Post by {{ post.author.html_name_or_handle }}{% endblock %}
|
||||||
|
|
||||||
|
{% block opengraph %}
|
||||||
|
{% include "_opengraph.html" with opengraph_local=post.to_opengraph_dict %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% for ancestor in ancestors reversed %}
|
{% for ancestor in ancestors reversed %}
|
||||||
{% include "activities/_post.html" with post=ancestor reply=True link_original=False %}
|
{% include "activities/_post.html" with post=ancestor reply=True link_original=False %}
|
||||||
|
|
|
@ -26,6 +26,9 @@
|
||||||
{% if config_identity.custom_css %}
|
{% if config_identity.custom_css %}
|
||||||
<style>{{ config_identity.custom_css }}</style>
|
<style>{{ config_identity.custom_css }}</style>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% block opengraph %}
|
||||||
|
{% include "_opengraph.html" with opengraph_local=opengraph_defaults %}
|
||||||
|
{% endblock %}
|
||||||
{% block extra_head %}{% endblock %}
|
{% block extra_head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="{% block body_class %}{% endblock %}" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
<body class="{% block body_class %}{% endblock %}" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
{% block title %}{{ identity }}{% endblock %}
|
{% block title %}{{ identity }}{% endblock %}
|
||||||
|
|
||||||
|
{% block opengraph %}
|
||||||
|
{% include "_opengraph.html" with opengraph_local=identity.to_opengraph_dict %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
{% if identity.local %}
|
{% if identity.local %}
|
||||||
<link rel="alternate" type="application/rss+xml" title="RSS feed for {{ identity.name }}" href="rss/" />
|
<link rel="alternate" type="application/rss+xml" title="RSS feed for {{ identity.name }}" href="rss/" />
|
||||||
|
|
|
@ -735,6 +735,19 @@ class Identity(StatorModel):
|
||||||
await sync_to_async(self.save)()
|
await sync_to_async(self.save)()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
### OpenGraph API ###
|
||||||
|
|
||||||
|
def to_opengraph_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
"og:title": f"{self.name} (@{self.handle})",
|
||||||
|
"og:type": "profile",
|
||||||
|
"og:description": self.summary,
|
||||||
|
"og:profile:username": self.handle,
|
||||||
|
"og:image:url": self.local_icon_url().absolute,
|
||||||
|
"og:image:height": 85,
|
||||||
|
"og:image:width": 85,
|
||||||
|
}
|
||||||
|
|
||||||
### Mastodon Client API ###
|
### Mastodon Client API ###
|
||||||
|
|
||||||
def to_mastodon_json(self):
|
def to_mastodon_json(self):
|
||||||
|
|
Loading…
Reference in New Issue