diff --git a/.gitignore b/.gitignore
index 9bb63229ff531347d03e5e256210d6f2065f61dd..36bea9ee70a3ad1bbaa79dd00cedcdefe380b831 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ build
 build-debug
 content
 static
+.buildconfig
diff --git a/index.js b/index.js
index fa36619b60748a2cdb04125932eb4b75e97d38db..243f658518836c063f02bf6a8d1bc4de712af310 100755
--- a/index.js
+++ b/index.js
@@ -1,178 +1,136 @@
 #!/usr/bin/env node
-var sitegin = require('./sitegin/sitegin');
-
-var input = new sitegin.input.Hugo(process.cwd()+'/content/');
-var transformerMD = new sitegin.transformer.Markdown;
-var transformerTags = new sitegin.transformer.Tags;
-var transformerNunjucks = new sitegin.transformer.Nunjucks;
-var transformerUrlizeTags = new sitegin.transformer.UrlizeTags;
-var output = new sitegin.output.Filesystem;
-var sequencer = new sitegin.Sequencer;
-var transformerGit = new sitegin.transformer.Git;
-var transformerOther = new sitegin.transformer.Other;
-var transformerImages = new sitegin.transformer.Images;
-
-var sass = require('node-sass');
-var fs = require('fs');
-
-var builddir = "build";
-if(process.argv[2] !== "compileonly") builddir = "build-debug";
-
-var oldEmit = sequencer.emit;
-sequencer.emit = function() {
-  console.log("Event:",arguments[0]);
-  oldEmit.apply(sequencer, arguments);
-}
+var moment = require('moment');
+var cli = require('cli');
+var config = require('./sitegin/config');
+
+cli.main(function(args, options) {
+  console.log(options);
+  config()
+  .then(function(obj) {
+    sitegin(obj.config);
+  })
+  .catch(function(e) {
+    console.log(e.stack);
+    process.exit();
+  });
+});
 
