Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion _data/tag_descriptions.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Tag-specific intros for tag pages.
# Key = tag slug (lowercase, jakim emit by _gentags.rb).
# Key = tag slug (lowercase, emitted by _plugins/tag_generator.rb via Jekyll::Utils.slugify).
# - description: 140-160 char SEO meta description
# - intro: rich HTML displayed at the top of the tag page (above the post list)
# Tags without an entry here get a generic meta description and no on-page intro.
Expand Down Expand Up @@ -35,3 +35,51 @@ performance:
apple-silicon:
description: "Posts tagged Apple Silicon — running Optimizely / Episerver CMS development on M1 / M2 Macs with Docker, .NET, and azure-sql-edge."
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."

m1:
description: "Posts tagged M1 — running Optimizely / Episerver CMS development on Apple Silicon Macs with Docker, .NET, and azure-sql-edge workarounds."
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."

arm:
description: "Posts tagged ARM — running Optimizely / Episerver CMS development on ARM-based Macs with Docker, azure-sql-edge, and .NET cross-platform builds."
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."

commerce:
description: "Posts tagged Commerce — Optimizely Commerce catalog APIs, scheduled jobs, memory-efficient traversal, and integration patterns for large catalogs."
intro: "Posts about <strong>Optimizely Commerce</strong> — catalog APIs, memory-efficient traversal patterns, scheduled jobs, and Hangfire integration for large product catalogs."

catalog:
description: "Posts tagged catalog — Optimizely Commerce catalog traversal, content APIs, memory-efficient enumeration, and scheduled-job patterns for large catalogs."
intro: "Posts about <strong>catalog</strong> work in Optimizely Commerce — traversal patterns, the Content APIs, batched enumeration, and scheduled jobs that respect memory budgets."

database:
description: "Posts tagged database — SQL Server index maintenance, statistics rebuilds, and database-tooling patterns for Optimizely CMS / Commerce projects."
intro: "Posts about <strong>databases</strong> for Optimizely projects — SQL Server index and statistics maintenance, scheduled cleanup jobs, and tooling for routine DBA work."

background-jobs:
description: "Posts tagged background jobs — Hangfire integration for Optimizely CMS, scheduled-job patterns, dashboard work, and progress reporting for long-running tasks."
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."

optipowertools:
description: "Posts tagged OptiPowerTools — open-source NuGet packages for Optimizely CMS and Commerce, including Hangfire integration and SQL maintenance tooling."
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>."

optipowertools-hangfire:
description: "Posts tagged OptiPowerTools.Hangfire — the open-source NuGet package wiring Hangfire into Optimizely CMS, with dashboards, scheduled jobs, and admin gates."
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."

ci:
description: "Posts tagged CI — GitHub Actions and Azure DevOps pipelines for Optimizely CMS, including SonarCloud analysis, Playwright UI tests, and build automation."
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."

devops:
description: "Posts tagged DevOps — CI/CD pipelines, build automation, code-quality gates, and developer-experience tooling for Optimizely CMS / Commerce projects."
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."

sonarcloud:
description: "Posts tagged SonarCloud — adding code-quality gates to Optimizely CMS pipelines, .NET-specific rules, and integrating analysis into GitHub Actions."
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."

nuget:
description: "Posts tagged NuGet — packaging and distributing Optimizely CMS / Commerce libraries, semantic versioning, and the OptiPowerTools.Hangfire release flow."
intro: "Posts about <strong>NuGet</strong> packaging for Optimizely projects — semantic versioning, package metadata, and the release flow behind the OptiPowerTools.Hangfire package."
45 changes: 0 additions & 45 deletions _gentags.rb

This file was deleted.

