├── .npmignore ├── .gitignore ├── test ├── expected │ ├── target1 │ ├── target2 │ └── target3 ├── input │ └── default_options ├── fixtures │ ├── test_robots.txt │ └── test_robots_sitemap.txt ├── utils.js ├── html_snapshots_target1.js ├── html_snapshots_target3.js ├── html_snapshots_target2.js ├── server │ ├── sitemap.xml │ ├── index.js │ ├── contact │ │ └── index.html │ ├── index.html │ └── services │ │ └── carpets │ │ └── index.html ├── helpers │ └── options.js └── html_snapshots_test.js ├── eslint.config.js ├── .github └── workflows │ ├── verify.yml │ └── deploy.yml ├── LICENSE.md ├── tasks └── html_snapshots.js ├── package.json ├── Gruntfile.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | /node_modules 3 | /test 4 | /tmp -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | tmp 4 | grunt-html-snapshots.sublime-project 5 | grunt-html-snapshots.sublime-workspace -------------------------------------------------------------------------------- /test/expected/target1: -------------------------------------------------------------------------------- 1 | tmp/target1/snapshots/index.html 2 | tmp/target1/snapshots/contact/index.html 3 | tmp/target1/snapshots/services/carpets/index.html -------------------------------------------------------------------------------- /test/expected/target2: -------------------------------------------------------------------------------- 1 | tmp/target2/snapshots/index.html 2 | tmp/target2/snapshots/contact/index.html 3 | tmp/target2/snapshots/services/carpets/index.html -------------------------------------------------------------------------------- /test/expected/target3: -------------------------------------------------------------------------------- 1 | tmp/target3/snapshots/index.html 2 | tmp/target3/snapshots/contact/index.html 3 | tmp/target3/snapshots/services/carpets/index.html -------------------------------------------------------------------------------- /test/input/default_options: -------------------------------------------------------------------------------- 1 | { 2 | "source": "test/fixtures/test_robots.txt", 3 | "selector": "#dynamic-content", 4 | "outputDirClean": "true", 5 | "hostname": "localhost" 6 | } -------------------------------------------------------------------------------- /test/fixtures/test_robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | 3 | Allow: / 4 | Allow: /contact 5 | Allow: /services/carpets 6 | 7 | Disallow: /api/ 8 | Disallow: /vendor/ 9 | Disallow: /fonts/ 10 | Disallow: /images/ 11 | 12 | User-agent: ia_archiver 13 | Disallow: / 14 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * test utils 3 | */ 4 | const fs = require('fs').promises; 5 | 6 | function pathExists (path) { 7 | return fs.access(path) 8 | .then(() => true) 9 | .catch(() => false); 10 | } 11 | 12 | module.exports = { 13 | pathExists 14 | }; 15 | -------------------------------------------------------------------------------- /test/fixtures/test_robots_sitemap.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | 3 | Allow: / 4 | Allow: /contact 5 | Allow: /services/carpets 6 | 7 | Disallow: /api/ 8 | Disallow: /vendor/ 9 | Disallow: /fonts/ 10 | Disallow: /images/ 11 | 12 | Sitemap: http://localhost:8051/sitemap.xml 13 | 14 | User-agent: ia_archiver 15 | Disallow: / 16 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const js = require('@eslint/js'); 2 | const globals = require('globals'); 3 | 4 | module.exports = [{ 5 | ignores: [ 6 | 'node_modules/**', 7 | 'tmp/**' 8 | ] 9 | }, { 10 | files: [ 11 | '**/*' 12 | ], 13 | ...js.configs.recommended, 14 | languageOptions: { 15 | globals: { 16 | ...globals.node 17 | } 18 | } 19 | }]; 20 | -------------------------------------------------------------------------------- /test/html_snapshots_target1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * tests, target1 3 | */ 4 | const grunt = require('grunt'); 5 | const { pathExists } = require('./utils'); 6 | 7 | exports.html_snapshots = { 8 | setUp: done => { 9 | // setup here if necessary 10 | done(); 11 | }, 12 | 13 | target1: test => { 14 | test.expect(3); 15 | 16 | const results = []; 17 | 18 | grunt.file.read('test/expected/target1').split('\n').forEach(line => { 19 | results.push(pathExists(line).then(exists => { 20 | test.equal(true, exists, `${line} should exist`); 21 | })); 22 | }); 23 | 24 | Promise.all(results).finally(()=> { 25 | test.done(); 26 | }) 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /test/html_snapshots_target3.js: -------------------------------------------------------------------------------- 1 | /** 2 | * tests, target1 3 | */ 4 | const grunt = require('grunt'); 5 | const { pathExists } = require('./utils'); 6 | 7 | exports.html_snapshots = { 8 | setUp: done => { 9 | // setup here if necessary 10 | done(); 11 | }, 12 | 13 | target1: test => { 14 | test.expect(3); 15 | 16 | const results = []; 17 | 18 | grunt.file.read('test/expected/target3').split('\n').forEach(line => { 19 | results.push(pathExists(line).then(exists => { 20 | test.equal(true, exists, `${line} should exist`); 21 | })); 22 | }); 23 | 24 | Promise.all(results).finally(()=> { 25 | test.done(); 26 | }) 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /test/html_snapshots_target2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * tests, target2 3 | */ 4 | const grunt = require('grunt'); 5 | const { pathExists } = require('./utils'); 6 | 7 | exports.html_snapshots = { 8 | setUp: done => { 9 | // setup here if necessary 10 | done(); 11 | }, 12 | 13 | target2: test => { 14 | test.expect(3); 15 | 16 | const results = []; 17 | 18 | grunt.file.read('test/expected/target2').split('\n').forEach(line => { 19 | results.push(pathExists(line).then(exists => { 20 | test.equal(false, exists, `${line} should not exist`); 21 | })); 22 | }); 23 | 24 | Promise.all(results).finally(() => { 25 | test.done(); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /test/server/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | http://localhost:8051/ 5 | 2012-08-11T12:16:09+00:00 6 | daily 7 | 1.0 8 | 9 | 10 | http://localhost:8051/contact 11 | 2012-11-02T02:53:40+00:00 12 | weekly 13 | 0.6 14 | 15 | 16 | http://localhost:8051/services/carpets 17 | 2012-10-19T10:45:23+00:00 18 | weekly 19 | 0.6 20 | 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: Verify 2 | on: 3 | pull_request: 4 | branches: [ master ] 5 | 6 | jobs: 7 | verify: 8 | runs-on: ubuntu-22.04 9 | strategy: 10 | matrix: 11 | node-version: [20.x, 22.x, 24.x] 12 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 13 | 14 | steps: 15 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # 6.1.0 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm ci 21 | - name: Lint 22 | run: npm run lint 23 | - name: Test 24 | if: ${{ success() }} 25 | run: npm test -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: [ master ] 5 | 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-22.04 9 | permissions: 10 | contents: read 11 | id-token: write 12 | steps: 13 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1 14 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # 6.1.0 15 | with: 16 | node-version: '24.x' 17 | registry-url: 'https://registry.npmjs.org' 18 | - name: Update npm 19 | run: npm install -g npm@latest # ensure npm 11.5.1 or later 20 | - run: npm ci 21 | - name: Test 22 | run: npm test 23 | - name: Publish 24 | if: ${{ success() }} 25 | run: npm publish --access public -------------------------------------------------------------------------------- /test/helpers/options.js: -------------------------------------------------------------------------------- 1 | /** 2 | * options.js 3 | * 4 | * A test helper to detect which html-snapshots options to use 5 | * 6 | * phantomjs 7 | * If a global phantomjs is defined, decorates html-snapshots options to specify that global 8 | * In some test environments (travis), local phantomjs will not install if a global is found. 9 | */ 10 | 11 | const spawn = require("child_process").spawn; 12 | 13 | module.exports = { 14 | 15 | // for now, callback is passed true if global phantomjs should be used 16 | detector: callback => { 17 | // try to run phantom globally 18 | const cp = spawn("phantomjs", ["--version"]); 19 | 20 | // if it fails, use local per the defaults 21 | cp.on("error", () => { 22 | callback(false); 23 | }); 24 | 25 | // if it succeeds, use the global 26 | cp.on("exit", code => { 27 | if (code === 0) { 28 | callback(true); 29 | } 30 | }); 31 | } 32 | }; -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 - 2025 Alex Grant 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /tasks/html_snapshots.js: -------------------------------------------------------------------------------- 1 | /** 2 | * grunt-html-snapshots 3 | * https://github.com/localnerve/grunt-html-snapshots 4 | * 5 | * Copyright (c) 2013 - 2025, LocalNerve, Alex Grant 6 | * Licensed under the MIT license. 7 | */ 8 | const html_snapshots = require('html-snapshots'); 9 | 10 | module.exports = function (grunt) { 11 | grunt.registerMultiTask('html_snapshots', 'Generate html snapshots.', function () { 12 | // Merge task-specific and/or target-specific options with these defaults. 13 | const options = this.options({ 14 | force: false 15 | }); 16 | 17 | // Report html_snapshot errors but dont fail the task 18 | const force = options.force; 19 | delete options.force; 20 | 21 | grunt.verbose.writeflags(options, 'html_snapshots options'); 22 | 23 | // Setup for async 24 | const done = this.async(); 25 | 26 | // Take the snapshots 27 | html_snapshots.run(options) 28 | .then(() => { 29 | grunt.log.ok(); 30 | }) 31 | .catch(err => { 32 | grunt.log.error('html_snapshots failed'); 33 | if (force) { 34 | grunt.log.error(err); 35 | } 36 | return err; 37 | }) 38 | .then(err => { 39 | done(force ? undefined : err); 40 | }); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-html-snapshots", 3 | "description": "The grunt task for html-snapshots", 4 | "version": "9.2.0", 5 | "homepage": "https://github.com/localnerve/grunt-html-snapshots", 6 | "author": { 7 | "name": "Alex Grant", 8 | "email": "alex@localnerve.com", 9 | "url": "http://www.localnerve.com" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/localnerve/grunt-html-snapshots.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/localnerve/grunt-html-snapshots/issues" 17 | }, 18 | "license": "MIT", 19 | "main": "Gruntfile.js", 20 | "engines": { 21 | "node": ">= 20" 22 | }, 23 | "files": [ 24 | "Gruntfile.js", 25 | "tasks/*" 26 | ], 27 | "scripts": { 28 | "test": "grunt test", 29 | "lint": "eslint ." 30 | }, 31 | "dependencies": { 32 | "html-snapshots": "^5.2.0" 33 | }, 34 | "devDependencies": { 35 | "@eslint/js": "^9.39.2", 36 | "eslint": "^9.39.2", 37 | "globals": "^16.5.0", 38 | "grunt-contrib-clean": "^2.0.1", 39 | "grunt-contrib-nodeunit": "^5.0.0", 40 | "grunt": "^1.6.1" 41 | }, 42 | "overrides": { 43 | "tap": "21.5.0", 44 | "request": "npm:@cypress/request@3.0.9" 45 | }, 46 | "peerDependencies": { 47 | "grunt": ">=1.0.0" 48 | }, 49 | "keywords": [ 50 | "SEO", 51 | "html", 52 | "snapshots", 53 | "selector", 54 | "robots.txt", 55 | "sitemap.xml", 56 | "gruntplugin" 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /test/html_snapshots_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test the task 3 | */ 4 | 5 | const grunt = require('grunt'); 6 | const { pathExists } = require('./utils'); 7 | 8 | exports.html_snapshots = { 9 | setUp: function (done) { 10 | // setup here if necessary 11 | done(); 12 | }, 13 | 14 | target1: function(test) { 15 | test.expect(3); 16 | 17 | const results = []; 18 | 19 | grunt.file.read('test/expected/target1').split('\n').forEach(line => { 20 | results.push(pathExists(line).then(exists => { 21 | test.equal(true, exists, `${line} should exist`); 22 | })); 23 | }); 24 | 25 | Promise.all(results).finally(() => { 26 | test.done(); 27 | }); 28 | }, 29 | 30 | target2: function(test) { 31 | test.expect(3); 32 | 33 | const results = []; 34 | 35 | grunt.file.read('test/expected/target2').split('\n').forEach(line => { 36 | results.push(pathExists(line).then(exists => { 37 | test.equal(false, exists, `${line} should not exist`); 38 | })); 39 | }); 40 | 41 | Promise.all(results).finally(() => { 42 | test.done(); 43 | }); 44 | }, 45 | 46 | target3: function(test) { 47 | test.expect(3); 48 | 49 | const results = []; 50 | 51 | grunt.file.read('test/expected/target3').split('\n').forEach(line => { 52 | results.push(pathExists(line).then(exists => { 53 | test.equal(true, exists, `${line} should exist`); 54 | })); 55 | }); 56 | 57 | Promise.all(results).finally(() => { 58 | test.done(); 59 | }); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /test/server/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a local web server for tests 3 | */ 4 | const http = require("http"), 5 | url = require("url"), 6 | path = require("path"), 7 | fs = require("fs"); 8 | 9 | module.exports = { 10 | 11 | start: function(rootDir, port, end) { 12 | http.createServer(function(request, response) { 13 | 14 | const uri = url.parse(request.url).pathname; 15 | let filename = path.join(rootDir, uri); 16 | 17 | fs.exists(filename, function(exists) { 18 | if(!exists) { 19 | response.writeHead(404, {"Content-Type": "text/plain"}); 20 | response.write("404 Not Found\n"); 21 | response.end(); 22 | return; 23 | } 24 | 25 | if (fs.statSync(filename).isDirectory()) filename += '/index.html'; 26 | 27 | fs.readFile(filename, "binary", function(err, file) { 28 | if(err) { 29 | response.writeHead(500, {"Content-Type": "text/plain"}); 30 | response.write(err + "\n"); 31 | response.end(); 32 | if (typeof end === "function") 33 | setTimeout(end, 1000); 34 | return; 35 | } 36 | 37 | if (path.extname(filename) === '.xml') { 38 | response.writeHead(200, {"Content-Type": "text/xml"}); 39 | response.write(file); 40 | } else { 41 | response.writeHead(200); 42 | response.write(file, "binary"); 43 | } 44 | response.end(); 45 | if (typeof end === "function") 46 | setTimeout(end, 1000); 47 | }); 48 | }); 49 | }).listen(parseInt(port, 10)); 50 | } 51 | }; -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * grunt-html-snapshots 3 | * https://github.com/localnerve/grunt-html-snapshots 4 | * 5 | * Copyright (c) 2013 - 2025 Alex Grant 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | module.exports = function (grunt) { 10 | 11 | // Project configuration. 12 | grunt.initConfig({ 13 | // Before generating any new files, remove any previously-created files. 14 | clean: { 15 | tests: ['tmp'], 16 | }, 17 | 18 | test_server1: { 19 | options: { 20 | port: 8051 21 | } 22 | }, 23 | 24 | test_server2: { 25 | options: { 26 | port: 8052 27 | } 28 | }, 29 | 30 | test_server3: { 31 | options: { 32 | port: 8051 33 | } 34 | }, 35 | 36 | // Configuration to be run (and then tested). 37 | html_snapshots: { 38 | options: grunt.file.readJSON("test/input/default_options"), 39 | 40 | target1: { 41 | options: { 42 | outputDir: "tmp/target1/snapshots", 43 | port: "<%= test_server1.options.port %>" 44 | } 45 | }, 46 | 47 | target2: { 48 | options: { 49 | outputDir: "tmp/target2/snapshots", 50 | hostname: "bogus", 51 | port: "<%= test_server2.options.port %>", 52 | force: true 53 | } 54 | }, 55 | 56 | target3: { 57 | options: { 58 | source: "test/fixtures/test_robots_sitemap.txt", 59 | outputDir: "tmp/target3/snapshots", 60 | port: "<%= test_server3.options.port %>" 61 | } 62 | } 63 | }, 64 | 65 | // Unit tests. 66 | nodeunit: { 67 | tests: ['test/*_test.js'], 68 | target1: ['test/html_snapshots_target1.js'], 69 | target2: ['test/html_snapshots_target2.js'] 70 | }, 71 | 72 | }); 73 | 74 | // Actually load this plugin's task(s). 75 | grunt.loadTasks('tasks'); 76 | 77 | // These plugins provide necessary tasks. 78 | grunt.loadNpmTasks('grunt-contrib-clean'); 79 | grunt.loadNpmTasks('grunt-contrib-nodeunit'); 80 | 81 | // Register the test server 82 | grunt.registerTask('test_server1', 'serve the test files', function(){ 83 | const options = this.options(); 84 | const server = require('./test/server'); 85 | server.start('./test/server', options.port); 86 | grunt.log.writeln("running test server on port "+options.port); 87 | grunt.log.ok(); 88 | }); 89 | 90 | grunt.registerTask('html_snapshots_options', 'prepare options for html_snapshots', function(){ 91 | const optionsHelper = require("./test/helpers/options"); 92 | const done = this.async(); 93 | optionsHelper.detector(globalPhantom => { 94 | if (globalPhantom) { 95 | var htmlSnapshots = grunt.config.get("html_snapshots"); 96 | htmlSnapshots.options.phantomjs = "phantomjs"; 97 | grunt.config.set("html_snapshots", htmlSnapshots); 98 | } 99 | done(); 100 | }); 101 | }); 102 | 103 | // Whenever the "test" task is run, run this plugin's task(s), then test the result. 104 | grunt.registerTask('test', ['clean', 'test_server1', 'html_snapshots_options', 'html_snapshots', 'nodeunit:tests']); 105 | grunt.registerTask('target1', ['clean', 'test_server1', 'html_snapshots_options', 'html_snapshots:target1', 'nodeunit:target1']); 106 | grunt.registerTask('target2', ['clean', 'html_snapshots_options', 'html_snapshots:target2', 'nodeunit:target2']); 107 | 108 | // By default, lint and run all tests. 109 | grunt.registerTask('default', ['test']); 110 | 111 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grunt-html-snapshots 2 | 3 | [![npm version](https://badge.fury.io/js/grunt-html-snapshots.svg)](http://badge.fury.io/js/grunt-html-snapshots) 4 | ![Verify](https://github.com/localnerve/grunt-html-snapshots/workflows/Verify/badge.svg) 5 | 6 | > The grunt task for [html-snapshots](http://github.com/localnerve/html-snapshots) 7 | 8 | ## Getting Started 9 | This plugin requires Grunt `>=1.0.0` 10 | *To use an older Grunt, use this library at tag `#v1.0.2`* 11 | 12 | If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide, as it explains how to create a [Gruntfile](http://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. Once you're familiar with that process, you may install this plugin with this command: 13 | 14 | ```shell 15 | npm install grunt-html-snapshots --save-dev 16 | ``` 17 | 18 | Once the plugin has been installed, it may be enabled inside your Gruntfile with this line of JavaScript: 19 | 20 | ```js 21 | grunt.loadNpmTasks('grunt-html-snapshots'); 22 | ``` 23 | 24 | ## The "html_snapshots" task 25 | 26 | ### Overview 27 | This is a simple grunt task that uses the [html-snapshots](http://github.com/localnerve/html-snapshots) library. In your project's Gruntfile, add a section named `html_snapshots` to the data object passed into `grunt.initConfig()`. 28 | 29 | ```js 30 | grunt.initConfig({ 31 | html_snapshots: { 32 | options: { 33 | // Task-specific options go here. 34 | }, 35 | your_target: { 36 | options: { 37 | // Target-specific options go here. 38 | } 39 | }, 40 | }, 41 | }) 42 | ``` 43 | 44 | ### Options 45 | 46 | #### options.force 47 | Type: `boolean` 48 | Default value: `false` 49 | 50 | A boolean value that is used to force the Gruntfile to continue, even if this task fails. 51 | 52 | #### MORE 53 | For details of task and target specific options, read the options section of [html-snapshots](http://github.com/localnerve/html-snapshots) 54 | 55 | ### Usage Examples 56 | 57 | #### Default Options and Targets 58 | In this example, the default options are used to specify that the output directory should always be cleaned prior to taking snapshots, that a local robots.txt file should be used, and the html should be served from localhost. On all pages except the home page, when the selector "#dynamic-content" appears in the output, the page is ready for a snapshot. On the home page, we only take the snapshot when the selector "#home-content" is visible in the output. 59 | 60 | ```js 61 | grunt.initConfig({ 62 | html_snapshots: { 63 | // options for all targets 64 | options: { 65 | source: "/path/to/local/robots.txt", 66 | hostname: "localhost", 67 | selector: { "__default": "#dynamic-content", "/": "#home-content" }, 68 | outputDirClean: "true", 69 | }, 70 | // the debug target 71 | debug: { 72 | options: { 73 | outputDir: "./snapshots/debug" 74 | } 75 | }, 76 | // the release target 77 | release: { 78 | options: { 79 | outputDir: "./snapshots/release" 80 | } 81 | } 82 | } 83 | }); 84 | 85 | grunt.loadNpmTasks('grunt-html-snapshots'); 86 | 87 | grunt.registerTask('debug', ['html_snapshots:debug']); 88 | grunt.registerTask('release', ['html_snapshots:release']); 89 | ``` 90 | 91 | Many more options are available. For details and examples of using the html-snapshots options, visit [html-snapshots](http://github.com/localnerve/html-snapshots). 92 | 93 | ## Contributing 94 | In lieu of a formal style guide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using [Grunt](http://gruntjs.com/). 95 | 96 | ## License 97 | [MIT License](LICENSE.md) -------------------------------------------------------------------------------- /test/server/contact/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Contact - North Star Cleaning Service 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 |
53 | 54 | 122 |
123 |
124 |
125 |

