2020-06-25 08:51:26 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-03-10 17:30:32 +00:00
|
|
|
|
|
|
|
struct TranslateContext {
|
2020-06-27 22:11:50 +00:00
|
|
|
import std.typetuple;
|
2021-03-10 17:30:32 +00:00
|
|
|
|
2020-06-27 22:11:50 +00:00
|
|
|
alias languages = TypeTuple!("en_GB", "nl_NL");
|
|
|
|
mixin translationModule!"mijnblog";
|
2021-03-10 17:30:32 +00:00
|
|
|
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");
|
|
|
|
}
|
2020-06-25 08:51:26 +00:00
|
|
|
}
|
|
|
|
|
2021-03-10 17:30:32 +00:00
|
|
|
/**
|
|
|
|
* 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
|
2020-06-27 22:11:50 +00:00
|
|
|
class MijnBlog {
|
2021-03-10 17:30:32 +00:00
|
|
|
|
|
|
|
public:
|
2020-06-27 22:11:50 +00:00
|
|
|
/**
|
|
|
|
* Generates response for /posts/:slug and /palen/:slug.
|
|
|
|
*/
|
|
|
|
@path("/posts/:slug")
|
|
|
|
void getArticleSingle(string _slug, HTTPServerRequest req, HTTPServerResponse res) {
|
2021-03-10 17:30:32 +00:00
|
|
|
//getSingle!(Article, "pages/article.dt")(articles, req, res);
|
|
|
|
mixin(singleResponseMixin("articles", "pages/article.dt"));
|
2020-06-27 22:11:50 +00:00
|
|
|
}
|
2020-06-25 08:51:26 +00:00
|
|
|
|
2020-06-27 22:11:50 +00:00
|
|
|
/**
|
|
|
|
* Generates response for /posts and /palen
|
|
|
|
*/
|
|
|
|
@path("/posts/")
|
|
|
|
void getArticleOverview(HTTPServerRequest req, HTTPServerResponse res) {
|
|
|
|
addCachingHeader(res);
|
2021-03-10 17:30:32 +00:00
|
|
|
render!("pages/article-list.dt", articleList);
|
2020-06-27 22:11:50 +00:00
|
|
|
}
|
2020-06-25 08:51:26 +00:00
|
|
|
|
2020-06-27 22:11:50 +00:00
|
|
|
/**
|
|
|
|
* Generates response for /projects and /projecten
|
|
|
|
*/
|
|
|
|
@path("/projects/")
|
|
|
|
void getProjectOverview(HTTPServerRequest req, HTTPServerResponse res) {
|
|
|
|
addCachingHeader(res);
|
2021-03-10 17:30:32 +00:00
|
|
|
render!("pages/project-list.dt", projectList);
|
2020-06-25 08:51:26 +00:00
|
|
|
}
|
2020-06-27 22:11:50 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate response for a project page
|
|
|
|
*/
|
|
|
|
@path("/projects/:slug")
|
|
|
|
void getProject(HTTPServerRequest req, HTTPServerResponse res) {
|
|
|
|
res.headers["Cache-Control"] = "public";
|
2021-03-10 17:30:32 +00:00
|
|
|
//getSingle!(Project, "pages/project.dt")(projects, req, res);
|
|
|
|
mixin(singleResponseMixin("projects", "pages/project.dt"));
|
2020-06-27 22:11:50 +00:00
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Generate response for a page
|
|
|
|
*/
|
|
|
|
@path("/:slug")
|
|
|
|
void getPage(HTTPServerRequest req, HTTPServerResponse res) {
|
|
|
|
addCachingHeader(res);
|
2021-03-10 17:30:32 +00:00
|
|
|
//getSingle!(Page, "pages/page.dt")(pages, req, res);
|
|
|
|
mixin(singleResponseMixin("pages", "pages/page.dt"));
|
2020-06-27 22:11:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@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");
|
2021-03-10 17:30:32 +00:00
|
|
|
//getSingle!(Page, "pages/page.dt")(pages, req, res);
|
|
|
|
mixin(singleResponseMixin("pages", "pages/page.dt"));
|
|
|
|
}
|
2020-06-25 08:51:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
2020-06-27 22:11:50 +00:00
|
|
|
router.get("/static/*", serveStaticFiles("./public/", fSettings));
|
|
|
|
router.registerWebInterface(new MijnBlog);
|
2020-06-25 08:51:26 +00:00
|
|
|
|
|
|
|
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();
|
|
|
|
}
|