8 changes: 8 additions & 0 deletions _includes/_all-tags.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{%- comment -%}
Captures every unique tag across site.tags into a Liquid array `all_tag_names`,
used by the sidebar and the tag cloud. Idempotent — re-including is a no-op.
{%- endcomment -%}
{%- unless all_tag_names -%}
{%- capture _all_tags_csv -%}{% for tag in site.tags %}{{ tag | first }}{% unless forloop.last %},{% endunless %}{% endfor %}{%- endcapture -%}
{%- assign all_tag_names = _all_tags_csv | split: ',' -%}
{%- endunless -%}
30 changes: 25 additions & 5 deletions _includes/_disqus.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,31 @@
this.page.url = '{{ page.url | absolute_url }}';
this.page.identifier = '{{ page.url }}';
};
(function() {
var d = document, s = d.createElement('script');
s.src = 'https://{{ site.disqus_username }}.disqus.com/embed.js';
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
(function () {
var thread = document.getElementById('disqus_thread');
if (!thread) return;
var loaded = false;
function loadDisqus() {
if (loaded) return;
loaded = true;
var s = document.createElement('script');
s.src = 'https://{{ site.disqus_username }}.disqus.com/embed.js';
s.setAttribute('data-timestamp', +new Date());
(document.head || document.body).appendChild(s);
}
if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) {
loadDisqus();
io.disconnect();
}
});
}, { rootMargin: '300px 0px' });
io.observe(thread);
} else {
loadDisqus();
}
})();
</script>
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
Expand Down
5 changes: 2 additions & 3 deletions _includes/_tagcloud.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
{% capture site_tags %}{% for tag in site.tags %}{{ tag | first }}{% unless forloop.last %},{% endunless %}{% endfor %}{% endcapture %}
{% assign site_tags = site_tags | split: ',' %}
{% include _all-tags.html %}

{% assign tag_count = 0 %}
{% for tag in site_tags %}
{% for tag in all_tag_names %}
{% assign tag_count = tag_count | plus: site.tags[tag].size %}
{% endfor %}

Expand Down
79 changes: 5 additions & 74 deletions _layouts/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,87 +54,18 @@

<aside id="sidebar">
<p class="repo-owner">Tags</p>
{% capture site_tags %}{% for tag in site.tags %}{{ tag | first }}{% unless forloop.last %},{% endunless %}{% endfor %}{% endcapture %}
{% assign tags = site_tags | split:',' | sort %}
{% include _all-tags.html %}
{% assign tags = all_tag_names | sort %}
{% include _tagcloud.html %}
{% include _badges.html %}
</aside>
</div>
</div>
{% include _footer.html %}

<script>
document.addEventListener("DOMContentLoaded", function () {
if (typeof hljs !== "undefined") {
hljs.highlightAll();
}

document.querySelectorAll(".open-code-modal").forEach(button => {
button.addEventListener("click", () => {
const targetId = button.getAttribute("data-target");
const modal = document.getElementById(targetId);
if (modal) {
modal.style.display = "block";
if (typeof hljs !== "undefined") {
modal.querySelectorAll("pre code").forEach(block => {
hljs.highlightElement(block);
});
}
} else {
console.warn("Modal not found for target:", targetId);
}
});
});

document.querySelectorAll(".code-modal-close").forEach(closeBtn => {
closeBtn.addEventListener("click", () => {
closeBtn.closest(".code-modal").style.display = "none";
});
});

window.addEventListener("click", function (event) {
document.querySelectorAll(".code-modal").forEach(modal => {
if (event.target === modal) {
modal.style.display = "none";
}
});
});

document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
document.querySelectorAll(".code-modal").forEach(modal => {
if (modal.style.display === "block") {
modal.style.display = "none";
}
});
}
});
});
</script>

<script>
document.addEventListener("click", function (event) {
var button = event.target.closest(".copy-btn");
if (!button) return;
var code = button.nextElementSibling.querySelector("code");
if (!code) return;
var text = code.innerText;

navigator.clipboard.writeText(text).then(function () {
button.classList.add("copied");
button.innerHTML = "<i class='fas fa-check'></i> Copied!";
setTimeout(function () {
button.classList.remove("copied");
button.innerHTML = "<i class='fas fa-copy'></i> Copy";
}, 1500);
}).catch(function () {
button.innerHTML = "<i class='fas fa-times'></i> Failed";
setTimeout(function () {
button.innerHTML = "<i class='fas fa-copy'></i> Copy";
}, 1500);
});
});
</script>
{%- if page.layout == "post" -%}
<script defer src="{{ '/assets/js/code-modal.js' | relative_url }}"></script>
{%- endif -%}

