diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..808891686
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+    "githubPullRequests.ignoredPullRequestBranches": [
+        "main"
+    ]
+}
diff --git a/BUILD.bazel b/BUILD.bazel
index 910fec18b..16c96ce15 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -6,26 +6,26 @@ exports_files(["LICENSE"])
 
 genrule(
     name = "bundle",
-    outs = ["bundle.zip"],
     srcs = [
         "LICENSE",
         "//codelab-elements:README.md",
         "//codelab-elements:all_files",
-        "@prettify//:prettify",
+        "@prettify",
         "@polyfill//:custom_elements",
         "@polyfill//:native_shim",
     ],
+    outs = ["bundle.zip"],
     cmd = "zip -j $@ $(SRCS)",
 )
 
 genrule(
     name = "npm_dist",
-    outs = ["npm_dist.zip"],
     srcs = [
         "LICENSE",
         "package.json",
         "//codelab-elements:README.md",
         "//codelab-elements:all_files",
     ],
+    outs = ["npm_dist.zip"],
     cmd = "zip -j $@ $(SRCS)",
 )
diff --git a/WORKSPACE b/WORKSPACE
index 5e2aec6c6..b1033f5ac 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1,163 +1,31 @@
-workspace(name = "googlecodelabs_custom_elements")
+workspace(name="googlecodelabs_tools")
 
-maven_server(
-    name = "default",
-    url = "https://repo1.maven.org/maven2/"
-)
-
-maven_jar(
-    name = "org_apache_httpcomponents_httpclient",
-    artifact = "org.apache.httpcomponents:httpclient:4.5.5",
-)
-
-maven_jar(
-    name = "org_apache_httpcomponents_httpmime",
-    artifact = "org.apache.httpcomponents:httpmime:4.5.5",
-)
-
-maven_jar(
-    name = "org_apache_httpcomponents_httpcore",
-    artifact = "org.apache.httpcomponents:httpcore:4.4.9",
-)
-
-maven_jar(
-    name = "org_apache_commons_exec",
-    artifact = "org.apache.commons:commons-exec:1.3",
-)
-
-maven_jar(
-    name = "org_seleniumhq_selenium_api",
-    artifact = "org.seleniumhq:selenium-api:3.9.1",
-)
-
-maven_jar(
-    name = "org_seleniumhq_selenium_remote_driver",
-    artifact = "org.seleniumhq.selenium:selenium-remote-driver:3.8.1",
-)
-
-maven_jar(
-    name = "net_java_dev_jna_platform",
-    artifact = "net.java.dev:jna-client:4.5.1",
-)
-
-maven_jar(
-    name = "net_java_dev_jna",
-    artifact = "net.java.dev:jna:4.5.1",
-)
-
-maven_jar(
-    name = "net_bytebuddy",
-    artifact = "net.bytebuddy:byte-buddy:1.7.9",
-)
-
-maven_jar(
-    name = "com_squareup_okio",
-    artifact = "com.squareup:okio:1.14.0",
-)
-
-maven_jar(
-    name = "com_squareup_okhttp3_okhttp",
-    artifact = "com.squareup.okhttp3:okhttp:3.9.1",
-)
-
-maven_jar(
-    name = "cglib_nodep",
-    artifact = "cglib:cglib-nodep:3.2.6",
-)
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
 
