This guide shows how to implement a Next.js-style file-based routing system using the standalone Summon implementation. While you won't get automatic build-time discovery, you can manually organize your pages by file structure and create a simple convention-based router.
Organize your page files following this suggested structure:
| File Path | URL Route | Notes | |----------------------------|--------------------|-------------------------------| | /pages/IndexPage.kt | / | Home/index route | | /pages/HomePage.kt | / | Alternative home route | | /pages/AboutPage.kt | /about | Standard route | | /pages/users/ProfilePage.kt | /users/profile | Nested route | | /pages/blog/PostPage.kt | /blog/post/{id} | Dynamic parameter route | | /pages/NotFoundPage.kt | * | 404 Not Found page |
Each page file contains a composable function using the standalone implementation:
// File: src/commonMain/kotlin/pages/AboutPage.kt
// Include the standalone Summon implementation (from quickstart.md)
@Composable
fun AboutPage(): String {
return Column(
modifier = Modifier().padding(20.px).gap(16.px)
) {
Text("About Page", modifier = Modifier().fontSize(24.px).fontWeight(FontWeight.Bold)) +
Text("Learn more about our application.") +
Text("This page demonstrates file-based routing patterns.") +
NavigationLinks()
}
}
@Composable
fun NavigationLinks(): String {
return Row(modifier = Modifier().gap(16.px).marginTop(20.px)) {
Button(
text = "Home",
modifier = Modifier()
.backgroundColor("#0077cc")
.color("white")
.padding(8.px, 16.px)
.borderRadius(4.px)
.onClick("navigateTo('home')")
) + Button(
text = "Profile",
modifier = Modifier()
.backgroundColor("#28a745")
.color("white")
.padding(8.px, 16.px)
.borderRadius(4.px)
.onClick("navigateTo('users/profile')")
)
}
}
Here are complete examples of different page types:
// File: src/commonMain/kotlin/pages/IndexPage.kt
@Composable
fun IndexPage(): String {
return Column(
modifier = Modifier().padding(20.px).gap(16.px)
) {
Text("Welcome Home", modifier = Modifier().fontSize(28.px).fontWeight(FontWeight.Bold)) +
Text("This is the home page of our file-based routing example.") +
Row(modifier = Modifier().gap(12.px).marginTop(20.px)) {
Button(
text = "About Us",
modifier = Modifier()
.backgroundColor("#0077cc")
.color("white")
.padding(10.px, 20.px)
.borderRadius(4.px)
.onClick("navigateTo('about')")
) + Button(
text = "User Profiles",
modifier = Modifier()
.backgroundColor("#28a745")
.color("white")
.padding(10.px, 20.px)
.borderRadius(4.px)
.onClick("navigateTo('users/profile')")
) + Button(
text = "Blog",
modifier = Modifier()
.backgroundColor("#6c757d")
.color("white")
.padding(10.px, 20.px)
.borderRadius(4.px)
.onClick("navigateTo('blog/post/my-first-post')")
)
}
}
}
// File: src/commonMain/kotlin/pages/users/ProfilePage.kt
@Composable
fun ProfilePage(userId: String? = null): String {
return Column(
modifier = Modifier().padding(20.px).gap(16.px)
) {
Text("User Profile", modifier = Modifier().fontSize(24.px).fontWeight(FontWeight.Bold)) +
Text("User ID: ${userId ?: "Not specified"}") +
Text("This page demonstrates dynamic routing with parameters.") +
if (userId != null) {
UserDetails(userId)
} else {
Text("No user ID provided in the route.")
} +
NavigationLinks()
}
}
@Composable
fun UserDetails(userId: String): String {
return Column(
modifier = Modifier()
.backgroundColor("#f8f9fa")
.padding(16.px)
.borderRadius(8.px)
.marginTop(16.px)
) {
Text("User Details", modifier = Modifier().fontWeight(FontWeight.Bold)) +
Text("Name: User $userId") +
Text("Email: user$userId@example.com") +
Text("Member since: 2024")
}
}
// File: src/commonMain/kotlin/pages/blog/PostPage.kt
@Composable
fun PostPage(postId: String? = null): String {
return Column(
modifier = Modifier().padding(20.px).gap(16.px)
) {
Text("Blog Post", modifier = Modifier().fontSize(24.px).fontWeight(FontWeight.Bold)) +
Text("Post ID: ${postId ?: "Not specified"}") +
if (postId != null) {
BlogPostContent(postId)
} else {
Text("No post ID provided in the route.")
} +
NavigationLinks()
}
}
@Composable
fun BlogPostContent(postId: String): String {
return Column(
modifier = Modifier()
.backgroundColor("#ffffff")
.border(1.px, BorderStyle.Solid, "#e0e0e0")
.padding(20.px)
.borderRadius(8.px)
.marginTop(16.px)
) {
Text("$postId", modifier = Modifier().fontSize(20.px).fontWeight(FontWeight.Bold)) +
Text("Published on: January 15, 2024", modifier = Modifier().color("#666").fontSize(14.px)) +
Text("This is the content of the blog post. In a real application, you would fetch this content from a database or API based on the post ID.") +
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
}
}
// File: src/commonMain/kotlin/pages/NotFoundPage.kt
@Composable
fun NotFoundPage(): String {
return Column(
modifier = Modifier()
.padding(20.px)
.gap(16.px)
.textAlign(TextAlign.Center)
) {
Text("404", modifier = Modifier().fontSize(48.px).fontWeight(FontWeight.Bold).color("#dc3545")) +
Text("Page Not Found", modifier = Modifier().fontSize(24.px).fontWeight(FontWeight.Bold)) +
Text("Sorry, the page you are looking for does not exist.") +
Button(
text = "Back to Home",
modifier = Modifier()
.backgroundColor("#0077cc")
.color("white")
.padding(12.px, 24.px)
.borderRadius(4.px)
.cursor(Cursor.Pointer)
.onClick("navigateTo('home')")
)
}
}
Since we're using a standalone implementation, you manually register routes based on your file organization:
// File: src/commonMain/kotlin/router/FileBasedRouter.kt
// Include the standalone Summon implementation (from quickstart.md)
// Route registry that maps file structure to routes
object FileBasedRouter {
// Map routes to page functions following file-based conventions
val routes = mapOf(
"home" to ::IndexPage,
"about" to ::AboutPage,
"users/profile" to { ProfilePage("default-user") },
"blog/post" to { PostPage("sample-post") },
"404" to ::NotFoundPage
)
// Handle dynamic routes with parameters
fun getPageForRoute(route: String): String {
return when {
route == "home" || route == "" -> IndexPage()
route == "about" -> AboutPage()
route.startsWith("users/profile") -> {
val userId = extractUserIdFromRoute(route)
ProfilePage(userId)
}
route.startsWith("blog/post") -> {
val postId = extractPostIdFromRoute(route)
PostPage(postId)
}
routes.containsKey(route) -> routes[route]!!()
else -> NotFoundPage()
}
}
// Helper functions to extract parameters from routes
private fun extractUserIdFromRoute(route: String): String? {
// Extract user ID from routes like "users/profile/123"
val parts = route.split("/")
return if (parts.size > 2) parts[2] else null
}
private fun extractPostIdFromRoute(route: String): String? {
// Extract post ID from routes like "blog/post/my-first-post"
val parts = route.split("/")
return if (parts.size > 2) parts[2] else null
}
}
// Enhanced router with file-based mapping
data class FileBasedRouterState(
var currentRoute: String = "home",
var params: Map<String, String> = emptyMap()
)
val fileBasedRouter = FileBasedRouterState()
@Composable
fun FileBasedApp(): String {
return Div(
modifier = Modifier()
.style("min-height", "100vh")
.backgroundColor("#f8f9fa")
) {
FileBasedRouter.getPageForRoute(fileBasedRouter.currentRoute)
}
}
Organize your project following this structure:
src/
├── commonMain/kotlin/
│ ├── SummonComponents.kt // Standalone Summon implementation
│ ├── router/
│ │ └── FileBasedRouter.kt // Router registration
│ └── pages/
│ ├── IndexPage.kt // Home page (route: /)
│ ├── AboutPage.kt // About page (route: /about)
│ ├── NotFoundPage.kt // 404 page
│ ├── users/
│ │ └── ProfilePage.kt // User profile (route: /users/profile/{id})
│ └── blog/
│ └── PostPage.kt // Blog post (route: /blog/post/{id})
├── jsMain/kotlin/
│ └── main.kt // Browser entry point
└── jvmMain/kotlin/
└── server.kt // Server entry point (optional)
For browser applications, set up the file-based router in your main function:
// File: src/jsMain/kotlin/main.kt
fun main() {
val root = kotlinx.browser.document.getElementById("root")
?: error("Root element not found")
// Add navigation JavaScript
addFileBasedNavigationJS()
// Initial render
updateFileBasedUI()
// Setup hash change listener for client-side routing
kotlinx.browser.window.addEventListener("hashchange", { updateFileBasedUI() })
}
fun addFileBasedNavigationJS() {
kotlinx.browser.document.head?.insertAdjacentHTML("beforeend", """
<script>
// File-based navigation function
function navigateTo(route) {
window.location.hash = route;
}
// Specific navigation functions for type safety
function goToHome() { navigateTo('home'); }
function goToAbout() { navigateTo('about'); }
function goToUserProfile(userId) { navigateTo('users/profile/' + userId); }
function goToBlogPost(postId) { navigateTo('blog/post/' + postId); }
// Browser history functions
function goBack() { window.history.back(); }
function goForward() { window.history.forward(); }
</script>
""")
}
fun updateFileBasedUI() {
val root = kotlinx.browser.document.getElementById("root")
?: error("Root element not found")
// Get current hash and parse route
val hash = kotlinx.browser.window.location.hash.removePrefix("#").ifEmpty { "home" }
fileBasedRouter.currentRoute = hash
// Re-render with file-based router
root.innerHTML = FileBasedApp()
}
For server-side applications, handle file-based routing on the server:
// File: src/jvmMain/kotlin/server.kt
fun handleFileBasedRequest(path: String): String {
// Convert URL path to route format
val route = convertPathToRoute(path)
// Set router state
fileBasedRouter.currentRoute = route
// Render page with HTML wrapper
return renderFileBasedPage { FileBasedApp() }
}
fun convertPathToRoute(path: String): String {
return when {
path == "/" -> "home"
path == "/about" -> "about"
path.startsWith("/users/profile") -> {
val userId = path.substringAfterLast("/")
if (userId != "profile") "users/profile/$userId" else "users/profile"
}
path.startsWith("/blog/post") -> {
val postId = path.substringAfterLast("/")
if (postId != "post") "blog/post/$postId" else "blog/post"
}
else -> "404"
}
}
fun renderFileBasedPage(pageContent: () -> String): String {
return """
<!DOCTYPE html>
<html>
<head>
<title>File-Based Routing App</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 0;
background-color: #f8f9fa;
}
</style>
</head>
<body>
${pageContent()}
</body>
</html>
""".trimIndent()
}
// Ktor integration example
fun Application.configureFileBasedRouting() {
routing {
get("/") { call.respondText(handleFileBasedRequest("/"), ContentType.Text.Html) }
get("/about") { call.respondText(handleFileBasedRequest("/about"), ContentType.Text.Html) }
get("/users/profile/{id?}") {
val id = call.parameters["id"]
val path = if (id != null) "/users/profile/$id" else "/users/profile"
call.respondText(handleFileBasedRequest(path), ContentType.Text.Html)
}
get("/blog/post/{id?}") {
val id = call.parameters["id"]
val path = if (id != null) "/blog/post/$id" else "/blog/post"
call.respondText(handleFileBasedRequest(path), ContentType.Text.Html)
}
get("/{...}") {
call.respondText(handleFileBasedRequest(call.request.uri), ContentType.Text.Html)
}
}
}
Here's how all the pieces fit together in a complete working application:
// File: src/jsMain/kotlin/FileBasedExample.kt
fun main() {
val root = kotlinx.browser.document.getElementById("root")
?: error("Root element not found")
// Setup file-based routing
addFileBasedNavigationJS()
updateFileBasedUI()
kotlinx.browser.window.addEventListener("hashchange", { updateFileBasedUI() })
// Add some sample data and navigation
kotlinx.browser.document.head?.insertAdjacentHTML("beforeend", """
<script>
// Sample navigation functions
function showUserProfile() {
navigateTo('users/profile/user123');
}
function showBlogPost() {
navigateTo('blog/post/getting-started-with-summon');
}
function showRandomUser() {
const userId = 'user' + Math.floor(Math.random() * 1000);
navigateTo('users/profile/' + userId);
}
</script>
""")
}
// Enhanced navigation component for file-based routing
@Composable
fun FileBasedNavigation(): String {
return Column(
modifier = Modifier()
.backgroundColor("#ffffff")
.padding(16.px)
.borderRadius(8.px)
.boxShadow("0 2px 4px rgba(0,0,0,0.1)")
.marginBottom(20.px)
) {
Text("Navigation", modifier = Modifier().fontSize(18.px).fontWeight(FontWeight.Bold).marginBottom(12.px)) +
Row(modifier = Modifier().gap(8.px).flexWrap(FlexWrap.Wrap)) {
Button(
text = "🏠 Home",
modifier = Modifier()
.backgroundColor("#0077cc")
.color("white")
.padding(8.px, 12.px)
.borderRadius(4.px)
.onClick("goToHome()")
) + Button(
text = "ℹ️ About",
modifier = Modifier()
.backgroundColor("#6c757d")
.color("white")
.padding(8.px, 12.px)
.borderRadius(4.px)
.onClick("goToAbout()")
) + Button(
text = "👤 Profile",
modifier = Modifier()
.backgroundColor("#28a745")
.color("white")
.padding(8.px, 12.px)
.borderRadius(4.px)
.onClick("showUserProfile()")
) + Button(
text = "📝 Blog",
modifier = Modifier()
.backgroundColor("#ffc107")
.color("black")
.padding(8.px, 12.px)
.borderRadius(4.px)
.onClick("showBlogPost()")
) + Button(
text = "🎲 Random User",
modifier = Modifier()
.backgroundColor("#17a2b8")
.color("white")
.padding(8.px, 12.px)
.borderRadius(4.px)
.onClick("showRandomUser()")
)
}
}
}
// Update all page components to include the new navigation
@Composable
fun EnhancedIndexPage(): String {
return Column(modifier = Modifier().padding(20.px)) {
FileBasedNavigation() +
Text("🏠 Welcome Home", modifier = Modifier().fontSize(28.px).fontWeight(FontWeight.Bold)) +
Text("This demonstrates file-based routing with the standalone Summon implementation.",
modifier = Modifier().marginTop(16.px)) +
Column(
modifier = Modifier()
.backgroundColor("#f8f9fa")
.padding(16.px)
.borderRadius(8.px)
.marginTop(20.px)
) {
Text("Current Route Information:", modifier = Modifier().fontWeight(FontWeight.Bold)) +
Text("Route: ${fileBasedRouter.currentRoute}") +
Text("URL: ${kotlinx.browser.window.location.href}")
}
}
}
Using the standalone file-based routing implementation provides:
✅ Convention Over Configuration: Organize pages by file structure with clear naming patterns ✅ Type Safety: All routes and page functions are fully type-checked by Kotlin compiler ✅ No Build Dependencies: Works immediately without complex build tools or plugins ✅ Clear Organization: File structure directly reflects your application's route structure ✅ Easy Maintenance: Adding new pages is as simple as creating new files and registering routes ✅ Cross-Platform: Same routing logic works on both JavaScript and JVM platforms ✅ Debugging Friendly: Simple, debuggable code without complex framework magic ✅ IDE Support: Full IDE navigation and refactoring support for all page components
As your application grows, you can enhance this pattern:
This approach gives you the benefits of file-based routing while maintaining full control and understanding of your routing system.