Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ jobs:
- run: bun run type-check
- run: bun run lint
- run: bun run test
- run: bun audit
- run: bun run build
env:
NEXT_PUBLIC_APP_URL: http://localhost:3000
47 changes: 47 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
import type { NextConfig } from "next";

const securityHeaders = [
{
key: "X-Frame-Options",
value: "DENY",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "X-XSS-Protection",
value: "1; mode=block",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=(), interest-cohort=()",
},
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{
key: "Content-Security-Policy",
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self'",
"frame-ancestors 'none'",
].join("; "),
},
];

const nextConfig: NextConfig = {
experimental: {
serverActions: {
Expand All @@ -14,6 +53,14 @@ const nextConfig: NextConfig = {
},
],
},
async headers() {
return [
{
source: "/(.*)",
headers: securityHeaders,
},
];
},
};

export default nextConfig;
31 changes: 30 additions & 1 deletion src/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { Badge } from "@/components/ui/badge";
import { buildMetadata } from "@/lib/metadata";
import { extractTableOfContents } from "@/lib/markdown";
import { estimateReadingTime, formatDate } from "@/lib/utils";
import { publicEnv } from "@/lib/env";
import { siteConfig } from "@/lib/site";
import {
getAdjacentPublishedPosts,
getPublishedPostBySlug,
Expand Down Expand Up @@ -55,8 +57,34 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
const [adjacentPosts] = await Promise.all([getAdjacentPublishedPosts(slug)]);
const tableOfContents = extractTableOfContents(post.content);

const articleJsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
description: post.excerpt,
url: `${publicEnv.NEXT_PUBLIC_APP_URL}/blog/${post.slug}`,
datePublished: post.createdAt,
dateModified: post.updatedAt,
author: {
"@type": "Person",
name: siteConfig.name,
url: publicEnv.NEXT_PUBLIC_APP_URL,
},
publisher: {
"@type": "Person",
name: siteConfig.name,
url: publicEnv.NEXT_PUBLIC_APP_URL,
},
...(post.coverImage ? { image: post.coverImage } : {}),
};

return (
<div className="site-container grid w-full gap-8 py-8 pb-16 sm:py-10 lg:grid-cols-[minmax(0,1fr)_19rem] lg:gap-10 lg:pb-20">
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
/>
<div className="site-container grid w-full gap-8 py-8 pb-16 sm:py-10 lg:grid-cols-[minmax(0,1fr)_19rem] lg:gap-10 lg:pb-20">
<article className="space-y-8">
<Link className="section-link" href="/blog">
<ArrowLeft className="h-4 w-4" />
Expand Down Expand Up @@ -136,5 +164,6 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
<TableOfContents items={tableOfContents} />
</div>
</div>
</>
);
}
84 changes: 84 additions & 0 deletions src/app/global-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"use client";

import { useEffect } from "react";

interface GlobalErrorProps {
error: Error & { digest?: string };
reset: () => void;
}

export default function GlobalError({ error, reset }: GlobalErrorProps) {
useEffect(() => {
console.error(error);
}, [error]);

return (
<html lang="en">
<body>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
gap: "1.5rem",
padding: "2rem",
textAlign: "center",
fontFamily: "system-ui, sans-serif",
background: "#f7f5ff",
color: "#11001c",
}}
>
<p
style={{
fontSize: "0.75rem",
fontWeight: 700,
letterSpacing: "0.3em",
textTransform: "uppercase",
color: "#7fb5d3",
}}
>
Critical error
</p>
<h1
style={{
fontSize: "clamp(2rem, 5vw, 3.5rem)",
fontWeight: 700,
lineHeight: 1,
margin: 0,
}}
>
Something went critically wrong.
</h1>
<p
style={{
maxWidth: "40rem",
lineHeight: 1.8,
color: "#251730",
}}
>
The application encountered an unexpected error at its root level.
Try refreshing the page. If the problem persists, please come back
later.
</p>
<button
onClick={reset}
style={{
padding: "0.75rem 1.5rem",
borderRadius: "9999px",
background: "#7fb5d3",
color: "#0f1720",
border: "none",
fontWeight: 600,
fontSize: "1rem",
cursor: "pointer",
}}
>
Try again
</button>
</div>
</body>
</html>
);
}
21 changes: 21 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,25 @@ import { Footer } from "@/components/layout/footer";
import { Providers } from "@/components/providers";
import { buildMetadata } from "@/lib/metadata";
import { siteConfig } from "@/lib/site";
import { publicEnv } from "@/lib/env";
import "./globals.css";