-maven_jar(
-    name = "junit",
-    artifact = "junit:junit:4.12",
-)
-
-maven_jar(
-    name = "commons_logging",
-    artifact = "commons-logging:commons-logging:1.2",
-)
-
-maven_jar(
-    name = "commons_codec",
-    artifact = "commons-codec:commons-codec:1.11",
-)
-
-maven_jar(
-    name = "org_hamcrest_core",
-    artifact = "org.hamcrest:hamcrest-core:1.3",
-)
-
-new_http_archive(
-    name = "com_google_javascript_closure_compiler",
-    build_file = "third_party/BUILD.closure",
-    url = "https://repo1.maven.org/maven2/com/google/javascript/closure-compiler-unshaded/v20180805/closure-compiler-unshaded-v20180805.jar",
-)
-
-# Required by io_bazel_rules_webtesting.
-skylib_ver = "f9b0ff1dd3d119d19b9cacbbc425a9e61759f1f5"
 http_archive(
-    name = "bazel_skylib",
-    sha256 = "ce27a2007deda8a1de65df9de3d4cd93a5360ead43c5ff3017ae6b3a2abe485e",
-    strip_prefix = "bazel-skylib-{v}".format(v=skylib_ver),
-    urls = [
-        "https://github.com/bazelbuild/bazel-skylib/archive/{v}.tar.gz".format(v=skylib_ver),
+    name="io_bazel_rules_go",
+    sha256="91585017debb61982f7054c9688857a2ad1fd823fc3f9cb05048b0025c47d023",
+    urls=[
+        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.42.0/rules_go-v0.42.0.zip",
+        "https://github.com/bazelbuild/rules_go/releases/download/v0.42.0/rules_go-v0.42.0.zip",
     ],
 )
 
-rules_closure_ver = "0.9.0"
-http_archive(
-    name = "io_bazel_rules_closure",
-    sha256 = "054717a2e6a415001bc4c608b208723526bdf6cace3592ca6efb3749ba18ce21",
-    strip_prefix = "rules_closure-{v}".format(v=rules_closure_ver),
-    url = "https://github.com/shawnbuso/rules_closure/archive/{v}.zip".format(v=rules_closure_ver),
-)
-load("@io_bazel_rules_closure//closure:defs.bzl", "closure_repositories")
-closure_repositories()
-
-http_archive(
-    name = "io_bazel_rules_go",
-    sha256 = "53c8222c6eab05dd49c40184c361493705d4234e60c42c4cd13ab4898da4c6be",
-    url = "https://github.com/bazelbuild/rules_go/releases/download/0.10.0/rules_go-0.10.0.tar.gz",
-)
-load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_toolchains")
-go_rules_dependencies()
-go_register_toolchains()
-
-rules_webtesting_ver = "936c760cff973a63031be0d0518b40a228e224e3"
 http_archive(
-    name = "io_bazel_rules_webtesting",
-    sha256 = "797b75e792a34728a6a3846c7c3d3ad669f12cd8490b888cc969bad93d236b1b",
-    strip_prefix = "rules_webtesting-{v}".format(v=rules_webtesting_ver),
-    url = "https://github.com/bazelbuild/rules_webtesting/archive/{v}.zip".format(v=rules_webtesting_ver),
-)
-load(
-    "@io_bazel_rules_webtesting//web:repositories.bzl",
-    "browser_repositories",
-    "web_test_repositories",
-)
-web_test_repositories()
-browser_repositories(chromium = True)
-
-prettify_ver = "2013-03-04"
-new_http_archive(
-    name = "prettify",
-    build_file = "third_party/BUILD.prettify",
-    strip_prefix = "code-prettify-{v}".format(v=prettify_ver),
-    url = "https://github.com/google/code-prettify/archive/{v}.zip".format(v=prettify_ver),
+    name="gazelle",
+    sha256="d3fa66a39028e97d76f9e2db8f1b0c11c099e8e01bf363a923074784e451f809",
+    urls=[
+        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.33.0/bazel-gazelle-v0.33.0.tar.gz",
+        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.33.0/bazel-gazelle-v0.33.0.tar.gz",
+    ],
 )
 
-new_http_archive(
-    name = "polyfill",
-    build_file = "third_party/BUILD.polyfill",
-    sha256 = "9606cdeacbb67f21fb495a4b0a0e5ea6a137fc453945907822e1b930e77124d4",
-    strip_prefix = "custom-elements-1.0.8",
-    url = "https://github.com/webcomponents/custom-elements/archive/v1.0.8.zip",
-)
+load("@io_bazel_rules_go//go:deps.bzl",
+     "go_register_toolchains", "go_rules_dependencies")
+load("@gazelle//:deps.bzl", "gazelle_dependencies")
 
-git_repository(
-    name = "io_bazel_rules_sass",
-    remote = "https://github.com/bazelbuild/rules_sass.git",
-    tag = "0.0.3",
-)
+go_rules_dependencies()
 
-load("@io_bazel_rules_sass//sass:sass.bzl", "sass_repositories")
+go_register_toolchains(version="1.20.7")
 
-sass_repositories()
+gazelle_dependencies()
diff --git a/claat/BUILD.bazel b/claat/BUILD.bazel
new file mode 100644
index 000000000..6193b8085
--- /dev/null
+++ b/claat/BUILD.bazel
@@ -0,0 +1,22 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+load("@gazelle//:def.bzl", "gazelle")
+
+gazelle(name = "gazelle")
+
+go_library(
+    name = "claat_lib",
+    srcs = ["main.go"],
+    importpath = "github.com/googlecodelabs/tools/claat",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//claat/cmd",
+        "//claat/parser/gdoc",
+        "//claat/parser/md",
+    ],
+)
+
+go_binary(
+    name = "claat",
+    embed = [":claat_lib"],
+    visibility = ["//visibility:public"],
+)
diff --git a/claat/cmd/BUILD.bazel b/claat/cmd/BUILD.bazel
new file mode 100644
index 000000000..59fea6cd3
--- /dev/null
+++ b/claat/cmd/BUILD.bazel
@@ -0,0 +1,31 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "cmd",
+    srcs = [
+        "export.go",
+        "serve.go",
+        "update.go",
+        "util.go",
+    ],
+    importpath = "github.com/googlecodelabs/tools/claat/cmd",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//claat/fetch",
+        "//claat/parser/gdoc",
+        "//claat/parser/md",
+        "//claat/render",
+        "//claat/types",
+        "//claat/util",
+    ],
+)
+
+go_test(
+    name = "cmd_test",
+    srcs = ["export_test.go"],
+    data = glob(["testdata/**"]),
+    deps = [
+        ":cmd",
+        "@com_github_google_go_cmp//cmp:go_default_library",
+    ],
+)
diff --git a/claat/fetch/BUILD.bazel b/claat/fetch/BUILD.bazel
new file mode 100644
index 000000000..859f3af06
--- /dev/null
+++ b/claat/fetch/BUILD.bazel
@@ -0,0 +1,23 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "fetch",
+    srcs = ["fetch.go"],
+    importpath = "github.com/googlecodelabs/tools/claat/fetch",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//claat/fetch/drive/auth",
+        "//claat/nodes",
+        "//claat/parser",
+        "//claat/types",
+        "//claat/util",
+    ],
+)
+
+go_test(
+    name = "fetch_test",
+    srcs = ["fetch_test.go"],
+    data = glob(["testdata/**"]),
+    embed = [":fetch"],
+    deps = ["//claat/parser/gdoc"],
+)
diff --git a/claat/fetch/drive/auth/BUILD.bazel b/claat/fetch/drive/auth/BUILD.bazel
new file mode 100644
index 000000000..a4e99fd04
--- /dev/null
+++ b/claat/fetch/drive/auth/BUILD.bazel
@@ -0,0 +1,19 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "auth",
+    srcs = ["auth.go"],
+    importpath = "github.com/googlecodelabs/tools/claat/fetch/drive/auth",
+    visibility = ["//visibility:public"],
+    deps = [
+        "@org_golang_x_net//context:go_default_library",
+        "@org_golang_x_oauth2//:go_default_library",
+    ],
+)
+
+go_test(
+    name = "auth_test",
+    srcs = ["auth_test.go"],
+    embed = [":auth"],
+    deps = ["@org_golang_x_oauth2//:go_default_library"],
+)
diff --git a/claat/nodes/BUILD.bazel b/claat/nodes/BUILD.bazel
new file mode 100644
index 000000000..08f97ecf2
--- /dev/null
+++ b/claat/nodes/BUILD.bazel
@@ -0,0 +1,47 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "nodes",
+    srcs = [
+        "button.go",
+        "code.go",
+        "grid.go",
+        "header.go",
+        "iframe.go",
+        "image.go",
+        "import.go",
+        "infobox.go",
+        "itemslist.go",
+        "list.go",
+        "nodes.go",
+        "survey.go",
+        "text.go",
+        "url.go",
+        "youtube.go",
+    ],
+    importpath = "github.com/googlecodelabs/tools/claat/nodes",
+    visibility = ["//visibility:public"],
+)
+
+go_test(
+    name = "nodes_test",
+    srcs = [
+        "button_test.go",
+        "code_test.go",
+        "grid_test.go",
+        "header_test.go",
+        "iframe_test.go",
+        "image_test.go",
+        "import_test.go",
+        "infobox_test.go",
+        "itemslist_test.go",
+        "list_test.go",
+        "nodes_test.go",
+        "survey_test.go",
+        "text_test.go",
+        "url_test.go",
+        "youtube_test.go",
+    ],
+    embed = [":nodes"],
+    deps = ["@com_github_google_go_cmp//cmp:go_default_library"],
+)
diff --git a/claat/parser/BUILD.bazel b/claat/parser/BUILD.bazel
new file mode 100644
index 000000000..473f973a6
--- /dev/null
+++ b/claat/parser/BUILD.bazel
@@ -0,0 +1,21 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "parser",
+    srcs = [
+        "parse.go",
+        "trim.go",
+    ],
+    importpath = "github.com/googlecodelabs/tools/claat/parser",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//claat/nodes",
+        "//claat/types",
+    ],
+)
+
+go_test(
+    name = "parser_test",
+    srcs = ["trim_test.go"],
+    embed = [":parser"],
+)
diff --git a/claat/parser/gdoc/BUILD.bazel b/claat/parser/gdoc/BUILD.bazel
new file mode 100644
index 000000000..4c1b99a32
--- /dev/null
+++ b/claat/parser/gdoc/BUILD.bazel
@@ -0,0 +1,41 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "gdoc",
+    srcs = [
+        "css.go",
+        "html.go",
+        "parse.go",
+    ],
+    importpath = "github.com/googlecodelabs/tools/claat/parser/gdoc",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//claat/nodes",
+        "//claat/parser",
+        "//claat/types",
+        "//claat/util",
+        "@com_github_stoewer_go_strcase//:go_default_library",
+        "@com_github_x1ddos_csslex//:go_default_library",
+        "@org_golang_x_net//html:go_default_library",
+        "@org_golang_x_net//html/atom:go_default_library",
+    ],
+)
+
+go_test(
+    name = "gdoc_test",
+    srcs = [
+        "css_test.go",
+        "html_test.go",
+        "parse_test.go",
+    ],
+    embed = [":gdoc"],
+    deps = [
+        "//claat/nodes",
+        "//claat/parser",
+        "//claat/render",
+        "//claat/types",
+        "@com_github_google_go_cmp//cmp:go_default_library",
+        "@org_golang_x_net//html:go_default_library",
+        "@org_golang_x_net//html/atom:go_default_library",
+    ],
+)
diff --git a/claat/parser/md/BUILD.bazel b/claat/parser/md/BUILD.bazel
new file mode 100644
index 000000000..823fb5e1f
--- /dev/null
+++ b/claat/parser/md/BUILD.bazel
@@ -0,0 +1,40 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "md",
+    srcs = [
+        "html.go",
+        "parse.go",
+    ],
+    importpath = "github.com/googlecodelabs/tools/claat/parser/md",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//claat/nodes",
+        "//claat/parser",
+        "//claat/types",
+        "//claat/util",
+        "@com_github_stoewer_go_strcase//:go_default_library",
+        "@com_github_yuin_goldmark//:go_default_library",
+        "@com_github_yuin_goldmark//extension:go_default_library",
+        "@com_github_yuin_goldmark//renderer/html:go_default_library",
+        "@org_golang_x_net//html:go_default_library",
+        "@org_golang_x_net//html/atom:go_default_library",
+    ],
+)
+
+go_test(
+    name = "md_test",
+    srcs = [
+        "html_test.go",
+        "parse_test.go",
+    ],
+    embed = [":md"],
+    deps = [
+        "//claat/nodes",
+        "//claat/parser",
+        "//claat/types",
+        "@com_github_google_go_cmp//cmp:go_default_library",
+        "@org_golang_x_net//html:go_default_library",
+        "@org_golang_x_net//html/atom:go_default_library",
+    ],
+)
diff --git a/claat/render/BUILD.bazel b/claat/render/BUILD.bazel
new file mode 100644
index 000000000..8579492f2
--- /dev/null
+++ b/claat/render/BUILD.bazel
@@ -0,0 +1,39 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "render",
+    srcs = [
+        "html.go",
+        "lite.go",
+        "md.go",
+        "template.go",
+    ],
+    embedsrcs = [
+        "template-offline.html",
+        "template.html",
+        "template.md",
+    ],
+    importpath = "github.com/googlecodelabs/tools/claat/render",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//claat/nodes",
+        "//claat/parser/md",
+        "//claat/types",
+        "@org_golang_x_net//html:go_default_library",
+        "@org_golang_x_net//html/atom:go_default_library",
+    ],
+)
+
+go_test(
+    name = "render_test",
+    srcs = [
+        "html_test.go",
+        "template_test.go",
+    ],
+    embed = [":render"],
+    deps = [
+        "//claat/nodes",
+        "//claat/types",
+        "@com_github_google_go_cmp//cmp:go_default_library",
+    ],
+)
diff --git a/claat/types/BUILD.bazel b/claat/types/BUILD.bazel
new file mode 100644
index 000000000..fb5c91aaf
--- /dev/null
+++ b/claat/types/BUILD.bazel
@@ -0,0 +1,24 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "types",
+    srcs = [
+        "codelab.go",
+        "context.go",
+        "legacystatus.go",
+    ],
+    importpath = "github.com/googlecodelabs/tools/claat/types",
+    visibility = ["//visibility:public"],
+    deps = ["//claat/nodes"],
+)
+
+go_test(
+    name = "types_test",
+    srcs = [
+        "codelab_test.go",
+        "context_test.go",
+        "legacystatus_test.go",
+    ],
+    embed = [":types"],
+    deps = ["@com_github_google_go_cmp//cmp:go_default_library"],
+)
diff --git a/claat/util/BUILD.bazel b/claat/util/BUILD.bazel
new file mode 100644
index 000000000..52e627d2d
--- /dev/null
+++ b/claat/util/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "util",
+    srcs = ["util.go"],
+    importpath = "github.com/googlecodelabs/tools/claat/util",
+    visibility = ["//visibility:public"],
+)
+
+go_test(
+    name = "util_test",
+    srcs = ["util_test.go"],
+    embed = [":util"],
+    deps = ["@com_github_google_go_cmp//cmp:go_default_library"],
+)
diff --git a/codelab-elements/BUILD.bazel b/codelab-elements/BUILD.bazel
index abf395eaa..57a186558 100644
--- a/codelab-elements/BUILD.bazel
+++ b/codelab-elements/BUILD.bazel
@@ -9,16 +9,15 @@ exports_files(["README.md"])
 filegroup(
     name = "all_files",
     srcs = [
-        ":codelab_elements_js",
         ":codelab_elements_css",
-        ":codelab_index_js",
+        ":codelab_elements_js",
         ":codelab_index_css",
+        ":codelab_index_js",
     ],
 )
 
 genrule(
     name = "codelab_elements_js",
-    outs = ["codelab-elements.js"],
     srcs = [
         "//codelab-elements/google-codelab-analytics:google_codelab_analytics_bin",
         "//codelab-elements/google-codelab:google_codelab_bin",
@@ -26,35 +25,36 @@ genrule(
         "//codelab-elements/google-codelab-step:google_codelab_step_bin",
         "//codelab-elements/google-codelab-survey:google_codelab_survey_bin",
     ],
+    outs = ["codelab-elements.js"],
     cmd = concat("js"),
 )
 
 genrule(
     name = "codelab_elements_css",
-    outs = ["codelab-elements.css"],
     srcs = [
         "//codelab-elements/google-codelab:google_codelab_scss_bin",
         "//codelab-elements/google-codelab-about:google_codelab_about_scss_bin",
         "//codelab-elements/google-codelab-step:google_codelab_step_scss_bin",
         "//codelab-elements/google-codelab-survey:google_codelab_survey_scss_bin",
     ],
+    outs = ["codelab-elements.css"],
     cmd = concat("css"),
 )
 
 genrule(
     name = "codelab_index_js",
-    outs = ["codelab-index.js"],
     srcs = [
         "//codelab-elements/google-codelab-index:google_codelab_index_bin",
     ],
+    outs = ["codelab-index.js"],
     cmd = concat("js"),
 )
 
 genrule(
     name = "codelab_index_css",
-    outs = ["codelab-index.css"],
     srcs = [
         "//codelab-elements/google-codelab-index:google_codelab_index_scss_bin",
     ],
+    outs = ["codelab-index.css"],
     cmd = concat("css"),
 )
diff --git a/codelab-elements/demo/BUILD.bazel b/codelab-elements/demo/BUILD.bazel
index ff1355546..83e2aebb7 100644
--- a/codelab-elements/demo/BUILD.bazel
+++ b/codelab-elements/demo/BUILD.bazel
@@ -4,8 +4,12 @@ licenses(["notice"])
 
 exports_files(["LICENSE"])
 
-load("//codelab-elements/tools:defs.bzl",
-     "closure_js_library", "closure_js_binary", "closure_js_test")
+load(
+    "//codelab-elements/tools:defs.bzl",
+    "closure_js_binary",
+    "closure_js_library",
+    "closure_js_test",
+)
 
 # All static artifacts needed for various demo files.
 # Used by //tools:server target.
diff --git a/codelab-elements/google-codelab-about/BUILD.bazel b/codelab-elements/google-codelab-about/BUILD.bazel
index fc3e47424..021114745 100644
--- a/codelab-elements/google-codelab-about/BUILD.bazel
+++ b/codelab-elements/google-codelab-about/BUILD.bazel
@@ -4,20 +4,23 @@ licenses(["notice"])
 
 exports_files(["LICENSE"])
 
-load("//codelab-elements/tools:defs.bzl",
-     "closure_js_library", "closure_js_binary", "closure_js_test")
+load(
+    "//codelab-elements/tools:defs.bzl",
+    "closure_js_binary",
+    "closure_js_library",
+    "closure_js_test",
+)
 load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_template_library")
 load("@io_bazel_rules_sass//sass:sass.bzl", "sass_binary", "sass_library")
 
-
 filegroup(
     name = "google_codelab_about_files",
     srcs = glob([
         "*.html",
         "*.png",
     ]) + [
-        ":google_codelab_about_scss_bin",
         ":google_codelab_about_bin",
+        ":google_codelab_about_scss_bin",
     ],
 )
 
@@ -26,11 +29,11 @@ closure_js_library(
     name = "google_codelab_about",
     srcs = [
         "google_codelab_about.js",
-        "google_codelab_about_def.js"
+        "google_codelab_about_def.js",
     ],
     deps = [
-        "@io_bazel_rules_closure//closure/library",
         ":google_codelab_about_soy",
+        "@io_bazel_rules_closure//closure/library",
     ],
 )
 
@@ -48,5 +51,5 @@ sass_binary(
 
 closure_js_template_library(
     name = "google_codelab_about_soy",
-    srcs = ["google_codelab_about.soy"]
+    srcs = ["google_codelab_about.soy"],
 )
diff --git a/codelab-elements/google-codelab-analytics/BUILD.bazel b/codelab-elements/google-codelab-analytics/BUILD.bazel
index ddb9f7744..2c8b889f8 100644
--- a/codelab-elements/google-codelab-analytics/BUILD.bazel
+++ b/codelab-elements/google-codelab-analytics/BUILD.bazel
@@ -4,14 +4,17 @@ licenses(["notice"])
 
 exports_files(["LICENSE"])
 
-load("//codelab-elements/tools:defs.bzl",
-     "closure_js_library", "closure_js_binary", "closure_js_test")
-
+load(
+    "//codelab-elements/tools:defs.bzl",
+    "closure_js_binary",
+    "closure_js_library",
+    "closure_js_test",
+)
 
 filegroup(
     name = "google_codelab_analytics_files",
     srcs = [
-      ":google_codelab_analytics_bin",
+        ":google_codelab_analytics_bin",
     ],
 )
 
@@ -20,7 +23,7 @@ closure_js_library(
     name = "google_codelab_analytics",
     srcs = [
         "google_codelab_analytics.js",
-        "google_codelab_analytics_def.js"
+        "google_codelab_analytics_def.js",
     ],
     deps = [
         "@io_bazel_rules_closure//closure/library",
@@ -33,8 +36,8 @@ closure_js_test(
     srcs = ["google_codelab_analytics_test.js"],
     entry_points = ["googlecodelabs.CodelabAnalyticsTest"],
     deps = [
-      "@io_bazel_rules_closure//closure/library",
-      ":google_codelab_analytics"
+        ":google_codelab_analytics",
+        "@io_bazel_rules_closure//closure/library",
     ],
 )
 
diff --git a/codelab-elements/google-codelab-index/BUILD.bazel b/codelab-elements/google-codelab-index/BUILD.bazel
index 92df4bd63..03dc95701 100644
--- a/codelab-elements/google-codelab-index/BUILD.bazel
+++ b/codelab-elements/google-codelab-index/BUILD.bazel
@@ -4,8 +4,12 @@ licenses(["notice"])
 
 exports_files(["LICENSE"])
 
-load("//codelab-elements/tools:defs.bzl",
-     "closure_js_library", "closure_js_binary", "closure_js_test")
+load(
+    "//codelab-elements/tools:defs.bzl",
+    "closure_js_binary",
+    "closure_js_library",
+    "closure_js_test",
+)
 load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_template_library")
 load("@io_bazel_rules_sass//sass:sass.bzl", "sass_binary", "sass_library")
 
@@ -14,8 +18,8 @@ filegroup(
     srcs = glob([
         "*.html",
     ]) + [
-        ":google_codelab_index_scss_bin",
         ":google_codelab_index_bin",
+        ":google_codelab_index_scss_bin",
     ],
 )
 
@@ -24,25 +28,25 @@ closure_js_library(
     name = "google_codelab_index",
     srcs = [
         "google_codelab_index.js",
-        "google_codelab_index_def.js"
+        "google_codelab_index_def.js",
     ],
     deps = [
-        "@io_bazel_rules_closure//closure/library",
-        ":google_codelab_index_soy",
         ":google_codelab_index_cards",
-    ]
+        ":google_codelab_index_soy",
+        "@io_bazel_rules_closure//closure/library",
+    ],
 )
 
 closure_js_library(
     name = "google_codelab_index_cards",
     srcs = [
         "google_codelab_index_cards.js",
-        "google_codelab_index_cards_def.js"
+        "google_codelab_index_cards_def.js",
     ],
     deps = [
-        "@io_bazel_rules_closure//closure/library",
         ":google_codelab_index_soy",
-    ]
+        "@io_bazel_rules_closure//closure/library",
+    ],
 )
 
 # Compiled version of GoogleCodelabStep element, suitable for distribution.
diff --git a/codelab-elements/google-codelab-step/BUILD.bazel b/codelab-elements/google-codelab-step/BUILD.bazel
index 6ff5c7699..69614d895 100644
--- a/codelab-elements/google-codelab-step/BUILD.bazel
+++ b/codelab-elements/google-codelab-step/BUILD.bazel
@@ -4,20 +4,23 @@ licenses(["notice"])
 
 exports_files(["LICENSE"])
 
-load("//codelab-elements/tools:defs.bzl",
-     "closure_js_library", "closure_js_binary", "closure_js_test")
+load(
+    "//codelab-elements/tools:defs.bzl",
+    "closure_js_binary",
+    "closure_js_library",
+    "closure_js_test",
+)
 load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_template_library")
 load("@io_bazel_rules_sass//sass:sass.bzl", "sass_binary", "sass_library")
 
-
 filegroup(
     name = "google_codelab_step_files",
     srcs = glob([
         "*.html",
         "*.png",
     ]) + [
-        ":google_codelab_step_scss_bin",
         ":google_codelab_step_bin",
+        ":google_codelab_step_scss_bin",
     ],
 )
 
@@ -26,11 +29,11 @@ closure_js_library(
     name = "google_codelab_step",
     srcs = [
         "google_codelab_step.js",
-        "google_codelab_step_def.js"
+        "google_codelab_step_def.js",
     ],
     deps = [
-        "@io_bazel_rules_closure//closure/library",
         ":google_codelab_step_soy",
+        "@io_bazel_rules_closure//closure/library",
     ],
 )
 
@@ -51,12 +54,12 @@ sass_binary(
     src = "google_codelab_step.scss",
     deps = [
         ":syntax",
-    ]
+    ],
 )
 
 closure_js_template_library(
     name = "google_codelab_step_soy",
-    srcs = ["google_codelab_step.soy"]
+    srcs = ["google_codelab_step.soy"],
 )
 
 closure_js_test(
diff --git a/codelab-elements/google-codelab-survey/BUILD.bazel b/codelab-elements/google-codelab-survey/BUILD.bazel
index 03377abaf..7305ba4a8 100644
--- a/codelab-elements/google-codelab-survey/BUILD.bazel
+++ b/codelab-elements/google-codelab-survey/BUILD.bazel
@@ -4,8 +4,12 @@ licenses(["notice"])
 
 exports_files(["LICENSE"])
 
-load("//codelab-elements/tools:defs.bzl",
-    "closure_js_library", "closure_js_binary", "closure_js_test")
+load(
+    "//codelab-elements/tools:defs.bzl",
+    "closure_js_binary",
+    "closure_js_library",
+    "closure_js_test",
+)
 load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_template_library")
 load("@io_bazel_rules_sass//sass:sass.bzl", "sass_binary", "sass_library")
 
@@ -15,8 +19,8 @@ filegroup(
         "*.html",
         "*.png",
     ]) + [
-        ":google_codelab_survey_scss_bin",
         ":google_codelab_survey_bin",
+        ":google_codelab_survey_scss_bin",
     ],
 )
 
