@whittakertech/virgil 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/dist/generators/og-image.d.ts.map +1 -1
- package/dist/generators/og-image.js +23 -0
- package/dist/generators/og-image.js.map +1 -1
- package/dist/templates/og-default.html +13 -3
- package/package.json +1 -1
- package/src/generators/og-image.ts +32 -7
- package/src/templates/og-default.html +13 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.1] - 2025-12-30
|
|
11
|
+
### Added
|
|
12
|
+
- Allow optional image blocks in templates via presence-based guards (`if / endif`)
|
|
13
|
+
- Reconfigure template to conditionally render brand and product logos
|
|
14
|
+
|
|
15
|
+
## [0.1.0] - 2025-12-29
|
|
16
|
+
### Added
|
|
17
|
+
- Initial Virgil specification for build-time artifact generation
|
|
18
|
+
- Support for Open Graph image generation via headless Chromium (Playwright)
|
|
19
|
+
- Deterministic OG image rendering from HTML/CSS templates
|
|
20
|
+
- Cache-busted asset filenames for safe static deployment
|
|
21
|
+
- `virgil.manifest.json` output mapping stable IDs to generated artifacts
|
|
22
|
+
- Brand and product configuration support (name, color, version)
|
|
23
|
+
- Page metadata support for OG images (title, URL, description)
|
|
24
|
+
|
|
25
|
+
### Notes
|
|
26
|
+
- This release establishes Virgil’s core philosophy: deterministic, build-time artifact generation with no runtime dependencies
|
|
27
|
+
- Template rendering supports static variable substitution only (no logic or control flow)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"og-image.d.ts","sourceRoot":"","sources":["../../src/generators/og-image.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAK7E,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,aAAa,CAAC;IACvB,MAAM,EAAE,aAAa,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;
|
|
1
|
+
{"version":3,"file":"og-image.d.ts","sourceRoot":"","sources":["../../src/generators/og-image.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAK7E,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,aAAa,CAAC;IACvB,MAAM,EAAE,aAAa,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAWD;;GAEG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,cAAc,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAkDf;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAG1D"}
|
|
@@ -4,6 +4,15 @@ import { join, dirname } from 'path';
|
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { ensureParentDir } from '../utils/fs.js';
|
|
6
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
function getPath(obj, path) {
|
|
8
|
+
return path
|
|
9
|
+
.split('.')
|
|
10
|
+
.reduce((acc, key) => {
|
|
11
|
+
if (acc == null || typeof acc !== 'object')
|
|
12
|
+
return undefined;
|
|
13
|
+
return acc[key];
|
|
14
|
+
}, obj);
|
|
15
|
+
}
|
|
7
16
|
/**
|
|
8
17
|
* Generate OG image using Playwright
|
|
9
18
|
*/
|
|
@@ -11,11 +20,25 @@ export async function generateOGImage(ctx, outputPath) {
|
|
|
11
20
|
// Load template
|
|
12
21
|
const templatePath = ctx.templatePath || join(__dirname, '../templates/og-default.html');
|
|
13
22
|
let html = await readFile(templatePath, 'utf-8');
|
|
23
|
+
const renderCtx = {
|
|
24
|
+
brand: ctx.brand,
|
|
25
|
+
product: ctx.product,
|
|
26
|
+
page: ctx.output.page,
|
|
27
|
+
output: ctx.output
|
|
28
|
+
};
|
|
29
|
+
// Omit falsy blocks
|
|
30
|
+
const IF_BLOCK = /{{\s*if\s+([a-zA-Z0-9_.]+)\s*}}([\s\S]*?){{\s*endif\s*}}/g;
|
|
31
|
+
html = html.replace(IF_BLOCK, (_, condition, body) => {
|
|
32
|
+
const value = getPath(renderCtx, condition);
|
|
33
|
+
return value !== undefined && value !== null ? body : '';
|
|
34
|
+
});
|
|
14
35
|
// Replace placeholders
|
|
15
36
|
html = html
|
|
16
37
|
.replace(/{{brand.name}}/g, ctx.brand.name)
|
|
38
|
+
.replace(/{{brand.logo}}/g, ctx.brand.logo || '')
|
|
17
39
|
.replace(/{{brand.color}}/g, ctx.brand.color)
|
|
18
40
|
.replace(/{{product.name}}/g, ctx.product.name)
|
|
41
|
+
.replace(/{{product.logo}}/g, ctx.product.logo || '')
|
|
19
42
|
.replace(/{{product.version}}/g, ctx.product.version)
|
|
20
43
|
.replace(/{{page.title}}/g, ctx.output.page.title)
|
|
21
44
|
.replace(/{{page.url}}/g, ctx.output.page.url)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"og-image.js","sourceRoot":"","sources":["../../src/generators/og-image.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAa,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAS1D;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,GAAmB,EACnB,UAAkB;IAElB,gBAAgB;IAChB,MAAM,YAAY,GAAG,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,SAAS,EAAE,8BAA8B,CAAC,CAAC;IACzF,IAAI,IAAI,GAAG,MAAM,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IAEjD,uBAAuB;IACvB,IAAI,GAAG,IAAI;
|
|
1
|
+
{"version":3,"file":"og-image.js","sourceRoot":"","sources":["../../src/generators/og-image.ts"],"names":[],"mappings":"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"}
|
|
@@ -83,7 +83,13 @@
|
|
|
83
83
|
<body>
|
|
84
84
|
<div class="container">
|
|
85
85
|
<div class="header">
|
|
86
|
-
|
|
86
|
+
{{ if brand.logo }}
|
|
87
|
+
<div class="image">
|
|
88
|
+
<img src="{{brand.logo}}" alt="{{brand.name}} logo">
|
|
89
|
+
</div>
|
|
90
|
+
{{ endif }}
|
|
91
|
+
<div class="product">{{product.name}}</div>
|
|
92
|
+
<div class="version">v{{product.version}}</div>
|
|
87
93
|
</div>
|
|
88
94
|
|
|
89
95
|
<div class="content">
|
|
@@ -92,8 +98,12 @@
|
|
|
92
98
|
</div>
|
|
93
99
|
|
|
94
100
|
<div class="footer">
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
{{ if brand.logo }}
|
|
102
|
+
<div class="image">
|
|
103
|
+
<img src="{{brand.logo}}" alt="{{brand.name}} logo">
|
|
104
|
+
</div>
|
|
105
|
+
{{ endif }}
|
|
106
|
+
<div class="brand">{{brand.name}}</div>
|
|
97
107
|
</div>
|
|
98
108
|
</div>
|
|
99
109
|
</body>
|
package/package.json
CHANGED
|
@@ -14,6 +14,15 @@ export interface OGImageContext {
|
|
|
14
14
|
templatePath?: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function getPath(obj: unknown, path: string): unknown {
|
|
18
|
+
return path
|
|
19
|
+
.split('.')
|
|
20
|
+
.reduce((acc: any, key) => {
|
|
21
|
+
if (acc == null || typeof acc !== 'object') return undefined;
|
|
22
|
+
return acc[key];
|
|
23
|
+
}, obj);
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
/**
|
|
18
27
|
* Generate OG image using Playwright
|
|
19
28
|
*/
|
|
@@ -25,15 +34,31 @@ export async function generateOGImage(
|
|
|
25
34
|
const templatePath = ctx.templatePath || join(__dirname, '../templates/og-default.html');
|
|
26
35
|
let html = await readFile(templatePath, 'utf-8');
|
|
27
36
|
|
|
37
|
+
const renderCtx = {
|
|
38
|
+
brand: ctx.brand,
|
|
39
|
+
product: ctx.product,
|
|
40
|
+
page: ctx.output.page,
|
|
41
|
+
output: ctx.output
|
|
42
|
+
}
|
|
43
|
+
// Omit falsy blocks
|
|
44
|
+
const IF_BLOCK =
|
|
45
|
+
/{{\s*if\s+([a-zA-Z0-9_.]+)\s*}}([\s\S]*?){{\s*endif\s*}}/g;
|
|
46
|
+
html = html.replace(IF_BLOCK, (_, condition, body) => {
|
|
47
|
+
const value = getPath(renderCtx, condition);
|
|
48
|
+
return value !== undefined && value !== null ? body : '';
|
|
49
|
+
});
|
|
50
|
+
|
|
28
51
|
// Replace placeholders
|
|
29
52
|
html = html
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
53
|
+
.replace(/{{brand.name}}/g, ctx.brand.name)
|
|
54
|
+
.replace(/{{brand.logo}}/g, ctx.brand.logo || '')
|
|
55
|
+
.replace(/{{brand.color}}/g, ctx.brand.color)
|
|
56
|
+
.replace(/{{product.name}}/g, ctx.product.name)
|
|
57
|
+
.replace(/{{product.logo}}/g, ctx.product.logo || '')
|
|
58
|
+
.replace(/{{product.version}}/g, ctx.product.version)
|
|
59
|
+
.replace(/{{page.title}}/g, ctx.output.page.title)
|
|
60
|
+
.replace(/{{page.url}}/g, ctx.output.page.url)
|
|
61
|
+
.replace(/{{page.description}}/g, ctx.output.page.description || '');
|
|
37
62
|
|
|
38
63
|
// Launch headless browser
|
|
39
64
|
const browser = await chromium.launch();
|
|
@@ -83,7 +83,13 @@
|
|
|
83
83
|
<body>
|
|
84
84
|
<div class="container">
|
|
85
85
|
<div class="header">
|
|
86
|
-
|
|
86
|
+
{{ if brand.logo }}
|
|
87
|
+
<div class="image">
|
|
88
|
+
<img src="{{brand.logo}}" alt="{{brand.name}} logo">
|
|
89
|
+
</div>
|
|
90
|
+
{{ endif }}
|
|
91
|
+
<div class="product">{{product.name}}</div>
|
|
92
|
+
<div class="version">v{{product.version}}</div>
|
|
87
93
|
</div>
|
|
88
94
|
|
|
89
95
|
<div class="content">
|
|
@@ -92,8 +98,12 @@
|
|
|
92
98
|
</div>
|
|
93
99
|
|
|
94
100
|
<div class="footer">
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
{{ if brand.logo }}
|
|
102
|
+
<div class="image">
|
|
103
|
+
<img src="{{brand.logo}}" alt="{{brand.name}} logo">
|
|
104
|
+
</div>
|
|
105
|
+
{{ endif }}
|
|
106
|
+
<div class="brand">{{brand.name}}</div>
|
|
97
107
|
</div>
|
|
98
108
|
</div>
|
|
99
109
|
</body>
|