diff --git a/source/_extensions/pwa_service.py b/source/_extensions/pwa_service.py
new file mode 100644
index 0000000000..d61f2cf68b
--- /dev/null
+++ b/source/_extensions/pwa_service.py
@@ -0,0 +1,125 @@
+import sphinx as Sphinx
+from typing import Any, Dict, List
+import os
+from docutils import nodes
+import json
+import shutil
+from urllib.parse import urljoin, urlparse, urlunparse
+from sphinx.util import logging
+from sphinx.util.console import green, red, yellow # pylint: disable=no-name-in-module
+
+manifest = {
+ "name": "",
+ "short_name": "",
+ "theme_color": "",
+ "background_color": "",
+ "display": "standalone",
+ "scope": "/",
+ "start_url": "/index.html",
+ "icons": [],
+}
+
+logger = logging.getLogger(__name__)
+
+def get_files_to_cache(outDir: str, config: Dict[str, Any]):
+ files_to_cache = []
+ for (dirpath, dirname, filenames) in os.walk(outDir):
+ dirpath = dirpath.split(outDir)[1]
+
+ # skip adding sources to cache
+ if os.sep + "_sources" + os.sep in dirpath:
+ continue
+
+ # add files to cache
+ for name in filenames:
+ if "sw.js" in name:
+ continue
+
+ dirpath = dirpath.replace("\\", "/")
+ dirpath = dirpath.lstrip("/")
+
+ # we have to use absolute urls in our cache resource, because fetch will return an absolute url
+ # this means that we cannot accurately cache resources that are in PRs because RTD does not give us
+ # the url
+ if config["html_baseurl"] is not None:
+ # readthedocs uses html_baseurl for sphinx > 1.8
+ parse_result = urlparse(config["html_baseurl"])
+
+ # Grab root url from canonical url
+ url = parse_result.netloc
+
+ # enables RTD multilanguage support
+ if os.getenv("READTHEDOCS"):
+ url = "https://" + url + "/" + os.getenv("READTHEDOCS_LANGUAGE") + "/" + os.getenv("READTHEDOCS_VERSION") + "/"
+
+ if config["html_baseurl"] is None and not os.getenv("CI"):
+ logger.warning(
+ red(f"html_baseurl is not configured. This can be ignored if deployed in RTD environments.")
+ )
+ url = ""
+
+ if dirpath == "":
+ resource_url = urljoin(
+ url, name
+ )
+ files_to_cache.append(resource_url)
+ else:
+ resource_url = url + dirpath + "/" + name
+ files_to_cache.append(resource_url)
+
+ return files_to_cache
+
+
+def build_finished(app: Sphinx, exception: Exception):
+ outDir = app.outdir
+ outDirStatic = outDir + os.sep + "_static" + os.sep
+ files_to_cache = get_files_to_cache(outDir, app.config)
+
+ # dumps a json file with our cache
+ with open(outDirStatic + "cache.json", "w") as f:
+ json.dump(files_to_cache, f)
+
+ # copies over our service worker
+ shutil.copyfile(
+ os.path.dirname(__file__) + os.sep + "pwa_service_files" + os.sep + "sw.js",
+ outDir + os.sep + "sw.js",
+ )
+
+
+def html_page_context(
+ app: Sphinx,
+ pagename: str,
+ templatename: str,
+ context: Dict[str, Any],
+ doctree: nodes.document,
+) -> None:
+ if pagename == "index":
+ context[
+ "metatags"
+ ] += ''
+ context[
+ "metatags"
+ ] += f''
+
+ if app.config["pwa_apple_icon"] is not None:
+ context[
+ "metatags"
+ ] += f''
+
+
+def setup(app: Sphinx) -> Dict[str, Any]:
+ app.add_config_value("pwa_name", "", "html")
+ app.add_config_value("pwa_short_name", "", "html")
+ app.add_config_value("pwa_theme_color", "", "html")
+ app.add_config_value("pwa_background_color", "", "html")
+ app.add_config_value("pwa_display", "standalone", "html")
+ app.add_config_value("pwa_icons", [], "html")
+ app.add_config_value("pwa_apple_icon", "", "html")
+
+ app.connect("html-page-context", html_page_context)
+ app.connect("build-finished", build_finished)
+
+ return {
+ "parallel_read_safe": True,
+ "parallel_write_safe": True,
+ }
diff --git a/source/_extensions/pwa_service_files/sw.js b/source/_extensions/pwa_service_files/sw.js
new file mode 100644
index 0000000000..621fe4e5f1
--- /dev/null
+++ b/source/_extensions/pwa_service_files/sw.js
@@ -0,0 +1,121 @@
+"use strict";
+// extend this to update the service worker every push
+// https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Offline_Service_workers
+let cacheName = 'js13kPWA-v1';
+
+// todo test
+self.addEventListener('install', function (e) {
+ e.waitUntil(async function() {
+ await fetch('_static/cache.json')
+ .then(response => response.json())
+ .then(async (data) => {
+ for (let i = 0; i < data.length / 10; i++) {
+ const tofetch = data.slice(i * 10, i * 10 + 10);
+ await addKeys(tofetch)
+ }
+ })
+ }());
+});
+
+// opt for a cache first response, for quickest load times
+// we'll still update the page assets in the background
+self.addEventListener('fetch', function (event) {
+ event.respondWith(async function () {
+ let request_url = event.request.url;
+
+ try {
+ await addKeys([request_url]) //put in format our addKeys function expects
+ } catch (error) {
+ console.error("Error downloading from remote:", error)
+ }
+
+ let res = await getKey(request_url)
+
+ console.log("Fetching:", event.request.url)
+ return res;
+ }());
+});
+
+let dbPromise;
+
+async function getDB() {
+ if (dbPromise) {
+ return dbPromise;
+ } else {
+ let request = indexedDB.open("frc-docs", "1")
+
+ dbPromise = new Promise((resolve, reject) => {
+ request.onsuccess = function (event) {
+ console.log("Successfully opened database!")
+ resolve(event.target.result)
+ }
+
+ request.onerror = function (event) {
+ console.error("Error opening database for getKey():", request.error)
+ reject()
+ }
+
+ request.onupgradeneeded = function (event) {
+ let db = event.target.result;
+ db.createObjectStore("urls", { keyPath: 'key' })
+ }
+ });
+
+ return dbPromise;
+ }
+}
+
+async function getKey(key) {
+ let db = await getDB()
+ console.log("Grabbing key", key)
+ return new Promise((resolve, reject) => {
+ try {
+ let transaction = db.transaction("urls").objectStore("urls");
+ let request = transaction.get(key)
+
+ request.onsuccess = function (event) {
+ let res = request.result;
+ console.log("Successfully retrieved result:", res)
+ resolve(new Response(res.value));
+ }
+
+ request.onerror = function (event) {
+ console.error("Error on retrieving blob:", key, request.error)
+ reject()
+ }
+
+ } catch (ex) {
+ console.error(ex.message);
+ reject()
+ }
+ })
+}
+
+async function addKeys(datas) {
+ let db = await getDB()
+ return Promise.all(
+ datas.map(async (data) => {
+ let fetchedData = await fetch(data)
+ .then(x => x.blob())
+ .catch((error) => {
+ console.error("Error fetching", data)
+ return new Promise((resolve, reject) => {
+ reject();
+ })
+ })
+ let transaction = db.transaction("urls", "readwrite").objectStore("urls")
+ let request = transaction.put({key: data, value: fetchedData})
+
+ return new Promise((resolve, reject) => {
+ request.onsuccess = function() {
+ resolve()
+ }
+ request.onerror = function () {
+ console.log(request.error)
+ reject(request.error)
+ }
+ });
+ })
+ );
+ // data is already a key/value object with url/data
+}
\ No newline at end of file
diff --git a/source/_static/first-logo-256px.png b/source/_static/first-logo-256px.png
new file mode 100644
index 0000000000..438488faf9
Binary files /dev/null and b/source/_static/first-logo-256px.png differ
diff --git a/source/_static/first-logo-512px.png b/source/_static/first-logo-512px.png
new file mode 100644
index 0000000000..19d0ca6304
Binary files /dev/null and b/source/_static/first-logo-512px.png differ
diff --git a/source/_static/frcdocs.webmanifest b/source/_static/frcdocs.webmanifest
new file mode 100644
index 0000000000..1d3e9622ec
--- /dev/null
+++ b/source/_static/frcdocs.webmanifest
@@ -0,0 +1,21 @@
+{
+ "name": "FRC Docs",
+ "short_name": "FRC Docs",
+ "theme_color": "#003974",
+ "background_color": "#003974",
+ "display": "standalone",
+ "scope": "../",
+ "start_url": "../index.html",
+ "icons": [
+ {
+ "src": "/_static/first-logo-256px.png",
+ "type": "image/png",
+ "sizes": "256x256"
+ },
+ {
+ "src": "/_static/first-logo-512px.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ]
+}
diff --git a/source/_static/touch-icon.png b/source/_static/touch-icon.png
new file mode 100644
index 0000000000..0b4b5c78eb
Binary files /dev/null and b/source/_static/touch-icon.png differ
diff --git a/source/conf.py b/source/conf.py
index 61d01af306..04b4c8e57e 100644
--- a/source/conf.py
+++ b/source/conf.py
@@ -59,6 +59,7 @@
"_extensions.post_process",
"_extensions.rtd_patch",
"_extensions.localization",
+ "_extensions.pwa_service",
]
extensions += local_extensions
@@ -177,6 +178,9 @@
# Use MathJax3 for better page loading times
mathjax_path = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
+# PWA Specific Settings
+pwa_apple_icon = "_static/touch-icon.png"
+
# -- Options for HTML output -------------------------------------------------
@@ -198,7 +202,10 @@
# Specify canonical root
# This tells search engines that this domain is preferred
-html_baseurl = "https://docs.wpilib.org/en/stable/"
+if os.getenv("TESTING"):
+ html_baseurl = "http://localhost:8000/"
+else:
+ html_baseurl = "https://frc-docs--1704.org.readthedocs.build/en/1704/"
html_theme_options = {
"collapse_navigation": True,