549 lines
22 KiB
HTML
549 lines
22 KiB
HTML
<!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 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 vapor’s <code>Leaf</code> templating engine. You can move into
|
||
the project directory and browse around the files that 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 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("api", "todos")
|
||
</code></pre>
|
||
<p>Everything else can stay the same. This changes these routes to be exposed at <code>http://localhost:8080/api/todos</code>, which will allow our routes
|
||
that return html views to be able to 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, let’s add 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 = "todos"
|
||
|
||
@ID(key: .id)
|
||
var id: UUID?
|
||
|
||
@Field(key: "title")
|
||
var title: String
|
||
|
||
@Field(key: "complete")
|
||
var complete: Bool
|
||
|
||
init() {}
|
||
|
||
init(id: UUID? = nil, title: String, complete: Bool) {
|
||
self.id = id
|
||
self.title = title
|
||
self.complete = complete
|
||
}
|
||
|
||
func toDTO() -> 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 (<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("todos")
|
||
.id()
|
||
.field("title", .string, .required)
|
||
.field("complete", .bool, .required)
|
||
.create()
|
||
}
|
||
|
||
func revert(on database: Database) async throws {
|
||
try await database.schema("todos").delete()
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>This just adds our new field to the database schema when we run the migrations, which we will do 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 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() -> 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 contents to match the following.</p>
|
||
<blockquote>
|
||
<p>Note: You can learn more about the <a href="https://docs.vapor.codes/leaf/getting-started/">leaf templating engine here.</a></p>
|
||
</blockquote>
|
||
<pre><code class="language-html"><!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>#(title)</title>
|
||
<link rel="stylesheet" href="css/main.css" />
|
||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||
</head>
|
||
|
||
<body>
|
||
<h1>#(title)</h1>
|
||
<div class="container">
|
||
<form hx-post="/todos" hx-target="#todos">
|
||
<label for="title">Todo</label>
|
||
<input type="text" name="title" placeholder="Title" />
|
||
<button type="submit">Submit</button>
|
||
</form>
|
||
|
||
<!-- Todos List -->
|
||
<table id="todos" class="todos" hx-get="/todos" hx-trigger="load"></table>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
</code></pre>
|
||
<p>The important parts here are the <code><script></code> tag in the head element which will include <code>Htmx</code> in our 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 basic / standard html form, but we are
|
||
using <code>Htmx</code> to post the form contents to the route <code>POST http://localhost:8080/todos</code>, which we will create shortly.</p>
|
||
<p>Then there’s the <code>table</code> element that will contain the contents of our todos. When the page is loaded it will use <code>Htmx</code> to fetch the todos
|
||
from <code>GET http://localhost:8080/todos</code> route, which we 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"><!-- Template for a list of todo's -->
|
||
<table id="todos"
|
||
class="todos">
|
||
<!-- The table header -->
|
||
<tr>
|
||
<th>Description</th>
|
||
<th>Completed</th>
|
||
<th></th>
|
||
</tr>
|
||
|
||
#for(todo in todos):
|
||
<tr>
|
||
<!-- Make the title column take up 90% of the width -->
|
||
<td style="width: 90%;">#(todo.title)</td>
|
||
<td>
|
||
<input type="checkbox"
|
||
id="todo_#(todo.id)"
|
||
hx-put="/todos/#(todo.id)"
|
||
hx-trigger="click"
|
||
hx-target="#todos"
|
||
#if(todo.complete): checked #endif>
|
||
</input>
|
||
</td>
|
||
<td>
|
||
<button hx-delete="/todos/#(todo.id)"
|
||
hx-trigger="click"
|
||
hx-target="#todos"
|
||
class="btn-delete-todo">
|
||
X
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
#endfor
|
||
</table>
|
||
</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 template. We use <code>Htmx</code> to handle
|
||
updating a todo if a user clicks a checkbox to mark the todo as <code>complete</code>, we also add a button in the last column of the table that we use
|
||
<code>Htmx</code> to handle 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 controller for us that handles <code>JSON</code> / <code>API</code>
|
||
requests, but we do need to make a couple of changes 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("api", "todos")
|
||
|
||
todos.get(use: index)
|
||
todos.post(use: create)
|
||
todos.group(":todoID") { todo in
|
||
todo.delete(use: self.delete)
|
||
todo.put(use: self.update)
|
||
}
|
||
}
|
||
|
||
@Sendable
|
||
func index(req: Request) async throws -> [TodoDTO] {
|
||
try await Todo.query(on: req.db).all().map { $0.toDTO() }
|
||
}
|
||
|
||
@Sendable
|
||
func create(req: Request) async throws -> 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 -> HTTPStatus {
|
||
guard let todo = try await Todo.find(req.parameters.get("todoID"), on: req.db) else {
|
||
throw Abort(.notFound)
|
||
}
|
||
|
||
try await todo.delete(on: req.db)
|
||
return .noContent
|
||
}
|
||
|
||
@Sendable
|
||
func update(req: Request) async throws -> TodoDTO {
|
||
// let todo = try req.content.decode(TodoDTO.self).toModel()
|
||
guard let todo = try await Todo.find(req.parameters.get("todoID"), 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 updating a todo that has already been
|
||
created. This will be used when a user clicks on the checkbox 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 accessible at <code>/api/todos</code> instead of the
|
||
original <code>/todos</code> as we will use the <code>/todos</code> routes for 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 return <code>html</code> content for our website. This
|
||
controller will actually use the api controller to do the majority of it’s 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("todos")
|
||
|
||
todos.get(use: index)
|
||
todos.post(use: create)
|
||
todos.group(":todoID") { todo in
|
||
todo.delete(use: self.delete)
|
||
todo.put(use: self.update)
|
||
}
|
||
}
|
||
|
||
@Sendable
|
||
func index(req: Request) async throws -> View {
|
||
let todos = try await api.index(req: req)
|
||
return try await req.view.render("todos", ["todos": todos])
|
||
}
|
||
|
||
@Sendable
|
||
func create(req: Request) async throws -> View {
|
||
_ = try await api.create(req: req)
|
||
return try await index(req: req)
|
||
}
|
||
|
||
@Sendable
|
||
func delete(req: Request) async throws -> View {
|
||
_ = try await api.delete(req: req)
|
||
return try await index(req: req)
|
||
}
|
||
|
||
@Sendable
|
||
func update(req: Request) async throws -> 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 just always return / render the
|
||
<code>todos.leaf</code> template that we created earlier, which will update our 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("index", ["title": "Todos"])
|
||
}
|
||
|
||
app.get("hello") { _ async -> String in
|
||
"Hello, world!"
|
||
}
|
||
|
||
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 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, let’s make sure the project builds.</p>
|
||
<pre><code class="language-bash">swift build
|
||
</code></pre>
|
||
<p>This may take a minute if it’s the first time building the project as it has to fetch the dependencies. If you experience problems here then
|
||
make sure you don’t 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 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 weren’t blinded the first time you opened the application. You can add custom styles 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 <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("db.sqlite")), 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
|
||
<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>
|
||
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>
|
||
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>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js">
|
||
</script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/keep-markup/prism-keep-markup.min.js">
|
||
</script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js">
|
||
</script>
|
||
</div>
|
||
</body>
|
||
</html> |