Skip to content

Core concepts

Tibor Bödecs edited this page Jan 8, 2021 · 1 revision

Swift Packages

The main components of Feather CMS are distributed as standalone packages using the Swift Package Manager.

Feather CMS was created using a modular event-driven design in mind. This means that the core system has a very small footprint and mainly focuses on the basic components rather than higher level business logic.

FeatherCore

This package contains all the shared code that is required to build a module for Feather CMS.

The core system is built on top of Vapor 4 and some other packages that allows developers to build modular backends.

The FeatherCore package exposes all the necessary package requirements, so you only have to import FeatherCore when building a new module, one exception is the Fluent ORM package, when you have to work with query operators you might have to writeimport Fluent as well.

The core system provides following components:

  • Database Abstraction Layer: Fluent ORM
  • Abstract File Storage: Liquid FS
  • Hook system
  • Dynamic routing
  • Frontend metadata interface
  • Module API - ViperKit
  • REST API support - ContentApi
  • Template system - ViewKit
  • Leaf extensions - Leaf Foundation
  • JWT support (as part of Vapor 4)

Core modules

Feather also comes with modules, there are just a few that are pretty much required for every project. We call them core modules and they provide basic functions such as the admin interface or system functions.

  • System - Basic system management functions (installer, variables)
  • User - User management and access control system
  • Admin - Basic admin interface components and dynamic route support
  • Api - A simple REST API interface layer with dynamic route support
  • Frontend - The backbone of the web-based frontend interface

By default you should use these modules since they provide relatively basic functions. It is possible to run Feather without the core modules or only use the ones that you need for your project.

For example if you are planning to build a headless CMS (without the frontend) you can remove the frontend module and keep the functions you need (Api, Admin, User, System).

Another example is a public Api project based on JSON files located on the server. In this case you might want to remove both the System, User, Admin and Frontend modules and only go with the Api.

Application structure

A custom Feather application very similar to a standard Swift package. First you need a manifest file that defines all your dependencies and build targets. Consider the following Package.swift file:

// swift-tools-version:5.3
import PackageDescription

let package = Package(
    name: "feather",
    platforms: [
       .macOS(.v10_15)
    ],
    products: [
        .executable(name: "Feather", targets: ["Feather"]),
    ],
    dependencies: [
        /// drivers
        .package(url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.0.0"),        .package(url: "https://github.com/binarybirds/liquid-local-driver", from: "1.2.0-beta"),
        /// feather core
        .package(url: "https://github.com/FeatherCMS/feather-core", from: "1.0.0-beta"),
        /// core modules
        .package(url: "https://github.com/FeatherCMS/system-module", from: "1.0.0-beta"),
        .package(url: "https://github.com/FeatherCMS/user-module", from: "1.0.0-beta"),
        .package(url: "https://github.com/FeatherCMS/api-module", from: "1.0.0-beta"),
        .package(url: "https://github.com/FeatherCMS/admin-module", from: "1.0.0-beta"),
        .package(url: "https://github.com/FeatherCMS/frontend-module", from: "1.0.0-beta"),
    ],
    targets: [
        .target(name: "Feather", dependencies: [
            /// drivers
            .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
            .product(name: "LiquidLocalDriver", package: "liquid-local-driver"),
            /// feather
            .product(name: "FeatherCore", package: "feather-core"),
            /// core modules
            .product(name: "SystemModule", package: "system-module"),
            .product(name: "UserModule", package: "user-module"),
            .product(name: "ApiModule", package: "api-module"),
            .product(name: "AdminModule", package: "admin-module"),
            .product(name: "FrontendModule", package: "frontend-module"),
        ]),
    ]
)

All the Swift source files should be located under the Sources directory under a given target name. In this case we have an executable target with one simple file called main.swift inside the Feather folder.

import FeatherCore
import FluentSQLiteDriver
import LiquidLocalDriver

import SystemModule
import UserModule
import ApiModule
import AdminModule
import FrontendModule

Feather.metadataDelegate = FrontendMetadataDelegate()

var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)
let feather = try Feather(env: env)
defer { feather.stop() }

