website: adds tests for inbound links

Test depends on building the site, scraping links, then visiting any internal
links to verify they are not broken.

An additional constraint for ensuring all href's resolve to an absolute path
was added to the link tests to make it easier to move content around later.

Additional, smaller changes:

* Makefile updated to properly define dependencies for build artifacts.
* Where possible .PHONY deps have been removed, ideally .PHONY targets are
  not depended on by build artifacts.

I intentionally didn't port the TOC tests from www. The changes and addition
of React casued enough complexity to warrent punting this to a new CL later.

Change-Id: Ic3013b9bbf21aa8f9b7ad9ebe0f95ee4fb5b1941
diff --git a/.gitignore b/.gitignore
index 1846feb..4e7de61 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 /.jiri
 /build
 /node_modules
+# Files compiled via browserify and LESS.
 /public/**/bundle.*
 /public/sh
 /public/tutorials
diff --git a/.jshintrc b/.jshintrc
index a066a72..5142df8 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -13,7 +13,7 @@
   "nonbsp": true,
   "nonew": true,
   "quotmark": "single",
-  "strict": "implied",
+  "strict": false,
   "sub": true,
   "trailing": true,
   "undef": true,
diff --git a/Makefile b/Makefile
index 46d235f..33e6b77 100644
--- a/Makefile
+++ b/Makefile
@@ -3,88 +3,69 @@
 MDRIP ?= $(JIRI_ROOT)/third_party/go/bin/mdrip
 
 .DELETE_ON_ERROR:
-.DEFAULT_GOAL := build
+.DEFAULT_GOAL := all
+
+#############################################################################
+# A note about debugging this Makefile
+#
+# Due to the way testing is managed there is a somewhat complex set of tasks
+# and dependencies which can be hard to get a sense of by looking at the
+# definitions in this Makefile. Additionally, targets for creating and testing
+# shell scripts from tutorial content can take a while to run. Some tips are
+# below to help anyone who might need to make edits or additions.
+#
+# Run a task with make's debug output. This will show information about the
+# target being run as well as it's prerequisites and whether make will rebuild
+# the target.
+#
+#     make <target> --debug
+#
+# Force the target and all prerequisites to build regardless of mtimes:
+#
+#     make <target> --always-make
+#
+# Do a dry run, this will spit out what will happen without actually building
+# the targets. This is probably helpful for creating new targets with
+# complex and/or slow prerequisites.
+#
+#     make <target> --dry-run
+#
+
+#############################################################################
+# Tooling and infrastructure
 
 # Add node and npm to PATH. Note, we run npm using 'node npm' to avoid relying
 # on the shebang line in the npm script, which can exceed the Linux shebang
 # length limit on Jenkins.
 NODE_DIR := $(shell jiri v23-profile list --info Target.InstallationDir nodejs)
+# Once the JS tutorials stop running npm, we can simplify this to setting
+# npm := node $(NODE_DIR)/bin/npm and using $(npm) in place of npm elsewhere
+# in this file.
 NPM_DIR := $(shell mktemp -d "/tmp/XXXXXX")
 export PATH := $(NPM_DIR):$(NODE_DIR)/bin:$(PATH)
 
-.PHONY: npm-executable
-npm-executable:
-	echo 'node $(NODE_DIR)/bin/npm "$$@"' > $(NPM_DIR)/npm
-	chmod +x $(NPM_DIR)/npm
+# SEE: https://www.gnu.org/software/make/manual/html_node/Chained-Rules.html
+npm = $(NPM_DIR)/npm
+.INTERMEDIATE: $(npm)
+$(npm):
+	echo 'node $(NODE_DIR)/bin/npm "$$@"' > $(npm)
+	chmod +x $(npm)
 
-# TODO(sadovsky):
-# - Add "site-test" unit tests
-# - "identity" subdir (needed by identity service?)
-# - deploy-production rule
+# mdrip is a tool for extracting shell scripts from any markdown content which
+# might have code blocks in it (like the tutorials). The mdrip tool is also
+# capable of running the extracted code in a subshell, simulating a user
+# stepping through the tutorials in a step by step fashion.
+$(MDRIP):
+	jiri go install github.com/monopole/mdrip
 
