Skip to content

Commit 7ae53ea

Browse files
Add meta images for blog series and introduce a script to extract series metadata from blog posts.
1 parent 2fcc0ba commit 7ae53ea

File tree

3 files changed

+185
-12
lines changed

3 files changed

+185
-12
lines changed

data/blog_series.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,46 +5,58 @@ series:
55
- slug: "platform-engineering-pillars"
66
title: "Platform Engineering Pillars Series"
77
description: "Six essential pillars for transforming infrastructure chaos into streamlined developer platforms: provisioning, self-service, workflows, security, observability, and governance."
8+
meta_image: "/blog/platform-engineering-pillars-1/meta.png"
89
- slug: "ai-slack-bot"
910
title: "AI Slack Bot Series"
1011
description: "Build AI-powered Slack bots using RAG, Embedchain, and AWS infrastructure with Pulumi, from basic chatbots to production-ready systems with real-time document processing."
12+
meta_image: "/blog/ai-slack-bot-to-chat-using-embedchain-and-pulumi-on-aws/meta.png"
1113
- slug: "iac-best-practices"
1214
title: "IaC Best Practices Series"
1315
description: "Master Infrastructure as Code through a real-world case study: code organization, stack management, developer workflows, security patterns, and automation strategies for containerized applications on Kubernetes."
1416
prefix: "IaC Best Practices: "
17+
meta_image: "/blog/iac-best-practices-understanding-code-organization-stacks/meta.png"
1518
- slug: "aws-networking-advanced"
1619
title: "Advanced AWS Networking Series"
1720
description: "Build hub-and-spoke network architecture in AWS with centralized egress and traffic inspection using Pulumi. Covers Transit Gateway setup, inspection VPC implementation, spoke VPC configuration, cost optimization through shared NAT gateways, and AWS Network Firewall integration for centralized security policies."
1821
prefix: "Advanced AWS Networking: "
22+
meta_image: "/blog/advanced-aws-networking-part-1/aws-advanced-networking-part-1.png"
1923
- slug: "organizational-patterns"
2024
title: "Organizational Patterns Series"
2125
description: "Platform teams implementing infrastructure patterns to enable developer self-service through centralized repositories, automation teams, and developer portals using Pulumi"
2226
prefix: "Organizational Patterns: "
27+
meta_image: "/blog/organizational-patterns-infra-repo/meta.png"
2328
- slug: "cloud-systems"
2429
title: "Cloud Systems Series"
2530
description: "Hands-on guide to modern cloud engineering practices: build and deploy a personal website through static hosting (S3), containerization (Docker), and orchestration (AWS ECS/Fargate) using infrastructure as code"
2631
prefix: "Cloud Systems: "
32+
meta_image: "/blog/cloud-systems-part-one/meta.png"
2733
- slug: "kubernetes-fundamentals"
2834
title: "Kubernetes Fundamentals Series"
2935
description: "Learn container orchestration from local development to managed cloud services - deploy containerized apps, manage scaling, understand cluster architecture, and bridge Dev/Ops workflows using YAML and infrastructure as code with Pulumi."
3036
prefix: "Kubernetes Fundamentals: "
37+
meta_image: "/blog/kubernetes-fundamentals-part-one/k8s-fundamentals.png"
3138
- slug: "azure-top-5"
3239
title: "Top 5 Things for Azure Developers Series"
3340
description: "Master virtual machines, serverless functions, static websites, Kubernetes cluster deployment, and DevOps automation on Azure"
3441
prefix: "Top 5 Things an Azure Developer Needs to Know: "
42+
meta_image: "/blog/top-5-things-for-azure-devs-intro/azure-top-5.png"
3543
- slug: "embrace-kubernetes"
3644
title: "Embrace Kubernetes Series"
3745
description: "Learn strategic Kubernetes adoption: evaluate organizational readiness, assess technical requirements, identify suitable first projects, and implement Kubernetes where it delivers real business value"
3846
prefix: "It's Time to Embrace Kubernetes: "
47+
meta_image: "/blog/embrace-kubernetes-part1/embrace-k8s.png"
3948
- slug: "kubernetes-getting-started"
4049
title: "Getting Started with Kubernetes Series"
4150
description: "Build Kubernetes clusters on AWS, Azure, GCP and deploy applications using Infrastructure as Code. Learn core objects like pods, services, volumes, deployments and move from basic setups to complex microservices."
4251
prefix: "Getting Started With Kubernetes: "
52+
meta_image: "/blog/getting-started-with-k8s-part1/getting-started.png"
4353
- slug: "aws-credentials-cicd"
4454
title: "Managing AWS Credentials on CI/CD Series"
4555
description: "Step-by-step guide to secure AWS credentials in CI/CD pipelines: create dedicated IAM users, automate credential rotation, assume IAM roles for temporary access, and encrypt sensitive data with Pulumi. Includes serverless automation code and multi-account security patterns."
4656
prefix: "Managing AWS Credentials on CI/CD:"
57+
meta_image: "/blog/managing-aws-credentials-on-cicd-part-1/meta.png"
4758
- slug: "architecture-as-code"
4859
title: "Architecture as Code Series"
4960
description: "Learn how to abstract infrastructure components across virtual machines, serverless, Kubernetes, and microservices architectures using modern programming languages with Pulumi"
5061
prefix: "Architecture as Code: "
62+
meta_image: "/blog/architecture-as-code-intro/architecture.png"

