{"id":22031102,"url":"https://github.com/russbaz/vaporhx","last_synced_at":"2025-05-07T12:22:54.770Z","repository":{"id":205613562,"uuid":"714657399","full_name":"RussBaz/VaporHX","owner":"RussBaz","description":"Vapor Extensions + HTMX","archived":false,"fork":false,"pushed_at":"2024-12-15T23:11:11.000Z","size":192,"stargazers_count":51,"open_issues_count":0,"forks_count":4,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-05-03T16:17:50.191Z","etag":null,"topics":["htmx","swift","vapor"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RussBaz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-11-05T14:20:42.000Z","updated_at":"2025-04-21T00:31:01.000Z","dependencies_parsed_at":"2023-12-23T07:30:43.199Z","dependency_job_id":"b1214bc4-8920-40f5-989b-a6b506afbe62","html_url":"https://github.com/RussBaz/VaporHX","commit_stats":null,"previous_names":["russbaz/vhx","russbaz/vaporhx"],"tags_count":26,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RussBaz%2FVaporHX","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RussBaz%2FVaporHX/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RussBaz%2FVaporHX/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RussBaz%2FVaporHX/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RussBaz","download_url":"https://codeload.github.com/RussBaz/VaporHX/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252874496,"owners_count":21817837,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["htmx","swift","vapor"],"created_at":"2024-11-30T08:14:19.534Z","updated_at":"2025-05-07T12:22:54.752Z","avatar_url":"https://github.com/RussBaz.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# VaporHX - Swift Vapor + Htmx + Extensions\n\n**VaporHX** is a collection of Htmx and other extensions that I made when started working with htmx in a personal project (and thus they can be quite opinionated). In any case, please feel free to discuss any changes, as I am open to new and convincing ideas.\n\nNOTE: All the HTMX parts are fully functional and they are actively used by me, even if the documentation is incomplete. I am sorry about this but it writing docs is incredibly tedious.\n\n## Core Idea\n\nThe core idea is that you can combine your existing API endpoints with HTMX endpoints with minimal effort. The response will depend on the value of the request `Accept` header and the request method.\n\nAll you need to do is to call the `hx(template: String)` method on your `Content` struct and return its value. It will automatically pick the appropriate response, whether it is JSON encoded data, a full HTML page or an HTMX fragment. When HTML (HTMX) is returned, your content is injected into the specified template as a context. It uses the Leaf templating engine by default.\n\nHowever, you do not have to use the Leaf engine if you do not want to. This package defines `HXTemplateable` protocol with a single render method that returns an html page as a string. For as long as your own templating engine implements it, you can pass its type into the `hx(template:)` method instead.\n\n```swift\nimport VHX\n\n// Do NOT forget to call 'configureHtmx' in your 'configure' method before trying this snippet in your project\n\n// Also, do NOT create a folder called '--page' in your template root without changing the default VHX settings\n// as it is used as a prefix for dynamically generated wrapper page templates\n// and the custom template provider is the last one to be checked after the default ones are run\n\n// Furthermore, this snippet assumes the default leaf naming conventions as they can be manually overriden\n\nstruct MyApi: Content {\n    let name: String\n}\n\nfunc routes(_ app: Application) throws {\n    // Combined API and HTMX endpoint\n    // 'api.leaf' template must exist\n    app.get(\"api\") { req in\n        MyApi(name: \"name\").hx(template: \"api\")\n        // The return type of this function call is 'HX\u003cMyApi\u003e'\n    }\n\n    // HTMX only endpoint\n    // 'index.leaf' template must exist\n    // It will automatically select whether to return a full page or only a fragment if the generic page template was configured.\n    // Otherwise, it will simply render the 'index.leaf' template\n    app.get { req in\n        try await req.htmx.render(\"index\")\n    }\n\n    // Or an even quicker definition of simple 'static' routes\n    app.get(\"static\", use: staticRoute(template: \"my-static-template\"))\n}\n```\n\nBasic configuration (`configure.swift`):\n\n```swift\nimport Vapor\nimport VHX\n\n// Basic config configures Leaf engine but you are not required to use it\n// Basic config assumes 'index-base.leaf' template exists and that it contains '#import(\"body\")' tag\n// It will generate a dynamic template that wraps the content of the specified template for NON-htmx calls to 'htmx.render'\n// It will simply plug the provided template into the 'body' slot of the base template\n\npublic func configure(_ app: Application) async throws {\n    let config = HtmxConfiguration.basic()\n    try configureHtmx(app, configuration: config)\n}\n```\n\n## Table of Contents\n\n-   [What is HTMX?](#what-is-htmx)\n-   [HTMX](#htmx)\n    -   [Installation](#installation)\n    -   [Configuration](#configuration)\n    -   [HX Request Extensions](#hx-request-extensions)\n    -   [HX Extension Method and HX\\\u003cMyType\\\u003e](#hx-extension-method-and-hxmytype)\n    -   [HX Templateable and Custom Templating Engines](#hx-templateable-and-custom-templating-engines)\n    -   [Request Headers](#request-headers)\n    -   [Response Headers](#response-headers)\n        -   [Overview](#overview)\n        -   [Location](#location)\n        -   [Push Url](#push-url)\n        -   [Redirect](#redirect)\n        -   [Refresh](#refresh)\n        -   [Replace Url](#htmx)\n        -   [Reselect](#htmx)\n        -   [Reswap](#htmx)\n        -   [Retarget](#htmx)\n        -   [Trigger, Trigger After Settle and Trigger After Swap](#htmx)\n    -   [HXError, Abort and HXErrorMiddleware](#htmx)\n    -   [HXRedirect](#htmx)\n-   [Simple Localisation](#htmx)\n    -   [Configuration](#htmx)\n    -   [HXLocalisable Protocol and HXLocalisation](#htmx)\n    -   [HXRequestLocalisation](#htmx)\n    -   [Custom HXTextTag Leaf Tag](#htmx)\n-   [Other Utilities](#htmx)\n    -   [String + View](#htmx)\n    -   [Date + Custom Interval](#htmx)\n    -   [Request + Base Url](#htmx)\n    -   [staticRoute Helper](#htmx)\n-   [Changelog](#htmx)\n-   [HTMX Demo](#htmxleaf-demo)\n\n## What is HTMX?\n\nHere is my hot take: Make your backend code the single source of truth for your project, and drop most of your front end bloat in favour of updating your HTML in-place and seamlessly. Without reloading the page and with only your server side HTML templates. Learn more at [htmx.org](https://htmx.org/).\n\nAnd here is the official intro:\n\n\u003e -   Why should only `\u003ca\u003e` and `\u003cform\u003e` be able to make HTTP requests?\n\u003e -   Why should only `click` \u0026 `submit` events trigger them?\n\u003e -   Why should only `GET` \u0026 `POST` methods be available?\n\u003e -   Why should you only be able to replace the **_entire_** screen?\n\u003e\n\u003e By removing these **_arbitrary constraints_**, htmx completes HTML as a **_hypertext_**.\n\nLastly, here is a quick introduction to HTMX by `Fireship`: [htmx in 100 seconds](https://www.youtube.com/watch?v=r-GSGH2RxJs).\n\n## HTMX\n\n### Installation\n\nSPM installation:\n\n-   Add the package to your package dependencies\n\n```swift\n.package(url: \"https://github.com/RussBaz/VaporHX.git\", from: \"0.0.26\"),\n```\n\n-   Then add it to your target dependencies\n\n```swift\n.product(name: \"VHX\", package: \"VaporHX\"),\n```\n\n### Configuration\n\nAssuming the standard use of `configure.swift' in all the following examples.\n\nThe simplest config usign the Leaf engine (without localisation helpers):\n\n```swift\nimport Vapor\nimport VHX\n\n// Basic config configures Leaf engine but you are not required to use it\n\n// Basic config assumes 'index-base.leaf' template exists and that it contains '#import(\"body\")' tag\n// It will generate a dynamic template that wraps the content of the specified template for NON-htmx calls to 'htmx.render'\n// It will simply plug the provided template into the 'body' slot of the base template\n\npublic func configure(_ app: Application) async throws {\n  // other configuration\n  let config = HtmxConfiguration.basic()\n  try configureHtmx(app, configuration: config)\n  // more configuration\n}\n```\n\nPlease note that the default wrapper allows dynamically changing the base template name and the slot name. Please refer to the `render` function.\n\nOtherwise, if you want to specify your own htmx page wrapper (plugs the provided template name into a dynamically generated page on NON-HTMX requests):\n\n```swift\n// The most straightforward configuration\nimport Vapor\nimport VHX\n\n// Defining the page dynamic template generator separately\n// Check the 'HXBasicLeafSource' later in this section for further details\nfunc pageTemplate(_ template: String) -\u003e String {\n  \"\"\"\n  #extend(\"index-base\"): #export(\"body\"): #extend(\"\\(template)\") #endexport #endextend\n  \"\"\"\n}\n\npublic func configure(_ app: Application) async throws {\n  // Other configuration\n  // HTMX configuration also enables leaf templating language\n  try configureHtmx(app, pageTemplate: pageTemplate)\n  // Later configuration and routes registration\n}\n```\n\nHere are all the signatures:\n\n```swift\nfunc configureHtmx(_ app: Application, pageTemplate template: ((_ name: String) -\u003e String)? = nil) throws\n// or\nfunc configureHtmx(_ app: Application, configuration: HtmxConfiguration) throws\n\n// -------------------------------------------- //\n\n// This struct stores globally available (through the Application) htmx configuration\nstruct HtmxConfiguration {\n  var pageSource: HXLeafSource\n  // A header name that will be copied back from the request when HXError is thrown\n  // The header type must be UInt, otherwise 0 is returned\n  // Should be used by the client when retrying\n  var errorAttemptCountHeaderName: String?\n\n  // Possible ways to init the configuration structure\n  init()\n  init(pagePrefix prefix: String)\n  init(pagePrefix prefix: String = \"--page\", pageTemplate template: @escaping (_ name: String) -\u003e String)\n  init(pageSource: HXLeafSource, errorAttemptCountHeaderName: String? = nil)\n\n  // Default basic configuration\n  static func basic(pagePrefix prefix: String = \"--page\", baseTemplate: String = \"index-base\", slotName: String = \"body\") -\u003e Self\n}\n```\n\n`HXLeafSource` is used to generate a dynamic template that is used for wrapping HTMX fragments with the rest of the page content when it is accessed through a normal browser request.\n\nIt satisfies the following protocol:\n\n```swift\nprotocol HXLeafSource: LeafSource {\n  var pagePrefix: String { get }\n}\n```\n\nWhere `LeafSource` is a special `Leaf` protocol designed for customising how leaf templates are discovered.\n\nThen **VaporHX** implements its implementation of this specialised protocol.\n\n```swift\nstruct HXBasicLeafSource: HXLeafSource {\n  let pagePrefix: String\n  // This is our custom template generator\n  let pageTemplate: (_ name: String) -\u003e String\n}\n```\n\nIn order to manually initialise this struct, please use the following functions:\n\n```swift\nfunc hxPageLeafSource(prefix: String = \"--page\", template: ((_ name: String) -\u003e String)?) -\u003e HXLeafSource\nfunc hxBasicPageLeafSource(prefix: String = \"--page\", baseTemplate: String = \"index-base\", slotName: String = \"body\") -\u003e HXLeafSource\n```\n\nIn our case the default `pagePrefix` value is `--page`. Therefore, everytime you ask `leaf` for a template prefixed with `--page/` (please do not miss `/` after the prefix, it is always required), the default `HXBasicLeafSource` will return a template generated by the `pageTemplate` closure. Everything after the prefix with `/` will be passed into the page template generator and the result of this function should be a valid `leaf` template as a string.\n\nThe value passed to the `pageTemplate` method must not be empty. If it is, then `HXBasicLeafSource` will return a 'not found' error.\n\nLastly, this `LeafSource` implementation is registered as a last leaf source, and this means that the default search path is fully preserved.\n\n### HX Request Extensions\n\nHow to check if the incoming request is an HTMX request?\n\n```swift\n// Check this extensions property on the 'Request' object\nreq.htmx.prefered // Bool\n// And if you want more more accuracy ...\nreq.htmx.prefers // Preference\n\n// -------------------------------------------- //\n\n// HTMX case implies an HTMX fragment\n// HTML case implies standard browser request\n// API case implies json api request\nenum Preference {\n  case htmx, html, api\n}\n```\n\nHow to automatically decide if you need to render an HTMX fragment or a full page?\n\n```swift\n// Try this method on the 'req.htmx' extension\n// This method tries to mimic the 'req.view.render' api\n// but it also can accept optional HXResponseHeaders\n// Setting the 'page' parameter to true will force the server to always return a full page or a page fragment only otherwise\nfunc render(_ name: String, _ context: some Encodable, page: Bool? = nil, headers: HXResponseHeaders? = nil) async throws -\u003e Response\n\nfunc render(_ name: String, page: Bool? = nil, headers: HXResponseHeaders? = nil) async throws -\u003e Response\n\n// If you are using a custom templating engine, then you should use this method\nfunc render\u003cT: HXTemplateable\u003e(_ template: T.Type, _ context: T.Context, page: Bool? = nil, headers: HXResponseHeaderAddable? = nil) async throws -\u003e Response\n```\n\nFurthermore, if you are using the default template generator, you can manually override the base template name and the slot name. Here is an example how it can be done:\n\n```swift\n// Use square brackets at the beginning of the name to override default base template and slot names\n\nroutes.get(\"template\") { req in\n  // Just square brackets without colons to override base template name only\n  try await req.htmx.render(\"[index-custom]name\")\n}\n\nroutes.get(\"slot\") { req in\n  // Use the following format to override a slot name: [template:slot]\n  // Using multiple colons will result in an error\n  try await req.htmx.render(\"[index-custom:extra]name\")\n}\n```\n\nTo add an HTMX specific header to a response, you can update `response.headers` extension:\n\n```swift\nreq.htmx.response.headers // HXResponseHeaders\n```\n\nOtherwise, you can provide a header to any `render` method. It will override any previously specified headers.\n\nTo learn more about the `HXResponseHeaders`, please refere to the [Response Headers](#response-headers) section.\n\nHow to redirect quickly with proper HTMX headers?\n\n```swift\n// The simplest type of redirect\nfunc redirect(to location: String, htmx: HXRedirect.Kind = .redirect, html: Redirect = .normal, refresh: Bool = false) async throws -\u003e Response\n\n// A helper that looks for a query parameter by the 'key' value and redirects to it\nfunc autoRedirect(key: String = \"next\", htmx: HXRedirect.Kind = .redirect, html: Redirect = .normal, refresh: Bool = false) async throws -\u003e Response\n\n// A helper that looks for a query parameter by the 'key' value\n// And then redirects to a 'through location' while preserving the query parameter from the first step during the redirect\n// e.g. it can redirect from '/redirect?next=/dashboard/' to '/login?next=/dashboard/'\n// by making 'through' equal to '/login'\nfunc autoRedirect(through location: String, key: String = \"next\", htmx: HXRedirect.Kind = .redirect, html: Redirect = .normal, refresh: Bool = false) async throws -\u003e Response\n\n// A helper that redirects to 'from location' while adding the current url as query parameter with a name specified by the 'key'\n// It preserves query parameteres from the original url\n// e.g from /dashboard/ to /login?next=/dashboard/\n// by making 'from' equal to '/login'\nfunc autoRedirectBack(from location: String, key: String = \"next\", htmx: HXRedirect.Kind = .redirect, html: Redirect = .normal, refresh: Bool = false) async throws -\u003e Response\n\n// -------------------------------------------- //\n\n// HXRedirect.Kind\nenum Kind {\n  case redirect\n  case redirectAndPush\n  case redirectAndReplace\n}\n\n// What is a 'Redirect' type?\n// It is simply the default 'Vapor' type which you use with the 'req.redirect'\n```\n\n### HX Extension Method and HX\\\u003cMyType\\\u003e\n\nThe inbuilt `Content` type was extended with an `hx()` method. This is the secret ingredient that adds automatic HTMX support to the standard responses. In addition, some other types have been extended, such as `HTTPStatus` and `Abort`.\n\nHere is how it works:\n\n```swift\n// This is a slightly simplified version of this extension method declaration\nextension Content where Self: AsyncResponseEncodable \u0026 Encodable {\n  func hx(template name: String? = nil, page: Bool? = nil, headers: HXResponseHeaders? = nil) -\u003e HX\u003cSelf\u003e\n\n  // For custom templating engines\n  func hx\u003cT: HXTemplateable\u003e(template: T.Type, page: Bool? = nil, headers: HXResponseHeaders? = nil) -\u003e HX\u003cSelf\u003e where T.Context == Self\n}\n\n// And this is how you would use it\napp.get(\"api\") { req in\n// Where 'MyApi' is some Content\n  MyApi(name: \"name\").hx(template: \"api\")\n  // The return type of this function call is 'HX\u003cMyApi\u003e'\n}\n```\n\nOne should not normally deal with the `HX` struct directly but in case it is ever needed, here is its definition:\n\n```swift\ntypealias TemplateRenderer = (_ req: Request, _ context: T, _ page: Bool?, _ headers: HXResponseHeaders?) async throws -\u003e Response\n\nstruct HX\u003cT: AsyncResponseEncodable \u0026 Encodable\u003e {\n  let context: T\n  let template: TemplateRenderer?\n  let page: Bool?\n  let htmxHeaders: HXResponseHeaders?\n}\n```\n\n### HX Templateable and Custom Templating Engines\n\nIf you would like to use your own templating engine, then each renderer should implement the following protocol:\n\n```swift\nprotocol HXTemplateable {\n    associatedtype Context: AsyncResponseEncodable \u0026 Encodable\n\n    static func render(req: Request, isPage: Bool, context: Context) -\u003e String\n}\n```\n\nIf this protocol is not enough for your use case, please open an issue and we can discuss it there.\n\n### Request Headers\n\n```swift\n// 'HTMX' request header getter on every request\nreq.htmx.headers\n\n// Request header structure\n// For the meaning of value of each header, please refer to the 'HTMX' docs\nstruct HXRequestHeaders {\n  let boosted: Bool\n  let currentUrl: String?\n  let historyRestoreRequest: Bool\n  let prompt: Bool\n  let request: Bool\n  let target: String?\n  let triggerName: String?\n  let trigger: String?\n}\n```\n\n### Response Headers\n\n#### Overview\n\n`HTMX` headers are defined as structs with a simple inbuilt validation. They can be added as individual headers to a `Response` object or as a whole collection by using `HXResponseHeaders` struct. The latter struct can be passed to `htmx` specific functions as an optional parameter, such as `.htmx.render` or `.hx`\n\n```swift\n// Cleaned up definition\nstruct HXResponseHeaders {\n  var location: HXLocationHeader?\n  var pushUrl: HXPushUrlHeader?\n  var redirect: HXRedirectHeader?\n  var refresh: HXRefreshHeader?\n  var replaceUrl: HXReplaceUrlHeader?\n  var reselect: HXReselectHeader?\n  var reswap: HXReswapHeader?\n  var retarget: HXRetargetHeader?\n  var trigger: HXTriggerHeader?\n  var triggerAfterSettle: HXTriggerAfterSettleHeader?\n  var triggerAfterSwap: HXTriggerAfterSwapHeader?\n}\n\n// Example usages\n// With 'hx' extension\napp.get(\"api\") { req in\n  let headers = HXResponseHeaders(retarget: HXRetargetHeader(\"#content\"))\n  return MyApi(name: \"name\").hx(template: \"api\", headers: headers)\n}\n\n// With 'htmx.render' function\napp.get(\"example\") { req in\n  let headers = HXResponseHeaders(retarget: HXRetargetHeader(\"#content\"))\n  return req.htmx.render(\"example\", headers: headers)\n}\n\n// With 'add' extension method on 'Response'\n// Can be used with the whole container ('HXResponseHeaders') or with individual headers (such as 'HXRetargetHeader')\n// Later header values will replace earlier headers\napp.get(\"redirect\") { req in\n  req.htmx.autoRedirect(through: \"/login\", html: .temporary).add(headers: HXResponseHeaders())\n}\n```\n\n#### Location\n\n`HXLocationHeader` is a type-safe constructor for a `HX-Location` response header. It is the most complicated response header in my opinion.\n\n```swift\nstruct HXLocationHeader {\n  let location: HXLocationType\n}\n\nenum HXLocationType {\n  case simple(String) // Header value is just a string\n  case custom(HXCustomLocation) // Header value is json\n}\n\nstruct HXCustomLocation: Encodable {\n  let path: String\n  let target: String?\n  let source: String?\n  let event: String?\n  let handler: String?\n  let swap: HXReswapHeader?\n  let values: [String]?\n  let headers: [String: String]?\n}\n```\n\nBecause of the sheer volume of possible options, there is a 'fluent' api to generate a new header struct with updated property values, such as:\n\n```swift\nfunc set(target newTarget: String) -\u003e Self\nfunc set(source newSource: String?) -\u003e Self\nfunc set(event newEvent: String?) -\u003e Self\nfunc set(handler newHandler: String?) -\u003e Self\nfunc set(swap newSwap: HXReswapHeader?) -\u003e Self\nfunc set(values newValues: [String]?) -\u003e Self\nfunc set(headers newHeaders: [String: String]?) -\u003e Self\n// Removes all other properties and sets the location property to simple\nfunc makeSimple(_ path: String? = nil) -\u003e Self\n```\n\nA few examples of how to create a simple and more complicated headers:\n\n```swift\nlet header1 = HXLocationHeader(\"/test\") // reuslts in \"HX-Location: /test\"\nlet header2 = HXLocationHeader(\"/test2\", target: \"#testdiv\") // results in \"HX-Location: {\"path\":\"/test2\", \"target\":\"#testdiv\"}\"\n// and so on\n```\n\n#### Push Url\n\n`HXPushUrlHeader` is a type-safe constructor for a `HX-Push-Url` response header.\n\n```swift\nstruct HXPushUrlHeader {\n    let url: HXPushType\n}\n\nenum HXPushType {\n    case disable\n    case enable(String) // where the string should contain the new value to be pushed onto the browser location history stack\n}\n```\n\nA few examples:\n\n```swift\nlet header1 = HXPushUrlHeader(url: .disable) // results in \"HX-Push-Url: false\"\nlet header2 = HXPushUrlHeader(\"/test\") // results in \"HX-Push-Url: /test\"\n```\n\n#### Redirect\n\n`HXRedirectHeader` is a type-safe constructor for a `HX-Redirect` response header. Please do not confuse this response header with the `HXRedirect` structure.\n\nIt is extremely simple:\n\n```swift\nstruct HXRedirectHeader {\n    let location: String\n}\n\n// sample usage\nlet header = HXRedirectHeader(\"/test\") // results in \"HX-Redirect: /test\"\n```\n\n#### Refresh\n\nTo be continued...\n\n## HTMX+Leaf Demo\n\nFor those new to HTMX, this package comes bundled with an HTMX Demo that you can run locally. It consists of a few examples from the [HTMX.org](https://htmx.org/examples/) website.\n\nThis is a showcase of some HTMX features built with the Leaf templating engine and VaporHX. It is not a showcase of all the capabilities of this library, but a good introduction to HTMX. It is also not a production ready example.\n\n#### To run the demo using Xcode\n\n-   Open this project in Xcode\n-   Switch the scheme from `VHX` to `Demo` (left hand side of the top url bar)\n-   Confirm that the correct Run Target is selected (likely 'My Mac')\n-   Set the custom working directory in the scheme editor to the root folder of this package as shown in the [Vapor docs](https://docs.vapor.codes/getting-started/xcode/). Otherwise, it will display a warning in the console and it will be unable to find leaf templates.\n-   Press the play / run button\n-   Then head to `http://localhost:8080`\n\n#### To run the demo using the command line\n\n-   execute the following from the VaporHX projects root dir\n\n```bash\nswift run Demo\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frussbaz%2Fvaporhx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frussbaz%2Fvaporhx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frussbaz%2Fvaporhx/lists"}