-sequencer.on("", function(ev) {
-  console.log("Event:",ev);
-})
+var sitegin = function(config) {
+  var options = config.options;
+  require('./sitegin/sitegin')({
+    watch: !options.noserver
+  })
+  .then(function onLoad(jobs) {
+    var sass = require('node-sass');
+    var fs = require('fs');
+
+    // Main builder function
+    // If builder is not running - run it
+    // If it is running - schedule rerun after it finishes
+    var isRunning = false;
+    var runAgain = false;
+    var doSync = function(){}
+    function run() {
+      var startTime = moment();
+      runAgain = false;
+      if(isRunning) {
+        console.log("Generator is still running. Queing another run after it finishes.")
+        runAgain = true;
+        return;
+      }
+      console.log("================================================================================")
+      console.log("Running generator")
+      isRunning = true;
+      jobs.run('pipeline',jobs)
+      .then(function() {
+        isRunning = false;
+        console.log("Generator finished in",moment().diff(startTime,'seconds'),'seconds')
+        doSync();
+        if(runAgain) run();
+      })
+      .catch(function(e) {
+        isRunning = false;
+        console.log("Generator crashed in",moment().diff(startTime,'seconds'),'seconds')
+        if(runAgain) run();
+        if(e.stack)
+          console.log(e.stack)
+        else
+          console.log(e)
+      });
+    }
 
-var tasks = [];
-var onDone = function(what) {
-  console.log(what, 'done');
-  var removed = false;
-  for(var i = 0; i < tasks.length; i++) {
-    if(tasks[i] == what) {
-      removed = true;
-      tasks.splice(i,1);
-      break;
+    console.log('Sitegin successfully loaded');
+    run();
+    copyStaticFiles(config.builddir);
+    rendersass(config.builddir);
+
+    if(!options.noserver) {
+      var sync = require('browser-sync').create();
+      doSync = function() {
+        sync.reload('*')
+      }
+      sync.init({
+        server: {
+          baseDir: config.builddir
+        },
+        ui: {
+          port: options.port+1
+        },
+        port: options.port
+      });
+
+      var chokidar = require('chokidar');
+      // article or theme reload
+      chokidar.watch(['content/','theme/'], {ignoreInitial: true})
+      .on('all', function(event, path) {
+        console.log('Content or theme changed. Rebuilding...');
+        run();
+      })
+
+      // sitegin reload
+      jobs.onReload(function() {
+        console.log("Sitegin reloaded");
+        run();
+      })
     }
-  }
-  if(!removed && process.argv[2] == "compileonly") throw new Error("Task "+what+" ended but was never registered");
-  if(tasks.length == 0 && process.argv[2] == "compileonly") {
-    console.log("exitting");
+  })
+  .catch(function(e) {
+    console.log(e.stack);
     process.exit();
-  }
+  });
 }
 
+var copyStaticFiles = function(builddir) {
+  // STATIC FILES
+  var fsextra = require('node-fs-extra');
+  fsextra.copy(
+      'static',
+      builddir,
+      function(file){ return !(file.match(".git") || file.match("static/articles")); },
+      function(){console.log("copy static done");}
+    );
+  fsextra.copy(
+      'static/articles',
+      builddir+"/clanek",
+      function(){console.log("copy static/articles done");}
+    );
+  fsextra.copy(
+      'theme/static',
+      builddir,
+      function(file){ return !file.match("\\.git") },
+      function(){console.log("copy theme/static done");}
+    );
+}
 
-// PIPELINE
-sequencer
-.load(input)
-.registerNext(transformerGit)
-.registerNext(transformerOther)
-.registerNext(transformerImages)
-.registerNext(transformerMD)
-.registerNext(transformerUrlizeTags)
-.registerNext(transformerTags)
-.registerNext(transformerNunjucks)
-.registerNext(output)
-.finish(function(list) {
-  onDone("sequencer");
-})
-tasks.push('sequencer');
-
-try {fs.mkdirSync(builddir);}catch(e){}
-try {fs.mkdirSync(builddir+"/theme");}catch(e){}
+var rendersass = function(builddir) {
+  // SASS
+  var sass = require('node-sass');
+  var fs = require('fs');
 
-// SASS
-var rendersass = function() {
   sass.render ({file: "theme/sass/style.scss"},function(err, result) {
     if(err == null) {
       console.log("compiled sass");
       fs.writeFile(builddir+"/theme/style.css",result.css);
     } else console.log("error ", err);
-    onDone("sass");
   });
 };
-tasks.push('sass');
-
-rendersass();
-
-// STATIC FILES
-var fsextra = require('node-fs-extra');
-tasks.push('copy static');
-fsextra.copy(
-    'static',
-    builddir,
-    function(file){ return !(file.match(".git") || file.match("static/articles")); },
-    function(){onDone("copy static");}
-  );
-tasks.push('copy static/articles');
-fsextra.copy(
-    'static/articles',
-    builddir+"/clanek",
-    function(){onDone("copy static/articles");}
-  );
-tasks.push('copy theme static');
-fsextra.copy(
-    'theme/static',
-    builddir,
-    function(file){ return !file.match("\\.git") },
-    function(){onDone("copy theme static");}
-  );
-
-// DEVEL INCOMING
-if(process.argv[2] !== "compileonly") {
-  var sync = require('browser-sync').create();
-  sync.init({
-    server: {
-      baseDir: builddir
-    },
-    ui: {
-      port: 1338
-    },
-    port: 1337
-  });
-
-  // CHANGE WATCHER
-  var chokidar = require('chokidar');
-
-  chokidar.watch('theme/static', {ignoreInitial: true, usePolling: false})
-  .on('all', (event, path) => {
-    console.log(path);
-    console.log("theme static copy watcher");
-    fsextra.copy(
-      'theme/static',
-      builddir,
-      function(file){ return !file.match(".git"); },
-      function(){}
-      );
-    sync.reload(path);
-  });
-  
-  chokidar.watch('static/', {ignoreInitial: true, usePolling: false})
-  .on('all', (event, path) => {
-    if(path.match(/^static\/articles\/.*$/)) return;
-    console.log("static copy watcher");
-    fsextra.copy(
-      'static',
-      builddir,
-      function(file){ return !(file.match(".git") || file.match("static/articles")); },
-      function(){}
-    );
-    sync.reload(path);
-  })
-  .unwatch('static/articles');
-  
-  chokidar.watch('static/articles', {ignoreInitial: true, usePolling: false})
-  .on('all', (event, path) => {
-    console.log("static articles copy watcher");
-    fsextra.copy('static/articles',builddir+"/clanek",function(){});
-    sync.reload(path);
-  })
-  .unwatch('static/articles');
-
-  chokidar.watch(['theme/','content/'], {ignoreInitial: true, usePolling: false})
-  .on('all', (event, path) => {
-    if(path.match(/^theme\/static\/.*$/)) return;
-    if(path.match(/^theme\/sass\/.*$/)) return;
-    console.log("rebuild watcher",path);
-    sequencer.load(input);
-  })
-  .unwatch("theme/static")
-  .unwatch("theme/sass");
-
-  sequencer.finish(function() {
-    sync.reload("*.html");
-  });
-
-  chokidar.watch('theme/sass', {ignoreInitial: true, usePolling: false})
-  .on('all', function(event, path) {
-    console.log("sass: file change detected - rerendering "+event+" "+path);
-    rendersass();
-    sync.reload(path);
-  });
-}
-
diff --git a/package.json b/package.json
index 73a647931908bd034f1dc71f593d0610eac6be94..5be70aa4c762322f93b7460ed097acc2bef0c80f 100644
--- a/package.json
+++ b/package.json
@@ -7,11 +7,13 @@
     "test": "./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha -- sitegin/test/ --recursive -R spec -u exports -t 60000"
   },
   "author": "",
-  "license": "BSD",
+  "license": "UNSPECIFIED",
   "dependencies": {
     "browser-sync": "^2.11.1",
     "chokidar": "^1.4.3",
+    "cli": "^0.11.2",
     "highlight.js": "^9.2.0",
+    "jsesc": "^1.0.0",
     "marked": "^0.3.5",
     "mkdirp": "^0.5.1",
     "moment": "^2.12.0",
@@ -21,8 +23,10 @@
     "nodegit-kit": "^0.8.1",
     "nunjucks": "^2.3.0",
     "nunjucks-date-filter": "^0.1.1",
+    "syntax-error": "^1.1.5",
     "toml": "^2.3.0",
-    "toml-js": "0.0.8"
+    "toml-js": "0.0.8",
+    "walk": "^2.3.9"
   },
   "devDependencies": {
     "istanbul": "^0.4.2",
diff --git a/sitegin/Page.js b/sitegin/Page.js
deleted file mode 100644
index 42e3f2899cf78586237a1ac5f09764db29927319..0000000000000000000000000000000000000000
--- a/sitegin/Page.js
+++ /dev/null
@@ -1,52 +0,0 @@
-
-exports.Page = function() {
-  this.file = "";
-  this.metadata = {};
-  this.content = "";
-  this.type = "article";
-}
-
-exports.PageList = function() {
-  var list = this;
-  list.list = [];
-
-  list.length = 0;
-
-  list.addPage = function(article) {
-    list.length++;
-    if(! (article instanceof exports.Page))
-      throw "article has to be of type Page";
-    else if(list.containsFile(article.file)) {
-      console.log(list.getPageByFile(article.file));
-      throw "article is already in this list"
-    } else
-      list.list.push(article);
-  }
-
-  list.forEach = function(f) {
-    list.list.forEach(f);
-  }
-
-  list.getPageByFile = function(file) {
-    var rarticle = undefined;
-    list.forEach(function(article) {
-      if(article.file == file) {
-        rarticle = article;
-      }
-    });
-    return rarticle;
-  }
-
-  list.containsFile = function(file) {
-    var contains = false;
-    list.forEach(function(article) {
-      if(article.file == file) {
-        contains = true;
-      }
-    });
-    return contains;
-  }
-
-  return list;
-}
-
diff --git a/sitegin/config.js b/sitegin/config.js
new file mode 100644
index 0000000000000000000000000000000000000000..02378ba8a51e34e13d4f5a859f22f01aa778ccfc
--- /dev/null
+++ b/sitegin/config.js
@@ -0,0 +1,33 @@
+/*
+ * This file specifies whole configuration of sitegin.
+ * In future it might actually read configuration files but for now - if you
+ * want to configure sitegin you have to modify this file.
+ * (or write config reader which would replace this file - I'd appreciate
+ * merge request for this feature ;) )
+ */
+var cli = require('cli');
+var options = cli.parse({
+ noserver: ['n', 'Dont run server'],
+ port: ['p', 'Port on which server should run', 'number', 1337],
+ uiport: [null, 'BrowserSync UI port', 'number', 'port+1']
+});
+
+module.exports = function() {
+  var builddir = "build";
+  if(!options.noserver) builddir = "build-debug";
+
+  if(options.uiport == 'port+1') {
+    options.uiport = options.port+1;
+  }
+
+  return Promise.resolve({
+    config: {
+      options: options,
+      builddir: builddir,
+      sourceDir: 'content',
+      articlesLocation: 'articles',
+      linksPerPage: 6
+    }
+  })
+}
+module.exports.watch = !options.noserver;
diff --git a/sitegin/transformer-git.js b/sitegin/gitInfo.js
similarity index 53%
rename from sitegin/transformer-git.js
rename to sitegin/gitInfo.js
index bb0dc377cde042a26bb8fe8754956016518e21f2..076fce51bd1856db1d422c97d85eafef7b0d4fa6 100644
--- a/sitegin/transformer-git.js
+++ b/sitegin/gitInfo.js
@@ -1,4 +1,8 @@
-// Add git commit author and date to metadata
+/*
+ * Add date: {creation, modification, modified} and author: {name, email}
+ * to metadata if not already present
+ */
+
 var nodegit = require('nodegit');
 var git = require('nodegit-kit');
 var path = require('path');
@@ -73,51 +77,38 @@ var getFilesHistory = function() {
   });
 }
 
-module.exports = function() {
-  var tr = this;
-  tr.fancyname = "transformer git";
-
-  tr.pre = function(cb, articleList) {
-    getFilesHistory()
-    .then((filesHistory) => {
-      var todo = articleList.length;
-      articleList.forEach(function(article) {
-        var file = path.relative(pathToRepo, article.origFile);
-        var commits = filesHistory[file];
-        if(commits !== undefined) {
-          commits.sort(function(a,b){
-            return b.date - a.date;
-          });
-          var newestCommit = commits[0];
-          var oldestCommit = commits[commits.length-1];
+module.exports = function(obj) {
+  return getFilesHistory()
+  .then((filesHistory) => {
+    obj.pages.forEach(function(article) {
+      var file = path.relative(pathToRepo, article.filename);
+      var commits = filesHistory[file];
+      if(commits !== undefined) {
+        commits.sort(function(a,b){
+          return b.date - a.date;
+        });
+        var newestCommit = commits[0];
+        var oldestCommit = commits[commits.length-1];
 
-          if(article.metadata.date == undefined)
-            article.metadata.date = {};
+        if(article.metadata.date == undefined)
+          article.metadata.date = {};
 
-          if(article.metadata.date.creation == undefined)
-            article.metadata.date.creation = oldestCommit.date;
+        if(article.metadata.date.creation == undefined)
+          article.metadata.date.creation = oldestCommit.date;
 
-          article.metadata.date.modification = newestCommit.date;
-          article.metadata.date.modified = (newestCommit !== oldestCommit);
+        article.metadata.date.modification = newestCommit.date;
+        article.metadata.date.modified = (newestCommit !== oldestCommit);
 
-          if(article.metadata.author === undefined)
-            article.metadata.author = {}
-          if(article.metadata.author.name == undefined)
-            article.metadata.author.name = oldestCommit.author.name
-          if(article.metadata.author.email == undefined)
-            article.metadata.author.email = oldestCommit.author.email
+        if(article.metadata.author === undefined)
+          article.metadata.author = {}
+        if(article.metadata.author.name == undefined)
+          article.metadata.author.name = oldestCommit.author.name
+        if(article.metadata.author.email == undefined)
+          article.metadata.author.email = oldestCommit.author.email
 
-          article.commits = commits;
-        }
-
-        todo--;
-        if(todo == 0) cb();
-      });
-    })
-    .catch(function(err) {console.log(err.stack);});
-  }
-
-  tr.forEachPage = function(article, cb) {
-    cb();
-  }
+        article.commits = commits;
+      }
+    });
+    return obj;
+  })
 }
diff --git a/sitegin/input-hugo.js b/sitegin/input-hugo.js
deleted file mode 100644
index a1725dcd23570e2c08084c28d87634b26a62e0b3..0000000000000000000000000000000000000000
--- a/sitegin/input-hugo.js
+++ /dev/null
@@ -1,130 +0,0 @@
-var fs = require('fs');
-var toml = require('toml');
-var PageList = require('./Page').PageList;
-var Page = require('./Page').Page;
-var moment = require('moment');
-var path = require('path');
-
-function walk(currentDirPath, callback, end_cb) {
-  var subcalls = 0;
-  var ender = function() {
-    subcalls--;
-    if(subcalls <= 0) {
-      if(end_cb !== undefined)
-        end_cb();
-    }
-  }
-  var fs = require('fs'), path = require('path');
-  fs.readdir(currentDirPath, function(err, files) {
-    if(err!=null) console.log(err);
-    files.forEach(function(name) {
-      var filePath = path.join(currentDirPath, name);
-      try {
-        var stat = fs.statSync(filePath);
-        if (stat.isFile()) {
-          callback(filePath, stat);
-        } else if (stat.isDirectory()) {
-          if(filePath.indexOf(".git") < 0) {
-            subcalls++;
-            walk(filePath, callback, ender);
-          }
-        }
-      } catch(e) {}
-    });
-    if(subcalls == 0) ender();
-    
-  });
-};
-
-var readPageWorker = function(file, articleName, contentDir) {
-  if(file === undefined) throw new Error("file is undefined");
-  file = file.substring(4);
-  var fm_end = file.indexOf("+++");
-  var obj = new Page;
-  obj.origFile = contentDir+articleName+'.md';
-  obj.file = articleName.replace("articles","clanek");
-  obj.content = file.substring(fm_end+4);
-  try {
-    obj.metadata = toml.parse(file.substring(0,fm_end-1)+"\n");
-  } catch(e) {
-    console.log(articleName+": Failed to parse metadata");
-    throw e;
-  }
-  if(obj.metadata.date && obj.metadata.date.creation) obj.metadata.date.creation = moment(obj.metadata.date.creation);
-  return obj;
-}
-module.exports = function(_contentDir) {
-  var exports = this;
-  exports.walk = walk;
-  exports.fancyname = "loader hugo";
-  var contentDir = _contentDir;
-  if(contentDir == undefined) contentDir = './content/';
-  if(!contentDir.endsWith('/')) contentDir+='/';
-
-  /*
-   * articleName:
-   *   string
-   * callback:
-   *   called when article is read
-   * returns:
-   *   {
-   *     string articleName,content
-   *     object frontmatter
-   *     function save
-   *   }
-   */
-  exports.readPage = function(articleName, cb) {
-    fs.readFile(contentDir+articleName+".md", 'utf8', function(err, data) {
-      if(err != null) throw err;
-      cb(readPageWorker(data, articleName, contentDir));
-    });
-  }
-
-  /*
-   * callback:
-   *     function(articleName)
-   *     called for each article on website
-   * cb_end:
-   *     function()
-   *     called when all articles are listed
-   */
-  exports.forEachPage = function(callback, cb_end) {
-    walk(contentDir+"articles",function(filePath, stat) {
-      console.log(filePath, path.basename(filePath));
-      if(path.basename(filePath).substr(0,1) == ".") return;
-      if(callback !== undefined) callback(filePath.substring(contentDir.length,filePath.length-3));
-    }, function() {
-      if(cb_end !== undefined) cb_end();
-    });
-  };
-
-  /*
-   * cb:
-   *    function(articleList)
-   *    called after all cb_on_each
-   */
-  exports.getPageList = function(cb) {
-    var currentlyReadingCount = 0;
-    var allPagesListed = false;
-    var cb_called = false;
-    var list = new PageList;
-
-    exports.forEachPage(function(articleName) {
-      currentlyReadingCount++;
-      exports.readPage(articleName, function(article){
-        list.addPage(article);
-        currentlyReadingCount--;
-        if(allPagesListed && currentlyReadingCount == 0 && !cb_called) {
-          if(cb !== undefined) cb(list);
-          cb_called = true;
-        }
-      });
-    },function(){
-      if(currentlyReadingCount == 0 && !cb_called) {
-        if(cb !== undefined) cb(list);
-        cb_called = true;
-      }
-      allPagesListed = true;
-    });
-  };
-}
diff --git a/sitegin/jobs.js b/sitegin/jobs.js
new file mode 100644
index 0000000000000000000000000000000000000000..bf4ed3e409398a7f185c5fbbc1bfbb6d058115e0
--- /dev/null
+++ b/sitegin/jobs.js
@@ -0,0 +1,156 @@
+var syntaxError = require('syntax-error');
+var fs = require('fs');
+var chokidar = require('chokidar');
+var eventEmitter = new (require('events').EventEmitter);
+
+var jobList = {};
+var watchers = [];
+
+function JobError(message, name) {
+  Error.captureStackTrace(this, JobError);
+  this.name = name+'Error';
+  this.message = message;
+  return this;
+}
+
+var onReload = function() {}
+var requireError = function(e, jobName, module, reject) {
+  if(e instanceof SyntaxError) {
+    var resolved = require.resolve(module);
+    fs.readFile(resolved, 'utf8', function(err, content) {
+      console.log(syntaxError(content,resolved));
+      reject(new JobError('Failed to register job '+jobName,'Require'))
+    })
+  } else {
+    if(e.code === 'MODULE_NOT_FOUND')
+    console.log(e.toString());
+    reject(new JobError('Failed to register job '+jobName,'Require'))
+  }
+}
+
+var jobs = {
+  register: function(jobName, module, watch) {
+    return new Promise(function(resolve, reject) {
+      if(watch === undefined) watch = true;
+      if(jobList[jobName] !== undefined) {
+        return Promise.reject(
+          new JobError('Job '+jobName+' is already registered', 'JobAlreadyRegistered')
+        )
+      }
+
+      var f;
+      try {
+        f = require(module);
+      } catch(e) {
+        requireError(e, jobName, module, reject);
+        return;
+      }
+
+      if(typeof f !== 'function')
+        throw new JobError('Module "'+module+'" for job "'+jobName+'" does not export function');
+      jobList[jobName] = {
+        f: f,
+        module: module
+      };
+      if(watch) jobs.watch(jobName);
+      resolve();
+
+    });
+  },
+  reload: function(jobName) {
+    var module = jobList[jobName].module;
+    delete require.cache[require.resolve(module)];
+
+    return new Promise(function(resolve, reject) {
+      try {
+        var f = require(module);
+        if(typeof f === 'function') {
+          var job = {
+            f: f,
+            module: module
+          };
+          delete jobList[jobName];
+          jobList[jobName] = job;
+          resolve();
+        } else {
+          console.log('Error reloading job '+jobName+' retry in 500ms');
+          setTimeout(function() {
+            resolve(jobs.reload(jobName))
+          },500)
+        }
+      } catch(e) {
+        console.log(e);
+        requireError(e, jobName, module, reject);
+      }
+    });
+  },
+  // registerMultiple(
+  //  {watch: true},
+  //  ['jobName','./module'],['jobName2','./module2']
+  // )
+  registerMultiple: function() {
+    var regs = [];
+    var watch = true;
+    for(i in arguments) {
+      var job = arguments[i];
+      if(i == 0) {
+        if(job.watch !== undefined) watch = job.watch;
+      } else {
+        regs.push(jobs.register(job[0], job[1], watch));
+      }
+    }
+    return Promise.all(regs);
+  },
+  run: function() {
+    var jobName = arguments[0];
+    if(jobList[jobName] === undefined) {
+      throw new JobError('Job '+jobName+' is not registered', 'JobNotRegistered')
+    }
+    try {
+      return jobList[jobName].f.apply(null, Array.prototype.slice.call(arguments, 1));
+    } catch(e) {
+      console.log("Error running job",jobName);
+      console.log(jobList[jobName]);
+      throw e;
+    }
+  },
+  runSequence: function() {
+    var prom;
+    console.log("Running",Array.prototype.slice.call(arguments, 0))
+    var jobSequence = [].splice.call(arguments, 0);
+    jobSequence.forEach(function(job){
+      if(prom === undefined) {
+        prom = jobs.run(job);
+      } else {
+        prom = prom.then(function() {
+          var args = [].splice.call(arguments, 0);
+          args.splice(0,0,job);
+          return jobs.run.apply(null,args)
+        })
+      }
+    });
+    return prom;
+  },
+  watch: function(jobName) {
+    var module = jobList[jobName].module;
+    var file = require.resolve(module);
+    var w = chokidar.watch(file);
+    w.on('change', path => {
+      jobs.reload(jobName)
+      .then(onReload);
+    });
+    watchers.push(w);
+  },
+  onReload: function(f) {
+    onReload = f;
+  },
+  close: function() {
+    watchers.forEach(function(w) {
+      w.close();
+      console.log(w.getWatched());
+    });
+    watchers = [];
+  }
+}
+
+module.exports = jobs;
diff --git a/sitegin/transformer-markdown.js b/sitegin/markdown.js
similarity index 61%
rename from sitegin/transformer-markdown.js
rename to sitegin/markdown.js
index e6bb54040ec45cb1ab64170433f9751cd8354ecc..81b2bd272680d24a5271084680d13d2407ce2f41 100644
--- a/sitegin/transformer-markdown.js
+++ b/sitegin/markdown.js
@@ -1,22 +1,21 @@
+/*
+ * Transforms pages[0..length].content from markdown to HTML
+ */
+
 var marked = require('marked');
 var renderer = new marked.Renderer();
-var toURL = require('./transformer-urlizetags.js').toURL;
 var highlightjs = require('highlight.js');
 var path = require('path');
-var minificationOK = true;
-try {require('lwip');}
-catch(e) {
-  minificationOK = false;
-}
-if(process.argv[2] !== "compileonly") minificationOK = false;
+var jobs = require('./jobs');
 
-var images = {};
+var toURL = function(url) {
+  return jobs.run('toURL', url);
+}
 
 renderer.heading = function(text, level, raw) {
   return '<h'
     + (level+2)
     + ' id="'
-    + this.options.headerPrefix
     + toURL(raw)
     + '">'
     + text
@@ -41,41 +40,14 @@ marked.setOptions({
   smartypants: false,
 });
 
-module.exports = function() {
-  var tr = this;
-  tr.fancyname = "transformer markdown";
-  tr.pre = function(cb, list) {
-    if(minificationOK) {
-      list.images.forEach((img) => {
-        images[img.file] = img;
-      });
-    }
-    cb();
-  };
-  tr.forEachPage = function(article, cb) {
-    article.origContent = article.content;
+module.exports = function(obj) {
+  obj.pages.forEach(function(article) {
     renderer.image = function(href, title, text) {
       href = href.split(path.sep).join("/");
       href = href.split(" =");
       var size = href[1];
       href = href[0];
-      var rel = path.relative(process.cwd(),path.resolve(article.file,href));
-      rel = rel.split(path.sep).join("/");
-      var img = images[rel];
-      var full;
-      if(img) {
-        if(img.minified) {
-          full = href;
-          href = path.relative(article.file, img.thumb);
-        }else if(!minificationOK) {
-          full = href;
-        }
-      }
-      else if(process.argv[3] !== "final") {
-        if(!href.match("://")) {
-          href = "https://ok1kvk.cz"+href;
-        }
-      }
+      var rel = path.relative(process.cwd(),path.resolve(article.filename,href));
 
       var out = '<img src="' + href + '" alt="' + text + '"';
       if(size !== undefined) {
@@ -86,15 +58,14 @@ module.exports = function() {
         if(size.length > 1 && size[1].length > 0) {
           out += ' height="' + size[1] + '"';
         }
-      } else if(!minificationOK) {
-        out += ' style="max-width:512px"';
       }
       if (title) {
         out += ' title="' + title + '"';
       }
-      
+
       out += '>';
-      if(full) {
+      if(true) { // if lightbox
+        var full = href;
         var a = '<a href="'+full+'"';
         a += ' data-lightbox="group"';
         a += ' data-title="' + text + '"';
@@ -116,6 +87,6 @@ module.exports = function() {
       },
       renderer: renderer
     });
-    cb();
-  }
+  });
+  return Promise.resolve(obj);
 }
diff --git a/sitegin/nunjucks.js b/sitegin/nunjucks.js
new file mode 100644
index 0000000000000000000000000000000000000000..aa87c04145ec7fa8971ff4ebf5fdfde20ca9ca46
--- /dev/null
+++ b/sitegin/nunjucks.js
@@ -0,0 +1,57 @@
+/*
+ * This job renders `data` using nunjucks template `type`
+ */
+
+var nunjucks = require('nunjucks');
+var fs = require('fs');
+var dateFilter = require('nunjucks-date-filter');
+var util = require('util');
+
+var watch = require('./config').watch;
+console.log('watch: '+watch);
+var env = new nunjucks.Environment(
+  new nunjucks.FileSystemLoader('theme/templates',{watch: watch})
+);
+
+env.addFilter('date', dateFilter);
+
+env.addFilter('paginationList', function(page, count) {
+    var curPage = page;
+    var pages = [];
+    pages.push(curPage);
+    for(var i = 0; i < count; i++) {
+      curPage = curPage.metadata.prevpage;
+      if(curPage === undefined) break;
+      pages.unshift(curPage);
+    }
+
+    curPage = page;
+    for(var i = 0; i < count; i++) {
+      curPage = curPage.metadata.nextpage;
+      if(curPage === undefined) break;
+      pages.push(curPage);
+    }
+    return pages;
+});
+
+env.addFilter('inspect', function(obj) {
+    return util.inspect(obj);
+});
+
+env.addFilter('relURL', function(filename, dir){
+  if(filename.substr(0,1) == '/' || filename.match('://')) return filename;
+  if(dir.substr(0,1) !== '/') dir = '/' + dir;
+  if(filename.match(/\/$/)) return dir+filename;
+  return dir+'/'+filename;
+})
+
+module.exports = function(data, type) {
+  return new Promise(function(resolve, reject) {
+    env.getTemplate(type+'.html.nunj', true, function(err, tmpl) {
+      if(err) reject(new Error('Failed to load template '+type+'.html.nunj'));
+      resolve(tmpl);
+    });
+  }).then(function(tmpl) {
+    return tmpl.render(data);
+  });
+}
diff --git a/sitegin/output-console.js b/sitegin/output-console.js
deleted file mode 100644
index b9074e197f3a216af4ce99d5c0802e9af2b648b3..0000000000000000000000000000000000000000
--- a/sitegin/output-console.js
+++ /dev/null
@@ -1,9 +0,0 @@
-
-module.exports = function() {
-  var out = this;
-  out.fancyname = "output console";
-  out.forEachPage = function(article, cb) {
-    cb();
-  }
-}
-
diff --git a/sitegin/output-filesystem.js b/sitegin/output-filesystem.js
deleted file mode 100644
index fe92cc5083ea6dae232592a4d6dbe76268f6caa9..0000000000000000000000000000000000000000
--- a/sitegin/output-filesystem.js
+++ /dev/null
@@ -1,21 +0,0 @@
-
-var fs = require('fs');
-var path = require('path');
-var mkdirp = require('mkdirp');
-var builddir = "build";
-if(process.argv[2] !== "compileonly") builddir = "build-debug";
-
-module.exports = function() {
-  var out = this;
-  out.fancyname = "output filesystem";
-  out.forEachPage = function(article, cb) {
-    mkdirp(path.dirname(builddir+'/'+article.file+"/index.html"), () => {
-      var file = builddir+'/'+article.file+"/index.html";
-      fs.writeFile(file, article.content, function(err){
-        if(err) throw err;
-        cb();
-      });
-    });
-  }
-}
-
diff --git a/sitegin/parseHugo.js b/sitegin/parseHugo.js
new file mode 100644
index 0000000000000000000000000000000000000000..43af577af8a6a926e919e45a19320225dcfa0387
--- /dev/null
+++ b/sitegin/parseHugo.js
@@ -0,0 +1,39 @@
+var toml = require('toml');
+var moment = require('moment');
+var path = require('path');
+var jsesc = require('jsesc')
+
+var readPageWorker = function(content, obj) {
+  if(content === undefined) throw new Error("content is undefined");
+  var head = content.substring(0,4)
+  if(head !== '+++\n')
+    throw new Error('Failed to parse file '+obj.filename+':\n'+
+                    'Wrong header (expected "+++\\n" got "'+jsesc(head)+'")')
+
+  content = content.substring(4);
+  var fm_end = content.indexOf("+++\n");
+  if(fm_end < 0) {
+    console.log(jsesc(content));
+    throw new Error('Cannot find terminating +++\n in file '+obj.filename);
+  }
+  obj.content = content.substring(fm_end+4);
+  try {
+    obj.metadata = toml.parse(content.substring(0,fm_end-1)+"\n");
+  } catch(e) {
+    console.log(obj.filename+": Failed to parse metadata");
+    throw e;
+  }
+  if(obj.metadata.date && obj.metadata.date.creation) obj.metadata.date.creation = moment(obj.metadata.date.creation);
+  return obj;
+}
+
+module.exports = function(obj) {
+  return new Promise(function(resolve, reject) {
+    var pages = [];
+    obj.pages.forEach(function(page) {
+      pages.push(readPageWorker(page.content, page));
+    })
+    obj.pages = pages;
+    resolve(obj);
+  });
+}
diff --git a/sitegin/pipeline.js b/sitegin/pipeline.js
new file mode 100644
index 0000000000000000000000000000000000000000..7bc89858899261bf76f44fb94436aea9f0e0499c
--- /dev/null
+++ b/sitegin/pipeline.js
@@ -0,0 +1,18 @@
+/*
+ * This jobs just runs all job in the right order
+ */
+
+module.exports = function(jobs) {
+  return jobs.runSequence(
+    'config',
+    'readFiles',
+    'parseHugo',
+    'gitInfo',
+    'urls',
+    'markdown',
+    'tags',
+    'theme',
+    'writeFiles',
+    'print'
+  );
+}
diff --git a/sitegin/print.js b/sitegin/print.js
new file mode 100644
index 0000000000000000000000000000000000000000..01fda30c8e409d84e6489bc57b7d49a826284303
--- /dev/null
+++ b/sitegin/print.js
@@ -0,0 +1,29 @@
+var util = require('util')
+
+module.exports = function(a) {
+  var save = {};
+  var savec = {};
+  var props = [];
+  var shortenProp = function(prop) {
+    if(a[prop]) {
+      save[prop] = a[prop];
+      a[prop] = [a[prop][0]];
+      savec[prop] = a[prop][0].content;
+      a[prop][0].content = '...';
+      props.push(prop);
+    }
+  }
+
+  shortenProp('pages')
+  shortenProp('tags')
+  shortenProp('redirects')
+
+  console.log(util.inspect(a,null,2))
+
+  props.forEach(function(prop){
+    a[prop] = save[prop];
+    a[prop][0].content = savec[prop];
+  });
+
+  return Promise.resolve(a)
+}
diff --git a/sitegin/readFiles.js b/sitegin/readFiles.js
new file mode 100644
index 0000000000000000000000000000000000000000..83fa9a5ae560c0e317ecd9ba1a637825cc0af354
--- /dev/null
+++ b/sitegin/readFiles.js
@@ -0,0 +1,38 @@
+/*
+ * This job reads files from config.articlesLocation and loads this list to `pages`
+ */
+var walk = require('walk');
+var fs = require('fs');
+var path = require('path');
+
+module.exports = function(obj) {
+  return new Promise(function(resolve, reject) {
+    var pages = [];
+    walk.walk(path.join(obj.config.sourceDir,obj.config.articlesLocation))
+    .on('file',function(root, fileStats, next) {
+      var filename = path.join(root,fileStats.name);
+      var file = path.relative(obj.config.sourceDir, filename);
+      fs.readFile(filename, 'utf8', function(err,data) {
+        if(err) {
+          console.log('Error reading file '+filename)
+          console.log(err);
+        } else {
+          pages.push({
+            filename: filename,
+            file: file,
+            content: data
+          })
+        }
+        next()
+      })
+    })
+    .on('errors',function(root, nodeStatsArray, next) {
+      console.log('Walker error', root, nodeStatsArray);
+      next();
+    })
+    .on('end', function() {
+      obj.pages = pages;
+      resolve(obj);
+    })
+  })
+}
diff --git a/sitegin/sequencer.js b/sitegin/sequencer.js
deleted file mode 100644
index b5c271da37c4c84bf18a83dd91e3e89ed668893a..0000000000000000000000000000000000000000
--- a/sitegin/sequencer.js
+++ /dev/null
@@ -1,59 +0,0 @@
-module.exports = function() {
-  var t = this;
-  var lastEvent;
-  this.load = function(input) {
-    lastEvent = input.fancyname+" done";
-    input.getPageList(function(list) {
-      t.emit(input.fancyname+" done", list);
-    });
-    return this;
-  }
-
-  this.registerNext = function(obj) {
-    // pre
-    t.on(lastEvent, function(list) {
-      if(obj.pre !== undefined) {
-        obj.pre(function() {
-          t.emit(obj.fancyname+' pre done', list);
-        }, list);
-      } else {
-        t.emit(obj.fancyname+' pre done', list);
-      }
-    });
-    // forEachPage
-    t.on(obj.fancyname+' pre done', function(list) {
-      var transforming = list.length;
-      if(obj.forEachPage !== undefined) {
-        list.forEach(function(article) {
-          obj.forEachPage(article, function() {
-            transforming--;
-            if(transforming == 0) {
-              t.emit(obj.fancyname+' forEachPage done', list);
-            }
-          });
-        });
-      } else {
-        t.emit(obj.fancyname+' forEachPage done', list);
-      }
-    });
-    // post
-    t.on(obj.fancyname+' forEachPage done', function(list) {
-      if(obj.post !== undefined) {
-        obj.post(function() {
-          t.emit(obj.fancyname+' post done', list);
-        }, list)
-      } else {
-        t.emit(obj.fancyname+' post done', list);
-      }
-    });
-
-    lastEvent = obj.fancyname+' post done';
-    return this;
-  }
-
-  this.finish = function(f) {
-    t.on(lastEvent, f);
-  }
-}
-
-module.exports.prototype.__proto__ = require('events').EventEmitter.prototype;
diff --git a/sitegin/sitegin.js b/sitegin/sitegin.js
index f7a4a9256e2274704eee5d8e37c6db86abadebd1..97bb02aa380c97ecc973a71ad7f8e89fdb9c4cad 100644
--- a/sitegin/sitegin.js
+++ b/sitegin/sitegin.js
@@ -1,22 +1,24 @@
-exports.input = {
-  Hugo: require('./input-hugo')
-}
+var jobs = require('./jobs');
 
-exports.output = {
-  Console: require('./output-console'),
-  Filesystem: require('./output-filesystem')
-}
+module.exports = function(config) {
+  return jobs.registerMultiple(
+    config,
+    ['config', './config'],
+    ['gitInfo', './gitInfo'],
+    ['markdown', './markdown'],
+    ['parseHugo', './parseHugo'],
+    ['print', './print'],
+    ['readFiles', './readFiles'],
+    ['toURL', './toURL'],
+    ['tags', './tags'],
+    ['nunjucks', './nunjucks'],
+    ['theme', './theme'],
+    ['urls', './urls'],
+    ['writeFiles', './writeFiles'],
 
-exports.transformer = {
-  Basichtml:  require('./transformer-basichtml'),
-  Git:        require('./transformer-git'),
-  Markdown:   require('./transformer-markdown'),
-  Nunjucks:   require('./transformer-nunjucks'),
-  Tags:       require('./transformer-tags'),
-  UrlizeTags: require('./transformer-urlizetags'),
-  Other:      require('./transformer-other'),
-  Images:     require('./transformer-images'),
+    ['pipeline','./pipeline']
+  )
+  .then(function() {
+    return jobs;
+  })
 }
-
-exports.Page = require('./Page');
-exports.Sequencer = require('./sequencer');
diff --git a/sitegin/transformer-tags.js b/sitegin/tags.js
similarity index 63%
rename from sitegin/transformer-tags.js
rename to sitegin/tags.js
index 22faaff2bc36c50c2e8b08ed8362bb326e9a6362..2e27c8dcef14b13b184f105d321f72fb3c987809 100644
--- a/sitegin/transformer-tags.js
+++ b/sitegin/tags.js
@@ -1,29 +1,41 @@
-var Page = require("./Page.js").Page;
 var moment = require('moment');
+var jobs = require('./jobs');
 
-module.exports = function() {
-  var tr = this;
-  tr.fancyname = "transformer tags";
-  tr.linksPerPage = 6;
-  tr.pre = function(cb, list) {
+var toURL = function(url) {
+  return jobs.run('toURL', url);
+}
+
+module.exports = function(obj) {
+  return new Promise(function(resolve, reject) {
     var tags = new Array();
     var tagPages = {};
-    list.forEach(function(article) {
-      if(article.metadata.tags)
-      article.metadata.tags.forEach(function(tag) {
-        if(tags.map(function(e) { return e.url; }).indexOf(tag.url) < 0)
-          tags.push(tag)
-        if(tagPages[tag.url] == undefined) tagPages[tag.url] = new Array();
-        tagPages[tag.url].push(article)
-      })
+    obj.pages.forEach(function(article) {
+      if(article.metadata.tags) {
+        var ntags = [];
+        article.metadata.tags.forEach(function(tag) {
+          tag = {
+            text: tag,
+            url: toURL(tag)
+          }
+          ntags.push(tag);
+          if(tags.map(function(e) { return e.url; }).indexOf(tag.url) < 0)
+            tags.push(tag)
+          if(tagPages[tag.url] == undefined) tagPages[tag.url] = new Array();
+          tagPages[tag.url].push(article)
+        })
+        article.metadata.tags = ntags;
+      }
     });
 
+    obj.tags = [];
+    obj.redirects = [];
+
     var generateTagPages = function(file, pageList) {
-      var p = new Page();
+      var p = {};
       p.type = "redirect";
       p.file = file;
       p.content = file+"/1";
-      list.addPage(p);
+      obj.redirects.push(p);
 
       pageList.sort(function (a, b) {
         var rhs = a.metadata.date.creation;
@@ -43,10 +55,10 @@ module.exports = function() {
           p++;
           if(tagPage !== undefined) {
             tagPage.metadata.nexturl = file+"/"+p;
-            list.addPage(tagPage);
+            obj.tags.push(tagPage);
             tagPageList.push(tagPage);
           }
-          tagPage = new Page();
+          tagPage = {metadata: {}};
           if(firstpage === undefined) firstpage = tagPage;
           lastpage = tagPage;
 
@@ -57,7 +69,7 @@ module.exports = function() {
           tagPage.metadata.prevurl = prevurl;
           tagPage.metadata.thispage = tagPage;
           tagPage.metadata.pagenumber = p;
-          
+
           tagPage.content = new Array();
           if(prevpage !== undefined)
             prevpage.metadata.nextpage = tagPage;
@@ -67,20 +79,18 @@ module.exports = function() {
         tagPage.content.push(page);
 
         c++;
-        c %= tr.linksPerPage;
+        c %= obj.config.linksPerPage;
       });
-      list.addPage(tagPage);
+      obj.tags.push(tagPage);
       tagPageList.push(tagPage);
       tagPageList.forEach(function(tagPage) {
         tagPage.metadata.lastpage = lastpage;
       });
     }
-    
 
     tags.forEach(function(tag) {
       generateTagPages("tag/"+tag.url, tagPages[tag.url])
     })
-    cb();
-  }
+    resolve(obj);
+  })
 }
-
diff --git a/sitegin/theme.js b/sitegin/theme.js
new file mode 100644
index 0000000000000000000000000000000000000000..aab5d9db9f7a23aa0e548f8ba8fc9941a5e14c9e
--- /dev/null
+++ b/sitegin/theme.js
@@ -0,0 +1,31 @@
+var jobs = require('./jobs');
+
+module.exports = function(obj) {
+  return new Promise(function(resolve,reject){
+    var todo = 0;
+    var done = function(){
+      todo--; if(todo <= 0) resolve(obj);
+    }
+
+    var runJob = function(obj, type) {
+      todo++;
+      jobs.run('nunjucks', obj, type)
+      .then(function(data) {
+        obj.content = data;
+        done();
+      })
+      .catch(function(e) {reject(e)})
+    }
+
+    obj.pages.forEach(function(page) {
+      runJob(page,'article')
+    })
+    obj.tags.forEach(function(tag) {
+      runJob(tag,'tag')
+    })
+    obj.redirects.forEach(function(redirect) {
+      runJob(redirect,'redirect')
+    })
+    if(todo <= 0) resolve(obj);
+  })
+}
diff --git a/sitegin/transformer-urlizetags.js b/sitegin/toURL.js
similarity index 93%
rename from sitegin/transformer-urlizetags.js
rename to sitegin/toURL.js
index 0d3069f92e7d49e996ed963b040c9ca170fb2afe..d94e805607e69fe72284b8645b79b9c6711426ba 100644
--- a/sitegin/transformer-urlizetags.js
+++ b/sitegin/toURL.js
@@ -4,30 +4,7 @@ String.prototype.latinise=function(){return this.replace(/[^A-Za-z0-9\[\] ]/g,fu
 String.prototype.latinize=String.prototype.latinise;
 String.prototype.isLatin=function(){return this==this.latinise()}
 
-var toURL = function(tag) {
+module.exports = function(tag) {
   if(tag == undefined) return undefined;
-  return tag.toLowerCase().latinise().replace(/ /g,"-");
+  return tag.toLowerCase().latinise().replace(/ /g,"-");;
 }
-
-var f = function() {
-  var tr = this;
-  tr.fancyname = "transformer urlizetags";
-  tr.toURL = toURL;
-  tr.forEachPage = function(article, cb) {
-    var ntags = [];
-    if(article.metadata.tags) {
-      article.metadata.tags.forEach(function(tag) {
-        var ntag = {};
-        ntag.text = tag;
-        ntag.url = tr.toURL(tag);
-        ntags.push(ntag);
-      });
-      article.metadata.tags = ntags;
-    }
-    cb();
-  }
-}
-
-f.toURL = toURL;
-
-module.exports = f;
diff --git a/sitegin/transformer-basichtml.js b/sitegin/transformer-basichtml.js
deleted file mode 100644
index 3ca958cfd51512aa7a916cdd2be4231919563a24..0000000000000000000000000000000000000000
--- a/sitegin/transformer-basichtml.js
+++ /dev/null
@@ -1,12 +0,0 @@
-
-module.exports = function() {
-  var tr = this;
-  tr.fancyname = "transformer basichtml";
-  tr.forEachPage = function(article, cb) {
-    article.content = "<!DOCTYPE html><html><meta charset=\"UTF-8\" /><head><title>"
-    +article.metadata.title+
-    "</title></head><body>" + article.content + "</body></html>";
-    cb();
-  }
-}
-
diff --git a/sitegin/transformer-images.js b/sitegin/transformer-images.js
deleted file mode 100644
index b764a473d2d6fef737b6e6cd2676a0cf8ca35925..0000000000000000000000000000000000000000
--- a/sitegin/transformer-images.js
+++ /dev/null
@@ -1,97 +0,0 @@
-var input = new (require('./input-hugo'));
-var lwip;
-try {lwip = require('lwip');}
-catch(e) {
-  lwip = null
-  console.log("WARNING! LWIP is not installed - image minification won't work.");
-  console.log("You can try to run npm install.");
-  console.log("If it doesn't help try to make npm install lwip work.");
-}
-var path = require('path');
-
-var builddir = "build";
-if(process.argv[2] !== "compileonly") builddir = "build-debug";
-
-var minify = function() {
-  return Promise.resolve(new Promise(function(resolve, reject) {
-    var fileList = [];
-    input.walk('static/',
-      function(file, stat) {
-        if(file.match(/\.png$/i) || file.match(/\.jpe?g$/i)) {
-          fileList.push(file);
-        }
-      },
-      function() {
-        resolve(fileList);
-      }
-    );
-  })).then(function(fileList) {
-    var promises = [];
-    fileList.forEach((file) => {
-      file = file.split(path.sep).join('/');
-      var f = file.split(".");
-      var ext = f.splice(-1,1);
-      f = f.join(".");
-      f = f+"_thumbnail."+ext;
-      f = f.split('/');
-      f.splice(0,1,builddir);
-      f = f.join('/').replace('articles','clanek');
-
-      var minifyPromise = function(resolve, reject) {
-        if(lwip) {
-          lwip.open(file, function(err, image) {
-            if(err) throw err;
-            var w = image.width(), h = image.height();
-            if(w <= 512) {
-              file = file.replace("static/","").replace('articles','clanek');
-              resolve({
-                minified: false,
-                file: file,
-                thumb: file
-              });
-            } else {
-              image.batch()
-              .resize(512, Math.round(512*h/w))
-              .writeFile(f, function(err) {
-                if(err) throw err;
-                resolve({
-                  minified: true,
-                  file: file.replace("static/","").replace('articles','clanek'),
-                  thumb: f.replace(builddir+'/',"")
-                });
-              });
-            }
-          })
-        } else {
-          file = file.replace("static/","").replace('articles','clanek');
-          resolve({
-            minified: false,
-            file: file,
-            thumb: file
-          });
-        }
-      }
-
-      promises.push(new Promise(minifyPromise));
-    });
-    return Promise.all(promises);
-  })
-}
-
-module.exports = function() {
-  var tr = this;
-  tr.fancyname = "transformer images";
-  tr.pre = function(cb, list) {
-    if(process.argv[2] == "compileonly") {
-      minify()
-      .then(function(imageList) {
-        list.images = imageList;
-        cb();
-      })
-      .catch(function(err) {console.log(err.stack); cb();});
-    } else {
-      console.log("Skipping image minification");
-      cb();
-    }
-  }
-}
diff --git a/sitegin/transformer-nunjucks.js b/sitegin/transformer-nunjucks.js
deleted file mode 100644
index 0471fee577308c6f6630a6eb1e8c5e87aeab46b8..0000000000000000000000000000000000000000
--- a/sitegin/transformer-nunjucks.js
+++ /dev/null
@@ -1,68 +0,0 @@
-var nunjucks = require('nunjucks');
-var fs = require('fs');
-var dateFilter = require('nunjucks-date-filter');
-var util = require('util');
-
-var MyLoader = nunjucks.Loader.extend({
-    fileSystemLoader: {},
-    init: function(searchPaths, opts) {
-        fileSystemLoader = new nunjucks.FileSystemLoader(searchPaths, opts);
-    },
-
-    getSource: function(name) {
-        return fileSystemLoader.getSource(name+".html.nunjucks");
-    }
-});
-
-
-module.exports = function() {
-  var tr = this;
-  var templates = {};
-  tr.pre = function(cb) {
-    var env = new nunjucks.Environment(new MyLoader('theme/templates',{watch: process.argv[2] !== "compileonly"}));
-    env.addFilter('date', dateFilter);
-    
-    env.addFilter('paginationList', function(page, count) {
-        var curPage = page;
-        var pages = [];
-        pages.push(curPage);
-        for(var i = 0; i < count; i++) {
-          curPage = curPage.metadata.prevpage;
-          if(curPage === undefined) break;
-          pages.unshift(curPage);
-        }
-        
-        curPage = page;
-        for(var i = 0; i < count; i++) {
-          curPage = curPage.metadata.nextpage;
-          if(curPage === undefined) break;
-          pages.push(curPage);
-        }
-        return pages;
-    });
-    
-    env.addFilter('inspect', function(obj) {
-        return util.inspect(obj);
-    });
-
-    
-    templates["article"] = env.getTemplate('article');
-    templates["tag"] = env.getTemplate('tag');
-    templates["redirect"] = env.getTemplate('redirect');
-    cb();
-  };
-
-  tr.fancyname = "transformer nunjucks";
-
-  tr.forEachPage = function(page, cb) {
-    page.config = {
-      baseurl: "",
-      devel: process.argv[2] !== "compileonly"
-    }
-    var html = templates[page.type].render(page);
-    //if(page.type == "tag") console.log(page.content[0].file);
-    page.content = html;
-    cb();
-  }
-}
-
diff --git a/sitegin/transformer-other.js b/sitegin/transformer-other.js
deleted file mode 100644
index b948057ec99cb5ecf3d99abae11e069f042da897..0000000000000000000000000000000000000000
--- a/sitegin/transformer-other.js
+++ /dev/null
@@ -1,18 +0,0 @@
-var path = require('path');
-module.exports = function() {
-  var tr = this;
-  tr.fancyname = "transformer other";
-  tr.forEachPage = function(article, cb) {
-    var img = article.metadata.image;
-    article.file = article.file.split(path.sep).join("/");
-    if(img) {
-      if(!img.match(/^\//)) {
-        article.metadata.image = article.file+"/"+img;
-      } else {
-        article.metadata.image = img.replace("/","");
-      }
-    }
-
-    cb();
-  }
-}
diff --git a/sitegin/urls.js b/sitegin/urls.js
new file mode 100644
index 0000000000000000000000000000000000000000..11693250eded17a113efbd10eb2be356fa135f42
--- /dev/null
+++ b/sitegin/urls.js
@@ -0,0 +1,15 @@
+/*
+ * This jobs translates files to resulting URL
+ */
+
+module.exports = function(obj) {
+  return new Promise(function(resolve, reject) {
+    obj.pages.forEach(function(page) {
+      var file = page.file;
+      file = file.replace(/\.md$/,'');
+      file = file.replace(/^articles/,'clanek');
+      page.file = file;
+    })
+    resolve(obj)
+  });
+}
diff --git a/sitegin/writeFiles.js b/sitegin/writeFiles.js
new file mode 100644
index 0000000000000000000000000000000000000000..04a17fd0a6e11e9989250e7f189c0e7e3e9e3d01
--- /dev/null
+++ b/sitegin/writeFiles.js
@@ -0,0 +1,26 @@
+
+var fs = require('fs');
+var path = require('path');
+var mkdirp = require('mkdirp');
+
+module.exports = function(obj) {
+  return new Promise(function(resolve,reject) {
+    var builddir = obj.config.builddir;
+    var todo = 0;
+
+    var writeFile = function(article) {
+      var filename = path.join(builddir,article.file,"index.html");
+      mkdirp(path.dirname(filename), function() {
+        fs.writeFile(filename, article.content, function(err) {
+          if(err) reject(err);
+          todo--;
+          if(todo <= 0) resolve(obj);
+        });
+      });
+    }
+    obj.pages.forEach(function(o) { writeFile(o); })
+    obj.redirects.forEach(function(o) { writeFile(o); })
+    obj.tags.forEach(function(o) { writeFile(o); })
+    if(todo <= 0) resolve(obj);
+  })
+}
diff --git a/theme/templates/404.html.nunjucks b/theme/templates/404.html.nunj
similarity index 100%
rename from theme/templates/404.html.nunjucks
rename to theme/templates/404.html.nunj
diff --git a/theme/templates/article.html.nunjucks b/theme/templates/article.html.nunj
similarity index 89%
rename from theme/templates/article.html.nunjucks
rename to theme/templates/article.html.nunj
index 090aeadf9f8bab3e183d9765c6bc61b933e7cc2e..afca50de2c4054598dd3abacfb8fded3f4d6340d 100644
--- a/theme/templates/article.html.nunjucks
+++ b/theme/templates/article.html.nunj
@@ -1,4 +1,8 @@
-{% include "partials/header" %}
+{% extends "partials/base.html.nunj" %}
+
+{% block bodyattr %} class="type-article"{% endblock %}
+
+{% block content %}
   <div class="section container" id="content">
     <div class="metadata">
       <h2 id="title">{{ metadata.title }}</h2>
@@ -30,4 +34,4 @@
       </p>
     </article>
   </div>
-{% include "partials/footer" %}
+{% endblock %}
diff --git a/theme/templates/gallery.html.nunjucks b/theme/templates/gallery.html.nunjucks
deleted file mode 100644
index 8cea0527492445075ae5c2e34944234b8d938bbe..0000000000000000000000000000000000000000
--- a/theme/templates/gallery.html.nunjucks
+++ /dev/null
@@ -1,3 +0,0 @@
-{{ include "partials/header.html" }}
-  
-{{ include "partials/footer.html" }}
diff --git a/theme/templates/partials/header.html.nunjucks b/theme/templates/partials/base.html.nunj
similarity index 80%
rename from theme/templates/partials/header.html.nunjucks
rename to theme/templates/partials/base.html.nunj
index 0c5b81765e82f0874b296e105993c3a92ee9398c..83d92a22eff2c694c4c1c260cb85941510aad4dc 100644
--- a/theme/templates/partials/header.html.nunjucks
+++ b/theme/templates/partials/base.html.nunj
@@ -38,8 +38,8 @@
   {% endif %}
   {{ metadata.headerextra | safe }}
 </head>
-<body {{ "class=type-article" if type=="article" }}>
-  {% import "partials/svgs" as svgs %}
+<body{% block bodyattr %}{% endblock %}>
+  {% import "partials/svgs.html.nunj" as svgs %}
   <div id="all">
     <!-- Top nav -->
     <nav class="supernav" role="navigation" style="z-index:5;position:absolute;">
@@ -141,5 +141,51 @@
         </ul>
       </div>
     </nav>
-    {% include "partials/search" %}
-      
+    {% include "partials/search.html.nunj" %}
+
+    {% block content %}
+    {% endblock %}
+
+    <footer class="page-footer blue">
+        <div class="container s6">
+          <a class="orange-text text-lighten-3" href="https://git.ok1kvk.cz/">Gitlab</a> |
+          <a class="orange-text text-lighten-3" href="https://ok1kvk.cz/forum">Fórum</a> |
+          <a class="orange-text text-lighten-3">Webkamera</a>
+          {% if type=="article" %}
+          | <a class="orange-text text-lighten-3" href="https://git.ok1kvk.cz/ok1kvk.cz/content/tree/master/articles/{{file | replace("clanek/","")}}.md">Zdroják tohoto článku</a>
+          {% endif %}
+        </div>
+        <div class="container s6">
+        Stránky pro OK1KVK vytvořil <a class="orange-text text-lighten-3">Jakub Skořepa</a> 2015-2016
+        </div>
+    </footer>
+
+    <script src="{{ config.baseurl }}/theme/js/materialize.js"></script>
+    <script src="{{ config.baseurl }}/theme/js/init.js"></script>
+
+    <script>
+      (function($) {
+      var onresize = function(){
+        $("footer").height("auto");
+        $("#content").css("margin-bottom",$("footer").height());
+      };
+      $(window).resize(onresize);
+      $(document).load(onresize);
+      $(document).ready(onresize);
+    })(jQuery);
+    </script>
+    {% if not config.debug %}
+      <script>
+        (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+        (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+        m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+        })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
+
+        ga('create', 'UA-74646565-1', 'auto');
+        ga('send', 'pageview');
+
+      </script>
+    {% endif %}
+    <script src="{{ config.baseurl }}/theme/lightbox2/dist/js/lightbox.min.js"></script>
+  </body>
+</html>
diff --git a/theme/templates/partials/footer.html.nunjucks b/theme/templates/partials/footer.html.nunjucks
deleted file mode 100644
index 2b2c3734de6178727c3f1237b630fc4e492851f1..0000000000000000000000000000000000000000
--- a/theme/templates/partials/footer.html.nunjucks
+++ /dev/null
@@ -1,44 +0,0 @@
-      <footer class="page-footer blue">
-          <div class="container s6">
-            <a class="orange-text text-lighten-3" href="https://git.ok1kvk.cz/">Gitlab</a> |
-            <a class="orange-text text-lighten-3" href="https://ok1kvk.cz/forum">Fórum</a> |
-            <a class="orange-text text-lighten-3">Webkamera</a>
-            {% if type=="article" %} 
-            | <a class="orange-text text-lighten-3" href="https://git.ok1kvk.cz/ok1kvk.cz/content/tree/master/articles/{{file | replace("clanek/","")}}.md">Zdroják tohoto článku</a>
-            {% endif %}
-          </div>
-          <div class="container s6">
-          Stránky pro OK1KVK vytvořil <a class="orange-text text-lighten-3">Jakub Skořepa</a> 2015-2016
-          </div>
-      </footer>
-
-      <script src="{{ config.baseurl }}/theme/js/materialize.js"></script>
-      <script src="{{ config.baseurl }}/theme/js/init.js"></script>
-
-    </div> <!-- #wrapper -->
-    <script>
-      (function($) {
-      var onresize = function(){
-        $("footer").height("auto");
-        $("#content").css("margin-bottom",$("footer").height());
-      };
-      $(window).resize(onresize);
-      $(document).load(onresize);
-      $(document).ready(onresize);
-    })(jQuery);
-    </script>
-    {% if not config.debug %}
-      <script>
-        (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
-        (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
-        m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
-        })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
-
-        ga('create', 'UA-74646565-1', 'auto');
-        ga('send', 'pageview');
-
-      </script>
-    {% endif %}
-    <script src="{{ config.baseurl }}/theme/lightbox2/dist/js/lightbox.min.js"></script>
-  </body>
-</html>
diff --git a/theme/templates/partials/list.html.nunjucks b/theme/templates/partials/list.html.nunj
similarity index 95%
rename from theme/templates/partials/list.html.nunjucks
rename to theme/templates/partials/list.html.nunj
index 8c4a2f7f4c9631e4ee71a81c8fc936b198bea75a..c40b1a05aa822ae0f3e8f9318ed7f7bb190016c1 100644
--- a/theme/templates/partials/list.html.nunjucks
+++ b/theme/templates/partials/list.html.nunj
@@ -1,4 +1,4 @@
-{% import "partials/svgs" as svgs %}
+{% import "partials/svgs.html.nunj" as svgs %}
   <div id=content class=container>
     <div class="section">
       <div class="row">
@@ -8,7 +8,7 @@
             <div class="block article" style="">
               <a href="{{ config.baseurl }}/{{ page.file }}">
                 <div style="height:120px;
-                  {% if page.metadata.image %}background-image: url('{{config.baseurl}}/{{ page.metadata.image }}'){% endif %}
+                  {% if page.metadata.image %}background-image: url('{{ config.baseurl }}{{ page.metadata.image | relURL(page.file) }}'){% endif %}
                   " class="leadimage light-blue darken-2"></div>
               </a>
               <div class="head-title">
@@ -35,8 +35,8 @@
           </div>
         {% endfor %}
       </div>
-      
-      {% include "partials/paginator" %}
+
+      {% include "partials/paginator.html.nunj" %}
     </div>
 
     <div class="hide-on-small-only">
@@ -62,21 +62,21 @@
            if ($(this).height() > maxHeight) { maxHeight = $(this).height(); }
         });
         $(".head-perex").height(maxHeight);
-        
+
         $(".head-title").height("auto");
         maxHeight = 0;
         $(".head-title").each(function(){
            if ($(this).height() > maxHeight) { maxHeight = $(this).height(); }
         });
         $(".head-title").height(maxHeight);
-        
+
         $(".head-info").height("auto");
         maxHeight = 0;
         $(".head-info").each(function(){
            if ($(this).height() > maxHeight) { maxHeight = $(this).height(); }
         });
         $(".head-info").height(maxHeight);
-        
+
         // Sidelink icons of same size
         $(".sidelink-icon").css("top",$(window).height()/2);
       };
diff --git a/theme/templates/partials/paginator.html.nunjucks b/theme/templates/partials/paginator.html.nunj
similarity index 96%
rename from theme/templates/partials/paginator.html.nunjucks
rename to theme/templates/partials/paginator.html.nunj
index e2a618ffb28c6a2c7ba256dc3b83abf6b497dbdc..f81ce3a5c477546af918c3cd44488bc027c73bdd 100644
--- a/theme/templates/partials/paginator.html.nunjucks
+++ b/theme/templates/partials/paginator.html.nunj
@@ -1,4 +1,4 @@
-{% import "partials/svgs" as svgs %}
+{% import "partials/svgs.html.nunj" as svgs %}
 
 <ul class="pagination">
   <li><a {{ (" href=\""+config.baseurl +"/"+ metadata.firstpage.file +"\"") | safe if metadata.firstpage.file != file }}>
@@ -17,9 +17,8 @@
   <li><a {{ " href="+config.baseurl +"/"+ metadata.nextpage.file if metadata.nextpage != undefined }}>
     {{ svgs.chevronright( "#999" if metadata.nextpage == undefined else "#000", 1,"em" ) }}
   </a></li>
-  
+
   <li><a {{ (" href=\""+config.baseurl +"/"+ metadata.lastpage.file +"\"") | safe if metadata.lastpage.file != file }}>
     {{ svgs.doublechevronright( "#999" if metadata.lastpage.file == file else "#000", 1,"em" ) }}
   </a></li>
 </ul>
-
diff --git a/theme/templates/partials/search.html.nunjucks b/theme/templates/partials/search.html.nunj
similarity index 100%
rename from theme/templates/partials/search.html.nunjucks
rename to theme/templates/partials/search.html.nunj
diff --git a/theme/templates/partials/svgs.html.nunjucks b/theme/templates/partials/svgs.html.nunj
similarity index 100%
rename from theme/templates/partials/svgs.html.nunjucks
rename to theme/templates/partials/svgs.html.nunj
diff --git a/theme/templates/redirect.html.nunjucks b/theme/templates/redirect.html.nunj
similarity index 100%
rename from theme/templates/redirect.html.nunjucks
rename to theme/templates/redirect.html.nunj
diff --git a/theme/templates/tag.html.nunj b/theme/templates/tag.html.nunj
new file mode 100644
index 0000000000000000000000000000000000000000..b086b5c739a3bbff12d526be441cd3c8f1867783
--- /dev/null
+++ b/theme/templates/tag.html.nunj
@@ -0,0 +1,5 @@
+{% extends "partials/base.html.nunj" %}
+
+{% block content %}
+    {% include "partials/list.html.nunj" %}
+{% endblock %}
diff --git a/theme/templates/tag.html.nunjucks b/theme/templates/tag.html.nunjucks
deleted file mode 100644
index 7ec1cad9b36c5d62b55ab81ad55f220bd78ea398..0000000000000000000000000000000000000000
--- a/theme/templates/tag.html.nunjucks
+++ /dev/null
@@ -1,3 +0,0 @@
-{% include "partials/header" %}
-{% include "partials/list" %}
-{% include "partials/footer" %}