feat: analytics

This commit is contained in:
Andreas Thomas 2023-03-26 13:26:13 +02:00
parent d13531c222
commit 88e2b1dc16
No known key found for this signature in database
14 changed files with 240 additions and 256 deletions

View File

@ -7,12 +7,12 @@ import rehypeAutolinkHeadings from "rehype-autolink-headings";
var computedFields = {
path: {
type: "string",
resolve: (doc) => `/${doc._raw.flattenedPath}`,
resolve: (doc) => `/${doc._raw.flattenedPath}`
},
slug: {
type: "string",
resolve: (doc) => doc._raw.flattenedPath.split("/").slice(1).join("/"),
},
resolve: (doc) => doc._raw.flattenedPath.split("/").slice(1).join("/")
}
};
var Project = defineDocumentType(() => ({
name: "Project",
@ -20,27 +20,27 @@ var Project = defineDocumentType(() => ({
contentType: "mdx",
fields: {
published: {
type: "boolean",
type: "boolean"
},
title: {
type: "string",
required: true,
required: true
},
description: {
type: "string",
required: true,
required: true
},
date: {
type: "date",
type: "date"
},
url: {
type: "string",
type: "string"
},
repository: {
type: "string",
type: "string"
}
},
},
computedFields,
computedFields
}));
var Page = defineDocumentType(() => ({
name: "Page",
@ -49,13 +49,13 @@ var Page = defineDocumentType(() => ({
fields: {
title: {
type: "string",
required: true,
required: true
},
description: {
type: "string",
type: "string"
}
},
},
computedFields,
computedFields
}));
var contentlayer_config_default = makeSource({
contentDirPath: "./content",
@ -78,20 +78,24 @@ var contentlayer_config_default = makeSource({
},
onVisitHighlightedWord(node) {
node.properties.className = ["word--highlighted"];
},
},
}
}
],
[
rehypeAutolinkHeadings,
{
properties: {
className: ["subheading-anchor"],
ariaLabel: "Link to section",
},
},
],
],
},
ariaLabel: "Link to section"
}
}
]
]
}
});
export { Page, Project, contentlayer_config_default as default };
export {
Page,
Project,
contentlayer_config_default as default
};
//# sourceMappingURL=compiled-contentlayer-config-AAEZAM7W.mjs.map

View File

@ -1,3 +1,5 @@
// NOTE This file is auto-generated by Contentlayer
export const allPages = [];
export const allPages = []

View File