const sora = Sora({
subsets: ["latin"],
variable: "--font-sora",
display: "swap",
});

const sourceSans = Source_Sans_3({
subsets: ["latin"],
variable: "--font-source-sans",
display: "swap",
});

const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-jetbrains-mono",
display: "swap",
});

export const metadata: Metadata = buildMetadata({
Expand All @@ -32,8 +36,25 @@ export default function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
const personJsonLd = {
"@context": "https://schema.org",
"@type": "Person",
name: siteConfig.name,
url: publicEnv.NEXT_PUBLIC_APP_URL,
jobTitle: siteConfig.title,
description: siteConfig.description,
email: siteConfig.email,
sameAs: [siteConfig.github, siteConfig.linkedin],
};

return (
<html lang="en" suppressHydrationWarning>
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(personJsonLd) }}
/>
</head>
<body
className={`${sora.variable} ${sourceSans.variable} ${jetbrainsMono.variable}`}
>
Expand Down
119 changes: 119 additions & 0 deletions src/schemas/blog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { describe, expect, it } from "vitest";
import { createPostSchema, updatePostSchema } from "@/schemas/blog";

describe("createPostSchema", () => {
it("accepts a valid post payload", () => {
const result = createPostSchema.safeParse({
title: "My First Post",
slug: "my-first-post",
category: "Systems",
content: "Some content here.",
excerpt: "A short description.",
published: false,
});

expect(result.success).toBe(true);
});

it("rejects an empty title", () => {
const result = createPostSchema.safeParse({
title: "",
slug: "valid-slug",
category: "Systems",
content: "Content",
published: false,
});

expect(result.success).toBe(false);
});

it("rejects an invalid slug with spaces", () => {
const result = createPostSchema.safeParse({
title: "Title",
slug: "invalid slug",
category: "Systems",
content: "Content",
published: false,
});

expect(result.success).toBe(false);
});

it("rejects an invalid slug with uppercase letters", () => {
const result = createPostSchema.safeParse({
title: "Title",
slug: "InvalidSlug",
category: "Systems",
content: "Content",
published: false,
});

expect(result.success).toBe(false);
});

it("rejects an invalid cover image URL", () => {
const result = createPostSchema.safeParse({
title: "Title",
slug: "valid-slug",
category: "Systems",
content: "Content",
coverImage: "not-a-url",
published: false,
});

expect(result.success).toBe(false);
});

it("accepts an empty string for coverImage (no image)", () => {
const result = createPostSchema.safeParse({
title: "Title",
slug: "valid-slug",
category: "Systems",
content: "Content",
coverImage: "",
published: false,
});

expect(result.success).toBe(true);
});

it("accepts a valid HTTPS cover image URL", () => {
const result = createPostSchema.safeParse({
title: "Title",
slug: "valid-slug",
category: "Systems",
content: "Content",
coverImage: "https://example.com/image.jpg",
published: true,
});

expect(result.success).toBe(true);
});
});

describe("updatePostSchema", () => {
it("requires currentSlug in addition to base fields", () => {
const withoutCurrent = updatePostSchema.safeParse({
title: "Title",
slug: "valid-slug",
category: "Systems",
content: "Content",
published: false,
});

expect(withoutCurrent.success).toBe(false);
});

it("accepts a valid update payload", () => {
const result = updatePostSchema.safeParse({
currentSlug: "old-slug",
title: "Updated Title",
slug: "updated-slug",
category: "Systems",
content: "Updated content.",
published: true,
});

expect(result.success).toBe(true);
});
});