Skip to content
Merged
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
33 changes: 33 additions & 0 deletions .claude/commands/new-post.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
Create a new blog post MDX file for this Next.js + Contentlayer2 blog.

The user will provide a title (or ask them for one if not given). Then:

1. Convert the title to a kebab-case filename: `data/blog/kebab-case-title.mdx`
2. Use today's date in `YYYY-MM-DD` format
3. Ask the user for a one-paragraph summary if they haven't provided one
4. Create the file with this exact frontmatter structure:

```mdx
---
title: 'Title Here'
date: 'YYYY-MM-DD'
tags: []
draft: true
summary: ''
layout: DaveLayout
images: []
authors: ['default']
---

Write your introduction here.
```

Set `draft: true` so it doesn't publish until the user is ready.
Leave `tags: []` empty — the user will add tags as they write.

After creating the file, tell the user:

- The file path created
- To run `yarn dev` to preview it at `http://localhost:3000/blog/[slug]`
- To change `draft: false` when ready to publish
- **Before publishing:** add at least one image to `images: []` — posts without an image fall back to the generic site banner for social sharing. Store images at `public/static/images/posts/YYYY/post-slug-name/` and reference as `/static/images/posts/YYYY/post-slug-name/image.png`.
7 changes: 7 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"permissions": {
"additionalDirectories": [
"c:\\Dave\\Projects\\DaveGoosem.github.io\\.claude"
]
}
}
73 changes: 73 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# DaveGoosem.com — Claude Code Context

## Stack

- **Framework**: Next.js 14 (App Router), TypeScript
- **Content**: Contentlayer2 — MDX files in `data/blog/` and `data/authors/`
- **Styles**: Tailwind CSS 3 (class-based dark mode, Space Grotesk font)
- **Deployment**: Vercel
- **Package manager**: `yarn` — never use `npm install` or `npm run`
- **Comments**: Giscus (GitHub Discussions), configured via env vars
- **Search**: kbar (local `public/search.json`, regenerated on build)
- **Analytics**: Google Analytics via Pliny

## Key Commands

```bash
yarn dev # start dev server
yarn build # production build + postbuild (RSS, search index)
yarn lint # ESLint with auto-fix
```

## Path Aliases (tsconfig.json)

| Alias | Resolves to |
| ------------------------ | ------------------------- |
| `@/components/*` | `components/*` |
| `@/data/*` | `data/*` |
| `@/layouts/*` | `layouts/*` |
| `@/css/*` | `css/*` |
| `contentlayer/generated` | `.contentlayer/generated` |

## Key Directories

| Path | Purpose |
| ----------------------- | -------------------------------------- |
| `app/` | Next.js App Router pages |
| `components/` | Shared React components |
| `layouts/` | Blog post layout templates |
| `data/blog/` | MDX blog posts (41 posts) |
| `data/authors/` | Author MDX profiles |
| `data/siteMetadata.js` | Site-wide config (title, URL, socials) |
| `public/static/images/` | Static image assets |
| `scripts/` | postbuild.mjs (RSS + search index) |

## SEO & Structured Data

The following are already implemented — do not duplicate or replace them:

- **Canonical URLs** — set in `generateMetadata()` in `app/blog/[...slug]/page.tsx` via `post.canonicalUrl ?? computed URL`
- **`BlogPosting` JSON-LD** — generated by Contentlayer in `contentlayer.config.ts` (includes `publisher`, `mainEntityOfPage`, `author`). Injected in `app/blog/[...slug]/page.tsx`
- **`BreadcrumbList` JSON-LD** — injected alongside BlogPosting in `app/blog/[...slug]/page.tsx`
- **`WebSite` JSON-LD** — injected in `app/layout.tsx` (covers all pages)
- **Publisher logo** — uses `DaveGoosem.com_Logo_Black.png` (not the white variant) so it is legible on white backgrounds as Google requires
- **Sitemap** — `app/sitemap.ts`, auto-generated, excludes drafts
- **Robots** — `app/robots.ts`, auto-generated

## Constraints

- Do not add API routes — this is a static/SSG blog with no backend
- Do not add a database or server-side state
- Contentlayer2 auto-generates TypeScript types on `yarn build` / `yarn dev` — do not edit `.contentlayer/` manually
- ESLint uses flat config (`eslint.config.mjs`) — not `.eslintrc`
- External links require `target="_blank"` and `rel="noopener noreferrer"` (enforced by ESLint)

## Gotchas

