[
  {
    "path": ".gitignore",
    "content": "node_modules\nweb/posts/2*\n"
  },
  {
    "path": "README.md",
    "content": "# [Service Workers & Offline Course](https://frontendmasters.com/courses/service-workers/) by Kyle Simpson\n\nCode for the Service Workers / PWA section of the Service Workers and Offline course by Kyle Simpson\n\n## Starter Exercise Files\n\nTo get started, download the [Starter Files (ZIP)](https://static.frontendmasters.com/resources/2019-05-10-service-worker-pwa/service-workers-starter.zip)\n\n### Web Workers Solution\n\nThe solution for the Web Workers section of the course: https://github.com/FrontendMasters/web-workers\n\n### Service Workers & PWA Solution\n\nThis repository has the final code for the course. You may refer to the [code commits on May 10th](https://github.com/FrontendMasters/service-workers-offline/commits/master), to walk through the course code as Kyle is completing it throughout the course.\n\nDon't forget to `npm install` the necessary modules.\n"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"my-ramblings\",\n\t\"main\": \"server.js\",\n\t\"dependencies\": {\n\t\t\"cookie\": \"~0.3.1\",\n\t\t\"get-stream\": \"~5.1.0\",\n\t\t\"node-static-alias\": \"~1.1.2\",\n\t\t\"random-number-csprng\": \"~1.0.2\"\n\t}\n}\n"
  },
  {
    "path": "server.js",
    "content": "\"use strict\";\n\nvar util = require(\"util\");\nvar fs = require(\"fs\");\nvar path = require(\"path\");\nvar http = require(\"http\");\nvar nodeStaticAlias = require(\"node-static-alias\");\nvar getStream = require(\"get-stream\");\nvar cookie = require(\"cookie\");\nvar rand = require(\"random-number-csprng\");\n\nvar fsReadDir = util.promisify(fs.readdir);\nvar fsReadFile = util.promisify(fs.readFile);\nvar fsWriteFile = util.promisify(fs.writeFile);\n\nconst PORT = 8049;\nconst WEB_DIR = path.join(__dirname,\"web\");\n\nvar httpServer = http.createServer(handleRequest);\n\nvar staticServer = new nodeStaticAlias.Server(WEB_DIR,{\n\tserverInfo: \"My Ramblings\",\n\tcache: 1,\n\talias: [\n\t\t{\n\t\t\t// basic static page friendly URL rewrites\n\t\t\tmatch: /^\\/(?:index)?(?:[#?]|$)/,\n\t\t\tserve: \"index.html\",\n\t\t\tforce: true,\n\t\t},\n\t\t{\n\t\t\t// basic static page friendly URL rewrites\n\t\t\tmatch: /^\\/(?:about|contact|login|404|offline)(?:[#?]|$)/,\n\t\t\tserve: \"<% basename %>.html\",\n\t\t\tforce: true,\n\t\t},\n\t\t{\n\t\t\t// URL rewrites for individual posts\n\t\t\tmatch: /^\\/post\\/[\\w\\d-]+(?:[#?]|$)/,\n\t\t\tserve: \"posts/<% basename %>.html\",\n\t\t\tforce: true,\n\t\t},\n\t\t{\n\t\t\t// match (with force) static files\n\t\t\tmatch: /^\\/(?:(?:(?:js|css|images)\\/.+))$/,\n\t\t\tserve: \".<% reqPath %>\",\n\t\t\tforce: true,\n\t\t},\n\t],\n});\n\n\nhttpServer.listen(PORT);\nconsole.log(`Server started on http://localhost:${PORT}...`);\n\n\n// *******************************\n\nvar sessions = [];\n\nasync function handleRequest(req,res) {\n\t// parse cookie values?\n\tif (req.headers.cookie) {\n\t\treq.headers.cookie = cookie.parse(req.headers.cookie);\n\t}\n\n\t// handle API calls\n\tif (\n\t\t[\"GET\",\"POST\"].includes(req.method) &&\n\t\t/^\\/api\\/.+$/.test(req.url)\n\t) {\n\t\tif (req.url == \"/api/get-posts\") {\n\t\t\tawait getPosts(req,res);\n\t\t\treturn;\n\t\t}\n\t\telse if (req.url == \"/api/login\") {\n\t\t\tlet loginData = JSON.parse(await getStream(req));\n\t\t\tawait doLogin(loginData,req,res);\n\t\t\treturn;\n\t\t}\n\t\telse if (\n\t\t\treq.url == \"/api/add-post\" &&\n\t\t\tvalidateSessionID(req,res)\n\t\t) {\n\t\t\tlet newPostData = JSON.parse(await getStream(req));\n\t\t\tawait addPost(newPostData,req,res);\n\t\t\treturn;\n\t\t}\n\n\t\t// didn't recognize the API request\n\t\tres.writeHead(404);\n\t\tres.end();\n\t}\n\t// handle all other file requests\n\telse if ([\"GET\",\"HEAD\"].includes(req.method)) {\n\t\t// special handling for empty favicon\n\t\tif (req.url == \"/favicon.ico\") {\n\t\t\tres.writeHead(204,{\n\t\t\t\t\"Content-Type\": \"image/x-icon\",\n\t\t\t\t\"Cache-Control\": \"public, max-age: 604800\"\n\t\t\t});\n\t\t\tres.end();\n\t\t\treturn;\n\t\t}\n\n\t\t// special handling for service-worker (virtual path)\n\t\tif (/^\\/sw\\.js(?:[?#].*)?$/.test(req.url)) {\n\t\t\tserveFile(\"/js/sw.js\",200,{ \"cache-control\": \"max-age=0\", },req,res)\n\t\t\t.catch(console.error);\n\t\t\treturn;\n\t\t}\n\n\t\t// handle admin pages\n\t\tif (/^\\/(?:add-post)(?:[#?]|$)/.test(req.url)) {\n\t\t\t// page not allowed without active session\n\t\t\tif (validateSessionID(req,res)) {\n\t\t\t\tawait serveFile(\"/add-post.html\",200,{},req,res);\n\t\t\t}\n\t\t\t// show the login page instead\n\t\t\telse {\n\t\t\t\tawait serveFile(\"/login.html\",200,{},req,res);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// login page when already logged in?\n\t\tif (\n\t\t\t/^\\/(?:login)(?:[#?]|$)/.test(req.url) &&\n\t\t\tvalidateSessionID(req,res)\n\t\t) {\n\t\t\tres.writeHead(307,{ Location: \"/add-post\", });\n\t\t\tres.end();\n\t\t\treturn;\n\t\t}\n\n\t\t// handle logout\n\t\tif (/^\\/(?:logout)(?:[#?]|$)/.test(req.url)) {\n\t\t\tclearSession(req,res);\n\t\t\tres.writeHead(307,{ Location: \"/\", });\n\t\t\tres.end();\n\t\t\treturn;\n\t\t}\n\n\t\t// handle other static files\n\t\tstaticServer.serve(req,res,function onStaticComplete(err){\n\t\t\tif (err) {\n\t\t\t\tif (req.headers[\"accept\"].includes(\"text/html\")) {\n\t\t\t\t\tserveFile(\"/404.html\",200,{ \"X-Not-Found\": \"1\" },req,res)\n\t\t\t\t\t.catch(console.error);\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tres.writeHead(404);\n\t\t\t\t\tres.end();\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\t// Oops, invalid/unrecognized request\n\telse {\n\t\tres.writeHead(404);\n\t\tres.end();\n\t}\n}\n\nfunction serveFile(url,statusCode,headers,req,res) {\n\tvar listener = staticServer.serveFile(url,statusCode,headers,req,res);\n\treturn new Promise(function c(resolve,reject){\n\t\tlistener.on(\"success\",resolve);\n\t\tlistener.on(\"error\",reject);\n\t});\n}\n\nasync function getPostIDs() {\n\tvar files = await fsReadDir(path.join(WEB_DIR,\"posts\"));\n\treturn (\n\t\tfiles\n\t\t.filter(function onlyPosts(filename){\n\t\t\treturn /^\\d+\\.html$/.test(filename);\n\t\t})\n\t\t.map(function postID(filename){\n\t\t\tlet [,postID] = filename.match(/^(\\d+)\\.html$/);\n\t\t\treturn Number(postID);\n\t\t})\n\t\t.sort(function desc(x,y){\n\t\t\treturn y - x;\n\t\t})\n\t);\n}\n\nasync function getPosts(req,res) {\n\tvar postIDs = await getPostIDs();\n\tsendJSONResponse(postIDs,res);\n}\n\nasync function addPost(newPostData,req,res) {\n\tif (\n\t\tnewPostData.title.length > 0 &&\n\t\tnewPostData.post.length > 0\n\t) {\n\t\tlet postTemplate = await fsReadFile(path.join(WEB_DIR,\"posts\",\"post.html\"),\"utf-8\");\n\t\tlet newPost =\n\t\t\tpostTemplate\n\t\t\t.replace(/\\{\\{TITLE\\}\\}/g,newPostData.title)\n\t\t\t.replace(/\\{\\{POST\\}\\}/,newPostData.post);\n\t\tlet postIDs = await getPostIDs();\n\t\tlet newPostCount = 1;\n\t\tlet [,year,month,day] = (new Date()).toISOString().match(/^(\\d{4})-(\\d{2})-(\\d{2})/);\n\t\tif (postIDs.length > 0) {\n\t\t\tlet [,latestYear,latestMonth,latestDay,latestCount] = String(postIDs[0]).match(/^(\\d{4})(\\d{2})(\\d{2})(\\d+)/);\n\t\t\tif (\n\t\t\t\tlatestYear == year &&\n\t\t\t\tlatestMonth == month &&\n\t\t\t\tlatestDay == day\n\t\t\t) {\n\t\t\t\tnewPostCount = Number(latestCount) + 1;\n\t\t\t}\n\t\t}\n\t\tlet newPostID = `${year}${month}${day}${newPostCount}`;\n\t\ttry {\n\t\t\tawait fsWriteFile(path.join(WEB_DIR,\"posts\",`${newPostID}.html`),newPost,\"utf8\");\n\t\t\tsendJSONResponse({ OK: true, postID: newPostID },res);\n\t\t\treturn;\n\t\t}\n\t\tcatch (err) {}\n\t}\n\n\tsendJSONResponse({ failed: true },res);\n}\n\nfunction validateSessionID(req,res) {\n\tif (req.headers.cookie && req.headers.cookie[\"sessionId\"]) {\n\t\tlet isLoggedIn = Number(req.headers.cookie[\"isLoggedIn\"]);\n\t\tlet sessionID = req.headers.cookie[\"sessionId\"];\n\t\tlet session;\n\n\t\tif (\n\t\t\tisLoggedIn == 1 &&\n\t\t\tsessions.includes(sessionID)\n\t\t) {\n\t\t\treq.sessionID = sessionID;\n\n\t\t\t// update cookie headers\n\t\t\tres.setHeader(\n\t\t\t\t\"Set-Cookie\",\n\t\t\t\tgetCookieHeaders(sessionID,new Date(Date.now() + /*1 hour in ms*/3.6E5).toUTCString())\n\t\t\t);\n\t\t\treturn true;\n\t\t}\n\t\telse {\n\t\t\tclearSession(req,res);\n\t\t}\n\t}\n\n\treturn false;\n}\n\nasync function randomString() {\n\tvar chars = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/\";\n\tvar str = \"\";\n\tfor (let i = 0; i < 20; i++) {\n\t\tstr += chars[ await rand(0,63) ];\n\t}\n\treturn str;\n}\n\nasync function createSession() {\n\tvar sessionID;\n\tdo {\n\t\tsessionID = await randomString();\n\t} while (sessions.includes(sessionID));\n\tsessions.push(sessionID);\n\treturn sessionID;\n}\n\nfunction clearSession(req,res) {\n\tvar sessionID =\n\t\treq.sessionID ||\n\t\t(req.headers.cookie && req.headers.cookie.sessionId);\n\n\tif (sessionID) {\n\t\tsessions = sessions.filter(function removeSession(sID){\n\t\t\treturn sID !== sessionID;\n\t\t});\n\t}\n\n\tres.setHeader(\"Set-Cookie\",getCookieHeaders(null,new Date(0).toUTCString()));\n}\n\nfunction getCookieHeaders(sessionID,expires = null) {\n\tvar cookieHeaders = [\n\t\t`sessionId=${sessionID || \"\"}; HttpOnly; Path=/`,\n\t\t`isLoggedIn=${sessionID ? \"1\" : \"\"}; Path=/`,\n\t];\n\n\tif (expires != null) {\n\t\tcookieHeaders = cookieHeaders.map(function addExpires(headerVal){\n\t\t\treturn `${headerVal}; Expires=${expires}`;\n\t\t});\n\t}\n\n\treturn cookieHeaders;\n}\n\nasync function doLogin(loginData,req,res) {\n\t// WARNING: This is absolutely NOT how you should handle logins,\n\t// having credentials hard-coded. Hash all credentials and store\n\t// them in a secure database.\n\tif (loginData.username == \"admin\" && loginData.password == \"changeme\") {\n\t\tlet sessionID = await createSession();\n\t\tsendJSONResponse({ OK: true },res,{\n\t\t\t\"Set-Cookie\": getCookieHeaders(\n\t\t\t\tsessionID,\n\t\t\t\tnew Date(Date.now() + /*1 hour in ms*/3.6E5).toUTCString()\n\t\t\t)\n\t\t});\n\t}\n\telse {\n\t\tsendJSONResponse({ failed: true },res);\n\t}\n}\n\nfunction sendJSONResponse(msg,res,otherHeaders = {}) {\n\tres.writeHead(200,{\n\t\t\"Content-Type\": \"application/json\",\n\t\t\"Cache-Control\": \"private, no-cache, no-store, must-revalidate, max-age=0\",\n\t\t...otherHeaders\n\t});\n\tres.end(JSON.stringify(msg));\n}\n"
  },
  {
    "path": "web/404.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>My Ramblings :: Not Found</title>\n<link rel=\"stylesheet\" href=\"/css/style.css\">\n</head>\n<body>\n\t<header>\n\t\t<h1>My Ramblings</h1>\n\t\t<nav>\n\t\t\t<ul>\n\t\t\t\t<li><a href=\"/\">Home</a></li>\n\t\t\t\t<li><a href=\"/about\">About</a></li>\n\t\t\t\t<li><a href=\"/contact\">Contact</a></li>\n\t\t\t</ul>\n\t\t</nav>\n\t\t<div id=\"connectivity-status\" class=\"hidden\"></div>\n\t</header>\n\n\t<main>\n\t\t<h1>Not Found</h1>\n\t\t<p>\n\t\t\tSorry, that couldn't be found. Please try again.\n\t\t</p>\n\t</main>\n\n\t<script src=\"/js/blog.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "web/about.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>My Ramblings :: About</title>\n<link rel=\"stylesheet\" href=\"/css/style.css\">\n</head>\n<body>\n\t<header>\n\t\t<h1>My Ramblings</h1>\n\t\t<nav>\n\t\t\t<ul>\n\t\t\t\t<li><a href=\"/\">Home</a></li>\n\t\t\t\t<li><a href=\"/about\">About</a></li>\n\t\t\t\t<li><a href=\"/contact\">Contact</a></li>\n\t\t\t</ul>\n\t\t</nav>\n\t\t<div id=\"connectivity-status\" class=\"hidden\"></div>\n\t</header>\n\n\t<main>\n\t\t<h1>About</h1>\n\t\t<p>\n\t\t\tThese are just some of my rambling thoughts. I hope they are interesting to some of you.\n\t\t</p>\n\t\t<p>\n\t\t\tPlease feel free to <a href=\"/contact\">reach out</a> if you have any thoughts to share with me!\n\t\t</p>\n\t</main>\n\n\t<script src=\"/js/blog.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "web/add-post.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>My Ramblings :: Add Post</title>\n<link rel=\"stylesheet\" href=\"/css/style.css\">\n</head>\n<body>\n\t<header>\n\t\t<h1>My Ramblings</h1>\n\t\t<nav>\n\t\t\t<ul>\n\t\t\t\t<li><a href=\"/\">Home</a></li>\n\t\t\t\t<li><a href=\"/about\">About</a></li>\n\t\t\t\t<li><a href=\"/contact\">Contact</a></li>\n\t\t\t\t<li><a href=\"/logout\">Logout</a></li>\n\t\t\t</ul>\n\t\t</nav>\n\t\t<div id=\"connectivity-status\" class=\"hidden\"></div>\n\t</header>\n\n\t<main>\n\t\t<h1>Add Post</h1>\n\t\t<p>\n\t\t\tTitle: <input id=\"new-title\" size=\"40\">\n\t\t</p>\n\t\t<p>\n\t\t\t<textarea id=\"new-post\" cols=\"70\" rows=\"15\"></textarea>\n\t\t</p>\n\t\t<p>\n\t\t\t<button type=\"button\" id=\"btn-add-post\">Add Post</button>\n\t\t</p>\n\t</main>\n\n\t<script src=\"/js/external/idb-keyval-iife.min.js\"></script>\n\t<script src=\"/js/blog.js\"></script>\n\t<script src=\"/js/add-post.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "web/contact.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>My Ramblings :: Contact</title>\n<link rel=\"stylesheet\" href=\"/css/style.css\">\n</head>\n<body>\n\t<header>\n\t\t<h1>My Ramblings</h1>\n\t\t<nav>\n\t\t\t<ul>\n\t\t\t\t<li><a href=\"/\">Home</a></li>\n\t\t\t\t<li><a href=\"/about\">About</a></li>\n\t\t\t\t<li><a href=\"/contact\">Contact</a></li>\n\t\t\t</ul>\n\t\t</nav>\n\t\t<div id=\"connectivity-status\" class=\"hidden\"></div>\n\t</header>\n\n\t<main>\n\t\t<h1>Contact</h1>\n\t\t<p>\n\t\t\tIf you'd like to reach out to me:\n\t\t</p>\n\t\t<ul>\n\t\t\t<li><a href=\"https://twitter.com/getify\">@getify</a></li>\n\t\t\t<li><a href=\"mailto:getify@gmail.com\">getify @ gmail</a></li>\n\t\t</ul>\n\t</main>\n\n\t<script src=\"/js/blog.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "web/css/style.css",
    "content": "html {\n\tbox-sizing: border-box;\n\tfont-family: sans-serif;\n\tfont-size: 1.3em;\n}\n\n*,\n*::before,\n*::after {\n\tbox-sizing: inherit;\n}\n\nhtml input,\nhtml button,\nhtml textarea {\n\tfont-size: 1em;\n}\n\nhtml,\nbody {\n\tbackground-color: #e5efff;\n}\n\n#connectivity-status {\n\tposition: absolute;\n\ttop: 5px;\n\tright: 5px;\n\twidth: 55px;\n\theight: 49px;\n\tbackground: url(/images/offline.png) 0px 0px/55px 49px no-repeat;\n}\n\n#connectivity-status.hidden {\n\tdisplay: none;\n}\n\nheader {\n\tposition: relative;\n\tmax-width: 800px;\n\tmargin: 0px auto;\n\tcolor: #222;\n}\n\nheader h1 {\n\tposition: relative;\n\tpadding-left: 90px;\n}\n\nheader h1::before {\n\tcontent: \"\";\n\tdisplay: block;\n\tposition: absolute;\n\tleft: 0px;\n\ttop: 0px;\n\twidth: 63px;\n\theight: 75px;\n\tbackground: url(/images/logo.gif) 0px 0px/63px 75px no-repeat;\n}\n\nnav {\n\tbackground-color: #5078ba;\n\tcolor: #fff;\n}\n\nnav ul {\n\tpadding: 0px;\n\tpadding-left: 20px;\n\tmargin: 0px;\n\tlist-style: none;\n}\n\nnav ul li {\n\tdisplay: inline-block;\n\tpadding: 10px;\n}\n\nnav ul li a {\n\tcolor: #fff;\n\ttext-decoration: none;\n}\n\nmain {\n\tmargin: 0px auto;\n\tmax-width: 800px;\n\tbackground-color: #fff;\n\tcolor: #000;\n\tpadding: 30px;\n}\n\nmain a {\n\tcolor: #000;\n}\n\nmain > h1:first-child {\n\tmargin-top: 0px;\n}\n\n#my-posts {\n\tlist-style: none;\n}\n\n#my-posts > li {\n\tmargin-bottom: 12px;\n}\n\n#my-posts > li:last-child {\n\tmargin-bottom: 0px;\n}\n"
  },
  {
    "path": "web/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>My Ramblings</title>\n<link rel=\"stylesheet\" href=\"/css/style.css\">\n</head>\n<body>\n\t<header>\n\t\t<h1>My Ramblings</h1>\n\t\t<nav>\n\t\t\t<ul>\n\t\t\t\t<li><a href=\"/\">Home</a></li>\n\t\t\t\t<li><a href=\"/about\">About</a></li>\n\t\t\t\t<li><a href=\"/contact\">Contact</a></li>\n\t\t\t</ul>\n\t\t</nav>\n\t\t<div id=\"connectivity-status\" class=\"hidden\"></div>\n\t</header>\n\n\t<main>\n\t\t<h1>My Posts:</h1>\n\t\t<ul id=\"my-posts\">\n\t\t\t<li>...</li>\n\t\t</ul>\n\t</main>\n\n\t<script src=\"/js/blog.js\"></script>\n\t<script src=\"/js/home.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "web/js/add-post.js",
    "content": "(function AddPost(){\n\t\"use strict\";\n\n\tvar titleInput;\n\tvar postInput;\n\tvar addPostBtn;\n\n\tdocument.addEventListener(\"DOMContentLoaded\",ready,false);\n\n\n\t// **********************************\n\n\tasync function ready() {\n\t\ttitleInput = document.getElementById(\"new-title\");\n\t\tpostInput = document.getElementById(\"new-post\");\n\t\taddPostBtn = document.getElementById(\"btn-add-post\");\n\n\t\taddPostBtn.addEventListener(\"click\",addPost,false);\n\t\ttitleInput.addEventListener(\"change\",backupPost,false);\n\t\tpostInput.addEventListener(\"change\",backupPost,false);\n\n\t\t// restore a backup?\n\t\tvar addPostBackup = await idbKeyval.get(\"add-post-backup\");\n\t\tif (addPostBackup) {\n\t\t\ttitleInput.value = addPostBackup.title || \"\";\n\t\t\tpostInput.value = addPostBackup.post || \"\";\n\t\t}\n\t}\n\n\t// save backup of post (in case posting fails or offline)\n\tasync function backupPost() {\n\t\tawait idbKeyval.set(\"add-post-backup\",{\n\t\t\ttitle: titleInput.value,\n\t\t\tpost: postInput.value\n\t\t});\n\t}\n\n\tasync function addPost() {\n\t\tif (\n\t\t\ttitleInput.value.length > 0 &&\n\t\t\tpostInput.value.length > 0\n\t\t) {\n\t\t\t// don't try posting while offline\n\t\t\tif (!isBlogOnline()) {\n\t\t\t\talert(\"You seem to be offline currently. Please try posting once you come back online.\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tlet res = await fetch(\"/api/add-post\",{\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tcredentials: \"same-origin\",\n\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\ttitle: titleInput.value,\n\t\t\t\t\t\tpost: postInput.value\n\t\t\t\t\t})\n\t\t\t\t});\n\n\t\t\t\tif (res && res.ok) {\n\t\t\t\t\tlet result = await res.json();\n\t\t\t\t\tif (result.OK) {\n\t\t\t\t\t\ttitleInput.value = \"\";\n\t\t\t\t\t\tpostInput.value = \"\";\n\t\t\t\t\t\tdocument.location.href = `/post/${result.postID}`;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcatch (err) {\n\t\t\t\tconsole.error(err);\n\t\t\t}\n\n\t\t\talert(\"Posting failed. Try again.\");\n\t\t}\n\t\telse {\n\t\t\talert(\"Please enter a title and some blog post content.\");\n\t\t}\n\t}\n\n})();\n"
  },
  {
    "path": "web/js/blog.js",
    "content": "(function Blog(global){\n\t\"use strict\";\n\n\tvar offlineIcon;\n\tvar isOnline = (\"onLine\" in navigator) && navigator.onLine;\n\tvar isLoggedIn = /isLoggedIn=1/.test(document.cookie.toString() || \"\");\n\tvar usingSW = (\"serviceWorker\" in navigator);\n\tvar swRegistration;\n\tvar svcworker;\n\n\tif (usingSW) {\n\t\tinitServiceWorker().catch(console.error);\n\t}\n\n\tglobal.isBlogOnline = isBlogOnline;\n\n\tdocument.addEventListener(\"DOMContentLoaded\",ready,false);\n\n\n\t// **********************************\n\n\tfunction ready() {\n\t\tofflineIcon = document.getElementById(\"connectivity-status\");\n\n\t\tif (!isOnline) {\n\t\t\tofflineIcon.classList.remove(\"hidden\");\n\t\t}\n\n\t\twindow.addEventListener(\"online\",function online(){\n\t\t\tofflineIcon.classList.add(\"hidden\");\n\t\t\tisOnline = true;\n\t\t\tsendStatusUpdate();\n\t\t},false);\n\t\twindow.addEventListener(\"offline\",function offline(){\n\t\t\tofflineIcon.classList.remove(\"hidden\");\n\t\t\tisOnline = false;\n\t\t\tsendStatusUpdate();\n\t\t},false);\n\t}\n\n\tfunction isBlogOnline() {\n\t\treturn isOnline;\n\t}\n\n\tasync function initServiceWorker() {\n\t\tswRegistration = await navigator.serviceWorker.register(\"/sw.js\",{\n\t\t\tupdateViaCache: \"none\",\n\t\t});\n\n\t\tsvcworker = swRegistration.installing || swRegistration.waiting || swRegistration.active;\n\t\tsendStatusUpdate(svcworker);\n\n\t\t// listen for new service worker to take over\n\t\tnavigator.serviceWorker.addEventListener(\"controllerchange\",async function onController(){\n\t\t\tsvcworker = navigator.serviceWorker.controller;\n\t\t\tsendStatusUpdate(svcworker);\n\t\t});\n\n\t\tnavigator.serviceWorker.addEventListener(\"message\",onSWMessage,false);\n\t}\n\n\tfunction onSWMessage(evt) {\n\t\tvar { data } = evt;\n\t\tif (data.statusUpdateRequest) {\n\t\t\tconsole.log(\"Status update requested from service worker, responding...\");\n\t\t\tsendStatusUpdate(evt.ports && evt.ports[0]);\n\t\t}\n\t\telse if (data == \"force-logout\") {\n\t\t\tdocument.cookie = \"isLoggedIn=\";\n\t\t\tisLoggedIn = false;\n\t\t\tsendStatusUpdate();\n\t\t}\n\t}\n\n\tfunction sendStatusUpdate(target) {\n\t\tsendSWMessage({ statusUpdate: { isOnline, isLoggedIn } },target);\n\t}\n\n\tfunction sendSWMessage(msg,target) {\n\t\tif (target) {\n\t\t\ttarget.postMessage(msg);\n\t\t}\n\t\telse if (svcworker) {\n\t\t\tsvcworker.postMessage(msg);\n\t\t}\n\t\telse if (navigator.serviceWorker.controller) {\n\t\t\tnavigator.serviceWorker.controller.postMessage(msg);\n\t\t}\n\t}\n\n})(window);\n"
  },
  {
    "path": "web/js/home.js",
    "content": "(function Home(){\n\t\"use strict\";\n\n\tvar postsList;\n\n\tdocument.addEventListener(\"DOMContentLoaded\",ready,false);\n\n\n\t// **********************************\n\n\tfunction ready() {\n\t\tpostsList = document.getElementById(\"my-posts\");\n\t\tmain().catch(console.error);\n\t}\n\n\tasync function main() {\n\t\tvar postIDs;\n\n\t\ttry {\n\t\t\tvar res = await fetch(\"/api/get-posts\");\n\t\t\tif (res && res.ok) {\n\t\t\t\tpostIDs = await res.json();\n\t\t\t}\n\t\t}\n\t\tcatch (err) {}\n\n\t\trenderPostIDs(postIDs || []);\n\t}\n\n\tfunction renderPostIDs(postIDs) {\n\t\tif (postIDs.length > 0) {\n\t\t\tpostsList.innerHTML = \"\";\n\t\t\tfor (let postID of postIDs) {\n\t\t\t\tlet [,year,month,day,postNum] = String(postID).match(/^(\\d{4})(\\d{2})(\\d{2})(\\d+)$/);\n\t\t\t\tlet postEntry = document.createElement(\"li\");\n\t\t\t\tpostEntry.innerHTML = `<a href=\"/post/${postID}\">Post-${+month}/${+day}/${year}-${postNum}</a>`;\n\t\t\t\tpostsList.appendChild(postEntry);\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tpostsList.innerHTML = \"<li>-- nothing yet, check back soon! --</li>\";\n\t\t}\n\t}\n\n})();\n"
  },
  {
    "path": "web/js/login.js",
    "content": "(function Login(){\n\t\"use strict\";\n\n\tvar usernameInput;\n\tvar passwordInput;\n\tvar loginBtn;\n\n\tdocument.addEventListener(\"DOMContentLoaded\",ready,false);\n\n\n\t// **********************************\n\n\tfunction ready() {\n\t\tusernameInput = document.getElementById(\"login-username\");\n\t\tpasswordInput = document.getElementById(\"login-password\");\n\t\tloginBtn = document.getElementById(\"btn-login\");\n\n\t\tloginBtn.addEventListener(\"click\",tryLogin,false);\n\t}\n\n\tasync function tryLogin() {\n\t\tif (\n\t\t\tusernameInput.value.length > 3 &&\n\t\t\tpasswordInput.value.length > 7\n\t\t) {\n\t\t\ttry {\n\t\t\t\tlet res = await fetch(\"/api/login\",{\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tcredentials: \"same-origin\",\n\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\tusername: usernameInput.value,\n\t\t\t\t\t\tpassword: passwordInput.value\n\t\t\t\t\t})\n\t\t\t\t});\n\n\t\t\t\tif (res && res.ok) {\n\t\t\t\t\tlet result = await res.json();\n\t\t\t\t\tusernameInput.value = \"\";\n\t\t\t\t\tpasswordInput.value = \"\";\n\t\t\t\t\tif (result.OK) {\n\t\t\t\t\t\tif (document.location.href == \"/add-post\") {\n\t\t\t\t\t\t\tdocument.location.reload();\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tdocument.location.href = \"/add-post\";\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcatch (err) {\n\t\t\t\tconsole.error(err);\n\t\t\t}\n\n\t\t\talert(\"Login failed. Try again.\");\n\t\t}\n\t\telse {\n\t\t\talert(\"Please enter a sufficient username and password.\");\n\t\t}\n\t}\n\n})();\n"
  },
  {
    "path": "web/js/sw.js",
    "content": "\"use strict\";\n\nimportScripts(\"/js/external/idb-keyval-iife.min.js\");\n\nvar version = 8;\nvar isOnline = true;\nvar isLoggedIn = false;\nvar cacheName = `ramblings-${version}`;\nvar allPostsCaching = false;\n\nvar urlsToCache = {\n\tloggedOut: [\n\t\t\"/\",\n\t\t\"/about\",\n\t\t\"/contact\",\n\t\t\"/404\",\n\t\t\"/login\",\n\t\t\"/offline\",\n\t\t\"/css/style.css\",\n\t\t\"/js/blog.js\",\n\t\t\"/js/home.js\",\n\t\t\"/js/login.js\",\n\t\t\"/js/add-post.js\",\n\t\t\"/js/external/idb-keyval-iife.min.js\",\n\t\t\"/images/logo.gif\",\n\t\t\"/images/offline.png\"\n\t]\n};\n\nself.addEventListener(\"install\",onInstall);\nself.addEventListener(\"activate\",onActivate);\nself.addEventListener(\"message\",onMessage);\nself.addEventListener(\"fetch\",onFetch);\n\nmain().catch(console.error);\n\n\n// ****************************\n\nasync function main() {\n\tawait sendMessage({ statusUpdateRequest: true });\n\tawait cacheLoggedOutFiles();\n\treturn cacheAllPosts();\n}\n\nfunction onInstall(evt) {\n\tconsole.log(`Service Worker (v${version}) installed`);\n\tself.skipWaiting();\n}\n\nfunction onActivate(evt) {\n\tevt.waitUntil(handleActivation());\n}\n\nasync function handleActivation() {\n\tawait clearCaches();\n\tawait cacheLoggedOutFiles(/*forceReload=*/true);\n\tawait clients.claim();\n\tconsole.log(`Service Worker (v${version}) activated`);\n\n\t// spin off background caching of all past posts (over time)\n\tcacheAllPosts(/*forceReload=*/true).catch(console.error);\n}\n\nasync function clearCaches() {\n\tvar cacheNames = await caches.keys();\n\tvar oldCacheNames = cacheNames.filter(function matchOldCache(cacheName){\n\t\tvar [,cacheNameVersion] = cacheName.match(/^ramblings-(\\d+)$/) || [];\n\t\tcacheNameVersion = cacheNameVersion != null ? Number(cacheNameVersion) : cacheNameVersion;\n\t\treturn (\n\t\t\tcacheNameVersion > 0 &&\n\t\t\tversion !== cacheNameVersion\n\t\t);\n\t});\n\tawait Promise.all(\n\t\toldCacheNames.map(function deleteCache(cacheName){\n\t\t\treturn caches.delete(cacheName);\n\t\t})\n\t);\n}\n\nasync function cacheLoggedOutFiles(forceReload = false) {\n\tvar cache = await caches.open(cacheName);\n\n\treturn Promise.all(\n\t\turlsToCache.loggedOut.map(async function requestFile(url){\n\t\t\ttry {\n\t\t\t\tlet res;\n\n\t\t\t\tif (!forceReload) {\n\t\t\t\t\tres = await cache.match(url);\n\t\t\t\t\tif (res) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tlet fetchOptions = {\n\t\t\t\t\tmethod: \"GET\",\n\t\t\t\t\tcache: \"no-store\",\n\t\t\t\t\tcredentials: \"omit\"\n\t\t\t\t};\n\t\t\t\tres = await fetch(url,fetchOptions);\n\t\t\t\tif (res.ok) {\n\t\t\t\t\treturn cache.put(url,res);\n\t\t\t\t}\n\t\t\t}\n\t\t\tcatch (err) {}\n\t\t})\n\t);\n}\n\nasync function cacheAllPosts(forceReload = false) {\n\t// already caching the posts?\n\tif (allPostsCaching) {\n\t\treturn;\n\t}\n\tallPostsCaching = true;\n\tawait delay(5000);\n\n\tvar cache = await caches.open(cacheName);\n\tvar postIDs;\n\n\ttry {\n\t\tif (isOnline) {\n\t\t\tlet fetchOptions = {\n\t\t\t\tmethod: \"GET\",\n\t\t\t\tcache: \"no-store\",\n\t\t\t\tcredentials: \"omit\"\n\t\t\t};\n\t\t\tlet res = await fetch(\"/api/get-posts\",fetchOptions);\n\t\t\tif (res && res.ok) {\n\t\t\t\tawait cache.put(\"/api/get-posts\",res.clone());\n\t\t\t\tpostIDs = await res.json();\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tlet res = await cache.match(\"/api/get-posts\");\n\t\t\tif (res) {\n\t\t\t\tlet resCopy = res.clone();\n\t\t\t\tpostIDs = await res.json();\n\t\t\t}\n\t\t\t// caching not started, try to start again (later)\n\t\t\telse {\n\t\t\t\tallPostsCaching = false;\n\t\t\t\treturn cacheAllPosts(forceReload);\n\t\t\t}\n\t\t}\n\t}\n\tcatch (err) {\n\t\tconsole.error(err);\n\t}\n\n\tif (postIDs && postIDs.length > 0) {\n\t\treturn cachePost(postIDs.shift());\n\t}\n\telse {\n\t\tallPostsCaching = false;\n\t}\n\n\n\t// *************************\n\n\tasync function cachePost(postID) {\n\t\tvar postURL = `/post/${postID}`;\n\t\tvar needCaching = true;\n\n\t\tif (!forceReload) {\n\t\t\tlet res = await cache.match(postURL);\n\t\t\tif (res) {\n\t\t\t\tneedCaching = false;\n\t\t\t}\n\t\t}\n\n\t\tif (needCaching) {\n\t\t\tawait delay(10000);\n\t\t\tif (isOnline) {\n\t\t\t\ttry {\n\t\t\t\t\tlet fetchOptions = {\n\t\t\t\t\t\tmethod: \"GET\",\n\t\t\t\t\t\tcache: \"no-store\",\n\t\t\t\t\t\tcredentials: \"omit\"\n\t\t\t\t\t};\n\t\t\t\t\tlet res = await fetch(postURL,fetchOptions);\n\t\t\t\t\tif (res && res.ok) {\n\t\t\t\t\t\tawait cache.put(postURL,res.clone());\n\t\t\t\t\t\tneedCaching = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcatch (err) {}\n\t\t\t}\n\n\t\t\t// failed, try caching this post again?\n\t\t\tif (needCaching) {\n\t\t\t\treturn cachePost(postID);\n\t\t\t}\n\t\t}\n\n\t\t// any more posts to cache?\n\t\tif (postIDs.length > 0) {\n\t\t\treturn cachePost(postIDs.shift());\n\t\t}\n\t\telse {\n\t\t\tallPostsCaching = false;\n\t\t}\n\t}\n}\n\nasync function sendMessage(msg) {\n\tvar allClients = await clients.matchAll({ includeUncontrolled: true, });\n\treturn Promise.all(\n\t\tallClients.map(function sendTo(client){\n\t\t\tvar chan = new MessageChannel();\n\t\t\tchan.port1.onmessage = onMessage;\n\t\t\treturn client.postMessage(msg,[chan.port2]);\n\t\t})\n\t);\n}\n\nfunction onMessage({ data }) {\n\tif (\"statusUpdate\" in data) {\n\t\t({ isOnline, isLoggedIn } = data.statusUpdate);\n\t\tconsole.log(`Service Worker (v${version}) status update... isOnline:${isOnline}, isLoggedIn:${isLoggedIn}`);\n\t}\n}\n\nfunction onFetch(evt) {\n\tevt.respondWith(router(evt.request));\n}\n\nasync function router(req) {\n\tvar url = new URL(req.url);\n\tvar reqURL = url.pathname;\n\tvar cache = await caches.open(cacheName);\n\n\t// request for site's own URL?\n\tif (url.origin == location.origin) {\n\t\t// are we making an API request?\n\t\tif (/^\\/api\\/.+$/.test(reqURL)) {\n\t\t\tlet fetchOptions = {\n\t\t\t\tcredentials: \"same-origin\",\n\t\t\t\tcache: \"no-store\"\n\t\t\t};\n\t\t\tlet res = await safeRequest(reqURL,req,fetchOptions,/*cacheResponse=*/false,/*checkCacheFirst=*/false,/*checkCacheLast=*/true,/*useRequestDirectly=*/true);\n\t\t\tif (res) {\n\t\t\t\tif (req.method == \"GET\") {\n\t\t\t\t\tawait cache.put(reqURL,res.clone());\n\t\t\t\t}\n\t\t\t\t// clear offline-backup of successful post?\n\t\t\t\telse if (reqURL == \"/api/add-post\") {\n\t\t\t\t\tawait idbKeyval.del(\"add-post-backup\");\n\t\t\t\t}\n\t\t\t\treturn res;\n\t\t\t}\n\n\t\t\treturn notFoundResponse();\n\t\t}\n\t\t// are we requesting a page?\n\t\telse if (req.headers.get(\"Accept\").includes(\"text/html\")) {\n\t\t\t// login-aware requests?\n\t\t\tif (/^\\/(?:login|logout|add-post)$/.test(reqURL)) {\n\t\t\t\tlet res;\n\n\t\t\t\tif (reqURL == \"/login\") {\n\t\t\t\t\tif (isOnline) {\n\t\t\t\t\t\tlet fetchOptions = {\n\t\t\t\t\t\t\tmethod: req.method,\n\t\t\t\t\t\t\theaders: req.headers,\n\t\t\t\t\t\t\tcredentials: \"same-origin\",\n\t\t\t\t\t\t\tcache: \"no-store\",\n\t\t\t\t\t\t\tredirect: \"manual\"\n\t\t\t\t\t\t};\n\t\t\t\t\t\tres = await safeRequest(reqURL,req,fetchOptions);\n\t\t\t\t\t\tif (res) {\n\t\t\t\t\t\t\tif (res.type == \"opaqueredirect\") {\n\t\t\t\t\t\t\t\treturn Response.redirect(\"/add-post\",307);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn res;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (isLoggedIn) {\n\t\t\t\t\t\t\treturn Response.redirect(\"/add-post\",307);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tres = await cache.match(\"/login\");\n\t\t\t\t\t\tif (res) {\n\t\t\t\t\t\t\treturn res;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn Response.redirect(\"/\",307);\n\t\t\t\t\t}\n\t\t\t\t\telse if (isLoggedIn) {\n\t\t\t\t\t\treturn Response.redirect(\"/add-post\",307);\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tres = await cache.match(\"/login\");\n\t\t\t\t\t\tif (res) {\n\t\t\t\t\t\t\treturn res;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn cache.match(\"/offline\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if (reqURL == \"/logout\") {\n\t\t\t\t\tif (isOnline) {\n\t\t\t\t\t\tlet fetchOptions = {\n\t\t\t\t\t\t\tmethod: req.method,\n\t\t\t\t\t\t\theaders: req.headers,\n\t\t\t\t\t\t\tcredentials: \"same-origin\",\n\t\t\t\t\t\t\tcache: \"no-store\",\n\t\t\t\t\t\t\tredirect: \"manual\"\n\t\t\t\t\t\t};\n\t\t\t\t\t\tres = await safeRequest(reqURL,req,fetchOptions);\n\t\t\t\t\t\tif (res) {\n\t\t\t\t\t\t\tif (res.type == \"opaqueredirect\") {\n\t\t\t\t\t\t\t\treturn Response.redirect(\"/\",307);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn res;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (isLoggedIn) {\n\t\t\t\t\t\t\tisLoggedIn = false;\n\t\t\t\t\t\t\tawait sendMessage(\"force-logout\");\n\t\t\t\t\t\t\tawait delay(100);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn Response.redirect(\"/\",307);\n\t\t\t\t\t}\n\t\t\t\t\telse if (isLoggedIn) {\n\t\t\t\t\t\tisLoggedIn = false;\n\t\t\t\t\t\tawait sendMessage(\"force-logout\");\n\t\t\t\t\t\tawait delay(100);\n\t\t\t\t\t\treturn Response.redirect(\"/\",307);\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\treturn Response.redirect(\"/\",307);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if (reqURL == \"/add-post\") {\n\t\t\t\t\tif (isOnline) {\n\t\t\t\t\t\tlet fetchOptions = {\n\t\t\t\t\t\t\tmethod: req.method,\n\t\t\t\t\t\t\theaders: req.headers,\n\t\t\t\t\t\t\tcredentials: \"same-origin\",\n\t\t\t\t\t\t\tcache: \"no-store\"\n\t\t\t\t\t\t};\n\t\t\t\t\t\tres = await safeRequest(reqURL,req,fetchOptions,/*cacheResponse=*/true);\n\t\t\t\t\t\tif (res) {\n\t\t\t\t\t\t\treturn res;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tres = await cache.match(\n\t\t\t\t\t\t\tisLoggedIn ? \"/add-post\" : \"/login\"\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (res) {\n\t\t\t\t\t\t\treturn res;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn Response.redirect(\"/\",307);\n\t\t\t\t\t}\n\t\t\t\t\telse if (isLoggedIn) {\n\t\t\t\t\t\tres = await cache.match(\"/add-post\");\n\t\t\t\t\t\tif (res) {\n\t\t\t\t\t\t\treturn res;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn cache.match(\"/offline\");\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tres = await cache.match(\"/login\");\n\t\t\t\t\t\tif (res) {\n\t\t\t\t\t\t\treturn res;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn cache.match(\"/offline\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// otherwise, just use \"network-and-cache\"\n\t\t\telse {\n\t\t\t\tlet fetchOptions = {\n\t\t\t\t\tmethod: req.method,\n\t\t\t\t\theaders: req.headers,\n\t\t\t\t\tcache: \"no-store\"\n\t\t\t\t};\n\t\t\t\tlet res = await safeRequest(reqURL,req,fetchOptions,/*cacheResponse=*/false,/*checkCacheFirst=*/false,/*checkCacheLast=*/true);\n\t\t\t\tif (res) {\n\t\t\t\t\tif (!res.headers.get(\"X-Not-Found\")) {\n\t\t\t\t\t\tawait cache.put(reqURL,res.clone());\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tawait cache.delete(reqURL);\n\t\t\t\t\t}\n\t\t\t\t\treturn res;\n\t\t\t\t}\n\n\t\t\t\t// otherwise, return an offline-friendly page\n\t\t\t\treturn cache.match(\"/offline\");\n\t\t\t}\n\t\t}\n\t\t// all other files use \"cache-first\"\n\t\telse {\n\t\t\tlet fetchOptions = {\n\t\t\t\tmethod: req.method,\n\t\t\t\theaders: req.headers,\n\t\t\t\tcache: \"no-store\"\n\t\t\t};\n\t\t\tlet res = await safeRequest(reqURL,req,fetchOptions,/*cacheResponse=*/true,/*checkCacheFirst=*/true);\n\t\t\tif (res) {\n\t\t\t\treturn res;\n\t\t\t}\n\n\t\t\t// otherwise, force a network-level 404 response\n\t\t\treturn notFoundResponse();\n\t\t}\n\t}\n}\n\nasync function safeRequest(reqURL,req,options,cacheResponse = false,checkCacheFirst = false,checkCacheLast = false,useRequestDirectly = false) {\n\tvar cache = await caches.open(cacheName);\n\tvar res;\n\n\tif (checkCacheFirst) {\n\t\tres = await cache.match(reqURL);\n\t\tif (res) {\n\t\t\treturn res;\n\t\t}\n\t}\n\n\tif (isOnline) {\n\t\ttry {\n\t\t\tif (useRequestDirectly) {\n\t\t\t\tres = await fetch(req,options);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tres = await fetch(req.url,options);\n\t\t\t}\n\n\t\t\tif (res && (res.ok || res.type == \"opaqueredirect\")) {\n\t\t\t\tif (cacheResponse) {\n\t\t\t\t\tawait cache.put(reqURL,res.clone());\n\t\t\t\t}\n\t\t\t\treturn res;\n\t\t\t}\n\t\t}\n\t\tcatch (err) {}\n\t}\n\n\tif (checkCacheLast) {\n\t\tres = await cache.match(reqURL);\n\t\tif (res) {\n\t\t\treturn res;\n\t\t}\n\t}\n}\n\nfunction notFoundResponse() {\n\treturn new Response(\"\",{\n\t\tstatus: 404,\n\t\tstatusText: \"Not Found\"\n\t});\n}\n\nfunction delay(ms) {\n\treturn new Promise(function c(res){\n\t\tsetTimeout(res,ms);\n\t});\n}\n"
  },
  {
    "path": "web/login.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>My Ramblings :: Add Post</title>\n<link rel=\"stylesheet\" href=\"/css/style.css\">\n</head>\n<body>\n\t<header>\n\t\t<h1>My Ramblings</h1>\n\t\t<nav>\n\t\t\t<ul>\n\t\t\t\t<li><a href=\"/\">Home</a></li>\n\t\t\t\t<li><a href=\"/about\">About</a></li>\n\t\t\t\t<li><a href=\"/contact\">Contact</a></li>\n\t\t\t</ul>\n\t\t</nav>\n\t\t<div id=\"connectivity-status\" class=\"hidden\"></div>\n\t</header>\n\n\t<main>\n\t\t<h1>Login</h1>\n\t\t<p>\n\t\t\tUsername: <input id=\"login-username\">\n\t\t</p>\n\t\t<p>\n\t\t\tPassword: <input type=\"password\" id=\"login-password\">\n\t\t</p>\n\t\t<p>\n\t\t\t<button type=\"button\" id=\"btn-login\">Login</button>\n\t\t</p>\n\t</main>\n\n\t<script src=\"/js/blog.js\"></script>\n\t<script src=\"/js/login.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "web/offline.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>My Ramblings :: Offline!</title>\n<link rel=\"stylesheet\" href=\"/css/style.css\">\n</head>\n<body>\n\t<header>\n\t\t<h1>My Ramblings</h1>\n\t\t<nav>\n\t\t\t<ul>\n\t\t\t\t<li><a href=\"/\">Home</a></li>\n\t\t\t\t<li><a href=\"/about\">About</a></li>\n\t\t\t\t<li><a href=\"/contact\">Contact</a></li>\n\t\t\t</ul>\n\t\t</nav>\n\t\t<div id=\"connectivity-status\" class=\"hidden\"></div>\n\t</header>\n\n\t<main>\n\t\t<h1>Offline</h1>\n\t\t<p>\n\t\t\tIt looks like you're offline and the page you requested couldn't be loaded. Please try again once you're back online.\n\t\t</p>\n\t</main>\n\n\t<script src=\"/js/blog.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "web/posts/post.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>My Ramblings :: {{TITLE}}</title>\n<link rel=\"stylesheet\" href=\"/css/style.css\">\n</head>\n<body>\n\t<header>\n\t\t<h1>My Ramblings</h1>\n\t\t<nav>\n\t\t\t<ul>\n\t\t\t\t<li><a href=\"/\">Home</a></li>\n\t\t\t\t<li><a href=\"/about\">About</a></li>\n\t\t\t\t<li><a href=\"/contact\">Contact</a></li>\n\t\t\t</ul>\n\t\t</nav>\n\t\t<div id=\"connectivity-status\" class=\"hidden\"></div>\n\t</header>\n\n\t<main>\n\t\t<h1>{{TITLE}}</h1>\n\n\t\t{{POST}}\n\n\t</main>\n\n\t<script src=\"/js/blog.js\"></script>\n</body>\n</html>\n"
  }
]