about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--ci-builds.nix1
-rw-r--r--web/blog/.skip-subtree1
-rw-r--r--web/blog/default.nix46
-rw-r--r--web/blog/fragments.nix81
-rw-r--r--web/blog/nginx.nix68
-rw-r--r--web/blog/static/blog.css35
6 files changed, 232 insertions, 0 deletions
diff --git a/ci-builds.nix b/ci-builds.nix
index d29edac43c..54d6f637a5 100644
--- a/ci-builds.nix
+++ b/ci-builds.nix
@@ -18,6 +18,7 @@ in with pkgs; [
   tools.blog_cli
   tools.cheddar
   tools.emacs
+  web.blog
   web.cgit-taz
   lisp.dns
   third_party.cgit
diff --git a/web/blog/.skip-subtree b/web/blog/.skip-subtree
new file mode 100644
index 0000000000..e7fa50d49b
--- /dev/null
+++ b/web/blog/.skip-subtree
@@ -0,0 +1 @@
+Subdirectories contain blog posts and static assets only
diff --git a/web/blog/default.nix b/web/blog/default.nix
new file mode 100644
index 0000000000..bc7b9666f7
--- /dev/null
+++ b/web/blog/default.nix
@@ -0,0 +1,46 @@
+# This creates the static files that make up my blog from the Markdown
+# files in this repository.
+#
+# All blog posts are rendered from Markdown by cheddar.
+{ pkgs, lib, ... }@args:
+
+with pkgs.nix.yants;
+
+let
+  # Type definition for a single blog post.
+  post = struct "blog-post" {
+    key = string; #
+    title = string;
+    date = string; # *sigh*
+
+    # Path to the Markdown file containing the post content.
+    content = path;
+
+    # Should this post be included in the index? (defaults to true)
+    listed = option bool;
+
+    # Is this a draft? (adds a banner indicating that the link should
+    # not be shared)
+    draft = option bool;
+
+    # Previously each post title had a numeric ID. For these numeric
+    # IDs, redirects are generated so that old URLs stay compatible.
+    oldKey = option string;
+  };
+
+  posts = list post (import ./posts.nix);
+  fragments = import ./fragments.nix args;
+
+  renderedBlog = pkgs.third_party.runCommandNoCC "tazjins-blog" {} ''
+    mkdir -p $out
+
+    cp ${fragments.blogIndex posts} $out/index.html
+
+    ${lib.concatStringsSep "\n" (map (post:
+      "cp ${fragments.renderPost post} $out/${post.key}.html"
+    ) posts)}
+  ''; # '' (this line makes nix-mode happy :/)
+
+in import ./nginx.nix (args // {
+  inherit posts renderedBlog;
+})
diff --git a/web/blog/fragments.nix b/web/blog/fragments.nix
new file mode 100644
index 0000000000..2c9127b7f7
--- /dev/null
+++ b/web/blog/fragments.nix
@@ -0,0 +1,81 @@
+# This file defines various fragments of the blog, such as the header
+# and footer, as functions that receive arguments to be templated into
+# them.
+#
+# An entire post is rendered by `renderPost`, which assembles the
+# fragments together in a runCommand execution.
+#
+# The post overview is rendered by 'postList'.
+{ pkgs, lib, ... }:
+
+let
+  inherit (builtins) filter map hasAttr replaceStrings toFile;
+  inherit (pkgs.third_party) runCommandNoCC writeText;
+
+  escape = replaceStrings [ "<" ">" "&" "'" ] [ "&lt;" "&gt;" "&amp;" "&#39;" ];
+
+  header = title: ''
+  <!DOCTYPE html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta name="description" content="tazjin&#39;s blog">
+    <link rel="stylesheet" type="text/css" href="static/blog.css" media="all">
+    <link rel="alternate" type="application/rss+xml" title="RSS-Feed" href="/rss.xml">
+    <title>tazjin&#39;s blog${lib.optionalString (title != "") (
+      ": " + (escape title)
+    )}</title>
+  </head>
+  <body>
+    <header>
+      <h1><a class="unstyled-link" href="/">tazjin&#39;s blog</a> </h1>
+      <hr>
+    </header>
+  '';
+
+  footer = ''
+    <hr>
+    <footer>
+      <p class="footer">
+        <a class="uncoloured-link" href="https://tazj.in">homepage</a>
+        |
+        <a class="uncoloured-link" href="https://git.tazj.in/about">code</a>
+        |
+        <a class="uncoloured-link" href="https://twitter.com/tazjin">twitter</a>
+      </p>
+      <p class="lod">ಠ_ಠ</p>
+    </footer>
+  </body>
+  '';
+
+  renderPost = post: runCommandNoCC "${post.key}.html" {} ''
+    cat ${toFile "header.html" (header post.title)} > $out
+
+    # Write the actual post
+    echo '<article><h2 class="inline">${escape post.title}</h2>' >> $out
+    echo '<aside class="date">${post.date}</aside>' >> $out
+    cat ${post.content} | ${pkgs.tools.cheddar}/bin/cheddar --about-filter ${post.content} >> $out
+    echo '</article>' >> $out
+
+    cat ${toFile "footer.html" footer} >> $out
+  '';
+
+  # Generate a post list for all listed, non-draft posts.
+  isDraft = post: (hasAttr "draft" post) && post.draft;
+  isUnlisted = post: (hasAttr "listed" post) && !post.listed;
+  includePost = post: !(isDraft post) && !(isUnlisted post);
+
+  indexEntry= post: "<li>a blog post</li>";
+  blogIndex = posts: writeText "blog-index.html" (lib.concatStrings (
+    [
+      (header "")
+      "<ul>"
+    ]
+    ++ (map indexEntry (filter includePost posts))
+    ++ [
+      "</ul>"
+      footer
+    ]));
+in {
+  inherit blogIndex renderPost;
+}
diff --git a/web/blog/nginx.nix b/web/blog/nginx.nix
new file mode 100644
index 0000000000..7c600c38c0
--- /dev/null
+++ b/web/blog/nginx.nix
@@ -0,0 +1,68 @@
+# This file creates an nginx server that serves the blog on port 8080.
+#
+# It's not intended to be the user-facing nginx.
+{ pkgs, lib, posts, renderedBlog, ... }:
+
+let
+  inherit (builtins) hasAttr filter map;
+  inherit (pkgs.third_party) writeText writeShellScriptBin nginx;
+
+  oldRedirects = lib.concatStringsSep "\n" (map (post: ''
+    location ~* ^(en)?/${post.oldKey} {
+      # TODO(tazjin): 301 once this works
+      return 302 /${post.key};
+    }
+  '') (filter (hasAttr "oldKey") posts));
+
+  config = writeText "blog-nginx.conf" ''
+    daemon off;
+    worker_processes 1;
+    error_log stderr;
+    pid /tmp/nginx-tazblog.pid;
+
+    events {
+      worker_connections  1024;
+    }
+
+    http {
+      include ${nginx}/conf/mime.types;
+      fastcgi_temp_path /tmp/nginx-tazblog;
+      uwsgi_temp_path /tmp/nginx-tazblog;
+      scgi_temp_path /tmp/nginx-tazblog;
+      client_body_temp_path /tmp/nginx-tazblog;
+      proxy_temp_path /tmp/nginx-tazblog;
+      sendfile on;
+
+      # Logging is handled by the primary nginx server
+      access_log off;
+
+      server {
+        listen 8080 default_server;
+        root ${renderedBlog};
+
+        location /static {
+          alias ${./static}/;
+        }
+
+        ${oldRedirects}
+
+        location / {
+          if ($request_uri ~ ^/(.*)\.html$) {
+            return 302 /$1;
+          }
+
+          try_files $uri $uri.html $uri/ =404;
+        }
+      }
+    }
+  '';
+in writeShellScriptBin "tazblog" ''
+  if [[ -v CONTAINER_SETUP ]]; then
+    cd /run
+    echo 'nogroup:x:30000:nobody' >> /etc/group
+    echo 'nobody:x:30000:30000:nobody:/tmp:/bin/bash' >> /etc/passwd
+  fi
+
+  mkdir -p /tmp/nginx-tazblog
+  exec ${pkgs.third_party.nginx}/bin/nginx -c ${config}
+''
diff --git a/web/blog/static/blog.css b/web/blog/static/blog.css
new file mode 100644
index 0000000000..e6e4ae3c2b
--- /dev/null
+++ b/web/blog/static/blog.css
@@ -0,0 +1,35 @@
+body {
+    margin: 40px auto;
+    max-width: 650px;
+    line-height: 1.6;
+    font-size: 18px;
+    color: #383838;
+    padding: 0 10px
+}
+h1, h2, h3 {
+    line-height: 1.2
+}
+.footer {
+    text-align: right;
+}
+.lod {
+    text-align: center;
+}
+.unstyled-link {
+    color: inherit;
+    text-decoration: none;
+}
+.uncoloured-link {
+    color: inherit;
+}
+.date {
+    text-align: right;
+    font-style: italic;
+    float: right;
+}
+.inline {
+    display: inline;
+}
+.navigation {
+    text-align: center;
+}