/// configure Feather using the db driver, fs driver and the core module builders
try feather.configure(database: .sqlite(.file("db.sqlite")),
                      databaseId: .sqlite,
                      fileStorage: .local(publicUrl: Application.baseUrl, publicPath: Application.Paths.public, workDirectory: "assets"),
                      fileStorageId: .local,
                      modules: [
                        SystemBuilder(),
                        UserBuilder(),
                        ApiBuilder(),
                        AdminBuilder(),
                        FrontendBuilder(),
                      ])

if feather.app.isDebug {
    try feather.reset(resourcesOnly: true)
}

try feather.start()

Directory structure

Feather will copy all the required assets to the right place when you start the server at the first time.

Public files and resources are bundled within modules by default, so if you need custom templates, assets or style sheets feel free to alter the contents of the Public and Resources directories (don't forget to commit the changes into your git repository).

Recommended folder structure for your project:

  • Public - publicly available resources, such as images, CSS & JS files
    • assets - default storage for the local file storage driver (uploaded file storage)
    • css - public css files
    • images - public images
    • javascript - public javascript files
  • Resources - Location of the private resource files
    • Templates - Leaf template files (used to render various outputs)
  • Sources - Swift source files for the server application
    • Modules - place local modules into this folder
  • Tests - Test targets for your application

If you want a fresh start, feel free to delete the Public and Resources folders, drop your database and simply restart Feather, the system will re-create everything that is needed to run the server.

Environment

Just like any Vapor application, Feather also supports multiple environments. Feather is a bit more strict about environments, this means that you always have to define two environmental variables.

  • BASE_URL - The base URL of your app server (e.g. http://0.0.0.0:8080)
  • BASE_PATH - The absolute path to the app (e.g. /Users/[me]/feather/)

The BASE_URL is used because there is no easy way to resolve the current domain based on an incoming request, this variable is always available as a static property on the Application object.

The BASE_PATH helps us with directory configuration. Feather also provides absolute path variables (always with a trailing slash) and locations (relative folder names with trailing slash) under the Application struct.

Application.baseUrl 			// shorthand for the "BASE_URL" env variable
Application.Paths.base 		// shorthand for the "BASE_PATH" env variable

In order to use Feather you always have to define these two environment variables. The most simple way is to create a dotenv file (.env or .env.development based on your needs) using the following format:

BASE_URL="http://0.0.0.0:8080"
BASE_PATH="/Users/[me]/feather/"

If you are using Xcode don't forget to set the custom working directory for your target. ⚠️

You can read more about environments in the official Vapor docs.

Database and file storage drivers

Database and file storage drivers are NOT part of the core system, you have to add the required ones as a Swift package dependency when creating your project. The following divers are available to use:

Database (Fluent) drivers:

  • PostgreSQL
  • SQLite
  • MySQL
  • MariaDB
  • MongoDB

File storage (Liquid) drivers:

  • Local
  • AWS S3

You should use the Feather struct to create, configure and run a new application.

Here's how you can setup your own drivers using the Swift package manager:

/// project dependencies
.package(url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.0.0"),
.package(url: "https://github.com/binarybirds/liquid-local-driver", from: "1.2.0-beta"),
.package(url: "https://github.com/FeatherCMS/feather-core", from: "1.0.0-beta"),

/// target dependencies
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
.product(name: "LiquidLocalDriver", package: "liquid-local-driver"),
.product(name: "FeatherCore", package: "feather-core"),

Then inside your main.swift file:

import FeatherCore
import FluentSQLiteDriver
import LiquidLocalDriver

try feather.configure(database: .sqlite(.file("db.sqlite")),
                      databaseId: .sqlite,
                      fileStorage: .local(publicUrl: Application.baseUrl, publicPath: Application.Paths.public, workDirectory: "assets"),
                      fileStorageId: .local,
                      modules: [])

Hook system

The hook system is a generic event processing system written in Swift for Feather CMS. This event-driven architecture pattern allows us to communicate with modules without forming actual dependencies.

Instead of utilizing dependencies, protocols and types the hook system allows us to create events and hook into those "extension points" later on.

Registering hooks

Every module can register hook function handlers by using the boot method.

For example the system module provides various install events that you can use to create your own database the first time you run Feather in your browser.

func boot(_ app: Application) throws {
    /// register the model install hoook
    app.hooks.register("model-install", use: modelInstallHook)
}

func modelInstallHook(args: HookArguments) -> EventLoopFuture<Void> {
    let req = args["req"] as! Request
  
	  /// install the necessary models using the req object and return a Void future
    return req.eventLoop.future()
}

In this case the system module will call the "model-install" hook when it's ready to install Fluent models. The HookArguments type is just a key-value dicitionary alias. Usually you can access the request under the req key and the application object using the app key.

Hooks are generic functions, the return type of the hook is specified during the invokation.

Invoking hooks

You can create your own hook event by invoking one or multiple registered hook functions.

/// invoke all hooks to return the model install futures
let modelInstallFutures: [EventLoopFuture<Void>] = req.invokeAll("model-install")

/// invoke all dynamic admin route hooks with a routes parameter 
let _: [Void] = app.invokeAll("admin", args: ["routes": protectedAdmin])

/// invoke the variable-get hook (returns only the first hook) with a given key
let result: EventLoopFuture<String?>? = req.invoke("variable-get", args: ["key": key])

As you can see there are two methods available on the Request and Application objects.

  • The invoke method only returns the very first instance of the registered hooks
  • The invokeAll method returns all the registered hooks as an array of objects

You always have to specify the return type of a given hook and you can also pass additional arguments using the arg parameter. It is also possible to use the first result of an optional EventLoopFuture, there is a helper method called findFirstValue if you need such functionality.

let futures: [EventLoopFuture<Response?>] = req.invokeAll("frontend-page")
return req.eventLoop.findFirstValue(futures).unwrap(or: Abort(.notFound))

This is useful when you depend on multiple database queries and you want to return the right object based on the circumstances, like the system handles the frontend page for a given slug.

Many hooks are available to use in Feather CMS and as you can see you can create your own extension points. You can read more about how these functions work under the hood if you read this blog post.

Dynamic routing

Vapor 4 has a static route system by default. This approach is great for many use cases, but Feather CMS enables us to register dynamic routes based on varius conditions using hook functions.

If you want to use the dynamic route system first you have to register a router hook function.

func boot(_ app: Application) throws {
    /// base routes
    app.hooks.register("routes", use: routesHook)
    /// session protected admin routes hook
    app.hooks.register("admin", use: adminRoutesHook)
    /// private api routes hook
    app.hooks.register("api", use: privateApiRoutesHook)
    /// public api routes hook
    app.hooks.register("public-api", use: publicApiRoutesHook)
}

There are multiple route hooks available the core modules provide the following routes:

  • routes - dynamic base route
  • admin - session protected /admin/ endpoints
  • api - publicly available /api/ endpoints
  • public-api - token protected /api/ endpoints

Inside the routes hook you can access the RoutesBuilder object under the routes key:

func routesHook(args: HookArguments) {
    let app = args["app"] as! Application
    let routes = args["routes"] as! RoutesBuilder

    let middlewares: [[Middleware]] = app.invokeAll("admin-auth-middlewares")

    /// groupd admin routes, first we use auth middlewares then the error middleware
    let protectedAdmin = routes.grouped("admin").grouped(AdminErrorMiddleware()).grouped(middlewares.flatMap { $0 })
    /// setup home view (dashboard)
    protectedAdmin.get(use: adminController.homeView)
    /// hook up other admin views that are protected by the authentication middleware
    let _: [Void] = app.invokeAll("admin", args: ["routes": protectedAdmin])
}

As you can see you can use the standard Vapor methods on the builder to setup new endpoints or you can use the hook system to extend your routes with additional middlewares or provide further extension point for other modules.

A routes hook function has no return type, just simply register the required endpoints.

Of course if you don't need the dynamic route system, you can use the original boot method of the RouteCollection object (part of Vapor) to register your static routes.

func boot(routes: RoutesBuilder) throws {
  /// register sitemap and rss routes
  routes.get("sitemap.xml", use: frontend.sitemap)
  routes.get("rss.xml", use: frontend.rss)
  routes.get("robots.txt", use: frontend.robots)
}

Route handlers are always standard functions with a Request argument and a Response return type.

The static route system is only good for the task when we work with a fixed set of URLs, but since Feather is a dynamic CMS we should be able to register new pages on the fly. The dynamic route system helps us to eliminate this issue so we can build frontends more easy.

Frontend metadata interface

When you render a HTML page you might want to provide some additional info alongside the contents of the body tags. Modern websites use stylesheets (CSS) and JavaScript snippets to provide rich functionality to the end user. Search Engine Bots might index your pages, they can also use sitemaps to get a basic understanding of your site structure. Many people prefer RSS readers to get updated from multiple sources at once, so you might want to include your content in a dynamically generated feed. Modern social media websites can also generate a preview of your content based on a title, short description, image and maybe some other details.

This is where the frontend metadata interface can help us. The frontend metadata interface is used to define publicly available content pages for your website. You can think of a Metadata object as an additional information provider for a given page. The frontend core module provides a base template that you can use to render HTML page bodies inside a "main frame", but we still have to provide the necessary meta description, that's why we have an abstraction layer in the FeatherCore module, so we can use a module independent Metadata object that's available for everyone to use.

Metadata

For example if you want to build a news module, you can use the frontend module and provide dynamic news items with SEO friendly URLs, without the need of reimplementing the entire Metadata structure. This way you can focus on your own model structure instead of thinking too much about the web. The metadata interface will provide all the necessary properties to safely render frontend pages.

struct Metadata: LeafDataRepresentable {
  
   public enum Status: String, CaseIterable, Codable {
       /// drafts are only visible through the admin interface
        case draft
        /// published articles can be indexed and they are visible for everyone
        case published
        /// archives are not visible nor indexed
        case archived
   }

    var id: UUID? /// unique identifier of the metadata
    var module: String? /// referenced module name
    var model: String?  /// referenced model name
    var reference: UUID? /// referenced model identifier 

    var slug: String? /// slug without leading and trailing slashes
    var status: Status? /// status of the metadata
    var title: String? /// meta title of the content (SEO, social media, feeds, etc.)
    var excerpt: String? /// meta description (SEO, social media, feeds, etc.)
    var imageKey: String? /// preview image (SEO, social media, feeds, etc.)
    var date: Date? /// publish date (SEO, social media, feeds, etc.)
    var feedItem: Bool? /// if true content will be included in the RSS feed
    var canonicalUrl: String? /// original content reference URL string (SEO)

    var filters: [String]? /// enabled Feather content filters
    var css: String? /// custom CSS for the content
    var js: String? /// custom JS for the content
}

Imagine that you create a NewsModel Fluent database object, if you want to use that as a base object for frontend pages you'll have to conform to the MetadataRepresentable protocol.

extension NewsModel: MetadataRepresentable {

    var metadata: Metadata {
        .init(slug: title.slugify(), title: title, excerpt: excerpt, imageKey: imageKey)
    }
}

Now if you modify a NewsModel using the CMS or a code snippet, you might want to reflect those changes to the associated Metadata model as well. This is why you have to add an new MetadataModelMiddleware instance to the database middlewares with the given object type.

func boot(_ app: Application) throws {
    app.databases.middleware.use(MetadataModelMiddleware<NewsModel>())
}

This database middleware will ensure that the associated metadata is created, updated or removed from the persistent storage if needed. Metadata objects are automatically managed by the frontend core module. Using the metadata API is highly recommended for creating dynamic pages.

Metadata delegate

Almost everything can be replaced in Feather CMS you can even create your own MetadataDelegate object that can handle the entire metadata management process. By default the frontend module provides one implementation that fetches, saves, updates and removes metadata objects under the hood.

Feather.metadataDelegate = FrontendMetadataDelegate()

You should always set the metadataDelegate property before you run Feather, otherwise you won't be able to use the metadata interface. You can also build your own metadata delegate if you want to replace the core implementation that's part of the frontend module.