diff --git a/.gitignore b/.gitignore index 9d0f300..56fa0a7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,3 @@ mijnblog-test-* /articles/ /pages/ /projects/ -/result diff --git a/defaultClasses.lua b/defaultClasses.lua index 2e5dba6..b1ddd6f 100644 --- a/defaultClasses.lua +++ b/defaultClasses.lua @@ -14,14 +14,6 @@ function get_default_code_class(meta) end end -function make_image_url_absolute (img) - if img.src:sub(1,1) == '/' then - img.src = os.getenv 'WEBROOT' .. img.src - end - return img -end - return {{Meta = get_default_code_class}, {Code = add_default_code_class}, - {CodeBlock = add_default_code_class}, - {Image = make_image_url_absolute}} + {CodeBlock = add_default_code_class}} diff --git a/dub.json b/dub.json index 073325b..0ff17ad 100644 --- a/dub.json +++ b/dub.json @@ -5,12 +5,16 @@ "copyright": "Copyright © 2019, Chris Josten", "dependencies": { "dyaml": "~>0.8.0", + "htmld": "~>0.3.7", "vibe-d": "~>0.9.0" }, "description": "A blog based on Markdown and JSON", "license": "AGPLv3", + "mainSourceFile": "source/nl/netsoj/chris/blog/main.d", "name": "mijnblog", - "targetType": "executable", - "stringImportPaths": ["views", "translations"], - "versions": ["DeimosOpenSSL_3_0"] -} + "stringImportPaths": [ + "views", + "translations" + ], + "targetType": "executable" +} \ No newline at end of file diff --git a/dub.selections.json b/dub.selections.json index af8b127..d511768 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -3,21 +3,21 @@ "versions": { "botan": "1.12.19", "botan-math": "1.0.3", - "diet-ng": "1.8.1", - "dyaml": "0.8.6", - "eventcore": "0.9.26", + "diet-ng": "1.7.5", + "dyaml": "0.8.3", + "eventcore": "0.9.13", "fswatch": "0.5.0", + "htmld": "0.3.7", "libasync": "0.8.6", + "libdominator": "1.1.7", "libevent": "2.0.2+2.0.16", - "memutils": "1.0.9", + "memutils": "1.0.4", "mir-linux-kernel": "1.0.1", - "openssl": "3.3.3", - "openssl-static": "1.0.2+3.0.8", + "openssl": "1.1.6+1.0.1g", "stdx-allocator": "2.77.5", - "taggedalgebraic": "0.11.22", + "taggedalgebraic": "0.11.19", "tinyendian": "0.2.0", - "vibe-container": "1.0.1", - "vibe-core": "2.5.1", - "vibe-d": "0.9.7" + "vibe-core": "1.13.0", + "vibe-d": "0.9.3" } } diff --git a/dub.selections.nix b/dub.selections.nix index b4e37e0..7ec6ca6 100644 --- a/dub.selections.nix +++ b/dub.selections.nix @@ -51,61 +51,49 @@ fetch = { type = "git"; url = "https://github.com/etcimon/memutils.git"; - rev = "v1.0.9"; - sha256 = "08j9grn3l6gr275m392l0dfsx5ma6hs08gjfhh48hpy3v0bvb0ny"; + rev = "v1.0.4"; + sha256 = "1m65iy03yl5km61ijk5ysapxc5mks9b15hmaqxpn111981qrqx9z"; fetchSubmodules = false; - date = "2023-03-02T15:19:28-05:00"; + date = "2020-02-02T20:53:12-05:00"; deepClone = false; leaveDotGit = false; - path = "/nix/store/glnf0ljd84mv6gzc9wgs1qq0vqqxvmzh-memutils"; + path = "/nix/store/3pmdm9szxsqfvi2dv8b58vk7856g3gd8-memutils"; }; } { fetch = { type = "git"; url = "https://github.com/s-ludwig/taggedalgebraic.git"; - rev = "v0.11.22"; - sha256 = "02iy90nwy0zzy25hwdqbcgd0w0lwzramcvi3pgyljhq0w0vl5hkq"; + rev = "v0.11.19"; + sha256 = "1mb4l9hhkzhwwj2v3m9l4g59q66msy15ky762wk5dv11viyfwrqb"; fetchSubmodules = false; - date = "2021-05-20T21:00:02+02:00"; + date = "2021-01-13T16:58:20+01:00"; deepClone = false; leaveDotGit = false; - path = "/nix/store/p8id0qb13j8pjdczflj2x35w6v63q4cx-taggedalgebraic"; - }; -} { - fetch = { - type = "git"; - url = "https://github.com/vibe-d/vibe-container.git"; - rev = "v1.0.1"; - sha256 = "0miynjmfzz340z3qw9mclaj6cyirzrpk528idr5yfmvl3jsi6wh8"; - fetchSubmodules = false; - date = "2023-11-27T09:12:47+01:00"; - deepClone = false; - leaveDotGit = false; - path = "/nix/store/2vnn20q8k335h5k078yk4lrxkp4y63pd-vibe-container"; - }; -} { - fetch = { - type = "git"; - url = "https://github.com/vibe-d/vibe.d.git"; - rev = "v0.9.7"; - sha256 = "1q4yvcaf36lmf29izg01x888v40snpwiph310nrlpr4gyhd834av"; - fetchSubmodules = false; - date = "2023-08-29T13:24:09+02:00"; - deepClone = false; - leaveDotGit = false; - path = "/nix/store/7fq089i7zib7m9hxyl75mlfy264d9gnx-vibe.d"; + path = "/nix/store/qlw3bv0xqd0dl01hd9lcyk9cx7v9qhvi-taggedalgebraic"; }; } { fetch = { type = "git"; url = "https://github.com/vibe-d/vibe-core.git"; - rev = "v2.5.1"; - sha256 = "1g66vyn9hivy8rmsdl1xg1vz41csq5k91c04yvp4wqbil9cwgqdr"; + rev = "v1.13.0"; + sha256 = "1zbx861dwmkp14pbzr4qyq71xsnlksx248x0a1q6afjmvvxiys8w"; fetchSubmodules = false; - date = "2023-11-24T17:25:20+01:00"; + date = "2021-01-15T21:35:13+01:00"; deepClone = false; leaveDotGit = false; - path = "/nix/store/iddq1amvzic8fn4xa2317ykmkrkdr51y-vibe-core"; + path = "/nix/store/zngy3z8hmdgjg8wl31d2qy194mhz0i09-vibe-core"; + }; +} { + fetch = { + type = "git"; + url = "https://github.com/vibe-d/vibe.d.git"; + rev = "v0.9.3"; + sha256 = "10a5njn2nq1z6gknmkg6m6wrbndzj19mpg5860ky6v12b9ks76z3"; + fetchSubmodules = false; + date = "2021-01-29T11:37:00+01:00"; + deepClone = false; + leaveDotGit = false; + path = "/nix/store/8r3h7npkhj9wq9lg64i3fih19hibpmg5-vibe.d"; }; } { fetch = { @@ -135,13 +123,13 @@ fetch = { type = "git"; url = "https://github.com/rejectedsoftware/diet-ng.git"; - rev = "v1.8.1"; - sha256 = "11hrbvsxhipvcz9m1qlq92iaw0h35pzdy5rf7z8c4vx3hwjrnhni"; + rev = "v1.7.5"; + sha256 = "1cymg3v924d499sbjagjf5dqv1pj196c55a2282knxgq18a3gynf"; fetchSubmodules = false; - date = "2022-04-22T11:38:43+02:00"; + date = "2021-02-07T14:20:30+01:00"; deepClone = false; leaveDotGit = false; - path = "/nix/store/xziliy9ah3s3ah8mslnxsw47n4w6wkca-diet-ng"; + path = "/nix/store/5r6v9f7y9kwkb90ihgimqqryf6ls3kqq-diet-ng"; }; } { fetch = { @@ -159,37 +147,37 @@ fetch = { type = "git"; url = "https://github.com/D-Programming-Deimos/openssl.git"; - rev = "v3.3.3"; - sha256 = "1634j4psp3qwgwhk2sa3jj6gvwv3i96hpg2wrdy9ihjhabnszn0f"; + rev = "v1.1.6+1.0.1g"; + sha256 = "0ramqjyq4v7xpqwf4nf4ddmsg6yk2fbzn2d1yj4b6dla3q5lv9i3"; fetchSubmodules = false; - date = "2023-09-14T12:05:32+00:00"; + date = "2017-11-05T20:15:26+01:00"; deepClone = false; leaveDotGit = false; - path = "/nix/store/i3pgh154r80xvh4vsnl6srg6khmk5dg7-openssl"; + path = "/nix/store/jbfb5in6gzqs2byn1hdc1625yh8sx6j2-openssl"; }; } { fetch = { type = "git"; url = "https://github.com/vibe-d/eventcore.git"; - rev = "v0.9.26"; - sha256 = "13bjs5v5l1387vi2ss4gvqlslhq33v9sn4pg7nis4r0wdw0zmlk1"; + rev = "v0.9.13"; + sha256 = "0frxifhjwzyi35cv4pvv8k11a966fg76gqxdpmx9bsqbx750lrvz"; fetchSubmodules = false; - date = "2023-09-16T09:46:45+02:00"; + date = "2021-01-12T19:20:28+01:00"; deepClone = false; leaveDotGit = false; - path = "/nix/store/zy7ydasg4fwnim2m1mi4gzdg9l087wpb-eventcore"; + path = "/nix/store/yikgbvj61xhk6r3x8pc8lb7sp5smcn5q-eventcore"; }; } { fetch = { type = "git"; url = "https://github.com/kiith-sa/D-YAML.git"; - rev = "v0.8.6"; - sha256 = "1bvidcxp1n65r4wmiqakyl8vjvhqh3gln9wbsmbxrx9mf6k0zv8h"; + rev = "v0.8.3"; + sha256 = "13wy304xjbwkpgg7ilql1lkxkm83s87jm59ffnrg26slp7cx149q"; fetchSubmodules = false; - date = "2022-05-15T16:18:02-03:00"; + date = "2020-09-19T23:46:57+02:00"; deepClone = false; leaveDotGit = false; - path = "/nix/store/zd0yayd1j11809j95sjs4gdqcngvkhbg-D-YAML"; + path = "/nix/store/3i8i56lkmw2xq3lxr5h66v909waq2mqg-D-YAML"; }; } { fetch = { @@ -203,16 +191,4 @@ leaveDotGit = false; path = "/nix/store/6l82qf5nav5pkbvnrhcs5v9vwj5xycq3-libasync"; }; -} { - fetch = { - type = "git"; - url = "https://github.com/bildhuus/deimos-openssl-static.git"; - rev = "v1.0.2+3.0.8"; - sha256 = "00wllmfrjpq5ln3zs9qcqf4kq2i6bqbjxwj0jnxwwz125kfm55sy"; - fetchSubmodules = false; - date = "2023-02-24T13:23:37+01:00"; - deepClone = false; - leaveDotGit = false; - path = "/nix/store/g9shdkm450yg8c15rvqms8v09vql8z0l-deimos-openssl-static"; - }; } ] \ No newline at end of file diff --git a/mijnblog.nix b/mijnblog.nix index 8c3dbaf..710b373 100644 --- a/mijnblog.nix +++ b/mijnblog.nix @@ -6,11 +6,7 @@ mkDubDerivation { src = ./.; dubJSON = ./dub.json; selections = ./dub.selections.nix; - version = "0.0.3"; + version = "0.0.1"; buildInputs = [ pkgs.openssl ]; propagatedBuildInputs = [ pkgs.nix-prefetch-git ]; - extraDubFlags = "--override-config openssl/library-manual-version"; - preBuild = '' - export DC=${pkgs.dmd}/bin/dmd - ''; } diff --git a/mkDub.nix b/mkDub.nix index bf3b394..2600d30 100644 --- a/mkDub.nix +++ b/mkDub.nix @@ -1,9 +1,7 @@ { pkgs ? import {}, stdenv ? pkgs.stdenv, - lib ? pkgs.lib, - dtools ? pkgs.dtools or pkgs.rdmd, + rdmd ? pkgs.rdmd, dmd ? pkgs.dmd, - dcompiler ? dmd, dub ? pkgs.dub }: with stdenv; @@ -22,7 +20,7 @@ let fromDub = dubDep: mkDerivation rec { name = "${src.name}-${version}"; version = rev-to-version dubDep.fetch.rev; - nativeBuildInputs = [ dcompiler dtools dub ]; + nativeBuildInputs = [ rdmd dmd dub ]; src = dep2src dubDep; buildPhase = '' @@ -48,18 +46,10 @@ let targetOf = package: "${package.targetPath or "."}/${package.targetName or package.name}"; # Remove reference to build tools and library sources - disallowedReferences = deps: [ dcompiler dtools dub ] ++ builtins.map dep2src deps; + disallowedReferences = deps: [ dmd rdmd dub ] ++ builtins.map dep2src deps; removeExpr = refs: ''remove-references-to ${lib.concatMapStrings (ref: " -t ${ref}") refs}''; - # Like split, but only keep the matches - matches = regex: str: builtins.filter lib.isList (builtins.split regex str); - - # Very primitive parsing of SDL files, but suffices for name, description, homepage, etc. - importSDL = path: builtins.foldl' (a: l: a // {"${lib.elemAt l 1}"=lib.elemAt l 2;}) {} (matches "(^|\n)([a-z]+) \"([^\"]+)\"" (builtins.readFile path)); - - importPackage = sdl: json: if builtins.pathExists sdl then importSDL sdl else lib.importJSON json; - in { inherit fromDub; @@ -67,23 +57,20 @@ in { src, nativeBuildInputs ? [], dubJSON ? src + "/dub.json", - dubSDL ? src + "/dub.sdl", - buildType ? "release", - extraDubFlags ? "", selections ? src + "/dub.selections.nix", deps ? import selections, - package ? importPackage dubSDL dubJSON, passthru ? {}, + package ? lib.importJSON dubJSON, ... - } @ attrs: stdenv.mkDerivation ((removeAttrs attrs ["package" "deps" "selections" "dubJSON" "dubSDL"]) // { + } @ attrs: stdenv.mkDerivation (attrs // { pname = package.name; - nativeBuildInputs = [ dcompiler dtools dub pkgs.removeReferencesTo ] ++ nativeBuildInputs; + nativeBuildInputs = [ rdmd dmd dub pkgs.removeReferencesTo ] ++ nativeBuildInputs; disallowedReferences = disallowedReferences deps; passthru = passthru // { - inherit dub dcompiler dtools pkgs; + inherit dub dmd rdmd pkgs; }; src = lib.cleanSourceWith { @@ -100,7 +87,7 @@ in { export HOME=$PWD ${lib.concatMapStringsSep "\n" dub-add-local deps} - dub build -b ${buildType} --combined --skip-registry=all ${extraDubFlags} + dub build -b release --combined --skip-registry=all runHook postBuild ''; @@ -110,7 +97,7 @@ in { export HOME=$PWD ${lib.concatMapStringsSep "\n" dub-add-local deps} - dub test --combined --skip-registry=all ${extraDubFlags} + dub test --combined --skip-registry=all runHook postCheck ''; @@ -126,8 +113,6 @@ in { meta = lib.optionalAttrs (package ? description) { description = package.description; - } // lib.optionalAttrs (package ? homepage) { - homepage = package.homepage; } // attrs.meta or {}; } // lib.optionalAttrs (!(attrs ? version)) { # Use name from dub.json, unless pname and version are specified diff --git a/public/static/style/base.css b/public/static/style/base.css index 7211e57..9d12161 100644 --- a/public/static/style/base.css +++ b/public/static/style/base.css @@ -1,3 +1,32 @@ +@media (max-width: 850px) { + body { + flex-wrap: wrap; + } + + body > main { + box-sizing: border-box; + min-width: 100%; + } + + body > section.header-navigation, body > footer { + width: 100%; + flex-grow: 1 !important; + } + + .hide-small { + display: none; + } + + .hide-big { + display: block !important; + } +} + + +.hide-big { + display: none; +} + :root { font-family: "sans-serif"; --colour-bg: #f0f0f0; @@ -33,54 +62,48 @@ body { background-color: #f0f0f0; background-color: var(--colour-bg); margin: 0; - display: grid; - grid-template-columns: 2em 200px 600px min-content; - grid-column-gap: 2em; - grid-template-rows: 2em min-content min-content 1fr; - grid-template-areas: - ". . main . " - ". header main sidebar" - ". menu main sidebar" - ". footer main sidebar"; + display: flex; justify-content: center; min-height: 100vh; color: #000000; color: var(--colour-fg); } -body > nav { - grid-area: menu; +body > section.header-navigation { + flex: 0 0 200px; + padding: 2em; +} + +@media (pointer: coarse) { + body > section.header-navigation li { + padding: 0.25em 0; + } } body > footer { - grid-area: footer; - margin-top: 0; + flex: 0 0 200px; + padding: 2em; + border: none; } -body > header { - grid-area: header; +body > section.header-navigation > header { text-align: center; - margin-bottom: 0; } -body > header > img { +body > section.header-navigation > header > img { width: 100%; max-width: 160px; } -body > section.main-sidebar { - grid-area: sidebar; -} - body > main { - grid-area: main; + flex: 1 1; padding: 2em; /* width: 600px; */ border-left: #7f0602 dotted 1px; border-right: #7f0602 dotted 1px; border-left: var(--colour-fg-highlight) dotted 1px; border-right: var(--colour-fg-highlight) dotted 1px; - max-width: calc(600px); + max-width: 600px; background-color: #ddd; background-color: var(--colour-bg-main); } @@ -251,65 +274,11 @@ a { text-decoration: underline dotted; } -a:hover, a:focus { +a:hover { color: var(--colour-fg); text-decoration: underline solid; } -@media (max-width: calc(800px + 8em)) { - body { - grid-template-columns: 100%; - grid-column-gap: 2em; - grid-template-rows: repeat(4, min-content); - grid-template-areas: - "header" - "menu" - "main" - "footer"; - } - - body > .main-sidebar { - display: none; - } - - body > main { - box-sizing: border-box; - min-width: 100%; - border: none; - border-top: #7f0602 dotted 1px; - border-bottom: #7f0602 dotted 1px; - border-top: var(--colour-fg-highlight) dotted 1px; - border-bottom: var(--colour-fg-highlight) dotted 1px; - } - - body > nav, body > footer { - width: calc(100% - 4em); - padding: 2em; - } -} - -@media (pointer: coarse) { - body > nav li a { - display: inline-block; - padding: 1em 2em; - width: calc(100% - 4em); - } - - body > nav > ul { - width: 100%; - } - - body > nav li:not(:last-child) { - border-bottom: 1px dotted #000000; - border-bottom: 1px dotted var(--colour-fg); - } - - body > nav { - padding: 1em 0em; - width: 100%; - } -} - @media print { body { background-color: initial; diff --git a/public/static/style/old.css b/public/static/style/old.css index 2c7b5db..6eda75d 100644 --- a/public/static/style/old.css +++ b/public/static/style/old.css @@ -2,17 +2,6 @@ body { background-image: url(old/chip.jpg); cursor: url(old/cursor.gif), auto; justify-content: flex-start; - grid-column-gap: 0em; - grid-template-columns: 200px 600px; - grid-template-rows: min-content min-content 1fr; - grid-template-areas: - "header main" - "menu main" - "footer main"; -} - -.main-sidebar { - display: none; } header { @@ -27,15 +16,14 @@ footer { a:hover { cursor: url(old/cursor-over.gif), auto; - color: red; } -body > nav, body > header, body > footer { +.header-navigation { background-color: cyan; color: black; } -body > nav ul { +.header-navigation> nav ul { padding-left: 2em; list-style-image: url(old/bullet.gif); } diff --git a/source/nl/netsoj/chris/blog/cache.d b/source/nl/netsoj/chris/blog/cache.d index b11a0d8..46d4e3f 100644 --- a/source/nl/netsoj/chris/blog/cache.d +++ b/source/nl/netsoj/chris/blog/cache.d @@ -1,11 +1,15 @@ +/** + * Implements and holds caches for several pages. + */ +module nl.netsoj.chris.blog.cache; + import std.experimental.logger; import std.traits; -import article; -import page; -import project; +import nl.netsoj.chris.blog.model.article; +import nl.netsoj.chris.blog.model.page; +import nl.netsoj.chris.blog.model.project; -@safe: /** * Default ordering and list with pointers to ordered articles. @@ -22,7 +26,7 @@ GenericCache!(Project, "a.title < b.title") projects; * again if needed. */ struct GenericCache(T, string sortOrder) - if (isImplicitlyConvertible!(T, Page)) { + if (is(T : Page)) { public: void addItem(T item) { diff --git a/source/nl/netsoj/chris/blog/constants.d b/source/nl/netsoj/chris/blog/constants.d index b483152..ee4b395 100644 --- a/source/nl/netsoj/chris/blog/constants.d +++ b/source/nl/netsoj/chris/blog/constants.d @@ -1,10 +1,14 @@ +module nl.netsoj.chris.blog.constants; + /** * Constants which are passed to templates while rendering. */ -struct Constants { +class Constants { public static immutable string SITE_NAME = "Chris Josten's site"; - public static immutable string SITE_HOST = "chris.netsoj.nl"; - public static immutable string SITE_URL = "https://chris.netsoj.nl"; - public static immutable string SITE_AUTHOR = "Chris Josten"; + debug { + public static immutable string SITE_URL = "https://kortstondig.chris.netsoj.nl"; + } else { + public static immutable string SITE_URL = "https://chris.netsoj.nl"; + } public static immutable string COPYRIGHT = "© Chris Josten, 2020"; } diff --git a/source/nl/netsoj/chris/blog/interfaces/http.d b/source/nl/netsoj/chris/blog/interfaces/http.d index 4f592cc..dfcb0cd 100644 --- a/source/nl/netsoj/chris/blog/interfaces/http.d +++ b/source/nl/netsoj/chris/blog/interfaces/http.d @@ -1,14 +1,15 @@ -import std.algorithm : map, maxElement; -import std.format : format; -import std.string: join; +module nl.netsoj.chris.blog.interfaces.http; + +import std.experimental.logger; import vibe.d; -import cache; -import constants; -import article; -import page; -import project; +import nl.netsoj.chris.blog.interfaces.indieauth; +import nl.netsoj.chris.blog.model.article; +import nl.netsoj.chris.blog.model.page; +import nl.netsoj.chris.blog.model.project; +import nl.netsoj.chris.blog.cache; +import nl.netsoj.chris.blog.constants; /** * Output types for the content. @@ -63,7 +64,7 @@ struct TranslateContext { 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 determineLanguageByHeader(req.query.get("lang", "en_GB"), languages); + return req.query.get("lang", "en_GB"); } } @@ -74,29 +75,41 @@ struct TranslateContext { * templateName = The name of the template to render. */ string singleResponseMixin(string arrayName, string templateName) { - return `string slug = req.params["slug"]; - OutputType outputType = getOutputType(slug); + 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: - res.render!("` ~ templateName ~ `", content); - break; - }`; - } + 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: + + this() { + m_indieAuth = new IndieAuth(); + } + + /** + * IndieAuth subinterface + */ + @safe + @path("/indieweb") + @property IndieAuth indieweb() { return m_indieAuth; }; + /** - * Generates response for /posts/:slug and /palen/:slug. - */ + * 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); @@ -145,62 +158,13 @@ public: addCachingHeader(res); // If no slug is supplied, it will be adjusted to "index" req.params.addField("slug", "index"); + res.headers.addField("Link", "<" ~ Constants.SITE_URL ~ "/indieweb/auth>; rel=\"authorization_endpoint\""); + res.headers.addField("Link", "<" ~ Constants.SITE_URL ~ "/indieweb/token>; rel=\"token_endpoint\""); + res.headers.addField("Link", "<" ~ Constants.SITE_URL ~ "/indieweb/micropub>; rel=\"micropub\""); mixin(singleResponseMixin("pages", "pages/page.dt")); } - - @path("/feeds/posts.atom") - void getPostFeed(HTTPServerRequest req, HTTPServerResponse res) { - Article[] articleList = articles.sortedList; - - DateTime lastUpdated = articleList.length > 0 - ? articleList.map!"a.firstPublished()".maxElement - : DateTime(2019, 06, 30); - - string response = q"EOS - - - %s - %s - %s - - - urn:uuid:036f1087-7fcd-466d-866d-78a0c60038cd - %s - %s - -EOS" - .format( - lastUpdated.toISOExtString() ~ "Z", - trWeb("template.feed.title").format(trWeb("template.feed.posts.title")), - Constants.SITE_URL ~ "/static/img/logo.png", - Constants.SITE_URL, - Constants.SITE_URL, - Constants.SITE_AUTHOR, - articleList.map!((article) { - return q"EOS - - %s - - %s - %s - %s - %s - %s - - -EOS" - .format(article.title, - Constants.SITE_URL ~ "/posts/" ~ article.slug, - "tag:" ~ Constants.SITE_HOST ~ ",2024:blog:posts:" ~ article.slug, - article.firstPublished().toISOExtString() ~ "Z", - article.updated().toISOExtString() ~ "Z", - article.excerpt(), - htmlEscape(article.content()) - ); - }).join() - ); - res.writeBody(response, "application/atom+xml"); - } +private: + IndieAuth m_indieAuth; } /** @@ -208,7 +172,9 @@ EOS" */ @safe void errorPage(HTTPServerRequest req, HTTPServerResponse res, HTTPServerErrorInfo error) { - render!("pages/error.dt", error)(res); + //render!("pages/error.dt", error)(res); + import std.conv; + res.writeBody(text("Error ", error.code, ": ", error.message, "\n\n", error.debugMessage), "text/plain"); } @trusted @@ -230,4 +196,7 @@ void startHTTPServer() { router.registerWebInterface(new MijnBlog); listenHTTP(settings, router); + foreach(route; router.getAllRoutes()) { + infof("Path: %s", route); + } } diff --git a/source/nl/netsoj/chris/blog/interfaces/indieauth.d b/source/nl/netsoj/chris/blog/interfaces/indieauth.d new file mode 100644 index 0000000..841f73c --- /dev/null +++ b/source/nl/netsoj/chris/blog/interfaces/indieauth.d @@ -0,0 +1,92 @@ +module nl.netsoj.chris.blog.interfaces.indieauth; + +import nl.netsoj.chris.blog.interfaces.http; +import nl.netsoj.chris.blog.interfaces.micropub; +import nl.netsoj.chris.blog.microformats.parser; + +import mfd = nl.netsoj.chris.blog.microformats.definitions; + +import vibe.d; + +struct App { + string logo;; + string name; + string url; + string clientId; + string redirectUri; + bool richInfo = false; +} + +/** + * An implementation of https://www.w3.org/TR/indieauth/ + */ +@translationContext!TranslateContext +class IndieAuth { +public: + this() { + m_microPub = new MicroPub(); + } + + @queryParam("response_type", "response_type") + @queryParam("me", "me") + @queryParam("client_id", "client_id") + @queryParam("redirect_uri", "redirect_uri") + @queryParam("state", "state") + void getAuth(HTTPServerRequest req, HTTPServerResponse res, + string response_type, string me, string client_id, string redirect_uri, string state) { + enforceHTTP(response_type == "code", HTTPStatus.badRequest); + + URL source = URL(client_id); + HTTPClientResponse response = requestHTTP(source); + mfd.App[] parsedApps = parsePage!(mfd.App)(response.bodyReader.readAllUTF8, source); + + App app; + app.name = client_id; + app.richInfo = false; + if (parsedApps.length > 0) { + auto parsedApp = parsedApps[0]; + app.logo = parsedApp.logo; + app.name = parsedApp.name; + app.richInfo = true; + } + + app.clientId = client_id; + URL redirectUrl = URL(redirect_uri); + redirectUrl.queryString = redirectUrl.queryString ~ "%scode=%s&state=%s".format( + (redirectUrl.queryString.length == 0 ? "" : "&"), + "123456", state); + app.redirectUri = redirectUrl.toString(); + render!("pages/indieauth.dt", app); + } + + @safe + @path("/") + void getIndex(HTTPServerResponse res) { + res.writeBody("IndieAuth root", "text/plain"); + } + + @safe + void postToken(HTTPServerRequest req, HTTPServerResponse res, + string grant_type, string code, string client_id, string redirect_uri, string me) { + + struct OkResponse { + string access_token; + string token_type = "Bearer"; + string me; + string scope_; + } + + OkResponse response; + response.me = me; + response.scope_ = "foo"; + response.access_token = "baz"; + + res.writeJsonBody(response, HTTPStatus.ok); + } + + @safe @property + MicroPub micropub() { return m_microPub; }; + +private: + MicroPub m_microPub; +} diff --git a/source/nl/netsoj/chris/blog/interfaces/micropub.d b/source/nl/netsoj/chris/blog/interfaces/micropub.d new file mode 100644 index 0000000..7e6ece2 --- /dev/null +++ b/source/nl/netsoj/chris/blog/interfaces/micropub.d @@ -0,0 +1,14 @@ +module nl.netsoj.chris.blog.interfaces.micropub; + +import vibe.d; + +import nl.netsoj.chris.blog.interfaces.http; + +@translationContext!TranslateContext +class MicroPub { + + @path("/") + void getIndex(HTTPServerRequest req, HTTPServerResponse res) { + res.writeBody("MicroPub endpoint", "text/plain"); + } +} diff --git a/source/nl/netsoj/chris/blog/main.d b/source/nl/netsoj/chris/blog/main.d index edce64a..5174e36 100644 --- a/source/nl/netsoj/chris/blog/main.d +++ b/source/nl/netsoj/chris/blog/main.d @@ -1,23 +1,14 @@ +module nl.netsoj.chris.blog.main; + import std.experimental.logger; import vibe.d; -import article; -import page; -import project; - -import cache; -import http; -import watcher; - -void watchTask(T, C)(C *cache, string directory) @safe nothrow { - do { - try { - initPages!T(cache, directory); - } catch(Exception e) { - logWarn("Error while watching pages: " ~ e.msg); - } - } while(Task.getThis().running); -} +import nl.netsoj.chris.blog.interfaces.http; +import nl.netsoj.chris.blog.model.article; +import nl.netsoj.chris.blog.model.page; +import nl.netsoj.chris.blog.model.project; +import nl.netsoj.chris.blog.cache; +import nl.netsoj.chris.blog.watcher; void main() { @@ -25,13 +16,13 @@ void main() { // Start indexing pages. runTask({ - watchTask!Page(&pages, "pages"); + initPages!Page(&pages, "pages"); }); runTask({ - watchTask!Article(&articles, "articles"); + initPages!Article(&articles, "articles"); }); runTask({ - watchTask!Project(&projects, "projects"); + initPages!Project(&projects, "projects"); }); runApplication(); } diff --git a/source/nl/netsoj/chris/blog/microformats/definitions.d b/source/nl/netsoj/chris/blog/microformats/definitions.d new file mode 100644 index 0000000..66c0417 --- /dev/null +++ b/source/nl/netsoj/chris/blog/microformats/definitions.d @@ -0,0 +1,12 @@ +module nl.netsoj.chris.blog.microformats.definitions; + +import nl.netsoj.chris.blog.microformats.parser; + +struct App { + @MicroFormat(MicroFormat.Type.Url) + string logo; + @MicroFormat(MicroFormat.Type.PlainText) + string name; + + string url; +} diff --git a/source/nl/netsoj/chris/blog/microformats/parser.d b/source/nl/netsoj/chris/blog/microformats/parser.d new file mode 100644 index 0000000..d72279a --- /dev/null +++ b/source/nl/netsoj/chris/blog/microformats/parser.d @@ -0,0 +1,116 @@ +/** + * Parser for microformats. + * Standards: http://microformats.org/wiki/microformats2-parsing + */ +module nl.netsoj.chris.blog.microformats.parser; + +import std.exception; +import std.traits; + +import vibe.inet.url; + +import html; + +import nl.netsoj.chris.blog.url; + +class MicroFormatParseException : Exception { + mixin basicExceptionCtors; +} + + +struct MicroFormatProperty { + enum Type { + RootClass, + PlainText, + Url, + DateTime, + EmbeddedMarkup + }; + + Type type = Type.PlainText; + + string getClassPrefix() pure { + final switch(type) { + case Type.RootClass: + return "h-"; + case Type.PlainText: + return "p-"; + case Type.Url: + return "u-"; + case Type.DateTime: + return "dt-"; + case Type.EmbeddedMarkup: + return "e-"; + } + } +}; + +/** + * Parses a web page to extract to the given microformat model out of it + */ +T[] parsePage(T)(string source, URL url) { + return parsePage!T(createDocument(source), url); +} + +T[] parsePage(T)(Document page, URL url) + if (isAggregateType!T) { + import std.algorithm; + import std.array; + import std.conv; + import std.range; + import std.string; + + string rootClass = "h-" ~ T.stringof.toLower; + + return page.querySelectorAll(".%s".format(rootClass)).map!((node){ + alias PropType = MicroFormatProperty.Type; + T instance = T(); + MicroFormatProperty uda; + string propertyClass; + Node propNode; + static foreach(sym; getSymbolsByUDA!(T, MicroFormatProperty)) { + uda = getUDAs!(sym, MicroFormatProperty)[0]; + propertyClass = uda.getClassPrefix() ~ sym.stringof.toLower; + + propNode = page.querySelector(".%s".format(propertyClass), node); + switch (uda.type) { + case PropType.PlainText: + if (propNode.firstChild && propNode.firstChild.isTextNode()) { + __traits(getMember, instance, sym.stringof) = to!string(propNode.text); + } + break; + case PropType.Url: + if (propNode.tag == "a" && propNode.hasAttr("href")) { + __traits(getMember, instance, sym.stringof) = resolveURL(url, to!string(propNode["href"])).toString; + } else if (propNode.tag == "img" && propNode.hasAttr("src")) { + __traits(getMember, instance, sym.stringof) = resolveURL(url, to!string(propNode["src"])).toString; + } + break; + default: + break; + } + } + + return instance; + }).array; +} + +unittest { + import std.stdio; + string page = q"eos +
+ + Example App +
" +eos"; + + struct App { + @MicroFormatProperty(MicroFormatProperty.Type.Url) + string logo; + @MicroFormatProperty(MicroFormatProperty.Type.PlainText) + string name; + } + + auto ts = parsePage!App(page, URL("https://example.com/")); + assert(ts[0] == App ("https://example.com:443/logo.png", "Example App")); +} diff --git a/source/nl/netsoj/chris/blog/model/article.d b/source/nl/netsoj/chris/blog/model/article.d index a1ec65f..b22677b 100644 --- a/source/nl/netsoj/chris/blog/model/article.d +++ b/source/nl/netsoj/chris/blog/model/article.d @@ -1,3 +1,5 @@ +module nl.netsoj.chris.blog.model.article; + import std.file; import std.stdio; import std.string; @@ -7,10 +9,9 @@ import std.experimental.logger; import dyaml; import vibe.d; -import page; -import utils; +import nl.netsoj.chris.blog.model.page; +import nl.netsoj.chris.blog.utils; -@safe: /** * Represents an article on the blog @@ -35,9 +36,9 @@ class Article : Page { // Find the first header and mark everything up to that as if (m_excerpt is null) { // an excerpt, used in search results. - const long seperatorIndex = cast(long) lastIndexOf(m_contentSource, "---\n"); + const uint seperatorIndex = cast(uint) indexOf(m_contentSource, "---\n"); this.m_excerpt = this.m_contentSource[seperatorIndex + 4..$]; - const long firstHeaderIndex = indexOf(this.m_excerpt, '#'); + const uint firstHeaderIndex = cast(uint) indexOf(this.m_excerpt, '#'); if (firstHeaderIndex >= 0) { this.m_excerpt = this.m_excerpt[0..firstHeaderIndex]; } diff --git a/source/nl/netsoj/chris/blog/model/page.d b/source/nl/netsoj/chris/blog/model/page.d index 26cc8e2..e0b14d8 100644 --- a/source/nl/netsoj/chris/blog/model/page.d +++ b/source/nl/netsoj/chris/blog/model/page.d @@ -1,3 +1,5 @@ +module nl.netsoj.chris.blog.model.page; + import std.exception; import std.experimental.logger; import std.file; @@ -7,10 +9,8 @@ import std.stdio; import dyaml; import vibe.vibe; -import constants; -import utils; +import nl.netsoj.chris.blog.utils; -@safe: /** * Exception thrown when a page has syntax errors e.g. @@ -53,11 +53,11 @@ class Page { /** * Creates a page from a file. This will read from the file and parse it. */ - this(string file) @safe { + this(string file) { this.m_name = file; this.m_contentSource = readText(file); // Find the seperator and split the string in two - const long seperatorIndex = lastIndexOf(m_contentSource, "\n---\n"); + const uint seperatorIndex = cast(uint) lastIndexOf(m_contentSource, "---\n"); enforce!ArticleParseException(seperatorIndex >= 0); string header = m_contentSource[0..seperatorIndex]; @@ -108,12 +108,10 @@ class Page { "-f", "markdown", "-t", "html", "--lua-filter", "defaultClasses.lua"]; - string[string] env; - env["WEBROOT"] = Constants.SITE_URL; if (shiftHeader != 0) args ~= "--shift-heading-level-by=" ~ to!string(shiftHeader); - ProcessPipes pandoc = pipeProcess(args, Redirect.all, env); + ProcessPipes pandoc = pipeProcess(args); pandoc.stdin.write(source); pandoc.stdin.writeln(); pandoc.stdin.flush(); diff --git a/source/nl/netsoj/chris/blog/model/project.d b/source/nl/netsoj/chris/blog/model/project.d index da7e6b2..1b02a91 100644 --- a/source/nl/netsoj/chris/blog/model/project.d +++ b/source/nl/netsoj/chris/blog/model/project.d @@ -1,3 +1,5 @@ +module nl.netsoj.chris.blog.model.project; + import std.array; import std.algorithm; import std.typecons; @@ -5,11 +7,9 @@ import std.typecons; import dyaml; import vibe.vibe; -import page; -import utils; -import staticpaths; - -@safe: +import nl.netsoj.chris.blog.model.page; +import nl.netsoj.chris.blog.staticpaths; +import nl.netsoj.chris.blog.utils; /** * Represents a project, like an unfinished application diff --git a/source/nl/netsoj/chris/blog/package.d b/source/nl/netsoj/chris/blog/package.d new file mode 100644 index 0000000..2ff5397 --- /dev/null +++ b/source/nl/netsoj/chris/blog/package.d @@ -0,0 +1 @@ +module nl.netsoj.chris.blog; diff --git a/source/nl/netsoj/chris/blog/staticpaths.d b/source/nl/netsoj/chris/blog/staticpaths.d index 663cc5d..36f16b3 100644 --- a/source/nl/netsoj/chris/blog/staticpaths.d +++ b/source/nl/netsoj/chris/blog/staticpaths.d @@ -1,3 +1,5 @@ +module nl.netsoj.chris.blog.staticpaths; + /** * Paths to static data. */ diff --git a/source/nl/netsoj/chris/blog/url.d b/source/nl/netsoj/chris/blog/url.d new file mode 100644 index 0000000..d069c36 --- /dev/null +++ b/source/nl/netsoj/chris/blog/url.d @@ -0,0 +1,27 @@ +module nl.netsoj.chris.blog.url; + +import std.exception; + +import vibe.inet.url; + +URL resolveURL(URL source, string other) { + URL otherUrl = URL(other); + if (otherUrl.schema.length > 0 || otherUrl.host.length > 0) { + return otherUrl; + } + + if (otherUrl.schema.length == 0) { + enforce(source.schema.length > 0, "Source URL must have a scheme to resolve the other URL"); + otherUrl.schema = source.schema; + } + + if (otherUrl.host.length == 0) { + otherUrl.host = source.host; + otherUrl.port = source.port; + if (!otherUrl.path.absolute) { + otherUrl.path = source.path ~ otherUrl.path; + } + } + + return otherUrl; +} diff --git a/source/nl/netsoj/chris/blog/utils.d b/source/nl/netsoj/chris/blog/utils.d index bb1a681..953e9ff 100644 --- a/source/nl/netsoj/chris/blog/utils.d +++ b/source/nl/netsoj/chris/blog/utils.d @@ -1,3 +1,5 @@ +module nl.netsoj.chris.blog.utils; + import std.algorithm; import std.array; import std.conv; diff --git a/source/nl/netsoj/chris/blog/watcher.d b/source/nl/netsoj/chris/blog/watcher.d index ca05cdc..6acf760 100644 --- a/source/nl/netsoj/chris/blog/watcher.d +++ b/source/nl/netsoj/chris/blog/watcher.d @@ -1,47 +1,41 @@ +module nl.netsoj.chris.blog.watcher; + import std.array; import std.algorithm; -//import std.experimental.logger; +import std.experimental.logger; import std.file; import std.stdio; import std.traits; import vibe.d; -import cache; -import page; +import nl.netsoj.chris.blog.cache; +import nl.netsoj.chris.blog.model.page; /** * Loads pages into memory and sets up a "watcher" to watch a directory for file changes. */ -void initPages(T, C)(C *cache, const string directory) @trusted - if (isImplicitlyConvertible!(T, Page)) { - - NativePath watchingDir; - try { - watchingDir = getWorkingDirectory() ~ directory; - } catch(PathValidationException) { - logError("Cannot watch path " ~ directory); - return; - } +void initPages(T, C)(C *cache, const string directory) + if (is(T : Page)) { bool addPage(string path) { try { T newPage = new T(path); - logInfo("Added %s", newPage.slug); + logf("Added %s", newPage.slug); cache.addItem(newPage); return true; - } catch (page.ArticleParseException e) { - logWarn("Could not parse %s: %s", path, e); + } catch (ArticleParseException e) { + logf("Could not parse %s: %s", path, e); return false; } catch (Exception e) { - logWarn("Other exception while parsing %s: %s", path, e); + logf("Other exception while parsing %s: %s", path, e); return false; } } // Initial scan void scan(NativePath path, int level = 0) { - logInfo("Scanning %s", path.toString()); + logf("Scanning %s", path.toString()); foreach(file; iterateDirectory(path)) { if (file.isDirectory) { scan(path ~ file.name, level + 1); @@ -51,11 +45,11 @@ void initPages(T, C)(C *cache, const string directory) @trusted } } - if (!existsFile(watchingDir)) { - createDirectory(watchingDir); + if (!existsFile(getWorkingDirectory() ~ directory)) { + createDirectory(getWorkingDirectory() ~ directory); } - scan(watchingDir); - DirectoryWatcher watcher = watchDirectory(watchingDir, true); + scan(getWorkingDirectory() ~ directory); + DirectoryWatcher watcher = watchDirectory(getWorkingDirectory() ~ directory, true); bool shouldStop = false; while (!shouldStop) { @@ -63,16 +57,16 @@ void initPages(T, C)(C *cache, const string directory) @trusted DirectoryChange[] changes; shouldStop = !watcher.readChanges(changes); foreach(change; changes) { - logInfo("=======[New changes]======"); + logf("=======[New changes]======"); string[] changeTypes = ["added", "removed", "modified"]; - logInfo("Path: %s, type: %s", change.path.toString(), changeTypes[change.type]); + logf("Path: %s, type: %s", change.path.toString(), changeTypes[change.type]); if (endsWith(change.path.toString(), ".kate-swp")) continue; switch (change.type) with (DirectoryChangeType){ case added: try { addPage(change.path.toString()); } catch(Exception e) { - logWarn("Error while updating %s: %s", change.path.toString(), e.msg); + warningf("Error while updating %s: %s", change.path.toString(), e.msg); } break; case modified: @@ -80,17 +74,17 @@ void initPages(T, C)(C *cache, const string directory) @trusted try { newPage = new T(change.path.toString()); cache.changeItem(newPage); - } catch(page.ArticleParseException e) { - logWarn("Could not parse %s", change.path.toString()); + } catch(ArticleParseException e) { + warningf("Could not parse %s", change.path.toString()); } catch (Exception e) { - logWarn("Error while updating %s: %s", change.path.toString(), e.msg); + warningf("Error while updating %s: %s", change.path.toString(), e.msg); } break; case removed: try { cache.removeItemByName(change.path.toString()); } catch(Exception e) { - logInfo("Error while trying to remove %s: %s", T.stringof, e.msg); + logf("Error while trying to remove %s: %s", T.stringof, e.msg); } break; default: break; diff --git a/translations/mijnblog.en_GB.po b/translations/mijnblog.en_GB.po index dcf2d5c..8aa177c 100644 --- a/translations/mijnblog.en_GB.po +++ b/translations/mijnblog.en_GB.po @@ -17,12 +17,6 @@ msgid "template.menu.contact" msgstr "Contact" msgid "template.page.copyright" -msgstr "© Chris Josten, 2024. If not specified otherwise, all content on this " +msgstr "© Chris Josten, 2021. If not specified otherwise, all content on this" "website is " "licensed under the CC-BY 4.0" - -msgid "template.feed.title" -msgstr "%s | Chris's website" - -msgid "template.feed.posts.title" -msgstr "Posts" diff --git a/translations/mijnblog.nl_NL.po b/translations/mijnblog.nl_NL.po index 9eac5b2..8271a8a 100644 --- a/translations/mijnblog.nl_NL.po +++ b/translations/mijnblog.nl_NL.po @@ -30,11 +30,5 @@ msgid "template.menu.contact" msgstr "Contact" msgid "template.page.copyright" -msgstr "© Chris Josten, 2024. Tenzij anders vermeld staat, valt alle inhoud op deze webstek " +msgstr "© Chris Josten, 2021. Tenzij anders vermeld staat, valt alle inhoud op deze webstek" " onder de CC-BY 4.0" - -msgid "template.feed.title" -msgstr "%s | Chris z'n webstekkie" - -msgid "template.feed.posts.title" -msgstr "Berichten" diff --git a/translations/mijnblog.pot b/translations/mijnblog.pot index dcf2d5c..4d4a992 100644 --- a/translations/mijnblog.pot +++ b/translations/mijnblog.pot @@ -1,28 +1,2 @@ -msgid "template.page.name" -msgstr "Chris's website" - -msgid "template.page.html_language" -msgstr "en-GB" - -msgid "template.menu.home" -msgstr "Home" - -msgid "template.menu.posts" -msgstr "Posts" - -msgid "template.menu.projects" -msgstr "Projects" - -msgid "template.menu.contact" -msgstr "Contact" - -msgid "template.page.copyright" -msgstr "© Chris Josten, 2024. If not specified otherwise, all content on this " -"website is " -"licensed under the CC-BY 4.0" - -msgid "template.feed.title" -msgstr "%s | Chris's website" - -msgid "template.feed.posts.title" -msgstr "Posts" +msgid "page.header-bottom-text" +msgstr "" diff --git a/views/pages/article-list.dt b/views/pages/article-list.dt index 03e6662..4846256 100644 --- a/views/pages/article-list.dt +++ b/views/pages/article-list.dt @@ -12,7 +12,7 @@ block content - if (articleList.length == 0) p No posts found - else - - import utils; + - import nl.netsoj.chris.blog.utils; - foreach(article; articleList) article header diff --git a/views/pages/article.dt b/views/pages/article.dt index 835f174..81d4502 100644 --- a/views/pages/article.dt +++ b/views/pages/article.dt @@ -14,7 +14,7 @@ block extra_meta_data block content article(itemscope, itemtype="https://schema.org/BlogPosting", lang="#{content.language}") - - import utils; + - import nl.netsoj.chris.blog.utils; header h1.title(itemprop="headline") #{content.title} p.subtitle diff --git a/views/pages/indieauth.dt b/views/pages/indieauth.dt new file mode 100644 index 0000000..4e01fc7 --- /dev/null +++ b/views/pages/indieauth.dt @@ -0,0 +1,21 @@ +extends parts/page + +block header + title Aanmelden - Netsoj.nl + +block sidebar + +block content + - if (app.richInfo) + header.project-header + img.project-icon(src="#{app.logo}", alt="Icon of #{app.name}") + div + h1.title Aanmelden met #[a(href="#{app.clientId}") #{app.name}] + span.project-description #{app.clientId} + - else + header + h1.title Aanmelden met #[a(href="#{app.clientId}") #{app.name}] + + p #{app.name} wil uw identiteit bevestigen. + + p #[a(href="#{app.redirectUri}") Aanvaarden en aanmelden]  #[a(href="javascript:history.back();") Weigeren] diff --git a/views/parts/footer.dt b/views/parts/footer.dt index b018fe1..1385b34 100644 --- a/views/parts/footer.dt +++ b/views/parts/footer.dt @@ -1,2 +1,2 @@ p - small !{trWeb("template.page.copyright")} + small& template.page.copyright diff --git a/views/parts/page.dt b/views/parts/page.dt index 7990c91..924b43c 100644 --- a/views/parts/page.dt +++ b/views/parts/page.dt @@ -1,12 +1,11 @@ doctype html html(prefix="og: http://ogp.me/ns#") - - import constants; + - import nl.netsoj.chris.blog.constants; - import vibe.d; head //- Kick off loading the css as fast as possible meta(name="viewport", content="width=device-width; initial-scale=1") link(rel="stylesheet", href="/static/style/base.css") - link(rel="alternate", href="/feeds/posts.atom", type="application/atom+xml", title=trWeb("template.feed.title").format(trWeb("template.feed.posts.title"))) - string page_image = "/static/img/logo.png"; - string page_title; @@ -44,25 +43,23 @@ html(prefix="og: http://ogp.me/ns#") li a(href="#{link}") #{trWeb(text)} - header - img.logo(src="/static/img/logo.png", alt="The logo of the website: the letter C drawn in an unprofessional manner with wobbly eyes on put on top") - p #{trWeb("template.page.name")} - - section.main-sidebar + section.header-navigation + header + img.logo(src="/static/img/logo.png", alt="The logo of the website: the letter C drawn in an unprofessional manner with wobbly eyes on put on top") + p& template.page.name + nav + ul + - menuItem("template.menu.home", "/"); + - menuItem("template.menu.posts", "/posts/"); + - menuItem("template.menu.projects", "/projects/"); + - menuItem("template.menu.contact", "/contact"); block sidebar - - nav - ul - - menuItem("template.menu.home", "/"); - - menuItem("template.menu.posts", "/posts/"); - - menuItem("template.menu.projects", "/projects/"); - - menuItem("template.menu.contact", "/contact"); - + footer.hide-small + block footer + include parts/footer main block content - - footer + + footer.hide-big block footer include parts/footer - -