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; } struct TranslateContext { import std.typetuple; alias languages = TypeTuple!("en_GB", "nl_NL"); mixin translationModule!"mijnblog"; static string determineLanguage(scope HTTPServerRequest req) { if ("lang" !in req.query) return req.determineLanguageByHeader(languages); // default behaviour using "Accept-Language" header return req.query.get("lang", "en_GB"); } } /** * Generates boilerplate code for a single response. * params: * arrayName = The name of the associative array to take the items from. * templateName = The name of the template to render. */ string singleResponseMixin(string arrayName, string templateName) { return `string slug = req.params["slug"]; OutputType outputType = getOutputType(slug); enforceHTTP(slug in ` ~ arrayName ~ `, HTTPStatus.notFound, "Page not found"); auto content = ` ~ arrayName ~ `[slug]; switch(outputType) with (OutputType) { case MARKDOWN: res.writeBody(content.contentSource, MIME_MARKDOWN); break; default: case HTML: render!("` ~ templateName ~ `", content); break; }`; } @translationContext!TranslateContext class MijnBlog { public: /** * Generates response for /posts/:slug and /palen/:slug. */ @path("/posts/:slug") void getArticleSingle(string _slug, HTTPServerRequest req, HTTPServerResponse res) { //getSingle!(Article, "pages/article.dt")(articles, req, res); mixin(singleResponseMixin("articles", "pages/article.dt")); } /** * Generates response for /posts and /palen */ @path("/posts/") void getArticleOverview(HTTPServerRequest req, HTTPServerResponse res) { addCachingHeader(res); render!("pages/article-list.dt", articleList); } /** * Generates response for /projects and /projecten */ @path("/projects/") void getProjectOverview(HTTPServerRequest req, HTTPServerResponse res) { addCachingHeader(res); render!("pages/project-list.dt", projectList); } /** * Generate response for a project page */ @path("/projects/:slug") void getProject(HTTPServerRequest req, HTTPServerResponse res) { res.headers["Cache-Control"] = "public"; //getSingle!(Project, "pages/project.dt")(projects, req, res); mixin(singleResponseMixin("projects", "pages/project.dt")); } /** * Generate response for a page */ @path("/:slug") void getPage(HTTPServerRequest req, HTTPServerResponse res) { addCachingHeader(res); //getSingle!(Page, "pages/page.dt")(pages, req, res); mixin(singleResponseMixin("pages", "pages/page.dt")); } @path("/") void getIndexPage(HTTPServerRequest req, HTTPServerResponse res) { addCachingHeader(res); // If no slug is supplied, it will be adjusted to "index" req.params.addField("slug", "index"); //getSingle!(Page, "pages/page.dt")(pages, req, res); mixin(singleResponseMixin("pages", "pages/page.dt")); } } /** * 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("/static/*", serveStaticFiles("./public/", fSettings)); router.registerWebInterface(new MijnBlog); 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(); }