Rendering Structurizr in Jekyll
Recently, I have taken a lot of interest in using C4 models for architecture, specifically using Structurizr to model architecture using C4 models and rendering the diagrams interactively. This post covers how to add Structurizr UI to a Jekyll site. I’m using Textile markup but the instructions will work also for Markdown pages.
There are a few main things we need to do:
- Get the UI package
- Add the CSS
- Add the Javascript
- Set up the page to show the diagram
- Initialise Structurizr with the source workspace and the diagram to show
- Avoid including Structurizr on every page
With these done, you end up with a picture like this on your page.
Create a Structurizr diagram
Of course, you need to create a diagram that you will use. You should use something like Structurizr-lite to describe your system and to create the views for it. You can also use that for laying out the diagram as you like. Once this is done, you will see that there is a file called workspace.json in the same folder as your workspace.dsl file. This is the file you need to use for the steps below. If you want, you can rename the file to something else.
For showing the structure of our system and more importantly, the integration in our page, we will create a set of levels:
- A system context diagram
- A container diagram
- A component diagram
Deciding where we store the file
I found it simplest to create a new folder called sructurizr in the root directly of my Jekyll setup. Let’s do that and copy the workspace.json [note: not workspace.dsl] file to this folder. You can rename the file to something else if you want, especially if you intend to have multiple files in that folder. Since the folder does not start with an _, it will be copied as-is to your site and available at /structurizr from the browser. So, your file will now be available as your-domain.com/structurizr/workspace.json from your browser. In terms of your Jekyll site, you should refer to this file using {% %}
Get the Structurizr UI package
The Structurizr team makes the UI code for rendering a Structurizr diagram available as open source. You can download the package from “https://github.com/structurizr/ui” on GitHub. Download the package and unzip it (or clone it and go to the folder where it is). Go to the folder called examples and you can try some of the examples from there to see how the view is rendered into the page. This gives us some familiarity with the code.
For this integration, we will use code from the basic, key, zoom and navigate examples for embedding it into our site.
What do we want to do?
Specifically, our integration will provide these abilities:
- The ability to load a single structurizr JSON into the page. This will be drawn into a single div. The path to the structurizr JSON file and the view to load are specified in the post.
- We set up the key in a different div. We provide a button to hide or show the div.
- Set up zoom in and out buttons to zoom into the diagram.
- A drop-down select box to show the list of views in the JSON file and to switch to a different one
- A button to reset back to the first view that was loaded.
Adding the CSS and Javascript to our site
Structurizr needs its own CSS and Javascript files to be available in the post. For this, I do the following:
- Create a folder under
CSScalledstructurizrand copy the following files there - Create a folder under
JScalledstructurizrand copy the following files there
Next, we need to include these into our template. It’s common to have CSS files in the head of an HTML page and JS in the footer of the HTML page. We create snippets in _includes for this.
For the CSS file, we create a snippet called structurizr_header and put this into it.
1
2
<link href="/css/structurizr/joint-3.6.5.css" rel="stylesheet" media="screen" />
<link href="/css/structurizr/structurizr-diagram.css" rel="stylesheet" media="screen" />
For the JS, we create a snippet called structurizr_footer and put this code into it.
>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
<!-- Includes Structurizr Javascript and initilaises it for the features we need.
Use page.structurizr to only include on relevant pages -->
<!-- All the JavaScript files -->
<script src="/js/structurizr/jquery-3.6.3.min.js"></script>
<!-- <== already included in our header -->
<script src="/js/structurizr/lodash-4.17.21.js"></script>
<script src="/js/structurizr/backbone-1.4.1.js"></script>
<script type="text/javascript" src="/js/structurizr/joint-3.6.5.js"></script>
<script type="text/javascript" src="/js/structurizr/structurizr.js"></script>
<script type="text/javascript" src="/js/structurizr/structurizr-util.js"></script>
<script type="text/javascript" src="/js/structurizr/structurizr-ui.js"></script>
<script type="text/javascript" src="/js/structurizr/structurizr-workspace.js"></script>
<script type="text/javascript" src="/js/structurizr/structurizr-diagram.js"></script>
<!-- Initialise structurizr and load the diagram -->
<script>
var str_diagram;
// Extract the URL to the workspace & the diagram name to load
function extractTextAndURL(text) {
const regex = /\[(.*?)\]\((.*?)\)/;
const match = text.match(regex);
if (match) {
const textItem = match[1].trim();
const urlItem = match[2].trim();
return { textItem, urlItem };
} else {
return null;
}
}
function updateViewDropdown(text) {
str_keyDropdown.value = text;
}
// Function to populate the dropdown with options
function populateDropdown(jsonArray) {
const dropdown = document.getElementById('str_keyDropdown');
jsonArray.forEach(item => {
const option = document.createElement('option');
option.value = item.key;
option.textContent = item.key;
dropdown.appendChild(option);
});
// Add an event listener to the dropdown
dropdown.addEventListener('change', str_handleDropdownChange);
}
// Function to handle the dropdown change event
function str_handleDropdownChange(event) {
const selectedKey = event.target.value;
console.log('Selected Key:', selectedKey);
str_diagram.changeView(selectedKey);
}
$(document).ready(function () {
// Get the text content of the example div
const exampleDiv = document.getElementById('diagram');
const divTextContent = exampleDiv.textContent.trim();
// Extract text and URL from the text content
const result = extractTextAndURL(divTextContent);
if (result) {
console.log('Text in box brackets:', result.textItem);
console.log('URL in parentheses:', result.urlItem);
exampleDiv.textContent = ''
} else {
console.error('No match found.');
}
$.ajax({
url: result.urlItem,
success: function (data) {
structurizr.workspace = new structurizr.Workspace(data);
// Our Initial Diagram
str_diagram = new structurizr.ui.Diagram('diagram', false, function () {
str_diagram.onViewChanged(viewChanged); // Needed to update the key
//str_diagram.changeView(result.textItem);
str_homeDiagram();
});
// Enable Navigation and listeners for element/ relationship clicked
str_diagram.setNavigationEnabled(true);
str_diagram.onElementDoubleClicked(elementDoubleClicked);
str_diagram.onRelationshipDoubleClicked(elementDoubleClicked);
// Add basic interaction buttons [Key, Home, Z-, Z+]
$('#infoButton').click(toggleDiv); // Display the key
$('#homeButton').click(function () { str_homeDiagram() }); //str_diagram.changeView(result.textItem) }); // Reset to original diagram
$('#zoomOutButton').click(str_diagram.zoomOut); // Zoom control -
$('#zoomInButton').click(str_diagram.zoomIn); // Zoom control +
views_list = structurizr.workspace.getViews();
populateDropdown(views_list);
}
});
// Allows interacting and navigating into the Structurizr Diagram
function elementDoubleClicked(event, elementId) {
const element = structurizr.workspace.findElementById(elementId);
var views = [];
if (element.type === structurizr.constants.SOFTWARE_SYSTEM_ELEMENT_TYPE) {
if (str_diagram.getCurrentView().type === structurizr.constants.SYSTEM_LANDSCAPE_VIEW_TYPE || str_diagram.getCurrentView().softwareSystemId !== element.id) {
views = structurizr.workspace.findSystemContextViewsForSoftwareSystem(element.id);
if (views.length === 0) {
views = structurizr.workspace.findContainerViewsForSoftwareSystem(element.id);
}
} else if (str_diagram.getCurrentView().type === structurizr.constants.SYSTEM_CONTEXT_VIEW_TYPE) {
views = structurizr.workspace.findContainerViewsForSoftwareSystem(element.id);
}
} else if (element.type === "Container") {
views = structurizr.workspace.findComponentViewsForContainer(element.id);
}
if (views.length > 0) {
str_diagram.changeView(views[0].key);
}
}
function str_homeDiagram() {
str_diagram.changeView(result.textItem);
updateViewDropdown(result.textItem);
}
// Reacts to relationship being clicked ?????????????????????
function onRelationshipDoubleClicked(event, relationshipId) {
const relationship = structurizr.workspace.findRelationshipById(relationshipId);
if (relationship.url) {
window.open(relationship.url);
}
}
// Function to toggle the visibility of the div for the key
function toggleDiv() {
const myDiv = document.getElementById('key');
//myDiv.classList.toggle('hidden');
myDiv.style.display = (myDiv.style.display === 'none') ? 'block' : 'none';
}
// Updates key when the view is changed
function viewChanged() {
$('#key').html(str_diagram.exportCurrentDiagramKeyToSVG());
}
});
</script>
Now, let’s add this to the header.
Finally, let’s add it to the footer.
Notice that we added a conditional check to include the code. This way, if the front matter of the page sets structurizr: true then the CSS and the JS snippets are included into the page.
Adding a post that references structurizr
We need to create a part on the page and point it to the Structurizr diagram. Since a structurizr workspace has multiple diagrams in it, we need to also indicate the diagram to load when the page loads.
We have chosen to do the following:
- The structurizr diagram will be shown in a
div that has an ID of structurizr - We will follow this format:
[Diagram_name](diagram_url)- On this page, we use:
[Overall_Context](/structurizr/workspace.json) Not including Structurizr on every page
The Structurizr Javascript is quite large and it does not make sense to include it for every page. It should only be included if the page actually has any diagrams. For this, we:
- Include a flag in the front matter of the post. If we include
structurizr: true in the front matter at the top of the page, we indicate to Jekyll that this page will use Structurizr and the Javascript should be included and initialised. - We add a condition in the footer to include the code above only if the flag is set.
For this, we wrap the code above in a condition in the footer file.
{% if page.structurizr }
… the rest of the Javascript code from above …
{ endif %}
Possible Problems
The main problem I had with this approach is that the diagrams do not always look very nice. This is often because the workspace is very wide (in my case, 4000px almost) but it’s being compressed into a smaller 1000px wide diagram area. This makes the text a bit difficult to see and the whole diagram looks a bit weird.
Of course, we added zoom buttons – so, you can zoom in and get a better view though we have not set up a way to to open up a larger view or open the diagram in a separate page. You could create a separate post page for this if you like with a different template.
Is it worth the effort?
I am a bit split on this – I feel that if you only need one overall system picture, then this may not be worth effort. You may be better off rendering the diagram into an SVG or PNG, and displaying that directly.
On the other hand, if you do have a diagram that can be navigated to different levels, then this becomes quite useful and may be totally worth it!
Links and References
The main link to refer to is the repository for the Structurizr UI on GitHub.
This completes this short post on how to use Mermaid in a Textile post using Jekyll. This is mainly written for me to remember what I need to do; if it helps someone, that’s great. Of course, feel free to share the post (you can tag me as @onghu on Twitter or on Mastodon as @onghu@ruby.social ) or leave a comment below.
This post is licensed under CC BY 4.0 by the author.Trending Tags
Comments powered by Disqus.