Skip to content

Commit 75a9765

Browse files
authored
feat(hugo): core hugo component support (#60)
* feat(hugo): hugo component rendering module * feat(hugo): hugo scss pipeline integration * test(hugo): building out hugo integration tests * test: add hugo to github action * docs: initial hugo docs
1 parent 010caf0 commit 75a9765

33 files changed

+864
-4
lines changed

.github/workflows/integration-test.yml

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ jobs:
1919

2020
steps:
2121
- uses: actions/checkout@v2
22+
- name: Use Hugo 0.89
23+
uses: peaceiris/actions-hugo@v2
24+
with:
25+
hugo-version: '0.89.0'
26+
extended: true
2227
- name: Use Node.js 14.x
2328
uses: actions/setup-node@v2
2429
with:

guides/hugo.adoc

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
= Hugo Bookshop
2+
ifdef::env-github[]
3+
:tip-caption: :bulb:
4+
:note-caption: :information_source:
5+
:important-caption: :heavy_exclamation_mark:
6+
:caution-caption: :fire:
7+
:warning-caption: :warning:
8+
endif::[]
9+
:toc:
10+
:toc-placement!:
11+
12+
toc::[]
13+
14+
IMPORTANT: Hugo support should be treated as a semi-stable beta
15+
16+
== Quick start
17+
To jump right into using Bookshop on a Hugo site, check out the link:https://github.com/CloudCannon/hugo-bookshop-starter[Hugo Bookshop Starter]
18+
19+
== Hugo Configuration
20+
21+
To use Bookshop with Hugo, the primary dependency is the `bookshop/hugo` module. This provides the bookshop partials needed to render components on your website. This import should be placed in your **site** config.toml.
22+
23+
.*site/config.toml*
24+
```toml
25+
[[module.imports]]
26+
path = 'github.com/cloudcannon/bookshop/hugo/v2'
27+
```
28+
29+
The best workflow is then to create a new Hugo module to house your components. This can then be used across multiple websites using the Hugo module system.
30+
31+
.*bash*
32+
```bash
33+
hugo mod init github.com/example/components
34+
```
35+
36+
The file structure within this components module is described in the link:conventions.adoc[Conventions Guide]
37+
38+
The last piece of config is adding the following mounts to the `config.toml` in your **components** module.
39+
40+
.*components/config.toml*
41+
```toml
42+
[[module.mounts]]
43+
source = "."
44+
target = "layouts/partials/bookshop"
45+
46+
[[module.mounts]]
47+
source = "."
48+
target = "assets/bookshop"
49+
```
50+
51+
These mounts allow the `bookshop/hugo` module to locate your components.
52+
53+
See the link:https://github.com/CloudCannon/hugo-bookshop-starter[Hugo Bookshop Starter] for an example of how all of the above looks in practice.
54+
55+
== Writing components
56+
57+
Let's look at an example `button.hugo.html` file.
58+
```
59+
components/
60+
└─ button/
61+
└─ button.hugo.html
62+
```
63+
These files are namespaced for the static site generator when the filetype is ambiguous, which is why the file is `button.hugo.html` and not `button.html`. Beyond the naming convention, these files are what you would expect when working with Hugo. Our `button.hugo.html` file might look like:
64+
```go
65+
<a class="c-button" href="{{ .link_url }}">{{ .text }}</a>
66+
```
67+
This looks like a normal Hugo include because... it is. While Bookshop provides developer tooling, the job of the `bookshop/hugo` module is to tell Hugo where to find component files. Loading and parsing these files goes through the normal Hugo partial flow.
68+
69+
TIP: The `@bookshop/init` package helps create component structures for you. Running `npx @bookshop/init --component button` would create the button structure needed for Hugo.
70+
71+
== Using components
72+
73+
To use components directly in a template, use the `bookshop` partial with the `components` syntax.
74+
75+
.*index.html*
76+
```html
77+
...
78+
<div class="hero">
79+
{{ partial "bookshop" (dict "component" "hero" "title" .Params.title "image" .Params.image) }}
80+
{{ partial "bookshop" (dict "component" "button" "label" .Params.cta_text "link_url" .Params.cta_url) }}
81+
</div>
82+
...
83+
```
84+
85+
This tag expects a dict containing a `component` field that references the Bookshop key of a component, as well as any fields to pass to the partial.
86+
87+
To render a list of components, you can pass a structure or an array of structures to the `bookshop` partial:
88+
89+
.*index.html*
90+
```html
91+
---
92+
components:
93+
- _bookshop_name: hero
94+
title: "Hello World"
95+
image: /image.png
96+
- _bookshop_name: button
97+
cta_text: "Get Started"
98+
link_url: /
99+
---
100+
<div class="hero">
101+
{{ partial "bookshop" .Params.components }}
102+
</div>
103+
```
104+
105+
This tag expects either an object containing a `_bookshop_name` key, or an array of objects containing `_bookshop_name` keys.
106+
107+
TIP: _The structures generated by Bookshop for CloudCannon include the `_bookshop_name` field for you. If you're not using structures you will need to add the `_bookshop_name` key by hand_
108+
109+
== Using Bookshop Partials
110+
111+
Bookshop partials can be placed in the `shared/hugo` directory. i.e:
112+
```text
113+
component-library/
114+
├─ components/
115+
└─ shared/
116+
└─ hugo/
117+
└─ helper.hugo.html
118+
```
119+
120+
This can then be included using the `bookshop` partial with the `partial` syntax:
121+
```html
122+
{{ partial "bookshop" (dict "partial" "helper" "lorem" "ipsum") }}
123+
```
124+
125+
This tag expects a dict containing a `partial` field that references the Bookshop key of a partial, as well as any fields to pass to the partial.
126+
127+
This is otherwise a standard Hugo partial, with the extra feature that it can be used anywhere within your Hugo site _or_ your components.
128+
129+
== Importing styles
130+
131+
To import Bookshop styles in Hugo, the plugin provides a `bookshop_scss` partial, which returns a slice of all SCSS resources in your bookshop. This can then be used as such:
132+
133+
.*baseof.html*
134+
```html
135+
{{ $bookshop_scss_files := partial "bookshop_scss" . }}
136+
{{ $scss := $bookshop_scss_files | resources.Concat "css/bookshop.css" | resources.ToCSS | resources.Minify | resources.Fingerprint }}
137+
<link rel="stylesheet" href="{{ $scss.Permalink }}">
138+
```

hugo/v2/config.toml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[module]
2+
hugoVersion.extended = true
3+
hugoVersion.min = "0.86.1"
4+
5+
[[module.mounts]]
6+
source = "core"
7+
target = "layouts/partials/_bookshop"
8+
9+
[[module.mounts]]
10+
source = "core/bookshop.html"
11+
target = "layouts/partials/bookshop.html"
12+
13+
[[module.mounts]]
14+
source = "core/bookshop_scss.html"
15+
target = "layouts/partials/bookshop_scss.html"

hugo/v2/core/bookshop.html

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{{/*
2+
Renders a Bookshop component, or an array of Bookshop components.
3+
4+
To render a single structure object, or an array of structure objects,
5+
the object or slice is expected to be passed in directly.
6+
7+
To render a single component:
8+
{
9+
component: String, # The bookshop name of a component to render
10+
... # any data to pass to the component
11+
}
12+
13+
To render a single partial:
14+
{
15+
partial: String, # The bookshop name of a partial to render
16+
... # any data to pass to the include
17+
}
18+
*/}}
19+
20+
{{- $is_structures := reflect.IsSlice . -}}
21+
{{- $is_component := false -}}
22+
{{- $is_partial := false -}}
23+
24+
{{- if reflect.IsMap . -}}
25+
{{- $is_structures = isset . "_bookshop_name" -}}
26+
{{- $is_component = isset . "component" -}}
27+
{{- $is_partial = isset . "partial" -}}
28+
{{- end -}}
29+
30+
{{- if $is_structures -}}
31+
{{- partial "_bookshop/helpers/render_direct_structures" . -}}
32+
{{- else if $is_component -}}
33+
{{- $component_params := dict "_bookshop_name" .component -}}
34+
{{- $component := merge . $component_params -}}
35+
{{- partial "_bookshop/helpers/component" (dict "component" $component) -}}
36+
{{- else if $is_partial -}}
37+
{{- $partial_params := dict "_bookshop_name" .partial -}}
38+
{{- $partial := merge . $partial_params -}}
39+
{{- partial "_bookshop/helpers/partial" (dict "partial" $partial) -}}
40+
{{- else -}}
41+
{{- partial "_bookshop/errors/bad_bookshop_tag" -}}
42+
{{- end -}}

hugo/v2/core/bookshop_scss.html

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{{/*
2+
Returns a slice of all Bookshop SCSS resources
3+
*/}}
4+
5+
{{ $components := resources.Match "bookshop/components/**.scss" }}
6+
{{ $shared := resources.Match "bookshop/shared/styles/**.scss" }}
7+
{{ $bookshop_scss := $shared | append $components }}
8+
9+
{{ return $bookshop_scss }}
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{{/*
2+
Prints examples of correct usage of the bookshop tag options
3+
*/}}
4+
5+
{{- $message := "Unknown Bookshop tag was used — the following Bookshop tag formats are valid:" -}}
6+
7+
{{- $structures_example := "{{ partial \"bookshop\" .Params.path.to.structures }}" -}}
8+
{{- $structures_message := printf " Render components from front matter:\n %s" $structures_example -}}
9+
10+
{{- $component_example := "{{ partial \"bookshop\" (dict \"component\" \"button\" \"text\" .button.text) }}" -}}
11+
{{- $component_message := printf " Render a \"button\" component with data:\n %s" $component_example -}}
12+
13+
{{- $include_example := "{{ partial \"bookshop\" (dict \"include\" \"tag\" \"message\" \"Hello World\") }}" -}}
14+
{{- $include_message := printf " Render a \"tag\" include with data:\n %s" $include_example -}}
15+
16+
{{- partial "_bookshop/errors/err" (printf "%s\n\n%s\n%s\n%s" $message $structures_message $component_message $include_message) -}}
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{{/*
2+
You cannot use the index function on a struct,
3+
so we cannot dig into it to find a value.
4+
This block provides a helpful suggestion
5+
on how to rearrange the given dict to work with Bookshop.
6+
7+
Expects a dict:
8+
{
9+
render: String, # The "dot.notation" string we wanted to dig for
10+
root_type: String # The type of the object we were asked to dig into
11+
}
12+
*/}}
13+
{{- $base_key := false -}}
14+
{{- $subsequent_keys := slice -}}
15+
16+
{{- range (split .render ".") -}}
17+
{{- if (ne . "") -}}
18+
{{- if not $base_key -}}
19+
{{- $base_key = . -}}
20+
{{- else -}}
21+
{{- $subsequent_keys = append (slice .) $subsequent_keys -}}
22+
{{- end -}}
23+
{{- end -}}
24+
{{- end -}}
25+
26+
{{- $bad := printf "- {{ partial \"bookshop\" (dict \"structures\" \"%s.%s\" \"source\" .) }}" $base_key (delimit $subsequent_keys ".") -}}
27+
{{- $good := printf "+ {{ partial \"bookshop\" (dict \"structures\" \"%s\" \"source\" .%s) }}" (delimit $subsequent_keys ".") $base_key -}}
28+
{{- partial "_bookshop/errors/err" (printf "\"%s\" could not be indexed from \"%s\"\nSuggested change:\n%s\n%s" .render .root_type $bad $good) -}}

hugo/v2/core/errors/err.html

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{{/*
2+
It is what it says on the box.
3+
*/}}
4+
5+
{{ errorf "📚 Error from Bookshop:\n📚❕ %s" . }}
6+
{{ return true }}

hugo/v2/core/helpers/component.html

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{{/*
2+
Renders a single Bookshop component,
3+
wrapping in in a live editing context tag.
4+
5+
Expects a dict:
6+
{
7+
component: [Bookshop Object],
8+
input: String, # See core/helpers/live_key.html
9+
index: int, # See core/helpers/live_key.html
10+
root_type: String # See core/helpers/live_key.html
11+
}
12+
*/}}
13+
14+
{{- $key := partial "_bookshop/helpers/live_key" . -}}
15+
16+
{{- if .component._bookshop_name -}}
17+
{{- $component_path := partial "_bookshop/helpers/component_key" .component._bookshop_name -}}
18+
19+
{{- if templates.Exists ( printf "partials/%s" $component_path ) -}}
20+
{{- if $key -}} {{ (printf "<!--bookshop-live name(%s) params(bind: %s) context() -->" .component._bookshop_name $key) | safeHTML }} {{ end }}
21+
{{ partial $component_path .component }}
22+
{{- if $key }}
23+
{{ "<!--bookshop-live end-->" | safeHTML }} {{- end -}}
24+
{{- else -}}
25+
{{- if $key -}}
26+
{{- partial "_bookshop/errors/err" (printf "component \"%s\" does not exist — referenced from \"%s\"" .component._bookshop_name $key) -}}
27+
{{- else -}}
28+
{{- partial "_bookshop/errors/err" (printf "component \"%s\" does not exist" .component._bookshop_name) -}}
29+
{{- end -}}
30+
{{- end -}}
31+
{{- else -}}
32+
{{- partial "_bookshop/errors/err" (printf "\"%s\" does not contain a _bookshop_name field, so no component can be rendered." $key) -}}
33+
{{- end -}}
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{{/*
2+
Converts a bare Bookshop component key to a Bookshop path
3+
i.e. "a/b" --> "bookshop/components/a/b/b.hugo.html"
4+
5+
Expects a String.
6+
*/}}
7+
8+
{{ $component_fragments := split . "/" }}
9+
{{ $component_fragments = append (last 1 $component_fragments) $component_fragments }}
10+
{{ $component_path := (printf "bookshop/components/%s.hugo.html" (delimit $component_fragments "/")) }}
11+
{{ return $component_path }}

hugo/v2/core/helpers/dig.html

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{{/*
2+
Digs through a map using dot notation, errors on failure.
3+
4+
Expects a dict:
5+
{
6+
obj: map, # Object to dig through
7+
render: String, # The "dot.notation" string to dig for
8+
}
9+
*/}}
10+
11+
{{ $obj := .obj }}
12+
{{ $input_str := .render }}
13+
{{ range (split .render ".") }}
14+
{{ if $obj }}
15+
{{ if (ne . "")}}
16+
{{ $obj = (index $obj .) }}
17+
{{ if not $obj }}
18+
{{ partial "_bookshop/errors/err" (printf "\"%s\" not found while evaluating \"%s\"" . $input_str)}}
19+
{{ end }}
20+
{{ end }}
21+
{{ end}}
22+
{{ end }}
23+
24+
{{ return $obj }}

hugo/v2/core/helpers/live_key.html

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{{/*
2+
Builds a context key for @bookshop/live to use in the frontend.
3+
i.e. "Params.content.blocks[0]"
4+
5+
Expects a dict:
6+
{
7+
input: String, # The input path "content.blocks"
8+
index: int, # A loop iteration counter "0"
9+
root_type: String # The type of the root map "maps.Params"
10+
}
11+
*/}}
12+
13+
{{ return "" }}
14+
15+
{{/* TODO: Implement the hashing method or something of the like.
16+
{{ $key := .input }}
17+
{{ if not (eq .index nil) }}
18+
{{ $key = printf "%s[%d]" $key .index }}
19+
{{ end }}
20+
{{ if hasPrefix .root_type "maps"}}
21+
{{ / * 💭 Helpfully, .Params is of the type `maps.Params`
22+
💭 so we can mark components that came from .Params as such.
23+
💭 Thus, live editing will know that this was rendered
24+
💭 directly from the front matter scope. * / }}
25+
{{ $key = printf "%s.%s" (replace .root_type "maps." "") $key }}
26+
{{ end }}
27+
{{ return $key }}
28+
*/}}

0 commit comments

Comments
 (0)