feat: Initial commit

This commit is contained in:
2025-02-19 17:01:08 -05:00
commit e0fb6129ad
2362 changed files with 325107 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8"/>
<meta content="#0e1112" media="(prefers-color-scheme: dark)" name="theme-color"/>
<meta content="#566B78" media="(prefers-color-scheme: light)" name="theme-color"/>
<meta content="Michael Housh" name="author"/>
<meta content="Mhoush" name="apple-mobile-web-app-title"/>
<meta content="initial-scale=1.0, width=device-width" name="viewport"/>
<meta content="telephone=no" name="format-detection"/>
<meta content="True" name="HandheldFriendly"/>
<meta content="320" name="MobileOptimized"/>
<meta content="Mhoush" name="og:site_name"/>
<meta content="hvac, developer, swift, home-performance, design" name="keywords"/>
<title>
mhoush: Articles in 2025
</title>
<link href="/static/favicon.ico" rel="shortcut icon"/>
<link href="/static/output.css" rel="stylesheet"/>
<link href="/static/style.css" rel="stylesheet"/>
<link href="/articles/feed.xml" rel="alternate" title="mhoush" type="application/rss+xml"/>
</head>
<body class="bg-page text-white pb-5 font-avenir articles">
<header class="bg-nav text-gray py-4 text-base/6 lg:fixed w-full lg:h-[62px]">
<nav class="container flex gap-x-5 lg:gap-x-y items-center">
<ul class="flex flex-wrap gap-x-2 lg:gap-x-5">
<li>
<a class href="/">Home</a>
</li>
<li>
<a class="active" href="/articles/">Articles</a>
</li>
<li>
<a class href="/about/">About</a>
</li>
</ul>
</nav>
</header>
<div class="container pt-12 lg:pt-28">
<section class="mb-10">
<h1 class="text-2xl font-bold mb-2">
<a class="[&:hover]:border-b border-orange" href="/articles/2025/vapor-htmx-todo-app/">Vapor + HTMX</a>
</h1>
<div class="text-gray gray-links text-sm">
<span class="border-r border-gray pr-2 mr-2">January 05, 2025</span>2155 words, posted in <a href="/articles/tag/general/">general</a>, <a href="/articles/tag/programming/">programming</a> and <a href="/articles/tag/software/">software</a>
</div>
<p class="mt-4">
<a href="/articles/2025/vapor-htmx-todo-app/">Build an example application using Vapor and HTMX.</a>
</p>
</section>
</div>
<div class="site-footer container text-gray gray-links border-t border-light text-center pt-6 mt-8 text-sm">
<p>
Copyright © Michael Housh 2023-2025.
</p>
<p>
Built in Swift using
<a href="https://github.com/loopwerk/Saga" rel="nofollow" target="_blank">Saga</a>
(<a href="https://github.com/m-housh/mhoush.com" rel="nofollow" target="_blank">source</a>).
</p>
<p>
<a href="http://localhost:3000/articles/feed.xml" rel="nofollow" target="_blank">RSS</a>
|
<a href="https://github.com/m-housh" rel="nofollow" target="_blank">Github</a>
|
<a href="https://www.youtube.com/channel/UCb58SeURd5bObfTiL0KoliA" rel="nofollow" target="_blank">Youtube</a>
|
<a href="https://www.facebook.com/michael.housh" rel="nofollow" target="_blank">Facebook</a>
|
<a href="mailto:michael@mhoush.com" rel="nofollow">Email</a>
</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,560 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8"/>
<meta content="#0e1112" media="(prefers-color-scheme: dark)" name="theme-color"/>
<meta content="#566B78" media="(prefers-color-scheme: light)" name="theme-color"/>
<meta content="Michael Housh" name="author"/>
<meta content="Mhoush" name="apple-mobile-web-app-title"/>
<meta content="initial-scale=1.0, width=device-width" name="viewport"/>
<meta content="telephone=no" name="format-detection"/>
<meta content="True" name="HandheldFriendly"/>
<meta content="320" name="MobileOptimized"/>
<meta content="Mhoush" name="og:site_name"/>
<meta content="hvac, developer, swift, home-performance, design" name="keywords"/>
<title>
mhoush: Vapor + HTMX
</title>
<link href="/static/favicon.ico" rel="shortcut icon"/>
<link href="/static/output.css" rel="stylesheet"/>
<link href="/static/style.css" rel="stylesheet"/>
<link href="/articles/feed.xml" rel="alternate" title="mhoush" type="application/rss+xml"/>
<link href="/static/prism.css" rel="stylesheet"/>
<meta content="Build an example application using Vapor and HTMX." name="description"/>
<meta content="summary_large_image" name="twitter:card"/>
<meta content="http://localhost:3000/articles/images/2025-01-05-vapor-htmx-todo-app.png" name="twitter:image"/>
<meta content="Vapor + HTMX" name="twitter:image:alt"/>
<meta content="http://localhost:3000/articles/images//articles/2025/vapor-htmx-todo-app/" name="og:url"/>
<meta content="Vapor + HTMX" name="og:title"/>
<meta content="Build an example application using Vapor and HTMX." name="og:description"/>
<meta content="http://localhost:3000/articles/images/2025-01-05-vapor-htmx-todo-app.png" name="og:image"/>
<meta content="1014" name="og:image:width"/>
<meta content="530" name="og:image:height"/>
<script crossorigin="anonymous" src="https://kit.fontawesome.com/f209982030.js">
</script>
</head>
<body class="bg-page text-white pb-5 font-avenir articles">
<header class="bg-nav text-gray py-4 text-base/6 lg:fixed w-full lg:h-[62px]">
<nav class="container flex gap-x-5 lg:gap-x-y items-center">
<ul class="flex flex-wrap gap-x-2 lg:gap-x-5">
<li>
<a class href="/">Home</a>
</li>
<li>
<a class="active" href="/articles/">Articles</a>
</li>
<li>
<a class href="/about/">About</a>
</li>
</ul>
</nav>
</header>
<div class="container pt-12 lg:pt-28">
<article class="prose">
<h1>
Vapor + HTMX
</h1>
<div class="-mt-6">
<div class="text-gray gray-links text-sm">
<span class="border-r border-gray pr-2 mr-2">January 05, 2025</span>2155 words, posted in <a href="/articles/tag/general/">general</a>, <a href="/articles/tag/programming/">programming</a> and <a href="/articles/tag/software/">software</a>
</div>
</div>
<img alt="banner" src="http://localhost:3000/articles/images/2025-01-05-vapor-htmx-todo-app.png"/>
<h2>Introduction</h2>
<p>This post is a quick example of creating a very basic todo web application using <code>Vapor</code> a swift web<br />
framework and <code>Htmx</code>, with no custom javascript required.</p>
<p><a href="https://docs.vapor.codes">Vapor</a></p>
<p><a href="https://htmx.org">Htmx</a></p>
<h2>Getting Started</h2>
<p>To get started you must install the vapor command-line tool that will generate our project.</p>
<pre><code class="language-bash">brew install vapor
</code></pre>
<p>Next, generate the project using the vapor command-line tool.</p>
<p><img src="/articles/images/2025-01-05-vapor.gif" alt="" /></p>
<pre><code class="language-bash">vapor new todo-htmx --fluent.db sqlite --leaf
</code></pre>
<p>The above command will generate a new project that uses an <code>SQLite</code> database along with vapors<br />
<code>Leaf</code> templating engine. You can move into the project directory and browse around the files that<br />
are generated.</p>
<pre><code class="language-bash">cd todo-htmx
</code></pre>
<h2>Update the Controller</h2>
<p>Open the <code>Sources/App/Controllers/TodoController.swift</code> file. This file handles the api routes for<br />
our <code>Todo</code> database model. Personally I like to prefix these routes with <code>api</code>.</p>
<p>Update the first line in the <code>boot(routes: RoutesBuilder)</code> function to look like this.</p>
<pre><code class="language-swift">let todos = routes.grouped(&quot;api&quot;, &quot;todos&quot;)
</code></pre>
<p>Everything else can stay the same. This changes these routes to be exposed at<br />
<code>http://localhost:8080/api/todos</code>, which will allow our routes that return html views to be able to<br />
be exposed at <code>http://localhost:8080/todos</code>.</p>
<h2>Update the Todo Model</h2>
<p>A todo is not very valuable without a way to tell if it needs to be completed or not. So, lets add<br />
a field to our database model (<code>Sources/App/Models/Todo.swift</code>).</p>
<p>Update the file to include the following:</p>
<pre><code class="language-swift">import Fluent
import struct Foundation.UUID
/// Property wrappers interact poorly with `Sendable` checking, causing a warning for the `@ID` property
/// It is recommended you write your model with sendability checking on and then suppress the warning
/// afterwards with `@unchecked Sendable`.
final class Todo: Model, @unchecked Sendable {
static let schema = &quot;todos&quot;
@ID(key: .id)
var id: UUID?
@Field(key: &quot;title&quot;)
var title: String
@Field(key: &quot;complete&quot;)
var complete: Bool
init() {}
init(id: UUID? = nil, title: String, complete: Bool) {
self.id = id
self.title = title
self.complete = complete
}
func toDTO() -&gt; TodoDTO {
.init(
id: id,
title: $title.value,
complete: $complete.value
)
}
}
</code></pre>
<p>Since we added a field to our database model, we also need to update the migration file<br />
(<code>Sources/App/Migrations/CreateTodo.swift</code>).</p>
<pre><code class="language-swift">import Fluent
struct CreateTodo: AsyncMigration {
func prepare(on database: Database) async throws {
try await database.schema(&quot;todos&quot;)
.id()
.field(&quot;title&quot;, .string, .required)
.field(&quot;complete&quot;, .bool, .required)
.create()
}
func revert(on database: Database) async throws {
try await database.schema(&quot;todos&quot;).delete()
}
}
</code></pre>
<p>This just adds our new field to the database schema when we run the migrations, which we will do<br />
later on in the tutorial.</p>
<h3>Update the Data Transfer Object</h3>
<p>We also need to add the <code>complete</code> field to our data transfer object (<code>DTO</code>). This model is used as<br />
an intermediate between our database and the user.</p>
<pre><code class="language-swift">import Fluent
import Vapor
struct TodoDTO: Content {
var id: UUID?
var title: String?
var complete: Bool?
func toModel() -&gt; Todo {
let model = Todo()
model.id = id
model.complete = complete ?? false
if let title = title {
model.title = title
}
return model
}
}
</code></pre>
<h2>Generate the View Templates</h2>
<p>Our index template was already generated at <code>Resources/Views/index.leaf</code>, open the file and edit the<br />
contents to match the following.</p>
<blockquote>
<p>Note: You can learn more about the<br />
<a href="https://docs.vapor.codes/leaf/getting-started/">leaf templating engine here.</a></p>
</blockquote>
<pre><code class="language-html">&lt;!doctype html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
&lt;meta charset=&quot;utf-8&quot; /&gt;
&lt;title&gt;#(title)&lt;/title&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;css/main.css&quot; /&gt;
&lt;script src=&quot;https://unpkg.com/htmx.org@2.0.4&quot;&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;#(title)&lt;/h1&gt;
&lt;div class=&quot;container&quot;&gt;
&lt;form hx-post=&quot;/todos&quot; hx-target=&quot;#todos&quot;&gt;
&lt;label for=&quot;title&quot;&gt;Todo&lt;/label&gt;
&lt;input type=&quot;text&quot; name=&quot;title&quot; placeholder=&quot;Title&quot; /&gt;
&lt;button type=&quot;submit&quot;&gt;Submit&lt;/button&gt;
&lt;/form&gt;
&lt;!-- Todos List --&gt;
&lt;table id=&quot;todos&quot; class=&quot;todos&quot; hx-get=&quot;/todos&quot; hx-trigger=&quot;load&quot;&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>The important parts here are the <code>&lt;script&gt;</code> tag in the head element which will include <code>Htmx</code> in our<br />
project.</p>
<p>The head element also contains a link to a custom <code>css</code> stylesheet that we will create shortly.</p>
<p>We add a <code>form</code> element that will be used to generate a new todo item in the database. This is a<br />
basic / standard html form, but we are using <code>Htmx</code> to post the form contents to the route<br />
<code>POST http://localhost:8080/todos</code>, which we will create shortly.</p>
<p>Then theres the <code>table</code> element that will contain the contents of our todos. When the page is<br />
loaded it will use <code>Htmx</code> to fetch the todos from <code>GET http://localhost:8080/todos</code> route, which we<br />
will create shortly.</p>
<h3>Todos Table Template</h3>
<p>Create a new view template that will return our populated table of todos.</p>
<pre><code class="language-bash">touch Resources/Views/todos.leaf
</code></pre>
<p>The contents of this file should be the following:</p>
<pre><code class="language-html">&lt;!-- Template for a list of todo's --&gt;
&lt;table id=&quot;todos&quot;
class=&quot;todos&quot;&gt;
&lt;!-- The table header --&gt;
&lt;tr&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Completed&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
#for(todo in todos):
&lt;tr&gt;
&lt;!-- Make the title column take up 90% of the width --&gt;
&lt;td style=&quot;width: 90%;&quot;&gt;#(todo.title)&lt;/td&gt;
&lt;td&gt;
&lt;input type=&quot;checkbox&quot;
id=&quot;todo_#(todo.id)&quot;
hx-put=&quot;/todos/#(todo.id)&quot;
hx-trigger=&quot;click&quot;
hx-target=&quot;#todos&quot;
#if(todo.complete): checked #endif&gt;
&lt;/input&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;button hx-delete=&quot;/todos/#(todo.id)&quot;
hx-trigger=&quot;click&quot;
hx-target=&quot;#todos&quot;
class=&quot;btn-delete-todo&quot;&gt;
X
&lt;/button&gt;
&lt;/td&gt;
&lt;/tr&gt;
#endfor
&lt;/table&gt;
</code></pre>
<p>Here, we just create a table that is 3 columns wide from a list of todos that we will pass in to the<br />
template. We use <code>Htmx</code> to handle updating a todo if a user clicks a checkbox to mark the todo as<br />
<code>complete</code>, we also add a button in the last column of the table that we use <code>Htmx</code> to handle<br />
deleting a todo from the database.</p>
<h2>Controllers</h2>
<p>The controllers handle the routes that our website exposes. The project template creates a<br />
controller for us that handles <code>JSON</code> / <code>API</code> requests, but we do need to make a couple of changes<br />
to the file (<code>Sources/App/Controllers/TodoController.swift</code>).</p>
<pre><code class="language-swift">import Fluent
import Vapor
struct TodoController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
let todos = routes.grouped(&quot;api&quot;, &quot;todos&quot;)
todos.get(use: index)
todos.post(use: create)
todos.group(&quot;:todoID&quot;) { todo in
todo.delete(use: self.delete)
todo.put(use: self.update)
}
}
@Sendable
func index(req: Request) async throws -&gt; [TodoDTO] {
try await Todo.query(on: req.db).all().map { $0.toDTO() }
}
@Sendable
func create(req: Request) async throws -&gt; TodoDTO {
let todo = try req.content.decode(TodoDTO.self).toModel()
try await todo.save(on: req.db)
return todo.toDTO()
}
@Sendable
func delete(req: Request) async throws -&gt; HTTPStatus {
guard let todo = try await Todo.find(req.parameters.get(&quot;todoID&quot;), on: req.db) else {
throw Abort(.notFound)
}
try await todo.delete(on: req.db)
return .noContent
}
@Sendable
func update(req: Request) async throws -&gt; TodoDTO {
// let todo = try req.content.decode(TodoDTO.self).toModel()
guard let todo = try await Todo.find(req.parameters.get(&quot;todoID&quot;), on: req.db) else {
throw Abort(.notFound)
}
todo.complete.toggle()
try await todo.save(on: req.db)
return todo.toDTO()
}
}
</code></pre>
<p>The primary changes here are to add the <code>update(req: Request)</code> function at the bottom, which handles<br />
updating a todo that has already been created. This will be used when a user clicks on the checkbox<br />
to mark a todo as complete or incomplete.</p>
<p>We also change the route in the <code>boot(routes: RoutesBuilder)</code> method to make all these routes<br />
accessible at <code>/api/todos</code> instead of the original <code>/todos</code> as we will use the <code>/todos</code> routes for<br />
returning our views from our view controller.</p>
<h3>Todo View Controller</h3>
<p>Next we need to create our view controller, it is what will be used to handle routes that should<br />
return <code>html</code> content for our website. This controller will actually use the api controller to do<br />
the majority of its work.</p>
<p>The easiest thing is to make a copy of the current api controller:</p>
<pre><code class="language-bash">cp Sources/App/Controllers/TodoController.swift Sources/App/Controllers/TodoViewController.swift
</code></pre>
<p>Then update the file to the following:</p>
<pre><code class="language-swift">import Fluent
import Vapor
struct TodoViewController: RouteCollection {
private let api = TodoController()
func boot(routes: RoutesBuilder) throws {
let todos = routes.grouped(&quot;todos&quot;)
todos.get(use: index)
todos.post(use: create)
todos.group(&quot;:todoID&quot;) { todo in
todo.delete(use: self.delete)
todo.put(use: self.update)
}
}
@Sendable
func index(req: Request) async throws -&gt; View {
let todos = try await api.index(req: req)
return try await req.view.render(&quot;todos&quot;, [&quot;todos&quot;: todos])
}
@Sendable
func create(req: Request) async throws -&gt; View {
_ = try await api.create(req: req)
return try await index(req: req)
}
@Sendable
func delete(req: Request) async throws -&gt; View {
_ = try await api.delete(req: req)
return try await index(req: req)
}
@Sendable
func update(req: Request) async throws -&gt; View {
_ = try await api.update(req: req)
return try await index(req: req)
}
}
</code></pre>
<p>Here we use the api controller to do the heavy lifting of communicating with the database, then we<br />
just always return / render the <code>todos.leaf</code> template that we created earlier, which will update our<br />
web page with the list of todos retreived from the database.</p>
<blockquote>
<p>Note: There are better ways to handle this, however this is just a simple example.</p>
</blockquote>
<h3>Update our routes</h3>
<p>Next, we need to tell vapor to use our new view controller (<code>Sources/App/routes.swift</code>)</p>
<pre><code class="language-swift">import Fluent
import Vapor
func routes(_ app: Application) throws {
app.get { req async throws in
try await req.view.render(&quot;index&quot;, [&quot;title&quot;: &quot;Todos&quot;])
}
app.get(&quot;hello&quot;) { _ async -&gt; String in
&quot;Hello, world!&quot;
}
try app.register(collection: TodoController())
try app.register(collection: TodoViewController())
}
</code></pre>
<p>Here, we just add the <code>TodoViewController</code> at the bottom so vapor will be able to handle those<br />
routes and also update the title to be <code>Todos</code> (in the first <code>app.get</code> near the top).</p>
<h2>Build and Run</h2>
<p>At this point we should be able to build and run the application.</p>
<p>First, lets make sure the project builds.</p>
<pre><code class="language-bash">swift build
</code></pre>
<p>This may take a minute if its the first time building the project as it has to fetch the<br />
dependencies. If you experience problems here then make sure you dont have typos in your files.</p>
<p>Next, we need to run the database migrations.</p>
<pre><code class="language-bash">swift run App migrate
</code></pre>
<p>Finally, we can run the application.</p>
<pre><code class="language-bash">swift run App
</code></pre>
<p>You should be able to open your browser and type in the url: <code>http://localhost:8080</code> to view the<br />
application. You can experiment with adding a new todo using the form.</p>
<blockquote>
<p>Note: To stop the application use <code>Ctrl-c</code></p>
</blockquote>
<h2>Bonus Styles</h2>
<p>Hopefully you werent blinded the first time you opened the application. You can add custom styles<br />
by creating a <code>css</code> file (<code>Public/css/main.css</code>).</p>
<pre><code class="language-bash">mkdir Public/css
touch Public/css/main.css
</code></pre>
<p>Update the file to the following:</p>
<pre><code class="language-css">body {
background-color: #1e1e2e;
color: #ff66ff;
}
table {
width: 100%;
}
th,
td {
border-bottom: 1px solid grey;
border-collapse: collapse;
}
td {
color: white;
}
.todos {
transition: all ease-in 1s;
}
.btn-delete-todo {
color: red;
margin-left: 20px;
}
</code></pre>
<p>Currently vapor does not know to serve files from the <code>Public</code> directory, so we need to update the<br />
<code>Sources/App/configure.swift</code> file, by uncommenting the line near the top.</p>
<pre><code class="language-swift">import Fluent
import FluentSQLiteDriver
import Leaf
import NIOSSL
import Vapor
// configures your application
public func configure(_ app: Application) async throws {
// uncomment to serve files from /Public folder
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
app.databases.use(DatabaseConfigurationFactory.sqlite(.file(&quot;db.sqlite&quot;)), as: .sqlite)
app.migrations.add(CreateTodo())
app.views.use(.leaf)
// register routes
try routes(app)
}
</code></pre>
<p>Now you can stop and restart to see the styled website.</p>
<pre><code class="language-bash">swift run App
</code></pre>
<h2>Conclusion</h2>
<p>I hope you enjoyed this quick example of using <code>Htmx</code> with <code>Vapor</code>. You can view the source files at<br />
<a href="https://github.com/m-housh/todo-htmx">here</a>.</p>
</article>
<div class="border-t border-light mt-8 pt-8">
<h2 class="text-4xl font-extrabold mb-8">
Written by
</h2>
<div class="flex flex-col lg:flex-row gap-8">
<div class="flex-[0_0_120px]">
<img class="w-[120px] h-[120px] rounded-full" src="/static/images/avatar.png"/>
</div>
<div class="prose">
<h3 class="!m-0">
Michael Housh
</h3>
<p class="text-gray">
HVAC business owner with over 27 years of experience. Writes articles about HVAC,
Programming, Home-Performance, and Building Science
</p>
</div>
</div>
</div>
<div class="mt-16">
<h2 class="text-4xl font-extrabold mb-8">
More articles
</h2>
<div class="grid lg:grid-cols-2 gap-10">
<section>
<h2 class="text-2xl font-bold mb-2">
<a class="[&:hover]:border-b border-orange" href="/articles/2024/free-as-in-freedom/">Free As In Freedom</a>
</h2>
<div class="text-gray gray-links text-sm mb-4">
<span class="border-r border-gray pr-2 mr-2">April 09, 2024</span><a href="/articles/tag/general/">general</a>, <a href="/articles/tag/open-source/">open-source</a> and <a href="/articles/tag/software/">software</a>
</div>
<p>
<a href="/articles/2024/free-as-in-freedom/"><div>
<img alt="banner" src="http://localhost:3000/articles/images/2024-04-09-free-as-in-freedom.png"/>
Salute to open-source software engineers
</div></a>
</p>
</section>
<section>
<h2 class="text-2xl font-bold mb-2">
<a class="[&:hover]:border-b border-orange" href="/articles/2024/pgp-encryption-introduction/">PGP Encryption Introduction</a>
</h2>
<div class="text-gray gray-links text-sm mb-4">
<span class="border-r border-gray pr-2 mr-2">April 04, 2024</span><a href="/articles/tag/gnupgp/">GnuPGP</a>, <a href="/articles/tag/pgp/">PGP</a>, <a href="/articles/tag/programming/">programming</a> and <a href="/articles/tag/security/">security</a>
</div>
<p>
<a href="/articles/2024/pgp-encryption-introduction/"><div>
<img alt="banner" src="http://localhost:3000/articles/images/2024-04-04-pgp-encryption-introduction.gif"/>
In this article I introduce PGP and show a use case for me, which perhaps you can use as well.
What is PGP
PGP stands for Pretty Good Privacy, it was first developed in 1991 by Phil Zimmermann. PGP uses
cryptographic privacy and authentication and is...
</div></a>
</p>
</section>
</div>
<p class="prose mt-8">
<a href="/articles/"> See all articles</a>
</p>
</div>
</div>
<div class="site-footer container text-gray gray-links border-t border-light text-center pt-6 mt-8 text-sm">
<p>
Copyright © Michael Housh 2023-2025.
</p>
<p>
Built in Swift using
<a href="https://github.com/loopwerk/Saga" rel="nofollow" target="_blank">Saga</a>
(<a href="https://github.com/m-housh/mhoush.com" rel="nofollow" target="_blank">source</a>).
</p>
<p>
<a href="http://localhost:3000/articles/feed.xml" rel="nofollow" target="_blank">RSS</a>
|
<a href="https://github.com/m-housh" rel="nofollow" target="_blank">Github</a>
|
<a href="https://www.youtube.com/channel/UCb58SeURd5bObfTiL0KoliA" rel="nofollow" target="_blank">Youtube</a>
|
<a href="https://www.facebook.com/michael.housh" rel="nofollow" target="_blank">Facebook</a>
|
<a href="mailto:michael@mhoush.com" rel="nofollow">Email</a>
</p>
</div>
</body>
</html>