@ -1,65 +1,19 @@
// NOTE This file is auto-generated by Contentlayer
import projects__accessMdx from "./projects__access.mdx.json" assert {
type: "json",
};
import projects__envshareMdx from "./projects__envshare.mdx.json" assert {
type: "json",
};
import projects__planetfallMdx from "./projects__planetfall.mdx.json" assert {
type: "json",
};
import projects__qstashMdx from "./projects__qstash.mdx.json" assert {
type: "json",
};
import projects__terraformProviderVercelMdx from "./projects__terraform-provider-vercel.mdx.json" assert {
type: "json",
};
import projects__upstashAuthAnalyticsMdx from "./projects__upstash-auth-analytics.mdx.json" assert {
type: "json",
};
import projects__upstashCliMdx from "./projects__upstash-cli.mdx.json" assert {
type: "json",
};
import projects__upstashCoreAnalyticsMdx from "./projects__upstash-core-analytics.mdx.json" assert {
type: "json",
};
import projects__upstashEdgeFlagsMdx from "./projects__upstash-edge-flags.mdx.json" assert {
type: "json",
};
import projects__upstashKafkaMdx from "./projects__upstash-kafka.mdx.json" assert {
type: "json",
};
import projects__upstashQstashSdkMdx from "./projects__upstash-qstash-sdk.mdx.json" assert {
type: "json",
};
import projects__upstashRatelimitMdx from "./projects__upstash-ratelimit.mdx.json" assert {
type: "json",
};
import projects__upstashReactUiMdx from "./projects__upstash-react-ui.mdx.json" assert {
type: "json",
};
import projects__upstashRedisMdx from "./projects__upstash-redis.mdx.json" assert {
type: "json",
};
import projects__upstashWebAnalyticsMdx from "./projects__upstash-web-analytics.mdx.json" assert {
type: "json",
};
import projects__accessMdx from './projects__access.mdx.json' assert { type: 'json' }
import projects__envshareMdx from './projects__envshare.mdx.json' assert { type: 'json' }
import projects__planetfallMdx from './projects__planetfall.mdx.json' assert { type: 'json' }
import projects__qstashMdx from './projects__qstash.mdx.json' assert { type: 'json' }
import projects__terraformProviderVercelMdx from './projects__terraform-provider-vercel.mdx.json' assert { type: 'json' }
import projects__upstashAuthAnalyticsMdx from './projects__upstash-auth-analytics.mdx.json' assert { type: 'json' }
import projects__upstashCliMdx from './projects__upstash-cli.mdx.json' assert { type: 'json' }
import projects__upstashCoreAnalyticsMdx from './projects__upstash-core-analytics.mdx.json' assert { type: 'json' }
import projects__upstashEdgeFlagsMdx from './projects__upstash-edge-flags.mdx.json' assert { type: 'json' }
import projects__upstashKafkaMdx from './projects__upstash-kafka.mdx.json' assert { type: 'json' }
import projects__upstashQstashSdkMdx from './projects__upstash-qstash-sdk.mdx.json' assert { type: 'json' }
import projects__upstashRatelimitMdx from './projects__upstash-ratelimit.mdx.json' assert { type: 'json' }
import projects__upstashReactUiMdx from './projects__upstash-react-ui.mdx.json' assert { type: 'json' }
import projects__upstashRedisMdx from './projects__upstash-redis.mdx.json' assert { type: 'json' }
import projects__upstashWebAnalyticsMdx from './projects__upstash-web-analytics.mdx.json' assert { type: 'json' }
export const allProjects = [
projects__accessMdx,
projects__envshareMdx,
projects__planetfallMdx,
projects__qstashMdx,
projects__terraformProviderVercelMdx,
projects__upstashAuthAnalyticsMdx,
projects__upstashCliMdx,
projects__upstashCoreAnalyticsMdx,
projects__upstashEdgeFlagsMdx,
projects__upstashKafkaMdx,
projects__upstashQstashSdkMdx,
projects__upstashRatelimitMdx,
projects__upstashReactUiMdx,
projects__upstashRedisMdx,
projects__upstashWebAnalyticsMdx,
];
export const allProjects = [projects__accessMdx, projects__envshareMdx, projects__planetfallMdx, projects__qstashMdx, projects__terraformProviderVercelMdx, projects__upstashAuthAnalyticsMdx, projects__upstashCliMdx, projects__upstashCoreAnalyticsMdx, projects__upstashEdgeFlagsMdx, projects__upstashKafkaMdx, projects__upstashQstashSdkMdx, projects__upstashRatelimitMdx, projects__upstashReactUiMdx, projects__upstashRedisMdx, projects__upstashWebAnalyticsMdx]

View File

@ -1,10 +1,11 @@
// NOTE This file is auto-generated by Contentlayer
import { Page, Project, DocumentTypes } from "./types";
import { Page, Project, DocumentTypes } from './types'
export * from "./types";
export * from './types'
export declare const allPages: Page[];
export declare const allProjects: Project[];
export declare const allPages: Page[]
export declare const allProjects: Project[]
export declare const allDocuments: DocumentTypes[]
export declare const allDocuments: DocumentTypes[];

View File

@ -1,12 +1,12 @@
// NOTE This file is auto-generated by Contentlayer
export { isType } from "contentlayer/client";
export { isType } from 'contentlayer/client'
// NOTE During development Contentlayer imports from `.mjs` files to improve HMR speeds.
// During (production) builds Contentlayer it imports from `.json` files to improve build performance.
import allPages from "./Page/_index.json" assert { type: "json" };
import allProjects from "./Project/_index.json" assert { type: "json" };
import { allPages } from './Page/_index.mjs'
import { allProjects } from './Project/_index.mjs'
export { allPages, allProjects };
export { allPages, allProjects }
export const allDocuments = [...allPages, ...allProjects];
export const allDocuments = [...allPages, ...allProjects]

View File

