├── .editorconfig ├── .eleventy.js ├── .eleventyignore ├── .github └── FUNDING.yml ├── .gitignore ├── .nvmrc ├── .travis.yml ├── LICENSE ├── README.md ├── functions └── google-analytics │ ├── google-analytics.js │ ├── package.json │ └── yarn.lock ├── netlify.toml ├── package.json ├── src ├── 404.md ├── _11ty │ └── getTagList.js ├── _data │ ├── metadata.json │ └── prefetchLinks.js ├── _includes │ ├── layouts │ │ ├── about.njk │ │ ├── base.njk │ │ ├── contact.njk │ │ ├── home.njk │ │ ├── menu.njk │ │ └── post.njk │ └── postslist.njk ├── about │ └── index.md ├── archive.njk ├── contact │ └── contact.md ├── css │ ├── index.css │ └── prism-base16-monokai.dark.css ├── feed │ ├── feed.njk │ └── htaccess.njk ├── img │ ├── .gitkeep │ ├── fries.png │ ├── tgif.png │ └── tgif.svg ├── index.njk ├── menu │ └── menu.md ├── page-list.njk ├── posts │ ├── firstpost.md │ ├── fourthpost.md │ ├── posts.json │ ├── secondpost.md │ └── thirdpost.md ├── sitemap.xml.njk ├── tags-list.njk └── tags.njk └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | -------------------------------------------------------------------------------- /.eleventy.js: -------------------------------------------------------------------------------- 1 | const { DateTime } = require("luxon"); 2 | const fs = require("fs"); 3 | const pluginRss = require("@11ty/eleventy-plugin-rss"); 4 | const pluginSyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight"); 5 | const pluginNavigation = require("@11ty/eleventy-navigation"); 6 | const markdownIt = require("markdown-it"); 7 | const markdownItAnchor = require("markdown-it-anchor"); 8 | require("dotenv").config(); 9 | const THRESHOLD = 0.5 10 | 11 | module.exports = function(eleventyConfig) { 12 | 13 | let env = process.env.ELEVENTY_ENV; 14 | 15 | eleventyConfig.addPlugin(pluginRss); 16 | eleventyConfig.addPlugin(pluginSyntaxHighlight); 17 | eleventyConfig.addPlugin(pluginNavigation); 18 | 19 | eleventyConfig.setDataDeepMerge(true); 20 | 21 | eleventyConfig.addLayoutAlias("post", "./src/layouts/post.njk"); 22 | 23 | eleventyConfig.addFilter("readableDate", dateObj => { 24 | return DateTime.fromJSDate(dateObj, {zone: 'utc'}).toFormat("dd LLL yyyy"); 25 | }); 26 | 27 | eleventyConfig.addFilter('prefetchNextURL', (entry, url) => { 28 | // check threshold // 29 | if (entry.nextPageCertainty > 0.4) { 30 | return entry.nextPagePath.replace("-", "") 31 | } 32 | }) 33 | 34 | // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-date-string 35 | eleventyConfig.addFilter('htmlDateString', (dateObj) => { 36 | return DateTime.fromJSDate(dateObj, {zone: 'utc'}).toFormat('yyyy-LL-dd'); 37 | }); 38 | 39 | // Get the first `n` elements of a collection. 40 | eleventyConfig.addFilter("head", (array, n) => { 41 | if( n < 0 ) { 42 | return array.slice(n); 43 | } 44 | 45 | return array.slice(0, n); 46 | }); 47 | 48 | eleventyConfig.addCollection("tagList", require("./src/_11ty/getTagList")); 49 | 50 | eleventyConfig.addPassthroughCopy("src/img"); 51 | eleventyConfig.addPassthroughCopy("src/css"); 52 | 53 | /* Markdown Overrides */ 54 | let markdownLibrary = markdownIt({ 55 | html: true, 56 | breaks: true, 57 | linkify: true 58 | }).use(markdownItAnchor, { 59 | permalink: true, 60 | permalinkClass: "direct-link", 61 | permalinkSymbol: "#" 62 | }); 63 | eleventyConfig.setLibrary("md", markdownLibrary); 64 | 65 | // Browsersync Overrides 66 | eleventyConfig.setBrowserSyncConfig({ 67 | callbacks: { 68 | ready: function(err, browserSync) { 69 | const content_404 = fs.readFileSync('_site/404.html'); 70 | 71 | browserSync.addMiddleware("*", (req, res) => { 72 | // Provides the 404 content without redirect. 73 | res.write(content_404); 74 | res.end(); 75 | }); 76 | } 77 | } 78 | }); 79 | 80 | return { 81 | templateFormats: [ 82 | "md", 83 | "njk", 84 | "html", 85 | "liquid" 86 | ], 87 | 88 | // If your site lives in a different subdirectory, change this. 89 | // Leading or trailing slashes are all normalized away, so don’t worry about those. 90 | 91 | // If you don’t have a subdirectory, use "" or "/" (they do the same thing) 92 | // This is only used for link URLs (it does not affect your file structure) 93 | // You can also pass this in on the command line using `--pathprefix` 94 | 95 | // pathPrefix: "/", 96 | 97 | markdownTemplateEngine: "liquid", 98 | htmlTemplateEngine: "njk", 99 | dataTemplateEngine: "njk", 100 | 101 | // These are all optional, defaults are shown: 102 | dir: { 103 | input: "src", 104 | includes: "_includes", 105 | data: "_data", 106 | output: "_site" 107 | } 108 | }; 109 | }; 110 | -------------------------------------------------------------------------------- /.eleventyignore: -------------------------------------------------------------------------------- 1 | README.md 2 | _11ty/ 3 | functions/ 4 | /.netlify/functions/google-analytics -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | open_collective: 11ty 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _site/ 2 | node_modules/ 3 | package-lock.json 4 | .env 5 | *.pem 6 | *.p12 7 | 8 | .DS_Store 9 | # Local Netlify folder 10 | .netlify -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | before_script: 5 | - npm install @11ty/eleventy -g 6 | script: eleventy --pathprefix="/eleventy-base-blog/" 7 | deploy: 8 | local-dir: _site 9 | provider: pages 10 | skip-cleanup: true 11 | github-token: $GITHUB_TOKEN # Set in travis-ci.org dashboard, marked secure 12 | keep-history: true 13 | on: 14 | branch: master 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Zach Leatherman @zachleat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Predictive Prefetching Demo 2 | 3 | An example site showing how to do predictive prefetches with 11ty. 4 | 5 | 6 | ## Getting Started 7 | 8 | ### 1. Clone this repository: 9 | 10 | ``` 11 | git clone git@github.com:shortdiv/cant-dutch-this.git my-first-predictive-prefetch 12 | ``` 13 | 14 | 15 | ### 2. Navigate to the directory 16 | 17 | ``` 18 | cd my-first-predictive-prefetch 19 | ``` 20 | 21 | Specifically have a look at `.eleventy.js` to see if you want to configure any Eleventy options differently. 22 | 23 | ### 3. Install dependencies 24 | 25 | ``` 26 | npm install OR yarn 27 | ``` 28 | 29 | ### 4. Edit _data/metadata.json 30 | 31 | ### 5. Run Eleventy 32 | 33 | ``` 34 | npm run build OR yarn build 35 | ``` 36 | 37 | Or build and host locally for local development 38 | ``` 39 | npm run serve OR yarn serve 40 | ``` 41 | 42 | Or build automatically when a template changes: 43 | ``` 44 | npm run watch OR yarn watch 45 | ``` 46 | 47 | Or in debug mode: 48 | ``` 49 | DEBUG=* npx eleventy 50 | ``` 51 | 52 | ### Authentication Notes 53 | ## Create Your Credentials 54 | 55 | ### Create a Service Account 56 | Go to the Credentials page in the Google APIs console. 57 | 58 | If you don't have an existing project, click "Create" to create a new project. Otherwise, select an existing project from the project dropdown. 59 | 60 | Select "Service Account key" from the "Create credentials" dropdown. 61 | 62 | Fill out the form for creating a service account key: 63 | 64 | Service account dropdown: Select "New Service Account". 65 | Service account name: Create a service account a name. 66 | Role: Select "Service Accounts" > "Service Account User". 67 | Service account ID: Leave as is. 68 | Key type: Select P12 key. 69 | Click Create. 70 | 71 | ### Setup Your Private Key 72 | Note the private key password, you'll need this when you're converting your password to a pem file. 73 | 74 | Move this key into the root directory for this project, outside of /src. 75 | 76 | Generate a *.p12 certificate by running this command from the directory for this project: 77 | ``` 78 | $ openssl pkcs12 -in *.p12 -out key.pem -nodes -clcerts 79 | ``` 80 | 81 | ### Configure GA 82 | 83 | You now need to add this service account to GA for the proper permissions. 84 | 85 | In your GA account, create a new user and add the necessary permissions. 86 | 87 | Add a new user. (Admin > User Management > + > Add New Users) 88 | Email Address: example@example-project-123456.iam.gserviceaccount.com. 89 | 90 | Permissions: Select "Read & Analyze." 91 | 92 | **Note that you may need to Enable the Google Analytics Reporting API in your project** 93 | 94 | 95 | ### Configure env vars 96 | We'll need to now configure our env variables. To do this in 11ty, we'll lean on nodeenv, which should alr be a dependency in this project. To take advantage of this, add `VIEW_ID` and `SERVICE_ACCOUNT` to a .env file in your root dir. You can find these values in the view column of the accounts dropdown in GA. 97 | 98 | VIEW_ID=12345678 99 | SERVICE_ACCOUNT_EMAIL=cant-dutch-this@some-example-account-123456.iam.gserviceaccount.com 100 | -------------------------------------------------------------------------------- /functions/google-analytics/google-analytics.js: -------------------------------------------------------------------------------- 1 | const {google} = require('googleapis') 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | const authClient = new google.auth.JWT({ 6 | email: process.env.SERVICE_ACCOUNT_EMAIL, 7 | key: fs.readFileSync(path.join(__dirname, "../../key1.pem"), 'utf8'), 8 | scopes: ['https://www.googleapis.com/auth/analytics.readonly'] 9 | }) 10 | 11 | const headers = { 12 | "Access-Control-Allow-Origin": "*", 13 | "Access-Control-Allow-Headers": "Content-Type" 14 | } 15 | 16 | const queryParams = { 17 | resource: { 18 | reportRequests: [ 19 | { 20 | viewId: process.env.VIEW_ID, 21 | dateRanges: [{startDate: '30daysAgo', endDate: 'yesterday'}], 22 | metrics: [ 23 | {expression: 'ga:pageviews'}, 24 | {expression: 'ga:exits'} 25 | ], 26 | dimensions: [ 27 | {name: 'ga:previousPagePath'}, 28 | {name: 'ga:pagePath'} 29 | ], 30 | orderBys: [ 31 | {fieldName: 'ga:previousPagePath', sortOrder: 'ASCENDING'}, 32 | {fieldName: 'ga:pageviews', sortOrder: 'DESCENDING'} 33 | ], 34 | pageSize: 10000 35 | } 36 | ] 37 | } 38 | } 39 | 40 | 41 | exports.handler = async (event, context, callback) => { 42 | try { 43 | await authClient.authorize() 44 | const analytics = google.analyticsreporting({ 45 | version: 'v4', 46 | auth: authClient 47 | }) 48 | const response = await analytics.reports.batchGet(queryParams) 49 | let [report] = response.data.reports 50 | 51 | let {rows} = report.data 52 | 53 | const data = {} 54 | 55 | for (let row of rows) { 56 | let [previousPagePath, pagePath] = row.dimensions 57 | let pageviews = +row.metrics[0].values[0] 58 | let exits = +row.metrics[0].values[1] 59 | 60 | if (/\?.*$/.test(pagePath) || /\?.*$/.test(previousPagePath)) { 61 | pagePath = pagePath.replace(/\?.*$/, '') 62 | previousPagePath = previousPagePath.replace(/\?.*$/, '') 63 | } 64 | 65 | // Ignore pageviews where the current and previous pages are the same. 66 | if (previousPagePath == pagePath) continue 67 | 68 | if (previousPagePath != '(entrance)') { 69 | data[previousPagePath] = data[previousPagePath] || { 70 | pagePath: previousPagePath, 71 | pageviews: 0, 72 | exits: 0, 73 | nextPageviews: 0, 74 | nextExits: 0, 75 | nextPages: {} 76 | } 77 | 78 | data[previousPagePath].nextPageviews += pageviews 79 | data[previousPagePath].nextExits += exits 80 | 81 | if (data[previousPagePath].nextPages[pagePath]) { 82 | data[previousPagePath].nextPages[pagePath] += pageviews 83 | } else { 84 | data[previousPagePath].nextPages[pagePath] = pageviews 85 | } 86 | } 87 | 88 | data[pagePath] = data[pagePath] || { 89 | pagePath: pagePath, 90 | pageviews: 0, 91 | exits: 0, 92 | nextPageviews: 0, 93 | nextExits: 0, 94 | nextPages: {} 95 | } 96 | 97 | data[pagePath].pageviews += pageviews 98 | data[pagePath].exits += exits 99 | } 100 | // Converts each pages `nextPages` object into a sorted array. 101 | Object.keys(data).forEach((pagePath) => { 102 | const page = data[pagePath] 103 | page.nextPages = Object.keys(page.nextPages) 104 | .map((pagePath) => ({ 105 | pagePath, 106 | pageviews: page.nextPages[pagePath] 107 | })) 108 | .sort((a, b) => { 109 | return b.pageviews - a.pageviews 110 | }) 111 | }) 112 | // Creates a sorted array of pages from the data object. 113 | const pages = Object.keys(data) 114 | .filter((pagePath) => data[pagePath].nextPageviews > 0) 115 | .map((pagePath) => { 116 | const page = data[pagePath] 117 | const {exits, nextPageviews, nextPages} = page 118 | page.percentExits = exits / (exits + nextPageviews) 119 | page.topNextPageProbability = 120 | nextPages[0].pageviews / (exits + nextPageviews) 121 | return page 122 | }) 123 | .sort((a, b) => { 124 | return b.pageviews - a.pageviews 125 | }) 126 | 127 | const aggregatePages = async (pages) => { 128 | const predictions = [] 129 | for (let page of pages) { 130 | const prediction = { 131 | pagePath: page.pagePath, 132 | nextPagePath: page.nextPages[0] ? page.nextPages[0].pagePath : '', 133 | nextPageCertainty: page.nextPages[0] ? page.topNextPageProbability : '' 134 | } 135 | predictions.push(prediction) 136 | } 137 | return predictions 138 | } 139 | 140 | const aggPages = await aggregatePages(pages); 141 | 142 | return { 143 | statusCode: 200, 144 | headers, 145 | body: JSON.stringify(aggPages) 146 | } 147 | } catch (err) { 148 | return { 149 | statusCode: 400, 150 | headers, 151 | body: JSON.stringify({ 152 | status: err 153 | }) 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /functions/google-analytics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-analytics", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "google-analytics.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "googleapis": "^45.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /functions/google-analytics/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | abort-controller@^3.0.0: 6 | version "3.0.0" 7 | resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" 8 | integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== 9 | dependencies: 10 | event-target-shim "^5.0.0" 11 | 12 | agent-base@^4.3.0: 13 | version "4.3.0" 14 | resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" 15 | integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== 16 | dependencies: 17 | es6-promisify "^5.0.0" 18 | 19 | arrify@^2.0.0: 20 | version "2.0.1" 21 | resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" 22 | integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== 23 | 24 | base64-js@^1.3.0: 25 | version "1.3.1" 26 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" 27 | integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== 28 | 29 | bignumber.js@^7.0.0: 30 | version "7.2.1" 31 | resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-7.2.1.tgz#80c048759d826800807c4bfd521e50edbba57a5f" 32 | integrity sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ== 33 | 34 | buffer-equal-constant-time@1.0.1: 35 | version "1.0.1" 36 | resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" 37 | integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= 38 | 39 | debug@^3.1.0: 40 | version "3.2.6" 41 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" 42 | integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== 43 | dependencies: 44 | ms "^2.1.1" 45 | 46 | ecdsa-sig-formatter@1.0.11: 47 | version "1.0.11" 48 | resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" 49 | integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== 50 | dependencies: 51 | safe-buffer "^5.0.1" 52 | 53 | es6-promise@^4.0.3: 54 | version "4.2.8" 55 | resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" 56 | integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== 57 | 58 | es6-promisify@^5.0.0: 59 | version "5.0.0" 60 | resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" 61 | integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= 62 | dependencies: 63 | es6-promise "^4.0.3" 64 | 65 | event-target-shim@^5.0.0: 66 | version "5.0.1" 67 | resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" 68 | integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== 69 | 70 | extend@^3.0.2: 71 | version "3.0.2" 72 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" 73 | integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== 74 | 75 | fast-text-encoding@^1.0.0: 76 | version "1.0.0" 77 | resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz#3e5ce8293409cfaa7177a71b9ca84e1b1e6f25ef" 78 | integrity sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ== 79 | 80 | gaxios@^2.0.1, gaxios@^2.1.0: 81 | version "2.1.0" 82 | resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-2.1.0.tgz#b5d04ec19bf853d4589ccc2e7d61f0f2ab62afee" 83 | integrity sha512-Gtpb5sdQmb82sgVkT2GnS2n+Kx4dlFwbeMYcDlD395aEvsLCSQXJJcHt7oJ2LrGxDEAeiOkK79Zv2A8Pzt6CFg== 84 | dependencies: 85 | abort-controller "^3.0.0" 86 | extend "^3.0.2" 87 | https-proxy-agent "^3.0.0" 88 | is-stream "^2.0.0" 89 | node-fetch "^2.3.0" 90 | 91 | gcp-metadata@^3.2.0: 92 | version "3.2.2" 93 | resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-3.2.2.tgz#dcac6bf65775d5caa3a2e161469c0af068849256" 94 | integrity sha512-vR7kcJMCYJG/mYWp/a1OszdOqnLB/XW1GorWW1hc1lWVNL26L497zypWb9cG0CYDQ4Bl1Wk0+fSZFFjwJlTQgQ== 95 | dependencies: 96 | gaxios "^2.1.0" 97 | json-bigint "^0.3.0" 98 | 99 | google-auth-library@^5.2.0: 100 | version "5.5.1" 101 | resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-5.5.1.tgz#2bf5ade93cb9d00c860d3fb15798db33b39a53a7" 102 | integrity sha512-zCtjQccWS/EHYyFdXRbfeSGM/gW+d7uMAcVnvXRnjBXON5ijo6s0nsObP0ifqileIDSbZjTlLtgo+UoN8IFJcg== 103 | dependencies: 104 | arrify "^2.0.0" 105 | base64-js "^1.3.0" 106 | fast-text-encoding "^1.0.0" 107 | gaxios "^2.1.0" 108 | gcp-metadata "^3.2.0" 109 | gtoken "^4.1.0" 110 | jws "^3.1.5" 111 | lru-cache "^5.0.0" 112 | 113 | google-p12-pem@^2.0.0: 114 | version "2.0.3" 115 | resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-2.0.3.tgz#14ecd78a94bd03bf86d74d9d0724787e85c7731f" 116 | integrity sha512-Tq2kBCANxYYPxaBpTgCpRfdoPs9+/lNzc/Iaee4kuMVW5ascD+HwhpBsTLwH85C9Ev4qfB8KKHmpPQYyD2vg2w== 117 | dependencies: 118 | node-forge "^0.9.0" 119 | 120 | googleapis-common@^3.1.0: 121 | version "3.1.1" 122 | resolved "https://registry.yarnpkg.com/googleapis-common/-/googleapis-common-3.1.1.tgz#387e75d914a3e09fec2cb3daf321e04194c8a379" 123 | integrity sha512-sXNS9oJifZOk2Pa6SzxoSfv0Mj9y/qIOsVV7D8WHuH//90CXNnpR/nCYVa+KcPMDT9ONq21sbtvjfKATMV1Bug== 124 | dependencies: 125 | extend "^3.0.2" 126 | gaxios "^2.0.1" 127 | google-auth-library "^5.2.0" 128 | qs "^6.7.0" 129 | url-template "^2.0.8" 130 | uuid "^3.3.2" 131 | 132 | googleapis@^45.0.0: 133 | version "45.0.0" 134 | resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-45.0.0.tgz#27917682f1eee9502164384ceacdc08a17ba9ffb" 135 | integrity sha512-v5+4Cw+MnG3z9FRNTdOCcOWBvzhp0zcbapI3D1E1ndykIRA7tE8oflSBRyyVpX6BDhRGSqNu2lU8G15Fe+Ih4g== 136 | dependencies: 137 | google-auth-library "^5.2.0" 138 | googleapis-common "^3.1.0" 139 | 140 | gtoken@^4.1.0: 141 | version "4.1.3" 142 | resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-4.1.3.tgz#efa9e42f59d02731f15de466b09331d7afc393cf" 143 | integrity sha512-ofW+FiXjswyKdkjMcDbe6E4K7cDDdE82dGDhZIc++kUECqaE7MSErf6arJPAjcnYn1qxE1/Ti06qQuqgVusovQ== 144 | dependencies: 145 | gaxios "^2.1.0" 146 | google-p12-pem "^2.0.0" 147 | jws "^3.1.5" 148 | mime "^2.2.0" 149 | 150 | https-proxy-agent@^3.0.0: 151 | version "3.0.1" 152 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81" 153 | integrity sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg== 154 | dependencies: 155 | agent-base "^4.3.0" 156 | debug "^3.1.0" 157 | 158 | is-stream@^2.0.0: 159 | version "2.0.0" 160 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" 161 | integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== 162 | 163 | json-bigint@^0.3.0: 164 | version "0.3.0" 165 | resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-0.3.0.tgz#0ccd912c4b8270d05f056fbd13814b53d3825b1e" 166 | integrity sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4= 167 | dependencies: 168 | bignumber.js "^7.0.0" 169 | 170 | jwa@^1.4.1: 171 | version "1.4.1" 172 | resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" 173 | integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== 174 | dependencies: 175 | buffer-equal-constant-time "1.0.1" 176 | ecdsa-sig-formatter "1.0.11" 177 | safe-buffer "^5.0.1" 178 | 179 | jws@^3.1.5: 180 | version "3.2.2" 181 | resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" 182 | integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== 183 | dependencies: 184 | jwa "^1.4.1" 185 | safe-buffer "^5.0.1" 186 | 187 | lru-cache@^5.0.0: 188 | version "5.1.1" 189 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" 190 | integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== 191 | dependencies: 192 | yallist "^3.0.2" 193 | 194 | mime@^2.2.0: 195 | version "2.4.4" 196 | resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" 197 | integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== 198 | 199 | ms@^2.1.1: 200 | version "2.1.2" 201 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 202 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 203 | 204 | node-fetch@^2.3.0: 205 | version "2.6.0" 206 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" 207 | integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== 208 | 209 | node-forge@^0.9.0: 210 | version "0.9.1" 211 | resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5" 212 | integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ== 213 | 214 | qs@^6.7.0: 215 | version "6.9.1" 216 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.1.tgz#20082c65cb78223635ab1a9eaca8875a29bf8ec9" 217 | integrity sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA== 218 | 219 | safe-buffer@^5.0.1: 220 | version "5.2.0" 221 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" 222 | integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== 223 | 224 | url-template@^2.0.8: 225 | version "2.0.8" 226 | resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21" 227 | integrity sha1-/FZaPMy/93MMd19WQflVV5FDnyE= 228 | 229 | uuid@^3.3.2: 230 | version "3.3.3" 231 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" 232 | integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== 233 | 234 | yallist@^3.0.2: 235 | version "3.1.1" 236 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" 237 | integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== 238 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "_site" 3 | functions="functions" 4 | command = "DEBUG=* eleventy" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eleventy-base-blog", 3 | "version": "5.0.0", 4 | "description": "A starter repository for a blog web site using the Eleventy static site generator.", 5 | "scripts": { 6 | "build": "npx eleventy", 7 | "watch": "npx eleventy --watch", 8 | "serve": "npx eleventy --serve", 9 | "debug": "DEBUG=* npx eleventy" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/11ty/eleventy-base-blog.git" 14 | }, 15 | "author": { 16 | "name": "Divya Sasidharan", 17 | "email": "hello@shortdiv.com", 18 | "url": "shortdiv.com" 19 | }, 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/11ty/eleventy-base-blog/issues" 23 | }, 24 | "homepage": "https://github.com/11ty/eleventy-base-blog#readme", 25 | "devDependencies": { 26 | "@11ty/eleventy": "^0.9.0", 27 | "@11ty/eleventy-navigation": "^0.1.1", 28 | "@11ty/eleventy-plugin-rss": "^1.0.7", 29 | "@11ty/eleventy-plugin-syntaxhighlight": "^2.0.3", 30 | "luxon": "^1.12.0", 31 | "markdown-it": "^8.4.2", 32 | "markdown-it-anchor": "^5.0.2" 33 | }, 34 | "dependencies": { 35 | "dotenv": "^8.2.0", 36 | "googleapis": "^45.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/404.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/home.njk 3 | permalink: 404.html 4 | eleventyExcludeFromCollections: true 5 | --- 6 | # Content not found. 7 | 8 | Go home. 9 | 10 | {% comment %} 11 | Read more: https://www.11ty.io/docs/quicktips/not-found/ 12 | 13 | This will work for both GitHub pages and Netlify: 14 | 15 | * https://help.github.com/articles/creating-a-custom-404-page-for-your-github-pages-site/ 16 | * https://www.netlify.com/docs/redirects/#custom-404 17 | {% endcomment %} 18 | -------------------------------------------------------------------------------- /src/_11ty/getTagList.js: -------------------------------------------------------------------------------- 1 | module.exports = function(collection) { 2 | let tagSet = new Set(); 3 | collection.getAll().forEach(function(item) { 4 | if( "tags" in item.data ) { 5 | let tags = item.data.tags; 6 | 7 | tags = tags.filter(function(item) { 8 | switch(item) { 9 | // this list should match the `filter` list in tags.njk 10 | case "all": 11 | case "nav": 12 | case "post": 13 | case "posts": 14 | return false; 15 | } 16 | 17 | return true; 18 | }); 19 | 20 | for (const tag of tags) { 21 | tagSet.add(tag); 22 | } 23 | } 24 | }); 25 | 26 | // returning an array in addCollection works in Eleventy 0.5.3 27 | return [...tagSet]; 28 | }; 29 | -------------------------------------------------------------------------------- /src/_data/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Can't Dutch This", 3 | "url": "https://myurl.com/", 4 | "description": "I am writing about my experiences as a naval navel-gazer.", 5 | "feed": { 6 | "subtitle": "I am writing about my experiences as a naval navel-gazer.", 7 | "filename": "feed.xml", 8 | "path": "/feed/feed.xml", 9 | "url": "https://myurl.com/feed/feed.xml", 10 | "id": "https://myurl.com/" 11 | }, 12 | "author": { 13 | "name": "Your Name Here", 14 | "email": "youremailaddress@example.com" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/_data/prefetchLinks.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const {google} = require('googleapis') 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | const authClient = new google.auth.JWT({ 7 | email: process.env.SERVICE_ACCOUNT_EMAIL, 8 | key: fs.readFileSync(path.join(__dirname, "../../key1.pem"), 'utf8'), 9 | scopes: ['https://www.googleapis.com/auth/analytics.readonly'] 10 | }) 11 | 12 | const queryParams = { 13 | resource: { 14 | reportRequests: [ 15 | { 16 | viewId: process.env.VIEW_ID, 17 | dateRanges: [{startDate: '30daysAgo', endDate: 'yesterday'}], 18 | metrics: [ 19 | {expression: 'ga:pageviews'}, 20 | {expression: 'ga:exits'} 21 | ], 22 | dimensions: [ 23 | {name: 'ga:previousPagePath'}, 24 | {name: 'ga:pagePath'} 25 | ], 26 | orderBys: [ 27 | {fieldName: 'ga:previousPagePath', sortOrder: 'ASCENDING'}, 28 | {fieldName: 'ga:pageviews', sortOrder: 'DESCENDING'} 29 | ], 30 | pageSize: 10000 31 | } 32 | ] 33 | } 34 | } 35 | 36 | module.exports = async function() { 37 | try { 38 | await authClient.authorize() 39 | const analytics = google.analyticsreporting({ 40 | version: 'v4', 41 | auth: authClient 42 | }) 43 | const response = await analytics.reports.batchGet(queryParams) 44 | let [report] = response.data.reports 45 | 46 | let {rows} = report.data 47 | 48 | const data = {} 49 | 50 | for (let row of rows) { 51 | let [previousPagePath, pagePath] = row.dimensions 52 | let pageviews = +row.metrics[0].values[0] 53 | let exits = +row.metrics[0].values[1] 54 | 55 | if (/\?.*$/.test(pagePath) || /\?.*$/.test(previousPagePath)) { 56 | pagePath = pagePath.replace(/\?.*$/, '') 57 | previousPagePath = previousPagePath.replace(/\?.*$/, '') 58 | } 59 | 60 | // Ignore pageviews where the current and previous pages are the same. 61 | if (previousPagePath == pagePath) continue 62 | 63 | if (previousPagePath != '(entrance)') { 64 | data[previousPagePath] = data[previousPagePath] || { 65 | pagePath: previousPagePath, 66 | pageviews: 0, 67 | exits: 0, 68 | nextPageviews: 0, 69 | nextExits: 0, 70 | nextPages: {} 71 | } 72 | 73 | data[previousPagePath].nextPageviews += pageviews 74 | data[previousPagePath].nextExits += exits 75 | 76 | if (data[previousPagePath].nextPages[pagePath]) { 77 | data[previousPagePath].nextPages[pagePath] += pageviews 78 | } else { 79 | data[previousPagePath].nextPages[pagePath] = pageviews 80 | } 81 | } 82 | 83 | data[pagePath] = data[pagePath] || { 84 | pagePath: pagePath, 85 | pageviews: 0, 86 | exits: 0, 87 | nextPageviews: 0, 88 | nextExits: 0, 89 | nextPages: {} 90 | } 91 | 92 | data[pagePath].pageviews += pageviews 93 | data[pagePath].exits += exits 94 | } 95 | // Converts each pages `nextPages` object into a sorted array. 96 | Object.keys(data).forEach((pagePath) => { 97 | const page = data[pagePath] 98 | page.nextPages = Object.keys(page.nextPages) 99 | .map((pagePath) => ({ 100 | pagePath, 101 | pageviews: page.nextPages[pagePath] 102 | })) 103 | .sort((a, b) => { 104 | return b.pageviews - a.pageviews 105 | }) 106 | }) 107 | // Creates a sorted array of pages from the data object. 108 | const pages = Object.keys(data) 109 | .filter((pagePath) => data[pagePath].nextPageviews > 0) 110 | .map((pagePath) => { 111 | const page = data[pagePath] 112 | const {exits, nextPageviews, nextPages} = page 113 | page.percentExits = exits / (exits + nextPageviews) 114 | page.topNextPageProbability = 115 | nextPages[0].pageviews / (exits + nextPageviews) 116 | return page 117 | }) 118 | .sort((a, b) => { 119 | return b.pageviews - a.pageviews 120 | }) 121 | 122 | const aggregatePages = async (pages) => { 123 | const predictions = [] 124 | for (let page of pages) { 125 | const prediction = { 126 | pagePath: page.pagePath, 127 | nextPagePath: page.nextPages[0] ? page.nextPages[0].pagePath : '', 128 | nextPageCertainty: page.nextPages[0] ? page.topNextPageProbability : '' 129 | } 130 | predictions.push(prediction) 131 | } 132 | return predictions 133 | } 134 | 135 | const aggPages = await aggregatePages(pages); 136 | console.log(aggPages) 137 | return aggPages 138 | } catch (err) { 139 | console.log(err) 140 | return err 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/_includes/layouts/about.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | templateClass: tmpl-menu 4 | --- 5 | 6 |
{{ post.url }}
{% endif %}
5 |
6 | {% for tag in post.data.tags %}
7 | {%- if tag != "posts" -%}
8 | {% set tagUrl %}/tags/{{ tag }}/{% endset %}
9 | {{ tag }}
10 | {%- endif -%}
11 | {% endfor %}
12 | URL | 12 |Page Title | 13 | 14 | 15 | {%- for entry in entries %} 16 |
---|---|
{{ entry.url }} |
18 | {{ entry.data.title }} | 19 |
See all tags.
24 | --------------------------------------------------------------------------------