@@ -25,11 +29,11 @@ closure_js_library(
     name = "google_codelab_survey",
     srcs = [
         "google_codelab_survey.js",
-        "google_codelab_survey_def.js"
+        "google_codelab_survey_def.js",
     ],
     deps = [
-        "@io_bazel_rules_closure//closure/library",
         ":google_codelab_survey_soy",
+        "@io_bazel_rules_closure//closure/library",
     ],
 )
 
@@ -45,14 +49,14 @@ closure_js_test(
     srcs = ["google_codelab_survey_test.js"],
     entry_points = ["googlecodelabs.CodelabSurveyTest"],
     deps = [
-      "@io_bazel_rules_closure//closure/library",
-      ":google_codelab_survey"
+        ":google_codelab_survey",
+        "@io_bazel_rules_closure//closure/library",
     ],
 )
 
 closure_js_template_library(
     name = "google_codelab_survey_soy",
-    srcs = ["google_codelab_survey.soy"]
+    srcs = ["google_codelab_survey.soy"],
 )
 
 sass_binary(
diff --git a/codelab-elements/google-codelab/BUILD.bazel b/codelab-elements/google-codelab/BUILD.bazel
index ca4fcbde7..6b122ef14 100644
--- a/codelab-elements/google-codelab/BUILD.bazel
+++ b/codelab-elements/google-codelab/BUILD.bazel
@@ -4,8 +4,12 @@ licenses(["notice"])
 
 exports_files(["LICENSE"])
 
-load("//codelab-elements/tools:defs.bzl",
-     "closure_js_library", "closure_js_binary", "closure_js_test")
+load(
+    "//codelab-elements/tools:defs.bzl",
+    "closure_js_binary",
+    "closure_js_library",
+    "closure_js_test",
+)
 load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_template_library")
 load("@io_bazel_rules_sass//sass:sass.bzl", "sass_binary", "sass_library")
 
@@ -15,8 +19,8 @@ filegroup(
         "*.html",
         "img/**",
     ]) + [
-        ":google_codelab_scss_bin",
         ":google_codelab_bin",
+        ":google_codelab_scss_bin",
     ],
 )
 
