feat: Initial commit
This commit is contained in:
560
deploy/articles/2025/vapor-htmx-todo-app/index.html
Normal file
560
deploy/articles/2025/vapor-htmx-todo-app/index.html
Normal 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 vapor’s<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("api", "todos")
|
||||
</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, let’s 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 = "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<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("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<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() -> 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"><!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<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 there’s 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"><!-- 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<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("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<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 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<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("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<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, 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<br />
|
||||
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<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 weren’t 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("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<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>
|
||||
Reference in New Issue
Block a user