@rathnasgala/theme 0.0.20 → 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 +28 -23
- package/payload/eleventy.config.js +15 -1
- 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 +75 -5
- package/payload/package-lock.json +487 -2
- package/payload/package.json +7 -6
- package/payload/scripts/build-reader.js +24 -0
- package/payload/scripts/lint.js +21 -0
- package/payload/src/_data/site.js +16 -0
- package/payload/src/_includes/components/ui.njk +38 -17
- package/payload/src/_includes/layouts/base.njk +33 -26
- package/payload/src/_includes/layouts/post.njk +11 -7
- package/payload/src/accent.11ty.js +10 -0
- package/payload/src/assets/engagement-comments.js +125 -234
- package/payload/src/assets/engagement-transport.js +10 -4
- package/payload/src/assets/interactions.js +161 -39
- package/payload/src/assets/reader.js +2 -0
- package/payload/src/assets/theme.css +1 -676
- package/payload/src/client/reader.js +5 -0
- package/payload/src/posts.11ty.js +6 -0
- package/payload/src/styles/theme.css +1058 -0
- package/payload/static/favicon.ico +0 -0
- package/payload/src/assets/vendor/hooks.js +0 -2
- package/payload/src/assets/vendor/htm.js +0 -2
- package/payload/src/assets/vendor/preact.js +0 -2
- package/payload/src/assets/vendor/signals-core.js +0 -2
- package/payload/src/assets/vendor/signals.js +0 -2
package/package.json
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"themePackage": {
|
|
4
4
|
"name": "@rathnasgala/theme",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "2.0.2",
|
|
6
6
|
"availableDesignThemes": [
|
|
7
|
-
"editorial"
|
|
7
|
+
"editorial",
|
|
8
|
+
"modern",
|
|
9
|
+
"technical"
|
|
8
10
|
],
|
|
9
11
|
"securityAdvisories": []
|
|
10
12
|
},
|
|
@@ -15,55 +17,58 @@
|
|
|
15
17
|
"files": {
|
|
16
18
|
".gala/publish.yml.template": "73080241d89bd09964b797a8bcc8e18471412e11bc873efad76305b60f497d08",
|
|
17
19
|
".gitignore": "b5fa24e10155097a0d372926fd2e59e6072e7cc2cb6399bbfb4beb8b33ea315b",
|
|
18
|
-
"eleventy.config.js": "
|
|
20
|
+
"eleventy.config.js": "0b72076f2f884a3a4a5b2ea4a124e0c1308c735526275bb847a3d6ebd082a186",
|
|
21
|
+
"lib/accent.js": "e6c921e0a3afe094b18709e3e40baa06692017fa6576d84b1ec6b7d788da8dd0",
|
|
19
22
|
"lib/build-manifest.js": "88949f0112d683ca809bb67ca969e15634792423559a67bc79a534cd807e5535",
|
|
20
23
|
"lib/engagement-snapshot.js": "298c11b1ab135a3a98a4e31b6320ab60dfd7fda6a9d9053df37d86282a5a6687",
|
|
21
24
|
"lib/performance-budget.js": "c279622dfde237cd6e3c64775c91d770a0e1f54f5f94257ccc95d9a44de2c4c9",
|
|
22
25
|
"lib/provider-fixtures/embeds.v1.json": "5f26909fb433135321ddf7fdd14e45aeb702805c88f5676941166c5735143ee2",
|
|
23
26
|
"lib/provider-fixtures/share-intents.v1.json": "66a8eedfcff711253ee45ceb94b53f7d5a6be3ddbbb3d1f0d4e31a114326cc0a",
|
|
24
27
|
"lib/publication-state.js": "1b59c29a18bbb1193bddea898dac3a4fb832c7bc4751263be086f3d64392dea0",
|
|
25
|
-
"lib/render-markdown.js": "
|
|
26
|
-
"lib/seo.js": "
|
|
28
|
+
"lib/render-markdown.js": "275b7da721aef1f68cda515673153471c0d597061e8fdf867c5f2b32c98b91db",
|
|
29
|
+
"lib/seo.js": "19cf1d67d5b711b8d3a533f207f267e770ee46fd4a4e6561c5f7fd4e9c1989ff",
|
|
27
30
|
"lib/share-targets.js": "458aff86fab2b62a364442cd8c612565c6e0b53eef2f20cb6237cc562b1e0731",
|
|
28
|
-
"lib/site-config.js": "
|
|
29
|
-
"package-lock.json": "
|
|
30
|
-
"package.json": "
|
|
31
|
-
"
|
|
31
|
+
"lib/site-config.js": "679c0eaea066bbe7569bf0211696868106dd1d3bf6118fcfb7aeb75962055384",
|
|
32
|
+
"package-lock.json": "dc11470b1ad897c58d91745f23ff25711c593e62972ec9fb83f4e10fd77e1ecd",
|
|
33
|
+
"package.json": "1b3b70f6e94391d309625cb8cef503ee92ecd282bec587643b535ae7a14c24c0",
|
|
34
|
+
"scripts/build-reader.js": "743194c8dc9cfb9b6631db1732b5f36a92e3a4f905a2965c71d56e1ef50c2e45",
|
|
35
|
+
"scripts/lint.js": "c4bb3c961686043fa8c79bed546758ef09d44441421d8209fec9d69f74171025",
|
|
32
36
|
"src/_data/buildManifest.js": "628163305984e0d3537e27b0d4050f5ba5ead12a834878266d2ce8004ffe1b1e",
|
|
33
37
|
"src/_data/engagementSnapshot.js": "9217dacebfaf008f7a0ad67ff4e8d1fa8fba609a9dc7e3430c78c5b32024da0e",
|
|
34
38
|
"src/_data/feedLinks.js": "06c93352ebd888acb431a1c9a8be6ecff757895468e9f2d336761038751c2d5e",
|
|
35
39
|
"src/_data/languages.js": "1358748f7fa2f74406d3295a030827d6229a53e94da72cf712e719d7a356aeac",
|
|
36
|
-
"src/_data/site.js": "
|
|
37
|
-
"src/_includes/components/ui.njk": "
|
|
38
|
-
"src/_includes/layouts/base.njk": "
|
|
39
|
-
"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",
|
|
40
46
|
"src/assets/embed-codepen.svg": "e9def697f38c466fb2b6652535c4c814e8cbbbefc6e052a69f335aeb27e8356b",
|
|
41
47
|
"src/assets/embed-gist.svg": "fcd9bc2bf29a5c118492b853c0a01390b7539c38ddb985da3c2a6e2f246a39f7",
|
|
42
48
|
"src/assets/embed-x.svg": "0ae74edc6790ec7e933067fe5392bce756e93fa7ad71caacf1c1ca99fa4ec9b6",
|
|
43
49
|
"src/assets/embed-youtube.svg": "10d192f2b14b67664fea88c8cf246321d20190def51d59fad5762e552219b067",
|
|
44
|
-
"src/assets/engagement-comments.js": "
|
|
45
|
-
"src/assets/engagement-transport.js": "
|
|
50
|
+
"src/assets/engagement-comments.js": "eda0fee471c7e42ee01a25b8aa14ed6ae9690d52b3fbb34027d97534ab62998b",
|
|
51
|
+
"src/assets/engagement-transport.js": "d4108cded823f859e7a8d6e36716162422ceaf5e4b94a57ed9bd7941d5096216",
|
|
46
52
|
"src/assets/favicon.svg": "8615e3d96fe37ae9ed14afb1886e7c0e690eef5b10be66ca91cccfd085a74bb6",
|
|
47
|
-
"src/assets/interactions.js": "
|
|
53
|
+
"src/assets/interactions.js": "d7ac42be1c3453fb5a7bc1b7de52bca2bc7322e60ef235ce4953f5382a3550c5",
|
|
48
54
|
"src/assets/preferences.js": "789127bee013ed6a1bbf8cefd7b9109291c16220e7a6e09314e3c93c54731d1d",
|
|
55
|
+
"src/assets/reader.js": "96018c305177c5cbea77b4958ccba7800be82ace513f47fc811bdd41fb8eee63",
|
|
49
56
|
"src/assets/search.js": "df425f0239277792ad39f2138bc89cb66cc4ecda5ed59042d46abc2f05372088",
|
|
50
57
|
"src/assets/theme-mode.js": "944b29db11a577961a60ddcfa52f07ef2411f3f3631bea85bd79be16428c08be",
|
|
51
|
-
"src/assets/theme.css": "
|
|
52
|
-
"src/
|
|
53
|
-
"src/assets/vendor/htm.js": "e4dac9bc40a6f2ec590413d787ae30bbcfdfcde4f28971ee65d9578271b7aad4",
|
|
54
|
-
"src/assets/vendor/preact.js": "712517dd2a42c3c22b5ab0264ad7e9a2b1e8be6cf97fa00dbedd98203617e616",
|
|
55
|
-
"src/assets/vendor/signals-core.js": "1e10560fec5118c9e35eb312c869fe51f4870424766ef2ec0f7cd069bd898c8b",
|
|
56
|
-
"src/assets/vendor/signals.js": "717a88151bacf4f8b79b528a96d71be20a133fb3345230c1146929903cecfd19",
|
|
58
|
+
"src/assets/theme.css": "43adf073599bf660534b26c7096c0fe5c25b12589d6dcb760d2d06fe2a477be4",
|
|
59
|
+
"src/client/reader.js": "5a61d629517c5315b3cb14dbae6cee226ba828e95132100f2ad36e614c57470a",
|
|
57
60
|
"src/cname.11ty.js": "206fc224df91c569e18312158e7f69803061f2952513f501ada54bd06c4f8006",
|
|
58
61
|
"src/contact.njk": "f4d74d038781e15af0acef571901c91e7753a2e21b6bef617429585698839a34",
|
|
59
62
|
"src/feed.11ty.js": "009bd2384e08e02a9405683bab1653bab3aeaec16accb0fd01ba69b24c777da9",
|
|
60
63
|
"src/index.njk": "5c313f0fcdad3ddbfb79376bc9671b3f37539ef70c14bdbbd26d6e2e6de250e8",
|
|
61
64
|
"src/languages.11ty.js": "399559199c65570d7d5efd88e20254838e316f81c06d7b28ec34990e963db915",
|
|
62
|
-
"src/posts.11ty.js": "
|
|
65
|
+
"src/posts.11ty.js": "c0bdc81183f5849670d100ceb97ecad0d1eb05f0de3eea34a0ce7427fd2e7e2a",
|
|
63
66
|
"src/redirects.11ty.js": "3c163246a59f5364861db6a6f05ae88afc938a5379d86cf42776d8d49b0031f3",
|
|
64
67
|
"src/search-index.11ty.js": "700dc2a2be0ef20de3a828a4612fcf7a082691eade29c6e1d75f09c24b6c6024",
|
|
65
68
|
"src/search.njk": "63f2e8aeebb63c90da2e375eece639ce096cad1eff7f28628cafc46faf7ceee6",
|
|
66
69
|
"src/sitemap.11ty.js": "4f233e94fb4a913ad5659116518432ed264984a7b622eba3558c77b4ec6435dc",
|
|
70
|
+
"src/styles/theme.css": "14496b8c692ea81427386125dc92822e1f06eadab30844380f77ca710c2ff7ef",
|
|
71
|
+
"static/favicon.ico": "ad3cb0b02f0164f3af6280d0bfe712879d55cec0e9a7a567f658aaddca5a55a2",
|
|
67
72
|
"static/robots.txt": "16ceb5ee3e0dc13aa9adf31a3ebbe45a1d965b8c2b9f72eaf84e5911e140ed95"
|
|
68
73
|
},
|
|
69
74
|
"artifactSources": {
|
|
@@ -4,6 +4,9 @@ import { loadSiteConfiguration } from './lib/site-config.js';
|
|
|
4
4
|
import { enforcePerformanceBudgets } from './lib/performance-budget.js';
|
|
5
5
|
import { lstat, realpath } from 'node:fs/promises';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
|
|
9
|
+
const themeBootstrap = "(function(){try{var m=localStorage.getItem('gala-color-mode');if(m==='light'||m==='dark'||m==='system')document.documentElement.dataset.mode=m}catch(e){}})();";
|
|
7
10
|
|
|
8
11
|
async function verifiedMediaSource(postSource, mediaSource) {
|
|
9
12
|
const metadata = await lstat(mediaSource);
|
|
@@ -25,9 +28,20 @@ export default async function (eleventyConfig) {
|
|
|
25
28
|
const [manifest, site] = await Promise.all([readBuildManifest(), loadSiteConfiguration()]);
|
|
26
29
|
const attributionTier = process.env.GALA_ATTRIBUTION_TIER === 'PAID' ? 'PAID' : 'FREE';
|
|
27
30
|
eleventyConfig.addGlobalData('attributionTier', attributionTier);
|
|
31
|
+
eleventyConfig.addGlobalData('themeBootstrap', themeBootstrap);
|
|
32
|
+
eleventyConfig.addFilter('sha256Csp', (value) =>
|
|
33
|
+
createHash('sha256').update(String(value)).digest('base64'));
|
|
28
34
|
eleventyConfig.setLibrary('md', markdownLibrary);
|
|
29
35
|
eleventyConfig.addPassthroughCopy({ static: '/' });
|
|
30
|
-
eleventyConfig.addPassthroughCopy({
|
|
36
|
+
eleventyConfig.addPassthroughCopy({
|
|
37
|
+
'src/assets/theme.css': 'assets/theme.css',
|
|
38
|
+
'src/assets/reader.js': 'assets/reader.js',
|
|
39
|
+
'src/assets/favicon.svg': 'assets/favicon.svg',
|
|
40
|
+
'src/assets/embed-codepen.svg': 'assets/embed-codepen.svg',
|
|
41
|
+
'src/assets/embed-gist.svg': 'assets/embed-gist.svg',
|
|
42
|
+
'src/assets/embed-x.svg': 'assets/embed-x.svg',
|
|
43
|
+
'src/assets/embed-youtube.svg': 'assets/embed-youtube.svg'
|
|
44
|
+
});
|
|
31
45
|
eleventyConfig.addPassthroughCopy('custom.css');
|
|
32
46
|
for (const post of manifest.posts) {
|
|
33
47
|
for (const copy of post.media ?? []) {
|
|
@@ -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 = {
|
|
@@ -12,14 +12,20 @@ const BUDGET_KEYS = Object.freeze([
|
|
|
12
12
|
* a publication looks; anything the CSS does not answer to has no business being offered.
|
|
13
13
|
*/
|
|
14
14
|
const DESIGN_VALUES = Object.freeze({
|
|
15
|
-
layout: Object.freeze(['article-first', 'portfolio']),
|
|
16
|
-
palette: Object.freeze(['default', 'ocean', 'forest', 'plum']),
|
|
17
15
|
theme: Object.freeze(['editorial', 'modern', 'technical']),
|
|
18
16
|
colorMode: Object.freeze(['system', 'light', 'dark'])
|
|
19
17
|
});
|
|
18
|
+
// Accepted only so repositories created before v2 keep building. New author surfaces never offer
|
|
19
|
+
// these independent knobs; v2's look owns its layout and palette as one reviewed composition.
|
|
20
|
+
const LEGACY_DESIGN_VALUES = Object.freeze({
|
|
21
|
+
layout: Object.freeze(['article-first', 'portfolio']),
|
|
22
|
+
palette: Object.freeze(['default', 'ocean', 'forest', 'plum'])
|
|
23
|
+
});
|
|
20
24
|
const CONTACT_KEYS = Object.freeze([
|
|
21
25
|
'enabled', 'websiteEnabled', 'phoneEnabled'
|
|
22
26
|
]);
|
|
27
|
+
const PROFILE_KEYS = Object.freeze(['displayName', 'bio', 'avatarUrl', 'profileUrl']);
|
|
28
|
+
const PUBLISHER_KEYS = Object.freeze(['name', 'url', 'logoUrl']);
|
|
23
29
|
|
|
24
30
|
/*
|
|
25
31
|
* `layout` and `palette` have always been required. The rest were added later and are optional:
|
|
@@ -27,8 +33,6 @@ const CONTACT_KEYS = Object.freeze([
|
|
|
27
33
|
* back to its own defaults for anything absent. Present-but-unknown is still an error, because
|
|
28
34
|
* that is a writer expecting something the CSS will not do.
|
|
29
35
|
*/
|
|
30
|
-
const REQUIRED_DESIGN = Object.freeze(['layout', 'palette']);
|
|
31
|
-
|
|
32
36
|
/** A free colour, not a menu — but still a colour. */
|
|
33
37
|
const ACCENT = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
34
38
|
|
|
@@ -40,11 +44,17 @@ function validateImplementedDesign(config) {
|
|
|
40
44
|
if (config?.design == null) return;
|
|
41
45
|
for (const [field, allowed] of Object.entries(DESIGN_VALUES)) {
|
|
42
46
|
const value = config.design[field];
|
|
43
|
-
if (value === undefined
|
|
47
|
+
if (value === undefined) continue;
|
|
44
48
|
if (!allowed.includes(value)) {
|
|
45
49
|
throw new TypeError(`Unsupported design.${field}: ${value}`);
|
|
46
50
|
}
|
|
47
51
|
}
|
|
52
|
+
for (const [field, allowed] of Object.entries(LEGACY_DESIGN_VALUES)) {
|
|
53
|
+
const value = config.design[field];
|
|
54
|
+
if (value !== undefined && !allowed.includes(value)) {
|
|
55
|
+
throw new TypeError(`Unsupported legacy design.${field}: ${value}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
export function validatePerformanceBudgets(value) {
|
|
@@ -96,6 +106,60 @@ export function validateContact(value) {
|
|
|
96
106
|
});
|
|
97
107
|
}
|
|
98
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
|
+
|
|
99
163
|
export async function loadSiteConfiguration({
|
|
100
164
|
root = process.cwd(),
|
|
101
165
|
configPath = process.env.GALA_CONFIG_PATH ?? 'site.config.yml'
|
|
@@ -118,5 +182,11 @@ export async function loadSiteConfiguration({
|
|
|
118
182
|
validatePerformanceBudgets(config?.performance?.budgets);
|
|
119
183
|
config.statistics = validateStatistics(config.statistics);
|
|
120
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
|
+
};
|
|
121
191
|
return config;
|
|
122
192
|
}
|