@@ -28,9 +32,9 @@ closure_js_library(
         "google_codelab_def.js",
     ],
     deps = [
-        "@io_bazel_rules_closure//closure/library",
         ":google_codelab_soy",
-    ]
+        "@io_bazel_rules_closure//closure/library",
+    ],
 )
 
 # Compiled version of GoogleCodelabStep element, suitable for distribution.
diff --git a/codelab-elements/tools/BUILD.bazel b/codelab-elements/tools/BUILD.bazel
index a2148d130..bc23968fd 100644
--- a/codelab-elements/tools/BUILD.bazel
+++ b/codelab-elements/tools/BUILD.bazel
@@ -1,10 +1,13 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_binary")
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
 
 licenses(["notice"])
 
 # The gen_test_html.template is used by js_test rule.
 # See defs.bzl for details.
-exports_files(["LICENSE", "gen_test_html.template"])
+exports_files([
+    "LICENSE",
+    "gen_test_html.template",
+])
 
 # The JS tests runner.
 # See defs.bzl for details.
@@ -38,6 +41,20 @@ go_binary(
         "//codelab-elements/google-codelab-survey:google_codelab_survey_files",
         "@polyfill//:custom_elements",
         "@polyfill//:native_shim",
-        "@prettify//:prettify",
+        "@prettify",
+    ],
+)
+
+go_library(
+    name = "lib",
+    srcs = [
+        "server.go",
+        "webtest.go",
+    ],
+    importpath = "",
+    visibility = ["//visibility:private"],
+    deps = [
+        "@com_github_bazelbuild_rules_webtesting//go/webtest:go_default_library",
+        "@com_github_tebeka_selenium//:go_default_library",
     ],
 )