@ -1,69 +1,66 @@
// NOTE This file is auto-generated by Contentlayer
import type {
Markdown,
MDX,
ImageFieldData,
IsoDateTimeString,
} from "contentlayer/core";
import * as Local from "contentlayer/source-files";
import type { Markdown, MDX, ImageFieldData, IsoDateTimeString } from 'contentlayer/core'
import * as Local from 'contentlayer/source-files'
export { isType } from "contentlayer/client";
export { isType } from 'contentlayer/client'
export type { Markdown, MDX, ImageFieldData, IsoDateTimeString };
export type { Markdown, MDX, ImageFieldData, IsoDateTimeString }
/** Document types */
export type Page = {
/** File path relative to `contentDirPath` */
_id: string;
_raw: Local.RawDocumentData;
type: "Page";
title: string;
description?: string | undefined;
_id: string
_raw: Local.RawDocumentData
type: 'Page'
title: string
description?: string | undefined
/** MDX file body */
body: MDX;
path: string;
slug: string;
};
body: MDX
path: string
slug: string
}
export type Project = {
/** File path relative to `contentDirPath` */
_id: string;
_raw: Local.RawDocumentData;
type: "Project";
published?: boolean | undefined;
title: string;
description: string;
date?: IsoDateTimeString | undefined;
url?: string | undefined;
repository?: string | undefined;
_id: string
_raw: Local.RawDocumentData
type: 'Project'
published?: boolean | undefined
title: string
description: string
date?: IsoDateTimeString | undefined
url?: string | undefined
repository?: string | undefined
/** MDX file body */
body: MDX;
path: string;
slug: string;
};
body: MDX
path: string
slug: string
}
/** Nested types */
/** Helper types */
export type AllTypes = DocumentTypes | NestedTypes;
export type AllTypeNames = DocumentTypeNames | NestedTypeNames;
export type AllTypes = DocumentTypes | NestedTypes
export type AllTypeNames = DocumentTypeNames | NestedTypeNames
export type DocumentTypes = Page | Project;
export type DocumentTypeNames = "Page" | "Project";
export type DocumentTypes = Page | Project
export type DocumentTypeNames = 'Page' | 'Project'
export type NestedTypes = never
export type NestedTypeNames = never
export type NestedTypes = never;
export type NestedTypeNames = never;
export interface ContentlayerGenTypes {
documentTypes: DocumentTypes;
documentTypeMap: DocumentTypeMap;
documentTypeNames: DocumentTypeNames;
nestedTypes: NestedTypes;
nestedTypeMap: NestedTypeMap;
nestedTypeNames: NestedTypeNames;
allTypeNames: AllTypeNames;
documentTypes: DocumentTypes
documentTypeMap: DocumentTypeMap
documentTypeNames: DocumentTypeNames
nestedTypes: NestedTypes
nestedTypeMap: NestedTypeMap
nestedTypeNames: NestedTypeNames
allTypeNames: AllTypeNames
}
declare global {
@ -71,8 +68,12 @@ declare global {
}
export type DocumentTypeMap = {
Page: Page;
Project: Project;
};
Page: Page
Project: Project
}
export type NestedTypeMap = {
}
export type NestedTypeMap = {};

View File

