About
About integrations.fun
2025-09-08
My name is Ryan. I have over 20 years experience managing and securing IT infrastructure, and I enjoy solving technical problems. For most of my career, I've benefitted from individuals who post solutions and suggestions online. This is my effort to return the favor. I hope you find something helpful here.
How this site is made
This site is built with Flask and hosted on Vercel. I wanted a simple way to make posts using markdown files. I drop a .md file into the posts directory, and the app automatically picks it up in the table of contents on the next refresh. The app.py file looks like this:
from flask import Flask, render_template, abort
import markdown
import yaml
from pathlib import Path
from datetime import datetime
from collections import defaultdict
import re
app = Flask(__name__)
POSTS_DIR = Path(__file__).parent / "posts"
def slugify(title):
return re.sub(r'[^a-z0-9]+', '-', title.lower()).strip('-')
def parse_markdown_file(post_file):
content = post_file.read_text(encoding="utf-8")
if content.lstrip().startswith("---"):
parts = content.split("---", 2)
if len(parts) >= 3:
_, fm, body = parts
meta = yaml.safe_load(fm) or {}
else:
meta = {}
body = content
else:
meta = {}
body = content
html_content = markdown.markdown(body, extensions=['fenced_code', 'codehilite'])
title = meta.get("title", "Untitled")
subtitle = meta.get("subtitle", "")
slug = slugify(title)
return {
"title": title,
"subtitle": subtitle,
"slug": slug,
"date": meta.get("date", ""),
"screenshots": meta.get("screenshots", []),
"content": html_content,
"filename": post_file.name,
"categories": [c.strip() for c in str(meta.get("categories", "")).split(",") if c.strip()],
}
def load_posts():
posts = []
for post_file in sorted(POSTS_DIR.glob("*.md"), reverse=True):
if post_file.name.lower() == "about.md":
continue
posts.append(parse_markdown_file(post_file))
return posts
def load_about_page():
about_file = POSTS_DIR / "about.md"
if not about_file.exists():
return None
post = parse_markdown_file(about_file)
post["slug"] = "about"
return post
@app.route("/")
def index():
posts = load_posts()
posts.sort(key=lambda p: p["title"].lower())
categories = defaultdict(list)
for post in posts:
cats = post.get('categories',[])
for cat in post["categories"]:
categories[cat].append(post)
for cat in categories:
categories[cat].sort(key=lambda x: x['date'], reverse=True)
sorted_categories = sorted(categories.items(), key=lambda x: x[0].lower())
return render_template("index.html", categories=sorted_categories, posts=posts, blog_name="Integrations", current_year=datetime.now().year)
@app.route("/about")
def about():
posts = load_posts()
posts.sort(key=lambda p: p["title"].lower())
about_post = load_about_page()
if not about_post:
abort(404)
return render_template("post.html",
post=about_post,
posts=posts,
blog_name="Integrations",
current_year=datetime.now().year)
@app.route("/post/<slug>")
def post(slug):
posts = load_posts()
posts.sort(key=lambda p: p["title"].lower())
post = next((p for p in posts if p["slug"] == slug), None)
if not post:
abort(404)
return render_template("post.html",
post=post,
posts=posts,
blog_name="Integrations",
current_year=datetime.now().year)
if __name__ == "__main__":
app.run(debug=True)
The app reads all the .md files in the directory, locates their title and associated tags, and then dynamically generates a sorted list for each tag category.
The load_posts() helper function parses the markdown file and enables code highlights. Posts inherit the base.html template which looks like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ blog_name }}</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&family=Roboto+Slab:wght@400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/powershell.min.js"></script>
<script>hljs.highlightAll();</script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<button id="sidebarToggle">☰</button>
<div id="sidebar" class="sidebar">
<h3>Posts</h3>
<ul>
{% for post in posts %}
<li>
<a href="{{ url_for('post', slug=post.slug) }}">{{ post.title }}</a>
</li>
{% endfor %}
</ul>
<a href="{{ url_for('index')}}">🏠</a>
<a href="{{ url_for('about') }}">About</a>
</div>
<header>
<h1>{{ blog_name }}</h1>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
© {{ current_year }} {{ blog_name }}
</footer>
<script src="{{ url_for('static', filename='js/scripts.js') }}"></script>
<script>
document.getElementById("sidebarToggle").addEventListener("click", function() {
document.getElementById("sidebar").classList.toggle("open");
});
</script>
<script>
window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };
</script>
<script defer src="/_vercel/insights/script.js"></script>
</body>
</html>
Post content is then loaded into the post.html template which looks like this:
{% extends "base.html" %}
{% block content %}
<article>
<h2 style="margin-bottom: -20px;">{{ post.title }}</h2>
<h4 style="margin-bottom: -20px;">{{ post.subtitle }}</h4>
<h5 style="color: #FFFFFF; font-weight: 100; font-style: italic;">{{ post.date }}</h5>
<div>
{{ post.content|safe }}
</div>
</article>
{% endblock %}
Markdown files are simple, and look like this:
---
title: Title Here
subtitle: Subtitle
date: 2025-09-08
---
<center><img src="/static/images/01_test.png" alt="Stub" style="width:50%;"></center>

Note that images can be loaded with HTML or using markdown syntax as shown in the two examples above.