/feather

Feather is a modern Swift-based content management system powered by Vapor 4.

Primary LanguageSwift

Feather CMS

Feather CMS 🦚

Feather is a modern Swift-based content management system powered by Vapor 4.

Requirements

To use Feather CMS you'll have to install Swift 5.2 or greater.

In other words Feather has the exact same system requirements as Vapor 4.

Installation

  • Clone or download the source files using this repository
git clone https://github.com/BinaryBirds/feather.git
  • Setup the .env.development file using the make env command or config the following values by hand according to your needs:
# the base url of your web server
BASE_URL="http://localhost:8080"

# the base location of the server files (Public, Resources)
BASE_PATH="/path/to/feather/" 
  • Created a Public/assets directory with write permission for your server (use chmod if needed).
  • Open the project via the Package.swift file using Xcode and set the custom working directory for the Run scheme.
  • You don't need to generate an xcodeproj file anymore, please always open the Package.swift file if possible.
  • You can also compile the project and run the server (without Xcode) using one of the following commands:
    • make run
    • swift run Run
    • vapor build && vapor run serve
  • Build and run the project and enjoy your Feather powered site at https://localhost:8080/
  • Migration & sample content installation will be performed at the first time when you hit the URL.
  • You can log in to the admin using the feather@binarybirds.com & FeatherCMS account.
  • Please change the default email & password using the admin / user menu. 😅

nginx

Setup nginx as a reverse proxy server.

You can disable the file middleware in this case in the configure.swift file if you prefer nginx as a static file server.

Check the link for more instructions and please note that nginx is the preferred way of hosting Vapor / Feather based apps.

PostgreSQL

  • First of all, you will need a running PostgreSQL database server.
  • In the Package.swift file uncomment the PosgreSQL related dependency.
  • Add the following DB_URL variable to your .env.development file:
# change the user, pass, host, port and db name according to your needs 
DB_URL=postgres://myuser:mypass@localhost:5432/mydb
  • Uncomment the psql related code in the configure.swift file and comment out the sqlite configs.
  • That's it. You are ready to use the PostgreSQL driver.

Other database drivers

It is possible to use MySQL (MariaDB) or MongoDB as your database via the Fluent framework.

You should follow the instructions using the official Vapor docs to setup the right driver, but please note that the preferred driver is PosgreSQL (and SQLite).


Modules

The architecture of Feather CMS is pretty much the same as the one I described in my Practical Server Side Swift book to build a modular blog engine. If you already purchased the book you should be familiar with most of the system. If you want to know more about server side Swift this is a great opportunity to learn more about building backend apps using Vapor 4. By purchasing the book you also support my work and I'm really greatful for that. Thank you. 🙏

Most of the functionality you see in Feather is provided by modules. There are two types of modules in the engine.

Core modules

These modules provide basic core functionality. You should never remove them.

  • System - This module is reponsible for the system functionalities.
  • User - This module is reponsible for user authentication.
  • Api - This module is reponsible for the API endpoints.
  • Admin - This module is reponsible for the web-based admin interface.
  • Frontend - This module is reponsible for the web-based frontend layout including page contents.

User modules

These modules can be removed if you don't need them. You can create new user modules to extend the system.

  • Redirect - This module is reponsible for dynamic URL redirects.
  • Blog - This module is reponsible for providing a simple blog platform.
  • Static - This module is reponsible for the displaying static pages.
  • Markdown - This module is reponsible for displaying markdown via a content filter.
  • Syntax - This module is reponsible for Swift related syntax highlights using a content filter.
  • Sponsor - This module is reponsible for displaying a sponsorship box.

Hook functions

Modules can communicate via hook functions.

System hooks

  • install - you can populate the database using the install hook.
  • frontend-page - you can use this hook to display dyanmic content using a specific url pattern (slug)
  • page-content - you can use this hook to render pages written as Swift functions
  • content-filter - you can apply content filters using this hook

Page hooks