</body>
</html>
61 changes: 61 additions & 0 deletions _plugins/tag_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
module Jekyll
# Generates a tag page (layout: tagpage, permalink: /tags/<slug>) for every
# unique tag found across site.posts. Hand-authored intros + meta descriptions
# for prominent tags live in _data/tag_descriptions.yml, keyed by slug; tags
# without an entry get a generic stub.
#
# Replaces the old standalone _gentags.rb script — runs on every jekyll build,
# so tag pages cannot drift out of sync with post frontmatter.
class TagPageGenerator < Generator
safe true
priority :normal

GENERIC_DESCRIPTION = "Posts on Szołkowski's Blog tagged %s — Optimizely CMS, .NET, and developer-experience writing.".freeze

def generate(site)
descriptions = site.data['tag_descriptions'] || {}

tags = site.posts.docs
.flat_map { |post| Array(post.data['tags']) }
.map { |t| t.to_s.strip }
.reject(&:empty?)
.uniq { |t| t.downcase }

tags.each do |tag|
site.pages << build_tag_page(site, tag, descriptions)
end
end

private

def build_tag_page(site, tag, descriptions)
slug = Jekyll::Utils.slugify(tag)
entry = descriptions[slug] || {}
desc = entry['description'] || (GENERIC_DESCRIPTION % tag)

page = PageWithoutAFile.new(site, site.source, 'tags', "#{slug}.html")
page.data.merge!(
'layout' => 'tagpage',
'tag' => tag,
'title' => "Posts tagged #{tag}",
'description' => desc,
'permalink' => "/tags/#{slug}",
# Set explicitly so jekyll-last-modified-at doesn't try to git-mtime a
# file that exists only in memory. Use the most-recent post date for
# this tag so dateModified actually means something.
'last_modified_at' => latest_post_date_for(site, slug)
)
page.data['intro'] = entry['intro'] if entry['intro']
page.content = ''
page
end

def latest_post_date_for(site, slug)
matching = site.posts.docs.select do |post|
Array(post.data['tags']).any? { |t| Jekyll::Utils.slugify(t.to_s) == slug }
end
return site.time if matching.empty?
matching.map(&:date).max
end
end
end
68 changes: 68 additions & 0 deletions assets/js/code-modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
document.addEventListener("DOMContentLoaded", function () {
if (typeof hljs !== "undefined") {
hljs.highlightAll();
}

document.querySelectorAll(".open-code-modal").forEach(function (button) {
button.addEventListener("click", function () {
var targetId = button.getAttribute("data-target");
var modal = document.getElementById(targetId);
if (modal) {
modal.style.display = "block";
if (typeof hljs !== "undefined") {
modal.querySelectorAll("pre code").forEach(function (block) {
hljs.highlightElement(block);
});
}
} else {
console.warn("Modal not found for target:", targetId);
}
});
});

document.querySelectorAll(".code-modal-close").forEach(function (closeBtn) {
closeBtn.addEventListener("click", function () {
closeBtn.closest(".code-modal").style.display = "none";
});
});

window.addEventListener("click", function (event) {
document.querySelectorAll(".code-modal").forEach(function (modal) {
if (event.target === modal) {
modal.style.display = "none";
}
});
});

document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
document.querySelectorAll(".code-modal").forEach(function (modal) {
if (modal.style.display === "block") {
modal.style.display = "none";
}
});
}
});
});

document.addEventListener("click", function (event) {
var button = event.target.closest(".copy-btn");
if (!button) return;
var code = button.nextElementSibling.querySelector("code");
if (!code) return;
var text = code.innerText;

navigator.clipboard.writeText(text).then(function () {
button.classList.add("copied");
button.innerHTML = "<i class='fas fa-check'></i> Copied!";
setTimeout(function () {
button.classList.remove("copied");
button.innerHTML = "<i class='fas fa-copy'></i> Copy";
}, 1500);
}).catch(function () {
button.innerHTML = "<i class='fas fa-times'></i> Failed";
setTimeout(function () {
button.innerHTML = "<i class='fas fa-copy'></i> Copy";
}, 1500);
});
});
8 changes: 0 additions & 8 deletions tags/apple-silicon.html

This file was deleted.

Loading
Loading