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 ( -
+ <> +