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