Skip to content

Commit

Permalink
Add support for static site layouts
Browse files Browse the repository at this point in the history
This should make it possible for users to export their site in
a format easy to deploy on a static hosting site, e.g. Netlify

This disables some of the features that make Kobweb powerful
(e.g. dynamic routes and server API routes), but for many use
cases, e.g. portfolios or blog sites, this is fine.

Bug #22
  • Loading branch information
bitspittle committed Feb 9, 2022
1 parent a65fa16 commit 703ee41
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.varabyte.kobweb.server
import com.varabyte.kobweb.common.error.KobwebException
import com.varabyte.kobweb.project.KobwebFolder
import com.varabyte.kobweb.project.conf.KobwebConfFile
import com.varabyte.kobweb.server.api.SiteLayout
import com.varabyte.kobweb.server.api.ServerEnvironment
import com.varabyte.kobweb.server.api.ServerRequest
import com.varabyte.kobweb.server.api.ServerRequestsFile
Expand All @@ -11,7 +12,6 @@ import com.varabyte.kobweb.server.io.ServerStateFile
import com.varabyte.kobweb.server.plugins.configureHTTP
import com.varabyte.kobweb.server.plugins.configureRouting
import com.varabyte.kobweb.server.plugins.configureSerialization
import io.ktor.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import kotlinx.coroutines.delay
Expand Down Expand Up @@ -60,10 +60,10 @@ fun main() = runBlocking {
throw KobwebException("Production server can't start as port $port is already occupied. If you need a different port number, consider modifying ${confFile.path}")
}
}