Contact us instructions go here. All form inputs are required.

126 |
127 |
128 |
129 | 130 | 131 |
132 |
133 |
134 |
135 | 136 | 137 |
138 |
139 |
140 |
141 | 142 | 143 |
144 |
145 | 146 |
147 |
148 |
149 |
150 | 215 | 216 |
217 |
-------------------------------------------------------------------------------- /test/server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | North Star Cleaning Service 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 |
53 | 54 | 122 |
123 |
124 |
125 |
126 |
127 | Test Art 128 |
129 |
130 |
131 |
132 |
133 | 134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 | Carpet Cleaning 143 |
144 |
145 |
146 |
147 | Floor Cleaning 148 |
149 |
150 |
151 |
152 |
153 |
154 | Upholstery Cleaning 155 |
156 |
157 |
158 |
159 | Tile & Grout 160 |
161 |
162 |
163 |
164 |
165 |
166 | Water Damage 167 |
168 |
169 |
170 |
171 | FAQ 172 |
173 |
174 |
175 |
176 |
177 | 242 | 243 |
244 |
-------------------------------------------------------------------------------- /test/server/services/carpets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Carpet Cleaning - North Star Cleaning Service 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 |
53 | 54 | 122 |
123 |
124 |
125 | Test Art 126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | 134 | 135 |
136 |
137 |
138 | 139 | small carpet 140 | 141 |
142 |
143 |
144 |
145 | 146 | small carpet 147 | 148 |
149 |
150 |
151 | 152 |
153 |
154 |
155 | 156 | small carpet 157 | 158 |
159 |
160 |
161 |
162 | 163 | small carpet 164 | 165 |
166 |
167 |
168 |
169 | 234 | 235 |
236 |
237 | big carpet 238 | × 239 |
240 | big carpet 241 | × 242 |
243 | big carpet 244 | × 245 |
246 | big carpet 247 | × 248 |
--------------------------------------------------------------------------------