These hooks can be used via the static page module. You can enter the page value in brackets as the content of a page and Feather will render that page. For example the [home-page] hook will display the home page provided the blog module, the [posts-page] will display all the available blog posts.

  • home-page - Home page with the most recent blog posts
  • posts-page - All the blog posts
  • categories-page - All the blog categories
  • authors-page - All the blog authors

Maybe we should prefix these page hooks with the module name later on, what do you think? 🤔

Dynamic route hooks

You can hook up routes dynamically via route hooks. Both the frontend, admin and the API module provides some extension points.

  • public-admin - publicly available admin pages (usually you don't want to use this)
  • protected-admin - protected admin pages (only available after a session based user auth)
  • public-api - public api endpoints (available without user authentication)
  • protected-api - protected api endpoints (user must be authenticated via a token)

Other

Frontend contents

Every user facing content has an associated frontend content type. You can create your own content that can be part of the sitemap and feed items, the static page and the blog module are a great examples of doing this.

Content properties

Basics:

  • slug - The unique url of the page (without the domain & port)
  • status (draft, published, archive) - drafts can be previewed with a noindex meta tag, published contents are public, archived content won't be visible & indexed at all
  • date - publish date of the given content
  • feed item - if it is true it'll be part of the rss feed
  • filters - enabled content filters

SEO related:

  • title - meta title of the content
  • excerpt - meta description of the content
  • image - meta image of the content
  • canonical url - additional canonical url

System related:

  • module - referenced module name
  • model - referenced model name
  • reference - referenced unique identifier of the model

Content filters

Some content can contain special character sequences, it is possible to replace these via content filters. You can write your own filter by using a module hook:

import Vapor
import Fluent
import ViperKit

final class CustomFilterModule: ViperModule {

    static var name: String = "custom-filter"

    func invokeSync(name: String, req: Request, params: [String: Any]) -> Any? {
        switch name {
        case "content-filter":
            return [CustomFilter()]
        default:
            return nil
        }
    }
}
import Vapor
import Fluent
import ViperKit

struct CustomFilter: ContentFilter {
    var key: String { "custom-filter" }
    var label: String { "Custom filter" }

    func filter(_ input: String) -> String {
        input.replacingOccurrences(of: "hello", with: "hi"))
    }
}

The example above will replace the hello text to hi if you enable the Custom filter for a specific content.

You can find two sample content filters for educational purposes:

  • Markdown (Ink)
  • Splash

Page templates

The static page module provides a special functionality called page templates. You can create your own page written in Swift and dynamically hook it up as a template via the static page module.

import Vapor
import Fluent
import ViperKit

final class ExamplePageTemplateModule: ViperModule {

    static let name = "example-page-template"

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

    func exampleView(req: Request, page content: FrontendContentModel) throws -> EventLoopFuture<Response> {
        return req.view.render("ExamplePageTemplateModule/Frontend/Template")
        .encodeResponse(for: req)
    }
}

ExamplePageTemplateModule/Views/Frontend/Template.html

#extend("Frontend/Index"):
    #export("body"):
    	<p>Hello world!</p>
    #endexport
#endextend

Since Xcode can't highlight leaf files by default Feather uses the .html extension, so we can get partial highlight for the views.

Views

Views can be loaded using two sources:

  • from the Sources/App/Modules/[module]/views directory
  • from the Resources/Views/[module] directory

You can copy all the views to the Resources folder by running the make views command. The system will try to load the view from the Resources directory first, if it can't find then it'll look for it in the Modules directory.


Debug

If you see a Segmentation fault: 11 error or something similar, you can start the server through the lldb debugger.

lldb ./.build/debug/Run
process launch serve

# print backtrace
bt
# look up a symbol
image lookup -a 0x1000 

Start the debugger and launch the serve command. Then try to repeat the steps that caused the crash. You can print out the backtrace using the bt command, this can help you to identify the problem.


Credits


License

WTFPL