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 @@ -13,6 +13,9 @@ jobs:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- name: Dependency vulnerability scan
run: bun audit
continue-on-error: true
- run: bun run type-check
- run: bun run lint
- run: bun run test
Expand Down
37 changes: 37 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
import type { NextConfig } from "next";

const securityHeaders = [
{ key: "X-DNS-Prefetch-Control", value: "on" },
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=(), browsing-topics=()",
},
{
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'",
"base-uri 'self'",
"form-action 'self'",
].join("; "),
},
];

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

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

const blogPostingSchema = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
description: post.excerpt,
datePublished: post.createdAt,
dateModified: post.updatedAt,
url: `${publicEnv.NEXT_PUBLIC_APP_URL}/blog/${post.slug}`,
author: {
"@type": "Person",
name: siteConfig.name,
url: publicEnv.NEXT_PUBLIC_APP_URL,
},
...(post.coverImage
? { image: { "@type": "ImageObject", url: 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
dangerouslySetInnerHTML={{
__html: JSON.stringify(blogPostingSchema).replace(/</g, "\\u003c"),
}}
type="application/ld+json"
/>
<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 +163,6 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
<TableOfContents items={tableOfContents} />
</div>
</div>
</>
);
}
60 changes: 60 additions & 0 deletions src/app/global-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use client";

import { useEffect } from "react";

export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
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: "5rem 1rem",
textAlign: "center",
fontFamily: "system-ui, sans-serif",
}}
>
<p style={{ fontSize: "0.875rem", textTransform: "uppercase", letterSpacing: "0.2em" }}>
Critical error
</p>
<h1 style={{ fontSize: "clamp(2rem, 5vw, 3.5rem)", margin: 0 }}>
Something went wrong at the application level.
</h1>
<p style={{ maxWidth: "36rem", color: "#6b7280" }}>
The error was handled safely. You can try refreshing the page or
clicking the button below to recover.
</p>
<button
onClick={reset}
style={{
padding: "0.75rem 1.5rem",
fontSize: "1rem",
cursor: "pointer",
borderRadius: "0.5rem",
border: "1px solid #d1d5db",
background: "#111827",
color: "#fff",
}}
>
Try again
</button>
</div>
</body>
</html>
);
}
17 changes: 17 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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({
Expand Down Expand Up @@ -32,11 +33,27 @@ export default function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
const personSchema = {
"@context": "https://schema.org",
"@type": "Person",
name: siteConfig.name,
url: publicEnv.NEXT_PUBLIC_APP_URL,
sameAs: [siteConfig.github, siteConfig.linkedin],
jobTitle: siteConfig.title,
email: siteConfig.email,
};

return (
<html lang="en" suppressHydrationWarning>
<body
className={`${sora.variable} ${sourceSans.variable} ${jetbrainsMono.variable}`}
>
<script
dangerouslySetInnerHTML={{
__html: JSON.stringify(personSchema).replace(/</g, "\\u003c"),
}}
type="application/ld+json"
/>
<Providers>
<div className="page-shell flex min-h-screen flex-col">
<a className="skip-link" href="#main-content">
Expand Down
4 changes: 2 additions & 2 deletions src/lib/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,10 @@ export function extractTableOfContents(
continue;
}

const headingText = stripMarkdownSyntax(match[2]);
const headingText = stripMarkdownSyntax(match[2] ?? "");

items.push({
level: match[1].length,
level: (match[1] ?? "").length,
text: headingText,
slug: slugForHeading(headingText),
});
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
Expand Down