From 5430e6ce51e057d3e1b7f8ef29129c011221a426 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Mar 2026 00:03:49 +0000
Subject: [PATCH 1/2] Initial plan
From 6f98cd28d9140d1ac8ee09ef410a4821895b41d1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Mar 2026 00:12:01 +0000
Subject: [PATCH 2/2] Add security headers, global-error handler, JSON-LD
structured data, font display swap, schema tests, CI enhancements
Co-authored-by: JevonThompsonx <104575457+JevonThompsonx@users.noreply.github.com>
---
.github/workflows/ci.yml | 3 +
next.config.ts | 47 ++++++++++++++
src/app/blog/[slug]/page.tsx | 31 ++++++++-
src/app/global-error.tsx | 84 +++++++++++++++++++++++++
src/app/layout.tsx | 21 +++++++
src/schemas/blog.test.ts | 119 +++++++++++++++++++++++++++++++++++
6 files changed, 304 insertions(+), 1 deletion(-)
create mode 100644 src/app/global-error.tsx
create mode 100644 src/schemas/blog.test.ts
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 407be2a..a82c928 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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
diff --git a/next.config.ts b/next.config.ts
index eec398f..dbfcaf6 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -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: {
@@ -14,6 +53,14 @@ const nextConfig: NextConfig = {
},
],
},
+ async headers() {
+ return [
+ {
+ source: "/(.*)",
+ headers: securityHeaders,
+ },
+ ];
+ },
};
export default nextConfig;
diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx
index 3b2192c..32daa5e 100644
--- a/src/app/blog/[slug]/page.tsx
+++ b/src/app/blog/[slug]/page.tsx
@@ -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,
@@ -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 (
-
+ <>
+
+
@@ -136,5 +164,6 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
+ >
);
}
diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx
new file mode 100644
index 0000000..fef1222
--- /dev/null
+++ b/src/app/global-error.tsx
@@ -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 (
+
+
+
+
+ Critical error
+
+
+ Something went critically wrong.
+
+
+ The application encountered an unexpected error at its root level.
+ Try refreshing the page. If the problem persists, please come back
+ later.
+
+
+
+
+
+ );
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f8b66d9..0df1d7c 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -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({
@@ -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 (
+
+
+
diff --git a/src/schemas/blog.test.ts b/src/schemas/blog.test.ts
new file mode 100644
index 0000000..0ffc159
--- /dev/null
+++ b/src/schemas/blog.test.ts
@@ -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);
+ });
+});