@rathnasgala/theme 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +20 -0
- package/payload/.gala/artifact-files/gitignore +5 -0
- package/payload/.gala/managed-files.json +51 -0
- package/payload/.gala/publish.yml.template +35 -0
- package/payload/eleventy.config.js +47 -0
- package/payload/lib/build-manifest.js +189 -0
- package/payload/lib/engagement-snapshot.js +43 -0
- package/payload/lib/publication-state.js +61 -0
- package/payload/lib/render-markdown.js +96 -0
- package/payload/lib/seo.js +287 -0
- package/payload/lib/site-config.js +23 -0
- package/payload/package-lock.json +2042 -0
- package/payload/package.json +21 -0
- package/payload/src/404.njk +12 -0
- package/payload/src/_data/buildManifest.js +3 -0
- package/payload/src/_data/engagementSnapshot.js +5 -0
- package/payload/src/_data/feedLinks.js +15 -0
- package/payload/src/_data/languages.js +10 -0
- package/payload/src/_data/site.js +5 -0
- package/payload/src/_includes/components/ui.njk +17 -0
- package/payload/src/_includes/layouts/base.njk +40 -0
- package/payload/src/_includes/layouts/post.njk +11 -0
- package/payload/src/assets/interactions.js +54 -0
- package/payload/src/assets/preferences.js +30 -0
- package/payload/src/assets/search.js +81 -0
- package/payload/src/assets/theme-mode.js +35 -0
- package/payload/src/assets/theme.css +125 -0
- package/payload/src/feed.11ty.js +46 -0
- package/payload/src/index.njk +8 -0
- package/payload/src/languages.11ty.js +38 -0
- package/payload/src/posts.11ty.js +35 -0
- package/payload/src/redirects.11ty.js +37 -0
- package/payload/src/search-index.11ty.js +23 -0
- package/payload/src/search.njk +16 -0
- package/payload/src/settings.njk +19 -0
- package/payload/src/sitemap.11ty.js +26 -0
- package/payload/static/robots.txt +2 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gala/site-template",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "eleventy",
|
|
8
|
+
"test": "node --test",
|
|
9
|
+
"lint": "node --check eleventy.config.js && node --check lib/build-manifest.js && node --check lib/engagement-snapshot.js && node --check lib/publication-state.js && node --check lib/render-markdown.js && node --check lib/seo.js && node --check lib/site-config.js && node --check src/_data/buildManifest.js && node --check src/_data/engagementSnapshot.js && node --check src/_data/feedLinks.js && node --check src/_data/languages.js && node --check src/_data/site.js && node --check src/assets/interactions.js && node --check src/assets/preferences.js && node --check src/assets/search.js && node --check src/assets/theme-mode.js && node --check src/feed.11ty.js && node --check src/languages.11ty.js && node --check src/posts.11ty.js && node --check src/redirects.11ty.js && node --check src/search-index.11ty.js && node --check src/sitemap.11ty.js && node --check test/build-manifest.test.js && node --check test/component-contract.test.js && node --check test/config.test.js && node --check test/eleventy-manifest-build.test.js && node --check test/engagement-snapshot.test.js && node --check test/feed.test.js && node --check test/fixtures/xss-payloads.js && node --check test/managed-files.test.js && node --check test/preferences.test.js && node --check test/publication-state.test.js && node --check test/render-markdown.test.js && node --check test/search.test.js && node --check test/seo.test.js && node --check test/supporting-surfaces.test.js && node --check test/theme-contract.test.js && node --check test/theme-contrast.test.js && node --check test/theme-mode.test.js"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@11ty/eleventy": "3.1.6",
|
|
16
|
+
"markdown-it": "15.0.0",
|
|
17
|
+
"markdown-it-footnote": "4.0.0",
|
|
18
|
+
"sanitize-html": "2.17.6",
|
|
19
|
+
"yaml": "2.9.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: layouts/base.njk
|
|
3
|
+
title: Page not found
|
|
4
|
+
permalink: /404.html
|
|
5
|
+
eleventyExcludeFromCollections: true
|
|
6
|
+
---
|
|
7
|
+
<section class="gala-error-page">
|
|
8
|
+
<p class="gala-badge">404</p>
|
|
9
|
+
<h1>Page not found</h1>
|
|
10
|
+
<p>The page may have moved or no longer exists.</p>
|
|
11
|
+
<a class="gala-button" href="{{ '/' | url }}">Return home</a>
|
|
12
|
+
</section>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { siteUrl } from '../../lib/seo.js';
|
|
2
|
+
import { loadSiteConfiguration } from '../../lib/site-config.js';
|
|
3
|
+
import languages from './languages.js';
|
|
4
|
+
|
|
5
|
+
const site = await loadSiteConfiguration();
|
|
6
|
+
|
|
7
|
+
export default languages.map((language) => Object.freeze({
|
|
8
|
+
language,
|
|
9
|
+
title: `${site.site.name} — ${language}`,
|
|
10
|
+
href: siteUrl({
|
|
11
|
+
canonicalBaseUrl: site.hosting.canonicalBaseUrl,
|
|
12
|
+
pathPrefix: site.hosting.pathPrefix ?? '/',
|
|
13
|
+
relativePath: `/feed/${language}.xml`
|
|
14
|
+
})
|
|
15
|
+
}));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import buildManifest from './buildManifest.js';
|
|
2
|
+
import { loadSiteConfiguration } from '../../lib/site-config.js';
|
|
3
|
+
|
|
4
|
+
const site = await loadSiteConfiguration();
|
|
5
|
+
const languages = new Set([site.site.defaultLanguage]);
|
|
6
|
+
for (const post of buildManifest.posts) {
|
|
7
|
+
if (post.publicationState === 'published') languages.add(post.language);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default [...languages].sort((left, right) => left.localeCompare(right));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{% macro button(label, type='button') %}<button class="gala-button" type="{{ type }}">{{ label }}</button>{% endmacro %}
|
|
2
|
+
{% macro badge(label) %}<span class="gala-badge">{{ label }}</span>{% endmacro %}
|
|
3
|
+
{% macro tagChip(label, href) %}<a class="gala-tag-chip" href="{{ href }}">{{ label }}</a>{% endmacro %}
|
|
4
|
+
{% macro sectionHeading(title, description='') %}<header class="gala-section-heading"><h2>{{ title }}</h2>{% if description %}<p>{{ description }}</p>{% endif %}</header>{% endmacro %}
|
|
5
|
+
{% macro hero(title, description='', image='', imageAlt='') %}<header class="gala-hero">{% if image %}<img class="gala-hero__image" src="{{ image }}" alt="{{ imageAlt }}">{% endif %}<div class="gala-hero__text"><h1>{{ title }}</h1>{% if description %}<p>{{ description }}</p>{% endif %}</div></header>{% endmacro %}
|
|
6
|
+
{% macro pageContent(content) %}<article class="gala-page-content">{{ content | safe }}</article>{% endmacro %}
|
|
7
|
+
{% macro authorProfile(name, bio='', avatar='', links=[]) %}<section class="gala-author-profile" aria-label="Author profile">{% if avatar %}<img src="{{ avatar }}" alt="" width="80" height="80">{% endif %}<div><h2>{{ name }}</h2>{% if bio %}<p>{{ bio }}</p>{% endif %}{% if links.length %}<ul class="gala-social-links">{% for link in links %}<li><a href="{{ link.url }}" rel="me">{{ link.label }}</a></li>{% endfor %}</ul>{% endif %}</div></section>{% endmacro %}
|
|
8
|
+
{% macro cardIndex(posts) %}<ol class="gala-card-index">{% for post in posts %}{% if post.publicationState == "published" %}<li><article class="gala-card" lang="{{ post.language | escape }}"><h2><a href="{{ post.relativeUrl | url }}">{{ post.frontmatter.title }}</a></h2>{% if post.frontmatter.description %}<p>{{ post.frontmatter.description }}</p>{% endif %}</article></li>{% endif %}{% endfor %}</ol>{% endmacro %}
|
|
9
|
+
{% macro statsGraph(label, points) %}<figure class="gala-stats-graph"><svg role="img" aria-label="{{ label }}" viewBox="0 0 100 40" preserveAspectRatio="none"><polyline points="{{ points }}" vector-effect="non-scaling-stroke"></polyline></svg><figcaption>{{ label }}</figcaption></figure>{% endmacro %}
|
|
10
|
+
{% macro loading(label='Loading') %}<div class="gala-loading" role="status"><span class="gala-loading__indicator" aria-hidden="true"></span><span class="gala-visually-hidden">{{ label }}</span></div>{% endmacro %}
|
|
11
|
+
{% macro pagination(previous='', next='') %}<nav class="gala-pagination" aria-label="Pagination">{% if previous %}<a href="{{ previous }}" rel="prev">Previous</a>{% endif %}{% if next %}<a href="{{ next }}" rel="next">Next</a>{% endif %}</nav>{% endmacro %}
|
|
12
|
+
{% macro search(action='/search/') %}<form class="gala-search" action="{{ action }}" role="search"><label for="gala-search-query">Search</label><input id="gala-search-query" name="q" type="search" autocomplete="off"><button type="submit">Search</button></form>{% endmacro %}
|
|
13
|
+
{% macro tableOfContents(items) %}<nav class="gala-toc" aria-label="Table of contents"><ol>{% for item in items %}<li><a href="#{{ item.id }}">{{ item.text }}</a></li>{% endfor %}</ol></nav>{% endmacro %}
|
|
14
|
+
{% macro shareControl(canonicalUrl, targets=[]) %}<section class="gala-share" aria-label="Share this article"><button type="button" data-copy-url="{{ canonicalUrl | escape }}" aria-label="Copy canonical URL">Copy link</button><output class="gala-share__status" aria-live="polite"></output><input class="gala-share__fallback" value="{{ canonicalUrl | escape }}" readonly aria-label="Canonical URL">{% if targets.length %}<ul>{% for target in targets %}<li><a href="{{ target.url | escape }}" rel="noopener noreferrer" aria-label="Share on {{ target.label | escape }}">{{ target.label }}</a></li>{% endfor %}</ul>{% endif %}</section>{% endmacro %}
|
|
15
|
+
{% macro languageSwitcher(links, currentLanguage) %}{% if links.length > 2 %}<label>Preferred language <select data-language-preference data-navigate-on-selection>{% for link in links %}{% if link.hreflang != "x-default" %}<option value="{{ link.hreflang | escape }}" data-url="{{ link.href | escape }}"{% if link.hreflang == currentLanguage %} selected{% endif %}>{{ link.hreflang }}</option>{% endif %}{% endfor %}</select></label>{% endif %}{% endmacro %}
|
|
16
|
+
{% macro engagementSummary(articleId, engagement) %}<section class="gala-engagement" aria-label="Engagement" data-article-id="{{ articleId | escape }}">{% if engagement.available %}<dl data-engagement-snapshot><div><dt>Reactions</dt><dd>{{ engagement.reactions }}</dd></div><div><dt>Comments</dt><dd>{{ engagement.comments }}</dd></div><div><dt>Views</dt><dd>{{ engagement.views }}</dd></div></dl>{% else %}<p class="gala-engagement__placeholder" role="status">Engagement data unavailable.</p>{% endif %}<output data-engagement-status aria-live="polite"></output></section>{% endmacro %}
|
|
17
|
+
{% macro pageFooter(siteName, links=[]) %}<footer class="gala-page-footer"><p>© {{ siteName }}</p>{% if links.length %}<nav aria-label="Footer"><ul>{% for link in links %}<li><a href="{{ link.url }}">{{ link.label }}</a></li>{% endfor %}</ul></nav>{% endif %}</footer>{% endmacro %}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="{{ post.language or site.site.defaultLanguage }}" data-theme="{{ site.design.theme }}" data-palette="{{ site.design.palette }}" data-mode="system">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<script>(function(){try{var m=localStorage.getItem('gala-color-mode');if(m==='light'||m==='dark'||m==='system')document.documentElement.dataset.mode=m}catch(e){}})();</script>
|
|
7
|
+
<title>{{ post.frontmatter.title or title }} · {{ site.site.name }}</title>
|
|
8
|
+
{% if post %}<link rel="canonical" href="{{ post.canonicalUrl }}">{% endif %}
|
|
9
|
+
{% for alternate in hreflangLinks %}<link rel="alternate" hreflang="{{ alternate.hreflang }}" href="{{ alternate.href }}">{% endfor %}
|
|
10
|
+
{% for feed in feedLinks %}<link rel="alternate" type="application/atom+xml" hreflang="{{ feed.language }}" title="{{ feed.title }}" href="{{ feed.href }}">{% endfor %}
|
|
11
|
+
{% if seo %}
|
|
12
|
+
<meta name="description" content="{{ seo.description }}">
|
|
13
|
+
<meta property="og:type" content="article">
|
|
14
|
+
<meta property="og:title" content="{{ seo.title }}">
|
|
15
|
+
<meta property="og:description" content="{{ seo.description }}">
|
|
16
|
+
<meta property="og:url" content="{{ seo.canonicalUrl }}">
|
|
17
|
+
{% if seo.imageUrl %}<meta property="og:image" content="{{ seo.imageUrl }}">{% endif %}
|
|
18
|
+
<meta name="twitter:card" content="{{ seo.twitterCard }}">
|
|
19
|
+
<meta name="twitter:title" content="{{ seo.title }}">
|
|
20
|
+
<meta name="twitter:description" content="{{ seo.description }}">
|
|
21
|
+
{% if seo.imageUrl %}<meta name="twitter:image" content="{{ seo.imageUrl }}">{% endif %}
|
|
22
|
+
<script type="application/ld+json">{{ seo.structuredDataJson | safe }}</script>
|
|
23
|
+
{% endif %}
|
|
24
|
+
{% if post.publicationState == "tombstoned" %}<meta name="robots" content="noindex">{% endif %}
|
|
25
|
+
<link rel="stylesheet" href="{{ '/assets/theme.css' | url }}">
|
|
26
|
+
<link rel="stylesheet" href="{{ '/custom.css' | url }}">
|
|
27
|
+
<script src="{{ '/assets/theme-mode.js' | url }}" defer></script>
|
|
28
|
+
<script src="{{ '/assets/preferences.js' | url }}" defer></script>
|
|
29
|
+
<script src="{{ '/assets/interactions.js' | url }}" defer></script>
|
|
30
|
+
{% if searchPage %}<script src="{{ '/assets/search.js' | url }}" defer></script>{% endif %}
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<header>
|
|
34
|
+
<button type="button" data-theme-mode-toggle aria-label="Color mode: system. Activate for light.">Theme: system</button>
|
|
35
|
+
<a href="{{ '/settings/' | url }}">Settings</a>
|
|
36
|
+
<a href="{{ '/search/' | url }}">Search</a>
|
|
37
|
+
</header>
|
|
38
|
+
<main id="main-content">{{ content | safe }}</main>
|
|
39
|
+
</body>
|
|
40
|
+
</html>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: layouts/base.njk
|
|
3
|
+
---
|
|
4
|
+
{% from "components/ui.njk" import engagementSummary, languageSwitcher, shareControl %}
|
|
5
|
+
<article>
|
|
6
|
+
{% if post.publicationState == "published" %}<h1>{{ post.frontmatter.title }}</h1>{% endif %}
|
|
7
|
+
{% if post.publicationState == "published" %}{{ languageSwitcher(hreflangLinks, post.language) }}{% endif %}
|
|
8
|
+
{{ content | safe }}
|
|
9
|
+
{% if post.publicationState == "published" %}{{ engagementSummary(post.id, engagement) }}{% endif %}
|
|
10
|
+
{% if post.publicationState == "published" %}{{ shareControl(post.canonicalUrl) }}{% endif %}
|
|
11
|
+
</article>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function selectableFallback(control) {
|
|
2
|
+
const region = control.closest('.gala-share');
|
|
3
|
+
const fallback = region?.querySelector('.gala-share__fallback');
|
|
4
|
+
fallback?.focus();
|
|
5
|
+
fallback?.select();
|
|
6
|
+
const status = region?.querySelector('.gala-share__status');
|
|
7
|
+
if (status) status.textContent = 'Select and copy the URL shown.';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
document.addEventListener('click', async (event) => {
|
|
11
|
+
const share = event.target.closest('[data-copy-url]');
|
|
12
|
+
if (share) {
|
|
13
|
+
const value = share.dataset.copyUrl;
|
|
14
|
+
if (!window.isSecureContext || !navigator.clipboard?.writeText) {
|
|
15
|
+
selectableFallback(share);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
await navigator.clipboard.writeText(value);
|
|
20
|
+
const status = share.closest('.gala-share')?.querySelector('.gala-share__status');
|
|
21
|
+
if (status) status.textContent = 'Link copied.';
|
|
22
|
+
} catch {
|
|
23
|
+
selectableFallback(share);
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const copy = event.target.closest('[data-copy-code]');
|
|
29
|
+
if (!copy) return;
|
|
30
|
+
const code = copy.closest('.gala-code-block')?.querySelector('code');
|
|
31
|
+
if (!code || !navigator.clipboard?.writeText || !window.isSecureContext) return;
|
|
32
|
+
try {
|
|
33
|
+
await navigator.clipboard.writeText(code.textContent);
|
|
34
|
+
copy.textContent = 'Copied';
|
|
35
|
+
} catch {
|
|
36
|
+
copy.textContent = 'Select code to copy';
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const codeBlocks = new Set(
|
|
41
|
+
[...document.querySelectorAll('pre code')].map((code) => code.closest('pre')).filter(Boolean)
|
|
42
|
+
);
|
|
43
|
+
codeBlocks.forEach((pre) => {
|
|
44
|
+
const wrapper = document.createElement('div');
|
|
45
|
+
wrapper.className = 'gala-code-block';
|
|
46
|
+
pre.before(wrapper);
|
|
47
|
+
wrapper.append(pre);
|
|
48
|
+
const control = document.createElement('button');
|
|
49
|
+
control.type = 'button';
|
|
50
|
+
control.dataset.copyCode = '';
|
|
51
|
+
control.textContent = 'Copy code';
|
|
52
|
+
control.setAttribute('aria-label', 'Copy code block');
|
|
53
|
+
wrapper.prepend(control);
|
|
54
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const LANGUAGE_STORAGE_KEY = 'gala-language-preference';
|
|
2
|
+
|
|
3
|
+
function storedLanguage() {
|
|
4
|
+
try {
|
|
5
|
+
return localStorage.getItem(LANGUAGE_STORAGE_KEY);
|
|
6
|
+
} catch {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function optionFor(control, language) {
|
|
12
|
+
return [...control.options].find((option) => option.value === language);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
16
|
+
const stored = storedLanguage();
|
|
17
|
+
document.querySelectorAll('[data-language-preference]').forEach((control) => {
|
|
18
|
+
if (stored != null && optionFor(control, stored) != null) control.value = stored;
|
|
19
|
+
control.addEventListener('change', () => {
|
|
20
|
+
try {
|
|
21
|
+
localStorage.setItem(LANGUAGE_STORAGE_KEY, control.value);
|
|
22
|
+
} catch {
|
|
23
|
+
// Selection and explicit navigation still work when storage is unavailable.
|
|
24
|
+
}
|
|
25
|
+
if (!control.hasAttribute('data-navigate-on-selection')) return;
|
|
26
|
+
const selected = optionFor(control, control.value);
|
|
27
|
+
if (selected?.dataset.url) window.location.assign(selected.dataset.url);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const search = document.querySelector('[data-gala-search]');
|
|
2
|
+
|
|
3
|
+
if (search) {
|
|
4
|
+
const form = search.querySelector('form');
|
|
5
|
+
const query = form?.elements.namedItem('q');
|
|
6
|
+
const status = search.querySelector('[data-search-status]');
|
|
7
|
+
const results = search.querySelector('[data-search-results]');
|
|
8
|
+
const indexUrl = search.dataset.indexUrl;
|
|
9
|
+
let index;
|
|
10
|
+
|
|
11
|
+
const normalize = (value) => String(value ?? '').normalize('NFKC').toLocaleLowerCase();
|
|
12
|
+
|
|
13
|
+
function render(entries, term) {
|
|
14
|
+
results.replaceChildren();
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const item = document.createElement('li');
|
|
17
|
+
const article = document.createElement('article');
|
|
18
|
+
const heading = document.createElement('h2');
|
|
19
|
+
const link = document.createElement('a');
|
|
20
|
+
link.href = entry.url;
|
|
21
|
+
link.lang = entry.language;
|
|
22
|
+
link.textContent = entry.title;
|
|
23
|
+
heading.append(link);
|
|
24
|
+
article.append(heading);
|
|
25
|
+
if (entry.description) {
|
|
26
|
+
const description = document.createElement('p');
|
|
27
|
+
description.textContent = entry.description;
|
|
28
|
+
article.append(description);
|
|
29
|
+
}
|
|
30
|
+
item.append(article);
|
|
31
|
+
results.append(item);
|
|
32
|
+
}
|
|
33
|
+
status.textContent = `${entries.length} result${entries.length === 1 ? '' : 's'} for “${term}”.`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function run(rawTerm) {
|
|
37
|
+
const term = rawTerm.trim();
|
|
38
|
+
if (term === '') {
|
|
39
|
+
results.replaceChildren();
|
|
40
|
+
status.textContent = 'Enter a search term.';
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
status.textContent = 'Searching…';
|
|
44
|
+
try {
|
|
45
|
+
index ??= fetch(indexUrl, { headers: { Accept: 'application/json' } }).then(async (response) => {
|
|
46
|
+
if (!response.ok) throw new Error(`Search index returned HTTP ${response.status}`);
|
|
47
|
+
const payload = await response.json();
|
|
48
|
+
if (payload?.schemaVersion !== 1 || !Array.isArray(payload.entries)) {
|
|
49
|
+
throw new TypeError('Search index schema is unsupported');
|
|
50
|
+
}
|
|
51
|
+
return payload.entries;
|
|
52
|
+
});
|
|
53
|
+
const needle = normalize(term);
|
|
54
|
+
const matches = (await index).filter((entry) => normalize([
|
|
55
|
+
entry.title,
|
|
56
|
+
entry.description,
|
|
57
|
+
entry.language,
|
|
58
|
+
...(Array.isArray(entry.tags) ? entry.tags : []),
|
|
59
|
+
entry.body
|
|
60
|
+
].join('\n')).includes(needle));
|
|
61
|
+
render(matches, term);
|
|
62
|
+
} catch {
|
|
63
|
+
results.replaceChildren();
|
|
64
|
+
status.textContent = 'Search is temporarily unavailable.';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
form?.addEventListener('submit', (event) => {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
const term = query.value;
|
|
71
|
+
const location = new URL(window.location.href);
|
|
72
|
+
if (term.trim() === '') location.searchParams.delete('q');
|
|
73
|
+
else location.searchParams.set('q', term);
|
|
74
|
+
window.history.replaceState(null, '', location);
|
|
75
|
+
run(term);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const initial = new URLSearchParams(window.location.search).get('q') ?? '';
|
|
79
|
+
if (query) query.value = initial;
|
|
80
|
+
run(initial);
|
|
81
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const MODES = ['system', 'light', 'dark'];
|
|
2
|
+
const STORAGE_KEY = 'gala-color-mode';
|
|
3
|
+
|
|
4
|
+
function storedMode() {
|
|
5
|
+
try {
|
|
6
|
+
const mode = localStorage.getItem(STORAGE_KEY);
|
|
7
|
+
return MODES.includes(mode) ? mode : 'system';
|
|
8
|
+
} catch {
|
|
9
|
+
return 'system';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function applyMode(mode) {
|
|
14
|
+
document.documentElement.dataset.mode = mode;
|
|
15
|
+
const next = MODES[(MODES.indexOf(mode) + 1) % MODES.length];
|
|
16
|
+
document.querySelectorAll('[data-theme-mode-toggle]').forEach((control) => {
|
|
17
|
+
control.textContent = `Theme: ${mode}`;
|
|
18
|
+
control.setAttribute('aria-label', `Color mode: ${mode}. Activate for ${next}.`);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
23
|
+
applyMode(storedMode());
|
|
24
|
+
document.querySelectorAll('[data-theme-mode-toggle]').forEach((control) => {
|
|
25
|
+
control.addEventListener('click', () => {
|
|
26
|
+
const next = MODES[(MODES.indexOf(document.documentElement.dataset.mode) + 1) % MODES.length];
|
|
27
|
+
try {
|
|
28
|
+
localStorage.setItem(STORAGE_KEY, next);
|
|
29
|
+
} catch {
|
|
30
|
+
// The selected mode still applies for this page when storage is unavailable.
|
|
31
|
+
}
|
|
32
|
+
applyMode(next);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light dark;
|
|
3
|
+
--gala-color-background: light-dark(#fff, #111);
|
|
4
|
+
--gala-color-text: light-dark(#181818, #f5f5f5);
|
|
5
|
+
--gala-space-page: clamp(1rem, 4vw, 4rem);
|
|
6
|
+
--gala-radius-control: 0.5rem;
|
|
7
|
+
--gala-color-surface: light-dark(#f4f4f4, #1d1d1d);
|
|
8
|
+
--gala-color-accent: light-dark(#174ea6, #8ab4f8);
|
|
9
|
+
--gala-color-border: light-dark(#6b6b6b, #aaa);
|
|
10
|
+
--gala-font-body: system-ui, sans-serif;
|
|
11
|
+
--gala-content-width: 70rem;
|
|
12
|
+
--gala-widget-min-block-size: 8rem;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
:root[data-mode='light'] {
|
|
16
|
+
color-scheme: light;
|
|
17
|
+
--gala-color-background: #fff;
|
|
18
|
+
--gala-color-text: #181818;
|
|
19
|
+
--gala-color-surface: #f4f4f4;
|
|
20
|
+
--gala-color-accent: #174ea6;
|
|
21
|
+
--gala-color-border: #6b6b6b;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
:root[data-mode='dark'] {
|
|
25
|
+
color-scheme: dark;
|
|
26
|
+
--gala-color-background: #111;
|
|
27
|
+
--gala-color-text: #f5f5f5;
|
|
28
|
+
--gala-color-surface: #1d1d1d;
|
|
29
|
+
--gala-color-accent: #8ab4f8;
|
|
30
|
+
--gala-color-border: #aaa;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
body {
|
|
34
|
+
margin: 0;
|
|
35
|
+
padding: var(--gala-space-page);
|
|
36
|
+
color: var(--gala-color-text);
|
|
37
|
+
background: var(--gala-color-background);
|
|
38
|
+
font-family: var(--gala-font-body);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
main,
|
|
42
|
+
.gala-page-footer {
|
|
43
|
+
max-inline-size: var(--gala-content-width);
|
|
44
|
+
margin-inline: auto;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
a { color: var(--gala-color-accent); }
|
|
48
|
+
|
|
49
|
+
button {
|
|
50
|
+
min-block-size: 2.75rem;
|
|
51
|
+
padding-inline: 1rem;
|
|
52
|
+
color: inherit;
|
|
53
|
+
background: transparent;
|
|
54
|
+
border: 0.125rem solid currentcolor;
|
|
55
|
+
border-radius: var(--gala-radius-control);
|
|
56
|
+
font: inherit;
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
button:focus-visible {
|
|
61
|
+
outline: 0.1875rem solid currentcolor;
|
|
62
|
+
outline-offset: 0.1875rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.gala-button,
|
|
66
|
+
.gala-badge,
|
|
67
|
+
.gala-tag-chip {
|
|
68
|
+
display: inline-flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
min-block-size: 2.75rem;
|
|
71
|
+
border: 0.125rem solid var(--gala-color-border);
|
|
72
|
+
border-radius: var(--gala-radius-control);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.gala-badge,
|
|
76
|
+
.gala-tag-chip { padding-inline: 0.75rem; }
|
|
77
|
+
.gala-tag-chip { text-decoration: none; }
|
|
78
|
+
.gala-section-heading { margin-block: 3rem 1rem; }
|
|
79
|
+
.gala-section-heading > :where(h2, p) { margin-block: 0 0.5rem; }
|
|
80
|
+
.gala-hero { display: grid; gap: 1.5rem; align-items: center; }
|
|
81
|
+
.gala-hero__image { inline-size: 100%; block-size: auto; border-radius: var(--gala-radius-control); }
|
|
82
|
+
.gala-page-content { max-inline-size: 70ch; }
|
|
83
|
+
.gala-page-content :where(img, video) { max-inline-size: 100%; block-size: auto; }
|
|
84
|
+
.gala-author-profile { display: flex; gap: 1rem; padding: 1rem; background: var(--gala-color-surface); border-radius: var(--gala-radius-control); }
|
|
85
|
+
.gala-author-profile img { border-radius: 50%; object-fit: cover; }
|
|
86
|
+
.gala-social-links,
|
|
87
|
+
.gala-page-footer ul,
|
|
88
|
+
.gala-share ul { display: flex; flex-wrap: wrap; gap: 1rem; padding: 0; list-style: none; }
|
|
89
|
+
.gala-card-index { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; padding: 0; list-style: none; }
|
|
90
|
+
.gala-card { block-size: 100%; box-sizing: border-box; padding: 1.25rem; background: var(--gala-color-surface); border-radius: var(--gala-radius-control); }
|
|
91
|
+
.gala-stats-graph { min-block-size: var(--gala-widget-min-block-size); margin-inline: 0; }
|
|
92
|
+
.gala-stats-graph svg { inline-size: 100%; block-size: 8rem; }
|
|
93
|
+
.gala-stats-graph polyline { fill: none; stroke: var(--gala-color-accent); stroke-width: 2; }
|
|
94
|
+
.gala-loading { display: grid; min-block-size: var(--gala-widget-min-block-size); place-items: center; }
|
|
95
|
+
.gala-loading__indicator { inline-size: 2rem; block-size: 2rem; border: 0.25rem solid var(--gala-color-border); border-block-start-color: var(--gala-color-accent); border-radius: 50%; animation: gala-spin 0.8s linear infinite; }
|
|
96
|
+
.gala-pagination { display: flex; justify-content: space-between; gap: 1rem; }
|
|
97
|
+
.gala-search { display: grid; grid-template-columns: 1fr auto; gap: 0.5rem; }
|
|
98
|
+
.gala-search label { grid-column: 1 / -1; }
|
|
99
|
+
.gala-search input { min-block-size: 2.75rem; font: inherit; }
|
|
100
|
+
.gala-search-results { display: grid; gap: 1rem; padding: 0; list-style: none; }
|
|
101
|
+
.gala-search-results article { padding: 1rem; background: var(--gala-color-surface); border-radius: var(--gala-radius-control); }
|
|
102
|
+
.gala-search-results h2 { margin-block-start: 0; }
|
|
103
|
+
.gala-toc { padding: 1rem; background: var(--gala-color-surface); }
|
|
104
|
+
.gala-share__fallback { inline-size: min(100%, 36rem); }
|
|
105
|
+
.gala-engagement { min-block-size: var(--gala-widget-min-block-size); }
|
|
106
|
+
.gala-engagement dl { display: flex; flex-wrap: wrap; gap: 1rem 2rem; }
|
|
107
|
+
.gala-engagement dl > div { display: grid; gap: 0.25rem; }
|
|
108
|
+
.gala-engagement dd { margin: 0; font-size: 1.25rem; font-weight: 700; }
|
|
109
|
+
.gala-engagement__placeholder { display: grid; min-block-size: var(--gala-widget-min-block-size); align-content: center; margin: 0; }
|
|
110
|
+
.gala-page-footer { margin-block-start: 4rem; padding-block-start: 1rem; border-block-start: 0.125rem solid var(--gala-color-border); }
|
|
111
|
+
.gala-error-page { min-block-size: 60vh; display: grid; align-content: center; justify-items: start; }
|
|
112
|
+
.gala-code-block { position: relative; }
|
|
113
|
+
.gala-code-block > button { position: absolute; inset-block-start: 0.5rem; inset-inline-end: 0.5rem; background: var(--gala-color-background); }
|
|
114
|
+
.gala-code-block pre { overflow: auto; padding: 3.5rem 1rem 1rem; background: var(--gala-color-surface); border-radius: var(--gala-radius-control); }
|
|
115
|
+
.gala-visually-hidden { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
116
|
+
|
|
117
|
+
@keyframes gala-spin { to { transform: rotate(1turn); } }
|
|
118
|
+
|
|
119
|
+
@media (min-width: 48rem) {
|
|
120
|
+
.gala-hero { grid-template-columns: minmax(0, 1fr) minmax(18rem, 1fr); }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
124
|
+
@view-transition { navigation: auto; }
|
|
125
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { markdownLibrary } from '../lib/render-markdown.js';
|
|
2
|
+
import { renderAtomFeed, siteUrl, sortFeedPosts } from '../lib/seo.js';
|
|
3
|
+
|
|
4
|
+
function feedUpdated() {
|
|
5
|
+
const configured = process.env.GALA_BUILD_INSTANT;
|
|
6
|
+
const instant = configured == null ? new Date() : new Date(configured);
|
|
7
|
+
if (Number.isNaN(instant.getTime())) throw new TypeError('GALA_BUILD_INSTANT must be an ISO instant');
|
|
8
|
+
return instant.toISOString();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default class LanguageFeeds {
|
|
12
|
+
data() {
|
|
13
|
+
return {
|
|
14
|
+
pagination: { data: 'languages', size: 1, alias: 'language' },
|
|
15
|
+
permalink: ({ language }) => `/feed/${language}.xml`,
|
|
16
|
+
eleventyExcludeFromCollections: true
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
render({ buildManifest, language, site }) {
|
|
21
|
+
const posts = sortFeedPosts(buildManifest.posts.filter(
|
|
22
|
+
(post) => post.publicationState === 'published' && post.language === language
|
|
23
|
+
)).slice(0, 20);
|
|
24
|
+
const selfUrl = siteUrl({
|
|
25
|
+
canonicalBaseUrl: site.hosting.canonicalBaseUrl,
|
|
26
|
+
pathPrefix: site.hosting.pathPrefix ?? '/',
|
|
27
|
+
relativePath: `/feed/${language}.xml`
|
|
28
|
+
});
|
|
29
|
+
return renderAtomFeed({
|
|
30
|
+
id: selfUrl,
|
|
31
|
+
title: `${site.site.name} — ${language}`,
|
|
32
|
+
author: typeof site.site.author === 'string' && site.site.author.trim() !== ''
|
|
33
|
+
? site.site.author.trim()
|
|
34
|
+
: site.site.name,
|
|
35
|
+
updated: feedUpdated(),
|
|
36
|
+
selfUrl,
|
|
37
|
+
entries: posts.map((post) => ({
|
|
38
|
+
id: `urn:gala:article:${post.id}:${post.language}`,
|
|
39
|
+
title: post.frontmatter.title,
|
|
40
|
+
updated: `${post.frontmatter.publishAfterDate}T00:00:00.000Z`,
|
|
41
|
+
url: post.canonicalUrl,
|
|
42
|
+
html: markdownLibrary.render(post.body)
|
|
43
|
+
}))
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { siteUrl } from '../lib/seo.js';
|
|
2
|
+
|
|
3
|
+
function html(value) {
|
|
4
|
+
return String(value)
|
|
5
|
+
.replaceAll('&', '&')
|
|
6
|
+
.replaceAll('<', '<')
|
|
7
|
+
.replaceAll('>', '>')
|
|
8
|
+
.replaceAll('"', '"')
|
|
9
|
+
.replaceAll("'", ''');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default class LanguageIndexes {
|
|
13
|
+
data() {
|
|
14
|
+
return {
|
|
15
|
+
pagination: { data: 'languages', size: 1, alias: 'language' },
|
|
16
|
+
layout: 'layouts/base.njk',
|
|
17
|
+
permalink: ({ language }) => `/${language}/index.html`,
|
|
18
|
+
eleventyComputed: {
|
|
19
|
+
title: ({ language }) => language
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
render({ buildManifest, language, site }) {
|
|
25
|
+
const posts = buildManifest.posts.filter(
|
|
26
|
+
(post) => post.publicationState === 'published' && post.language === language
|
|
27
|
+
);
|
|
28
|
+
const items = posts.map((post) => {
|
|
29
|
+
const href = siteUrl({
|
|
30
|
+
canonicalBaseUrl: site.hosting.canonicalBaseUrl,
|
|
31
|
+
pathPrefix: site.hosting.pathPrefix ?? '/',
|
|
32
|
+
relativePath: post.relativeUrl
|
|
33
|
+
});
|
|
34
|
+
return `<li><a href="${html(href)}">${html(post.frontmatter.title)}</a></li>`;
|
|
35
|
+
}).join('');
|
|
36
|
+
return `<h1>${html(site.site.name)} — ${html(language)}</h1><ol>${items}</ol>`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { markdownLibrary } from '../lib/render-markdown.js';
|
|
2
|
+
import { articleHreflang, postSeo } from '../lib/seo.js';
|
|
3
|
+
import { engagementFor } from '../lib/engagement-snapshot.js';
|
|
4
|
+
|
|
5
|
+
export default class ValidatedPostPages {
|
|
6
|
+
data() {
|
|
7
|
+
return {
|
|
8
|
+
pagination: {
|
|
9
|
+
data: 'buildManifest.posts',
|
|
10
|
+
size: 1,
|
|
11
|
+
alias: 'post'
|
|
12
|
+
},
|
|
13
|
+
layout: 'layouts/post.njk',
|
|
14
|
+
permalink: ({ post }) => `${post.relativeUrl}index.html`,
|
|
15
|
+
eleventyComputed: {
|
|
16
|
+
hreflangLinks: ({ buildManifest, post, site }) => {
|
|
17
|
+
if (buildManifest?.posts == null || post?.source == null
|
|
18
|
+
|| site?.site?.defaultLanguage == null) return [];
|
|
19
|
+
return articleHreflang(buildManifest.posts, site).get(post.source) ?? [];
|
|
20
|
+
},
|
|
21
|
+
engagement: ({ engagementSnapshot, post }) => engagementFor(engagementSnapshot, post.id),
|
|
22
|
+
seo: ({ post, site }) => post?.publicationState === 'published'
|
|
23
|
+
? postSeo({ post, site, renderedHtml: markdownLibrary.render(post.body) })
|
|
24
|
+
: null
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
render({ post }) {
|
|
30
|
+
if (post.publicationState === 'tombstoned') {
|
|
31
|
+
return `<p>POST deleted on ${post.frontmatter.deleteDate}</p>`;
|
|
32
|
+
}
|
|
33
|
+
return markdownLibrary.render(post.body);
|
|
34
|
+
}
|
|
35
|
+
}
|