@@ -4,6 +4,7 @@ import Navigation from "../../components/base/Navigation.astro";
44import ProductLayout from " ../../layouts/ProductLayout.astro" ;
55import FlexibleSection from " ../../components/FlexibleSection.astro" ;
66import BlogListJSONLD from " ../../scripts/BlogListJSONLD.astro" ;
7+ import { blogFilterTags } from " ../../data/blog-filter-tags" ;
78
89// Get all blog posts, sorted by date
910const allPosts = await getCollection (" blog" , ({ data }) => {
@@ -16,6 +17,9 @@ const sortedPosts = allPosts.sort(
1617 new Date (b .data .publishDate ).valueOf () - new Date (a .data .publishDate ).valueOf (),
1718);
1819
20+ // Filter pills: show all configured tags in config order (so edits to blogFilterTags are visible immediately)
21+ const allTags = [... blogFilterTags ];
22+
1923// Default fallback image for posts without images
2024const defaultImage = " /images/png/defguard.png" ;
2125
@@ -104,6 +108,14 @@ const blogTags = ["defguard", "blog", "vpn", "wireguard", "cybersecurity", "ente
104108
105109 <section class =" blog-posts-listing" >
106110 <div class =" container" >
111+ { sortedPosts .length > 0 && allTags .length > 0 && (
112+ <div class = " blog-tag-filter" role = " group" aria-label = " Filter by tag" >
113+ <button type = " button" class = " filter-pill active" data-tag = " " >All</button >
114+ { allTags .map ((tag ) => (
115+ <button type = " button" class = " filter-pill" data-tag = { tag } >{ tag } </button >
116+ ))}
117+ </div >
118+ )}
107119 {
108120 sortedPosts .length === 0 ? (
109121 <div class = " no-posts" >
@@ -112,7 +124,11 @@ const blogTags = ["defguard", "blog", "vpn", "wireguard", "cybersecurity", "ente
112124 ) : (
113125 <div class = " posts-list" >
114126 { sortedPosts .map ((post , index ) => (
115- <a href = { ` /blog/${post .slug } ` } class = " post-row" >
127+ <a
128+ href = { ` /blog/${post .slug } ` }
129+ class = " post-row"
130+ data-tags = { post .data .tags ?.map ((t ) => t .toLowerCase ()).join (" ," ) ?? " " }
131+ >
116132 <div class = " post-thumbnail" >
117133 <img
118134 src = { getPostImage (post )}
@@ -159,6 +175,63 @@ const blogTags = ["defguard", "blog", "vpn", "wireguard", "cybersecurity", "ente
159175 </section >
160176 </main >
161177
178+ <script >
179+ const tagParam = "tag";
180+
181+ function getTagFromUrl(): string {
182+ const params = new URLSearchParams(window.location.search);
183+ const raw = params.get(tagParam);
184+ return raw ? raw.toLowerCase().trim() : "";
185+ }
186+
187+ function setTagInUrl(tag: string): void {
188+ const url = new URL(window.location.href);
189+ if (tag) {
190+ url.searchParams.set(tagParam, tag);
191+ } else {
192+ url.searchParams.delete(tagParam);
193+ }
194+ window.history.replaceState({ tag }, "", url.toString());
195+ }
196+
197+ function applyFilter(needle: string): void {
198+ const pillTags = Array.from(document.querySelectorAll(".blog-tag-filter .filter-pill")).map(
199+ (b) => ((b as HTMLButtonElement).dataset.tag ?? "").toLowerCase()
200+ );
201+ const validNeedle = needle && pillTags.includes(needle) ? needle : "";
202+ if (needle && !validNeedle) {
203+ setTagInUrl("");
204+ }
205+ document.querySelectorAll(".blog-tag-filter .filter-pill").forEach((b) => {
206+ const pillTag = (b as HTMLButtonElement).dataset.tag ?? "";
207+ const isActive = (pillTag.toLowerCase() === validNeedle) || (!validNeedle && pillTag === "");
208+ b.classList.toggle("active", isActive);
209+ });
210+ document.querySelectorAll(".posts-list .post-row").forEach((row) => {
211+ const tags = (row.getAttribute("data-tags") ?? "").split(",").filter(Boolean);
212+ const show = !validNeedle || tags.some((t) => t === validNeedle);
213+ (row as HTMLElement).style.display = show ? "" : "none";
214+ });
215+ }
216+
217+ function initFromUrl(): void {
218+ const tag = getTagFromUrl();
219+ applyFilter(tag);
220+ }
221+
222+ document.querySelectorAll(".blog-tag-filter .filter-pill").forEach((btn) => {
223+ btn.addEventListener("click", (e) => {
224+ e.preventDefault();
225+ const tag = (e.currentTarget as HTMLButtonElement).dataset.tag ?? "";
226+ const needle = tag.toLowerCase();
227+ setTagInUrl(needle);
228+ applyFilter(needle);
229+ });
230+ });
231+
232+ initFromUrl();
233+ </script >
234+
162235 <style lang =" scss" >
163236 @use "../../styles/mixins/typography" as *;
164237 @use "../../styles/mixins/breakpoints" as *;
@@ -177,6 +250,38 @@ const blogTags = ["defguard", "blog", "vpn", "wireguard", "cybersecurity", "ente
177250 padding: 0 1rem;
178251 }
179252
253+ .blog-tag-filter {
254+ display: flex;
255+ flex-wrap: wrap;
256+ gap: 0.5rem;
257+ margin-bottom: 1.5rem;
258+ padding: 0.25rem 0;
259+ }
260+
261+ .filter-pill {
262+ padding: 0.4rem 0.9rem;
263+ font-size: 0.875rem;
264+ font-weight: 500;
265+ color: var(--text-body-secondary, #555);
266+ background: #fff;
267+ border: 1px solid #e0e0e0;
268+ border-radius: 999px;
269+ cursor: pointer;
270+ transition: background-color 0.2s, border-color 0.2s, color 0.2s;
271+
272+ &:hover {
273+ background: #f5f5f5;
274+ border-color: #ccc;
275+ color: var(--text-body-primary);
276+ }
277+
278+ &.active {
279+ background: var(--primary-button-bg, #0c8ce0);
280+ border-color: var(--primary-button-bg, #0c8ce0);
281+ color: #fff;
282+ }
283+ }
284+
180285 // Posts List
181286 .posts-list {
182287 display: flex;
0 commit comments