mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-04-26 15:28:28 -04:00
only reveal a db record via /api/hits
if it matches a real page
This commit is contained in:
parent
8d47958473
commit
155c6cacd9
@ -1,9 +1,7 @@
|
||||
// Next.js constants (not needed in frontend)
|
||||
|
||||
import path from "path";
|
||||
|
||||
// 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:
|
||||
export const RELEASE_DATE = new Date(process.env.RELEASE_DATE || Date.now()).toISOString();
|
||||
|
@ -3,8 +3,7 @@ import { getAllNotes } from "./parse-notes";
|
||||
import * as config from "../config";
|
||||
import { RELEASE_DATE } from "../config/constants";
|
||||
import { favicons } from "../config/seo";
|
||||
import type { GetServerSidePropsContext, GetServerSidePropsResult, PreviewData } from "next";
|
||||
import type { ParsedUrlQuery } from "querystring";
|
||||
import type { GetServerSideProps } from "next";
|
||||
|
||||
export type BuildFeedOptions = {
|
||||
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.
|
||||
// all the page needs to do is `return buildFeed(context, "rss")` from getServerSideProps.
|
||||
export const buildFeed = async (
|
||||
context: GetServerSidePropsContext<ParsedUrlQuery, PreviewData>,
|
||||
context: Parameters<GetServerSideProps>[0],
|
||||
type: "rss" | "atom" | "json",
|
||||
options?: BuildFeedOptions
|
||||
): Promise<GetServerSidePropsResult<Record<string, never>>> => {
|
||||
): Promise<ReturnType<GetServerSideProps<Record<string, never>>>> => {
|
||||
const { res } = context;
|
||||
|
||||
// https://github.com/jpmonette/feed#example
|
||||
|
@ -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
|
||||
// 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)
|
||||
export const formatDate = (date?: dayjs.ConfigType, formatStr?: string) => {
|
||||
export const formatDate = (date?: dayjs.ConfigType, formatStr?: string): string => {
|
||||
return IsomorphicDayJs(date).tz(timeZone).format(formatStr);
|
||||
};
|
||||
|
||||
// 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
|
||||
export const formatTimeAgo = (date: dayjs.ConfigType, options?: { suffix?: boolean }) => {
|
||||
export const formatTimeAgo = (date: dayjs.ConfigType, options?: { suffix?: boolean }): string => {
|
||||
return IsomorphicDayJs().isBefore(date)
|
||||
? IsomorphicDayJs(date).toNow(!options?.suffix)
|
||||
: IsomorphicDayJs(date).fromNow(!options?.suffix);
|
||||
|
@ -14,7 +14,7 @@ import type { NoteFrontMatter } from "../../types";
|
||||
export const getNoteSlugs = async (): Promise<string[]> => {
|
||||
// list all .mdx files in NOTES_DIR
|
||||
const mdxFiles = await glob("*.mdx", {
|
||||
cwd: NOTES_DIR,
|
||||
cwd: path.join(process.cwd(), NOTES_DIR),
|
||||
dot: false,
|
||||
});
|
||||
|
||||
@ -31,7 +31,7 @@ export const getNoteData = async (
|
||||
frontMatter: NoteFrontMatter;
|
||||
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 { data, content } = matter(rawContent);
|
||||
|
||||
@ -47,7 +47,7 @@ export const getNoteData = async (
|
||||
smartypants: true,
|
||||
}),
|
||||
slug,
|
||||
permalink: `${baseUrl}/notes/${slug}/`,
|
||||
permalink: `${baseUrl}/${NOTES_DIR}/${slug}/`,
|
||||
date: formatDate(data.date), // validate/normalize the date string provided from front matter
|
||||
},
|
||||
content,
|
||||
|
@ -29,8 +29,8 @@
|
||||
"@primer/octicons": "^17.3.0",
|
||||
"@prisma/client": "^4.0.0",
|
||||
"@react-spring/web": "^9.4.5",
|
||||
"@sentry/node": "^7.5.0",
|
||||
"@sentry/tracing": "^7.5.0",
|
||||
"@sentry/node": "^7.5.1",
|
||||
"@sentry/tracing": "^7.5.1",
|
||||
"@stitches/react": "^1.2.8",
|
||||
"comma-number": "^2.1.0",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { prisma } from "../../lib/helpers/prisma";
|
||||
import { getAllNotes } from "../../lib/helpers/parse-notes";
|
||||
import { logServerError } from "../../lib/helpers/sentry";
|
||||
import { NOTES_DIR } from "../../lib/config/constants";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import type { PageStats, DetailedPageStats, SiteStats } from "../../types";
|
||||
|
||||
@ -58,7 +59,8 @@ const incrementPageHits = async (slug: string): Promise<PageStats> => {
|
||||
};
|
||||
|
||||
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({
|
||||
orderBy: [
|
||||
{
|
||||
@ -69,22 +71,29 @@ const getSiteStats = async (): Promise<SiteStats> => {
|
||||
getAllNotes(),
|
||||
]);
|
||||
|
||||
const pages: DetailedPageStats[] = [];
|
||||
const total = { hits: 0 };
|
||||
|
||||
pages.forEach((page: DetailedPageStats) => {
|
||||
// match URLs from RSS feed with db to populate some metadata
|
||||
const match = notes.find((note) => `notes/${note.slug}` === page.slug);
|
||||
hits.forEach((record) => {
|
||||
// match slugs from getAllNotes() with db results to populate some metadata
|
||||
// TODO: add support for pages other than notes.
|
||||
const match = notes.find((note) => `${NOTES_DIR}/${note.slug}` === record.slug);
|
||||
|
||||
if (match) {
|
||||
page.title = match.title;
|
||||
page.url = match.permalink;
|
||||
page.date = match.date;
|
||||
// don't reveal via API if the db entry doesn't belong to a valid page
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
// add these hits to running tally
|
||||
total.hits += page.hits;
|
||||
// merge record with its matching front matter data
|
||||
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 };
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { SitemapStream, SitemapItemLoose, EnumChangefreq } from "sitemap";
|
||||
import { getAllNotes } from "../lib/helpers/parse-notes";
|
||||
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";
|
||||
|
||||
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();
|
||||
notes.forEach((note) => {
|
||||
pages.push({
|
||||
url: `/notes/${note.slug}/`,
|
||||
url: `/${NOTES_DIR}/${note.slug}/`,
|
||||
// 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
|
||||
pages.push({
|
||||
url: "/notes/",
|
||||
lastmod: new Date(notes[0].date).toISOString(),
|
||||
url: `/${NOTES_DIR}/`,
|
||||
lastmod: notes[0].date,
|
||||
});
|
||||
|
||||
// sort alphabetically by URL
|
||||
|
12
types/stats.d.ts
vendored
12
types/stats.d.ts
vendored
@ -1,13 +1,13 @@
|
||||
import type { NoteFrontMatter } from "./note";
|
||||
|
||||
export type PageStats = {
|
||||
hits: number;
|
||||
};
|
||||
|
||||
export type DetailedPageStats = PageStats & {
|
||||
slug: string;
|
||||
title?: string;
|
||||
url?: string;
|
||||
date?: string;
|
||||
};
|
||||
export type DetailedPageStats = PageStats &
|
||||
Pick<NoteFrontMatter, "slug" | "title" | "date"> & {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type SiteStats = {
|
||||
total: PageStats;
|
||||
|
80
yarn.lock
80
yarn.lock
@ -1378,60 +1378,60 @@
|
||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.4.tgz#0c8b74c50f29ee44f423f7416829c0bf8bb5eb27"
|
||||
integrity sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA==
|
||||
|
||||
"@sentry/core@7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.5.0.tgz#4ccc2312017fc6158cc379f5828dc6bbe2cdf1f7"
|
||||
integrity sha512-2KO2hVUki3WgvPlB0qj9+yea56CmsK2b1XtBSyAnqbs+JiXWgerF4qshVsH52kS/1h2B0CisyeIv64/WfuGvQQ==
|
||||
"@sentry/core@7.5.1":
|
||||
version "7.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.5.1.tgz#6ad186e671d0398dfa4552a2e5686bff6c1938d3"
|
||||
integrity sha512-1ac5eaJi9LBIpCaert+IrttyaL8rnrK5fcdB6tyqDf8jNV5s9O32PyqjvjpWCrGOvZ4kmp+6UXB9bw/NNtvpkQ==
|
||||
dependencies:
|
||||
"@sentry/hub" "7.5.0"
|
||||
"@sentry/types" "7.5.0"
|
||||
"@sentry/utils" "7.5.0"
|
||||
"@sentry/hub" "7.5.1"
|
||||
"@sentry/types" "7.5.1"
|
||||
"@sentry/utils" "7.5.1"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/hub@7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.5.0.tgz#30801accb9475cc3f155802a3fefd218d66fbfda"
|
||||
integrity sha512-R3jGEOtRtZaYCswSNs/7SmjOj/Pp8BhRyXk4q0a5GXghbuVAdzZvlJH0XnD/6jOJAF0iSXFuyGSLqVUmjkY9Ow==
|
||||
"@sentry/hub@7.5.1":
|
||||
version "7.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.5.1.tgz#15817d30aec9e4c1b1bbf7ded6735813f8613e66"
|
||||
integrity sha512-q14zzf5GlE4xvwFP7lZaAI4UnuqWMc3nD62Md5XBptY35bm42CGzawx9aDQ8cegZoQ5bHyX1GPzFju4lDO3O6g==
|
||||
dependencies:
|
||||
"@sentry/types" "7.5.0"
|
||||
"@sentry/utils" "7.5.0"
|
||||
"@sentry/types" "7.5.1"
|
||||
"@sentry/utils" "7.5.1"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/node@^7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.5.0.tgz#e22b27cfb19157aea5019daf6241615887e64321"
|
||||
integrity sha512-lcHIgzcOjKnBeXf9CPbHOwFTuRf+huYMwhF3IVz5Ewbpm4eZn3LPk838ypqtspD7UtFbAdaTGXs/w3Y9P3Zi4g==
|
||||
"@sentry/node@^7.5.1":
|
||||
version "7.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.5.1.tgz#b0c94ed4bc24891c51809e75199e6d1f8de284f2"
|
||||
integrity sha512-XSpNbxBVIpcklLpk9NtQSkTZM0/mEj0TYMnzQmE2UR7UChpGhZyc19nbQWceSsaLMrrAgOX4Zzo28ENk/QY5FA==
|
||||
dependencies:
|
||||
"@sentry/core" "7.5.0"
|
||||
"@sentry/hub" "7.5.0"
|
||||
"@sentry/types" "7.5.0"
|
||||
"@sentry/utils" "7.5.0"
|
||||
"@sentry/core" "7.5.1"
|
||||
"@sentry/hub" "7.5.1"
|
||||
"@sentry/types" "7.5.1"
|
||||
"@sentry/utils" "7.5.1"
|
||||
cookie "^0.4.1"
|
||||
https-proxy-agent "^5.0.0"
|
||||
lru_map "^0.3.3"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/tracing@^7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.5.0.tgz#ad6da27563246e9d754c36a2a3d398cfb979117e"
|
||||
integrity sha512-tSVnCJNImsWms4tBhJ2Xr+HI1i9zKg4eZ0dImi93/H3sf5hmK9r2E11Xs/8rTxqpGWzB8axVi2tcmqmfqXKGTg==
|
||||
"@sentry/tracing@^7.5.1":
|
||||
version "7.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.5.1.tgz#86aca47db8de2c940ca587fb771acd2a88bff815"
|
||||
integrity sha512-fOmzTk3/mTKF5d1P43yZ29lc5z/1wyL2+qX+N5rLluIeR6dEYISyjFistK8z1esMCCvJu8/x3u0imbrrFDNx0Q==
|
||||
dependencies:
|
||||
"@sentry/hub" "7.5.0"
|
||||
"@sentry/types" "7.5.0"
|
||||
"@sentry/utils" "7.5.0"
|
||||
"@sentry/hub" "7.5.1"
|
||||
"@sentry/types" "7.5.1"
|
||||
"@sentry/utils" "7.5.1"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/types@7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.5.0.tgz#610f14c1219ba461ca84a3c89e06de8c0cf357bc"
|
||||
integrity sha512-VPQ/53mLo5N8NQUB4k6R2GQBWoW8otFyhhPnC75gYXeBTItVCzJAylVyWy8b+gGqGst+pQN3wb2dl9xhrd69YQ==
|
||||
"@sentry/types@7.5.1":
|
||||
version "7.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.5.1.tgz#3dd647973fee256588f92d3d74d8ce560b73758b"
|
||||
integrity sha512-+OHxQL4lXCEsUA31qlhcPABOjxtbuL+VTpgamXJjxEpQQDPUPyPK0pu7c+uTc7x4Re96Ss3pwUYE9tl3WW3xIg==
|
||||
|
||||
"@sentry/utils@7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.5.0.tgz#64435ea094aa7d79d1dfe7586d2d5a2bff9e3839"
|
||||
integrity sha512-DgHrkGgHplVMgMbU9hGBfGBV6LcOwNBrhHiVaFwo2NHiXnGwMkaILi5XTRjKm9Iu/m2choAFABA80HEtPKmjtA==
|
||||
"@sentry/utils@7.5.1":
|
||||
version "7.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.5.1.tgz#10877a19372040ebf5bc0342fed69b8147a8d269"
|
||||
integrity sha512-5w5dEDilAkH/4x5h8VMlfFcGKdDQ8tbSEfxnMOheD3/bwk18lTVTgp6kk+VxmugGdvxsTLiPEoORsuofufWvGQ==
|
||||
dependencies:
|
||||
"@sentry/types" "7.5.0"
|
||||
"@sentry/types" "7.5.1"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@stitches/react@^1.2.8":
|
||||
@ -1653,9 +1653,9 @@
|
||||
"@types/unist" "*"
|
||||
|
||||
"@types/node@*":
|
||||
version "18.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.2.tgz#a594e580c396c22dd6b1470be81737c79ec0b1b1"
|
||||
integrity sha512-b947SdS4GH+g2W33wf5FzUu1KLj5FcSIiNWbU1ZyMvt/X7w48ZsVcsQoirIgE/Oq03WT5Qbn/dkY0hePi4ZXcQ==
|
||||
version "18.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.3.tgz#463fc47f13ec0688a33aec75d078a0541a447199"
|
||||
integrity sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==
|
||||
|
||||
"@types/node@^17.0.5":
|
||||
version "17.0.45"
|
||||
|
Loading…
x
Reference in New Issue
Block a user