@rathnasgala/theme 2.0.1 → 2.0.2
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 +1 -1
- package/payload/.gala/managed-files.json +22 -20
- package/payload/lib/accent.js +105 -0
- package/payload/lib/render-markdown.js +1 -1
- package/payload/lib/seo.js +17 -3
- package/payload/lib/site-config.js +62 -0
- package/payload/src/_data/site.js +16 -0
- package/payload/src/_includes/components/ui.njk +36 -24
- package/payload/src/_includes/layouts/base.njk +23 -15
- package/payload/src/_includes/layouts/post.njk +10 -5
- package/payload/src/assets/engagement-comments.js +19 -9
- package/payload/src/assets/interactions.js +113 -41
- package/payload/src/assets/reader.js +2 -2
- package/payload/src/assets/theme.css +1 -1
- package/payload/src/posts.11ty.js +6 -0
- package/payload/src/styles/theme.css +295 -48
- package/payload/static/favicon.ico +0 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"themePackage": {
|
|
4
4
|
"name": "@rathnasgala/theme",
|
|
5
|
-
"version": "2.0.
|
|
5
|
+
"version": "2.0.2",
|
|
6
6
|
"availableDesignThemes": [
|
|
7
7
|
"editorial",
|
|
8
8
|
"modern",
|
|
@@ -18,56 +18,58 @@
|
|
|
18
18
|
".gala/publish.yml.template": "73080241d89bd09964b797a8bcc8e18471412e11bc873efad76305b60f497d08",
|
|
19
19
|
".gitignore": "b5fa24e10155097a0d372926fd2e59e6072e7cc2cb6399bbfb4beb8b33ea315b",
|
|
20
20
|
"eleventy.config.js": "0b72076f2f884a3a4a5b2ea4a124e0c1308c735526275bb847a3d6ebd082a186",
|
|
21
|
+
"lib/accent.js": "e6c921e0a3afe094b18709e3e40baa06692017fa6576d84b1ec6b7d788da8dd0",
|
|
21
22
|
"lib/build-manifest.js": "88949f0112d683ca809bb67ca969e15634792423559a67bc79a534cd807e5535",
|
|
22
23
|
"lib/engagement-snapshot.js": "298c11b1ab135a3a98a4e31b6320ab60dfd7fda6a9d9053df37d86282a5a6687",
|
|
23
24
|
"lib/performance-budget.js": "c279622dfde237cd6e3c64775c91d770a0e1f54f5f94257ccc95d9a44de2c4c9",
|
|
24
25
|
"lib/provider-fixtures/embeds.v1.json": "5f26909fb433135321ddf7fdd14e45aeb702805c88f5676941166c5735143ee2",
|
|
25
26
|
"lib/provider-fixtures/share-intents.v1.json": "66a8eedfcff711253ee45ceb94b53f7d5a6be3ddbbb3d1f0d4e31a114326cc0a",
|
|
26
27
|
"lib/publication-state.js": "1b59c29a18bbb1193bddea898dac3a4fb832c7bc4751263be086f3d64392dea0",
|
|
27
|
-
"lib/render-markdown.js": "
|
|
28
|
-
"lib/seo.js": "
|
|
28
|
+
"lib/render-markdown.js": "275b7da721aef1f68cda515673153471c0d597061e8fdf867c5f2b32c98b91db",
|
|
29
|
+
"lib/seo.js": "19cf1d67d5b711b8d3a533f207f267e770ee46fd4a4e6561c5f7fd4e9c1989ff",
|
|
29
30
|
"lib/share-targets.js": "458aff86fab2b62a364442cd8c612565c6e0b53eef2f20cb6237cc562b1e0731",
|
|
30
|
-
"lib/site-config.js": "
|
|
31
|
+
"lib/site-config.js": "679c0eaea066bbe7569bf0211696868106dd1d3bf6118fcfb7aeb75962055384",
|
|
31
32
|
"package-lock.json": "dc11470b1ad897c58d91745f23ff25711c593e62972ec9fb83f4e10fd77e1ecd",
|
|
32
33
|
"package.json": "1b3b70f6e94391d309625cb8cef503ee92ecd282bec587643b535ae7a14c24c0",
|
|
33
|
-
"
|
|
34
|
+
"scripts/build-reader.js": "743194c8dc9cfb9b6631db1732b5f36a92e3a4f905a2965c71d56e1ef50c2e45",
|
|
35
|
+
"scripts/lint.js": "c4bb3c961686043fa8c79bed546758ef09d44441421d8209fec9d69f74171025",
|
|
34
36
|
"src/_data/buildManifest.js": "628163305984e0d3537e27b0d4050f5ba5ead12a834878266d2ce8004ffe1b1e",
|
|
35
37
|
"src/_data/engagementSnapshot.js": "9217dacebfaf008f7a0ad67ff4e8d1fa8fba609a9dc7e3430c78c5b32024da0e",
|
|
36
38
|
"src/_data/feedLinks.js": "06c93352ebd888acb431a1c9a8be6ecff757895468e9f2d336761038751c2d5e",
|
|
37
39
|
"src/_data/languages.js": "1358748f7fa2f74406d3295a030827d6229a53e94da72cf712e719d7a356aeac",
|
|
38
|
-
"src/_data/site.js": "
|
|
39
|
-
"src/_includes/components/ui.njk": "
|
|
40
|
-
"src/_includes/layouts/base.njk": "
|
|
41
|
-
"src/_includes/layouts/post.njk": "
|
|
40
|
+
"src/_data/site.js": "432be73b49c20f088c304a2f7d7cd85906a747e0f3b381a4b395c5c44d68bb57",
|
|
41
|
+
"src/_includes/components/ui.njk": "0634c206c2db0507f77686635e3b406e1d8878011fd7eac64563df8499fe8f60",
|
|
42
|
+
"src/_includes/layouts/base.njk": "28043f394af8e2e3b09b6218ed3de2a506535fd5d59e67225c0fbd4431f967ff",
|
|
43
|
+
"src/_includes/layouts/post.njk": "579421054d8b547cb974a87114a96bb90cc2792b1436eb90c1f620589b2bfbfa",
|
|
44
|
+
"src/404.njk": "be0d44370ac5402156aaf3d32ea581057bc61ec94c1a5f92bb809733af7a8a20",
|
|
45
|
+
"src/accent.11ty.js": "52e7d3eb731c5f4649c14c4745f65da026c25b58cf462d075b8ef363f0e487dc",
|
|
42
46
|
"src/assets/embed-codepen.svg": "e9def697f38c466fb2b6652535c4c814e8cbbbefc6e052a69f335aeb27e8356b",
|
|
43
47
|
"src/assets/embed-gist.svg": "fcd9bc2bf29a5c118492b853c0a01390b7539c38ddb985da3c2a6e2f246a39f7",
|
|
44
48
|
"src/assets/embed-x.svg": "0ae74edc6790ec7e933067fe5392bce756e93fa7ad71caacf1c1ca99fa4ec9b6",
|
|
45
49
|
"src/assets/embed-youtube.svg": "10d192f2b14b67664fea88c8cf246321d20190def51d59fad5762e552219b067",
|
|
46
|
-
"src/assets/engagement-comments.js": "
|
|
50
|
+
"src/assets/engagement-comments.js": "eda0fee471c7e42ee01a25b8aa14ed6ae9690d52b3fbb34027d97534ab62998b",
|
|
47
51
|
"src/assets/engagement-transport.js": "d4108cded823f859e7a8d6e36716162422ceaf5e4b94a57ed9bd7941d5096216",
|
|
48
52
|
"src/assets/favicon.svg": "8615e3d96fe37ae9ed14afb1886e7c0e690eef5b10be66ca91cccfd085a74bb6",
|
|
49
|
-
"src/assets/interactions.js": "
|
|
53
|
+
"src/assets/interactions.js": "d7ac42be1c3453fb5a7bc1b7de52bca2bc7322e60ef235ce4953f5382a3550c5",
|
|
50
54
|
"src/assets/preferences.js": "789127bee013ed6a1bbf8cefd7b9109291c16220e7a6e09314e3c93c54731d1d",
|
|
55
|
+
"src/assets/reader.js": "96018c305177c5cbea77b4958ccba7800be82ace513f47fc811bdd41fb8eee63",
|
|
51
56
|
"src/assets/search.js": "df425f0239277792ad39f2138bc89cb66cc4ecda5ed59042d46abc2f05372088",
|
|
52
57
|
"src/assets/theme-mode.js": "944b29db11a577961a60ddcfa52f07ef2411f3f3631bea85bd79be16428c08be",
|
|
53
|
-
"src/assets/theme.css": "
|
|
58
|
+
"src/assets/theme.css": "43adf073599bf660534b26c7096c0fe5c25b12589d6dcb760d2d06fe2a477be4",
|
|
59
|
+
"src/client/reader.js": "5a61d629517c5315b3cb14dbae6cee226ba828e95132100f2ad36e614c57470a",
|
|
54
60
|
"src/cname.11ty.js": "206fc224df91c569e18312158e7f69803061f2952513f501ada54bd06c4f8006",
|
|
55
61
|
"src/contact.njk": "f4d74d038781e15af0acef571901c91e7753a2e21b6bef617429585698839a34",
|
|
56
62
|
"src/feed.11ty.js": "009bd2384e08e02a9405683bab1653bab3aeaec16accb0fd01ba69b24c777da9",
|
|
57
63
|
"src/index.njk": "5c313f0fcdad3ddbfb79376bc9671b3f37539ef70c14bdbbd26d6e2e6de250e8",
|
|
58
64
|
"src/languages.11ty.js": "399559199c65570d7d5efd88e20254838e316f81c06d7b28ec34990e963db915",
|
|
59
|
-
"src/posts.11ty.js": "
|
|
65
|
+
"src/posts.11ty.js": "c0bdc81183f5849670d100ceb97ecad0d1eb05f0de3eea34a0ce7427fd2e7e2a",
|
|
60
66
|
"src/redirects.11ty.js": "3c163246a59f5364861db6a6f05ae88afc938a5379d86cf42776d8d49b0031f3",
|
|
61
67
|
"src/search-index.11ty.js": "700dc2a2be0ef20de3a828a4612fcf7a082691eade29c6e1d75f09c24b6c6024",
|
|
62
68
|
"src/search.njk": "63f2e8aeebb63c90da2e375eece639ce096cad1eff7f28628cafc46faf7ceee6",
|
|
63
69
|
"src/sitemap.11ty.js": "4f233e94fb4a913ad5659116518432ed264984a7b622eba3558c77b4ec6435dc",
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"src/assets/reader.js": "cb53c4702bae396c708b6763352843868447ca9838bbf2ee451bb81c7c0b5b6c",
|
|
68
|
-
"scripts/lint.js": "c4bb3c961686043fa8c79bed546758ef09d44441421d8209fec9d69f74171025",
|
|
69
|
-
"src/styles/theme.css": "5ba8ccced10ceecd4ead7e60980d6d2f792eb9abff1748484d1a3de0273d2dc8",
|
|
70
|
-
"src/accent.11ty.js": "52e7d3eb731c5f4649c14c4745f65da026c25b58cf462d075b8ef363f0e487dc"
|
|
70
|
+
"src/styles/theme.css": "14496b8c692ea81427386125dc92822e1f06eadab30844380f77ca710c2ff7ef",
|
|
71
|
+
"static/favicon.ico": "ad3cb0b02f0164f3af6280d0bfe712879d55cec0e9a7a567f658aaddca5a55a2",
|
|
72
|
+
"static/robots.txt": "16ceb5ee3e0dc13aa9adf31a3ebbe45a1d965b8c2b9f72eaf84e5911e140ed95"
|
|
71
73
|
},
|
|
72
74
|
"artifactSources": {
|
|
73
75
|
".gitignore": ".gala/artifact-files/gitignore"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A writer picks one colour; this makes it work in both modes.
|
|
3
|
+
*
|
|
4
|
+
* Offering a handful of named presets was not picking a colour, and an unconstrained colour picker
|
|
5
|
+
* is worse: most colours a person likes fail contrast against one ground or the other, and a
|
|
6
|
+
* writer cannot see that until the site is deployed. So the seed is treated as a *hue* the writer
|
|
7
|
+
* chose, and the lightness is moved — as little as possible — until the result is readable on the
|
|
8
|
+
* background it will actually sit on.
|
|
9
|
+
*
|
|
10
|
+
* The maths is OKLab, so a hue keeps its identity while its lightness changes. Doing this in sRGB
|
|
11
|
+
* shifts the colour itself, and a writer who picked a warm red gets back a different red.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const LIGHT_GROUND = '#fdfcfb';
|
|
15
|
+
const DARK_GROUND = '#131211';
|
|
16
|
+
/** WCAG AA for body-sized text, which is what a link is. */
|
|
17
|
+
const REQUIRED = 4.5;
|
|
18
|
+
|
|
19
|
+
export function parseHex(value) {
|
|
20
|
+
if (typeof value !== 'string') return null;
|
|
21
|
+
const hex = value.trim().replace(/^#/, '');
|
|
22
|
+
const full = hex.length === 3 ? [...hex].map((digit) => digit + digit).join('') : hex;
|
|
23
|
+
if (!/^[0-9a-f]{6}$/i.test(full)) return null;
|
|
24
|
+
return [0, 2, 4].map((offset) => Number.parseInt(full.slice(offset, offset + 2), 16));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const toLinear = (channel) => {
|
|
28
|
+
const value = channel / 255;
|
|
29
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const toSrgb = (value) => {
|
|
33
|
+
const channel = value <= 0.0031308 ? value * 12.92 : 1.055 * value ** (1 / 2.4) - 0.055;
|
|
34
|
+
return Math.min(255, Math.max(0, Math.round(channel * 255)));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function relativeLuminance([red, green, blue]) {
|
|
38
|
+
return 0.2126 * toLinear(red) + 0.7152 * toLinear(green) + 0.0722 * toLinear(blue);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function contrast(a, b) {
|
|
42
|
+
const [high, low] = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x);
|
|
43
|
+
return (high + 0.05) / (low + 0.05);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function rgbToOklab([red, green, blue]) {
|
|
47
|
+
const [r, g, b] = [red, green, blue].map(toLinear);
|
|
48
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
49
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
50
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
51
|
+
return [
|
|
52
|
+
0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
|
|
53
|
+
1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
|
|
54
|
+
0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s,
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function oklabToRgb([lightness, a, b]) {
|
|
59
|
+
const l = (lightness + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
60
|
+
const m = (lightness - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
61
|
+
const s = (lightness - 0.0894841775 * a - 1.2914855480 * b) ** 3;
|
|
62
|
+
return [
|
|
63
|
+
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
64
|
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
65
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
|
|
66
|
+
].map(toSrgb);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const hex = ([red, green, blue]) =>
|
|
70
|
+
`#${[red, green, blue].map((c) => c.toString(16).padStart(2, '0')).join('')}`;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The writer's colour, moved along its own lightness until it is readable on `ground`.
|
|
74
|
+
*
|
|
75
|
+
* Searched from the seed outwards in both directions so the answer is the nearest readable
|
|
76
|
+
* version of what they chose, rather than always darker or always lighter.
|
|
77
|
+
*/
|
|
78
|
+
function readableOn(seed, ground) {
|
|
79
|
+
const groundRgb = parseHex(ground);
|
|
80
|
+
if (contrast(seed, groundRgb) >= REQUIRED) return hex(seed);
|
|
81
|
+
|
|
82
|
+
const [lightness, a, b] = rgbToOklab(seed);
|
|
83
|
+
let nearest = null;
|
|
84
|
+
for (let step = 0.01; step <= 1; step += 0.01) {
|
|
85
|
+
for (const direction of [-1, 1]) {
|
|
86
|
+
const candidate = lightness + direction * step;
|
|
87
|
+
if (candidate < 0 || candidate > 1) continue;
|
|
88
|
+
const rgb = oklabToRgb([candidate, a, b]);
|
|
89
|
+
if (contrast(rgb, groundRgb) >= REQUIRED) { nearest = rgb; break; }
|
|
90
|
+
}
|
|
91
|
+
if (nearest) break;
|
|
92
|
+
}
|
|
93
|
+
// Only reachable for a ground no colour can meet, which neither of ours is.
|
|
94
|
+
return hex(nearest ?? seed);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The two values the page needs: one readable on the light ground, one on the dark.
|
|
99
|
+
* Returns null for anything that is not a colour, so the caller falls back to the look's own.
|
|
100
|
+
*/
|
|
101
|
+
export function accentPair(seed) {
|
|
102
|
+
const rgb = parseHex(seed);
|
|
103
|
+
if (!rgb) return null;
|
|
104
|
+
return { light: readableOn(rgb, LIGHT_GROUND), dark: readableOn(rgb, DARK_GROUND) };
|
|
105
|
+
}
|
|
@@ -195,7 +195,7 @@ markdown.renderer.rules.fence = (tokens, index, options, environment, self) => {
|
|
|
195
195
|
lang: language,
|
|
196
196
|
themes: SHIKI_THEMES,
|
|
197
197
|
defaultColor: false
|
|
198
|
-
}));
|
|
198
|
+
}).replaceAll(/ style="[^"]*"/g, ''));
|
|
199
199
|
const tokensByIndex = environment.galaHighlightTokens ??= [];
|
|
200
200
|
tokensByIndex.push(token);
|
|
201
201
|
return `<div class="gala-highlight-placeholder" data-highlight-token="${token}"></div>`;
|
package/payload/lib/seo.js
CHANGED
|
@@ -134,9 +134,12 @@ export function postSeo({ post, site, renderedHtml }) {
|
|
|
134
134
|
? fallbackDescription(renderedHtml)
|
|
135
135
|
: authoredDescription;
|
|
136
136
|
const siteName = requiredText(site.site?.name, 'site name');
|
|
137
|
-
const
|
|
137
|
+
const configuredAuthor = site.site?.authorProfile?.displayName || site.site?.author;
|
|
138
|
+
const author = [post.frontmatter?.author, configuredAuthor, siteName]
|
|
138
139
|
.find((value) => typeof value === 'string' && value.trim() !== '')
|
|
139
140
|
.trim();
|
|
141
|
+
const authorProfile = author === configuredAuthor ? site.site?.authorProfile : null;
|
|
142
|
+
const publisherProfile = site.site?.publisher;
|
|
140
143
|
const pageUrl = absoluteHttpsUrl(post.pageUrl, 'page URL').toString();
|
|
141
144
|
const selectedCanonical = canonicalUrl({ pageUrl, canonicalOverride: post.canonicalUrl });
|
|
142
145
|
const language = Intl.getCanonicalLocales(requiredText(post.language, 'post language'))[0];
|
|
@@ -168,8 +171,19 @@ export function postSeo({ post, site, renderedHtml }) {
|
|
|
168
171
|
datePublished,
|
|
169
172
|
dateModified,
|
|
170
173
|
inLanguage: language,
|
|
171
|
-
author: {
|
|
172
|
-
|
|
174
|
+
author: {
|
|
175
|
+
'@type': 'Person',
|
|
176
|
+
name: author,
|
|
177
|
+
...(authorProfile?.profileUrl ? { url: authorProfile.profileUrl } : {}),
|
|
178
|
+
...(authorProfile?.avatarUrl ? { image: authorProfile.avatarUrl } : {})
|
|
179
|
+
},
|
|
180
|
+
publisher: {
|
|
181
|
+
'@type': 'Organization',
|
|
182
|
+
name: publisherProfile?.name || siteName,
|
|
183
|
+
...(publisherProfile?.url ? { url: publisherProfile.url } : {}),
|
|
184
|
+
...(publisherProfile?.logoUrl
|
|
185
|
+
? { logo: { '@type': 'ImageObject', url: publisherProfile.logoUrl } } : {})
|
|
186
|
+
},
|
|
173
187
|
...(imageUrl == null ? {} : { image: imageUrl })
|
|
174
188
|
};
|
|
175
189
|
const breadcrumbList = {
|
|
@@ -24,6 +24,8 @@ const LEGACY_DESIGN_VALUES = Object.freeze({
|
|
|
24
24
|
const CONTACT_KEYS = Object.freeze([
|
|
25
25
|
'enabled', 'websiteEnabled', 'phoneEnabled'
|
|
26
26
|
]);
|
|
27
|
+
const PROFILE_KEYS = Object.freeze(['displayName', 'bio', 'avatarUrl', 'profileUrl']);
|
|
28
|
+
const PUBLISHER_KEYS = Object.freeze(['name', 'url', 'logoUrl']);
|
|
27
29
|
|
|
28
30
|
/*
|
|
29
31
|
* `layout` and `palette` have always been required. The rest were added later and are optional:
|
|
@@ -104,6 +106,60 @@ export function validateContact(value) {
|
|
|
104
106
|
});
|
|
105
107
|
}
|
|
106
108
|
|
|
109
|
+
function profileText(value, field, maximum) {
|
|
110
|
+
if (value == null || value === 'unavailable') return '';
|
|
111
|
+
if (typeof value !== 'string') throw new TypeError(`${field} must be a string`);
|
|
112
|
+
const normalized = value.trim();
|
|
113
|
+
if ([...normalized].length > maximum) {
|
|
114
|
+
throw new TypeError(`${field} must contain at most ${maximum} characters`);
|
|
115
|
+
}
|
|
116
|
+
return normalized;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function profileUrl(value, field) {
|
|
120
|
+
const normalized = profileText(value, field, 2048);
|
|
121
|
+
if (normalized === '') return '';
|
|
122
|
+
let parsed;
|
|
123
|
+
try { parsed = new URL(normalized); } catch { throw new TypeError(`${field} must be an HTTPS URL`); }
|
|
124
|
+
if (parsed.protocol !== 'https:' || parsed.username !== '' || parsed.password !== '') {
|
|
125
|
+
throw new TypeError(`${field} must be a credential-free HTTPS URL`);
|
|
126
|
+
}
|
|
127
|
+
return normalized;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function validateProfile(site) {
|
|
131
|
+
const legacyAuthor = profileText(site?.author, 'site.author', 120);
|
|
132
|
+
const author = site?.authorProfile;
|
|
133
|
+
const publisher = site?.publisher;
|
|
134
|
+
if (author != null && (Array.isArray(author) || typeof author !== 'object')) {
|
|
135
|
+
throw new TypeError('site.authorProfile must be a mapping');
|
|
136
|
+
}
|
|
137
|
+
if (publisher != null && (Array.isArray(publisher) || typeof publisher !== 'object')) {
|
|
138
|
+
throw new TypeError('site.publisher must be a mapping');
|
|
139
|
+
}
|
|
140
|
+
const unknownAuthor = Object.keys(author ?? {}).filter((key) => !PROFILE_KEYS.includes(key));
|
|
141
|
+
const unknownPublisher = Object.keys(publisher ?? {}).filter((key) => !PUBLISHER_KEYS.includes(key));
|
|
142
|
+
if (unknownAuthor.length) throw new TypeError(`Unsupported author profile field: ${unknownAuthor.join(', ')}`);
|
|
143
|
+
if (unknownPublisher.length) throw new TypeError(`Unsupported publisher field: ${unknownPublisher.join(', ')}`);
|
|
144
|
+
const normalized = Object.freeze({
|
|
145
|
+
author: Object.freeze({
|
|
146
|
+
displayName: profileText(author?.displayName, 'site.authorProfile.displayName', 120) || legacyAuthor,
|
|
147
|
+
bio: profileText(author?.bio, 'site.authorProfile.bio', 500),
|
|
148
|
+
avatarUrl: profileUrl(author?.avatarUrl, 'site.authorProfile.avatarUrl'),
|
|
149
|
+
profileUrl: profileUrl(author?.profileUrl, 'site.authorProfile.profileUrl')
|
|
150
|
+
}),
|
|
151
|
+
publisher: Object.freeze({
|
|
152
|
+
name: profileText(publisher?.name, 'site.publisher.name', 120),
|
|
153
|
+
url: profileUrl(publisher?.url, 'site.publisher.url'),
|
|
154
|
+
logoUrl: profileUrl(publisher?.logoUrl, 'site.publisher.logoUrl')
|
|
155
|
+
})
|
|
156
|
+
});
|
|
157
|
+
if (!normalized.publisher.name && (normalized.publisher.url || normalized.publisher.logoUrl)) {
|
|
158
|
+
throw new TypeError('site.publisher.name is required when publisher URLs are set');
|
|
159
|
+
}
|
|
160
|
+
return normalized;
|
|
161
|
+
}
|
|
162
|
+
|
|
107
163
|
export async function loadSiteConfiguration({
|
|
108
164
|
root = process.cwd(),
|
|
109
165
|
configPath = process.env.GALA_CONFIG_PATH ?? 'site.config.yml'
|
|
@@ -126,5 +182,11 @@ export async function loadSiteConfiguration({
|
|
|
126
182
|
validatePerformanceBudgets(config?.performance?.budgets);
|
|
127
183
|
config.statistics = validateStatistics(config.statistics);
|
|
128
184
|
config.contact = validateContact(config.contact);
|
|
185
|
+
const profile = validateProfile(config.site);
|
|
186
|
+
config.site = {
|
|
187
|
+
...(config.site ?? {}),
|
|
188
|
+
authorProfile: profile.author,
|
|
189
|
+
publisher: profile.publisher
|
|
190
|
+
};
|
|
129
191
|
return config;
|
|
130
192
|
}
|
|
@@ -3,6 +3,22 @@ import { loadSiteConfiguration } from '../../lib/site-config.js';
|
|
|
3
3
|
|
|
4
4
|
export default async function siteConfiguration() {
|
|
5
5
|
const configuration = await loadSiteConfiguration();
|
|
6
|
+
const repositoryOwner = configuration?.site?.repository?.split('/')[0];
|
|
7
|
+
const author = configuration?.site?.authorProfile?.displayName || configuration?.site?.author;
|
|
8
|
+
const name = configuration?.site?.name;
|
|
9
|
+
const fallback = author && author !== 'unavailable'
|
|
10
|
+
? author
|
|
11
|
+
: repositoryOwner && repositoryOwner !== 'unavailable' ? repositoryOwner : 'Publication';
|
|
12
|
+
configuration.site = {
|
|
13
|
+
...configuration.site,
|
|
14
|
+
name: name && name !== 'unavailable' && name !== 'New Gala Site' ? name : fallback,
|
|
15
|
+
author: author && author !== 'unavailable' && author !== 'New Gala Site' ? author : undefined,
|
|
16
|
+
authorProfile: {
|
|
17
|
+
...configuration.site.authorProfile,
|
|
18
|
+
displayName: author && author !== 'unavailable' && author !== 'New Gala Site'
|
|
19
|
+
? author : fallback
|
|
20
|
+
}
|
|
21
|
+
};
|
|
6
22
|
/*
|
|
7
23
|
* The writer picks one colour; the page needs two, because the same colour cannot be readable on
|
|
8
24
|
* both a light and a dark ground. Derived here rather than in CSS so the contrast is exact — a
|
|
@@ -10,45 +10,57 @@
|
|
|
10
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
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
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) %}<
|
|
13
|
+
{% macro tableOfContents(items) %}<details class="gala-toc" open><summary>On this page</summary><nav aria-label="Table of contents"><ol>{% for item in items %}<li><a href="#{{ item.id }}">{{ item.text }}</a></li>{% endfor %}</ol></nav></details>{% endmacro %}
|
|
14
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
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
|
|
17
|
-
|
|
16
|
+
{% macro reactionIcon(value) %}<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">{% if value == 'like' %}<path d="M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5"/>{% elif value == 'insightful' %}<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6M10 22h4"/>{% elif value == 'celebrate' %}<path d="M5.8 11.3 2 22l10.7-3.79M4 3h.01M22 8h.01M15 2h.01M22 20h.01"/><path d="m22 2-2.24.75a2.9 2.9 0 0 0-1.96 3.12c.1.86-.57 1.63-1.45 1.63h-.38c-.86 0-1.6.6-1.76 1.44L14 10M22 13l-.82-.33c-.86-.34-1.82.2-1.98 1.11-.11.7-.72 1.22-1.43 1.22H17M11 2l.33.82c.34.86-.2 1.82-1.11 1.98C9.52 4.9 9 5.52 9 6.23V7"/><path d="M11 13c1.93 1.93 2.83 4.17 2 5-.83.83-3.07-.07-5-2-1.93-1.93-2.83-4.17-2-5 .83-.83 3.07.07 5 2Z"/>{% elif value == 'funny' %}<path d="M15 10V9M16.472 15a6 6 0 0 1-8.943 0M9 10V9"/><circle cx="12" cy="12" r="10"/>{% elif value == 'mind-blown' %}<path d="M12 18V5M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5M17.997 5.125a4 4 0 0 1 2.526 5.77M18 18a4 4 0 0 0 2-7.464M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517M6 18a4 4 0 0 1-2-7.464M6.003 5.125a4 4 0 0 0-2.526 5.77"/>{% else %}<path d="M11 14h2a2 2 0 0 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 16"/><path d="m14.45 13.39 5.05-4.694C20.196 8 21 6.85 21 5.75a2.75 2.75 0 0 0-4.797-1.837.276.276 0 0 1-.406 0A2.75 2.75 0 0 0 11 5.75c0 1.2.802 2.248 1.5 2.946L16 11.95M2 15l6 6"/><path d="m7 20 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a1 1 0 0 0-2.75-2.91"/>{% endif %}</svg>{% endmacro %}
|
|
17
|
+
{% macro actionRail(canonicalUrl, engagement) %}
|
|
18
18
|
<aside class="gala-action-rail" aria-label="Reader actions">
|
|
19
|
-
<
|
|
19
|
+
<dl class="gala-action-stats" data-action-stats aria-label="Article statistics">
|
|
20
|
+
{% set reactionCount = engagement.reactions if engagement.available else 0 %}{% set commentCount = engagement.comments if engagement.available else 0 %}{% set viewCount = engagement.views if engagement.available else 0 %}
|
|
21
|
+
<div title="{{ reactionCount }} reactions"><dt><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1.1-1.1a5.5 5.5 0 0 0-7.8 7.8l1.1 1.1L12 21l7.8-7.5 1.1-1.1a5.5 5.5 0 0 0-.1-7.8Z"/></svg><span class="gala-visually-hidden">Reactions</span></dt><dd data-engagement-stat="reactions" data-stat-label="Reactions">{{ reactionCount if reactionCount >= 1000 else '<1K' | safe }}</dd></div>
|
|
22
|
+
<div title="{{ commentCount }} comments"><dt><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4Z"/></svg><span class="gala-visually-hidden">Comments</span></dt><dd data-engagement-stat="comments" data-stat-label="Comments">{{ commentCount if commentCount >= 1000 else '<1K' | safe }}</dd></div>
|
|
23
|
+
<div title="{{ viewCount }} views"><dt><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg><span class="gala-visually-hidden">Views</span></dt><dd data-engagement-stat="views" data-stat-label="Views">{{ viewCount if viewCount >= 1000 else '<1K' | safe }}</dd></div>
|
|
24
|
+
</dl>
|
|
25
|
+
<span class="gala-action-rail__separator gala-action-rail__separator--stats" aria-hidden="true"></span>
|
|
26
|
+
<div class="gala-action-group gala-action-group--reactions" role="group" aria-label="React to this article">
|
|
27
|
+
<div class="gala-reaction-actions">
|
|
20
28
|
{% for reaction in [
|
|
21
|
-
{ value: 'like', label: 'Like'
|
|
22
|
-
{ value: 'insightful', label: 'Insightful'
|
|
23
|
-
{ value: 'celebrate', label: 'Celebrate'
|
|
24
|
-
{ value: 'funny', label: 'Funny'
|
|
25
|
-
{ value: 'mind-blown', label: 'Mind blown'
|
|
26
|
-
{ value: 'thank-you', label: 'Thank you'
|
|
27
|
-
] %}<button type="button" data-reaction="{{ reaction.value }}" aria-label="{{ reaction.label }}" title="{{ reaction.label }}" aria-pressed="false"
|
|
29
|
+
{ value: 'like', label: 'Like' },
|
|
30
|
+
{ value: 'insightful', label: 'Insightful' },
|
|
31
|
+
{ value: 'celebrate', label: 'Celebrate' },
|
|
32
|
+
{ value: 'funny', label: 'Funny' },
|
|
33
|
+
{ value: 'mind-blown', label: 'Mind blown' },
|
|
34
|
+
{ value: 'thank-you', label: 'Thank you' }
|
|
35
|
+
] %}<button type="button" data-reaction="{{ reaction.value }}" aria-label="{{ reaction.label }}" title="{{ reaction.label }}" aria-pressed="false">{{ reactionIcon(reaction.value) }}</button>{% endfor %}
|
|
36
|
+
</div>
|
|
28
37
|
</div>
|
|
29
|
-
<
|
|
30
|
-
<div class="gala-
|
|
31
|
-
<
|
|
32
|
-
<button type="button" data-
|
|
33
|
-
<a href="
|
|
34
|
-
<
|
|
38
|
+
<span class="gala-action-rail__separator" aria-hidden="true"></span>
|
|
39
|
+
<div class="gala-action-group gala-action-group--utilities" role="group" aria-label="Reader utilities">
|
|
40
|
+
<div class="gala-utility-actions">
|
|
41
|
+
<button type="button" data-follow-article aria-label="Follow article" title="Follow article" aria-pressed="false"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9M10 21h4"/></svg><span class="gala-visually-hidden" data-follow-label>Follow article</span></button>
|
|
42
|
+
<a href="#comments" aria-label="Comments" title="Comments"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4Z"/></svg></a>
|
|
43
|
+
<div class="gala-share" aria-label="Share this article">
|
|
44
|
+
<button type="button" data-copy-url="{{ canonicalUrl | escape }}" aria-label="Copy link" title="Copy link"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
|
|
45
|
+
<button type="button" data-native-share="{{ canonicalUrl | escape }}" aria-label="Share" title="Share"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.6 10.5 6.8-4M8.6 13.5l6.8 4"/></svg></button>
|
|
35
46
|
<output class="gala-share__status gala-visually-hidden" aria-live="polite"></output>
|
|
36
47
|
<input class="gala-share__fallback" value="{{ canonicalUrl | escape }}" readonly aria-label="Canonical URL">
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
37
50
|
</div>
|
|
38
51
|
</aside>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<
|
|
52
|
+
{% endmacro %}
|
|
53
|
+
{% macro conversation(engagement) %}
|
|
54
|
+
<div id="conversation" class="gala-conversation" aria-label="Article conversation">
|
|
55
|
+
<header class="gala-conversation__header"><h2>Conversation</h2></header>
|
|
43
56
|
{# Readers see what they can do before they are asked who they are. Signing in is requested
|
|
44
57
|
at the moment of intent, and the intent is replayed once they are back. #}
|
|
45
58
|
{# The conversation is an island: it owns its own state, because a thread five deep with a page
|
|
46
59
|
cursor, an open reply form and an edit in progress is state, and hand-written DOM kept losing
|
|
47
60
|
track of it. Nothing here is needed by a search engine — comments have always arrived from the
|
|
48
61
|
API after load — so rendering them on the client costs nothing the article cares about. #}
|
|
49
|
-
<div data-gala-comments></div>
|
|
50
|
-
<output data-engagement-status aria-live="polite"
|
|
62
|
+
<div id="comments" data-gala-comments></div>
|
|
63
|
+
<output class="gala-conversation__status" data-engagement-status aria-live="polite">Loading comments…</output>
|
|
51
64
|
</div>
|
|
52
|
-
</section>
|
|
53
65
|
{% endmacro %}
|
|
54
66
|
{% 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 %}
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
<link rel="icon" href="{{ '/assets/favicon.svg' | url }}" type="image/svg+xml">
|
|
30
30
|
<script src="{{ '/assets/reader.js' | url }}" defer></script>
|
|
31
31
|
</head>
|
|
32
|
-
<body>
|
|
32
|
+
<body{% if post %} class="gala-page--article"{% endif %}>
|
|
33
33
|
<a class="gala-skip-link" href="#main-content">Skip to content</a>
|
|
34
34
|
<header class="gala-site-header">
|
|
35
35
|
<a class="gala-site-name" href="{{ '/' | url }}" aria-label="Home">{{ site.site.name }}</a>
|
|
@@ -56,7 +56,17 @@
|
|
|
56
56
|
</header>
|
|
57
57
|
<main id="main-content">{{ content | safe }}</main>
|
|
58
58
|
<footer class="gala-page-footer">
|
|
59
|
-
<
|
|
59
|
+
<section class="gala-page-footer__identity" aria-label="Publication author">
|
|
60
|
+
{% if site.site.authorProfile.avatarUrl %}<img src="{{ site.site.authorProfile.avatarUrl | escape }}" alt="" width="40" height="40">{% endif %}
|
|
61
|
+
<div class="gala-page-footer__details">
|
|
62
|
+
<p class="gala-page-footer__line">
|
|
63
|
+
{% if site.site.authorProfile.profileUrl %}<a class="gala-page-footer__author" href="{{ site.site.authorProfile.profileUrl | escape }}" rel="author">{{ site.site.authorProfile.displayName }}</a>{% else %}<strong class="gala-page-footer__author">{{ site.site.authorProfile.displayName }}</strong>{% endif %}
|
|
64
|
+
{% if site.site.publisher.name %}<span>Published by {% if site.site.publisher.url %}<a href="{{ site.site.publisher.url | escape }}" rel="publisher">{{ site.site.publisher.name }}</a>{% else %}{{ site.site.publisher.name }}{% endif %}</span>{% endif %}
|
|
65
|
+
<span>© {{ site.site.name }}</span>
|
|
66
|
+
</p>
|
|
67
|
+
{% if site.site.authorProfile.bio %}<p class="gala-page-footer__bio">{{ site.site.authorProfile.bio }}</p>{% endif %}
|
|
68
|
+
</div>
|
|
69
|
+
</section>
|
|
60
70
|
{# Which repository sent the traffic, which footer the click came from, which language it
|
|
61
71
|
was read in, and which author owns it. Sites registered before these managed values
|
|
62
72
|
existed fall back rather than emitting the literal placeholder. #}
|
|
@@ -64,21 +74,20 @@
|
|
|
64
74
|
{% set galaLinkLanguage = post.language or site.site.defaultLanguage %}
|
|
65
75
|
{% set galaLinkSource = site.site.repository if site.site.repository and site.site.repository != 'unavailable' else 'gala-site' %}
|
|
66
76
|
{% set galaLinkOwner = site.site.ownerId if site.site.ownerId and site.site.ownerId != 'unavailable' else site.site.id %}
|
|
67
|
-
|
|
77
|
+
<nav aria-label="Publication footer"><ul>
|
|
78
|
+
{% if attributionTier != 'PAID' %}<li><a href="https://gala67.com/?utm_source={{ galaLinkSource | urlencode }}&utm_medium=referral&utm_campaign=powered-by-gala&utm_content={{ galaLinkPlacement | urlencode }}&utm_term={{ galaLinkLanguage | urlencode }}&utm_id={{ galaLinkOwner | urlencode }}" rel="external">Powered by Gala</a></li>{% endif %}
|
|
79
|
+
<li><a href="https://app.gala67.com/s/terms" rel="external">Terms</a></li>
|
|
80
|
+
<li><a href="https://app.gala67.com/s/privacy" rel="external">Privacy</a></li>
|
|
81
|
+
</ul></nav>
|
|
68
82
|
</footer>
|
|
69
83
|
<dialog id="gala-settings-dialog" class="gala-dialog" aria-labelledby="gala-settings-title">
|
|
70
|
-
<form method="dialog" class="gala-dialog__close"><button type="submit" aria-label="Close settings">Close</button></form>
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
<label>Preferred language
|
|
75
|
-
<select data-language-preference>{% for language in languages %}<option value="{{ language | escape }}">{{ language }}</option>{% endfor %}</select>
|
|
76
|
-
</label>
|
|
77
|
-
</section>
|
|
84
|
+
<header class="gala-dialog__header"><h2 id="gala-settings-title">Reader settings</h2><form method="dialog" class="gala-dialog__close"><button type="submit" aria-label="Close settings">Close</button></form></header>
|
|
85
|
+
<label>Preferred language
|
|
86
|
+
<select data-language-preference>{% for language in languages %}<option value="{{ language | escape }}">{{ language }}</option>{% endfor %}</select>
|
|
87
|
+
</label>
|
|
78
88
|
</dialog>
|
|
79
89
|
<dialog id="gala-search-dialog" class="gala-dialog" aria-labelledby="gala-search-title">
|
|
80
|
-
<form method="dialog" class="gala-dialog__close"><button type="submit" aria-label="Close search">Close</button></form>
|
|
81
|
-
<h2 id="gala-search-title">Search</h2>
|
|
90
|
+
<header class="gala-dialog__header"><h2 id="gala-search-title">Search</h2><form method="dialog" class="gala-dialog__close"><button type="submit" aria-label="Close search">Close</button></form></header>
|
|
82
91
|
<section data-gala-search data-index-url="{{ '/search-index.json' | url }}">
|
|
83
92
|
<form class="gala-search" action="{{ '/search/' | url }}" role="search">
|
|
84
93
|
<label for="gala-dialog-search-query">Search published posts</label>
|
|
@@ -88,13 +97,12 @@
|
|
|
88
97
|
<p data-search-status role="status" aria-live="polite">Enter a search term.</p>
|
|
89
98
|
<ol class="gala-search-results" data-search-results></ol>
|
|
90
99
|
</section>
|
|
91
|
-
<p><a href="{{ '/search/' | url }}">Open search page</a></p>
|
|
92
100
|
</dialog>
|
|
93
101
|
{% if site.site.id and site.site.id != "unavailable" %}
|
|
94
102
|
<dialog id="gala-account-dialog" class="gala-dialog gala-account-dialog" aria-labelledby="gala-account-title">
|
|
95
103
|
<form method="dialog" class="gala-dialog__close"><button type="submit" aria-label="Close account">Close</button></form>
|
|
96
104
|
<h2 id="gala-account-title" class="gala-visually-hidden">Gala account</h2>
|
|
97
|
-
<iframe data-gala-session-frame title="Gala account and sign in" allow="identity-credentials-get" src="{{ site.platform.apiBaseUrl or 'https://api.gala67.com' }}/v1/widget/session?siteId={{ site.site.id | escape }}"></iframe>
|
|
105
|
+
<iframe data-gala-session-frame title="Gala account and sign in" loading="lazy" allow="identity-credentials-get" src="{{ site.platform.apiBaseUrl or 'https://api.gala67.com' }}/v1/widget/session?siteId={{ site.site.id | escape }}"></iframe>
|
|
98
106
|
</dialog>
|
|
99
107
|
{% endif %}
|
|
100
108
|
</body>
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
layout: layouts/base.njk
|
|
3
3
|
---
|
|
4
|
-
{% from "components/ui.njk" import
|
|
4
|
+
{% from "components/ui.njk" import actionRail, conversation, languageSwitcher, tableOfContents %}
|
|
5
5
|
<article id="article" class="gala-article">
|
|
6
|
-
{% if post.publicationState == "published" %}<header class="gala-article-header"><p class="gala-eyebrow">Published {{ post.frontmatter.publishAfterDate }} · {{ post.language }}</p><h1>{{ post.frontmatter.title }}</h1
|
|
6
|
+
{% if post.publicationState == "published" %}<progress class="gala-reading-progress" data-reading-progress max="1" value="0" aria-label="Reading progress"></progress><header class="gala-article-header"><p class="gala-eyebrow">Published {{ post.frontmatter.publishAfterDate }} · {{ post.language }} · {{ readingMinutes }} min read</p><h1>{{ post.frontmatter.title }}</h1>{% set byline = post.frontmatter.author or site.site.author %}{% if byline and byline != "unavailable" and byline != "New Gala Site" and byline != site.site.name %}<p class="gala-byline">By {{ byline }}</p>{% endif %}</header>{% endif %}
|
|
7
7
|
{% if post.publicationState == "published" %}{{ languageSwitcher(hreflangLinks, post.language) }}{% endif %}
|
|
8
|
-
{% if
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
{% if post.publicationState == "published" %}<section class="gala-engagement" data-article-id="{{ post.id | escape }}" data-engagement-url="{{ site.platform.apiBaseUrl or 'https://api.gala67.com' }}/v1/articles/{{ post.id | escape }}/engagement">{% endif %}
|
|
9
|
+
<div class="gala-reading-layout">
|
|
10
|
+
{% if postTableOfContents.length %}{{ tableOfContents(postTableOfContents) }}{% else %}<span></span>{% endif %}
|
|
11
|
+
<div class="gala-markdown">{{ content | safe }}</div>
|
|
12
|
+
{% if post.publicationState == "published" %}<div class="gala-article-boundary"><strong>Reader interactions</strong></div><span class="gala-action-dock" data-action-dock aria-hidden="true"></span>{{ actionRail(post.canonicalUrl, engagement) }}{% endif %}
|
|
13
|
+
</div>
|
|
14
|
+
{% if post.publicationState == "published" %}{{ conversation(engagement) }}{% endif %}
|
|
15
|
+
{% if post.publicationState == "published" %}</section>{% endif %}
|
|
11
16
|
</article>
|