-define BROWSERIFY
-	@mkdir -p $(dir $2)
-	browserify $1 -d -o $2
-endef
-
-node_modules: package.json npm-executable
-	npm prune
-	npm install
-	touch $@
-
-# NOTE(sadovsky): Some files under public/{css,js} were copied over from the
-# node_modules directory as follows:
-# cp node_modules/highlight.js/styles/github.css public/css
-# cp node_modules/material-design-lite/material*css* public/css
-# cp node_modules/material-design-lite/material*js* public/js
-
-# NOTE(sadovsky): Newer versions of postcss-cli and autoprefixer use JavaScript
-# Promises, which doesn't work with Vanadium's old version of node, 0.10.24.
-public/css/bundle.css: $(shell find stylesheets) node_modules
-	lessc -sm=on stylesheets/index.less | postcss -u autoprefixer > $@
-
-public/js/bundle.js: browser/index.js $(shell find browser) node_modules
-	$(call BROWSERIFY,$<,$@)
-
-################################################################################
-# Build, serve, watch and deploy
-
-build: $(MDRIP) node_modules public/css/bundle.css public/js/bundle.js gen-scripts
-	haiku build --helpers helpers.js --build-dir $@
-
-.PHONY: serve
-serve: build
-	@static build -H '{"Cache-Control": "no-cache, must-revalidate"}'
-
-# 'entr' can be installed on Debian/Ubuntu using 'apt-get install entr', and on
-# OS X using 'brew install entr'.
-.PHONY: watch
-watch: browser/ content/ public/ stylesheets/ templates/
-	@echo "Watching for changes in $^"
-	@find $^ | entr $(MAKE) build
+#############################################################################
+# Variables, functions, and helpers
 
 TMPDIR := $(shell mktemp -d "/tmp/XXXXXX")
 HEAD := $(shell git rev-parse HEAD)
 
