diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644
index 0000000000000000000000000000000000000000..6f677210fbe8d388ff337ff78384533dd1bca9dd
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,96 @@
+var every = require('lodash/collection/every');
+var fs = require('fs');
+
+module.exports = function (grunt) {
+  var lessDir = 'www/less';
+  var lessVendorDir = lessDir + '/lib';
+  var cssDir = 'www/css';
+  var jsDir = 'www/js';
+
+  var cssFiles = [
+    'search',
+    'search-google',
+  ];
+
+  var jsFiles = [
+    jsDir + '/search.js',
+  ];
+
+  var wdnMixinLibBaseUrl = 'https://raw.githubusercontent.com/unl/wdntemplates/4.1/wdn/templates_4.1/less/_mixins/';
+  var wdnMixins = [
+    'breakpoints.less',
+    'colors.less',
+    'fonts.less',
+  ];
+  var allMixinsExist = every(wdnMixins, function(value) {
+    return fs.existsSync(lessVendorDir + '/' + value);
+  });
+
+  var lessFiles = {};
+  cssFiles.forEach(function(file) {
+    lessFiles[cssDir + '/' + file + '.css'] = lessDir + '/' + file + '.less';
+  });
+
+  var builtJsFiles = {};
+  builtJsFiles[jsDir + '/search.min.js'] = jsFiles;
+
+  var autoprefixPlugin = new (require('less-plugin-autoprefix'))({browsers: ["last 2 versions"]});
+  var cleanCssPlugin = new (require('less-plugin-clean-css'))();
+
+  // load all grunt tasks matching the ['grunt-*', '@*/grunt-*'] patterns
+  require('load-grunt-tasks')(grunt);
+
+  grunt.initConfig({
+    'curl-dir': {
+      'less-libs': {
+        src: wdnMixins.map(function(file) {
+          return wdnMixinLibBaseUrl + file;
+        }),
+        dest: lessVendorDir
+      }
+    },
+    less: {
+      all: {
+        options: {
+    				paths: [lessDir],
+    				plugins: [
+  						autoprefixPlugin,
+              cleanCssPlugin
+  					]
+    			},
+    			files: lessFiles
+      }
+    },
+    uglify: {
+      options: {
+        sourceMap: true
+      },
+      all: {
+        files: builtJsFiles
+      }
+    },
+    clean: {
+      css: Object.keys(lessFiles).concat(lessVendorDir),
+      js: Object.keys(builtJsFiles).concat(jsDir + '/**/*.map')
+    },
+    watch: {
+      less: {
+  			files: lessDir + '/**/*.less',
+  			tasks: ['less']
+  		}
+    }
+  });
+
+  // establish grunt default
+  var defaultTasks = ['less', 'uglify'];
+  var localTasks = defaultTasks.slice();
+  if (!allMixinsExist) {
+    defaultTasks.unshift('curl-dir');
+  }
+  grunt.registerTask('default', defaultTasks);
+  grunt.registerTask('all-local', localTasks);
+
+  // legacy targets from Makefile
+  grunt.registerTask('all', ['default']);
+  grunt.registerTask('js', ['uglify']);
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..bad93c9f401f31f1a44fc37e739b8e33b806eb7a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,29 @@
+{
+  "name": "unl-search",
+  "version": "1.0.0",
+  "description": "A web application for searching UNL websites via Google Custom Search.",
+  "dependencies": {},
+  "devDependencies": {
+    "grunt": "^0.4.5",
+    "grunt-contrib-clean": "^0.6.0",
+    "grunt-contrib-less": "^1.0.1",
+    "grunt-contrib-uglify": "^0.9.2",
+    "grunt-contrib-watch": "^0.6.1",
+    "grunt-curl": "^2.2.0",
+    "less-plugin-autoprefix": "^1.4.2",
+    "less-plugin-clean-css": "^1.5.1",
+    "load-grunt-tasks": "^3.2.0",
+    "lodash": "^3.10.1"
+  },
+  "scripts": {},
+  "repository": {
+    "type": "git",
+    "url": "git@git.unl.edu:iim/UNL_Search.git"
+  },
+  "keywords": [
+    "UNL",
+    "Search"
+  ],
+  "author": "UNL Web Developer Network <wdn@unl.edu> (http://wdn.unl.edu/)",
+  "license": "BSD-3-Clause"
+}