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
2 changes: 2 additions & 0 deletions .claude/commands/new-post.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ 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`.
59 changes: 43 additions & 16 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# 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)
Expand All @@ -11,36 +12,62 @@
- **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/*` |

| 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 (39 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) |

| 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.
46 changes: 33 additions & 13 deletions app/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,23 +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 |

| 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
- Use `app/seo.tsx` utilities for metadata — do not write raw `<meta>` tags
- `robots.ts` and `sitemap.ts` are auto-generated at build time
## 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
52 changes: 27 additions & 25 deletions app/tag-data.json
Original file line number Diff line number Diff line change
@@ -1,39 +1,41 @@
{
"sitecoreai": 8,
"composable": 1,
"dxp": 1,
"architecture": 5,
"ai": 1,
"strategy": 1,
"sitecore": 38,
"xmcloud": 8,
"multi-site": 2,
"react": 1,
"nextjs": 5,
"vercel": 4,
"atomic-design": 1,
"sitecore": 39,
"headless-cms": 1,
"multi-site": 2,
"design-systems": 1,
"storybook": 2,
"saas": 6,
"unit-testing": 2,
"github-workflows": 1,
"pages": 1,
"security": 1,
"headless": 11,
"jss": 9,
"azure": 5,
"cicd": 4,
"cloud": 7,
"sitecore-send": 2,
"cdp": 1,
"xmcloud": 8,
"sitecoreai": 8,
"pages": 1,
"saas": 6,
"sxa": 4,
"seo": 1,
"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,
"aws": 1
"seo": 1,
"github-workflows": 1,
"composable": 1,
"dxp": 1,
"ai": 1,
"strategy": 1
}
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