From 503d2d1ad097a31a0d8bfac9373d69dd932c3aa7 Mon Sep 17 00:00:00 2001 From: Henk Kalkwater Date: Thu, 25 Jun 2020 10:51:26 +0200 Subject: [PATCH] Improve page speeds * Defer loading of highlight css * Cache /static for up to 16 days --- source/app.d | 375 +++++++++++++++++++++++--------------------- views/parts/page.dt | 2 +- 2 files changed, 195 insertions(+), 182 deletions(-) diff --git a/source/app.d b/source/app.d index 46bcef6..16bb4a3 100644 --- a/source/app.d +++ b/source/app.d @@ -1,181 +1,194 @@ -import std.experimental.logger; -import std.range; -import std.string; -import std.stdio; -import std.typecons; - -import vibe.d; - -import article; -import page; -import project; -import watcher; - -/** - * Internal list of articles, pages and projects by slug. - */ -Article[string] articles; -Page[string] pages; -Project[string] projects; - -/** - * Default ordering and list with pointers to ordered articles. - * (Note: this is code which will actually be compiled and passed on!) - */ -immutable string articleSortPred = "a.firstPublished > b.firstPublished"; -Article*[] articleList; - -immutable string pageSortPred = "a.title < b.title"; -Page*[] pageList; - -immutable string projectSortPred = "a.title < b.title"; -Project*[] projectList; - -/** - * Output types for the content. - */ -enum OutputType { - HTML, - MARKDOWN -} - -const string MIME_MARKDOWN = "text/markdown"; - - - -/** - * Get's the document type for the given slug based on extension - * and returns the slug without extension and the document type. Also removes the extension from the - * slug. - */ -private OutputType getOutputType(ref string slug) { - if (slug.endsWith(".md")) { - slug = chomp(slug, ".md"); - return OutputType.MARKDOWN; - } else if (slug.endsWith(".html")){ - // If explicitly asking for HTML, we'll return HTML - slug = chomp(slug, ".html"); - return OutputType.HTML; - } else { - // If in the future, for any reason, we no longer use HTML - // this allows to us to keep the current urls with an option - // to change the output in the future. - return OutputType.HTML; - } -} - -/** - * Template method for fetching a single page for a subclass of page. - * Params: - * T = the data structure/class to be passed as template parameter. Must have a slug parameter. - * templ = The template to use when rendering. - * array = An associative array where the keys are slugs for parameters and the values the template parameter. - * req = The server request to consume. Assumes there is an slug parameter. - * res = The server response to write to. - * - */ -void getSingle(T, string templ)(ref T[string] array, HTTPServerRequest req, HTTPServerResponse res) { - string slug = req.params["slug"]; - OutputType outputType = getOutputType(slug); - - enforceHTTP(slug in array, HTTPStatus.notFound, "Page not found"); - T content = array[slug]; - res.headers["Cache-Control"] = "public"; - switch(outputType) with (OutputType) { - case MARKDOWN: - res.writeBody(content.contentSource, MIME_MARKDOWN); - break; - default: - case HTML: - res.render!(templ, content); - break; - } -} - -/** - * Generates response for /posts/:slug and /palen/:slug. - */ -void articleGetSingle(HTTPServerRequest req, HTTPServerResponse res) { - getSingle!(Article, "pages/article.dt")(articles, req, res); -} - -/** - * Generates response for /posts and /palen - */ -void articleGetOverview(HTTPServerRequest req, HTTPServerResponse res) { - res.headers["Cache-Control"] = "public"; - render!("pages/article-list.dt", articleList)(res); -} - -/** - * Generates response for /projects and /projecten - */ -void projectGetOverview(HTTPServerRequest req, HTTPServerResponse res) { - res.headers["Cache-Control"] = "public"; - render!("pages/project-list.dt", projectList)(res); -} - -/** - * Generate response for a page - */ -void pageGet(HTTPServerRequest req, HTTPServerResponse res) { - // If no slug is supplied, it will be adjusted to "index" - if (("slug" in req.params) is null) { - req.params.addField("slug", "index"); - } - getSingle!(Page, "pages/page.dt")(pages, req, res); -} - -/** - * Generate response for a project page - */ -void projectGet(HTTPServerRequest req, HTTPServerResponse res) { - res.headers["Cache-Control"] = "public"; - getSingle!(Project, "pages/project.dt")(projects, req, res); -} - -/** - * Generates response whenever an error occurs. - */ -@safe -void errorPage(HTTPServerRequest req, HTTPServerResponse res, HTTPServerErrorInfo error) { - render!("pages/error.dt", error)(res); -} - -void main() { - HTTPServerSettings settings = new HTTPServerSettings; - settings.bindAddresses = ["0.0.0.0"]; - settings.port = 3465; - settings.serverString = "zeg ik lekker niet"; - settings.errorPageHandler = toDelegate(&errorPage); - settings.keepAliveTimeout = dur!"seconds"(60); - debug { - settings.accessLogToConsole = true; - } - - URLRouter router = new URLRouter; - router.get("/posts/:slug", &articleGetSingle); - router.get("/palen/:slug", &articleGetSingle); - router.get("/posts/", &articleGetOverview); - router.get("/palen/", &articleGetOverview); - router.get("/projects/", &projectGetOverview); - router.get("/projects/:slug", &projectGet); - router.get("/projecten/:slug", &projectGet); - router.get("/static/*", serveStaticFiles("./public/")); - router.get("/:slug", &pageGet); - router.get("/", &pageGet); - - listenHTTP(settings, router); - - // Start indexing pages. - runTask({ - initPages!(Page, pageSortPred)(pages, pageList, "pages"); - }); - runTask({ - initPages!(Article, articleSortPred)(articles, articleList, "articles"); - }); - runTask({ - initPages!(Project, projectSortPred)(projects, projectList, "projects"); - }); - runApplication(); -} +import std.experimental.logger; +import std.range; +import std.string; +import std.stdio; +import std.typecons; + +import vibe.d; + +import article; +import page; +import project; +import watcher; + +/** + * Internal list of articles, pages and projects by slug. + */ +Article[string] articles; +Page[string] pages; +Project[string] projects; + +/** + * Default ordering and list with pointers to ordered articles. + * (Note: this is code which will actually be compiled and passed on!) + */ +immutable string articleSortPred = "a.firstPublished > b.firstPublished"; +Article*[] articleList; + +immutable string pageSortPred = "a.title < b.title"; +Page*[] pageList; + +immutable string projectSortPred = "a.title < b.title"; +Project*[] projectList; + +/** + * Output types for the content. + */ +enum OutputType { + HTML, + MARKDOWN +} + +immutable string MIME_MARKDOWN = "text/markdown"; + +immutable Duration CACHE_TIME = days(16); + +/** + * Get's the document type for the given slug based on extension + * and returns the slug without extension and the document type. Also removes the extension from the + * slug. + */ +private OutputType getOutputType(ref string slug) { + if (slug.endsWith(".md")) { + slug = chomp(slug, ".md"); + return OutputType.MARKDOWN; + } else if (slug.endsWith(".html")){ + // If explicitly asking for HTML, we'll return HTML + slug = chomp(slug, ".html"); + return OutputType.HTML; + } else { + // If in the future, for any reason, we no longer use HTML + // this allows to us to keep the current urls with an option + // to change the output in the future. + return OutputType.HTML; + } +} + +void addCachingHeader(bool publicCache = true)(ref HTTPServerResponse res) { + string header = ""; + static if (publicCache) { + header ~= "public"; + } else { + header ~= "private"; + } + header ~= ", max-age=" ~ to!string(CACHE_TIME.total!"seconds"); + res.headers["Cache-Control"] = header; +} + +/** + * Template method for fetching a single page for a subclass of page. + * Params: + * T = the data structure/class to be passed as template parameter. Must have a slug parameter. + * templ = The template to use when rendering. + * array = An associative array where the keys are slugs for parameters and the values the template parameter. + * req = The server request to consume. Assumes there is an slug parameter. + * res = The server response to write to. + * + */ +void getSingle(T, string templ)(ref T[string] array, HTTPServerRequest req, HTTPServerResponse res) { + string slug = req.params["slug"]; + OutputType outputType = getOutputType(slug); + + enforceHTTP(slug in array, HTTPStatus.notFound, "Page not found"); + T content = array[slug]; + switch(outputType) with (OutputType) { + case MARKDOWN: + res.writeBody(content.contentSource, MIME_MARKDOWN); + break; + default: + case HTML: + res.render!(templ, content); + break; + } +} + +/** + * Generates response for /posts/:slug and /palen/:slug. + */ +void articleGetSingle(HTTPServerRequest req, HTTPServerResponse res) { + getSingle!(Article, "pages/article.dt")(articles, req, res); +} + +/** + * Generates response for /posts and /palen + */ +void articleGetOverview(HTTPServerRequest req, HTTPServerResponse res) { + addCachingHeader(res); + render!("pages/article-list.dt", articleList)(res); +} + +/** + * Generates response for /projects and /projecten + */ +void projectGetOverview(HTTPServerRequest req, HTTPServerResponse res) { + addCachingHeader(res); + render!("pages/project-list.dt", projectList)(res); +} + +/** + * Generate response for a page + */ +void pageGet(HTTPServerRequest req, HTTPServerResponse res) { + addCachingHeader(res); + // If no slug is supplied, it will be adjusted to "index" + if (("slug" in req.params) is null) { + req.params.addField("slug", "index"); + } + getSingle!(Page, "pages/page.dt")(pages, req, res); +} + +/** + * Generate response for a project page + */ +void projectGet(HTTPServerRequest req, HTTPServerResponse res) { + res.headers["Cache-Control"] = "public"; + getSingle!(Project, "pages/project.dt")(projects, req, res); +} + +/** + * Generates response whenever an error occurs. + */ +@safe +void errorPage(HTTPServerRequest req, HTTPServerResponse res, HTTPServerErrorInfo error) { + render!("pages/error.dt", error)(res); +} + +void main() { + HTTPServerSettings settings = new HTTPServerSettings; + settings.bindAddresses = ["0.0.0.0"]; + settings.port = 3465; + settings.serverString = "zeg ik lekker niet"; + settings.errorPageHandler = toDelegate(&errorPage); + settings.keepAliveTimeout = dur!"seconds"(60); + debug { + settings.accessLogToConsole = true; + } + HTTPFileServerSettings fSettings = new HTTPFileServerSettings; + fSettings.maxAge = days(16); + + URLRouter router = new URLRouter; + router.get("/posts/:slug", &articleGetSingle); + router.get("/palen/:slug", &articleGetSingle); + router.get("/posts/", &articleGetOverview); + router.get("/palen/", &articleGetOverview); + router.get("/projects/", &projectGetOverview); + router.get("/projects/:slug", &projectGet); + router.get("/projecten/:slug", &projectGet); + router.get("/static/*", serveStaticFiles("./public/", fSettings)); + router.get("/:slug", &pageGet); + router.get("/", &pageGet); + + listenHTTP(settings, router); + + // Start indexing pages. + runTask({ + initPages!(Page, pageSortPred)(pages, pageList, "pages"); + }); + runTask({ + initPages!(Article, articleSortPred)(articles, articleList, "articles"); + }); + runTask({ + initPages!(Project, projectSortPred)(projects, projectList, "projects"); + }); + runApplication(); +} diff --git a/views/parts/page.dt b/views/parts/page.dt index 74eb6f3..3caa13f 100644 --- a/views/parts/page.dt +++ b/views/parts/page.dt @@ -4,7 +4,7 @@ html(prefix="og: http://ogp.me/ns#") head meta(name="viewport", content="width=device-width; initial-scale=1") link(rel="stylesheet", href="/static/style/base.css") - link(rel="stylesheet", href="/static/style/highlight.css") + link(rel="stylesheet", href="/static/style/highlight.css", defer) link(rel="alternate stylesheet", href="/static/style/old.css", title="1999") link(rel="shortcut icon", href="/static/img/logo.png") link(rel="apple-touch-icon", href="/static/img/logo.png")