@apleasantview/eleventy-plugin-baseline 0.1.0-next.8
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/LICENSE +21 -0
- package/README.md +37 -0
- package/core/debug.js +20 -0
- package/core/filters/isString.js +3 -0
- package/core/filters/markdown.js +10 -0
- package/core/filters/related-posts.js +6 -0
- package/core/filters.js +9 -0
- package/core/globals/date.js +18 -0
- package/core/globals.js +6 -0
- package/core/helpers.js +139 -0
- package/core/logging.js +32 -0
- package/core/modules.js +22 -0
- package/core/shortcodes/image.js +120 -0
- package/core/shortcodes.js +3 -0
- package/eleventy.config.js +98 -0
- package/modules/assets-core/plugins/assets-core.js +81 -0
- package/modules/assets-esbuild/filters/inline-esbuild.js +21 -0
- package/modules/assets-esbuild/plugins/assets-esbuild.js +61 -0
- package/modules/assets-postcss/fallback/postcss.config.js +23 -0
- package/modules/assets-postcss/filters/inline-postcss.js +31 -0
- package/modules/assets-postcss/plugins/assets-postcss.js +62 -0
- package/modules/head-core/drivers/posthtml-head-elements.js +148 -0
- package/modules/head-core/plugins/head-core.js +60 -0
- package/modules/head-core/utils/head-utils.js +193 -0
- package/modules/multilang-core/plugins/multilang-core.js +35 -0
- package/modules/navigator-core/plugins/navigator-core.js +37 -0
- package/modules/navigator-core/templates/navigator-core.html +48 -0
- package/modules/sitemap-core/plugins/sitemap-core.js +65 -0
- package/modules/sitemap-core/templates/sitemap-core.html +25 -0
- package/modules/sitemap-core/templates/sitemap-index.html +15 -0
- package/package.json +51 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import * as esbuild from "esbuild";
|
|
3
|
+
import { resolveAssetsDir } from "../../../core/helpers.js";
|
|
4
|
+
import inlineESbuild from "../filters/inline-esbuild.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* assets-esbuild
|
|
8
|
+
*
|
|
9
|
+
* - Registers `js` as a template format and bundles/minifies `index.js` entries under the resolved assets `js` dir.
|
|
10
|
+
* - Registers `inlineESbuild` filter to inline arbitrary JS by bundling it with esbuild.
|
|
11
|
+
* - Filters the `all` collection to drop `11tydata.js` files (added by the `js` template format).
|
|
12
|
+
*
|
|
13
|
+
* Options: none (inherits global Baseline verbose only).
|
|
14
|
+
*/
|
|
15
|
+
/** @param {import("@11ty/eleventy").UserConfig} eleventyConfig */
|
|
16
|
+
export default function assetsESBuild(eleventyConfig) {
|
|
17
|
+
const { assetsDir } = resolveAssetsDir(
|
|
18
|
+
eleventyConfig.dir?.input || "./",
|
|
19
|
+
eleventyConfig.dir?.output || "./",
|
|
20
|
+
eleventyConfig.dir?.assets || "assets"
|
|
21
|
+
);
|
|
22
|
+
const jsDir = `${assetsDir}js/`;
|
|
23
|
+
|
|
24
|
+
eleventyConfig.addTemplateFormats("js");
|
|
25
|
+
|
|
26
|
+
eleventyConfig.addExtension("js", {
|
|
27
|
+
outputFileExtension: "js",
|
|
28
|
+
useLayouts: false,
|
|
29
|
+
compile: async function (_inputContent, inputPath) {
|
|
30
|
+
if (inputPath.includes('11tydata.js') || !inputPath.startsWith(jsDir) || path.basename(inputPath) !== "index.js") {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return async () => {
|
|
35
|
+
let result = await esbuild.build({
|
|
36
|
+
entryPoints: [inputPath],
|
|
37
|
+
bundle: true,
|
|
38
|
+
minify: true,
|
|
39
|
+
target: "es2020",
|
|
40
|
+
write: false
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return result.outputFiles[0].text;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Filter to inline a bundled entry.
|
|
49
|
+
eleventyConfig.addFilter("inlineESbuild", inlineESbuild);
|
|
50
|
+
|
|
51
|
+
// Override the default collection behavior. Adding js as template format and extension collects 11tydata.js files.
|
|
52
|
+
eleventyConfig.addCollection("all", function (collectionApi) {
|
|
53
|
+
return collectionApi.getAll().filter(item => {
|
|
54
|
+
// Skip 11tydata.js files
|
|
55
|
+
if (item.inputPath.endsWith('11tydata.js')) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import postcssImportExtGlob from "postcss-import-ext-glob";
|
|
2
|
+
import postcssImport from "postcss-import";
|
|
3
|
+
import postcssPresetEnv from "postcss-preset-env";
|
|
4
|
+
import cssnano from "cssnano"; // Import cssnano for minification
|
|
5
|
+
|
|
6
|
+
const isProd = process.env.ELEVENTY_ENV === "production" || false;
|
|
7
|
+
const productionPlugins = [];
|
|
8
|
+
|
|
9
|
+
if (isProd) {
|
|
10
|
+
productionPlugins.push(cssnano);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const config = {
|
|
14
|
+
plugins: [postcssImportExtGlob, postcssImport, postcssPresetEnv({
|
|
15
|
+
"browsers": [
|
|
16
|
+
"> 0.2% and not dead"
|
|
17
|
+
],
|
|
18
|
+
"preserve": true,
|
|
19
|
+
}), ...productionPlugins],
|
|
20
|
+
map: !isProd
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default config
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import postcss from "postcss";
|
|
3
|
+
import loadPostCSSConfig from "postcss-load-config";
|
|
4
|
+
import fallbackPostCSSConfig from "../fallback/postcss.config.js";
|
|
5
|
+
|
|
6
|
+
export default async function inlinePostCSS(cssFilePath) {
|
|
7
|
+
try {
|
|
8
|
+
let cssContent = await fs.readFile(cssFilePath, 'utf8');
|
|
9
|
+
|
|
10
|
+
let plugins;
|
|
11
|
+
let options;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
// Prefer the consuming project's PostCSS config (postcss.config.* or package.json#postcss).
|
|
15
|
+
({ plugins, options } = await loadPostCSSConfig({}, configRoot));
|
|
16
|
+
} catch (error) {
|
|
17
|
+
// If none is found, fall back to the bundled Baseline config to keep builds working.
|
|
18
|
+
({plugins, ...options } = fallbackPostCSSConfig);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let result = await postcss(plugins).process(cssContent, {
|
|
22
|
+
from: cssFilePath,
|
|
23
|
+
map: options?.map
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return `<style>${result.css}</style>`;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(error);
|
|
29
|
+
return `<style>/* Error processing CSS */</style>`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import postcss from "postcss";
|
|
3
|
+
import loadPostCSSConfig from "postcss-load-config";
|
|
4
|
+
import fallbackPostCSSConfig from "../fallback/postcss.config.js";
|
|
5
|
+
import inlinePostCSS from "../filters/inline-postcss.js";
|
|
6
|
+
import { resolveAssetsDir } from "../../../core/helpers.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* assets-postcss
|
|
10
|
+
*
|
|
11
|
+
* - Registers `css` as a template format and processes `index.css` entries under the resolved assets `css` dir with PostCSS.
|
|
12
|
+
* - Registers `inlinePostCSS` filter to inline arbitrary CSS by processing it with PostCSS.
|
|
13
|
+
* - No module-specific options (inherits global Baseline verbose only).
|
|
14
|
+
*/
|
|
15
|
+
/** @param {import("@11ty/eleventy").UserConfig} eleventyConfig */
|
|
16
|
+
export default function assetsPostCSS(eleventyConfig) {
|
|
17
|
+
const { assetsDir } = resolveAssetsDir(
|
|
18
|
+
eleventyConfig.dir?.input || "./",
|
|
19
|
+
eleventyConfig.dir?.output || "./",
|
|
20
|
+
eleventyConfig.dir?.assets || "assets"
|
|
21
|
+
);
|
|
22
|
+
const cssDir = `${assetsDir}css/`;
|
|
23
|
+
|
|
24
|
+
// Resolve user PostCSS config from the project root (cwd), not the Eleventy input dir.
|
|
25
|
+
const configRoot = process.cwd();
|
|
26
|
+
|
|
27
|
+
eleventyConfig.addTemplateFormats("css");
|
|
28
|
+
|
|
29
|
+
eleventyConfig.addExtension("css", {
|
|
30
|
+
outputFileExtension: "css",
|
|
31
|
+
useLayouts: false,
|
|
32
|
+
compile: async function (_inputContent, inputPath) {
|
|
33
|
+
if (!inputPath.startsWith(cssDir) || path.basename(inputPath) !== "index.css") {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return async () => {
|
|
38
|
+
let plugins;
|
|
39
|
+
let options;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Prefer the consuming project's PostCSS config (postcss.config.* or package.json#postcss).
|
|
43
|
+
({ plugins, options } = await loadPostCSSConfig({}, configRoot));
|
|
44
|
+
} catch (error) {
|
|
45
|
+
// If none is found, fall back to the bundled Baseline config to keep builds working.
|
|
46
|
+
({plugins, ...options } = fallbackPostCSSConfig);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const result = await postcss(plugins).process(_inputContent, {
|
|
50
|
+
from: inputPath,
|
|
51
|
+
map: options?.map,
|
|
52
|
+
...options
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return result.css;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Filter to inline a bundled entry.
|
|
61
|
+
eleventyConfig.addFilter("inlinePostCSS", inlinePostCSS);
|
|
62
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Based on posthtml-head-elements (MIT License).
|
|
2
|
+
// Original: https://github.com/posthtml/posthtml-head-elements
|
|
3
|
+
// Adapted for Baseline head-core.
|
|
4
|
+
|
|
5
|
+
// Based on posthtml-head-elements (MIT License).
|
|
6
|
+
// Original: https://github.com/posthtml/posthtml-head-elements
|
|
7
|
+
// Adapted for Baseline head-core.
|
|
8
|
+
|
|
9
|
+
import util from "node:util";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
|
|
14
|
+
function nonString(type, attrsArr) {
|
|
15
|
+
return attrsArr.map(function(attrs) {
|
|
16
|
+
return {tag: type, attrs: attrs};
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function nonArray(type, content) {
|
|
21
|
+
return {tag: type, content: [content]};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findElmType(type, objectData) {
|
|
25
|
+
|
|
26
|
+
var elementType = {
|
|
27
|
+
'meta': function() {
|
|
28
|
+
|
|
29
|
+
if (Array.isArray(objectData)) {
|
|
30
|
+
return nonString(type, objectData);
|
|
31
|
+
} else {
|
|
32
|
+
util.log('posthtml-head-elements: Please use the correct syntax for a meta element');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
},
|
|
36
|
+
'title': function() {
|
|
37
|
+
|
|
38
|
+
if (typeof objectData === 'string') {
|
|
39
|
+
return nonArray('title', objectData);
|
|
40
|
+
} else {
|
|
41
|
+
util.log('posthtml-head-elements: Please use the correct syntax for a title element');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
},
|
|
45
|
+
'link': function() {
|
|
46
|
+
|
|
47
|
+
if (Array.isArray(objectData)) {
|
|
48
|
+
return nonString(type, objectData);
|
|
49
|
+
} else {
|
|
50
|
+
util.log('posthtml-head-elements: Please use the correct syntax for a link element');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
},
|
|
54
|
+
'linkCanonical': function() {
|
|
55
|
+
if (Array.isArray(objectData)) {
|
|
56
|
+
return nonString('link', objectData);
|
|
57
|
+
} else {
|
|
58
|
+
util.log('posthtml-head-elements: Please use the correct syntax for a linkCanonical element');
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
'script': function() {
|
|
62
|
+
if (Array.isArray(objectData)) {
|
|
63
|
+
return objectData.map(function(entry) {
|
|
64
|
+
const { content, ...attrs } = entry || {};
|
|
65
|
+
return content !== undefined
|
|
66
|
+
? { tag: "script", attrs, content: [content] }
|
|
67
|
+
: { tag: "script", attrs };
|
|
68
|
+
});
|
|
69
|
+
} else {
|
|
70
|
+
util.log('posthtml-head-elements: Please use the correct syntax for a script element');
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
'style': function() {
|
|
74
|
+
if (Array.isArray(objectData)) {
|
|
75
|
+
return objectData.map(function(entry) {
|
|
76
|
+
const { content, ...attrs } = entry || {};
|
|
77
|
+
return content !== undefined
|
|
78
|
+
? { tag: "style", attrs, content: [content] }
|
|
79
|
+
: { tag: "style", attrs };
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
util.log('posthtml-head-elements: Please use the correct syntax for a style element');
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
'base': function() {
|
|
86
|
+
|
|
87
|
+
if (Array.isArray(objectData)) {
|
|
88
|
+
return nonString(type, objectData);
|
|
89
|
+
} else {
|
|
90
|
+
util.log('posthtml-head-elements: Please use the correct syntax for a base element');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
},
|
|
94
|
+
'default': function() {
|
|
95
|
+
util.log('posthtml-head-elements: Please make sure the HTML head type is correct');
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (type.indexOf('_') !== -1) {
|
|
100
|
+
type = type.substr(0, type.indexOf('_'));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (elementType[type]() || elementType['default']());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildNewTree(headElements, EOL) {
|
|
107
|
+
|
|
108
|
+
var newHeadElements = [];
|
|
109
|
+
|
|
110
|
+
Object.keys(headElements).forEach(function(value) {
|
|
111
|
+
|
|
112
|
+
newHeadElements.push(findElmType(value, headElements[value]));
|
|
113
|
+
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
function cnct(arr) {
|
|
117
|
+
return Array.prototype.concat.apply([], arr);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return cnct(cnct(newHeadElements).map(function(elem) {
|
|
121
|
+
return [elem, EOL];
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default function(options) {
|
|
126
|
+
options = options || {};
|
|
127
|
+
options.headElementsTag = options.headElementsTag || 'posthtml-head-elements';
|
|
128
|
+
|
|
129
|
+
if (!options.headElements) {
|
|
130
|
+
util.log('posthtml-head-elements: Don\'t forget to add a link to the JSON file containing the head elements to insert');
|
|
131
|
+
}
|
|
132
|
+
var jsonOne = typeof options.headElements !== 'string' ? options.headElements : require(options.headElements);
|
|
133
|
+
|
|
134
|
+
return function posthtmlHeadElements(tree) {
|
|
135
|
+
|
|
136
|
+
tree.match({tag: options.headElementsTag}, function() {
|
|
137
|
+
return {
|
|
138
|
+
tag: false, // delete this node, safe content
|
|
139
|
+
content: buildNewTree(jsonOne, options.EOL || '\n')
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return tree;
|
|
144
|
+
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
};
|
|
148
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import headElements from "../drivers/posthtml-head-elements.js";
|
|
2
|
+
import { getVerbose, logIfVerbose } from "../../../core/logging.js";
|
|
3
|
+
import { buildHead } from "../utils/head-utils.js";
|
|
4
|
+
|
|
5
|
+
/** @param {import("@11ty/eleventy").UserConfig} eleventyConfig */
|
|
6
|
+
export default function headCore(eleventyConfig, options = {}) {
|
|
7
|
+
const verbose = getVerbose(eleventyConfig) || options.verbose || false;
|
|
8
|
+
|
|
9
|
+
// Following options are not public.
|
|
10
|
+
const userKey = options.dirKey || "head";
|
|
11
|
+
const headElementsTag = options.headElementsTag || "baseline-head";
|
|
12
|
+
const eol = options.EOL || "\n";
|
|
13
|
+
const pathPrefix = options.pathPrefix ?? eleventyConfig?.pathPrefix ?? "";
|
|
14
|
+
const siteUrl = options.siteUrl;
|
|
15
|
+
const inputDir = eleventyConfig.dir?.input || ".";
|
|
16
|
+
|
|
17
|
+
let cachedContentMap = {};
|
|
18
|
+
eleventyConfig.on("eleventy.contentMap", ({ inputPathToUrl, urlToInputPath }) => {
|
|
19
|
+
cachedContentMap = { inputPathToUrl, urlToInputPath };
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
eleventyConfig.addGlobalData("eleventyComputed.page.head", () => {
|
|
23
|
+
return (data) =>
|
|
24
|
+
buildHead(data, {
|
|
25
|
+
userKey,
|
|
26
|
+
siteUrl,
|
|
27
|
+
pathPrefix,
|
|
28
|
+
contentMap: cachedContentMap,
|
|
29
|
+
pageUrlOverride: data?.page?.url,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
eleventyConfig.htmlTransformer.addPosthtmlPlugin("html", function (context) {
|
|
34
|
+
logIfVerbose(
|
|
35
|
+
verbose,
|
|
36
|
+
"head-core: injecting head elements for",
|
|
37
|
+
context?.page?.inputPath || context?.outputPath
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const headElementsSpec =
|
|
41
|
+
context?.page?.head ||
|
|
42
|
+
buildHead(context, {
|
|
43
|
+
userKey,
|
|
44
|
+
siteUrl,
|
|
45
|
+
pathPrefix,
|
|
46
|
+
contentMap: cachedContentMap,
|
|
47
|
+
pageUrlOverride: context?.page?.url,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const plugin = headElements({
|
|
51
|
+
headElements: headElementsSpec,
|
|
52
|
+
headElementsTag,
|
|
53
|
+
EOL: eol,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return async function asyncHead(tree) {
|
|
57
|
+
return plugin(tree);
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import Merge from "@11ty/eleventy-utils/src/Merge.js";
|
|
2
|
+
import { TemplatePath } from "@11ty/eleventy-utils";
|
|
3
|
+
|
|
4
|
+
const pick = (...values) => values.find((v) => v !== undefined && v !== null);
|
|
5
|
+
|
|
6
|
+
const normalizePathPrefix = (pathPrefix = "") => {
|
|
7
|
+
// Align with Eleventy’s normalizeUrlPath behavior
|
|
8
|
+
const normalized = TemplatePath.normalizeUrlPath("/", pathPrefix);
|
|
9
|
+
return normalized === "/" ? "" : normalized; // empty means root
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const isAbsoluteUrl = (url = "") =>
|
|
13
|
+
/^[a-z][a-z\d+\-.]*:\/\//i.test(url) || url.startsWith("//");
|
|
14
|
+
|
|
15
|
+
const absoluteUrl = (siteUrl, pathPrefix, url) => {
|
|
16
|
+
if (!url) return url;
|
|
17
|
+
if (isAbsoluteUrl(url)) return url;
|
|
18
|
+
const prefix = normalizePathPrefix(pathPrefix);
|
|
19
|
+
const joined = TemplatePath.normalizeUrlPath(prefix || "/", url);
|
|
20
|
+
return siteUrl ? `${siteUrl.replace(/\/+$/, "")}${joined}` : joined;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const mergeBaseHead = (site, user, page, title, description, noindex, url) => {
|
|
24
|
+
return Merge(
|
|
25
|
+
{},
|
|
26
|
+
{
|
|
27
|
+
title,
|
|
28
|
+
meta: [
|
|
29
|
+
{ charset: "UTF-8" },
|
|
30
|
+
{ name: "viewport", content: "width=device-width, initial-scale=1.0" },
|
|
31
|
+
{ name: "description", content: description },
|
|
32
|
+
{ name: "robots", content: noindex ? "noindex, nofollow" : "index, follow" },
|
|
33
|
+
],
|
|
34
|
+
link: [],
|
|
35
|
+
script: [],
|
|
36
|
+
style: [],
|
|
37
|
+
hreflang: [],
|
|
38
|
+
openGraph: {
|
|
39
|
+
"og:title": title,
|
|
40
|
+
"og:description": description,
|
|
41
|
+
"og:type": "website",
|
|
42
|
+
"og:url": url || "",
|
|
43
|
+
"og:image": "",
|
|
44
|
+
},
|
|
45
|
+
twitter: {
|
|
46
|
+
"twitter:card": "summary_large_image",
|
|
47
|
+
"twitter:title": title,
|
|
48
|
+
"twitter:description": description,
|
|
49
|
+
"twitter:image": "",
|
|
50
|
+
},
|
|
51
|
+
miscMeta: [],
|
|
52
|
+
structuredData: null,
|
|
53
|
+
},
|
|
54
|
+
user
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const resolveCanonical = (head, page, contentMap, env = {}) => {
|
|
59
|
+
const { siteUrl, pathPrefix = "", pageUrlOverride } = env;
|
|
60
|
+
const explicit = pick(head.canonical);
|
|
61
|
+
if (explicit) return absoluteUrl(siteUrl, pathPrefix, explicit);
|
|
62
|
+
|
|
63
|
+
const url = pick(
|
|
64
|
+
pageUrlOverride,
|
|
65
|
+
page?.url,
|
|
66
|
+
page?.inputPath && contentMap?.inputPathToUrl?.[page.inputPath]?.[0]
|
|
67
|
+
);
|
|
68
|
+
if (!url) return undefined;
|
|
69
|
+
|
|
70
|
+
return absoluteUrl(siteUrl, pathPrefix, url);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const dedupeMeta = (arr = []) => {
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
const out = [];
|
|
76
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
77
|
+
const m = arr[i];
|
|
78
|
+
const key =
|
|
79
|
+
m.charset
|
|
80
|
+
? "charset"
|
|
81
|
+
: m.name
|
|
82
|
+
? `name:${m.name}`
|
|
83
|
+
: m.property
|
|
84
|
+
? `prop:${m.property}`
|
|
85
|
+
: m["http-equiv"]
|
|
86
|
+
? `http:${m["http-equiv"]}`
|
|
87
|
+
: null;
|
|
88
|
+
if (!key || seen.has(key)) continue;
|
|
89
|
+
seen.add(key);
|
|
90
|
+
out.push(m);
|
|
91
|
+
}
|
|
92
|
+
return out.reverse();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const dedupeLink = (links = []) => {
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
const out = [];
|
|
98
|
+
for (let i = links.length - 1; i >= 0; i--) {
|
|
99
|
+
const l = links[i];
|
|
100
|
+
const key = l.rel && l.href ? `rel:${l.rel}|${l.href}` : null;
|
|
101
|
+
if (!key || seen.has(key)) continue;
|
|
102
|
+
seen.add(key);
|
|
103
|
+
out.push(l);
|
|
104
|
+
}
|
|
105
|
+
return out.reverse();
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const flattenHead = (head = {}, canonical) => {
|
|
109
|
+
// base/meta first, keep OG/Twitter last by placing them in a separate meta bucket
|
|
110
|
+
const baseMeta = dedupeMeta([...(head.meta || []), ...(head.miscMeta || [])]);
|
|
111
|
+
|
|
112
|
+
const socialMeta = dedupeMeta([
|
|
113
|
+
...(head.openGraph
|
|
114
|
+
? Object.entries(head.openGraph)
|
|
115
|
+
.filter(([, v]) => v)
|
|
116
|
+
.map(([k, v]) => ({ property: k, content: v }))
|
|
117
|
+
: []),
|
|
118
|
+
...(head.twitter
|
|
119
|
+
? Object.entries(head.twitter)
|
|
120
|
+
.filter(([, v]) => v)
|
|
121
|
+
.map(([k, v]) => ({ name: k, content: v }))
|
|
122
|
+
: []),
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
const style = [...(head.style || [])];
|
|
126
|
+
|
|
127
|
+
const linkCanonical = canonical ? [{ rel: "canonical", href: canonical }] : [];
|
|
128
|
+
const link = dedupeLink([...(head.link || []), ...(head.hreflang || [])].filter(Boolean));
|
|
129
|
+
|
|
130
|
+
const script = [...(head.script || [])];
|
|
131
|
+
if (head.structuredData) {
|
|
132
|
+
script.unshift({
|
|
133
|
+
type: "application/ld+json",
|
|
134
|
+
content: JSON.stringify(head.structuredData),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Key order matters for posthtml-head-elements.
|
|
139
|
+
return {
|
|
140
|
+
meta: baseMeta,
|
|
141
|
+
title: head.title || "",
|
|
142
|
+
linkCanonical,
|
|
143
|
+
style,
|
|
144
|
+
link,
|
|
145
|
+
script,
|
|
146
|
+
meta_social: socialMeta,
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const buildHead = (data = {}, env = {}) => {
|
|
151
|
+
const { userKey = "head", contentMap = {}, siteUrl, pathPrefix } = env;
|
|
152
|
+
const site = data.site || {};
|
|
153
|
+
const user = userKey ? data[userKey] || {} : {};
|
|
154
|
+
const page = data.page || {};
|
|
155
|
+
const resolvedSiteUrl =
|
|
156
|
+
siteUrl ||
|
|
157
|
+
site.url ||
|
|
158
|
+
process.env.URL ||
|
|
159
|
+
process.env.DEPLOY_URL ||
|
|
160
|
+
process.env.DEPLOY_PRIME_URL;
|
|
161
|
+
|
|
162
|
+
const siteTitle = site.title || "";
|
|
163
|
+
const pageTitle = pick(data.title, user.title, site.title, "");
|
|
164
|
+
const title =
|
|
165
|
+
siteTitle && pageTitle && siteTitle !== pageTitle
|
|
166
|
+
? `${pageTitle} | ${siteTitle}`
|
|
167
|
+
: pageTitle || siteTitle || "";
|
|
168
|
+
|
|
169
|
+
const description = pick(data.description, user.description, site.tagline, "");
|
|
170
|
+
const noindex = pick(page.noindex, user.noindex, site.noindex, false);
|
|
171
|
+
|
|
172
|
+
const canonical = resolveCanonical(
|
|
173
|
+
{ canonical: absoluteUrl(resolvedSiteUrl, pathPrefix, user.canonical) },
|
|
174
|
+
page,
|
|
175
|
+
contentMap,
|
|
176
|
+
{ ...env, siteUrl: resolvedSiteUrl }
|
|
177
|
+
);
|
|
178
|
+
const merged = mergeBaseHead(site, user, page, title, description, noindex, canonical);
|
|
179
|
+
return flattenHead(merged, canonical);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const buildHeadSpec = (context, contentMap, env = {}) => {
|
|
183
|
+
return buildHead(context, { ...env, contentMap });
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export {
|
|
187
|
+
pick,
|
|
188
|
+
resolveCanonical,
|
|
189
|
+
flattenHead,
|
|
190
|
+
buildHead,
|
|
191
|
+
buildHeadSpec,
|
|
192
|
+
absoluteUrl,
|
|
193
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { I18nPlugin } from "@11ty/eleventy";
|
|
2
|
+
|
|
3
|
+
/** @param { import("@11ty/eleventy/src/UserConfig.js").default } eleventyConfig */
|
|
4
|
+
export default function multilangCore(eleventyConfig, options = {}) {
|
|
5
|
+
const userOptions = {
|
|
6
|
+
defaultLanguage: "en",
|
|
7
|
+
languages: [],
|
|
8
|
+
...options
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
eleventyConfig.addPlugin(I18nPlugin, {
|
|
12
|
+
defaultLanguage: userOptions.defaultLanguage,
|
|
13
|
+
errorMode: "allow-fallback"
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Add translations collection
|
|
17
|
+
eleventyConfig.addCollection("translations", function (collection) {
|
|
18
|
+
const translations = {};
|
|
19
|
+
for (const page of collection.getAll()) {
|
|
20
|
+
const translationKey = page.data.translationKey;
|
|
21
|
+
if (!translationKey) continue;
|
|
22
|
+
const lang = page.data.lang || page.data.language || userOptions.defaultLanguage;
|
|
23
|
+
if (!lang) continue;
|
|
24
|
+
if (!translations[translationKey]) translations[translationKey] = {};
|
|
25
|
+
translations[translationKey][lang] = {
|
|
26
|
+
title: page.data.title,
|
|
27
|
+
url: page.url,
|
|
28
|
+
lang,
|
|
29
|
+
isDefault: lang === userOptions.defaultLanguage,
|
|
30
|
+
data: page.data
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return translations;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
|
|
8
|
+
/** @param {import("@11ty/eleventy").UserConfig} eleventyConfig */
|
|
9
|
+
export default function navigatorCore(eleventyConfig, options = {}) {
|
|
10
|
+
const raw = options.enableNavigatorTemplate;
|
|
11
|
+
const [enableNavigatorTemplate, inspectorDepth] = Array.isArray(raw)
|
|
12
|
+
? [raw[0], raw[1]]
|
|
13
|
+
: [raw, undefined];
|
|
14
|
+
|
|
15
|
+
const userOptions = {
|
|
16
|
+
...options,
|
|
17
|
+
enableNavigatorTemplate: enableNavigatorTemplate ?? false,
|
|
18
|
+
inspectorDepth: inspectorDepth ?? 2
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
eleventyConfig.addNunjucksGlobal("_navigator", function () { return this; });
|
|
22
|
+
eleventyConfig.addNunjucksGlobal("_context", function () { return this.ctx; });
|
|
23
|
+
|
|
24
|
+
if (userOptions.enableNavigatorTemplate) {
|
|
25
|
+
// Read virtual template synchronously; Nunjucks pipeline here is sync-only.
|
|
26
|
+
const templatePath = path.join(__dirname, "../templates/navigator-core.html");
|
|
27
|
+
const virtualTemplateContent = fs.readFileSync(templatePath, "utf-8");
|
|
28
|
+
eleventyConfig.addTemplate("navigator-core.html", virtualTemplateContent, {
|
|
29
|
+
permalink: "/navigator-core.html",
|
|
30
|
+
title: "Navigator Core",
|
|
31
|
+
description: "",
|
|
32
|
+
layout: null,
|
|
33
|
+
eleventyExcludeFromCollections: true,
|
|
34
|
+
inspectorDepth: userOptions.inspectorDepth
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|