Dynamic routes and page hooks in Feather CMS on top of Vapor 4

Learn how to create custom pages in Swift and register SEO friendly clean URLs using the Feather CMS routing system.

Vapor

Routing in Vapor 4

Clicking a link in your browser usually takes you to a new page on some website. This is pretty straightforward, everybody loves browsing the world wide web through clicking links, right? 😅

The other part of the story happens on the server side behind the scenes. A backend server can handle a given URL in multiple ways, but nowadays the most popular approach is to use a SEO friendly component (let's call this path) to determine the content that needs to be returned as a response to the client (browser), of course we're using the HTTP protocol to transfer data.

So when a client asks for a specific route (path, URL, whatever... e.g. /foo) the server needs to decide what to return. This decision making process is called routing. In Vapor 4 you can easily setup route handlers for a specific endpoint.

import Vapor

public func configure(_ app: Application) throws {
    app.get("foo") { req -> String in
        return "bar"
    }
}

This is perfectly fine, but quite a static approach. By static I mean that you have to alter the Swift code if you want to register a new route. You also have to rebuild and eventually redeploy the server application if you want to reflect the changes. That's programmer friendly, but not so user friendly.

Another thing is that you have to be specific about your path when you setup your routes, otherwise you have to use the catchall pattern if you want to handle dynamic routes. Sometimes this can also lead to some unexpected problems, but it's not the end of the world. Vapor is prepared for the issue and the framework handles this problem in such an elegant way. Good job team Vapor. 👍

So, how do we register dynamic routes using Feather and allow the end user to specify custom endpoints? Is it possible to write the page in Swift and later on attach the code to an endpoint?


Dynamic routes in Feather CMS

In order to write a page in Swift and register it later on under a dynamic path component you have to implement a custom page hook function. This is pretty simple, you just have to create a new module and implement the invoke method just like we did it for the frontend-page hook in the Feather CMS module tutorial post. A custom page hook is a special hook function where you explicitly set the name of the hook with a -page suffix. The static module will allow you to render this page via a special content filter, more about this part later on. 💪

import Vapor
import Fluent
import ViperKit

final class FooModule: ViperModule {

    static var name: String = "foo-module"

    func invoke(name: String, req: Request, params: [String : Any] = [:]) -> EventLoopFuture<Any?>? {
        switch name {
        case "foo-page":
            let content = params["page-content"] as! FrontendContentModel
            return try! self.renderPage(req: req, page: content).map { $0 as Any }
        default:
            return nil
        }
    }
    
    func renderPage(req: Request, page content: FrontendContentModel) throws -> EventLoopFuture<Response> {
        req.eventLoop.future("foo").encodeResponse(for: req)
    }
}

The code is pretty simple, every single page has to be associated with a FrontendContentModel object. You'll can retreive this model as an input parameter and use it if you need special frontend related attributes during the rendering process. Please keep in mind that you always have to return a future with a standard Response type. This is because hooks are very special functions and the page hook can only work with Response objects, you can always use the encodeResponse method in most of the cases. In the hook you have to cast once more to Any, because we love casting dynamic types in strongly typed languages. Anyway, it works great in practice... 🙈

So if you don't care too much about this entire type casting hell, you can simply place your existing Vapor route handler into the render function and return the output as a response. This way you can log in to the CMS and create a new page with a unique slug for it. Let me show you how to do this real quick. After you log in as an admin, just open the "Static / Pages" menu item from the dashboard and create a new page with the [foo-page] shortcode content. Give it a name and press save.

If you click on the eye icon on the top left corner you should be able to see the draft version of your page. As you can see it's just a blank page with the raw "foo" text on it, nothing special, but hey, feel free to use Leaf and render some actual content. Now if you go back to the admin you should see a little feather icon next to the preview button, if you click on that you can edit the user-facing content details of your page, such as the slug (path, permalink, URL, whatever you call it), meta title and description, social preview image, publish date, content filters etc.

You can always change the slug and hook up your page into a new route. When you press the save button (or hit cmd+s) the changes will be live immediately, no need to rebuild your backend server at all. Of course, if you messed up your code on the first place you still have to fix that before you can hook it up, but hopefully that won't happen too often. 🔨

The key takeaway here is that you can write your code in Swift and later on hook the page up to a specific route using a dynamic approach. This is real nice if you have to change the URL of a post or a page, but you still have the opportunity to create a regular page or post using the admin interface. You can use HTML or markdown or you can always build your own content filter for Feather CMS if you need more custom behavior. If you don't know where to start with Feather, you should read my getting started tutorial, if you have issues, please report on GitHub or submit a PR.

Share this article on Twitter.
Thank you. 🙏

Picture of Tibor Bödecs

Tibor Bödecs

Creator of https://theswiftdev.com (weekly Swift articles), server side Swift enthusiast, full-time dad. -- Follow me & feel free to say hi. 🤘🏻 -- #iOSDev #SwiftLang

Twitter · GitHub


📬

100% Swift news, delivered right into your mailbox

Subscribe to my monthly newsletter. On the first Monday of every month, you'll get an update about the most important Swift community news, including my articles.