1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-04-26 22:28:30 -04:00

only reveal a db record via /api/hits if it matches a real page

This commit is contained in:
Jake Jarvis 2022-07-06 11:49:41 -04:00
parent 8d47958473
commit 155c6cacd9
Signed by: jake
GPG Key ID: 2B0C9CF251E69A39
9 changed files with 82 additions and 76 deletions

View File

@ -1,9 +1,7 @@
// Next.js constants (not needed in frontend) // Next.js constants (not needed in frontend)
import path from "path";
// directory containing .mdx files relative to project root // directory containing .mdx files relative to project root
export const NOTES_DIR = path.join(process.cwd(), "notes"); export const NOTES_DIR = "notes";
// normalize the timestamp saved when building/deploying (see next.config.js) and fall back to right now: // normalize the timestamp saved when building/deploying (see next.config.js) and fall back to right now:
export const RELEASE_DATE = new Date(process.env.RELEASE_DATE || Date.now()).toISOString(); export const RELEASE_DATE = new Date(process.env.RELEASE_DATE || Date.now()).toISOString();

View File

@ -3,8 +3,7 @@ import { getAllNotes } from "./parse-notes";
import * as config from "../config"; import * as config from "../config";
import { RELEASE_DATE } from "../config/constants"; import { RELEASE_DATE } from "../config/constants";
import { favicons } from "../config/seo"; import { favicons } from "../config/seo";
import type { GetServerSidePropsContext, GetServerSidePropsResult, PreviewData } from "next"; import type { GetServerSideProps } from "next";
import type { ParsedUrlQuery } from "querystring";
export type BuildFeedOptions = { export type BuildFeedOptions = {
edgeCacheAge?: number; // in seconds, defaults to 43200 (12 hours) edgeCacheAge?: number; // in seconds, defaults to 43200 (12 hours)
@ -13,10 +12,10 @@ export type BuildFeedOptions = {
// handles literally *everything* about building the server-side rss/atom feeds and writing the response. // handles literally *everything* about building the server-side rss/atom feeds and writing the response.
// all the page needs to do is `return buildFeed(context, "rss")` from getServerSideProps. // all the page needs to do is `return buildFeed(context, "rss")` from getServerSideProps.
export const buildFeed = async ( export const buildFeed = async (
context: GetServerSidePropsContext<ParsedUrlQuery, PreviewData>, context: Parameters<GetServerSideProps>[0],
type: "rss" | "atom" | "json", type: "rss" | "atom" | "json",
options?: BuildFeedOptions options?: BuildFeedOptions
): Promise<GetServerSidePropsResult<Record<string, never>>> => { ): Promise<ReturnType<GetServerSideProps<Record<string, never>>>> => {
const { res } = context; const { res } = context;
// https://github.com/jpmonette/feed#example // https://github.com/jpmonette/feed#example

View File

@ -21,13 +21,13 @@ const IsomorphicDayJs = (date?: dayjs.ConfigType): dayjs.Dayjs => {
// simple wrapper around dayjs.format() to normalize timezone across the site, both server and client side, to prevent // simple wrapper around dayjs.format() to normalize timezone across the site, both server and client side, to prevent
// hydration errors by returning an instance of dayjs with these defaults set. // hydration errors by returning an instance of dayjs with these defaults set.
// date defaults to now, format defaults to ISO 8601 (e.g. 2022-04-07T21:53:33-04:00) // date defaults to now, format defaults to ISO 8601 (e.g. 2022-04-07T21:53:33-04:00)
export const formatDate = (date?: dayjs.ConfigType, formatStr?: string) => { export const formatDate = (date?: dayjs.ConfigType, formatStr?: string): string => {
return IsomorphicDayJs(date).tz(timeZone).format(formatStr); return IsomorphicDayJs(date).tz(timeZone).format(formatStr);
}; };
// returns the human-friendly difference between now and given date (e.g. "5 minutes", "9 months", etc.) // returns the human-friendly difference between now and given date (e.g. "5 minutes", "9 months", etc.)
// set `{ suffix: true }` to include the "... ago" or "in ..." for past/future // set `{ suffix: true }` to include the "... ago" or "in ..." for past/future
export const formatTimeAgo = (date: dayjs.ConfigType, options?: { suffix?: boolean }) => { export const formatTimeAgo = (date: dayjs.ConfigType, options?: { suffix?: boolean }): string => {
return IsomorphicDayJs().isBefore(date) return IsomorphicDayJs().isBefore(date)
? IsomorphicDayJs(date).toNow(!options?.suffix) ? IsomorphicDayJs(date).toNow(!options?.suffix)
: IsomorphicDayJs(date).fromNow(!options?.suffix); : IsomorphicDayJs(date).fromNow(!options?.suffix);

View File

@ -14,7 +14,7 @@ import type { NoteFrontMatter } from "../../types";
export const getNoteSlugs = async (): Promise<string[]> => { export const getNoteSlugs = async (): Promise<string[]> => {
// list all .mdx files in NOTES_DIR // list all .mdx files in NOTES_DIR
const mdxFiles = await glob("*.mdx", { const mdxFiles = await glob("*.mdx", {
cwd: NOTES_DIR, cwd: path.join(process.cwd(), NOTES_DIR),
dot: false, dot: false,
}); });
@ -31,7 +31,7 @@ export const getNoteData = async (
frontMatter: NoteFrontMatter; frontMatter: NoteFrontMatter;
content: string; content: string;
}> => { }> => {
const fullPath = path.join(NOTES_DIR, `${slug}.mdx`); const fullPath = path.join(process.cwd(), NOTES_DIR, `${slug}.mdx`);
const rawContent = await fs.readFile(fullPath, "utf8"); const rawContent = await fs.readFile(fullPath, "utf8");
const { data, content } = matter(rawContent); const { data, content } = matter(rawContent);
@ -47,7 +47,7 @@ export const getNoteData = async (
smartypants: true, smartypants: true,
}), }),
slug, slug,
permalink: `${baseUrl}/notes/${slug}/`, permalink: `${baseUrl}/${NOTES_DIR}/${slug}/`,
date: formatDate(data.date), // validate/normalize the date string provided from front matter date: formatDate(data.date), // validate/normalize the date string provided from front matter
}, },
content, content,

View File

@ -29,8 +29,8 @@
"@primer/octicons": "^17.3.0", "@primer/octicons": "^17.3.0",
"@prisma/client": "^4.0.0", "@prisma/client": "^4.0.0",
"@react-spring/web": "^9.4.5", "@react-spring/web": "^9.4.5",
"@sentry/node": "^7.5.0", "@sentry/node": "^7.5.1",
"@sentry/tracing": "^7.5.0", "@sentry/tracing": "^7.5.1",
"@stitches/react": "^1.2.8", "@stitches/react": "^1.2.8",
"comma-number": "^2.1.0", "comma-number": "^2.1.0",
"copy-to-clipboard": "^3.3.1", "copy-to-clipboard": "^3.3.1",

View File

@ -1,6 +1,7 @@
import { prisma } from "../../lib/helpers/prisma"; import { prisma } from "../../lib/helpers/prisma";
import { getAllNotes } from "../../lib/helpers/parse-notes"; import { getAllNotes } from "../../lib/helpers/parse-notes";
import { logServerError } from "../../lib/helpers/sentry"; import { logServerError } from "../../lib/helpers/sentry";
import { NOTES_DIR } from "../../lib/config/constants";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import type { PageStats, DetailedPageStats, SiteStats } from "../../types"; import type { PageStats, DetailedPageStats, SiteStats } from "../../types";
@ -58,7 +59,8 @@ const incrementPageHits = async (slug: string): Promise<PageStats> => {
}; };
const getSiteStats = async (): Promise<SiteStats> => { const getSiteStats = async (): Promise<SiteStats> => {
const [pages, notes] = await Promise.all([ // simultaneously fetch the entire hits db and notes from the filesystem
const [hits, notes] = await Promise.all([
prisma.hits.findMany({ prisma.hits.findMany({
orderBy: [ orderBy: [
{ {
@ -69,22 +71,29 @@ const getSiteStats = async (): Promise<SiteStats> => {
getAllNotes(), getAllNotes(),
]); ]);
const pages: DetailedPageStats[] = [];
const total = { hits: 0 }; const total = { hits: 0 };
pages.forEach((page: DetailedPageStats) => { hits.forEach((record) => {
// match URLs from RSS feed with db to populate some metadata // match slugs from getAllNotes() with db results to populate some metadata
const match = notes.find((note) => `notes/${note.slug}` === page.slug); // TODO: add support for pages other than notes.
const match = notes.find((note) => `${NOTES_DIR}/${note.slug}` === record.slug);
if (match) { // don't reveal via API if the db entry doesn't belong to a valid page
page.title = match.title; if (!match) {
page.url = match.permalink; return;
page.date = match.date;
} }
// add these hits to running tally // merge record with its matching front matter data
total.hits += page.hits; pages.push({
...record,
title: match.title,
url: match.permalink,
date: match.date,
});
return page; // add these hits to running tally
total.hits += record.hits;
}); });
return { total, pages }; return { total, pages };

View File

@ -1,7 +1,7 @@
import { SitemapStream, SitemapItemLoose, EnumChangefreq } from "sitemap"; import { SitemapStream, SitemapItemLoose, EnumChangefreq } from "sitemap";
import { getAllNotes } from "../lib/helpers/parse-notes"; import { getAllNotes } from "../lib/helpers/parse-notes";
import { baseUrl } from "../lib/config"; import { baseUrl } from "../lib/config";
import { RELEASE_DATE } from "../lib/config/constants"; import { RELEASE_DATE, NOTES_DIR } from "../lib/config/constants";
import type { GetServerSideProps } from "next"; import type { GetServerSideProps } from "next";
export const getServerSideProps: GetServerSideProps<Record<string, never>> = async (context) => { export const getServerSideProps: GetServerSideProps<Record<string, never>> = async (context) => {
@ -42,16 +42,16 @@ export const getServerSideProps: GetServerSideProps<Record<string, never>> = asy
const notes = await getAllNotes(); const notes = await getAllNotes();
notes.forEach((note) => { notes.forEach((note) => {
pages.push({ pages.push({
url: `/notes/${note.slug}/`, url: `/${NOTES_DIR}/${note.slug}/`,
// pull lastMod from front matter date // pull lastMod from front matter date
lastmod: new Date(note.date).toISOString(), lastmod: note.date,
}); });
}); });
// set lastmod of /notes/ page to most recent post's date // set lastmod of /notes/ page to most recent post's date
pages.push({ pages.push({
url: "/notes/", url: `/${NOTES_DIR}/`,
lastmod: new Date(notes[0].date).toISOString(), lastmod: notes[0].date,
}); });
// sort alphabetically by URL // sort alphabetically by URL

12
types/stats.d.ts vendored
View File

@ -1,13 +1,13 @@
import type { NoteFrontMatter } from "./note";
export type PageStats = { export type PageStats = {
hits: number; hits: number;
}; };
export type DetailedPageStats = PageStats & { export type DetailedPageStats = PageStats &
slug: string; Pick<NoteFrontMatter, "slug" | "title" | "date"> & {
title?: string; url: string;
url?: string; };
date?: string;
};
export type SiteStats = { export type SiteStats = {
total: PageStats; total: PageStats;

View File

@ -1378,60 +1378,60 @@
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.4.tgz#0c8b74c50f29ee44f423f7416829c0bf8bb5eb27" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.4.tgz#0c8b74c50f29ee44f423f7416829c0bf8bb5eb27"
integrity sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA== integrity sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA==
"@sentry/core@7.5.0": "@sentry/core@7.5.1":
version "7.5.0" version "7.5.1"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.5.0.tgz#4ccc2312017fc6158cc379f5828dc6bbe2cdf1f7" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.5.1.tgz#6ad186e671d0398dfa4552a2e5686bff6c1938d3"
integrity sha512-2KO2hVUki3WgvPlB0qj9+yea56CmsK2b1XtBSyAnqbs+JiXWgerF4qshVsH52kS/1h2B0CisyeIv64/WfuGvQQ== integrity sha512-1ac5eaJi9LBIpCaert+IrttyaL8rnrK5fcdB6tyqDf8jNV5s9O32PyqjvjpWCrGOvZ4kmp+6UXB9bw/NNtvpkQ==
dependencies: dependencies:
"@sentry/hub" "7.5.0" "@sentry/hub" "7.5.1"
"@sentry/types" "7.5.0" "@sentry/types" "7.5.1"
"@sentry/utils" "7.5.0" "@sentry/utils" "7.5.1"
tslib "^1.9.3" tslib "^1.9.3"
"@sentry/hub@7.5.0": "@sentry/hub@7.5.1":
version "7.5.0" version "7.5.1"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.5.0.tgz#30801accb9475cc3f155802a3fefd218d66fbfda" resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.5.1.tgz#15817d30aec9e4c1b1bbf7ded6735813f8613e66"
integrity sha512-R3jGEOtRtZaYCswSNs/7SmjOj/Pp8BhRyXk4q0a5GXghbuVAdzZvlJH0XnD/6jOJAF0iSXFuyGSLqVUmjkY9Ow== integrity sha512-q14zzf5GlE4xvwFP7lZaAI4UnuqWMc3nD62Md5XBptY35bm42CGzawx9aDQ8cegZoQ5bHyX1GPzFju4lDO3O6g==
dependencies: dependencies:
"@sentry/types" "7.5.0" "@sentry/types" "7.5.1"
"@sentry/utils" "7.5.0" "@sentry/utils" "7.5.1"
tslib "^1.9.3" tslib "^1.9.3"
"@sentry/node@^7.5.0": "@sentry/node@^7.5.1":
version "7.5.0" version "7.5.1"
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.5.0.tgz#e22b27cfb19157aea5019daf6241615887e64321" resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.5.1.tgz#b0c94ed4bc24891c51809e75199e6d1f8de284f2"
integrity sha512-lcHIgzcOjKnBeXf9CPbHOwFTuRf+huYMwhF3IVz5Ewbpm4eZn3LPk838ypqtspD7UtFbAdaTGXs/w3Y9P3Zi4g== integrity sha512-XSpNbxBVIpcklLpk9NtQSkTZM0/mEj0TYMnzQmE2UR7UChpGhZyc19nbQWceSsaLMrrAgOX4Zzo28ENk/QY5FA==
dependencies: dependencies:
"@sentry/core" "7.5.0" "@sentry/core" "7.5.1"
"@sentry/hub" "7.5.0" "@sentry/hub" "7.5.1"
"@sentry/types" "7.5.0" "@sentry/types" "7.5.1"
"@sentry/utils" "7.5.0" "@sentry/utils" "7.5.1"
cookie "^0.4.1" cookie "^0.4.1"
https-proxy-agent "^5.0.0" https-proxy-agent "^5.0.0"
lru_map "^0.3.3" lru_map "^0.3.3"
tslib "^1.9.3" tslib "^1.9.3"
"@sentry/tracing@^7.5.0": "@sentry/tracing@^7.5.1":
version "7.5.0" version "7.5.1"
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.5.0.tgz#ad6da27563246e9d754c36a2a3d398cfb979117e" resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.5.1.tgz#86aca47db8de2c940ca587fb771acd2a88bff815"
integrity sha512-tSVnCJNImsWms4tBhJ2Xr+HI1i9zKg4eZ0dImi93/H3sf5hmK9r2E11Xs/8rTxqpGWzB8axVi2tcmqmfqXKGTg== integrity sha512-fOmzTk3/mTKF5d1P43yZ29lc5z/1wyL2+qX+N5rLluIeR6dEYISyjFistK8z1esMCCvJu8/x3u0imbrrFDNx0Q==
dependencies: dependencies:
"@sentry/hub" "7.5.0" "@sentry/hub" "7.5.1"
"@sentry/types" "7.5.0" "@sentry/types" "7.5.1"
"@sentry/utils" "7.5.0" "@sentry/utils" "7.5.1"
tslib "^1.9.3" tslib "^1.9.3"
"@sentry/types@7.5.0": "@sentry/types@7.5.1":
version "7.5.0" version "7.5.1"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.5.0.tgz#610f14c1219ba461ca84a3c89e06de8c0cf357bc" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.5.1.tgz#3dd647973fee256588f92d3d74d8ce560b73758b"
integrity sha512-VPQ/53mLo5N8NQUB4k6R2GQBWoW8otFyhhPnC75gYXeBTItVCzJAylVyWy8b+gGqGst+pQN3wb2dl9xhrd69YQ== integrity sha512-+OHxQL4lXCEsUA31qlhcPABOjxtbuL+VTpgamXJjxEpQQDPUPyPK0pu7c+uTc7x4Re96Ss3pwUYE9tl3WW3xIg==
"@sentry/utils@7.5.0": "@sentry/utils@7.5.1":
version "7.5.0" version "7.5.1"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.5.0.tgz#64435ea094aa7d79d1dfe7586d2d5a2bff9e3839" resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.5.1.tgz#10877a19372040ebf5bc0342fed69b8147a8d269"
integrity sha512-DgHrkGgHplVMgMbU9hGBfGBV6LcOwNBrhHiVaFwo2NHiXnGwMkaILi5XTRjKm9Iu/m2choAFABA80HEtPKmjtA== integrity sha512-5w5dEDilAkH/4x5h8VMlfFcGKdDQ8tbSEfxnMOheD3/bwk18lTVTgp6kk+VxmugGdvxsTLiPEoORsuofufWvGQ==
dependencies: dependencies:
"@sentry/types" "7.5.0" "@sentry/types" "7.5.1"
tslib "^1.9.3" tslib "^1.9.3"
"@stitches/react@^1.2.8": "@stitches/react@^1.2.8":
@ -1653,9 +1653,9 @@
"@types/unist" "*" "@types/unist" "*"
"@types/node@*": "@types/node@*":
version "18.0.2" version "18.0.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.2.tgz#a594e580c396c22dd6b1470be81737c79ec0b1b1" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.3.tgz#463fc47f13ec0688a33aec75d078a0541a447199"
integrity sha512-b947SdS4GH+g2W33wf5FzUu1KLj5FcSIiNWbU1ZyMvt/X7w48ZsVcsQoirIgE/Oq03WT5Qbn/dkY0hePi4ZXcQ== integrity sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==
"@types/node@^17.0.5": "@types/node@^17.0.5":
version "17.0.45" version "17.0.45"