val globals = ServerGlobals()
val siteLayout = SiteLayout.get()
val engine = embeddedServer(Netty, port) {
configureRouting(env, conf, globals)
configureRouting(env, siteLayout, conf, globals)
configureSerialization()
configureHTTP(conf)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.varabyte.kobweb.api.http.Request
import com.varabyte.kobweb.api.log.Logger
import com.varabyte.kobweb.project.conf.KobwebConf
import com.varabyte.kobweb.server.ServerGlobals
import com.varabyte.kobweb.server.api.SiteLayout
import com.varabyte.kobweb.server.api.ServerEnvironment
import com.varabyte.kobweb.server.io.ApiJarFile
import io.ktor.application.*
Expand All @@ -21,15 +22,14 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File
import java.io.IOException
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.name

/** Somewhat uniqueish parameter key name so it's unlikely to clash with anything a user would choose by chance. */
private const val KOBWEB_PARAMS = "kobweb-params"

fun Application.configureRouting(env: ServerEnvironment, conf: KobwebConf, globals: ServerGlobals) {
fun Application.configureRouting(env: ServerEnvironment, siteLayout: SiteLayout, conf: KobwebConf, globals: ServerGlobals) {
val logger = object : Logger {
override fun trace(message: String) = log.trace(message)
override fun debug(message: String) = log.debug(message)
Expand All @@ -38,10 +38,26 @@ fun Application.configureRouting(env: ServerEnvironment, conf: KobwebConf, globa
override fun error(message: String) = log.error(message)
}

when (env) {
ServerEnvironment.DEV -> configureDevRouting(conf, globals, logger)
ServerEnvironment.PROD -> configureProdRouting(conf, logger)
if (siteLayout == SiteLayout.STATIC && env != ServerEnvironment.PROD) {
log.warn("""
Static site layout is configured for a development server.
This isn't expected, as development servers expect to read their values from the user's project. Static
layouts are really only designed to be used in production. The server will still run in static mode as
requested, but live-reloading, server APIs, etc. will not work with this configuration.
""".trimIndent())
}

when (siteLayout) {
SiteLayout.KOBWEB -> {
when (env) {
ServerEnvironment.DEV -> configureDevRouting(conf, globals, logger)
ServerEnvironment.PROD -> configureProdRouting(conf, logger)
}
}
SiteLayout.STATIC -> configureStaticRouting(conf)
}

}

private suspend fun PipelineContext<Unit, ApplicationCall>.handleApiCall(
Expand Down Expand Up @@ -267,4 +283,46 @@ private fun Application.configureProdRouting(conf: KobwebConf, logger: Logger) {

configureCatchAllRouting(script, fallbackIndex)
}
}

/**
* Run a Kobweb server as a dumb, static server.
*
* This is kind of a waste of a Kobweb server, since it has all the smarts removed, but at the same time, it's supported
* so a user can test-run the static site experience which will ultimately be provided by some external provider.
*/
private fun Application.configureStaticRouting(conf: KobwebConf) {
val siteRoot = Path(conf.server.files.prod.siteRoot)
routing {
siteRoot.toFile().let { siteRootFile ->
siteRootFile.walkBottomUp().filter { it.isFile }.forEach { file ->
val relativeFile = file.relativeTo(siteRootFile)
val name = relativeFile.name.removeSuffix(".html")
val parent = relativeFile.parent?.let { "$it/" } ?: ""
if (name != "index") {
get("/$parent$name") {
call.respondFile(file)
}
} else {
get("/$parent") {
call.respondFile(file)
}
}
}

// Anything not found is an error
val errorFile = siteRootFile.resolve("404.html")
if (errorFile.exists()) {
// Catch URLs of the form a/b/c/
get("{...}/") {
call.respondFile(errorFile)
}

// Catch URLs of the form a/b/c/slug
get("{...}") {
call.respondFile(errorFile)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.varabyte.kobweb.cli.common

import com.varabyte.kobweb.common.error.KobwebException
import com.varabyte.kobweb.project.KobwebProject
import com.varabyte.kobweb.server.api.SiteLayout
import com.varabyte.kobweb.server.api.ServerEnvironment
import com.varabyte.kotter.runtime.Session

Expand Down Expand Up @@ -34,8 +35,8 @@ class KobwebGradle(private val env: ServerEnvironment) {
return Runtime.getRuntime().gradlew(*finalArgs.toTypedArray())
}

fun startServer(enableLiveReloading: Boolean = (env == ServerEnvironment.DEV)): Process {
val args = mutableListOf("-PkobwebEnv=$env", "kobwebStart")
fun startServer(enableLiveReloading: Boolean, siteLayout: SiteLayout): Process {
val args = mutableListOf("-PkobwebEnv=$env", "-PkobwebRunLayout=$siteLayout", "kobwebStart")
if (enableLiveReloading) {
args.add("-t")
}
Expand All @@ -46,7 +47,9 @@ class KobwebGradle(private val env: ServerEnvironment) {
return gradlew("kobwebStop")
}

fun export(): Process {
return gradlew("-PkobwebReuseServer=false", "-PkobwebBuildTarget=RELEASE", "kobwebExport")
fun export(siteLayout: SiteLayout): Process {
// Even if we are exporting a non-Kobweb layout, we still want to start up a dev server using a Kobweb layout so
// it looks for the source files in the right place.
return gradlew("-PkobwebReuseServer=false", "-PkobwebEnv=DEV", "-PkobwebRunLayout=KOBWEB", "-PkobwebBuildTarget=RELEASE", "-PkobwebExportLayout=$siteLayout", "kobwebExport")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.varabyte.kobweb.cli.common.findKobwebProject
import com.varabyte.kobweb.cli.common.assertKobwebProject
import com.varabyte.kobweb.cli.common.handleConsoleOutput
import com.varabyte.kobweb.cli.common.newline
import com.varabyte.kobweb.server.api.SiteLayout
import com.varabyte.kobweb.server.api.ServerEnvironment
import com.varabyte.kotter.foundation.anim.textAnimOf
import com.varabyte.kotter.foundation.input.Keys
Expand All @@ -28,7 +29,7 @@ private enum class ExportState {
}

@Suppress("BlockingMethodInNonBlockingContext")
fun handleExport(isInteractive: Boolean) {
fun handleExport(siteLayout: SiteLayout, isInteractive: Boolean) {
val kobwebGradle = KobwebGradle(ServerEnvironment.PROD) // exporting is a production-only action

if (isInteractive) session {
Expand Down Expand Up @@ -57,7 +58,7 @@ fun handleExport(isInteractive: Boolean) {
}
}.run {
val exportProcess = try {
kobwebGradle.export()
kobwebGradle.export(siteLayout)
}
catch (ex: Exception) {
exception = ex
Expand Down Expand Up @@ -100,7 +101,7 @@ fun handleExport(isInteractive: Boolean) {
} else {
assert(!isInteractive)
assertKobwebProject()
kobwebGradle.export().also { it.consumeProcessOutput(); it.waitFor() }
kobwebGradle.export(siteLayout).also { it.consumeProcessOutput(); it.waitFor() }
kobwebGradle.stopServer().also { it.consumeProcessOutput(); it.waitFor() }
}
}
11 changes: 8 additions & 3 deletions cli/kobweb/src/main/kotlin/com/varabyte/kobweb/cli/run/Run.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.varabyte.kobweb.cli.common.consumeProcessOutput
import com.varabyte.kobweb.cli.common.findKobwebProject
import com.varabyte.kobweb.cli.common.handleConsoleOutput
import com.varabyte.kobweb.cli.common.newline
import com.varabyte.kobweb.server.api.SiteLayout
import com.varabyte.kobweb.server.api.ServerEnvironment
import com.varabyte.kobweb.server.api.ServerRequest
import com.varabyte.kobweb.server.api.ServerRequestsFile
Expand Down Expand Up @@ -42,7 +43,11 @@ private enum class RunState {
INTERRUPTED,
}

fun handleRun(env: ServerEnvironment, isInteractive: Boolean) {
fun handleRun(
env: ServerEnvironment,
siteLayout: SiteLayout,
isInteractive: Boolean,
) {
val kobwebGradle = KobwebGradle(env)
if (isInteractive) session {
val kobwebFolder = findKobwebProject()?.kobwebFolder ?: return@session
Expand Down Expand Up @@ -107,7 +112,7 @@ fun handleRun(env: ServerEnvironment, isInteractive: Boolean) {
}
}.runUntilSignal {
val startServerProcess = try {
kobwebGradle.startServer()
kobwebGradle.startServer(enableLiveReloading = (env == ServerEnvironment.DEV), siteLayout)
}
catch (ex: Exception) {
exception = ex
Expand Down Expand Up @@ -198,6 +203,6 @@ fun handleRun(env: ServerEnvironment, isInteractive: Boolean) {
assertKobwebProject()
// If we're non-interactive, it means we just want to start the Kobweb server and exit without waiting for
// for any additional changes. (This is essentially used when run in a web server environment)
kobwebGradle.startServer(enableLiveReloading = false).also { it.consumeProcessOutput(); it.waitFor() }
kobwebGradle.startServer(enableLiveReloading = false, siteLayout).also { it.consumeProcessOutput(); it.waitFor() }
}
}
14 changes: 12 additions & 2 deletions cli/kobweb/src/main/kotlin/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.varabyte.kobweb.cli.list.handleList
import com.varabyte.kobweb.cli.run.handleRun
import com.varabyte.kobweb.cli.stop.handleStop
import com.varabyte.kobweb.cli.version.handleVersion
import com.varabyte.kobweb.server.api.SiteLayout
import com.varabyte.kobweb.server.api.ServerEnvironment
import kotlinx.cli.ArgParser
import kotlinx.cli.ArgType
Expand All @@ -29,6 +30,13 @@ private fun ArgParser.mode() = option(
description = "If interactive, runs in an ANSI-enabled terminal expecting user input. If dumb, command only outputs, using simple console logging",
).default(Mode.INTERACTIVE)

private fun ArgParser.layout() = option(
ArgType.Choice<SiteLayout>(),
fullName = "layout",
shortName = "l",
description = "Specify the organizational layout of the site files.",
).default(SiteLayout.KOBWEB)

private const val VERSION_HELP = "Print the version of this binary"

@ExperimentalCli
Expand Down Expand Up @@ -66,18 +74,20 @@ fun main(args: Array<String>) {

class Export : Subcommand("export", "Generate a static version of a Kobweb app / site") {
val mode by mode()
val layout by layout()

override fun execute() {
handleExport(mode == Mode.INTERACTIVE)
handleExport(layout, mode == Mode.INTERACTIVE)
}
}

class Run : Subcommand("run", "Run a Kobweb server") {
val env by option(ArgType.Choice<ServerEnvironment>(), "env").default(ServerEnvironment.DEV)
val mode by mode()
val layout by layout()

override fun execute() {
handleRun(env, mode == Mode.INTERACTIVE)
handleRun(env, layout, mode == Mode.INTERACTIVE)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.varabyte.kobweb.server.api

import com.varabyte.kobweb.common.error.KobwebException
import com.varabyte.kobweb.project.conf.KobwebConf

private val PROPERTY_SITE_LAYOUT = "kobweb.site.layout"

enum class SiteLayout {
/**
* Files live in multiple roots as specified in the [KobwebConf] file.
*
* Furthermore, the files searched for are different in developement and production versions.
*
* With this format, your pages, resources, scripts, and server jar are served from all potentially different
* locations, with a fallback `index.html` that can be used for unknown URLs.
*
* This format is guaranteed to support all Kobweb features, although it will require you hosting your own server
* which might be more expensive than a static layout.
*/
KOBWEB,

/**
* Files live in single root folder, from which they are served directly without any complex routing logic.
*
* This is ideal for serving simple static blog sites. External hosting providers should be able to handle this
* layout.
*
* Kobweb server features and dynamic routing are not supported with this format. However, a huge advantage for this
* format is it can often be served fast and cheap.
*/
STATIC;

companion object {
fun get(): SiteLayout {
val envValue: String = System.getProperty(PROPERTY_SITE_LAYOUT) ?: KOBWEB.name
return SiteLayout.values().firstOrNull { layout -> layout.name == envValue }
?: throw KobwebException("Invalid export layout: $envValue, expected one of [${ServerEnvironment.values().joinToString()}]")
}
}

fun toSystemPropertyParam(): String = "-D${PROPERTY_SITE_LAYOUT}=${this.name}"
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.varabyte.kobweb.gradle.application.tasks.KobwebGenerateSiteTask
import com.varabyte.kobweb.gradle.application.tasks.KobwebStartTask
import com.varabyte.kobweb.gradle.application.tasks.KobwebStopTask
import com.varabyte.kobweb.project.KobwebFolder
import com.varabyte.kobweb.server.api.SiteLayout
import com.varabyte.kobweb.server.api.ServerEnvironment
import com.varabyte.kobweb.server.api.ServerRequest
import com.varabyte.kobweb.server.api.ServerRequestsFile
Expand All @@ -38,6 +39,10 @@ class KobwebApplicationPlugin : Plugin<Project> {

val env =
project.findProperty("kobwebEnv")?.let { ServerEnvironment.valueOf(it.toString()) } ?: ServerEnvironment.DEV
val runLayout =
project.findProperty("kobwebRunLayout")?.let { SiteLayout.valueOf(it.toString()) } ?: SiteLayout.KOBWEB
val exportLayout =
project.findProperty("kobwebExportLayout")?.let { SiteLayout.valueOf(it.toString()) } ?: SiteLayout.KOBWEB
val buildTarget = project.findProperty("kobwebBuildTarget")?.let { BuildTarget.valueOf(it.toString()) }
?: if (env == ServerEnvironment.DEV) BuildTarget.DEBUG else BuildTarget.RELEASE

Expand All @@ -53,10 +58,11 @@ class KobwebApplicationPlugin : Plugin<Project> {

val kobwebStartTask = run {
val reuseServer = project.findProperty("kobwebReuseServer")?.let { it.toString().toBoolean() } ?: true
project.tasks.register("kobwebStart", KobwebStartTask::class.java, env, reuseServer)
project.tasks.register("kobwebStart", KobwebStartTask::class.java, env, runLayout, reuseServer)
}
project.tasks.register("kobwebStop", KobwebStopTask::class.java)
val kobwebExportTask = project.tasks.register("kobwebExport", KobwebExportTask::class.java, kobwebConfig)
val kobwebExportTask =
project.tasks.register("kobwebExport", KobwebExportTask::class.java, kobwebConfig, env, exportLayout)

project.afterEvaluate {
project.tasks.named("clean") {
Expand Down
Loading

0 comments on commit 703ee41

Please sign in to comment.