**Contentlayer scans all of `data/`** — any plain `.md` file placed inside `data/` (e.g. a `CLAUDE.md`) must be added to `contentDirExclude` in `contentlayer.config.ts` → `makeSource()`, otherwise Contentlayer warns and skips it at startup.

**Content Security Policy** — `next.config.js` contains a hand-written CSP string. If you add any external script, font, frame, or image source (e.g. a new analytics provider, embed, or CDN), add its hostname to the appropriate CSP directive in that file or the browser will silently block it.

**Remote images** — `next/image` only proxies domains listed in `next.config.js` → `images.remotePatterns`. Currently only `picsum.photos` is allowed. Add new domains there before using external image URLs in posts or components.

**Bundle analysis** — run `ANALYZE=true yarn build` to open the webpack bundle visualiser. Useful before committing large new dependencies.
43 changes: 43 additions & 0 deletions app/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# App Router — Claude Code Context

## Route Structure

| Route | File | Notes |
| ------------------- | ------------------------------- | ---------------------------------------- |
| `/` | `app/page.tsx` | Home (recent posts via `Main.tsx`) |
| `/blog` | `app/blog/page.tsx` | Post listing |
| `/blog/[...slug]` | `app/blog/[...slug]/page.tsx` | Individual post — slug from MDX filename |
| `/blog/page/[page]` | `app/blog/page/[page]/page.tsx` | Paginated listing |
| `/tags` | `app/tags/page.tsx` | All tags |
| `/tags/[tag]` | `app/tags/[tag]/page.tsx` | Posts by tag |
| `/about` | `app/about/page.tsx` | About page |
| `/projects` | `app/projects/page.tsx` | Projects page |

## Blog Post Rendering

Posts flow: MDX file → Contentlayer2 (generates typed object) → `app/blog/[...slug]/page.tsx` → layout component (e.g. `DaveLayout`).

## SEO & Metadata

**For static pages** (`/about`, `/projects`, `/tags`, etc.) — use `genPageMetadata()` from `app/seo.tsx`:

```ts
import { genPageMetadata } from 'app/seo'
export const metadata = genPageMetadata({ title: 'Page Title', description: '...' })
```

**For blog posts** — metadata is generated dynamically in `app/blog/[...slug]/page.tsx` via `generateMetadata()`. It reads from the post's Contentlayer fields directly — do not duplicate it elsewhere.

**Do not write raw `<meta>` tags** — always go through the Next.js Metadata API or `genPageMetadata()`.

**Structured data (JSON-LD)** — blog posts render two `<script type="application/ld+json">` blocks: `BlogPosting` (from Contentlayer's `structuredData` computed field, enriched with author in `page.tsx`) and `BreadcrumbList` (built inline in `page.tsx`). The root layout renders a `WebSite` schema. Do not add duplicate schema blocks.

**Auto-generated files** (do not edit manually):

- `app/tag-data.json` — tag counts, written by Contentlayer's `onSuccess` callback
- `public/search.json` — kbar search index, written by the same callback
- `robots.ts` and `sitemap.ts` generate `/robots.txt` and `/sitemap.xml` at build time

## No API Routes

This is a static blog — there are no `/api/` routes. Do not add them.
23 changes: 23 additions & 0 deletions app/blog/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export async function generateMetadata({
url: img.includes('http') ? img : siteMetadata.siteUrl + img,
}
})
const canonicalUrl = post.canonicalUrl ?? `${siteMetadata.siteUrl}/${post.path}`

return {
title: post.title,
Expand All @@ -74,6 +75,9 @@ export async function generateMetadata({
description: post.summary,
images: imageList,
},
alternates: {
canonical: canonicalUrl,
},
}
}

Expand Down Expand Up @@ -110,6 +114,21 @@ export default async function Page({ params }: { params: Promise<{ slug: string[
}
})

const breadcrumbSchema = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: siteMetadata.siteUrl },
{ '@type': 'ListItem', position: 2, name: 'Blog', item: `${siteMetadata.siteUrl}/blog` },
{
'@type': 'ListItem',
position: 3,
name: post.title,
item: `${siteMetadata.siteUrl}/${post.path}`,
},
],
}

const Layout = layouts[post.layout || defaultLayout]

