Skip to content

Commit d2947eb

Browse files
committed
Schema.org metadata as JSON-LD
1 parent 2918486 commit d2947eb

11 files changed

Lines changed: 117 additions & 32 deletions

File tree

inc/foot.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
- [RSS](@root/feed.xml)
2-
- [ORCiD](https://orcid.org/0000-0003-3230-6090/)
3-
- [zirk.us](https://zirk.us/@joemull)
4-
- [LinkedIn](https://www.linkedin.com/in/jhmuller/)
2+
- [Bluesky](https://bsky.app/profile/joemull.zirk.us.ap.brid.gy)
3+
- [Mastodon](https://zirk.us/@joemull)
54
- [GitHub](https://github.com/joemull/)
5+
- [ORCiD](https://orcid.org/0000-0003-3230-6090/)

lib/meteor/extensions/jinja_config.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,13 @@
55
from minify_html import minify
66
from tablerpy import OutlineIcon, get_icon
77

8-
# SVG namespace
9-
ElementTree.register_namespace("", "http://www.w3.org/2000/svg")
10-
118

129
# Jinja filters
1310
def icon(icon_name: str, a11y_title: str) -> str:
1411
"""
1512
A Jinja filter for inserting an SVG Tabler icon.
1613
"""
14+
ElementTree.register_namespace("", "http://www.w3.org/2000/svg")
1715
tablerpy_name = icon_name.replace("-", "_").upper()
1816
try:
1917
icon_path = str(get_icon(getattr(OutlineIcon, tablerpy_name)))

lib/meteor/extensions/rdf.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import json
2+
import os
3+
4+
import ark
5+
from rdflib import Graph, URIRef, Literal
6+
from rdflib.namespace import RDF, SDO
7+
8+
9+
SITE_GRAPH = Graph()
10+
11+
12+
def add_via_site_graph(g: Graph, page_data: dict, data: str, a: URIRef) -> None:
13+
if not SITE_GRAPH.value(predicate=RDF.type, object=a, any=True):
14+
SITE_GRAPH.parse(data=data, format="turtle")
15+
for s, p, o in SITE_GRAPH:
16+
g.add((s, p, o))
17+
18+
19+
def add_webpage_to_graph(g: Graph, page_data: dict) -> None:
20+
node = page_data["node"]
21+
site = g.value(predicate=RDF.type, object=SDO.WebSite, any=False)
22+
path = ark.site.out("")
23+
node_path = ark.utils.rewrite_urls(f"'{node.url}'", path).strip("'")
24+
page = URIRef(os.path.join(str(site), node_path))
25+
g.add((page, RDF.type, SDO.WebPage))
26+
if node_path.startswith("blog"):
27+
g.add((page, RDF.type, SDO.BlogPosting))
28+
g.add((page, SDO.isPartOf, site))
29+
author = g.value(subject=site, predicate=SDO.author, any=False)
30+
g.add((page, SDO.author, author))
31+
g.add((page, SDO.copyrightHolder, author))
32+
if node.get("title"):
33+
g.add((page, SDO.headline, Literal(node.get("title"))))
34+
g.add((page, SDO.license, Literal(ark.site.config.get("license", ""))))
35+
g.add((page, SDO.inLanguage, Literal("en")))
36+
if node.get("word_count"):
37+
g.add((page, SDO.wordCount, Literal(node.get("word_count", 0))))
38+
url = Literal(os.path.join(str(site), node_path))
39+
g.add((page, SDO.url, url))
40+
if node.get("date"):
41+
g.add((page, SDO.dateCreated, Literal(node.get("date"))))
42+
43+
44+
# JSON-LD generation event
45+
@ark.events.register(ark.events.Event.RENDER_PAGE)
46+
def create_json_ld(page_data: dict) -> str:
47+
g = Graph()
48+
add_via_site_graph(g, page_data, data=ark.site.config["site_ttl"], a=SDO.WebSite)
49+
add_via_site_graph(g, page_data, data=ark.site.config["author_ttl"], a=SDO.Person)
50+
add_webpage_to_graph(g, page_data)
51+
json_ld_g = g.serialize(
52+
format="json-ld",
53+
context={"schema": SDO._NS},
54+
auto_compact=True,
55+
)
56+
json_ld_g = json.dumps(json.loads(json_ld_g), separators=(",", ":"))
57+
page_data["json_ld"] = json_ld_g
58+
return page_data

lib/meteor/extensions/renderer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def count_words(raw_text: str, meta_dict: dict[str, str]) -> str:
4040
"""
4141
Count the words in a node and add the count to the metadata.
4242
"""
43-
meta_dict["word_count"] = "{:,}".format(len(raw_text.split(" ")))
43+
meta_dict["word_count"] = len(raw_text.split(" "))
4444
return raw_text
4545

4646

lib/meteor/extensions/rss.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,7 @@ def generate_feed():
5252
description = filters.do_striptags(
5353
ark.site.includes().get("site_description", ""),
5454
)
55-
homepage = config.get("homepage", "")
56-
if not homepage:
57-
raise Exception(
58-
"There must be a homepage specified in config.py to generate the RSS feed."
59-
)
55+
homepage = config["homepage"]
6056
rss_link = os.path.join(homepage, relative_link)
6157
feed = Atom1Feed(
6258
title=title,
@@ -67,7 +63,7 @@ def generate_feed():
6763
nodes = get_nodes(feed)
6864
path = ark.site.out(relative_link)
6965
for node in nodes:
70-
author = node.get("author") or config.get("default_author", "")
66+
author = node.get("author") or config.get("author", "")
7167
node_path = ark.utils.rewrite_urls(f"'{node.url}'", path).strip("'")
7268
url = os.path.join(homepage, node_path)
7369
feed.add_item(
Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
{% from "components/vt_name.jinja" import vt_name %}
22

33
{% macro node_date_and_word_count(node) -%}
4-
{% if node.date or node.word_count and not node.is_dir_index and not node.is_homepage_index and not node.title == "About" %}
5-
<div class="node-date-and-word-count">
6-
{% if node.date %}
7-
<time
8-
style="view-transition-name: {{ vt_name(node, "date") }};"
9-
datetime="{{ node.date }}"
10-
>
11-
{{ node.date.strftime('%d %B %Y') }}
12-
</time>
13-
{% endif %}
14-
{% if node.word_count %}
15-
<span class="word-count">{{ node.word_count }} words</span>
16-
{% endif %}
17-
</div>
4+
{% if node.date or node.word_count %}
5+
{% if not node.is_dir_index and not node.is_homepage_index and not node.title == "About" %}
6+
<div class="node-date-and-word-count">
7+
{% if node.date %}
8+
<time
9+
style="view-transition-name: {{ vt_name(node, "date") }};"
10+
datetime="{{ node.date }}"
11+
>
12+
{{ node.date.strftime("%d %B %Y") }}
13+
</time>
14+
{% endif %}
15+
{% if node.word_count %}
16+
<span class="word-count">
17+
{{ "{:,}".format(node.word_count) }} words
18+
</span>
19+
{% endif %}
20+
</div>
21+
{% endif %}
1822
{% endif %}
1923
{%- endmacro %}

lib/meteor/templates/node.jinja

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
<link rel="preload" as="style" href="@root/utilities.min.css" />
4949
<link rel="stylesheet" href="@root/utilities.min.css" />
5050
<link rel="icon" href="@root/images/favicon.svg" />
51-
<meta name="author" content="{{ node.author or site.default_author }}" />
51+
<meta name="author" content="{{ node.author or site.author }}" />
5252
{% if inc.site_description %}
5353
<meta name="description" content="{{ inc.site_description|striptags }}" />
5454
{% else %}
@@ -57,6 +57,11 @@
5757
content="{{ node.html|striptags|truncate(100) }}"
5858
/>
5959
{% endif %}
60+
<!-- prettier-ignore-start -->
61+
<script type="application/ld+json">
62+
{{- json_ld -}}
63+
</script>
64+
<!-- prettier-ignore-end -->
6065
{{ inc.head }}
6166
</head>
6267
<body>

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies = [
1515
"minify-html",
1616
"pytest",
1717
"tablerpy",
18+
"rdflib[json]",
1819
"ruff",
1920
]
2021

requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,14 @@ pygments==2.19.2
3939
# ark
4040
# pytest
4141
# syntext
42+
pyparsing==3.2.5
43+
# via rdflib
4244
pytest==8.4.2
4345
# via joemull (pyproject.toml)
4446
pyyaml==6.0.3
4547
# via ark
48+
rdflib==7.4.0
49+
# via joemull (pyproject.toml)
4650
ruff==0.14.2
4751
# via joemull (pyproject.toml)
4852
shortcodes==5.4.0

site.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,27 @@
1717
# Site tagline.
1818
# tagline = ""
1919

20-
# Default author
21-
default_author = "Joe Muller"
20+
# Linked data for JSON-LD
21+
site_ttl = """
22+
@prefix sdo: <https://schema.org/>.
23+
<https://joemull.net> a sdo:WebSite,
24+
sdo:Blog;
25+
sdo:url "https://joemull.net";
26+
sdo:name "Joe Muller";
27+
sdo:author <https://orcid.org/0000-0003-3230-6090>;
28+
sdo:copyrightHolder <https://orcid.org/0000-0003-3230-6090>.
29+
"""
30+
author_ttl = """
31+
@prefix sdo: <https://schema.org/>.
32+
<https://orcid.org/0000-0003-3230-6090> a sdo:Person;
33+
sdo:sameAs "https://bsky.app/profile/joemull.zirk.us.ap.brid.gy",
34+
"https://bsky.app/profile/joemull.bsky.social",
35+
"https://zirk.us/@joemull",
36+
"https://github.com/joemull".
37+
"""
38+
39+
# License
40+
license = "https://creativecommons.org/licenses/by/4.0/"
2241

2342
# Domain for refernce links on PDF slidedecks
2443
if os.environ.get("DEBUG"):
@@ -27,7 +46,7 @@
2746
homepage = "https://joemull.net/"
2847

2948
# Language
30-
lang = "en-US"
49+
lang = "en"
3150

3251
# Extensions
3352
extensions = [

0 commit comments

Comments
 (0)