diff --git a/backend/src/main/kotlin/Module.kt b/backend/src/main/kotlin/Module.kt index 959daee..f62fac2 100644 --- a/backend/src/main/kotlin/Module.kt +++ b/backend/src/main/kotlin/Module.kt @@ -3,18 +3,21 @@ package dev.triumphteam.backend import dev.triumphteam.backend.api.apiRoutes import dev.triumphteam.backend.api.auth.TriumphPrincipal import dev.triumphteam.backend.meilisearch.Meili +import dev.triumphteam.backend.website.pages.respondNotFound import dev.triumphteam.backend.website.websiteRoutes import dev.triumphteam.website.JsonSerializer import io.ktor.http.CacheControl import io.ktor.http.ContentType import io.ktor.http.HttpHeaders import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode import io.ktor.http.content.CachingOptions import io.ktor.serialization.kotlinx.json.json import io.ktor.server.application.Application import io.ktor.server.application.install import io.ktor.server.auth.Authentication import io.ktor.server.auth.bearer +import io.ktor.server.html.respondHtml import io.ktor.server.http.content.staticFiles import io.ktor.server.http.content.staticResources import io.ktor.server.plugins.cachingheaders.CachingHeaders @@ -24,7 +27,10 @@ import io.ktor.server.plugins.cors.routing.CORS import io.ktor.server.plugins.defaultheaders.DefaultHeaders import io.ktor.server.plugins.forwardedheaders.ForwardedHeaders import io.ktor.server.plugins.forwardedheaders.XForwardedHeaders +import io.ktor.server.plugins.statuspages.StatusPages import io.ktor.server.resources.Resources +import io.ktor.server.response.respondRedirect +import io.ktor.server.response.respondText import io.ktor.server.routing.routing /** Module of the application. */ @@ -37,6 +43,12 @@ public fun Application.module() { else -> propertyValue } + install(StatusPages) { + status(HttpStatusCode.NotFound) { call, status -> + call.respondRedirect("/404") + } + } + install(Resources) install(ContentNegotiation) { json(JsonSerializer.json) diff --git a/backend/src/main/kotlin/api/ProjectSetup.kt b/backend/src/main/kotlin/api/ProjectSetup.kt index c884d9d..e023a5a 100644 --- a/backend/src/main/kotlin/api/ProjectSetup.kt +++ b/backend/src/main/kotlin/api/ProjectSetup.kt @@ -68,6 +68,7 @@ public suspend fun setupRepository(meili: Meili, projects: File) { this.navigation = version.navigation this.stable = version.stable this.recommended = version.recommended + this.defaultPage = version.pages.find { it.default }?.id ?: error("Could not find default page.") } val versionFolder = DATA_FOLDER.resolve("core/${project.id}/${version.reference}").also { diff --git a/backend/src/main/kotlin/api/database/Entities.kt b/backend/src/main/kotlin/api/database/Entities.kt index acddf17..ea4b691 100644 --- a/backend/src/main/kotlin/api/database/Entities.kt +++ b/backend/src/main/kotlin/api/database/Entities.kt @@ -28,6 +28,7 @@ public object DocVersions : IntIdTable("docs_version") { public val navigation: Column = serializable("navigation") public val stable: Column = bool("stable") public val recommended: Column = bool("recommended") + public val defaultPage: Column = varchar("default_page", 255) init { uniqueIndex("ref_project_uq", reference, project) @@ -67,6 +68,7 @@ public class DocVersionEntity(id: EntityID) : IntEntity(id) { public var navigation: Navigation by DocVersions.navigation public var stable: Boolean by DocVersions.stable public var recommended: Boolean by DocVersions.recommended + public var defaultPage: String by DocVersions.defaultPage } public class PageEntity(id: EntityID) : IntEntity(id) { diff --git a/backend/src/main/kotlin/website/WebsiteRoutes.kt b/backend/src/main/kotlin/website/WebsiteRoutes.kt index 6988bed..255053d 100644 --- a/backend/src/main/kotlin/website/WebsiteRoutes.kt +++ b/backend/src/main/kotlin/website/WebsiteRoutes.kt @@ -3,12 +3,22 @@ package dev.triumphteam.backend.website import dev.triumphteam.backend.meilisearch.Meili import dev.triumphteam.backend.website.pages.docs.docsRoutes import dev.triumphteam.backend.website.pages.home.homeRoutes +import dev.triumphteam.backend.website.pages.respondNotFound +import io.ktor.server.application.call import io.ktor.server.application.plugin +import io.ktor.server.html.respondHtml import io.ktor.server.routing.Routing +import io.ktor.server.routing.get public fun Routing.websiteRoutes(developmentMode: Boolean) { val meili = plugin(Meili) homeRoutes(developmentMode) docsRoutes(meili, developmentMode) + + get("404") { + call.respondHtml { + respondNotFound(call.application.developmentMode) + } + } } diff --git a/backend/src/main/kotlin/website/pages/NotFound.kt b/backend/src/main/kotlin/website/pages/NotFound.kt new file mode 100644 index 0000000..c9c9bc2 --- /dev/null +++ b/backend/src/main/kotlin/website/pages/NotFound.kt @@ -0,0 +1,46 @@ +package dev.triumphteam.backend.website.pages + +import kotlinx.css.h1 +import kotlinx.html.HTML +import kotlinx.html.body +import kotlinx.html.classes +import kotlinx.html.div +import kotlinx.html.h1 +import kotlinx.html.meta +import kotlinx.html.title + +public fun HTML.respondNotFound(developmentMode: Boolean) { + setupHead(developmentMode) { + meta { + name = "og:type" + content = "article" + } + + meta { + name = "og:title" + content = "TriumphTeam" + } + + meta { + name = "og:image" + // TODO: Replace with final URL, sucks that it can't be relative + content = "https://new.triumphteam.dev/static/images/banner_not_found.png" + } + + title("TriumphTeam") + } + + body { + + classes = setOf( + "w-screen h-screen", + "bg-docs-bg", + "text-[20em] text-white/50", + "flex justify-center items-center", + ) + + h1 { + +"404" + } + } +} diff --git a/backend/src/main/kotlin/website/pages/docs/DocsPage.kt b/backend/src/main/kotlin/website/pages/docs/DocsPage.kt index f28ed73..ca6b14e 100644 --- a/backend/src/main/kotlin/website/pages/docs/DocsPage.kt +++ b/backend/src/main/kotlin/website/pages/docs/DocsPage.kt @@ -86,12 +86,12 @@ public fun Routing.docsRoutes(meili: Meili, developmentMode: Boolean) { val pages = currentVersion.pages val page = when { - paramPage != null -> pages[paramPage] ?: return@get call.respond(HttpStatusCode.NotFound) + !paramPage.isNullOrEmpty() -> pages[paramPage] ?: return@get call.respond(HttpStatusCode.NotFound) else -> { // If the page doesn't exist, redirect to 404 - val page = pages.values.firstOrNull() ?: return@get call.respond(HttpStatusCode.NotFound) + val page = pages[currentVersion.defaultPage] ?: return@get call.respond(HttpStatusCode.NotFound) // If exist redirect to default - return@get call.respondRedirect("${call.request.uri}/${page.id}") + return@get call.respondRedirect("${call.request.uri.removeSuffix("/")}/${page.id}") } } @@ -511,6 +511,7 @@ private fun getProject(project: String): ProjectData? { reference = entity.reference, navigation = entity.navigation, stable = entity.stable, + defaultPage = entity.defaultPage, pages = PageEntity.find { (Pages.project eq projectEntity.id) and (Pages.version eq entity.id) } .map { pageEntity -> ProjectPage( @@ -550,6 +551,7 @@ public data class Version( public val navigation: Navigation, public val stable: Boolean, public val pages: Map, + public val defaultPage: String, ) { public data class Data(public val reference: String, public val stable: Boolean) diff --git a/backend/src/main/resources/static/images/banner_not_found.jpg b/backend/src/main/resources/static/images/banner_not_found.jpg new file mode 100644 index 0000000..6e7a933 Binary files /dev/null and b/backend/src/main/resources/static/images/banner_not_found.jpg differ diff --git a/common/src/main/kotlin/project/SerialRepresentation.kt b/common/src/main/kotlin/project/SerialRepresentation.kt index 77b24a0..aa73377 100644 --- a/common/src/main/kotlin/project/SerialRepresentation.kt +++ b/common/src/main/kotlin/project/SerialRepresentation.kt @@ -41,6 +41,7 @@ public data class Page( public val content: String, public val path: String, public val description: Description, + public val default: Boolean, ) { @Serializable diff --git a/docs/src/main/kotlin/Application.kt b/docs/src/main/kotlin/Application.kt index e856748..dcd9c2a 100644 --- a/docs/src/main/kotlin/Application.kt +++ b/docs/src/main/kotlin/Application.kt @@ -9,7 +9,6 @@ import dev.triumphteam.website.docs.markdown.hint.HintExtension import dev.triumphteam.website.docs.markdown.summary.SummaryExtractor import dev.triumphteam.website.docs.markdown.tab.TabExtension import dev.triumphteam.website.docs.serialization.GroupConfig -import dev.triumphteam.website.docs.serialization.PageConfig import dev.triumphteam.website.docs.serialization.ProjectConfig import dev.triumphteam.website.docs.serialization.RepoSettings import dev.triumphteam.website.docs.serialization.VersionConfig @@ -208,9 +207,9 @@ private fun parseVersions(versionDirs: List, parentDir: File, repoSettings navigationCollector.collect(Navigation.Group(parsedGroupConfig.header, parsedGroupConfig.mapPages())) val filesMap = groupFiles.associateBy(File::nameWithoutExtension) - parsedGroupConfig.pages.map(PageConfig::link).forEach { link -> - val pageFile = requireNotNull(filesMap[link]) { - "Could not find file named '$link', make sure the file is created before adding it to the group config." + parsedGroupConfig.pages.forEach { page -> + val pageFile = requireNotNull(filesMap[page.link]) { + "Could not find file named '${page.link}', make sure the file is created before adding it to the group config." } if (pageFile.nameWithoutExtension.contains(" ")) { @@ -233,6 +232,7 @@ private fun parseVersions(versionDirs: List, parentDir: File, repoSettings group = parsedGroupConfig.header, summary = summaryExtractor.extract(parsedFile), ), + default = page.default, ) ) } @@ -243,7 +243,11 @@ private fun parseVersions(versionDirs: List, parentDir: File, repoSettings recommended = parsedVersionConfig.recommended, stable = parsedVersionConfig.stable, navigation = navigationCollector.collection(), - pages = pageCollector.collection(), + pages = pageCollector.collection().also { pages -> + require(pages.count { it.default } == 1) { + "Versions must have 1 and only 1 default page." + } + }, ) }.also { docVersions -> require(docVersions.count(DocVersion::recommended) == 1) { diff --git a/docs/src/main/kotlin/serialization/FileRepresentation.kt b/docs/src/main/kotlin/serialization/FileRepresentation.kt index ea19b15..2ba2447 100644 --- a/docs/src/main/kotlin/serialization/FileRepresentation.kt +++ b/docs/src/main/kotlin/serialization/FileRepresentation.kt @@ -30,4 +30,4 @@ public data class GroupConfig(public val header: String, public val pages: List< } @Serializable -public data class PageConfig(public val header: String, public val link: String) +public data class PageConfig(public val header: String, public val link: String, public val default: Boolean = false) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0ca64e7..26e771d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -48,6 +48,7 @@ ktor-server-caching-headers = { module = "io.ktor:ktor-server-caching-headers-jv ktor-server-html = { module = "io.ktor:ktor-server-html-builder", version.ref = "ktor" } ktor-server-css = { module = "org.jetbrains.kotlin-wrappers:kotlin-css", version.ref = "ktor-css" } ktor-server-default-headers = { module = "io.ktor:ktor-server-default-headers-jvm", version.ref = "ktor" } +ktor-server-status-pages = { module = "io.ktor:ktor-server-status-pages", version.ref = "ktor" } # Common ktor-resources = { module = "io.ktor:ktor-resources", version.ref = "ktor" } @@ -109,6 +110,7 @@ ktor-server = [ "ktor-server-caching-headers", "ktor-server-default-headers", "ktor-server-css", + "ktor-server-status-pages", ] ktor-client = [ "ktor-client-core",