diff --git a/pom.xml b/pom.xml
index 5db1ea3..bd68090 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,7 +9,7 @@
${java.version}
${java.version}
UTF-8
- be.simplenotes.c2c/C2cKt
+ be.simplenotes.c2c/Camp2CampKt
1.0.4
@@ -204,20 +204,6 @@
${main.class}
-
-
- com.github.ben-manes.caffeine:caffeine
-
- **
-
-
-
- org.jetbrains.kotlin:kotlin-reflect
-
- **
-
-
-
diff --git a/src/main/kotlin/C2c.kt b/src/main/kotlin/C2c.kt
deleted file mode 100644
index ede849f..0000000
--- a/src/main/kotlin/C2c.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-package be.simplenotes.c2c
-
-import be.simplenotes.c2c.api.Api
-import be.simplenotes.c2c.api.ApiUrls
-import be.simplenotes.c2c.pdf.PdfCreator
-import be.simplenotes.c2c.pdf.StreamFactory
-import be.simplenotes.c2c.routes.IndexRoute
-import be.simplenotes.c2c.routes.PdfRoute
-import be.simplenotes.c2c.routes.RouteRoute
-import be.simplenotes.c2c.routes.SearchRoute
-import com.mitchellbosecke.pebble.PebbleEngine
-import com.mitchellbosecke.pebble.loader.ClasspathLoader
-import org.apache.hc.client5.http.impl.DefaultRedirectStrategy
-import org.apache.hc.client5.http.impl.classic.HttpClientBuilder
-import org.http4k.client.ApacheClient
-import org.http4k.contract.ContractRoute
-import org.http4k.contract.contract
-import org.http4k.contract.openapi.ApiInfo
-import org.http4k.contract.openapi.v3.OpenApi3
-import org.http4k.core.Filter
-import org.http4k.core.Response
-import org.http4k.core.Status
-import org.http4k.core.then
-import org.http4k.filter.RequestFilters
-import org.http4k.filter.ServerFilters
-import org.http4k.format.Jackson
-import org.http4k.server.asServer
-import org.koin.core.context.startKoin
-import org.koin.core.qualifier.named
-import org.koin.dsl.bind
-import org.koin.dsl.module
-import org.slf4j.LoggerFactory
-import java.io.PrintWriter
-import java.io.StringWriter
-
-val module = module {
- single {
- ApacheClient(
- HttpClientBuilder
- .create()
- .setRedirectStrategy(DefaultRedirectStrategy())
- .build()
- )
- }
- single { ApiUrls(get()) }
- single { Jackson.mapper }
- single { Api(get(), get()) }
- single { PdfCreator(get()) }
- single { StreamFactory(get()) }
- single {
- PebbleEngine
- .Builder()
- .loader(ClasspathLoader().apply {
- prefix = "views/"
- suffix = ".twig"
- })
- .cacheActive(false)
- .build()
- }
-}
-
-val routes = module {
- single(named()) { SearchRoute(get()).searchRoute() } bind ContractRoute::class
- single(named()) { RouteRoute(get()).routeRoute() } bind ContractRoute::class
- single(named()) { PdfRoute(get(), get()).route() } bind ContractRoute::class
- single(named()) { IndexRoute(get()).route() } bind ContractRoute::class
-}
-
-fun main() {
- val koin = startKoin {
- modules(module, routes)
- }.koin
-
- val appRoutes = koin.getAll()
- val app = contract {
- renderer = OpenApi3(ApiInfo("Camp2Camp", "1.0-SNAPSHOT"), Jackson)
- descriptionPath = "/api/swagger.json"
- routes.all.addAll(appRoutes)
- }
-
- val logger = LoggerFactory.getLogger("Camp2Camp")
-
- val loggingFilter = RequestFilters.Tap {
- logger.info("${it.method} ${it.uri}")
- }
-
- val catchAll = Filter { next ->
- {
- try {
- next(it)
- } catch (e: Exception) {
- val sw = StringWriter()
- e.printStackTrace(PrintWriter(sw))
- logger.error(sw.toString())
- Response(Status.INTERNAL_SERVER_ERROR)
- }
- }
- }
-
- catchAll.then(ServerFilters.GZip()).then(loggingFilter).then(app).asServer(CustomApacheServer(4000)).start()
- logger.info("Listening on http://localhost:4000")
-}
diff --git a/src/main/kotlin/Camp2Camp.kt b/src/main/kotlin/Camp2Camp.kt
new file mode 100644
index 0000000..c744670
--- /dev/null
+++ b/src/main/kotlin/Camp2Camp.kt
@@ -0,0 +1,53 @@
+package be.simplenotes.c2c
+
+import be.simplenotes.c2c.filters.catchAllFilter
+import be.simplenotes.c2c.filters.loggingFilter
+import be.simplenotes.c2c.routes.RouteProvider
+import org.http4k.contract.contract
+import org.http4k.contract.openapi.ApiInfo
+import org.http4k.contract.openapi.v3.OpenApi3
+import org.http4k.core.then
+import org.http4k.filter.ServerFilters
+import org.http4k.format.Jackson
+import org.http4k.server.ServerConfig
+import org.http4k.server.asServer
+import org.koin.core.context.startKoin
+import org.slf4j.LoggerFactory
+import java.net.InetSocketAddress
+
+fun main() {
+ val koin = startKoin {
+ modules(
+ serverModule,
+ routes,
+ clientModule,
+ apiModule,
+ utilsModule,
+ pdfModule,
+ )
+ properties(mapOf(
+ "port" to System.getenv().getOrDefault("PORT", "4000"),
+ "host" to System.getenv().getOrDefault("HOST", "localhost"),
+ ))
+ }.koin
+
+ val appRoutes = koin.getAll().map(RouteProvider::route)
+ val app = contract {
+ renderer = OpenApi3(ApiInfo("Camp2Camp", "1.0-SNAPSHOT"), Jackson)
+ descriptionPath = "/api/swagger.json"
+ routes.all.addAll(appRoutes)
+ }
+
+ val logger = LoggerFactory.getLogger("Camp2Camp")
+ val server = koin.get()
+ val address = koin.get()
+
+ catchAllFilter(logger)
+ .then(ServerFilters.GZip())
+ .then(loggingFilter(logger))
+ .then(app)
+ .asServer(server)
+ .start()
+
+ logger.info("Listening on http://${address.hostName}:${address.port}")
+}
diff --git a/src/main/kotlin/Modules.kt b/src/main/kotlin/Modules.kt
new file mode 100644
index 0000000..0489ceb
--- /dev/null
+++ b/src/main/kotlin/Modules.kt
@@ -0,0 +1,53 @@
+package be.simplenotes.c2c
+
+import be.simplenotes.c2c.api.Api
+import be.simplenotes.c2c.api.ApiUrls
+import be.simplenotes.c2c.api.ImageDownloader
+import be.simplenotes.c2c.pdf.PdfCreator
+import be.simplenotes.c2c.routes.*
+import be.simplenotes.c2c.server.CustomApacheServer
+import be.simplenotes.c2c.templates.PebbleEngine
+import com.mitchellbosecke.pebble.PebbleEngine
+import org.apache.hc.client5.http.impl.DefaultRedirectStrategy
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder
+import org.http4k.client.ApacheClient
+import org.http4k.format.Jackson
+import org.http4k.server.ServerConfig
+import org.koin.core.qualifier.named
+import org.koin.dsl.bind
+import org.koin.dsl.module
+import java.net.InetSocketAddress
+
+val routes = module {
+ single { SearchRouteProvider(get()) } bind RouteProvider::class
+ single { RouteRouteProvider(get()) } bind RouteProvider::class
+ single { PdfRouteProvider(get(), get(), get()) } bind RouteProvider::class
+ single { IndexRouteProvider(get()) } bind RouteProvider::class
+}
+
+val serverModule = module {
+ single { InetSocketAddress(getProperty("host"), getProperty("port").toInt()) }
+ single { CustomApacheServer(get()) }
+}
+
+val clientModule = module {
+ single { ApacheClient(get()) }
+ single { HttpClientBuilder.create().setRedirectStrategy(DefaultRedirectStrategy()).build() }
+}
+
+val apiModule = module {
+ single { ApiUrls(get()) }
+ single { Api(get(), get()) }
+ single { ImageDownloader(get()) }
+}
+
+val utilsModule = module {
+ single { Jackson.mapper }
+ single { PebbleEngine(cache = false, prefix = "templates/") }
+}
+
+val pdfModule = module {
+ val pdf = named("pdf")
+ single { PdfCreator(get(pdf)) }
+ single(pdf) { get().getTemplate("pdf") }
+}
diff --git a/src/main/kotlin/api/ImageDownloader.kt b/src/main/kotlin/api/ImageDownloader.kt
new file mode 100644
index 0000000..74bd80c
--- /dev/null
+++ b/src/main/kotlin/api/ImageDownloader.kt
@@ -0,0 +1,24 @@
+package be.simplenotes.c2c.api
+
+import org.http4k.core.HttpHandler
+import org.http4k.core.Method
+import org.http4k.core.Request
+import org.slf4j.LoggerFactory
+import java.util.stream.Collectors
+
+class ImageDownloader(private val client: HttpHandler) {
+ private val logger = LoggerFactory.getLogger(javaClass)
+
+ fun download(images: List): Map =
+ images.map(Image::medium)
+ .parallelStream()
+ .collect(Collectors.toConcurrentMap(
+ { it },
+ {
+ val res = client(Request(Method.GET, it))
+ logger.info("GET ${res.status} $it")
+ if (res.status.successful) res.body.stream.readAllBytes()
+ else null
+ }
+ ))
+}
diff --git a/src/main/kotlin/filters/Filters.kt b/src/main/kotlin/filters/Filters.kt
new file mode 100644
index 0000000..6e59d5b
--- /dev/null
+++ b/src/main/kotlin/filters/Filters.kt
@@ -0,0 +1,26 @@
+package be.simplenotes.c2c.filters
+
+import org.http4k.core.Filter
+import org.http4k.core.Response
+import org.http4k.core.Status
+import org.http4k.filter.RequestFilters
+import org.slf4j.Logger
+import java.io.PrintWriter
+import java.io.StringWriter
+
+fun loggingFilter(logger: Logger) = RequestFilters.Tap {
+ logger.info("${it.method} ${it.uri}")
+}
+
+fun catchAllFilter(logger: Logger) = Filter { next ->
+ {
+ try {
+ next(it)
+ } catch (e: Exception) {
+ val sw = StringWriter()
+ e.printStackTrace(PrintWriter(sw))
+ logger.error(sw.toString())
+ Response(Status.INTERNAL_SERVER_ERROR)
+ }
+ }
+}
diff --git a/src/main/kotlin/pdf/PdfCreator.kt b/src/main/kotlin/pdf/PdfCreator.kt
index a92d260..1cd7417 100644
--- a/src/main/kotlin/pdf/PdfCreator.kt
+++ b/src/main/kotlin/pdf/PdfCreator.kt
@@ -2,59 +2,19 @@ package be.simplenotes.c2c.pdf
import be.simplenotes.c2c.api.Route
import be.simplenotes.c2c.templates.render
-import com.github.benmanes.caffeine.cache.Caffeine
-import com.mitchellbosecke.pebble.PebbleEngine
-import com.mitchellbosecke.pebble.loader.ClasspathLoader
-import com.openhtmltopdf.extend.FSStream
+import com.mitchellbosecke.pebble.template.PebbleTemplate
import com.openhtmltopdf.extend.FSStreamFactory
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder
import com.openhtmltopdf.util.XRLog
-import org.http4k.core.HttpHandler
-import org.http4k.core.Method
-import org.http4k.core.Request
import org.jsoup.Jsoup
import org.jsoup.helper.W3CDom
-import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
-import java.io.InputStream
-import java.util.concurrent.TimeUnit
-class StreamFactory(private val client: HttpHandler) : FSStreamFactory {
- private val logger = LoggerFactory.getLogger(javaClass)
+class PdfCreator(private val template: PebbleTemplate) {
- private val imgCache = Caffeine.newBuilder()
- .maximumSize(15)
- .expireAfterWrite(15, TimeUnit.MINUTES)
- .build()
-
- override fun getUrl(url: String) = object : FSStream {
- override fun getStream(): InputStream? {
- val bytes = imgCache.get(url) {
- logger.info("Downloading $url")
- val res = client(Request(Method.GET, url))
- if (res.status.successful) res.body.stream.readAllBytes()
- else null
- }
- return bytes?.inputStream()
- }
-
- override fun getReader() = null
- }
-}
-
-class PdfCreator(private val streamFactory: StreamFactory) {
- private val engine = PebbleEngine
- .Builder()
- .loader(ClasspathLoader().apply {
- prefix = "templates/"
- suffix = ".twig"
- })
- .cacheActive(true)
- .build()
-
- fun create(route: Route): ByteArrayInputStream {
- val html = engine.render("index", mapOf("route" to route))
+ fun create(route: Route, streamFactory: FSStreamFactory): ByteArrayInputStream {
+ val html = template.render(mapOf("route" to route))
XRLog.setLoggingEnabled(false)
diff --git a/src/main/kotlin/pdf/PreloadedStreamFactory.kt b/src/main/kotlin/pdf/PreloadedStreamFactory.kt
new file mode 100644
index 0000000..8ae0f50
--- /dev/null
+++ b/src/main/kotlin/pdf/PreloadedStreamFactory.kt
@@ -0,0 +1,12 @@
+package be.simplenotes.c2c.pdf
+
+import com.openhtmltopdf.extend.FSStream
+import com.openhtmltopdf.extend.FSStreamFactory
+import java.io.InputStream
+
+class PreloadedStreamFactory(private val images: Map) : FSStreamFactory {
+ override fun getUrl(url: String) = object : FSStream {
+ override fun getStream(): InputStream? = images[url]?.inputStream()
+ override fun getReader() = null
+ }
+}
diff --git a/src/main/kotlin/routes/IndexRoute.kt b/src/main/kotlin/routes/IndexRouteProvider.kt
similarity index 55%
rename from src/main/kotlin/routes/IndexRoute.kt
rename to src/main/kotlin/routes/IndexRouteProvider.kt
index 7944157..378a425 100644
--- a/src/main/kotlin/routes/IndexRoute.kt
+++ b/src/main/kotlin/routes/IndexRouteProvider.kt
@@ -4,20 +4,18 @@ import be.simplenotes.c2c.templates.render
import com.mitchellbosecke.pebble.PebbleEngine
import org.http4k.contract.ContractRoute
import org.http4k.contract.meta
-import org.http4k.core.HttpHandler
-import org.http4k.core.Method
-import org.http4k.core.Response
-import org.http4k.core.Status
+import org.http4k.core.*
-class IndexRoute(private val engine: PebbleEngine) {
+class IndexRouteProvider(private val engine: PebbleEngine) : RouteProvider {
- fun route(): ContractRoute {
+ override fun route(): ContractRoute {
val spec = "/" meta {
summary = "Redoc ui"
+ produces += ContentType.TEXT_HTML
} bindContract Method.GET
val route: HttpHandler = {
- Response(Status.OK).body(engine.render("index"))
+ Response(Status.OK).body(engine.render("index")).header("Content-Type", "text/html; charset=utf-8")
}
return spec to route
diff --git a/src/main/kotlin/routes/PdfRoute.kt b/src/main/kotlin/routes/PdfRoute.kt
deleted file mode 100644
index 418e260..0000000
--- a/src/main/kotlin/routes/PdfRoute.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package be.simplenotes.c2c.routes
-
-import be.simplenotes.c2c.api.Api
-import be.simplenotes.c2c.pdf.PdfCreator
-import org.http4k.contract.ContractRoute
-import org.http4k.contract.div
-import org.http4k.contract.meta
-import org.http4k.core.HttpHandler
-import org.http4k.core.Method
-import org.http4k.core.Response
-import org.http4k.core.Status
-import org.http4k.lens.Path
-
-// TODO: rename..
-class PdfRoute(private val api: Api, private val pdfCreator: PdfCreator) {
-
- fun route(): ContractRoute {
- val spec = "/route" / Path.of("id", "Route's ID") / "pdf" meta {
- summary = "Generate pdf from routes"
- } bindContract Method.GET
-
- fun route(id: String, pdf: String): HttpHandler = {
- when (val route = api.getRoute(id)) {
- null -> Response(Status.NOT_FOUND)
- else -> {
- val bytes = pdfCreator.create(route)
- Response(Status.OK)
- .body(bytes)
- .header("Content-Type", "application/pdf")
- .header("Content-Disposition", "attachment; filename=\"out.pdf\"")
- }
- }
- }
-
- return spec to ::route
- }
-
-}
diff --git a/src/main/kotlin/routes/PdfRouteProvider.kt b/src/main/kotlin/routes/PdfRouteProvider.kt
new file mode 100644
index 0000000..1a60773
--- /dev/null
+++ b/src/main/kotlin/routes/PdfRouteProvider.kt
@@ -0,0 +1,39 @@
+package be.simplenotes.c2c.routes
+
+import be.simplenotes.c2c.api.Api
+import be.simplenotes.c2c.api.ImageDownloader
+import be.simplenotes.c2c.pdf.PdfCreator
+import be.simplenotes.c2c.pdf.PreloadedStreamFactory
+import org.http4k.contract.ContractRoute
+import org.http4k.contract.div
+import org.http4k.contract.meta
+import org.http4k.core.*
+import org.http4k.lens.Path
+
+class PdfRouteProvider(
+ private val api: Api,
+ private val pdfCreator: PdfCreator,
+ private val imageDownloader: ImageDownloader,
+) : RouteProvider {
+
+ override fun route(): ContractRoute {
+ val spec = "/route" / Path.of("id", "Route's ID") / "pdf" meta {
+ summary = "Generate pdf from routes"
+ produces += ContentType("application/pdf")
+ } bindContract Method.GET
+
+ fun route(id: String, pdf: String): HttpHandler = {
+ api.getRoute(id)?.let {
+ val imageMap = imageDownloader.download(it.associations.images)
+
+ Response(Status.OK)
+ .body(pdfCreator.create(it, PreloadedStreamFactory(imageMap)))
+ .header("Content-Type", "application/pdf")
+ .header("Content-Disposition", "attachment; filename=\"out.pdf\"")
+ } ?: Response(Status.NOT_FOUND)
+ }
+
+ return spec to ::route
+ }
+
+}
diff --git a/src/main/kotlin/routes/RouteProvider.kt b/src/main/kotlin/routes/RouteProvider.kt
new file mode 100644
index 0000000..39cbc3a
--- /dev/null
+++ b/src/main/kotlin/routes/RouteProvider.kt
@@ -0,0 +1,7 @@
+package be.simplenotes.c2c.routes
+
+import org.http4k.contract.ContractRoute
+
+interface RouteProvider {
+ fun route(): ContractRoute
+}
diff --git a/src/main/kotlin/routes/RouteRoute.kt b/src/main/kotlin/routes/RouteRouteProvider.kt
similarity index 82%
rename from src/main/kotlin/routes/RouteRoute.kt
rename to src/main/kotlin/routes/RouteRouteProvider.kt
index 3350460..6f0b0f1 100644
--- a/src/main/kotlin/routes/RouteRoute.kt
+++ b/src/main/kotlin/routes/RouteRouteProvider.kt
@@ -10,13 +10,14 @@ import org.http4k.format.Jackson.auto
import org.http4k.lens.Path
// TODO: rename..
-class RouteRoute(private val api: Api) {
+class RouteRouteProvider(private val api: Api) : RouteProvider {
- fun routeRoute(): ContractRoute {
+ override fun route(): ContractRoute {
val responseLens = Body.auto("Route").toLens()
val spec = "/route" / Path.of("id", "Route's ID") meta {
summary = "Route details"
+ produces += ContentType.APPLICATION_JSON
} bindContract Method.GET
fun route(id: String): HttpHandler = {
diff --git a/src/main/kotlin/routes/SearchRoute.kt b/src/main/kotlin/routes/SearchRouteProvider.kt
similarity index 83%
rename from src/main/kotlin/routes/SearchRoute.kt
rename to src/main/kotlin/routes/SearchRouteProvider.kt
index 71b530a..1492dd1 100644
--- a/src/main/kotlin/routes/SearchRoute.kt
+++ b/src/main/kotlin/routes/SearchRouteProvider.kt
@@ -5,21 +5,22 @@ import be.simplenotes.c2c.api.Api
import be.simplenotes.c2c.api.RouteResult
import be.simplenotes.c2c.api.SearchResults
import org.http4k.contract.ContractRoute
-import org.http4k.contract.Tag
+import org.http4k.contract.RequestMeta
import org.http4k.contract.meta
import org.http4k.core.*
import org.http4k.format.Jackson.auto
import org.http4k.lens.Query
-class SearchRoute(private val api: Api) {
+class SearchRouteProvider(private val api: Api) : RouteProvider {
- fun searchRoute(): ContractRoute {
+ override fun route(): ContractRoute {
val query = Query.required("q", "Search terms")
val responseLens = Body.auto("Search results").toLens()
val spec = "/search" meta {
summary = "Search routes and waypoints"
queries += query
+ receiving(RequestMeta(Request(Method.GET, "/search").query("q", "tour de bavon")))
returning(Status.OK, responseLens to SearchResults(listOf(RouteResult(
id = "57103",
title = "Tour de Bavon : Thor",
diff --git a/src/main/kotlin/CustomApacheServer.kt b/src/main/kotlin/server/CustomApacheServer.kt
similarity index 76%
rename from src/main/kotlin/CustomApacheServer.kt
rename to src/main/kotlin/server/CustomApacheServer.kt
index 400c8b4..8b36370 100644
--- a/src/main/kotlin/CustomApacheServer.kt
+++ b/src/main/kotlin/server/CustomApacheServer.kt
@@ -1,4 +1,4 @@
-package be.simplenotes.c2c
+package be.simplenotes.c2c.server
import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap
import org.apache.hc.core5.http.io.SocketConfig
@@ -6,13 +6,16 @@ import org.http4k.core.HttpHandler
import org.http4k.server.Http4kRequestHandler
import org.http4k.server.Http4kServer
import org.http4k.server.ServerConfig
+import java.net.InetSocketAddress
+
+class CustomApacheServer(private val socketAddress: InetSocketAddress) : ServerConfig {
-class CustomApacheServer(val port: Int) : ServerConfig {
override fun toServer(httpHandler: HttpHandler): Http4kServer = object : Http4kServer {
val handler = Http4kRequestHandler(httpHandler)
val server = ServerBootstrap.bootstrap()
- .setListenerPort(port)
+ .setListenerPort(socketAddress.port)
+ .setLocalAddress(socketAddress.address)
.setSocketConfig(SocketConfig.custom()
.setTcpNoDelay(true)
.setSoKeepAlive(true)
@@ -28,6 +31,6 @@ class CustomApacheServer(val port: Int) : ServerConfig {
override fun stop() = apply { server.stop() }
- override fun port(): Int = port
+ override fun port(): Int = socketAddress.port
}
}
diff --git a/src/main/kotlin/templates/Templates.kt b/src/main/kotlin/templates/Templates.kt
index d8edcd8..a2cf8af 100644
--- a/src/main/kotlin/templates/Templates.kt
+++ b/src/main/kotlin/templates/Templates.kt
@@ -1,11 +1,21 @@
package be.simplenotes.c2c.templates
import com.mitchellbosecke.pebble.PebbleEngine
+import com.mitchellbosecke.pebble.loader.ClasspathLoader
+import com.mitchellbosecke.pebble.template.PebbleTemplate
import java.io.StringWriter
-fun PebbleEngine.render(name: String, args: Map = mapOf()): String {
- val template = getTemplate(name)
+fun PebbleEngine.render(name: String, args: Map = mapOf()) = getTemplate(name).render(args)
+
+fun PebbleTemplate.render(args: Map = mapOf()): String {
val writer = StringWriter()
- template.evaluate(writer, args)
+ evaluate(writer, args)
return writer.toString()
}
+
+@Suppress("FunctionName")
+fun PebbleEngine(cache: Boolean, prefix: String? = null, suffix: String? = ".twig"): PebbleEngine =
+ PebbleEngine.Builder().loader(ClasspathLoader().also { loader ->
+ prefix?.let { loader.prefix = it }
+ suffix?.let { loader.suffix = it }
+ }).cacheActive(cache).build()
diff --git a/src/main/resources/templates/index.twig b/src/main/resources/templates/index.twig
index 88bf5a0..c10fd97 100644
--- a/src/main/resources/templates/index.twig
+++ b/src/main/resources/templates/index.twig
@@ -1,10 +1,19 @@
-{{ route.title }}
-{{ route.summary }}
-{{ route.description }}
-{% for img in route.associations.images %}
-
-
- {{ img.title }}
-
-{% endfor %}
+
+
+
+ ReDoc
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/templates/pdf.twig b/src/main/resources/templates/pdf.twig
new file mode 100644
index 0000000..88bf5a0
--- /dev/null
+++ b/src/main/resources/templates/pdf.twig
@@ -0,0 +1,10 @@
+{{ route.title }}
+{{ route.summary }}
+{{ route.description }}
+{% for img in route.associations.images %}
+
+
+ {{ img.title }}
+
+{% endfor %}
diff --git a/src/main/resources/views/index.twig b/src/main/resources/views/index.twig
deleted file mode 100644
index c10fd97..0000000
--- a/src/main/resources/views/index.twig
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
- ReDoc
-
-
-
-
-
-
-
-
-
-