Skip to content

Commit ada3773

Browse files
authored
Merge pull request #42 from szolkowski/feature/wave-2-tags-plugin-and-perf
Refactor tag generation and descriptions for improved performance and maintainability
2 parents f91168c + 9ded61d commit ada3773

40 files changed

Lines changed: 218 additions & 360 deletions

_data/tag_descriptions.yml

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Tag-specific intros for tag pages.
2-
# Key = tag slug (lowercase, jakim emit by _gentags.rb).
2+
# Key = tag slug (lowercase, emitted by _plugins/tag_generator.rb via Jekyll::Utils.slugify).
33
# - description: 140-160 char SEO meta description
44
# - intro: rich HTML displayed at the top of the tag page (above the post list)
55
# Tags without an entry here get a generic meta description and no on-page intro.
@@ -35,3 +35,51 @@ performance:
3535
apple-silicon:
3636
description: "Posts tagged Apple Silicon — running Optimizely / Episerver CMS development on M1 / M2 Macs with Docker, .NET, and azure-sql-edge."
3737
intro: "Posts about <strong>Apple Silicon</strong> (M1 / M2 / ARM Macs) for Optimizely / Episerver development — Docker setups, azure-sql-edge workarounds, and the CMS 13 Alloy Site running cross-platform on .NET."
38+
39+
m1:
40+
description: "Posts tagged M1 — running Optimizely / Episerver CMS development on Apple Silicon Macs with Docker, .NET, and azure-sql-edge workarounds."
41+
intro: "Posts about <strong>M1 / Apple Silicon</strong> for Optimizely development — same scope as the <a href='/tags/apple-silicon'>apple silicon</a> tag, kept stable for inbound search results."
42+
43+
arm:
44+
description: "Posts tagged ARM — running Optimizely / Episerver CMS development on ARM-based Macs with Docker, azure-sql-edge, and .NET cross-platform builds."
45+
intro: "Posts about <strong>ARM</strong> CPUs for Optimizely development — overlaps the <a href='/tags/apple-silicon'>apple silicon</a> and <a href='/tags/m1'>M1</a> tags."
46+
47+
commerce:
48+
description: "Posts tagged Commerce — Optimizely Commerce catalog APIs, scheduled jobs, memory-efficient traversal, and integration patterns for large catalogs."
49+
intro: "Posts about <strong>Optimizely Commerce</strong> — catalog APIs, memory-efficient traversal patterns, scheduled jobs, and Hangfire integration for large product catalogs."
50+
51+
catalog:
52+
description: "Posts tagged catalog — Optimizely Commerce catalog traversal, content APIs, memory-efficient enumeration, and scheduled-job patterns for large catalogs."
53+
intro: "Posts about <strong>catalog</strong> work in Optimizely Commerce — traversal patterns, the Content APIs, batched enumeration, and scheduled jobs that respect memory budgets."
54+
55+
database:
56+
description: "Posts tagged database — SQL Server index maintenance, statistics rebuilds, and database-tooling patterns for Optimizely CMS / Commerce projects."
57+
intro: "Posts about <strong>databases</strong> for Optimizely projects — SQL Server index and statistics maintenance, scheduled cleanup jobs, and tooling for routine DBA work."
58+
59+
background-jobs:
60+
description: "Posts tagged background jobs — Hangfire integration for Optimizely CMS, scheduled-job patterns, dashboard work, and progress reporting for long-running tasks."
61+
intro: "Posts about <strong>background jobs</strong> in Optimizely projects — Hangfire integration, the built-in scheduled-job system, progress reporting, and dashboards for long-running tasks."
62+
63+
optipowertools:
64+
description: "Posts tagged OptiPowerTools — open-source NuGet packages for Optimizely CMS and Commerce, including Hangfire integration and SQL maintenance tooling."
65+
intro: "Posts about <strong>OptiPowerTools</strong> — a small set of open-source NuGet packages for Optimizely CMS and Commerce. Source on <a href='https://github.com/szolkowski/OptiPowerTools.Hangfire'>GitHub</a>."
66+
67+
optipowertools-hangfire:
68+
description: "Posts tagged OptiPowerTools.Hangfire — the open-source NuGet package wiring Hangfire into Optimizely CMS, with dashboards, scheduled jobs, and admin gates."
69+
intro: "Posts about <strong>OptiPowerTools.Hangfire</strong> — the NuGet package that wires <a href='https://www.hangfire.io/'>Hangfire</a> into Optimizely CMS. Source on <a href='https://github.com/szolkowski/OptiPowerTools.Hangfire'>GitHub</a>; CMS 12 and 13 supported."
70+
71+
ci:
72+
description: "Posts tagged CI — GitHub Actions and Azure DevOps pipelines for Optimizely CMS, including SonarCloud analysis, Playwright UI tests, and build automation."
73+
intro: "Posts about <strong>continuous integration</strong> for Optimizely projects — GitHub Actions and Azure DevOps pipelines, SonarCloud quality gates, and UI regression testing with Playwright."
74+
75+
devops:
76+
description: "Posts tagged DevOps — CI/CD pipelines, build automation, code-quality gates, and developer-experience tooling for Optimizely CMS / Commerce projects."
77+
intro: "Posts about <strong>DevOps</strong> for Optimizely projects — CI/CD pipelines, build automation, code-quality gates (SonarCloud), and the boring infrastructure that makes shipping uneventful."
78+
79+
sonarcloud:
80+
description: "Posts tagged SonarCloud — adding code-quality gates to Optimizely CMS pipelines, .NET-specific rules, and integrating analysis into GitHub Actions."
81+
intro: "Posts about <strong>SonarCloud</strong> for Optimizely / .NET projects — wiring quality gates into Azure DevOps and GitHub Actions, .NET-specific rule sets, and triaging the noise."
82+
83+
nuget:
84+
description: "Posts tagged NuGet — packaging and distributing Optimizely CMS / Commerce libraries, semantic versioning, and the OptiPowerTools.Hangfire release flow."
85+
intro: "Posts about <strong>NuGet</strong> packaging for Optimizely projects — semantic versioning, package metadata, and the release flow behind the OptiPowerTools.Hangfire package."

