1
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:
2021-05-30 08:55:37 -04:00
committed by GitHub
parent b3e916e063
commit cfc3334da7
11 changed files with 3571 additions and 114 deletions

View File

@@ -10,3 +10,6 @@ LHCI_TOKEN=
PERCY_TOKEN=
WEBMENTIONS_TOKEN=
FAUNADB_ADMIN_SECRET=
FAUNADB_SERVER_SECRET=

31
assets/js/counter.js Normal file
View 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) => {});
}
})();

View File

@@ -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) {
*,

View File

@@ -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;
}
}
}

View File

@@ -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
View 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),
}),
};
};

View File

@@ -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>

View File

@@ -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" . }}

View File

@@ -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]]

View File

@@ -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",

3470
yarn.lock

File diff suppressed because it is too large Load Diff