@ -9,8 +9,8 @@ import {
import { MouseEventHandler, PropsWithChildren } from "react";
export const Card: React.FC<PropsWithChildren> = ({ children }) => {
const mouseX = useSpring(0, { stiffness: 200, damping: 100 });
const mouseY = useSpring(0, { stiffness: 200, damping: 100 });
const mouseX = useSpring(0, { stiffness: 500, damping: 100 });
const mouseY = useSpring(0, { stiffness: 500, damping: 100 });
function onMouseMove({ currentTarget, clientX, clientY }: any) {
const { left, top } = currentTarget.getBoundingClientRect();
@ -23,7 +23,7 @@ export const Card: React.FC<PropsWithChildren> = ({ children }) => {
return (
<div
onMouseMove={onMouseMove}
className="overflow-hidden relative duration-700 border rounded-xl hover:bg-zinc-800/30 group md:gap-8 hover:border-zinc-400/50 border-zinc-600 "
className="overflow-hidden relative duration-700 border rounded-xl hover:bg-zinc-800/10 group md:gap-8 hover:border-zinc-400/50 border-zinc-600 "
>
<div className="pointer-events-none">
<div className="absolute inset-0 z-0 transition duration-1000 [mask-image:linear-gradient(black,transparent)]" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -7,10 +7,11 @@ export const metadata: Metadata = {
default: "chronark.com",
template: "%s | chronark.com",
},
description: "software engineer at Upstash and founder of planetfall.io",
description: "Software engineer at upstash.com and founder of planetfall.io",
openGraph: {
title: "chronark.com",
description: "software engineer at Upstash and founder of planetfall.io",
description:
"Software engineer at upstash.com and founder of planetfall.io",
url: "https://chronark.com",
siteName: "chronark.com",
images: [

View File

@ -3,6 +3,7 @@ import { allProjects } from "contentlayer/generated";
import { Mdx } from "@/app/components/mdx";
import { Header } from "./header";
import "./mdx.css";
import { ReportView } from "./view";
type Props = {
params: {
@ -29,6 +30,7 @@ export default async function PostPage({ params }: Props) {
return (
<div className="min-h-screen">
<Header project={project} />
<ReportView slug={project.slug} />
<main className="bg-zinc-50">
<article className="px-4 py-12 mx-auto prose prose-zinc">

View File

@ -0,0 +1,17 @@
"use client";
import { useEffect } from "react";
export const ReportView: React.FC<{ slug: string }> = ({ slug }) => {
useEffect(() => {
fetch("/api/incr", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ slug }),
});
}, [slug]);
return null;
};

View File

@ -75,7 +75,7 @@ export default function ProjectsPage() {
</Link>
</Card>
<div className="flex flex-col w-full max-w-2xl gap-8 pt-12 mx-auto border-t border-gray-900/10 sm:pt-16 lg:mx-0 lg:max-w-none lg:border-t-0 lg:pt-0">
<div className="flex flex-col w-full gap-8 mx-auto border-t border-gray-900/10 lg:pt-16 lg:mx-0 lg:border-t-0 ">
{[top2, top3].map((project) => (
<Card key={project.slug}>
<Article project={project} />
@ -85,7 +85,7 @@ export default function ProjectsPage() {
</div>
<div className="hidden w-full h-px md:block bg-zinc-800" />
<div className="grid max-w-2xl grid-cols-1 gap-4 mx-auto lg:mx-0 lg:max-w-none md:grid-cols-2 lg:grid-cols-3">
<div className="grid grid-cols-1 gap-4 mx-auto lg:mx-0 md:grid-cols-3">
<div className="grid grid-cols-1 gap-4">
{sorted
.filter((_, i) => i % 3 === 0)

View File

@ -1,41 +0,0 @@
import { Redis } from "@upstash/redis";
import { NextRequest, NextResponse, NextFetchEvent } from "next/server";
const redis = Redis.fromEnv();
export const config = {
runtime: "experimental-edge",
matcher: "/projects/:slug*",
};
export default async function middleware(
req: NextRequest,
evt: NextFetchEvent,
): Promise<NextResponse> {
const path = new URL(req.url).pathname;
console.log({ path });
evt.waitUntil(incrementPageView(req.ip, path));
return NextResponse.next();
}
async function incrementPageView(
identifier: string | undefined,
pathname: string,
): Promise<void> {
if (identifier) {
// deduplicate the ip for each slug
const isNew = await redis.set(
["deduplicate", identifier, pathname].join(":"),
true,
{
nx: true,
ex: 24 * 60 * 60,
},
);
if (!isNew) {
return;
}
}
await redis.incr(["pageviews", pathname].join(":"));
}

43
pages/api/incr.ts Normal file
View File

@ -0,0 +1,43 @@
import { Redis } from "@upstash/redis";
import { NextRequest, NextResponse } from "next/server";
const redis = Redis.fromEnv();
export const config = {
runtime: "edge",
};
export default async function incr(req: NextRequest): Promise<NextResponse> {
if (req.method !== "POST") {
return new NextResponse("use POST", { status: 405 });
}
if (req.headers.get("Content-Type") !== "application/json") {
return new NextResponse("must be json", { status: 400 });
}
const body = await req.json();
let slug: string | undefined = undefined;
if ("slug" in body) {
slug = body.slug;
}
if (!slug) {
return new NextResponse("Slug not found", { status: 400 });
}
const identifier = req.ip;
if (identifier) {
// deduplicate the ip for each slug
const isNew = await redis.set(
["deduplicate", identifier, slug].join(":"),
true,
{
nx: true,
ex: 24 * 60 * 60,
},
);
if (!isNew) {
new NextResponse(null, { status: 202 });
}
}
await redis.incr(["pageviews", slug].join(":"));
return new NextResponse(null, { status: 202 });
}