This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 819 KiB |
@@ -0,0 +1,62 @@
|
||||
(() => {
|
||||
const announce = (component, message) => {
|
||||
const status = component.querySelector("[data-share-status]");
|
||||
if (!status) return;
|
||||
status.textContent = "";
|
||||
window.requestAnimationFrame(() => {
|
||||
status.textContent = message;
|
||||
});
|
||||
};
|
||||
|
||||
const fallbackCopy = (url) => {
|
||||
const field = document.createElement("textarea");
|
||||
field.value = url;
|
||||
field.setAttribute("readonly", "");
|
||||
field.style.position = "fixed";
|
||||
field.style.opacity = "0";
|
||||
document.body.appendChild(field);
|
||||
field.select();
|
||||
const copied = document.execCommand("copy");
|
||||
field.remove();
|
||||
return copied;
|
||||
};
|
||||
|
||||
const copyLink = async (component) => {
|
||||
const url = component.dataset.shareUrl;
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(url);
|
||||
} else if (!fallbackCopy(url)) {
|
||||
throw new Error("Copy command unavailable");
|
||||
}
|
||||
announce(component, "Link copied.");
|
||||
} catch (_error) {
|
||||
window.prompt("Copy this link:", url);
|
||||
announce(component, "Copy the link from the dialog.");
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelectorAll("[data-share-component]").forEach((component) => {
|
||||
component.querySelector("[data-share-native]")?.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
if (!navigator.share) {
|
||||
await copyLink(component);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.share({
|
||||
title: component.dataset.shareTitle,
|
||||
text: component.dataset.shareDescription,
|
||||
url: component.dataset.shareUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.name !== "AbortError") await copyLink(component);
|
||||
}
|
||||
});
|
||||
|
||||
component.querySelector("[data-share-copy]")?.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
await copyLink(component);
|
||||
});
|
||||
});
|
||||
})();
|
||||
+49
-2
@@ -432,7 +432,8 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
|
||||
grid-template-columns: minmax(0, 640px) minmax(230px, 280px);
|
||||
grid-template-areas:
|
||||
"header info"
|
||||
"body info";
|
||||
"body info"
|
||||
"share info";
|
||||
gap: 34px clamp(54px, 7vw, 92px);
|
||||
align-items: start;
|
||||
padding-block: 68px 90px;
|
||||
@@ -486,6 +487,45 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
|
||||
.publication-links a:hover,
|
||||
.publication-links a:focus-visible { text-decoration: underline; text-underline-offset: 3px; }
|
||||
|
||||
.share-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(150px, .55fr) minmax(0, 1.45fr);
|
||||
gap: 12px 28px;
|
||||
border-block: 1px solid var(--rule);
|
||||
padding-block: 22px;
|
||||
}
|
||||
.blog-post-layout > .share-panel,
|
||||
.article-post-layout > .share-panel { grid-area: share; }
|
||||
.share-panel-heading { align-self: center; }
|
||||
.share-kicker {
|
||||
margin: 0 0 5px;
|
||||
color: var(--magenta);
|
||||
font: 500 9px var(--mono);
|
||||
letter-spacing: .14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.share-panel h2 { margin: 0; font: 600 17px/1.3 var(--serif); }
|
||||
.share-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
||||
.share-action {
|
||||
border: 1px solid var(--rule);
|
||||
padding: 7px 9px;
|
||||
color: var(--muted);
|
||||
font: 500 9px var(--mono);
|
||||
letter-spacing: .07em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.share-action:hover,
|
||||
.share-action:focus-visible { border-color: var(--teal); color: var(--teal); outline-color: var(--teal); }
|
||||
.share-action-primary { border-color: rgba(81,182,165,.6); color: var(--teal); }
|
||||
.share-status {
|
||||
grid-column: 2;
|
||||
min-height: 1.3em;
|
||||
margin: 0;
|
||||
color: var(--teal);
|
||||
font: 500 9px var(--mono);
|
||||
letter-spacing: .06em;
|
||||
}
|
||||
|
||||
.narration-player {
|
||||
border: 1px solid var(--rule);
|
||||
padding: 18px 20px;
|
||||
@@ -548,6 +588,7 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
|
||||
padding-block: 68px 78px;
|
||||
}
|
||||
.project-narrative { width: 100%; }
|
||||
.project-content > .share-panel { grid-column: 1 / -1; }
|
||||
.repository-card { border: 1px solid var(--rule); background: var(--panel); padding: 24px; }
|
||||
.repository-card dl { margin: 0; }
|
||||
.repository-card a { color: var(--teal); }
|
||||
@@ -601,6 +642,7 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
|
||||
grid-template-areas:
|
||||
"header info"
|
||||
"body info"
|
||||
"share share"
|
||||
"navigation navigation"
|
||||
"back back";
|
||||
gap: 34px clamp(54px, 7vw, 92px);
|
||||
@@ -610,6 +652,7 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
|
||||
.devlog-entry-header { grid-area: header; }
|
||||
.devlog-entry-header h1 { font-style: normal; }
|
||||
.devlog-entry-body { grid-area: body; width: 100%; }
|
||||
.devlog-entry-layout > .share-panel { grid-area: share; }
|
||||
.devlog-info { grid-area: info; }
|
||||
.devlog-info .post-info-title { color: var(--violet); }
|
||||
.devlog-navigation { grid-area: navigation; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; margin-top: 38px; border: 1px solid var(--rule); background: var(--rule); }
|
||||
@@ -702,6 +745,7 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
|
||||
"header"
|
||||
"info"
|
||||
"body"
|
||||
"share"
|
||||
"navigation"
|
||||
"back";
|
||||
gap: 30px;
|
||||
@@ -716,11 +760,14 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
|
||||
grid-template-areas:
|
||||
"header"
|
||||
"info"
|
||||
"body";
|
||||
"body"
|
||||
"share";
|
||||
gap: 30px;
|
||||
padding-block: 44px 65px;
|
||||
}
|
||||
.post-info { padding: 20px; }
|
||||
.share-panel { grid-template-columns: 1fr; }
|
||||
.share-status { grid-column: 1; }
|
||||
.narration-player { padding: 18px; }
|
||||
.narration-details { align-items: flex-start; flex-direction: column-reverse; }
|
||||
.footer-inner { align-items: flex-start; flex-direction: column; padding-block: 28px; }
|
||||
|
||||
+4
-2
@@ -1,19 +1,21 @@
|
||||
<!doctype html>
|
||||
{% from "macros/social-sharing.html" import social_metadata with context %}
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="{{ this.summary|default('Labyricorn developer devlog and project hub')|striptags }}">
|
||||
<title>{% if this._path != '/' %}{{ this.title }} — {% endif %}Labyricorn</title>
|
||||
{% block social_head %}{{ social_metadata(this) }}{% endblock %}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500&family=Playfair+Display:ital,wght@0,600;0,700;1,500;1,600&display=swap" rel="stylesheet">
|
||||
<link rel="icon" href="{{ '/static/favicon.svg'|url }}" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="{{ '/static/style.css'|url }}?v=15">
|
||||
<link rel="stylesheet" href="{{ '/static/style.css'|url }}?v=16">
|
||||
<script src="{{ '/static/tags.js'|url }}?v=1" defer></script>
|
||||
<script src="{{ '/static/activity.js'|url }}?v=1" defer></script>
|
||||
<script src="{{ '/static/hero-circuit.js'|url }}?v=2" defer></script>
|
||||
<script src="{{ '/static/narration.js'|url }}?v=1" defer></script>
|
||||
<script src="{{ '/static/social-share.js'|url }}?v=1" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "macros/tag-links.html" import taxonomy_labels with context %}
|
||||
{% from "macros/social-sharing.html" import social_metadata, share_controls with context %}
|
||||
{% block social_head %}
|
||||
{% set social_project = this.parent.parent %}
|
||||
{{ social_metadata(this, 'TechArticle', 'article', social_project) }}
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
{% set project = this.parent.parent %}
|
||||
{% set siblings = this.get_siblings() %}
|
||||
@@ -28,6 +33,8 @@
|
||||
{{ this.body }}
|
||||
</div>
|
||||
|
||||
{{ share_controls(this, 'development note', project) }}
|
||||
|
||||
<nav class="devlog-navigation" aria-label="Development log chronology">
|
||||
{% if siblings.prev_page %}
|
||||
<a class="devlog-navigation-link" href="{{ siblings.prev_page|url }}"><span>← Older entry</span><strong>{{ siblings.prev_page.title }}</strong></a>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "macros/tag-links.html" import tag_links with context %}
|
||||
{% from "macros/narration-player.html" import narration_player with context %}
|
||||
{% from "macros/social-sharing.html" import social_metadata, share_controls with context %}
|
||||
{% block social_head %}
|
||||
{% if this.parent._id == 'blog' %}{{ social_metadata(this, 'BlogPosting', 'article') }}{% else %}{{ social_metadata(this, 'Article', 'article') }}{% endif %}
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
{% if this.parent._id == 'blog' %}
|
||||
<article class="blog-post-layout shell">
|
||||
@@ -37,6 +41,7 @@
|
||||
{{ this.body }}
|
||||
{% if this.external_url %}<p><a class="button-link" href="{{ this.external_url }}">Visit project →</a></p>{% endif %}
|
||||
</div>
|
||||
{{ share_controls(this, 'post') }}
|
||||
</article>
|
||||
{% elif this.parent._id == 'articles' %}
|
||||
{% set article_word_count = this.body|striptags|wordcount %}
|
||||
@@ -105,6 +110,7 @@
|
||||
{{ this.body }}
|
||||
{% if this.external_url %}<p><a class="button-link" href="{{ this.external_url }}">Visit project →</a></p>{% endif %}
|
||||
</div>
|
||||
{{ share_controls(this, 'article') }}
|
||||
</article>
|
||||
{% else %}
|
||||
<article>
|
||||
@@ -119,6 +125,7 @@
|
||||
<div class="shell prose">
|
||||
{{ this.body }}
|
||||
{% if this.external_url %}<p><a class="button-link" href="{{ this.external_url }}">Visit project →</a></p>{% endif %}
|
||||
{{ share_controls(this, 'page') }}
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
{% macro social_metadata(record, schema_type='WebPage', og_type='website', project=none) -%}
|
||||
{% set site_url = 'https://www.labyricorn.com' %}
|
||||
{% set canonical_url = site_url ~ record.url_path %}
|
||||
{% set default_image_url = site_url ~ '/static/labyricorn-social-card.png' %}
|
||||
{% set image_url = default_image_url %}
|
||||
{% if record.social_image %}
|
||||
{% if record.social_image.startswith('https://') %}
|
||||
{% set image_url = record.social_image %}
|
||||
{% elif record.social_image.startswith('/') %}
|
||||
{% set image_url = site_url ~ record.social_image %}
|
||||
{% else %}
|
||||
{% set image_url = site_url ~ record.url_path ~ record.social_image %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% set social_title = record.title|default('Labyricorn', true)|striptags|trim %}
|
||||
{% set base_description = record.summary|default('Labyricorn developer devlog and project hub', true)|striptags|trim %}
|
||||
{% set social_description = base_description|truncate(220, true, '…') %}
|
||||
{% if schema_type == 'TechArticle' and project %}
|
||||
{% set social_title = record.title ~ ' — ' ~ project.title ~ ' Devlog' %}
|
||||
{% set social_description = (project.title ~ ' devlog — ' ~ base_description)|truncate(220, true, '…') %}
|
||||
{% elif schema_type == 'SoftwareSourceCode' and record.status %}
|
||||
{% set social_description = (base_description ~ ' Status: ' ~ (record.status|capitalize) ~ '.')|truncate(220, true, '…') %}
|
||||
{% endif %}
|
||||
<meta name="description" content="{{ social_description }}">
|
||||
<link rel="canonical" href="{{ canonical_url }}">
|
||||
<meta property="og:title" content="{{ social_title }}">
|
||||
<meta property="og:description" content="{{ social_description }}">
|
||||
<meta property="og:url" content="{{ canonical_url }}">
|
||||
<meta property="og:type" content="{{ og_type }}">
|
||||
<meta property="og:site_name" content="Labyricorn">
|
||||
<meta property="og:image" content="{{ image_url }}">
|
||||
<meta property="og:image:secure_url" content="{{ image_url }}">
|
||||
<meta property="og:image:type" content="image/png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta property="og:image:alt" content="Labyricorn — systems, ideas, infrastructure, built to evolve.">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{ social_title }}">
|
||||
<meta name="twitter:description" content="{{ social_description }}">
|
||||
<meta name="twitter:image" content="{{ image_url }}">
|
||||
<meta name="twitter:image:alt" content="Labyricorn — systems, ideas, infrastructure, built to evolve.">
|
||||
{% if record.author %}<meta name="author" content="{{ record.author }}">{% endif %}
|
||||
{% if og_type == 'article' and record.date %}<meta property="article:published_time" content="{{ record.date|dateformat('YYYY-MM-dd') }}">{% endif %}
|
||||
{% if og_type == 'article' and record.updated %}<meta property="article:modified_time" content="{{ record.updated|dateformat('YYYY-MM-dd') }}">{% endif %}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": {{ schema_type|tojson }},
|
||||
"name": {{ social_title|tojson }},
|
||||
"headline": {{ social_title|tojson }},
|
||||
"description": {{ social_description|tojson }},
|
||||
"url": {{ canonical_url|tojson }},
|
||||
"mainEntityOfPage": {
|
||||
"@type": "WebPage",
|
||||
"@id": {{ canonical_url|tojson }}
|
||||
},
|
||||
"image": {
|
||||
"@type": "ImageObject",
|
||||
"url": {{ image_url|tojson }},
|
||||
"width": 1200,
|
||||
"height": 630
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "Labyricorn",
|
||||
"url": "https://www.labyricorn.com/"
|
||||
},
|
||||
"isAccessibleForFree": true
|
||||
{% if record.author %},
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": {{ record.author|tojson }}
|
||||
}
|
||||
{% endif %}
|
||||
{% if schema_type in ['Article', 'BlogPosting', 'TechArticle'] and record.date %},
|
||||
"datePublished": {{ record.date|dateformat('YYYY-MM-dd')|tojson }}
|
||||
{% endif %}
|
||||
{% if schema_type in ['Article', 'BlogPosting'] and record.updated %},
|
||||
"dateModified": {{ record.updated|dateformat('YYYY-MM-dd')|tojson }}
|
||||
{% endif %}
|
||||
{% if schema_type in ['Article', 'BlogPosting'] and record.tags %},
|
||||
"keywords": {{ record.tags|join(', ')|tojson }}
|
||||
{% endif %}
|
||||
{% if schema_type == 'TechArticle' and project %},
|
||||
"articleSection": {{ (project.title ~ ' development log')|tojson }},
|
||||
"isPartOf": {
|
||||
"@type": "CreativeWork",
|
||||
"name": {{ project.title|tojson }},
|
||||
"url": {{ (site_url ~ project.url_path)|tojson }}
|
||||
},
|
||||
"about": {
|
||||
"@type": "SoftwareSourceCode",
|
||||
"name": {{ project.title|tojson }},
|
||||
"url": {{ (site_url ~ project.url_path)|tojson }},
|
||||
"codeRepository": {{ project.repository_url|string|tojson }}
|
||||
}
|
||||
{% if record.topic_tags %},
|
||||
"keywords": {{ record.topic_tags|join(', ')|tojson }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if schema_type == 'SoftwareSourceCode' %},
|
||||
"codeRepository": {{ record.repository_url|string|tojson }},
|
||||
"sameAs": {{ record.repository_url|string|tojson }},
|
||||
"creativeWorkStatus": {{ record.status|capitalize|tojson }}
|
||||
{% if record.started %},
|
||||
"dateCreated": {{ record.started|dateformat('YYYY-MM-dd')|tojson }}
|
||||
{% endif %}
|
||||
{% if record.repository_languages %},
|
||||
"programmingLanguage": [
|
||||
{% for language in record.repository_languages %}{{ language|tojson }}{% if not loop.last %},{% endif %}{% endfor %}
|
||||
]
|
||||
{% endif %}
|
||||
{% if record.repository_latest_release %},
|
||||
"version": {{ record.repository_latest_release|tojson }}
|
||||
{% endif %}
|
||||
{% if record.repository_license %},
|
||||
"license": {{ record.repository_license|tojson }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
}
|
||||
</script>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro share_controls(record, content_kind='page', project=none) -%}
|
||||
{% set canonical_url = 'https://www.labyricorn.com' ~ record.url_path %}
|
||||
{% set share_title = record.title|striptags|trim %}
|
||||
{% if project %}{% set share_title = project.title ~ ' devlog: ' ~ record.title %}{% endif %}
|
||||
{% set share_description = record.summary|default('', true)|striptags|trim|truncate(180, true, '…') %}
|
||||
{% set bluesky_text = (share_title|truncate(210, true, '…')) ~ ' ' ~ canonical_url %}
|
||||
<aside class="share-panel" aria-labelledby="share-heading" data-share-component data-share-url="{{ canonical_url }}" data-share-title="{{ share_title }}" data-share-description="{{ share_description }}">
|
||||
<div class="share-panel-heading">
|
||||
<p class="share-kicker">Share</p>
|
||||
<h2 id="share-heading">Send this {{ content_kind }}</h2>
|
||||
</div>
|
||||
<div class="share-actions">
|
||||
<a class="share-action share-action-primary" href="{{ canonical_url }}" data-share-native aria-label="Share {{ share_title }}">Share</a>
|
||||
<a class="share-action" href="https://www.facebook.com/sharer/sharer.php?u={{ canonical_url|urlencode }}" target="_blank" rel="noopener noreferrer" aria-label="Share {{ share_title }} on Facebook">Facebook</a>
|
||||
<a class="share-action" href="https://www.linkedin.com/sharing/share-offsite/?url={{ canonical_url|urlencode }}" target="_blank" rel="noopener noreferrer" aria-label="Share {{ share_title }} on LinkedIn">LinkedIn</a>
|
||||
<a class="share-action" href="https://www.reddit.com/submit?url={{ canonical_url|urlencode }}&title={{ share_title|urlencode }}" target="_blank" rel="noopener noreferrer" aria-label="Share {{ share_title }} on Reddit">Reddit</a>
|
||||
<a class="share-action" href="https://bsky.app/intent/compose?text={{ bluesky_text|urlencode }}" target="_blank" rel="noopener noreferrer" aria-label="Share {{ share_title }} on Bluesky">Bluesky</a>
|
||||
<a class="share-action" href="{{ canonical_url }}" data-share-copy aria-label="Copy the link to {{ share_title }}">Copy Link</a>
|
||||
</div>
|
||||
<p class="share-status" data-share-status role="status" aria-live="polite"></p>
|
||||
</aside>
|
||||
{%- endmacro %}
|
||||
@@ -1,5 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "macros/tag-links.html" import tag_links, taxonomy_labels with context %}
|
||||
{% from "macros/social-sharing.html" import social_metadata, share_controls with context %}
|
||||
{% block social_head %}{{ social_metadata(this, 'SoftwareSourceCode', 'website') }}{% endblock %}
|
||||
{% block body %}
|
||||
{% set devlog = site.get(this.path ~ '/devlog') %}
|
||||
<article class="project-page">
|
||||
@@ -70,6 +72,7 @@
|
||||
</div>
|
||||
</dl>
|
||||
</aside>
|
||||
{{ share_controls(this, 'project') }}
|
||||
</div>
|
||||
|
||||
{% if this.technology_labels %}
|
||||
|
||||
Reference in New Issue
Block a user