_gentags.rb

Lines changed: 0 additions & 45 deletions
This file was deleted.

_includes/_all-tags.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{%- comment -%}
2+
Captures every unique tag across site.tags into a Liquid array `all_tag_names`,
3+
used by the sidebar and the tag cloud. Idempotent — re-including is a no-op.
4+
{%- endcomment -%}
5+
{%- unless all_tag_names -%}
6+
{%- capture _all_tags_csv -%}{% for tag in site.tags %}{{ tag | first }}{% unless forloop.last %},{% endunless %}{% endfor %}{%- endcapture -%}
7+
{%- assign all_tag_names = _all_tags_csv | split: ',' -%}
8+
{%- endunless -%}

_includes/_disqus.html

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,31 @@
55
this.page.url = '{{ page.url | absolute_url }}';
66
this.page.identifier = '{{ page.url }}';
77
};
8-
(function() {
9-
var d = document, s = d.createElement('script');
10-
s.src = 'https://{{ site.disqus_username }}.disqus.com/embed.js';
11-
s.setAttribute('data-timestamp', +new Date());
12-
(d.head || d.body).appendChild(s);
8+
(function () {
9+
var thread = document.getElementById('disqus_thread');
10+
if (!thread) return;
11+
var loaded = false;
12+
function loadDisqus() {
13+
if (loaded) return;
14+
loaded = true;
15+
var s = document.createElement('script');
16+
s.src = 'https://{{ site.disqus_username }}.disqus.com/embed.js';
17+
s.setAttribute('data-timestamp', +new Date());
18+
(document.head || document.body).appendChild(s);
19+
}
20+
if ('IntersectionObserver' in window) {
21+
var io = new IntersectionObserver(function (entries) {
22+
entries.forEach(function (e) {
23+
if (e.isIntersecting) {
24+
loadDisqus();
25+
io.disconnect();
26+
}
27+
});
28+
}, { rootMargin: '300px 0px' });
29+
io.observe(thread);
30+
} else {
31+
loadDisqus();
32+
}
1333
})();
1434
</script>
1535
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>

_includes/_tagcloud.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
{% capture site_tags %}{% for tag in site.tags %}{{ tag | first }}{% unless forloop.last %},{% endunless %}{% endfor %}{% endcapture %}
2-
{% assign site_tags = site_tags | split: ',' %}
1+
{% include _all-tags.html %}
32

43
{% assign tag_count = 0 %}
5-
{% for tag in site_tags %}
4+
{% for tag in all_tag_names %}
65
{% assign tag_count = tag_count | plus: site.tags[tag].size %}
76
{% endfor %}
87

_layouts/default.html

Lines changed: 5 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -54,87 +54,18 @@
5454

5555
<aside id="sidebar">
5656
<p class="repo-owner">Tags</p>
57-
{% capture site_tags %}{% for tag in site.tags %}{{ tag | first }}{% unless forloop.last %},{% endunless %}{% endfor %}{% endcapture %}
58-
{% assign tags = site_tags | split:',' | sort %}
57+
{% include _all-tags.html %}
58+
{% assign tags = all_tag_names | sort %}
5959
{% include _tagcloud.html %}
6060
{% include _badges.html %}
6161
</aside>
6262
</div>
6363
</div>
6464
{% include _footer.html %}
6565

66-
<script>
67-
document.addEventListener("DOMContentLoaded", function () {
68-
if (typeof hljs !== "undefined") {
69-
hljs.highlightAll();
70-
}
71-
72-
document.querySelectorAll(".open-code-modal").forEach(button => {
73-
button.addEventListener("click", () => {
74-
const targetId = button.getAttribute("data-target");
75-
const modal = document.getElementById(targetId);
76-
if (modal) {
77-
modal.style.display = "block";
78-
if (typeof hljs !== "undefined") {
79-
modal.querySelectorAll("pre code").forEach(block => {
80-
hljs.highlightElement(block);
81-
});
82-
}
83-
} else {
84-
console.warn("Modal not found for target:", targetId);
85-
}
86-
});
87-
});
88-
89-
document.querySelectorAll(".code-modal-close").forEach(closeBtn => {
90-
closeBtn.addEventListener("click", () => {
91-
closeBtn.closest(".code-modal").style.display = "none";
92-
});
93-
});
94-
95-
window.addEventListener("click", function (event) {
96-
document.querySelectorAll(".code-modal").forEach(modal => {
97-
if (event.target === modal) {
98-
modal.style.display = "none";
99-
}
100-
});
101-
});
102-
103-
document.addEventListener("keydown", function (event) {
104-
if (event.key === "Escape") {
105-
document.querySelectorAll(".code-modal").forEach(modal => {
106-
if (modal.style.display === "block") {
107-
modal.style.display = "none";
108-
}
109-
});
110-
}
111-
});
112-
});
113-
</script>
114-
115-
<script>
116-
document.addEventListener("click", function (event) {
117-
var button = event.target.closest(".copy-btn");
118-
if (!button) return;
119-
var code = button.nextElementSibling.querySelector("code");
120-
if (!code) return;
121-
var text = code.innerText;
122-
123-
navigator.clipboard.writeText(text).then(function () {
124-
button.classList.add("copied");
125-
button.innerHTML = "<i class='fas fa-check'></i> Copied!";
126-
setTimeout(function () {
127-
button.classList.remove("copied");
128-
button.innerHTML = "<i class='fas fa-copy'></i> Copy";
129-
}, 1500);
130-
}).catch(function () {
131-
button.innerHTML = "<i class='fas fa-times'></i> Failed";
132-
setTimeout(function () {
133-
button.innerHTML = "<i class='fas fa-copy'></i> Copy";
134-
}, 1500);
135-
});
136-
});
137-
</script>
66+
{%- if page.layout == "post" -%}
67+
<script defer src="{{ '/assets/js/code-modal.js' | relative_url }}"></script>
68+
{%- endif -%}
13869

13970
</body>
14071
</html>

_plugins/tag_generator.rb

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
module Jekyll
2+
# Generates a tag page (layout: tagpage, permalink: /tags/<slug>) for every
3+
# unique tag found across site.posts. Hand-authored intros + meta descriptions
4+
# for prominent tags live in _data/tag_descriptions.yml, keyed by slug; tags
5+
# without an entry get a generic stub.
6+
#
7+
# Replaces the old standalone _gentags.rb script — runs on every jekyll build,
8+
# so tag pages cannot drift out of sync with post frontmatter.
9+
class TagPageGenerator < Generator
10+
safe true
11+
priority :normal
12+
13+
GENERIC_DESCRIPTION = "Posts on Szołkowski's Blog tagged %s — Optimizely CMS, .NET, and developer-experience writing.".freeze
14+
15+
def generate(site)
16+
descriptions = site.data['tag_descriptions'] || {}
17+
18+
tags = site.posts.docs
19+
.flat_map { |post| Array(post.data['tags']) }
20+
.map { |t| t.to_s.strip }
21+
.reject(&:empty?)
22+
.uniq { |t| t.downcase }
23+
24+
tags.each do |tag|
25+
site.pages << build_tag_page(site, tag, descriptions)
26+
end
27+
end
28+
29+
private
30+
31+
def build_tag_page(site, tag, descriptions)
32+
slug = Jekyll::Utils.slugify(tag)
33+
entry = descriptions[slug] || {}
34+
desc = entry['description'] || (GENERIC_DESCRIPTION % tag)
35+
36+
page = PageWithoutAFile.new(site, site.source, 'tags', "#{slug}.html")
37+
page.data.merge!(
38+
'layout' => 'tagpage',
39+
'tag' => tag,
40+
'title' => "Posts tagged #{tag}",
41+
'description' => desc,
42+
'permalink' => "/tags/#{slug}",
43+
# Set explicitly so jekyll-last-modified-at doesn't try to git-mtime a
44+
# file that exists only in memory. Use the most-recent post date for
45+
# this tag so dateModified actually means something.
46+
'last_modified_at' => latest_post_date_for(site, slug)
47+
)
48+
page.data['intro'] = entry['intro'] if entry['intro']
49+
page.content = ''
50+
page
51+
end
52+
53+
def latest_post_date_for(site, slug)
54+
matching = site.posts.docs.select do |post|
55+
Array(post.data['tags']).any? { |t| Jekyll::Utils.slugify(t.to_s) == slug }
56+
end
57+
return site.time if matching.empty?
58+
matching.map(&:date).max
59+
end
60+
end
61+
end

assets/js/code-modal.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
document.addEventListener("DOMContentLoaded", function () {
2+
if (typeof hljs !== "undefined") {
3+
hljs.highlightAll();
4+
}
5+
6+
document.querySelectorAll(".open-code-modal").forEach(function (button) {
7+
button.addEventListener("click", function () {
8+
var targetId = button.getAttribute("data-target");
9+
var modal = document.getElementById(targetId);
10+
if (modal) {
11+
modal.style.display = "block";
12+
if (typeof hljs !== "undefined") {
13+
modal.querySelectorAll("pre code").forEach(function (block) {
14+
hljs.highlightElement(block);
15+
});
16+
}
17+
} else {
18+
console.warn("Modal not found for target:", targetId);
19+
}
20+
});
21+
});
22+
23+
document.querySelectorAll(".code-modal-close").forEach(function (closeBtn) {
24+
closeBtn.addEventListener("click", function () {
25+
closeBtn.closest(".code-modal").style.display = "none";
26+
});
27+
});
28+
29+
window.addEventListener("click", function (event) {
30+
document.querySelectorAll(".code-modal").forEach(function (modal) {
31+
if (event.target === modal) {
32+
modal.style.display = "none";
33+
}
34+
});
35+
});
36+
37+
document.addEventListener("keydown", function (event) {
38+
if (event.key === "Escape") {
39+
document.querySelectorAll(".code-modal").forEach(function (modal) {
40+
if (modal.style.display === "block") {
41+
modal.style.display = "none";
42+
}
43+
});
44+
}
45+
});
46+
});
47+
48+
document.addEventListener("click", function (event) {
49+
var button = event.target.closest(".copy-btn");
50+
if (!button) return;
51+
var code = button.nextElementSibling.querySelector("code");
52+
if (!code) return;
53+
var text = code.innerText;
54+
55+
navigator.clipboard.writeText(text).then(function () {
56+
button.classList.add("copied");
57+
button.innerHTML = "<i class='fas fa-check'></i> Copied!";
58+
setTimeout(function () {
59+
button.classList.remove("copied");
60+
button.innerHTML = "<i class='fas fa-copy'></i> Copy";
61+
}, 1500);
62+
}).catch(function () {
63+
button.innerHTML = "<i class='fas fa-times'></i> Failed";
64+
setTimeout(function () {
65+
button.innerHTML = "<i class='fas fa-copy'></i> Copy";
66+
}, 1500);
67+
});
68+
});

tags/apple-silicon.html

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)