return (
Expand All @@ -118,6 +137,10 @@ export default async function Page({ params }: { params: Promise<{ slug: string[
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
/>
<Layout content={mainContent} authorDetails={authorDetails} next={next} prev={prev}>
<MDXLayoutRenderer code={post.body.code} components={components} toc={post.toc} />
</Layout>
Expand Down
25 changes: 25 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,27 @@ export const metadata: Metadata = {
},
}

const websiteSchema = {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: siteMetadata.title,
url: siteMetadata.siteUrl,
description: siteMetadata.description,
author: {
'@type': 'Person',
name: siteMetadata.author,
url: siteMetadata.siteUrl,
},
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: `${siteMetadata.siteUrl}/blog?q={search_term_string}`,
},
'query-input': 'required name=search_term_string',
},
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html
Expand All @@ -76,6 +97,10 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000" />
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
<body className="bg-white pl-[calc(100vw-100%)] text-black antialiased dark:bg-gray-950 dark:text-white">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema) }}
/>
<ThemeProviders>
<Analytics analyticsConfig={siteMetadata.analytics as AnalyticsConfig} />
<SpeedInsights />
Expand Down
42 changes: 41 additions & 1 deletion app/tag-data.json
Original file line number Diff line number Diff line change
@@ -1 +1,41 @@
{"atomic-design":1,"sitecore":38,"headless-cms":1,"multi-site":2,"design-systems":1,"azure":5,"cloud":7,"xmcloud":8,"sitecoreai":8,"pages":1,"saas":6,"sxa":4,"jss":9,"headless":11,"devops":2,"searchstax":2,"solr":6,"cicd":4,"vercel":4,"helix":3,"react":1,"nextjs":5,"architecture":5,"sitecore-send":2,"cdp":1,"aws":1,"accessibility":1,"security":1,"storybook":2,"unit-testing":2,"aspnet":1,"seo":1,"github-workflows":1,"composable":1,"dxp":1,"ai":1,"strategy":1}
{
"atomic-design": 1,
"sitecore": 39,
"headless-cms": 1,
"multi-site": 2,
"design-systems": 1,
"azure": 5,
"cloud": 7,
"xmcloud": 8,
"sitecoreai": 8,
"pages": 1,
"saas": 6,
"sxa": 4,
"jss": 9,
"headless": 11,
"devops": 2,
"searchstax": 2,
"solr": 6,
"cicd": 4,
"vercel": 4,
"helix": 3,
"react": 1,
"nextjs": 5,
"architecture": 5,
"sitecore-send": 2,
"cdp": 1,
"search": 1,
"sitecore-search": 1,
"aws": 1,
"accessibility": 1,
"security": 1,
"storybook": 2,
"unit-testing": 2,
"aspnet": 1,
"seo": 1,
"github-workflows": 1,
"composable": 1,
"dxp": 1,
"ai": 1,
"strategy": 1
}
23 changes: 23 additions & 0 deletions components/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Components — Claude Code Context

## Import Aliases
Use `@/components/` not relative paths when importing from outside this directory.

## Wrapper Components — Always Use These
| Use this | Instead of |
|----------|-----------|
| `@/components/Link` | `next/link` directly |
| `@/components/Image` | `next/image` directly |

The wrappers add security attributes to external links and handle the blog's image conventions.

## MDX Components
`MDXComponents.tsx` maps HTML elements and custom tags to React components for use inside MDX blog posts. Register any new component here if it needs to be usable in MDX.

## Styling
- Use Tailwind utility classes — no custom CSS unless in `css/tailwind.css`
- Dark mode: use `dark:` prefix (class-based, toggled by ThemeSwitch)
- Typography in post body is handled by `@tailwindcss/typography` (the `prose` class applied by layouts)

## Social Icons
Located in `components/social-icons/` — add new icons there, not inline in Header/Footer.
13 changes: 13 additions & 0 deletions contentlayer.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ export const Blog = defineDocumentType(() => ({
description: doc.summary,
image: doc.images ? doc.images[0] : siteMetadata.socialBanner,
url: `${siteMetadata.siteUrl}/${doc._raw.flattenedPath}`,
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `${siteMetadata.siteUrl}/${doc._raw.flattenedPath}`,
},
publisher: {
'@type': 'Organization',
name: siteMetadata.title,
logo: {
'@type': 'ImageObject',
url: `${siteMetadata.siteUrl}/static/images/Logos/DaveGoosem.com_Logo_Black.png`,
},
},
}),
},
},
Expand All @@ -132,6 +144,7 @@ export const Authors = defineDocumentType(() => ({

export default makeSource({
contentDirPath: 'data',
contentDirExclude: ['blog/CLAUDE.md'],
documentTypes: [Blog, Authors],
mdx: {
cwd: process.cwd(),
Expand Down
Loading
Loading