Skip to content

Commit 67ebe44

Browse files
Sync Merge: docs/inject_dynamic_content (#557)
* feat: in dynamic-block shortcode, add local json testing mode and docs * docs: document workflow for injecting dynamic content * feat: add simplified syntax for md lists and tables and document it * docs: add guidelines about emojis and do further polishing --------- Co-authored-by: kirill.chalov <[email protected]>
1 parent 0e642f8 commit 67ebe44

File tree

3 files changed

+244
-2
lines changed

3 files changed

+244
-2
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
---
2+
title: "Dynamic content"
3+
date: 2025-09-09
4+
tags: ["Contribute"]
5+
showTableOfContents: true
6+
showAuthor: false
7+
authors:
8+
- "kirill-chalov"
9+
---
10+
11+
For pages that change frequently, it is useful to bypass git and inject the updated content dynamically instead of creating PRs every time. As of now, this is helpful for hardware and software product support pages that can update weekly. This article will provide a workflow for injecting dynamic content focusing on the product support pages.
12+
13+
14+
## Plan your page
15+
16+
First of all, the content on a product support page should be classified into the following types:
17+
18+
- **Static**: Rarely changing content
19+
- Committed to the git repo
20+
- Stored under
21+
`content/{hardware|software}/product-x/index.md`
22+
- **Dymanic**: Frequently changing content
23+
- Dynamically injected into HTML pages using the [dynamic-block](https://github.com/espressif/developer-portal/blob/main/layouts/shortcodes/dynamic-block.html) shortcode
24+
- Stored in the root of the web server under
25+
`./persist/{hardware|software}/product-x/product-x.json`
26+
- Updated either manually or using CI
27+
- For uploading to the web server, talk to the project maintainers
28+
29+
The dynamic content must be stored in the root of the web server under `persist`. All other folders are fully overwritten daily.
30+
31+
32+
## Arrange your content
33+
34+
From the hints in [Plan your page](#plan-your-page), it is not hard to understand the idea:
35+
36+
- In your markdown file where a dynamic part needs to be injected, you add a `dynamic-block` shortcode with part's `jsonKey`
37+
- In your JSON file, you add all dynamic parts in markdown
38+
39+
### Simplified syntax
40+
41+
As you can see from [Example files](#example-files) below, raw markdown lists (`feature_list`) and tables (`periph_support_table`) are not reasonable to store in JSON. A simplified syntax provides a way to strip unnecessary characters for storing, and add them back during rendering. This way, the JSON source is easier to read and `dynamic-block` still renders it correctly.
42+
43+
To use simplified syntax, mark the `jsonKey` accordingly. In each case, the raw and simplified versions render identically:
44+
45+
- **Simplified list** -- append `list_simple` to its `jsonKey`<br>
46+
Output: `feature_list` = `feature_list_simple`
47+
- **Simplified table** -- append `table_simple` to its `jsonKey`<br>
48+
Output: `periph_support_table` = `periph_support_table_simple`
49+
50+
### Emojis
51+
52+
In JSON, include emojis as Unicode characters ✅ instaed of `: white_check_mark :`. This considerably reduces the render time of injected content.
53+
54+
### Example files
55+
56+
Git repo: `content/software/product-x/index.md`
57+
```
58+
---
59+
title: "Product X"
60+
date: 2025-08-28
61+
---
62+
63+
**Last updated:** {{</* dynamic-block contentPath="persist/software/product-x/product-x.json" jsonKey="timestamp" */>}}
64+
65+
This is a product status page for Product X.
66+
67+
The following features are supported as of now:
68+
69+
{{</* dynamic-block contentPath="persist/software/product-x/product-x.json" jsonKey="feature_list_simple" */>}}
70+
71+
## Peripheral support table
72+
73+
{{</* dynamic-block contentPath="persist/software/product-x/product-x.json" jsonKey="periph_support_table_simple" */>}}
74+
```
75+
76+
Web server: `persist/software/product-x/product-x.json`
77+
78+
```json
79+
{
80+
"timestamp": "2025-08-28T00:07:19.716630Z",
81+
"feature_list": "- Supported SDKs\n - ✅ [ESP-IDF](https://github.com/espressif/esp-idf/)\n - ⏳ SDK 2",
82+
"periph_support_table": "| Peripheral | ESP32 |\n| :--- | :---: |\n| UART | ✅ |\n| LCD | ❌ |",
83+
"feature_list_simple": [
84+
"- Supported SDKs",
85+
" - ✅ [ESP-IDF](https://github.com/espressif/esp-idf/)",
86+
" - ⏳ SDK 2"
87+
],
88+
"periph_support_table_simple": [
89+
"Peripheral,ESP32",
90+
":---,:---:",
91+
"UART,✅",
92+
"LCD,❌"
93+
]
94+
}
95+
```
96+
97+
The final page with the dynamic content should look somewhat like this:
98+
99+
---
100+
101+
<span style="font-size:2em; font-weight:bold;">Product X</span>
102+
103+
**Last updated:** 28 Aug 2025, 8:07 am
104+
105+
This is a product status page for Product X.
106+
107+
The following features are supported as of now:
108+
109+
- Supported SDKs
110+
-[ESP-IDF](https://github.com/espressif/esp-idf/)
111+
- ⏳ SDK 2
112+
113+
<span style="font-size:1.5em; font-weight:bold;">Peripheral support table</span>
114+
115+
| Peripheral | ESP32 |
116+
| :--- | :---: |
117+
| UART ||
118+
| LCD ||
119+
120+
---
121+
122+
123+
## Test dynamic content
124+
125+
Test your `.json` file locally before uploading to the web server:
126+
127+
- In your git repo, place your `.json` file at the same path as on the server:
128+
```sh
129+
📂 content/software/
130+
├── 📝 _index.md
131+
└── 📂 product-x/
132+
├── 📝 index.md
133+
└── 🧩 persist/software/product-x/product-x.json # remove after testing
134+
```
135+
- In your git repo's `layouts/shortcodes/dynamic-block.html`, adjust the toggle for testing:
136+
```javascript
137+
{{ $localMode := true }} <!-- change to true for local -->
138+
```
139+
140+
After you run `hugo server` locally, the JSON content should be injected dynamically on your page.
141+
142+
**If you update JSON**, do this for the changes to show up:
143+
144+
- Restart `hugo server`
145+
- Refresh your browser tab
146+
- If no effect: clear the page cache

content/pages/contribution-guide/writing-content/index.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ Please have your featured image converted to WebP as requested in [Use WebP for
143143

144144
[blowfish-front-matter]: https://blowfish.page/docs/front-matter/
145145

146+
### Inject dynamic content
147+
148+
If your page is going to be updated frequently, consider implementing injection of dynamic content. Usually, it is used for product status pages, such as [ESP32-C61 status](../../../hardware/esp32c61 "ESP32-C61 status").
149+
150+
For more information, see [Dynamic content](../dynamic-content "Dynamic content").
151+
146152
## Use additional content types
147153

148154
Apart from the usual content types supported by markdown, such as visuals or code blocks, you can use other content types enabled by Hugo shortcodes. This section briefly introduces the most relevant shortcodes implemented on the Espressif Developer Portal.

layouts/shortcodes/dynamic-block.html

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,94 @@
1+
{{/*
2+
Shortcode: dynamic-block
3+
------------------------
4+
5+
This shortcode allows you to inject frequently changing content from a JSON file
6+
into your Hugo pages. It is designed for cases where some parts of a page (such as
7+
status, last update timestamps, or feature lists) need to be updated often without
8+
rebuilding the entire site.
9+
10+
See also https://developer.espressif.com/pages/contribution-guide/dynamic-content/
11+
12+
Usage example
13+
14+
{{< dynamic-block contentPath="persist/path-to/file.json" jsonKey="timestamp" >}}
15+
16+
Arguments
17+
18+
- contentPath (string)
19+
Path to the JSON file relative to the site base URL (e.g. "persist/status.json").
20+
In production, JSON must be served from https://developer.espressif.com/persist/...
21+
22+
- jsonKey (string)
23+
The key within the JSON object whose value will be injected into the page.
24+
The content can be plain text, Markdown, or a special field.
25+
26+
Special field handling
27+
28+
- jsonKey = "timestamp"
29+
If the key is `"timestamp"`, the shortcode expects an ISO8601 date/time string.
30+
It is automatically converted into a localized, human-readable format:
31+
`DD Mon YYYY, HH:MM AM/PM` (using the en-GB locale).
32+
33+
- jsonKey containing "list_simple"
34+
If the key name includes "list_simple", the shortcode expects an array of strings representing a Markdown list.
35+
The array is automatically joined into a proper Markdown list before rendering.
36+
Example:
37+
{
38+
"feature_list_simple": [
39+
"- Supported SDKs",
40+
" - ✅ [ESP-IDF](https://github.com/espressif/esp-idf/)",
41+
" - ⏳ SDK 2"
42+
]
43+
}
44+
45+
- jsonKey containing "table_simple"
46+
If the key name includes "table_simple", the shortcode expects an array of CSV-style strings representing table rows.
47+
Each string is converted into a Markdown table row before rendering.
48+
Example:
49+
{
50+
"periph_support_table_simple": [
51+
"Peripheral,ESP32",
52+
":---,:---:",
53+
"UART,✅",
54+
"LCD,❌"
55+
]
56+
}
57+
58+
Other keys
59+
60+
- Any other key is assumed to contain Markdown content.
61+
The Markdown is rendered to HTML before being injected.
62+
63+
Error handling
64+
65+
If the JSON file cannot be fetched or the key is not found, the shortcode will display
66+
an error message instead of the content.
67+
*/}}
68+
69+
{{/*
70+
Toggle for local testing: set to true to load JSON locally from contentPath
71+
(relative to your index.md location).
72+
Leave false (default) to resolve via .Site.BaseURL (web mode).
73+
*/}}
74+
{{ $localMode := true }} <!-- change to true for local -->
75+
76+
177
{{ $uniqueID := .Get "jsonKey" | urlize }} <!-- Generate a unique ID -->
278
<span id="content-{{ $uniqueID }}">Loading...</span>
379

480
{{ $contentPath := .Get "contentPath" }}
581
{{ $jsonKey := .Get "jsonKey" }}
6-
{{ $fullURL := printf "%s%s" .Site.BaseURL $contentPath }}
82+
83+
{{/* Only build $fullURL in web mode */}}
84+
{{ $fullURL := "" }}
85+
{{ if not $localMode }}
86+
{{ $fullURL = printf "%s%s" .Site.BaseURL $contentPath }}
87+
{{ end }}
788

889
<script>
990
document.addEventListener("DOMContentLoaded", function () {
10-
const url = '{{ $fullURL }}';
91+
const url = '{{ if $localMode }}{{ $contentPath }}{{ else }}{{ $fullURL }}{{ end }}';
1192
const jsonKey = '{{ $jsonKey }}';
1293
const uniqueID = '{{ $uniqueID }}';
1394

@@ -29,6 +110,15 @@
29110
const isoTime = data[jsonKey];
30111
const options = { day: 'numeric', month: 'short', year: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true };
31112
content = new Intl.DateTimeFormat('en-GB', options).format(new Date(isoTime));
113+
} else if (jsonKey.includes("list_simple")) {
114+
// Join simplified array back into markdown list
115+
let markdown = data[jsonKey].join("\n");
116+
content = marked.parse(markdown);
117+
} else if (jsonKey.includes("table_simple")) {
118+
// Convert simplified array back into a markdown table
119+
const rows = data[jsonKey].map(row => `| ${row.replace(/,/g, " | ")} |`);
120+
const markdown = rows.join("\n");
121+
content = marked.parse(markdown);
32122
} else {
33123
// Extract the value for the key and convert the markdown to HTML
34124
let markdown = data[jsonKey];

0 commit comments

Comments
 (0)