extract_series_meta_images.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/usr/bin/env python3
2+
import os
3+
from datetime import datetime
4+
import re
5+
from pathlib import Path
6+
7+
def extract_frontmatter(file_path):
8+
"""Extract key-value pairs from YAML frontmatter manually."""
9+
try:
10+
with open(file_path, 'r', encoding='utf-8') as f:
11+
content = f.read()
12+
13+
# Look for frontmatter between --- delimiters
14+
match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
15+
if not match:
16+
return None
17+
18+
frontmatter_text = match.group(1)
19+
20+
# Simple parsing for the fields we need
21+
data = {}
22+
23+
# Extract series
24+
series_match = re.search(r'^series:\s*(.+)$', frontmatter_text, re.MULTILINE)
25+
if series_match:
26+
data['series'] = series_match.group(1).strip().strip('"').strip("'")
27+
28+
# Extract date
29+
date_match = re.search(r'^date:\s*(.+)$', frontmatter_text, re.MULTILINE)
30+
if date_match:
31+
data['date'] = date_match.group(1).strip().strip('"').strip("'")
32+
33+
# Extract meta_image
34+
meta_image_match = re.search(r'^meta_image:\s*(.+)$', frontmatter_text, re.MULTILINE)
35+
if meta_image_match:
36+
data['meta_image'] = meta_image_match.group(1).strip().strip('"').strip("'")
37+
38+
return data
39+
except Exception as e:
40+
print(f"Error processing {file_path}: {e}")
41+
return None
42+
43+
def find_blog_posts():
44+
"""Find all blog post markdown files."""
45+
blog_dir = Path("content/blog")
46+
markdown_files = []
47+
48+
for item in blog_dir.iterdir():
49+
if item.is_dir():
50+
index_file = item / "index.md"
51+
if index_file.exists():
52+
markdown_files.append(index_file)
53+
54+
return markdown_files
55+
56+
def main():
57+
series_data = {}
58+
59+
blog_posts = find_blog_posts()
60+
print(f"Found {len(blog_posts)} blog posts")
61+
62+
for post_path in blog_posts:
63+
frontmatter = extract_frontmatter(post_path)
64+
if not frontmatter:
65+
continue
66+
67+
# Check if post has a series tag
68+
series = frontmatter.get('series')
69+
if not series:
70+
continue
71+
72+
# Get the date
73+
date = frontmatter.get('date')
74+
if not date:
75+
continue
76+
77+
# Convert date to datetime object for comparison
78+
if isinstance(date, str):
79+
try:
80+
# Try different date formats
81+
for date_format in ['%Y-%m-%d', '%Y-%m-%dT%H:%M:%S%z', '%Y-%m-%dT%H:%M:%S-%H:%M', '%Y-%m-%dT%H:%M:%SZ']:
82+
try:
83+
date_obj = datetime.strptime(date, date_format)
84+
break
85+
except ValueError:
86+
continue
87+
else:
88+
# If none of the formats work, try to extract just the date part
89+
date_part = date.split('T')[0]
90+
date_obj = datetime.strptime(date_part, '%Y-%m-%d')
91+
92+
# Convert to naive datetime if it has timezone info
93+
if date_obj.tzinfo is not None:
94+
date_obj = date_obj.replace(tzinfo=None)
95+
96+
except ValueError:
97+
print(f"Could not parse date '{date}' in {post_path}")
98+
continue
99+
else:
100+
date_obj = date
101+
102+
# Track the earliest date for each series (first article)
103+
if series not in series_data:
104+
series_data[series] = {
105+
'earliest_date': date_obj,
106+
'first_post': post_path,
107+
'meta_image': frontmatter.get('meta_image'),
108+
'post_count': 1
109+
}
110+
else:
111+
series_data[series]['post_count'] += 1
112+
if date_obj < series_data[series]['earliest_date']:
113+
series_data[series]['earliest_date'] = date_obj
114+
series_data[series]['first_post'] = post_path
115+
series_data[series]['meta_image'] = frontmatter.get('meta_image')
116+
117+
# Sort series by earliest date (oldest first to see first articles)
118+
sorted_series = sorted(series_data.items(),
119+
key=lambda x: x[1]['earliest_date'])
120+
121+
print("\nFirst article meta_image for each series:")
122+
print("=" * 60)
123+
for series_slug, data in sorted_series:
124+
earliest_date = data['earliest_date'].strftime('%Y-%m-%d')
125+
post_count = data['post_count']
126+
meta_image = data['meta_image'] or 'None'
127+
128+
# Get the relative path from the blog post directory
129+
first_post_dir = data['first_post'].parent.name
130+
if meta_image and meta_image != 'None':
131+
# Build the full path relative to the blog post
132+
full_meta_image_path = f"/blog/{first_post_dir}/{meta_image}"
133+
else:
134+
full_meta_image_path = 'None'
135+
136+
print(f"{series_slug}:")
137+
print(f" First post: {earliest_date} - {first_post_dir}")
138+
print(f" Meta image: {full_meta_image_path}")
139+
print(f" Total posts: {post_count}")
140+
print()
141+
142+
if __name__ == "__main__":
143+
main()