-# TODO(sadovsky): Check that we're in a clean master branch. Also, automate
-# deployment so that changes are picked up automatically.
-.PHONY: deploy
-deploy: clean build
-	git clone git@github.com:vanadium/vanadium.github.io.git $(TMPDIR)
-	rm -rf $(TMPDIR)/*
-	rsync -r build/* $(TMPDIR)
-	cd $(TMPDIR) && git add -A && git commit -m "pull $(HEAD)" && git push
-
-################################################################################
-# Clean and lint
-
-.PHONY: clean
-clean:
-	rm -rf build node_modules public/**/bundle.* public/sh public/tutorials
-
-.PHONY: lint banned_words
-lint: banned_words node_modules
-	jshint .
+bundles := public/css/bundle.css public/js/bundle.js
+haiku_inputs := $(shell find public/* content/* templates/*)
 
 # A list of case-sensitive banned words.
 BANNED_WORDS := Javascript node.js Oauth
@@ -98,8 +79,15 @@
 		fi \
 	done
 
-################################################################################
-# Tutorial script generation and tests
+define BROWSERIFY
+	@mkdir -p $(dir $2)
+	browserify $1 -d -o $2
+endef
+
+#############################################################################
+# Variables, functions, and helpers
+#
+# It's important these are defined above any targets that may depend on them.
 
 install_md = installation/step-by-step.md
 install_sh = public/sh/vanadium-install.sh
@@ -154,12 +142,81 @@
 	$(scenario)-e-setup.sh \
 	$(scenario)-f-setup.sh
 
+depsCommon = \
+	content/$(tutSetup).md \
+	content/$(tutCheckup).md \
+	content/$(tutWipeSlate).md
+
 # This target builds the hosted web assets for the JavaScript tutorials.
 jsTutorialResults := public/tutorials/javascript/results
 
-# Install mdrip if needed.
-$(MDRIP):
-	jiri go install github.com/monopole/mdrip
+scripts := $(completerScripts) $(setupScripts) $(jsTutorialResults)
+
+#############################################################################
+# Target definitions
+
+# Silence error output "make: `build' is up to date." for tools like watch by
+# adding @true.
+.PHONY: all
+all: build
+	@true
+
+node_modules: package.json | $(npm)
+	npm prune
+	npm install
+	touch $@
+
+# NOTE(sadovsky): Some files under public/{css,js} were copied over from the
+# node_modules directory as follows:
+# cp node_modules/highlight.js/styles/github.css public/css
+# cp node_modules/material-design-lite/material*css* public/css
+# cp node_modules/material-design-lite/material*js* public/js
+
+# NOTE(sadovsky): Newer versions of postcss-cli and autoprefixer use JavaScript
+# Promises, which doesn't work with Vanadium's old version of node, 0.10.24.
+public/css/bundle.css: $(shell find stylesheets/*) node_modules
+	lessc -sm=on stylesheets/index.less | postcss -u autoprefixer > $@
+
+public/js/bundle.js: browser/index.js $(shell find browser) node_modules
+	$(call BROWSERIFY,$<,$@)
+
+build: $(bundles) $(scripts) $(haiku_inputs) node_modules | $(MDRIP)
+	haiku build --helpers helpers.js --build-dir $@
+	@touch $@
+
+################################################################################
+# Task definitions, targets without build artifacts.
+
+.PHONY: serve
+serve: build
+	@static build -H '{"Cache-Control": "no-cache, must-revalidate"}'
+
+# 'entr' can be installed on Debian/Ubuntu using 'apt-get install entr', and on
+# OS X using 'brew install entr'.
+.PHONY: watch
+watch: browser/ content/ public/ stylesheets/ templates/
+	@echo "Watching for changes in $^"
+	@find $^ | entr $(MAKE) build
+
+# TODO(sadovsky): Check that we're in a clean master branch. Also, automate
+# deployment so that changes are picked up automatically.
+.PHONY: deploy
+deploy: clean build
+	git clone git@github.com:vanadium/vanadium.github.io.git $(TMPDIR)
+	rm -rf $(TMPDIR)/*
+	rsync -r build/* $(TMPDIR)
+	cd $(TMPDIR) && git add -A && git commit -m "pull $(HEAD)" && git push
+
+.PHONY: clean
+clean:
+	rm -rf build node_modules public/**/bundle.* public/sh public/tutorials
+
+.PHONY: lint banned_words
+lint: banned_words node_modules
+	jshint .
+
+################################################################################
+# Tutorial script generation and tests
 
 # Vanadium install script.
 # This can be run as a prerequisite for tutorial setup.
@@ -186,11 +243,6 @@
 	mkdir -p $(@D)
 	$(MDRIP) --preambled 0 completer $^ > $@
 
-depsCommon = \
-	content/$(tutSetup).md \
-	content/$(tutCheckup).md \
-	content/$(tutWipeSlate).md
-
 # Targets of the from test-{tutorial-name} are meant for interactive use to test
 # an individual tutorial and/or extract the code associated with said tutorial.
 # The code winds up in $V_TUT/src, where $V_TUT is defined in tutSetup.
@@ -363,7 +415,8 @@
 test: test-site test-tutorials-core test-tutorials-js-node test-tutorials-java
 
 .PHONY: test-site
-test-site: build
+test-site: build node_modules
+	tape test/test-*.js
 
 # Test core tutorials against an existing development install.
 #
@@ -467,6 +520,3 @@
 	jiri go install v.io/v23/... v.io/x/ref/...
 	V_TUT=$(abspath $@) $(MDRIP) --subshell --blockTimeOut 1m buildjs $^
 	rm -rf $(abspath $@)/node_modules
-
-.PHONY: gen-scripts
-gen-scripts: $(completerScripts) $(setupScripts) $(jsTutorialResults)
diff --git a/content/community/coding-guidelines.md b/content/community/coding-guidelines.md
index 610147d..8ee38fb 100644
--- a/content/community/coding-guidelines.md
+++ b/content/community/coding-guidelines.md
@@ -113,10 +113,9 @@
 [Effective Go]: http://golang.org/doc/effective_go.html
 [Google Shell Style Guide]: https://google-styleguide.googlecode.com/svn/trunk/shell.xml
 [Node.js Style Guide]: https://github.com/felixge/node-style-guide
-[RPC]: ../glossary.html#remote-procedure-call-rpc-
-[VDL]: ../glossary.html#vandium-definition-language-vdl-
+[RPC]: /glossary.html#remote-procedure-call-rpc-
+[VDL]: /glossary.html#vandium-definition-language-vdl-
 [brad talk]: http://talks.golang.org/2014/gocon-tokyo.slide#36
-[contributor installation]: contributing.html#repositories
 [git repositories]: https://github.com/vanadium
 [gofmt]: https://golang.org/cmd/gofmt/
 [packages]: https://golang.org/doc/code.html#PackagePaths
diff --git a/content/community/contributing.md b/content/community/contributing.md
index 2e6d520..1244060 100644
--- a/content/community/contributing.md
+++ b/content/community/contributing.md
@@ -177,10 +177,10 @@
 *  [v.io/review](https://v.io/review): Takes you to your review dashboard.
 *  v.io/c/[num]: Takes you to the review for a specific change.
 
-[installation instructions]: ../installation/
+[installation instructions]: /installation/
 [cla]: https://cla.developers.google.com/about/google-individual?csw=1
 [corp-cla]: https://cla.developers.google.com/about/google-corporate?csw=1
 [issue tracker]: https://github.com/vanadium/issues/issues
 [git]: http://git-scm.com/
 [gerrit]: https://vanadium-review.googlesource.com
-[jiri]: ../tools/jiri.html
+[jiri]: /tools/jiri.html
diff --git a/content/concepts/device-management.md b/content/concepts/device-management.md
index b86183d..462c45f 100644
--- a/content/concepts/device-management.md
+++ b/content/concepts/device-management.md
@@ -113,4 +113,4 @@
 perceived value of the app feature requesting the new capability.
 
 [docker]: https://www.docker.com/
-[vanadium-security]: security.html
+[vanadium-security]: /concepts/security.html
diff --git a/content/concepts/naming.md b/content/concepts/naming.md
index a0fee89..33e13a0 100644
--- a/content/concepts/naming.md
+++ b/content/concepts/naming.md
@@ -146,4 +146,4 @@
 [DNS]: http://en.wikipedia.org/wiki/Domain_Name_System
 [forest]: http://en.wikipedia.org/wiki/Forest_(graph_theory)#forest
 [Unix Filesystem]: http://en.wikipedia.org/wiki/Unix_File_System
-[endpoint]: ../glossary.html#endpoint
+[endpoint]: /glossary.html#endpoint
diff --git a/content/concepts/rpc.md b/content/concepts/rpc.md
index 559ea60..3fd0f40 100644
--- a/content/concepts/rpc.md
+++ b/content/concepts/rpc.md
@@ -119,5 +119,5 @@
 
 [RPC]: http://en.wikipedia.org/wiki/Remote_procedure_call
 [communication]: http://en.wikipedia.org/wiki/Inter-process_communication
-[VDL specification]: ../designdocs/vdl-spec.html
-[VOM specification]: ../designdocs/vom-spec.html
+[VDL specification]: /designdocs/vdl-spec.html
+[VOM specification]: /designdocs/vom-spec.html
diff --git a/content/concepts/security.md b/content/concepts/security.md
index 9c29df7..872cd70 100644
--- a/content/concepts/security.md
+++ b/content/concepts/security.md
@@ -359,7 +359,7 @@
 [Public-key certificate]: http://en.wikipedia.org/wiki/Public_key_certificate
 [Forward-secrecy]: http://en.wikipedia.org/wiki/Forward_secrecy
 [NaCl box]: http://nacl.cr.yp.to/box.html
-[Vanadium authentication protocol]: ../designdocs/authentication.html
+[Vanadium authentication protocol]: /designdocs/authentication.html
 [TPM]: http://en.wikipedia.org/wiki/Trusted_Platform_Module
 [Simple Distributed Security Infrastructure]: http://people.csail.mit.edu/rivest/sdsi11.html#secoverview
 [personally identifiable information]: http://en.wikipedia.org/wiki/Personally_identifiable_information
diff --git a/content/concepts/syncbase-overview.md b/content/concepts/syncbase-overview.md
index 6764718..8b69559 100644
--- a/content/concepts/syncbase-overview.md
+++ b/content/concepts/syncbase-overview.md
@@ -408,4 +408,4 @@
 It would not support Put/Get/Delete/Scan/Query. By removing that functionality,
 we could potentially increase scalability and simplify deployment.
 
-[Vanadium security model]: security.html
+[Vanadium security model]: /concepts/security.html
diff --git a/content/designdocs/authentication.md b/content/designdocs/authentication.md
index e413300..4e9aa15 100644
--- a/content/designdocs/authentication.md
+++ b/content/designdocs/authentication.md
@@ -125,11 +125,11 @@
   - To prevent active network attackers from learning the Client's blessings.
 
 [authenticated-encryption]: http://en.wikipedia.org/wiki/Authenticated_encryption
-[security concepts]: ../concepts/security.html
+[security concepts]: /concepts/security.html
 [Elliptic curve Diffie-Hellman]: http://en.wikipedia.org/wiki/Elliptic_curve_Diffie%E2%80%93Hellman
 [session resumption is not used]: https://secure-resumption.com/#channelbindings
 [channel ids]: http://tools.ietf.org/html/draft-balfanz-tls-channelid-00
-[principal]: ../glossary.html#principal
+[principal]: /glossary.html#principal
 [`v.io/v23/security.Blessings`]: https://godoc.org/v.io/v23/security#Blessings
 [`v.io/v23/security.Principal`]: https://godoc.org/v.io/v23/security#Principal
 [`v.io/x/ref/runtime/internal/rpc/stream`]: https://godoc.org/v.io/x/ref/runtime/internal/rpc/stream
diff --git a/content/designdocs/identity-service.md b/content/designdocs/identity-service.md
index db02169..2f5e4d8 100644
--- a/content/designdocs/identity-service.md
+++ b/content/designdocs/identity-service.md
@@ -42,7 +42,7 @@
 
 Before diving into the details of [`identityd`]'s design, some prerequisites:
 
-- [Principals, Blessings and Caveats](../concepts/security.html).
+- [Principals, Blessings and Caveats](/concepts/security.html).
 - Third-party caveats: In a nutshell, a third-party caveat is a restriction on
   the use of a blessing that must be validated by a party other than the two
   communicating with and authorizing each other. The [blessing] with a
@@ -203,15 +203,15 @@
 
 
 [`identityd`]: https://github.com/vanadium/go.ref/tree/master/services/identity/identityd
-[blessing]: ../glossary.html#blessing
-[discharge]: ../glossary.html#discharge
+[blessing]: /glossary.html#blessing
+[discharge]: /glossary.html#discharge
 [OAuth2]: http://oauth.net/2/
-[caveat]: ../glossary.html#caveat
+[caveat]: /glossary.html#caveat
 [HMAC]: http://en.wikipedia.org/wiki/Hash-based_message_authentication_code
 [`principal`]: https://github.com/vanadium/go.ref/tree/master/cmd/principal
 [web service flow]: https://developers.google.com/accounts/docs/OAuth2WebServer
-[blessing root]: ../glossary.html#blessing-root
-[authentication protocol]: authentication.html
+[blessing root]: /glossary.html#blessing-root
+[authentication protocol]: /designdocs/authentication.html
 [_Expiry_]: https://github.com/vanadium/go.v23/blob/master/security/caveat.vdl
 [_Method_]: https://github.com/vanadium/go.v23/blob/master/security/caveat.vdl
 [_PeerBlessings_]: https://github.com/vanadium/go.v23/blob/master/security/caveat.vdl
diff --git a/content/designdocs/vom-spec.md b/content/designdocs/vom-spec.md
index b2f9d1d..0b42790 100644
--- a/content/designdocs/vom-spec.md
+++ b/content/designdocs/vom-spec.md
@@ -297,8 +297,8 @@
 }
 ```
 
-[VDL]: vdl-spec.html
-[VDL type system]: vdl-spec.html#types
-[RPC]: ../concepts/rpc.html
+[VDL]: /designdocs/vdl-spec.html
+[VDL type system]: /designdocs/vdl-spec.html#types
+[RPC]: /concepts/rpc.html
 [gob]: http://golang.org/pkg/encoding/gob/
 [IEEE 754]: http://en.wikipedia.org/wiki/IEEE_floating_point
diff --git a/content/example-apps.md b/content/example-apps.md
index 075eeb0..ed1c12c 100644
--- a/content/example-apps.md
+++ b/content/example-apps.md
@@ -52,7 +52,7 @@
 inside a running instance of the P2B application.
 
 [todos]: https://github.com/vanadium/todos
-[syncbase]: /syncbase/index.html
+[syncbase]: /concepts/syncbase-overview.html
 [todos-readme]: https://github.com/vanadium/todos/blob/master/README.md
 [chat]: https://github.com/vanadium/chat
 [web client]: https://chat.v.io
diff --git a/content/glossary.md b/content/glossary.md
index 0575bfa..a1762ac 100644
--- a/content/glossary.md
+++ b/content/glossary.md
@@ -414,13 +414,13 @@
 WSPR as a concept will move from a freestanding server into a browser
 extension, and ultimately become native to browsers.
 
-[naming-concepts]: concepts/naming.html
-[rpc-concepts]: concepts/rpc.html
-[vdl-spec]: designdocs/vdl-spec.html
-[vom-spec]: designdocs/vom-spec.html
+[naming-concepts]: /concepts/naming.html
+[rpc-concepts]: /concepts/rpc.html
+[vdl-spec]: /designdocs/vdl-spec.html
+[vom-spec]: /designdocs/vom-spec.html
 [gob]: http://golang.org/pkg/encoding/gob/
 [key pair]: http://en.wikipedia.org/wiki/Public-key_cryptography
 [certificate forging]: https://www.linshunghuang.com/papers/mitm.pdf
 [ssh-agent]: http://en.wikipedia.org/wiki/Ssh-agent
 [vanadium-element]: http://en.wikipedia.org/wiki/Vanadium
-[Security Concepts]: concepts/security.html
+[Security Concepts]: /concepts/security.html
diff --git a/content/tools/identity-service-faq.md b/content/tools/identity-service-faq.md
index 21f7319..a4ab6a1 100644
--- a/content/tools/identity-service-faq.md
+++ b/content/tools/identity-service-faq.md
@@ -75,9 +75,9 @@
 
 https://github.com/vanadium/go.ref/services/identity/identityd/main.go
 
-[blessing root]: ../glossary.html#blessing-root
-[Vanadium Security Model]: ../concepts/security.html
-[principals]: ../glossary.html#principal
-[design-doc]: ../designdocs/identity-service.html
-[terms of service]: ../tos.html
-[discharge]: ../glossary.html#discharge
+[blessing root]: /glossary.html#blessing-root
+[Vanadium Security Model]: /concepts/security.html
+[principals]: /glossary.html#principal
+[design-doc]: /designdocs/identity-service.html
+[terms of service]: /tos.html
+[discharge]: /glossary.html#discharge
diff --git a/content/tools/jiri.md b/content/tools/jiri.md
index ed9c0c9..95225c2 100644
--- a/content/tools/jiri.md
+++ b/content/tools/jiri.md
@@ -9,7 +9,7 @@
 
 `jiri` is available here: https://github.com/vanadium/go.jiri
 
-The "fetch repositories" step of [Vanadium installation](../installation/)
+The "fetch repositories" step of [Vanadium installation](/installation/)
 installs the `jiri` tool in `$JIRI_ROOT/devtools/bin`.
 
 Run `jiri help` to learn more about the tool. (Note, this command assumes your
diff --git a/content/tools/services.md b/content/tools/services.md
index ebb4711..8d0a1f4 100644
--- a/content/tools/services.md
+++ b/content/tools/services.md
@@ -26,9 +26,9 @@
 
 [proxy Go documentation]: https://godoc.org/v.io/x/ref/services/proxy
 [`mounttable` Go documentation]: https://godoc.org/v.io/x/ref/services/mounttable
-[Mount table]: ../glossary.html#mount-table
-[Naming Concepts]: ../concepts/naming.html
-[Security Concepts]: ../concepts/security.html
+[Mount table]: /glossary.html#mount-table
+[Naming Concepts]: /concepts/naming.html
+[Security Concepts]: /concepts/security.html
 [identityd Go documentation]: https://godoc.org/v.io/x/ref/services/identity/identityd
-[terms of service]: ../tos.html
-[Identity service FAQ]: identity-service-faq.html
+[terms of service]: /tos.html
+[Identity service FAQ]: /tools/identity-service-faq.html
diff --git a/content/tools/vanadium-chrome-extension.md b/content/tools/vanadium-chrome-extension.md
index a9a0924..0ec765a 100644
--- a/content/tools/vanadium-chrome-extension.md
+++ b/content/tools/vanadium-chrome-extension.md
@@ -67,7 +67,7 @@
 app, and finally to the web app.
 
 [background page]: https://developer.chrome.com/extensions/background_pages "Background Pages"
-[blessing]: ../glossary.html#blessing
+[blessing]: /glossary.html#blessing
 [content script]: https://developer.chrome.com/extensions/content_scripts "Content Scripts"
 [Go]: http://golang.org/ "The Go Programming Language"
 [Native Client]: https://developer.chrome.com/native-client "Native Client"
diff --git a/content/tos.md b/content/tos.md
index 2d7e21a..bce083c 100644
--- a/content/tos.md
+++ b/content/tos.md
@@ -8,7 +8,7 @@
 [Google APIs Terms of Service], and Google's general [Privacy Policy].
 
 [v.io]: https://v.io
-[Vanadium Cloud Services]: tools/services.html
+[Vanadium Cloud Services]: /tools/services.html
 [Terms of Service]: https://www.google.com/intl/en/policies/terms/
 [Google APIs Terms of Service]: https://developers.google.com/terms/
 [Privacy Policy]: https://www.google.com/intl/en/policies/privacy/
diff --git a/package.json b/package.json
index 068eecc..8454560 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,14 @@
 {
   "name": "website",
   "version": "0.0.1",
+  "private": true,
+  "description": "Source for the Vanadium website",
+  "repository": {
+    "type": "git",
+    "url": "https://vanadium.googlesource.com/website"
+  },
+  "author": "The Vanadium Authors",
+  "license": "BSD-style",
   "browserify": {
     "transform": [
       "browserify-shim"
@@ -21,11 +29,18 @@
     "autoprefixer": "^5.2.0",
     "browserify": "^13.0.0",
     "browserify-shim": "^3.8.12",
+    "cheerio": "^0.20.0",
+    "format": "^0.2.2",
+    "graceful-fs": "^4.1.3",
     "haiku": "^0.2.13",
     "jshint": "^2.9.1",
     "less": "^2.5.3",
     "marked": "^0.3.5",
     "node-static": "^0.7.7",
-    "postcss-cli": "^1.4.0"
+    "postcss-cli": "^1.4.0",
+    "powerwalk": "^2.0.2",
+    "pump": "^1.0.1",
+    "tape": "^4.4.0",
+    "through2": "^2.0.0"
   }
 }
diff --git a/test/link-stream.js b/test/link-stream.js
new file mode 100644
index 0000000..c8d5d86
--- /dev/null
+++ b/test/link-stream.js
@@ -0,0 +1,67 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Returns a stream that consumes filenames corresponding to HTML pages, and
+// emits anchor urls extracted from these HTML pages. Emits any given url at
+// most once. Strips fragments from urls, and does not emit fragment-only
+// URLs.
+var cheerio = require('cheerio');
+var fs = require('graceful-fs');
+var path = require('path');
+var through = require('through2');
+
+module.exports = create;
+
+function create() {
+  return through.obj(write);
+
+  // Called every-time a file is emitted from the powerwalk stream.
+  function write(buffer, enc, callback) {
+    var stream = this;
+    var file = buffer.toString();
+
+    // Do not scrape non-html files.
+    if (path.extname(file) !== '.html') {
+      callback();
+      return;
+    }
+
+    fs.readFile(file, function ondata(err, data) {
+      if (err) {
+        callback(err);
+        return;
+      }
+
+      // Scrape links.
+      var $ = cheerio.load(data);
+      $('a').each(function(i, element) {
+        var href = $(element).attr('href') || '';
+        // Remove trailing fragements.
+        href = href.replace(/html#(.*)/, 'html');
+        var isFragement = !!href.match(/^#/);
+
+        // Filter out any empty hrefs, or if the href is only an anchor tag (a
+        // link to a heading in the current document).
+        if (!href || isFragement) {
+          return;
+        }
+
+        // Ignore external links.
+        // NOTE: This could be made as an option if there is a need to test
+        // outbound links.
+        var isExternal = !!href.match(/^http/);
+        if (isExternal && !stream._external) {
+          return;
+        }
+
+        stream.push({
+          destination: href,
+          source: file
+        });
+      });
+
+      callback();
+    });
+  }
+}
diff --git a/test/test-page-links.js b/test/test-page-links.js
new file mode 100644
index 0000000..d8b1c92
--- /dev/null
+++ b/test/test-page-links.js
@@ -0,0 +1,70 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Test links which appear on the site.
+var format = require('format');
+var fs = require('graceful-fs');
+var linkstream = require('./link-stream');
+var path = require('path');
+var powerwalk = require('powerwalk');
+var pump = require('pump');
+var test = require('tape');
+var through = require('through2');
+
+var build = path.resolve(__dirname, '../build');
+
+test('inbound links', function(t) {
+  var fileStream = powerwalk(build);
+  var ls = linkstream();
+  var ts = through.obj(write);
+
+  // Called everytime the link-stream emits a "data" event.
+  function write(data, enc, callback) {
+    // HREF is the HTML attribute extracted from anchor tags with a
+    // destination that links back into the site.
+    var href = data.destination;
+    // To aid in debugging, set 'source' to the original source of this link -
+    // the Markdown page from which the HTML containing the link was
+    // generated.
+    var source = data.source
+          .replace(build, './content')
+          .replace('.html', '.md');
+    var prefix = format('"%s" linked from "%s" - ', href, source);
+
+    t.equal(href[0], '/', format('%s should be absolute', prefix));
+
+    // The linked destination should have a file in the build directory.
+    var file = path.join(build, href);
+    stat(file, function onstat(err, stats) {
+      t.error(err, format('%s should exist', prefix));
+      callback();
+    });
+  }
+
+  // Pipe the three streams together using the pump module, passing t.end as
+  // the final callback. The t.end method comes from the tape module and takes
+  // a callback wich is fired when pump encounters an end-of-stream event
+  // (either "end" or "error") anywhere in the pipeline.
+  //
+  // SEE: https://www.npmjs.com/package/pump
+  pump(fileStream, ls, ts, function done(err) {
+    t.error(err, 'streaming link pipeline should not error');
+    t.end();
+  });
+});
+
+function stat(pathname, callback) {
+  fs.stat(pathname, function onstat(err, stats) {
+    if (err) {
+      callback(err);
+      return;
+    }
+
+    if (stats.isDirectory()) {
+      stat(path.join(pathname, 'index.html'), callback);
+    } else {
+      callback(null, stats);
+    }
+  });
+}