hi
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 1.1 MiB |
BIN
assets/images/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.0 KiB |
@ -26,7 +26,7 @@ banner:
|
||||
enable: true
|
||||
label: "Download Resume"
|
||||
isDownloadable: true
|
||||
link: "http://cdn.alpin.sbs/resume.pdf"
|
||||
link: "https://cdn.joren.blog/resume.pdf"
|
||||
|
||||
|
||||
# skill
|
||||
|
@ -7,8 +7,6 @@ categories: ["cybersecurity", "event"]
|
||||
draft: false
|
||||
---
|
||||
|
||||
#### On a Mission with NATO: Cyber Defence on the Frontline
|
||||
|
||||
Howest’s ongoing commitment to world-class cybersecurity training took center stage again as six lecturers from the Cyber Security program joined forces with experts from Latvia, Luxembourg and Belgium in one of NATO's most intensive simulations: the Locked Shields exercise, hosted by the Cooperative Cyber Defence Centre of Excellence (CCDCOE). Now in its fifth year of participation, the Howest team shared their firsthand experiences at a special evening talk at Howest Bruges.
|
||||
|
||||
#### The Exercise: Locked Shields
|
||||
|
@ -14,7 +14,7 @@ Smart homes are convenient. But with convenience comes risk. If your doorbell ru
|
||||
|
||||
---
|
||||
|
||||
### Why Segmentation Matters
|
||||
#### Why Segmentation Matters
|
||||
|
||||
Most people treat their home network like a trust zone. All devices are equal. But they’re not. You wouldn’t let your robot vacuum log into your online banking, yet they live on the same flat LAN. That’s the fundamental issue.
|
||||
|
||||
@ -29,7 +29,7 @@ A compromise is inevitable. The only question is: does that compromise stay loca
|
||||
|
||||
---
|
||||
|
||||
### VLANs 101
|
||||
#### VLANs 101
|
||||
|
||||
A VLAN (Virtual Local Area Network) logically segments traffic on the same physical infrastructure. Think of it as creating isolated “subnet bubbles” where traffic can be controlled and filtered.
|
||||
|
||||
@ -44,7 +44,7 @@ And you don’t need enterprise gear to do this. Many consumer-grade routers and
|
||||
|
||||
---
|
||||
|
||||
### Sample Home Setup
|
||||
#### Sample Home Setup
|
||||
|
||||
Let’s say you have a smart home with:
|
||||
|
||||
@ -80,7 +80,7 @@ Optional: Use static DHCP leases and force DNS through Pi-hole for logging and f
|
||||
|
||||
---
|
||||
|
||||
### Real-World Examples
|
||||
#### Real-World Examples
|
||||
|
||||
**Case 1: Smart TV**
|
||||
|
||||
@ -100,7 +100,7 @@ On the LAN, it had access to the NAS and router UI. After VLAN isolation, its ac
|
||||
|
||||
---
|
||||
|
||||
### Caveats & Limitations
|
||||
#### Caveats & Limitations
|
||||
|
||||
* Some IoT devices rely on MDNS or SSDP for pairing/setup. Consider temporarily whitelisting during setup, then blocking.
|
||||
* Chromecast-style devices need special rules if you want casting from your main network.
|
||||
@ -111,7 +111,7 @@ Still, the benefits far outweigh the complexity.
|
||||
|
||||
---
|
||||
|
||||
### Final Thoughts
|
||||
#### Final Thoughts
|
||||
|
||||
If you’ve ever installed a smart plug and noticed it phones home every few minutes, you're not alone. And if you haven't noticed, maybe you should.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Reversing, Rebuilding, and Failing Better: My Cyber Security Challenge Belgium Qualifier Experience"
|
||||
image: "images/blog/post-2/binexpl.png"
|
||||
image: "images/blog/blog-2.jpg"
|
||||
date: "2025-03-15 00:00:00 +0000 UTC"
|
||||
description: "I tackled buffer overflows, reversed Android apps, cracked crypto puzzles, and solved a 'one-in-a-million' guessing game, but the challenge that stuck with me was rebuilding a fragmented DEX in memory."
|
||||
categories: ["cybersecurity", "CTF", "education"]
|
||||
|
@ -50,4 +50,3 @@ Priced under 150 EUR, the SARV001 offers features typically found in higher-end
|
||||
|
||||
The Seiko SARV001 exemplifies the brand's commitment to craftsmanship and value. Its understated design, robust movement, and unique JDM characteristics make it a compelling option for those looking to add a versatile and reliable watch to their collection.
|
||||
|
||||
---
|
||||
|
@ -30,12 +30,12 @@ A `.drmd` file is a structured JSON document that defines one or more encrypted
|
||||
|
||||
Each item includes:
|
||||
|
||||
* **MPD**: A DASH manifest, either a direct URL or a base64-encoded version. If base64-encoded, DRMDTool decodes and temporarily saves it before use.
|
||||
* **Keys**: A comma-separated list of **KID\:key** pairs (e.g., `abcd1234ef567890:00112233445566778899aabbccddeeff`). These are required for decrypting encrypted media streams and are passed directly to N\_m3u8DL-RE using `--key` flags.
|
||||
* **Filename**: The name to be used for the final output file.
|
||||
* **Subtitles**: Comma-separated list of subtitle URLs in `.vtt` format. DRMDTool downloads and converts these to `.srt`, then muxes them into the final file.
|
||||
* **Metadata**: A semicolon-separated string like `Title;Type;Season` (e.g., `Example Show;serie;1`) used to determine directory structure (`Movies/Title` or `Series/Title/Season`).
|
||||
* **Description** and **Poster**: Optional fields used only for display in the web UI.
|
||||
* MPD: A DASH manifest, either a direct URL or a base64-encoded version. If base64-encoded, DRMDTool decodes and temporarily saves it before use.
|
||||
* Keys: A comma-separated list of **KID\:key** pairs (e.g., `abcd1234ef567890:00112233445566778899aabbccddeeff`). These are required for decrypting encrypted media streams and are passed directly to N\_m3u8DL-RE using `--key` flags.
|
||||
* Filename: The name to be used for the final output file.
|
||||
* Subtitles: Comma-separated list of subtitle URLs in `.vtt` format. DRMDTool downloads and converts these to `.srt`, then muxes them into the final file.
|
||||
* Metadata**: A semicolon-separated string like `Title;Type;Season` (e.g., `Example Show;serie;1`) used to determine directory structure (`Movies/Title` or `Series/Title/Season`).
|
||||
* Description and Poster: Optional fields used only for display in the web UI.
|
||||
|
||||
##### Example `.drmd` Structure
|
||||
|
||||
@ -57,11 +57,11 @@ Each item includes:
|
||||
|
||||
#### Processing Steps
|
||||
|
||||
1. **Detection**: DRMDTool either watches a folder or receives `.drmd` uploads through the web UI.
|
||||
2. **Validation**: It waits for the file to finish writing (based on file size stability), then parses its contents.
|
||||
3. **MPD Handling**: If base64-encoded, the MPD is decoded and written to a temp file; otherwise, the URL is fetched or passed as-is.
|
||||
4. **Command Generation**: Using the MPD, `KID:key` pairs, output paths, and subtitles, DRMDTool builds a command line for N\_m3u8DL-RE.
|
||||
5. **Execution**: The download is launched with live progress tracking. Users can pause, resume, or abort jobs, and optionally stream console output via WebSocket.
|
||||
1. Detection: DRMDTool either watches a folder or receives `.drmd` uploads through the web UI.
|
||||
2. Validation: It waits for the file to finish writing (based on file size stability), then parses its contents.
|
||||
3. MPD Handling: If base64-encoded, the MPD is decoded and written to a temp file; otherwise, the URL is fetched or passed as-is.
|
||||
4. Command Generation: Using the MPD, `KID:key` pairs, output paths, and subtitles, DRMDTool builds a command line for N\_m3u8DL-RE.
|
||||
5. Execution: The download is launched with live progress tracking. Users can pause, resume, or abort jobs, and optionally stream console output via WebSocket.
|
||||
|
||||
These files serve as portable job definitions. When DRMDTool detects or receives a `.drmd` file, it parses the items, decodes or downloads the MPD, applies the keys, and builds a download command using N\_m3u8DL-RE. Files are saved in organized directories like `Movies/Title` or `Series/Title/Season`, and subtitles are embedded if available. Pausing, resuming, and aborting downloads is supported per file.
|
||||
|
||||
|
@ -31,7 +31,7 @@ Blocky handles all local DNS queries, with DoT upstreams, custom mappings, and d
|
||||
|
||||
Highlights:
|
||||
|
||||
* Local resolution for custom domains like `directme.in`
|
||||
* Local resolution for custom domains like `joren.blog`
|
||||
* Cloudflare, Google as upstream resolvers
|
||||
* Per-IP blocking rules
|
||||
* Prometheus metrics for monitoring
|
||||
@ -48,7 +48,7 @@ blocking:
|
||||
clientGroupsBlock:
|
||||
default:
|
||||
- ads
|
||||
192.168.178.123:
|
||||
172.16.0.120:
|
||||
- vtm
|
||||
```
|
||||
|
||||
@ -83,8 +83,6 @@ In this context, PiVPN reduces the friction of managing WireGuard while remainin
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
#### IRC: ngIRCd
|
||||
|
||||
For real-time messaging, I run a public-facing **ngIRCd** instance accessible over both plaintext (port 6667) and encrypted TLS (ports 6697, 6698). Despite its modest footprint, *ngIRCd* is stable, portable, and well-suited for both LAN and internet-facing deployments.
|
||||
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
|
||||
title: "Sniffing on a Budget: Malware Detection with Suricata and Alpine Linux"
|
||||
title: "Network Sniffing on a Budget: Malware Detection with Suricata"
|
||||
image: "/images/project/project-4.jpg"
|
||||
date: "2025-04-15 00:00:00 +0000 UTC"
|
||||
description: "Setting up Suricata on minimal hardware to passively inspect mirrored traffic for malware and exploits using open threat intelligence rules."
|
||||
|
@ -204,12 +204,10 @@
|
||||
"p-0",
|
||||
"p-1",
|
||||
"p-2",
|
||||
"p-3",
|
||||
"p-4",
|
||||
"pb-1",
|
||||
"pb-2",
|
||||
"pb-3",
|
||||
"pb-4",
|
||||
"pb-5",
|
||||
"pe-3",
|
||||
"position-fixed",
|
||||
@ -221,7 +219,6 @@
|
||||
"progress-bar",
|
||||
"progress-item",
|
||||
"progress-value",
|
||||
"project-item",
|
||||
"projects",
|
||||
"ps-0",
|
||||
"ps-lg-4",
|
||||
@ -321,7 +318,6 @@
|
||||
"name-resolution-chaos",
|
||||
"navbar",
|
||||
"offensive-vs-defensive-security",
|
||||
"on-a-mission-with-nato-cyber-defence-on-the-frontline",
|
||||
"ourencissec-zip-bombs-and-oeis",
|
||||
"phone",
|
||||
"portfolio",
|
||||
|
@ -18314,46 +18314,6 @@ html.light .navbar-nav .nav-item a.nav-link:hover {
|
||||
max-height: 600px;
|
||||
object-fit: cover; }
|
||||
|
||||
.project-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%; }
|
||||
|
||||
.project-item > a {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9; }
|
||||
|
||||
.project-item > a img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
display: block;
|
||||
transition: transform 0.4s ease; }
|
||||
|
||||
.project-item > a:hover img {
|
||||
transform: scale(1.05); }
|
||||
|
||||
.project-item h3, .project-item .h3 {
|
||||
min-height: 3.5em;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500; }
|
||||
|
||||
.project-item .card-text {
|
||||
flex-grow: 1;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem; }
|
||||
|
||||
.project-item .card-btn {
|
||||
font-weight: 500;
|
||||
margin-top: auto; }
|
||||
|
||||
#blog {
|
||||
position: relative; }
|
||||
#blog::before {
|
||||
|
@ -1 +1 @@
|
||||
{"Target":"/css/style.e0f0e604485dadc53beafefb6a6887ddbbfd8348186bc86d1fc9e99ad8162ccd.css","MediaType":"text/css","Data":{"Integrity":"sha256-4PDmBEhdrcU76v77amiH3bv9g0gYa8htH8npmtgWLM0="}}
|
||||
{"Target":"/css/style.30c2f870a09379db333105ec31b030d3bad723d3ad8e71a83f60db00f48d4f85.css","MediaType":"text/css","Data":{"Integrity":"sha256-MML4cKCTedszMQXsMbAw07rXI9OtjnGoP2DbAPSNT4U="}}
|
@ -8817,46 +8817,6 @@ html.light .navbar-nav .nav-item a.nav-link:hover {
|
||||
max-height: 600px;
|
||||
object-fit: cover; }
|
||||
|
||||
.project-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%; }
|
||||
|
||||
.project-item > a {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9; }
|
||||
|
||||
.project-item > a img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
display: block;
|
||||
transition: transform 0.4s ease; }
|
||||
|
||||
.project-item > a:hover img {
|
||||
transform: scale(1.05); }
|
||||
|
||||
.project-item h3, .project-item .h3 {
|
||||
min-height: 3.5em;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500; }
|
||||
|
||||
.project-item .card-text {
|
||||
flex-grow: 1;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem; }
|
||||
|
||||
.project-item .card-btn {
|
||||
font-weight: 500;
|
||||
margin-top: auto; }
|
||||
|
||||
#blog {
|
||||
position: relative; }
|
||||
#blog::before {
|
||||
|
After Width: | Height: | Size: 370 B |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 364 B |
After Width: | Height: | Size: 126 B |
BIN
resources/_gen/images/images/blog/blog-2_hu_57390d5858ed789.jpg
Normal file
After Width: | Height: | Size: 656 B |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 232 B |
BIN
resources/_gen/images/images/blog/blog-2_hu_672cb15fee3825f0.jpg
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
resources/_gen/images/images/blog/blog-2_hu_728150f6097443fa.jpg
Normal file
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 28 KiB |
BIN
resources/_gen/images/images/blog/blog-2_hu_89c109e000ed8c9.webp
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
resources/_gen/images/images/blog/blog-2_hu_9f875c2f58006099.jpg
Normal file
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 58 KiB |
BIN
resources/_gen/images/images/blog/blog-2_hu_ae22ee6b650bfda2.jpg
Normal file
After Width: | Height: | Size: 648 B |
After Width: | Height: | Size: 112 B |
After Width: | Height: | Size: 228 B |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 8.6 KiB |
BIN
resources/_gen/images/images/favicon_hu_3e8ef64f51702173.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
resources/_gen/images/images/favicon_hu_44e6c8b37eb36d59.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
resources/_gen/images/images/favicon_hu_6c15ef0eeacaf617.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
resources/_gen/images/images/favicon_hu_80c72ef352d70226.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
resources/_gen/images/images/favicon_hu_ad05c28a032bed34.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
resources/_gen/images/images/favicon_hu_baf4ac5f8b02350c.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
resources/_gen/images/images/favicon_hu_be7dfdbacefd23c7.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
resources/_gen/images/images/favicon_hu_d11aaba54c3c98fd.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
resources/_gen/images/images/favicon_hu_d8371c65696cde6b.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
@ -311,6 +311,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.project-item,
|
||||
.blog-post {
|
||||
.card {
|
||||
@ -442,53 +443,7 @@
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.project-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.project-item > a {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.project-item > a img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
display: block;
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
|
||||
.project-item > a:hover img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
|
||||
.project-item h3 {
|
||||
min-height: 3.5em; // equal height titles
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.project-item .card-text {
|
||||
flex-grow: 1;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.project-item .card-btn {
|
||||
font-weight: 500;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
|
||||
// -----------------------
|
||||
|
@ -1,7 +1,7 @@
|
||||
{{define "main"}}
|
||||
|
||||
<!-- checking blog -->
|
||||
{{ if or (eq .Section "post") (eq .Section "posts") (eq .Section "blog") (eq .Section "blogs") (eq .Section "news") (eq .Section "categories") (eq .Section "tags") }}
|
||||
{{ if or (eq .Section "post") (eq .Section "posts") (eq .Section "blog") (eq .Section "blogs") (eq .Section "news") (eq .Section "categories") (eq .Section "tags") (eq .Section "project") }}
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
|
@ -1,18 +1,40 @@
|
||||
<div class="project-item mb-4">
|
||||
{{ if .Params.image }}
|
||||
<a href="{{ .RelPermalink | urlize }}" class="overflow-hidden d-block mb-3" data-aos="fade-up">
|
||||
{{ partial "image.html" (dict "Src" .Params.image "Alt" .Params.title "Class" "rounded-2 w-100") }}
|
||||
</a>
|
||||
{{ end }}
|
||||
<div data-aos="fade-up" data-aos-delay="50">
|
||||
<h3 class="h5 fw-medium text-capitalize mb-2">
|
||||
<a class="text-white" href="{{ .RelPermalink }}">{{ .Params.title | markdownify }}</a>
|
||||
</h3>
|
||||
<p class="card-text mb-3">{{ .Params.description | truncate 115 }}</p>
|
||||
</div>
|
||||
<div class="blog-post mb-4">
|
||||
<article class="card bg-transparent border-0 p-1">
|
||||
{{ if .Params.image }}
|
||||
<a href="{{ .RelPermalink | urlize }}" class="rounded-2 overflow-hidden" data-aos="fade-up">
|
||||
{{ partial "image.html" (dict "Src" .Params.image "Alt" .Params.title "Class" "w-100") }}
|
||||
</a>
|
||||
{{ end }}
|
||||
|
||||
<a class="card-btn text-primary d-inline-block" href="{{ .RelPermalink | urlize }}" data-aos="fade-up" data-aos-delay="100">
|
||||
Discover <i class="fa-solid fa-arrow-right-long"></i>
|
||||
</a>
|
||||
<div class="card-body pt-4 px-0">
|
||||
<div class="post-meta mb-3 mt-2" data-aos="fade-up" data-aos-delay="50">
|
||||
{{ with .Params.date }}
|
||||
<div class="post-date mb-1">
|
||||
<i class="fa-solid fa-calendar-days me-2"></i>{{ . | time.Format "02 Jan 2006" }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ with .Params.tags }}
|
||||
<div class="post-categories">
|
||||
<i class="fa-solid fa-folder-open me-2"></i>
|
||||
{{ range $index, $tag := . }}
|
||||
{{ if $index }}, {{ end }}
|
||||
<span class="meta-link">{{ $tag }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div data-aos="fade-up" data-aos-delay="100">
|
||||
<h3 class="h5 card-title fw-normal mb-3">
|
||||
<a class="text-white" href="{{ .RelPermalink }}">{{ .Params.title | markdownify }}</a>
|
||||
</h3>
|
||||
<p class="card-text mb-4">{{ .Params.description | truncate 80 }}</p>
|
||||
</div>
|
||||
|
||||
<a href="{{ .RelPermalink }}" class="card-btn text-primary d-inline-block" data-aos="fade-up" data-aos-delay="150">
|
||||
View Project <i class="fa-solid fa-arrow-right-long"></i>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
|
@ -259,7 +259,7 @@
|
||||
</div>
|
||||
|
||||
<div class="projects align-items-start overflow-hidden">
|
||||
{{ range .Pages.ByDate.Reverse }}
|
||||
{{ range first 6 .Pages.ByDate.Reverse }}
|
||||
{{ .Render "project-card" }}
|
||||
{{ end }}
|
||||
</div>
|
||||
@ -293,7 +293,7 @@
|
||||
</div>
|
||||
|
||||
<div class="blog-wrapper">
|
||||
{{ range .Pages.ByDate.Reverse }}
|
||||
{{ range first 6 .Pages.ByDate.Reverse }}
|
||||
{{ .Render "blog-card" }}
|
||||
{{ end }}
|
||||
</div>
|
||||
|
@ -1,42 +1,25 @@
|
||||
{{define "main"}}
|
||||
{{ define "main" }}
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-xxl-11 mx-auto">
|
||||
<div class="mb-5 pb-2">
|
||||
{{ if or (eq .Section "categories") (eq .Section "tags") }}
|
||||
<p class="mb-3">Category</p>
|
||||
{{ end }}
|
||||
<h2 class="h3"><span class="text-primary pe-3 small">//</span>{{.Title | title}}</h2>
|
||||
<h2 class="h3">
|
||||
<span class="text-primary pe-3 small">//</span>{{ .Title | title }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="projects align-items-start overflow-hidden">
|
||||
{{range .Data.Pages}}
|
||||
<div class="project-item mb-4">
|
||||
<div class="card bg-dark rounded-3">
|
||||
{{if .Params.image}}
|
||||
<a href="{{.RelPermalink | urlize}}" class="overflow-hidden p-3">
|
||||
{{ partial "image.html" (dict "Src" .Params.image "Alt" .Params.title "Class" `rounded-2`) }}
|
||||
</a>
|
||||
{{ end }}
|
||||
<div class="card-body pb-4">
|
||||
<div class="px-2">
|
||||
<h3 class="h5 card-title fw-medium text-capitalize">
|
||||
<a class="text-white" href="{{.RelPermalink}}">{{.Params.title | markdownify}}</a>
|
||||
</h3>
|
||||
<p class="card-text mb-3">{{.Params.description | truncate 115}}</p>
|
||||
|
||||
<a class="card-btn text-primary" href="{{.RelPermalink | urlize}}">Discover<i class="fa-solid fa-arrow-right-long"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="blog-wrapper">
|
||||
{{ range .Pages }}
|
||||
{{ .Render "project-card" }}
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{end}}
|
||||
{{ end }}
|
||||
|
||||
|