Skip to content

Commit a71e1ea

Browse files
committed
feat(hugo): provide site string functions in the visual editor
`{{ site.BaseURL }}`, `{{ site.Title }}`, and `{{ site.Copyright }}` will now work in the visual editor, with the correct values from your Hugo build.
1 parent 798e7ed commit a71e1ea

File tree

14 files changed

+140
-63
lines changed

14 files changed

+140
-63
lines changed

hugo/v2/core/bookshop_bindings.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
Expects one argument, a string to use as the data binding in CloudCannon
55
*/}}
66

7-
{{- (printf `<!--bookshop-live meta(version: "2.6.1")-->`) | safeHTML }}
7+
{{- (printf `<!--bookshop-live meta(version: "2.6.1" baseurl: "%s" copyright: "%s" title: "%s")-->` site.BaseURL site.Copyright site.Title) | safeHTML }}
88
{{ $is_string := eq "string" (printf "%T" .) -}}
99

1010
{{- if $is_string -}}

javascript-modules/builder/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@
3434
"engines": {
3535
"node": ">=14.16"
3636
}
37-
}
37+
}

javascript-modules/engines/eleventy-engine/lib/engine.js

+11-7
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export class Engine {
2424
this.plugins.push(unbind, slug, loopContext);
2525

2626
this.meta = {};
27+
this.info = {};
2728
this.plugins.push(urlFilterBuilder(this.meta));
2829

2930
this.initializeLiquid();
@@ -101,19 +102,23 @@ export class Engine {
101102
return false;
102103
}
103104

104-
injectInfo(props, info = {}, meta = {}) {
105+
injectInfo(props) {
105106
return {
106-
...(info.collections || {}),
107-
...(info.data || {}),
107+
...(this.info.collections || {}),
108+
...(this.info.data || {}),
108109
...props,
109110
};
110111
}
111112

112-
async updateMeta(meta = {}) {
113+
async storeMeta(meta = {}) {
113114
this.meta.pathPrefix = meta.pathPrefix ? await this.eval(meta.pathPrefix) : undefined;
114115
}
115116

116-
async render(target, name, props, globals, cloudcannonInfo, meta, logger) {
117+
async storeInfo(info = {}) {
118+
this.info = info;
119+
}
120+
121+
async render(target, name, props, globals, logger) {
117122
let source = this.getComponent(name);
118123
// TODO: Remove the below check and update the live comments to denote shared
119124
if (!source) source = this.getShared(name);
@@ -125,8 +130,7 @@ export class Engine {
125130
source = translateLiquid(source);
126131
logger?.log?.(`Rewritten the template for ${name}`);
127132
if (!globals || typeof globals !== "object") globals = {};
128-
props = this.injectInfo({ ...globals, ...props }, cloudcannonInfo, meta);
129-
await this.updateMeta(meta);
133+
props = this.injectInfo({ ...globals, ...props });
130134
logger?.log?.(`Rendered ${name}`);
131135
target.innerHTML = await this.liquid.parseAndRender(source || "", props);
132136
}

javascript-modules/engines/hugo-engine/hugo-renderer/main.go

+7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ func main() {
1212
js.Global().Set("renderHugo", js.FuncOf(renderHugo))
1313
js.Global().Set("loadHugoBookshopPartials", js.FuncOf(loadHugoBookshopPartials))
1414
js.Global().Set("loadHugoBookshopData", js.FuncOf(loadHugoBookshopData))
15+
js.Global().Set("loadHugoBookshopMeta", js.FuncOf(loadHugoBookshopMeta))
1516
<-c
1617
}
1718

@@ -33,3 +34,9 @@ func loadHugoBookshopData(this js.Value, args []js.Value) interface{} {
3334

3435
return library.LoadHugoBookshopData(bookshopData)
3536
}
37+
38+
func loadHugoBookshopMeta(this js.Value, args []js.Value) interface{} {
39+
bookshopMeta := args[0].String()
40+
41+
return library.LoadHugoBookshopMeta(bookshopMeta)
42+
}

javascript-modules/engines/hugo-engine/hugo-renderer/tpl/bookshop_library/bookshop_data.go

+21
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,24 @@ func LoadHugoBookshopData(data string) interface{} {
3030
func RetrieveHugoBookshopData() *bookshopData {
3131
return &bookshopDataStorage
3232
}
33+
34+
var bookshopMetaStorage map[string]interface{}
35+
36+
// loadHugoBookshopMeta takes any data that hugo/bookshop
37+
// injected into the template and stashes it all away
38+
// on this side of the wasm boundary, for the site
39+
// function to use.
40+
func LoadHugoBookshopMeta(data string) interface{} {
41+
err := json.Unmarshal([]byte(data), &bookshopMetaStorage)
42+
if err != nil {
43+
buf := bytes.NewBufferString(fmt.Sprintf("bad json unmarshal: %s", err.Error()))
44+
return buf.String()
45+
}
46+
47+
buf := bytes.NewBufferString("loaded Bookshop site data")
48+
return buf.String()
49+
}
50+
51+
func RetrieveHugoBookshopMeta() map[string]interface{} {
52+
return bookshopMetaStorage
53+
}

javascript-modules/engines/hugo-engine/hugo-renderer/tpl/site/bookshop_site.go

+21
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,24 @@ func (ns *Namespace) Data() (map[string]interface{}, error) {
3636

3737
return data.Data, nil
3838
}
39+
40+
// site.Title should have been stashed inside a Bookshop meta comment
41+
func (ns *Namespace) Title() (interface{}, error) {
42+
meta := library.RetrieveHugoBookshopMeta()
43+
44+
return meta["title"], nil
45+
}
46+
47+
// site.Copyright should have been stashed inside a Bookshop meta comment
48+
func (ns *Namespace) Copyright() (interface{}, error) {
49+
meta := library.RetrieveHugoBookshopMeta()
50+
51+
return meta["copyright"], nil
52+
}
53+
54+
// site.BaseURL should have been stashed inside a Bookshop meta comment
55+
func (ns *Namespace) BaseURL() (interface{}, error) {
56+
meta := library.RetrieveHugoBookshopMeta()
57+
58+
return meta["baseurl"], nil
59+
}

javascript-modules/engines/hugo-engine/lib/engine.js

+15-3
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,25 @@ export class Engine {
120120
};
121121
}
122122

123-
async render(target, name, props, globals, cloudcannonInfo, meta, logger) {
123+
async storeMeta(meta = {}) {
124+
while (!window.loadHugoBookshopMeta) {
125+
await sleep(100);
126+
};
127+
window.loadHugoBookshopMeta(JSON.stringify(meta));
128+
}
129+
130+
async storeInfo(info = {}) {
131+
while (!window.loadHugoBookshopData) {
132+
await sleep(100);
133+
};
134+
window.loadHugoBookshopData(JSON.stringify(info));
135+
}
136+
137+
async render(target, name, props, globals, logger) {
124138
while (!window.renderHugo) {
125139
logger?.log?.(`Waiting for the Hugo WASM to be available...`);
126140
await sleep(100);
127141
};
128-
logger?.log?.(`Moving cloudcannonInfo across the WASM boundary`);
129-
window.loadHugoBookshopData(JSON.stringify(cloudcannonInfo));
130142

131143
let source = this.getComponent(name);
132144
// TODO: Remove the below check and update the live comments to denote shared

javascript-modules/engines/jekyll-engine/lib/engine.js

+14-9
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export class Engine {
3131
this.plugins.push(jsonify, slugify, unbind, emulateJekyll, local, liquidHighlight, loop_context, markdownify);
3232

3333
this.meta = {};
34+
this.info = {};
3435
this.plugins.push(relativeUrlFilterBuilder(this.meta));
3536

3637
this.initializeLiquid();
@@ -119,23 +120,28 @@ export class Engine {
119120
};
120121
}
121122

122-
injectInfo(props, info = {}, meta = {}) {
123+
injectInfo(props) {
123124
return {
124125
site: {
125-
...(info.collections || {}),
126-
data: (info.data || {}),
127-
baseurl: meta.baseurl || "",
128-
title: meta.title || "",
126+
...(this.info.collections || {}),
127+
data: (this.info.data || {}),
128+
baseurl: this.meta.baseurl || "",
129+
title: this.meta.title || "",
129130
},
130131
...props,
131132
};
132133
}
133134

134-
async updateMeta(meta = {}) {
135+
async storeMeta(meta = {}) {
135136
this.meta.baseurl = meta.baseurl ? await this.eval(meta.baseurl) : undefined;
137+
this.meta.title = meta.title ? await this.eval(meta.title) : undefined;
136138
}
137139

138-
async render(target, name, props, globals, cloudcannonInfo, meta, logger) {
140+
async storeInfo(info = {}) {
141+
this.info = info;
142+
}
143+
144+
async render(target, name, props, globals, logger) {
139145
let source = this.getComponent(name);
140146
// TODO: Remove the below check and update the live comments to denote shared
141147
if (!source) source = this.getShared(name);
@@ -147,8 +153,7 @@ export class Engine {
147153
source = translateLiquid(source, {});
148154
logger?.log?.(`Rewritten the template for ${name}`);
149155
if (!globals || typeof globals !== "object") globals = {};
150-
props = this.injectInfo({ ...globals, include: props }, cloudcannonInfo, meta);
151-
await this.updateMeta(meta);
156+
props = this.injectInfo({ ...globals, include: props });
152157
logger?.log?.(`Rendered ${name}`);
153158
target.innerHTML = await this.liquid.parseAndRender(source || "", props);
154159
}

javascript-modules/integration-tests/features/hugo/hugo_bookshop_live.feature

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Feature: Hugo Bookshop CloudCannon Integration
5151
And stdout should contain "Total in"
5252
And site/public/index.html should contain the text:
5353
"""
54-
<!--bookshop-live meta(version: "[version]")-->
54+
<!--bookshop-live meta(version: "[version]" baseurl: "https://bookshop.build/" copyright: "🎉" title: "Hugo Bookshop Cucumber")-->
5555
<!--bookshop-live name(__bookshop__subsequent) params(.: .Params)-->
5656
<!--bookshop-live name(page)-->
5757

javascript-modules/integration-tests/features/hugo/live_editing/hugo_bookshop_live_data.feature

+34-33
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,6 @@ Feature: Hugo Bookshop CloudCannon Live Editing Site Data
1414
go.mod from starters/hugo/site.go.mod
1515
config.toml from starters/hugo/site.config.toml
1616
"""
17-
18-
Scenario: Bookshop live renders website data
19-
Given a site/cloudcannon.config.yml file containing:
20-
"""
21-
data_config: true
22-
"""
23-
Given a site/data/cat.yml file containing:
24-
"""
25-
name: Cheeka
26-
"""
27-
* a component-lib/components/cat/cat.hugo.html file containing:
28-
"""
29-
<h1>{{ if .show }}{{ site.Data.cat.name }}{{ end }}</h1>
30-
"""
3117
* [front_matter]:
3218
"""
3319
show: false
@@ -43,10 +29,24 @@ Feature: Hugo Bookshop CloudCannon Live Editing Site Data
4329
<html>
4430
<body>
4531
{{ partial "bookshop_bindings" `(dict "show" .Params.show)` }}
46-
{{ partial "bookshop" (slice "cat" (dict "show" .Params.show)) }}
32+
{{ partial "bookshop" (slice "block" (dict "show" .Params.show)) }}
4733
</body>
4834
</html>
4935
"""
36+
37+
Scenario: Bookshop live renders website data
38+
Given a site/cloudcannon.config.yml file containing:
39+
"""
40+
data_config: true
41+
"""
42+
Given a site/data/cat.yml file containing:
43+
"""
44+
name: Cheeka
45+
"""
46+
* a component-lib/components/block/block.hugo.html file containing:
47+
"""
48+
<h1>{{ if .show }}{{ site.Data.cat.name }}{{ end }}</h1>
49+
"""
5050
* 🌐 I have loaded my site in CloudCannon
5151
When 🌐 CloudCannon pushes new yaml:
5252
"""
@@ -74,32 +74,32 @@ Feature: Hugo Bookshop CloudCannon Live Editing Site Data
7474
useful: yes
7575
messy: yes
7676
"""
77-
* a component-lib/components/cat/cat.hugo.html file containing:
77+
* a component-lib/components/block/block.hugo.html file containing:
7878
"""
7979
{{ if .show }}
8080
{{ range site.Data.cats }}
8181
<p>{{ .name }} — {{ .useful }}/{{ .messy }}</p>
8282
{{ end }}
8383
{{ end }}
8484
"""
85-
* [front_matter]:
86-
"""
87-
show: false
88-
"""
89-
* a site/content/_index.md file containing:
85+
* 🌐 I have loaded my site in CloudCannon
86+
When 🌐 CloudCannon pushes new yaml:
9087
"""
91-
---
92-
[front_matter]
93-
---
88+
show: true
9489
"""
95-
* a site/layouts/index.html file containing:
90+
Then 🌐 There should be no errors
91+
* 🌐 There should be no logs
92+
* 🌐 The selector p:nth-of-type(1) should contain "Cheeka — no/no"
93+
* 🌐 The selector p:nth-of-type(2) should contain "Smudge — yes/yes"
94+
95+
Scenario: Bookshop live renders special website config
96+
Given a component-lib/components/block/block.hugo.html file containing:
9697
"""
97-
<html>
98-
<body>
99-
{{ partial "bookshop_bindings" `(dict "show" .Params.show)` }}
100-
{{ partial "bookshop" (slice "cat" (dict "show" .Params.show)) }}
101-
</body>
102-
</html>
98+
{{ if .show }}
99+
<h1>{{ site.BaseURL }}</h1>
100+
<h2>{{ site.Copyright }}</h2>
101+
<h3>{{ site.Title }}</h3>
102+
{{ end }}
103103
"""
104104
* 🌐 I have loaded my site in CloudCannon
105105
When 🌐 CloudCannon pushes new yaml:
@@ -108,5 +108,6 @@ Feature: Hugo Bookshop CloudCannon Live Editing Site Data
108108
"""
109109
Then 🌐 There should be no errors
110110
* 🌐 There should be no logs
111-
* 🌐 The selector p:nth-of-type(1) should contain "Cheeka — no/no"
112-
* 🌐 The selector p:nth-of-type(2) should contain "Smudge — yes/yes"
111+
* 🌐 The selector h1 should contain "https://bookshop.build/"
112+
* 🌐 The selector h2 should contain "🎉"
113+
* 🌐 The selector h3 should contain "Hugo Bookshop Cucumber"

javascript-modules/integration-tests/features/jekyll/live_editing/jekyll_bookshop_live_data.feature

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,4 @@ Feature: Jekyll Bookshop CloudCannon Live Editing Site Data
7979
* 🌐 There should be no logs
8080
* 🌐 The selector h1 should contain "/documentation"
8181
* 🌐 The selector h2 should contain "/documentation/home"
82-
* 🌐 The selector h3 should contain "My Site"
82+
* 🌐 The selector h3 should contain "My Site"

javascript-modules/integration-tests/support/starters/hugo/site.config.toml

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
baseURL = "http://example.org/"
1+
baseURL = "https://bookshop.build/"
22
languageCode = "en-us"
3-
title = "Hugo Bookshop Starter"
3+
title = "Hugo Bookshop Cucumber"
4+
copyright = "🎉"
45

56
[module]
67
replacements = "bookshop.test/components -> ../../component-lib,github.com/cloudcannon/bookshop/hugo/v2 -> ../../../../../../hugo/v2/"

javascript-modules/live/lib/app/core.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ const evaluateTemplate = async (liveInstance, documentNode, parentPathStack, tem
130130
);
131131
}
132132
}
133+
134+
await liveInstance.storeMeta(meta);
133135
}
134136

135137
for (const [name, identifier] of parseParams(liveTag?.context)) {
@@ -233,7 +235,6 @@ const evaluateTemplate = async (liveInstance, documentNode, parentPathStack, tem
233235
params,
234236
stashedNodes,
235237
depth: stack.length - 1,
236-
meta,
237238
});
238239
stashedParams = [];
239240
stashedNodes = [];
@@ -260,7 +261,7 @@ export const renderComponentUpdates = async (liveInstance, documentNode, logger)
260261
const vDom = document.implementation.createHTMLDocument();
261262
const updates = []; // Rendered elements and their DOM locations
262263

263-
const templateBlockHandler = async ({ startNode, endNode, name, scope, pathStack, depth, stashedNodes, meta }, logger) => {
264+
const templateBlockHandler = async ({ startNode, endNode, name, scope, pathStack, depth, stashedNodes }, logger) => {
264265
// We only need to render the outermost component
265266
logger?.log?.(`Received a template block to render for ${name}`);
266267
if (depth) {
@@ -282,7 +283,6 @@ export const renderComponentUpdates = async (liveInstance, documentNode, logger)
282283
await liveInstance.renderElement(
283284
name,
284285
scope,
285-
meta,
286286
output,
287287
logger?.nested?.(),
288288
)

0 commit comments

Comments
 (0)