mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2026-01-09 19:52:55 -05:00
super simple serverless hit counter for fun (#410)
This commit is contained in:
@@ -10,3 +10,6 @@ LHCI_TOKEN=
|
||||
PERCY_TOKEN=
|
||||
|
||||
WEBMENTIONS_TOKEN=
|
||||
|
||||
FAUNADB_ADMIN_SECRET=
|
||||
FAUNADB_SERVER_SECRET=
|
||||
|
||||
31
assets/js/counter.js
Normal file
31
assets/js/counter.js
Normal file
@@ -0,0 +1,31 @@
|
||||
(function () {
|
||||
// don't continue if there isn't a span#meta-hits element on this page
|
||||
var wrapper = document.getElementById("meta-hits");
|
||||
|
||||
if (wrapper) {
|
||||
wrapper.style.display = "inline-block";
|
||||
|
||||
// deduce a consistent identifier for this page, no matter the URL
|
||||
var canonical = document.createElement("a");
|
||||
canonical.href = document.querySelector("link[rel='canonical']").href;
|
||||
|
||||
// strip beginning and ending forward slash
|
||||
var slug = canonical.pathname.slice(1, -1);
|
||||
|
||||
// this will return an error from the API anyways
|
||||
if (!slug || slug === "/") return;
|
||||
|
||||
fetch("/api/hits?slug=" + slug)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
// finally inject the hits and hide the loading spinner
|
||||
var spinner = document.getElementById("hit-spinner");
|
||||
var counter = document.getElementById("hit-counter");
|
||||
|
||||
spinner.style.display = "none";
|
||||
wrapper.title = data.pretty_hits + " " + data.pretty_unit;
|
||||
counter.appendChild(document.createTextNode(data.pretty_hits));
|
||||
})
|
||||
.catch((error) => {});
|
||||
}
|
||||
})();
|
||||
@@ -16,6 +16,31 @@
|
||||
will-change: transform; // stylelint-disable-line plugin/no-unsupported-browser-features
|
||||
}
|
||||
|
||||
// modified from https://tobiasahlin.com/spinkit/
|
||||
.spinner {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
|
||||
> div {
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
display: inline-block;
|
||||
animation: loader 1.4s infinite ease-in-out both;
|
||||
|
||||
@include colors() {
|
||||
background-color: c(medium-light);
|
||||
}
|
||||
}
|
||||
|
||||
.spin-bounce1 {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
.spin-bounce2 {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
@@ -66,6 +91,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loader {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
40% {
|
||||
transform: scale(0.6);
|
||||
}
|
||||
}
|
||||
|
||||
// https://web.dev/prefers-reduced-motion/#(bonus)-forcing-reduced-motion-on-all-websites
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
|
||||
@@ -6,10 +6,10 @@ div.layout-single {
|
||||
padding-bottom: 1em;
|
||||
|
||||
div#meta {
|
||||
font-size: 0.875em;
|
||||
line-height: 1.3;
|
||||
font-size: 0.825em;
|
||||
line-height: 1.8;
|
||||
letter-spacing: 0.04em;
|
||||
margin-top: 0.8em;
|
||||
margin-top: 0.5em;
|
||||
|
||||
@include colors() {
|
||||
color: c(medium);
|
||||
@@ -19,8 +19,15 @@ div.layout-single {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
span.dash {
|
||||
margin: 0 0.7em;
|
||||
> span {
|
||||
margin-right: 1.25em;
|
||||
white-space: nowrap;
|
||||
|
||||
img.emoji {
|
||||
margin-right: 0.25em;
|
||||
vertical-align: -0.27em;
|
||||
cursor: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,11 @@ disableAliases = true
|
||||
useResourceCacheWhen = "never"
|
||||
|
||||
[server]
|
||||
# proxy /api requests to netlify-lambda's local server (port 9337) for each function
|
||||
[[server.redirects]]
|
||||
from = "/api/**"
|
||||
to = "http://localhost:9337/"
|
||||
status = 200
|
||||
[[server.headers]]
|
||||
for = "/**"
|
||||
[server.headers.values]
|
||||
|
||||
74
functions/hits.js
Normal file
74
functions/hits.js
Normal file
@@ -0,0 +1,74 @@
|
||||
const faunadb = require("faunadb"),
|
||||
q = faunadb.query;
|
||||
const numeral = require("numeral");
|
||||
const pluralize = require("pluralize");
|
||||
require("dotenv").config();
|
||||
|
||||
// .....??????
|
||||
// https://github.com/netlify/netlify-lambda/issues/201
|
||||
require("encoding");
|
||||
|
||||
exports.handler = async (event, context) => {
|
||||
const { slug } = event.queryStringParameters;
|
||||
const client = new faunadb.Client({
|
||||
secret: process.env.FAUNADB_SERVER_SECRET,
|
||||
});
|
||||
|
||||
// some rudimentary error handling
|
||||
if (!slug || slug === "/") {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: JSON.stringify({
|
||||
message: "Page slug required.",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const result = await client.query(
|
||||
q.Let(
|
||||
{
|
||||
match: q.Match(q.Index("hits_by_slug"), slug),
|
||||
},
|
||||
q.If(
|
||||
q.Exists(q.Var("match")),
|
||||
q.Let(
|
||||
{
|
||||
ref: q.Select("ref", q.Get(q.Var("match"))),
|
||||
hits: q.ToInteger(q.Select("hits", q.Select("data", q.Get(q.Var("match"))))),
|
||||
},
|
||||
q.Update(q.Var("ref"), {
|
||||
data: {
|
||||
hits: q.Add(q.Var("hits"), 1),
|
||||
},
|
||||
})
|
||||
),
|
||||
q.Create(q.Collection("hits"), {
|
||||
data: {
|
||||
slug: slug,
|
||||
hits: 1,
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
client.close();
|
||||
|
||||
const hits = result.data.hits;
|
||||
|
||||
// send client the new hit count
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
"Cache-Control": "private, no-cache, no-store, must-revalidate",
|
||||
Expires: "0",
|
||||
Pragma: "no-cache",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
slug: slug,
|
||||
hits: hits,
|
||||
pretty_hits: numeral(hits).format("0,0"),
|
||||
pretty_unit: pluralize("hit", hits),
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -2,7 +2,19 @@
|
||||
<div class="layout layout-single">
|
||||
<article>
|
||||
<div id="meta">
|
||||
<a class="no-underline" href="{{ .Permalink }}" title="{{ .Date.Format "Mon, Jan 2 2006 3:04:05 PM MST" }}">{{ .Date.Format "January 2, 2006" }}</a>{{ with .Scratch.Get "sourceURL" }}<span class="dash">—</span><a class="no-underline" href="{{ . | safeURL }}" title="Edit this post on GitHub" target="_blank" rel="noopener">Improve This Post</a>{{ end }}
|
||||
<span id="meta-date">
|
||||
<a class="no-underline" href="{{ .Permalink }}" title="{{ .Date.Format "Mon, Jan 2 2006 3:04:05 PM MST" }}">📅 {{ .Date.Format "January 2, 2006" }}</a>
|
||||
</span>
|
||||
{{ with .Scratch.Get "sourceURL" }}
|
||||
<span id="meta-edit">
|
||||
<a class="no-underline" href="{{ . | safeURL }}" title="Edit this post on GitHub" target="_blank" rel="noopener">✏️ Improve This Post</a>
|
||||
</span>
|
||||
{{ end }}
|
||||
<span id="meta-hits" style="display:none;">
|
||||
👀
|
||||
<div id="hit-spinner" class="spinner"><div class="spin-bounce1"></div><div class="spin-bounce2"></div><div class="spin-bounce3"></div></div>
|
||||
<span id="hit-counter"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 class="title"><a class="no-underline" href="{{ .Permalink }}">{{ .Title | markdownify }}</a></h1>
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
{{- $includeProdScripts := eq hugo.Environment "production" -}}
|
||||
|
||||
{{- $twemoji := resources.Get "js/vendor/twemoji.js" | resources.ExecuteAsTemplate "js/vendor/twemoji.js" . -}}
|
||||
{{- $counter := resources.Get "js/counter.js" -}}
|
||||
|
||||
{{- $bundle := slice $twemoji -}}
|
||||
{{- $bundle := slice $twemoji $counter -}}
|
||||
|
||||
{{- if $includeProdScripts }}
|
||||
{{- $fathom := resources.Get "js/fathom.js" | resources.ExecuteAsTemplate "js/fathom.js" . }}
|
||||
|
||||
13
netlify.toml
13
netlify.toml
@@ -1,6 +1,7 @@
|
||||
[build]
|
||||
command = "yarn build"
|
||||
publish = "public"
|
||||
functions = ".netlify/functions"
|
||||
|
||||
[build.environment]
|
||||
NODE_VERSION = "14"
|
||||
@@ -24,15 +25,16 @@
|
||||
compress = false
|
||||
|
||||
[context.deploy-preview]
|
||||
command = "yarn build:hugo --environment development --baseURL $DEPLOY_PRIME_URL --buildDrafts --buildFuture"
|
||||
command = "yarn build:functions && yarn build:hugo --environment development --baseURL $DEPLOY_PRIME_URL --buildDrafts --buildFuture"
|
||||
|
||||
[context.branch-deploy]
|
||||
command = "yarn build:hugo --environment development --baseURL $DEPLOY_PRIME_URL --buildDrafts --buildFuture"
|
||||
command = "yarn build:functions && yarn build:hugo --environment development --baseURL $DEPLOY_PRIME_URL --buildDrafts --buildFuture"
|
||||
|
||||
# https://github.com/netlify/cli/blob/master/docs/netlify-dev.md#netlifytoml-dev-block
|
||||
[dev]
|
||||
framework = "#custom"
|
||||
command = "yarn start --port 1338 --baseURL=/ --appendPort=false --disableLiveReload"
|
||||
# only start Hugo, `netlify dev` builds/serves functions itself
|
||||
command = "yarn start:hugo --port=1338 --baseURL=/ --appendPort=false --disableLiveReload"
|
||||
# swap ports to keep consistent w/ normal local server
|
||||
targetPort = 1338
|
||||
port = 1337
|
||||
@@ -152,6 +154,11 @@
|
||||
to = "https://webmention.io/jarv.is/xmlrpc"
|
||||
status = 200
|
||||
force = true
|
||||
## Prettier URLs for Netlify Functions
|
||||
[[redirects]]
|
||||
from = "/api/*"
|
||||
to = "/.netlify/functions/:splat"
|
||||
status = 200
|
||||
|
||||
# More miscellaneous mirrors/redirects:
|
||||
[[redirects]]
|
||||
|
||||
19
package.json
19
package.json
@@ -15,9 +15,12 @@
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf public/ resources/ builds/ _vendor/ .netlify/{cache,functions}/ $TMPDIR/hugo_cache/ hugo_stats.json",
|
||||
"build": "run-s build:hugo minify",
|
||||
"build": "run-s build:** minify",
|
||||
"build:hugo": "hugo --gc --cleanDestinationDir --verbose",
|
||||
"start": "hugo server --disableFastRender --buildDrafts --buildFuture --port=1337 --baseURL=/ --appendPort=false --bind=0.0.0.0 --verbose",
|
||||
"build:functions": "netlify-lambda build functions",
|
||||
"start": "run-p start:hugo start:functions",
|
||||
"start:hugo": "hugo server --disableFastRender --buildDrafts --buildFuture --port=1337 --baseURL=/ --appendPort=false --bind=0.0.0.0 --verbose",
|
||||
"start:functions": "netlify-lambda serve --port 9337 functions",
|
||||
"start:docker": "docker run --rm -v $(pwd):/src:ro -p 1337:1337 $(docker build -q .)",
|
||||
"minify": "run-s minify:**",
|
||||
"minify:html": "html-minifier --html5 --collapse-whitespace --collapse-boolean-attributes --preserve-line-breaks --minify-css --remove-comments --file-ext html --input-dir public --output-dir public **/*.html",
|
||||
@@ -34,15 +37,18 @@
|
||||
"@fontsource/comic-neue": "4.4.0",
|
||||
"@fontsource/inter": "4.4.0",
|
||||
"@fontsource/roboto-mono": "4.4.0",
|
||||
"fathom-client": "3.0.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"encoding": "^0.1.13",
|
||||
"faunadb": "fauna/faunadb-js#master",
|
||||
"modern-normalize": "1.1.0",
|
||||
"numeral": "^2.0.6",
|
||||
"pluralize": "^8.0.0",
|
||||
"twemoji": "13.1.0",
|
||||
"twemoji-emojis": "13.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.2.6",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^10.0.0",
|
||||
"eslint": "~7.27.0",
|
||||
"eslint-config-prettier": "~8.3.0",
|
||||
"eslint-plugin-compat": "~3.9.0",
|
||||
@@ -53,6 +59,7 @@
|
||||
"imagemin-cli": "^6.0.0",
|
||||
"lint-staged": "^11.0.0",
|
||||
"markdownlint-cli": "~0.27.1",
|
||||
"netlify-lambda": "^2.0.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.3.0",
|
||||
"postcss-clean": "jakejarvis/postcss-clean#master",
|
||||
@@ -60,10 +67,10 @@
|
||||
"postcss-color-rgba-fallback": "^4.0.0",
|
||||
"postcss-focus": "^5.0.1",
|
||||
"postcss-import": "^14.0.2",
|
||||
"postcss-merge-rules": "^5.0.1",
|
||||
"postcss-merge-rules": "^5.0.2",
|
||||
"postcss-normalize-charset": "^5.0.1",
|
||||
"postcss-reporter": "^7.0.2",
|
||||
"postcss-svgo": "^5.0.1",
|
||||
"postcss-svgo": "^5.0.2",
|
||||
"prettier": "^2.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"simple-git-hooks": "^2.4.1",
|
||||
|
||||
Reference in New Issue
Block a user