layouts/blog/series.html

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,38 @@
88
<div class="lg:w-7/12">
99
<header>
1010
<h1 class="no-anchor">{{ .Title }}</h1>
11-
1211
{{ .Content }}
13-
14-
15-
<ul class="tags list-none p-0 my-8 inline-flex flex-wrap text-xs">
16-
{{ with .Site.Data.blog_series }}
17-
{{ range .series }}
18-
<li>
19-
<a class="tag tag-blog text-xs m-1" href="/blog/tag/{{ .slug }}/">{{ .title }}</a>
20-
</li>
21-
{{ end }}
22-
{{ end }}
23-
</ul>
2412
</header>
13+
14+
{{ with .Site.Data.blog_series }}
15+
{{ range .series }}
16+
<article class="mb-8 pb-8 border-b border-gray-200">
17+
<h2 class="no-anchor">
18+
<a data-track="series-{{ .slug }}" href="/blog/tag/{{ .slug }}/">{{ .title }}</a>
19+
</h2>
20+
21+
<section class="blog-list-description">
22+
{{/* Display meta image if available */}}
23+
{{ if .meta_image }}
24+
<a href="/blog/tag/{{ .slug }}/"
25+
><img
26+
class="rounded-lg md:m-2 md:mt-0 md:max-w-xs md:float-right my-8 border-2 border-gray-100"
27+
src="{{ .meta_image }}"
28+
alt="{{ .title }}"
29+
/></a>
30+
{{ end }}
31+
32+
{{/* Display description */}}
33+
<p>{{ .description }}</p>
34+
35+
{{/* Read more link */}}
36+
<p>
37+
<a data-track="read-more-series" href="/blog/tag/{{ .slug }}/">View series &rarr;</a>
38+
</p>
39+
</section>
40+
</article>
41+
{{ end }}
42+
{{ end }}
2543
</div>
2644
<div class="lg:w-2/12 pl-8"></div>
2745
</div>

0 commit comments

Comments
 (0)