diff --git a/package-lock.json b/package-lock.json
index b3600c4dd..be90b6c90 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,13 +1,78 @@
 {
   "name": "codelab-elements",
-  "version": "4.0.0",
-  "lockfileVersion": 1,
+  "version": "1.0.1",
+  "lockfileVersion": 2,
   "requires": true,
+  "packages": {
+    "": {
+      "name": "codelab-elements",
+      "version": "1.0.1",
+      "license": "Apache-2.0",
+      "devDependencies": {
+        "@bazel/bazel": "^0.18.1"
+      }
+    },
+    "node_modules/@bazel/bazel": {
+      "version": "0.18.1",
+      "resolved": "https://registry.npmjs.org/@bazel/bazel/-/bazel-0.18.1.tgz",
+      "integrity": "sha512-2KjB0umWsW5or78hTG/RVLmAKmeouyDjqjNsGlOY3pzpj9yqDMG5iDPJh4Q1/hXxiDmF2qZWwqWIEh55LFZXaQ==",
+      "deprecated": "@bazel/bazel is deprecated, please use @bazel/bazelisk instead",
+      "dev": true,
+      "bin": {
+        "bazel": "bazel.js"
+      },
+      "optionalDependencies": {
+        "@bazel/bazel-darwin_x64": "0.18.0",
+        "@bazel/bazel-linux_x64": "0.18.0",
+        "@bazel/bazel-win32_x64": "0.18.0"
+      }
+    },
+    "node_modules/@bazel/bazel/node_modules/@bazel/bazel-darwin_x64": {
+      "version": "0.18.0",
+      "resolved": "https://registry.npmjs.org/@bazel/bazel-darwin_x64/-/bazel-darwin_x64-0.18.0.tgz",
+      "integrity": "sha512-um2OzgLL2Gd/W6joOpvrSTcqpnupliPNpwe/uE7sB0huBSJ/4Im0w2IlCTI6C7OfgMcbpUj4YxgUa9T6u6WY6w==",
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "bin": {
+        "bazel": "bazel-0.18.0-darwin-x86_64"
+      }
+    },
+    "node_modules/@bazel/bazel/node_modules/@bazel/bazel-linux_x64": {
+      "version": "0.18.0",
+      "resolved": "https://registry.npmjs.org/@bazel/bazel-linux_x64/-/bazel-linux_x64-0.18.0.tgz",
+      "integrity": "sha512-Rq8X8bL6SgQvbOHnfPhSgF6hp+f6Fbt2w6pRmBlFvV1J+CeUyrSrrRXfnnO1bjIuq05Ur3mV8ULA0qK6rtA5lQ==",
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "bin": {
+        "bazel": "bazel-0.18.0-linux-x86_64"
+      }
+    },
+    "node_modules/@bazel/bazel/node_modules/@bazel/bazel-win32_x64": {
+      "version": "0.18.0",
+      "resolved": "https://registry.npmjs.org/@bazel/bazel-win32_x64/-/bazel-win32_x64-0.18.0.tgz",
+      "integrity": "sha512-U2TbfK8B7dc3JqXSFwj2oXCQrxEaSzCCUkAHjAOIGOKzx/HLKIKs+NJj9IQkLLr7BsMU+Qqzo8aqo11E+Vs+aA==",
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "bin": {
+        "bazel": "bazel-0.18.0-windows-x86_64.exe"
+      }
+    }
+  },
   "dependencies": {
     "@bazel/bazel": {
       "version": "0.18.1",
       "resolved": "https://registry.npmjs.org/@bazel/bazel/-/bazel-0.18.1.tgz",
       "integrity": "sha512-2KjB0umWsW5or78hTG/RVLmAKmeouyDjqjNsGlOY3pzpj9yqDMG5iDPJh4Q1/hXxiDmF2qZWwqWIEh55LFZXaQ==",
+      "dev": true,
       "requires": {
         "@bazel/bazel-darwin_x64": "0.18.0",
         "@bazel/bazel-linux_x64": "0.18.0",
@@ -18,18 +83,21 @@
           "version": "0.18.0",
           "resolved": "https://registry.npmjs.org/@bazel/bazel-darwin_x64/-/bazel-darwin_x64-0.18.0.tgz",
           "integrity": "sha512-um2OzgLL2Gd/W6joOpvrSTcqpnupliPNpwe/uE7sB0huBSJ/4Im0w2IlCTI6C7OfgMcbpUj4YxgUa9T6u6WY6w==",
+          "dev": true,
           "optional": true
         },
         "@bazel/bazel-linux_x64": {
           "version": "0.18.0",
           "resolved": "https://registry.npmjs.org/@bazel/bazel-linux_x64/-/bazel-linux_x64-0.18.0.tgz",
           "integrity": "sha512-Rq8X8bL6SgQvbOHnfPhSgF6hp+f6Fbt2w6pRmBlFvV1J+CeUyrSrrRXfnnO1bjIuq05Ur3mV8ULA0qK6rtA5lQ==",
+          "dev": true,
           "optional": true
         },
         "@bazel/bazel-win32_x64": {
           "version": "0.18.0",
           "resolved": "https://registry.npmjs.org/@bazel/bazel-win32_x64/-/bazel-win32_x64-0.18.0.tgz",
           "integrity": "sha512-U2TbfK8B7dc3JqXSFwj2oXCQrxEaSzCCUkAHjAOIGOKzx/HLKIKs+NJj9IQkLLr7BsMU+Qqzo8aqo11E+Vs+aA==",
+          "dev": true,
           "optional": true
         }
       }
diff --git a/third_party/BUILD.bazel b/third_party/BUILD.bazel
new file mode 100644
index 000000000..c1d81a50e
--- /dev/null
+++ b/third_party/BUILD.bazel
@@ -0,0 +1,23 @@
+load("@rules_proto//proto:defs.bzl", "proto_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+
+proto_library(
+    name = "tutorial_proto",
+    srcs = ["tutorial.proto"],
+    visibility = ["//visibility:public"],
+)
+
+go_proto_library(
+    name = "tutorial_go_proto",
+    importpath = "third_party",
+    proto = ":tutorial_proto",
+    visibility = ["//visibility:public"],
+)
+
+go_library(
+    name = "third_party",
+    embed = [":tutorial_go_proto"],
+    importpath = "third_party",
+    visibility = ["//visibility:public"],
+)