From cd59593b6dc6817fb721ed30ba65eb9274ac4c63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:42:26 +0000 Subject: [PATCH 1/8] Bump qs in the npm_and_yarn group across 1 directory Bumps the npm_and_yarn group with 1 update in the / directory: [qs](https://github.com/ljharb/qs). Updates `qs` from 6.14.1 to 6.15.0 - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/qs/compare/v6.14.1...v6.15.0) --- updated-dependencies: - dependency-name: qs dependency-version: 6.15.0 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- package-lock.json | 34 ++++------------------------------ 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index a478ef5..f8b9b25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -185,7 +185,6 @@ "node_modules/@algolia/client-search": { "version": "5.48.0", "license": "MIT", - "peer": true, "dependencies": { "@algolia/client-common": "5.48.0", "@algolia/requester-browser-xhr": "5.48.0", @@ -300,7 +299,6 @@ "version": "7.29.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1828,7 +1826,6 @@ "node_modules/@citation-js/core": { "version": "0.7.21", "license": "MIT", - "peer": true, "dependencies": { "@citation-js/date": "^0.5.0", "@citation-js/name": "^0.4.2", @@ -2055,7 +2052,6 @@ "node_modules/@effect-ts/core": { "version": "0.60.5", "license": "MIT", - "peer": true, "dependencies": { "@effect-ts/system": "^0.57.5" } @@ -3931,7 +3927,6 @@ "node_modules/@opentelemetry/api": { "version": "1.9.0", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -3959,7 +3954,6 @@ "node_modules/@opentelemetry/core": { "version": "1.30.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, @@ -4110,7 +4104,6 @@ "node_modules/@opentelemetry/sdk-trace-base": { "version": "1.30.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1", @@ -4126,7 +4119,6 @@ "node_modules/@opentelemetry/sdk-trace-node": { "version": "1.30.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/context-async-hooks": "1.30.1", "@opentelemetry/core": "1.30.1", @@ -4455,7 +4447,6 @@ "version": "8.1.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -4678,7 +4669,6 @@ "version": "18.3.28", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -4740,7 +4730,6 @@ "version": "8.55.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/types": "8.55.0", @@ -4979,7 +4968,6 @@ "node_modules/acorn": { "version": "8.15.0", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5022,7 +5010,6 @@ "node_modules/algoliasearch": { "version": "5.48.0", "license": "MIT", - "peer": true, "dependencies": { "@algolia/abtesting": "1.14.0", "@algolia/client-abtesting": "5.48.0", @@ -5501,7 +5488,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5962,7 +5948,6 @@ "version": "0.5.8", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@contentlayer2/cli": "0.5.8", "@contentlayer2/client": "0.5.8", @@ -6773,7 +6758,6 @@ "version": "0.25.1", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -6829,7 +6813,6 @@ "version": "9.39.2", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6888,7 +6871,6 @@ "version": "10.1.8", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -8921,7 +8903,6 @@ "node_modules/jiti": { "version": "1.21.7", "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -10682,7 +10663,6 @@ "node_modules/next": { "version": "15.5.10", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "15.5.10", "@swc/helpers": "0.5.15", @@ -11267,7 +11247,6 @@ "node_modules/picomatch": { "version": "4.0.3", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11358,7 +11337,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11496,7 +11474,6 @@ "version": "3.8.1", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11678,7 +11655,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -11718,7 +11697,6 @@ "node_modules/react": { "version": "18.2.0", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11729,7 +11707,6 @@ "node_modules/react-dom": { "version": "18.2.0", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -13547,7 +13524,6 @@ "node_modules/tailwindcss": { "version": "3.4.19", "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -13723,8 +13699,7 @@ }, "node_modules/tslib": { "version": "2.8.1", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/typanion": { "version": "3.14.0", @@ -13833,7 +13808,6 @@ "version": "5.9.3", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From c07088d5da527af12913b2bab8be2cb04744d0fb Mon Sep 17 00:00:00 2001 From: Dave Goosem Date: Tue, 7 Apr 2026 13:33:42 +1000 Subject: [PATCH 2/8] Claude AI tooling added --- .claude/commands/new-post.md | 31 ++++++++++++++++++++++++ .claude/settings.local.json | 7 ++++++ CLAUDE.md | 46 ++++++++++++++++++++++++++++++++++++ app/CLAUDE.md | 23 ++++++++++++++++++ components/CLAUDE.md | 23 ++++++++++++++++++ data/blog/CLAUDE.md | 45 +++++++++++++++++++++++++++++++++++ 6 files changed, 175 insertions(+) create mode 100644 .claude/commands/new-post.md create mode 100644 .claude/settings.local.json create mode 100644 CLAUDE.md create mode 100644 app/CLAUDE.md create mode 100644 components/CLAUDE.md create mode 100644 data/blog/CLAUDE.md diff --git a/.claude/commands/new-post.md b/.claude/commands/new-post.md new file mode 100644 index 0000000..d4cc1ad --- /dev/null +++ b/.claude/commands/new-post.md @@ -0,0 +1,31 @@ +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 diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..c5a9c62 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "additionalDirectories": [ + "c:\\Dave\\Projects\\DaveGoosem.github.io\\.claude" + ] + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..25f6fc4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,46 @@ +# 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 (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) | + +## 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) diff --git a/app/CLAUDE.md b/app/CLAUDE.md new file mode 100644 index 0000000..055345e --- /dev/null +++ b/app/CLAUDE.md @@ -0,0 +1,23 @@ +# 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 +- Use `app/seo.tsx` utilities for metadata — do not write raw `` tags +- `robots.ts` and `sitemap.ts` are auto-generated at build time + +## No API Routes +This is a static blog — there are no `/api/` routes. Do not add them. diff --git a/components/CLAUDE.md b/components/CLAUDE.md new file mode 100644 index 0000000..7488b4e --- /dev/null +++ b/components/CLAUDE.md @@ -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. diff --git a/data/blog/CLAUDE.md b/data/blog/CLAUDE.md new file mode 100644 index 0000000..a50f111 --- /dev/null +++ b/data/blog/CLAUDE.md @@ -0,0 +1,45 @@ +# Blog Posts — Claude Code Context + +## Creating a New Post +- File name: `kebab-case-title.mdx` in this directory (`data/blog/`) +- Contentlayer picks up all `*.mdx` files here automatically — no registration needed + +## Frontmatter Schema +```yaml +--- +title: 'Your Post Title' +date: 'YYYY-MM-DD' +tags: ['XMCloud', 'Sitecore', 'Next.js'] +draft: false +summary: 'One-paragraph summary displayed on listing pages and in RSS.' +layout: DaveLayout +images: [] +authors: ['default'] +--- +``` + +### Field notes +| Field | Required | Notes | +|-------|----------|-------| +| `title` | yes | Displayed in `

` and `` | +| `date` | yes | ISO format `'YYYY-MM-DD'` | +| `tags` | yes | Array of strings; capitalise consistently (e.g. `'XMCloud'` not `'xmcloud'`) | +| `draft` | yes | `false` to publish, `true` to hide from listings | +| `summary` | yes | Also used for OG description | +| `layout` | no | Default: `DaveLayout`. Options: `DaveLayout`, `PostLayout`, `PostSimple`, `PostBanner` | +| `images` | no | Array of image paths or `[]`. First image used as OG image if provided | +| `authors` | no | Defaults to `['default']` (David Goosem) | + +## Images +Store post images at: +``` +public/static/images/posts/YYYY/post-slug-name/image1.png +``` +Reference in MDX as: +```md +![Alt text](/static/images/posts/YYYY/post-slug-name/image1.png 'Optional title') +``` + +## Common Tags (existing — keep casing consistent) +`XMCloud`, `Sitecore`, `JSS`, `SXA`, `SaaS`, `Next.js`, `Vercel`, `Azure`, `AWS`, +`CI/CD`, `Accessibility`, `SEO`, `Search`, `Solr`, `Architecture`, `Helix` From 08d5ba14557eec0813073da9b310d3b844ce3a9f Mon Sep 17 00:00:00 2001 From: Dave Goosem <davidgoosem@aceik.com.au> Date: Tue, 14 Apr 2026 08:17:11 +1000 Subject: [PATCH 3/8] Sitecore Search configuration blog post --- ...arch-sdk-into-your-jss-nextjs-solution.mdx | 571 ++++++++++++++++++ 1 file changed, 571 insertions(+) create mode 100644 data/blog/sitecore-search-end-to-end-integrating-the-search-sdk-into-your-jss-nextjs-solution.mdx diff --git a/data/blog/sitecore-search-end-to-end-integrating-the-search-sdk-into-your-jss-nextjs-solution.mdx b/data/blog/sitecore-search-end-to-end-integrating-the-search-sdk-into-your-jss-nextjs-solution.mdx new file mode 100644 index 0000000..c40e118 --- /dev/null +++ b/data/blog/sitecore-search-end-to-end-integrating-the-search-sdk-into-your-jss-nextjs-solution.mdx @@ -0,0 +1,571 @@ +--- +title: 'Sitecore Search End-to-End: Integrating the Search SDK into Your JSS Next.js Headless Solution' +date: '2026-04-13' +tags: [] +draft: true +summary: 'A comprehensive guide to integrating Sitecore Search into a JSS Next.js headless solution — covering CEC setup, SDK options, front-end integration, multi-site source scoping, and a disciplined environment promotion workflow.' +layout: DaveLayout +images: [] +authors: ['default'] +--- + +If you're running a Sitecore JSS Next.js headless front end, adding Sitecore Search is a natural next step in the composable DXP story. Unlike Solr — which comes bundled with on-premise Sitecore but requires you to manage infrastructure, scaling, and version compatibility — Sitecore Search is a fully SaaS-based AI-powered search platform. You configure it through a browser, it crawls your sites, and you integrate the results into your front end via a React SDK. No servers. No Solr topology decisions. + +That said, there are real gotchas around multi-site setups, managing multiple environments, and the currently limited ability to promote configuration changes from non-prod to prod. This post walks through all of it end-to-end — from setting up the Customer Engagement Console (CEC) through to wiring up your JSS components, handling multi-site source scoping, and maintaining a sane dev-to-prod promotion workflow. + +--- + +## How Sitecore Search Fits Into the Composable Stack + +Before diving into implementation, it's worth being clear about where Sitecore Search sits relative to your other products. In a typical Sitecore JSS headless stack, you're looking at something like this: + +- **Sitecore CMS** — content management and authoring, delivering content via GraphQL (Experience Edge or inline) +- **Vercel / hosting** — hosting your Next.js JSS head(s) +- **Sitecore Personalize** — behavioural targeting and experimentation +- **Sitecore Search** — content discovery, typeahead, search results pages + +Sitecore Search is independent of your CMS's GraphQL/Edge delivery. Rather than querying your content tree directly, it crawls your *live website* (or non-prod equivalent) and builds its own index. This is important to understand early — your Search index is populated from rendered web pages, not directly from Sitecore content items. That has implications for what you index, when you index it, and how your non-prod crawl targets are configured. + +--- + +## The Two SDK Options (and Which to Use) + +As of 2025/2026 there are two SDK paths for integrating Sitecore Search into your React/Next.js app, and most articles mix them up without explaining the difference. + +**Option 1: `@sitecore-search/react` + `@sitecore-search/ui`** + +This is the original SDK. You install the core React SDK and optionally the UI components package — a pre-built kit of around 15 widget components (PreviewSearch, SearchResults, etc.) built on top of styled-components. + +```bash +npm install @sitecore-search/react @sitecore-search/ui +``` + +**Option 2: `@sitecore-cloudsdk/search`** + +This is the newer Cloud SDK approach, part of the broader `@sitecore-cloudsdk` family that also handles CDP and Personalize integrations. It uses an Edge Context ID rather than a standalone API key, and is the direction Sitecore is moving for all new headless builds. + +```bash +npm install @sitecore-cloudsdk/core @sitecore-cloudsdk/search +``` + +**Which should you use?** For a new project targeting the latest composable Sitecore stack, lean towards `@sitecore-cloudsdk/search` as it aligns with the unified Cloud SDK direction. However, if your team is already familiar with the `@sitecore-search/react` SDK, or you're referencing the official Starter Kit, the older SDK is still fully supported and better documented at the time of writing. This post primarily uses the `@sitecore-search/react` SDK as it has more community examples and a more developed UI kit — but the CEC configuration and multi-environment concepts apply to both. + +--- + +## Part 1: CEC Setup — Do This Before Writing Any Code + +The Customer Engagement Console (CEC) at `cec.sitecorecloud.io` is where all search configuration lives. Every attribute, source, crawler, and widget is managed here. Critically, **this is also where your biggest operational challenge lives** — and we'll come back to that in the environment management section. + +### Understanding Your Environments in the CEC + +The CEC operates with a concept of separate domain instances — typically a **Non-Production** instance and a **Production** instance. Each is a completely separate tenant with its own sources, attributes, API keys, and widget configuration. + +The licensing model for Sitecore Search typically provides one production instance and one non-production instance. The Search non-prod instance is used by *all* your non-prod environments (dev, QA, UAT) simultaneously — there is no per-environment isolation built in. Your production instance is used only by your live production environment. + +This gives you an architecture that looks like this: + +``` +┌─────────────────────────────────────────────┐ +│ Sitecore Search: Non-Prod CEC │ +│ Sources: dev.site-a.com, dev.site-b.com │ +│ API Key: <non-prod-api-key> │ +└─────────────────────────────────────────────┘ + ↑ ↑ ↑ + JSS DEV JSS TEST JSS UAT + Vercel (DEV) Vercel (TEST) Vercel (UAT) + +┌─────────────────────────────────────────────┐ +│ Sitecore Search: Prod CEC │ +│ Sources: site-a.com, site-b.com │ +│ API Key: <prod-api-key> │ +└─────────────────────────────────────────────┘ + ↑ + JSS Production + Vercel (production) +``` + +The implication is that any widget tuning, boosting rules, facet configuration, or document extractor changes you make in the Non-Prod CEC need to be **manually replicated** into the Prod CEC when you're ready to go live. More on managing this process below. + +### Step 1: Define Your Attributes + +Before setting up sources or widgets, define your domain attributes. Think of these as the fields your content will be indexed with. Navigate to **Administration → Domain Settings → Attributes**. + +Sitecore ships a few default entities (Content, Product, Category, Store). For a typical JSS/content site you'll be working with the **Content** entity. Common attributes to set up: + +| Attribute | Type | Features | +|---|---|---| +| `title` | String | Textual Relevance, Return in API Response | +| `description` | String | Textual Relevance, Return in API Response | +| `url` | String | Return in API Response | +| `image_url` | String | Return in API Response | +| `type` | String | Facets, Return in API Response | +| `category` | String | Facets, Suggestions, Return in API Response | +| `date` | Timestamp | Sorting, Return in API Response | +| `site_name` | String | Facets (critical for multi-site) | + +> ⚠️ **Important:** The "Will be used in" feature settings on an attribute **cannot be changed after creation**. If you need to add Facet support to an attribute you've already created without it, you must delete it and recreate it. Plan your attribute schema carefully before you start indexing. + +You'll also need a suggestions block. Ensure the field used for title suggestions is named `title_context_aware` or configure your suggestion block to point to your `title` attribute. This is what powers the typeahead experience. + +### Step 2: Set Up Your Sources + +A **Source** is a crawler configuration targeting a website (or set of pages). For each site in your multi-site setup, you'll create a separate source pointing at each head. + +Navigate to **Sources → Add Source** and select **Web Crawler (Advanced)**. This is the option you want — it supports multi-language, JavaScript rendering, and gives you the most control over document extraction. + +Key settings to configure for each source: + +**Triggers — how the crawler knows what to crawl** + +Using your sitemap is the most reliable trigger. Most sites already expose `/sitemap.xml` for SEO purposes, so reuse it here. The crawler will enumerate all pages in the sitemap and crawl each one. + +``` +Trigger type: Sitemap +URL: https://dev.site-a.com/sitemap.xml +``` + +**Scan Frequency** + +Configure how often the crawler re-indexes your site. For non-prod you might set this less frequently (weekly is fine), while for production you'll want to balance freshness against crawl cost. + +**Document Extractors** + +This is where most of the custom logic lives, and is the most common area that differs between implementations. Extractors define how the crawler maps page content to your attributes. You'll use the **JS Extractor**, which runs a Cheerio-based script against the crawled HTML. + +A basic extractor for a JSS site might look like this: + +```javascript +function extract(request, response, document) { + const $ = document.content; + + // Pull from meta tags that your JSS head renders + const title = $('meta[property="og:title"]').attr('content') + || $('title').text() + || ''; + + const description = $('meta[property="og:description"]').attr('content') + || $('meta[name="description"]').attr('content') + || ''; + + const imageUrl = $('meta[property="og:image"]').attr('content') || ''; + + // Use a data attribute on your layout for structured type data + const type = $('body').data('page-type') || 'content'; + + // Site name — critical for multi-site filtering + const siteName = $('body').data('site-name') || 'default'; + + return { + title, + description, + image_url: imageUrl, + type, + site_name: siteName, + }; +} +``` + +> 💡 **Tip:** In your JSS layout component, render `data-site-name` and `data-page-type` attributes on `<body>` using your SXA site settings. This gives your crawler reliable structured metadata to extract without needing to scrape visible content. This is especially important in multi-site solutions where you want to filter results per-site at query time. + +You can test your extractor JS against live HTML using the [Cheerio playground](https://try.cheerio.js.org/) before saving it in the CEC. This will save you a lot of unnecessary re-crawl cycles. + +**Excluding Pages from Search** + +Not every page should appear in search results — search pages themselves, landing pages, gated content, etc. The cleanest approach is to add a custom meta tag to those pages: + +```html +<meta property="excludeFromSearch" content="true" /> +``` + +Then in your document extractor, check for it and return `null` to prevent indexing: + +```javascript +function extract(request, response, document) { + const $ = document.content; + + const exclude = $('meta[property="excludeFromSearch"]').attr('content'); + if (exclude === 'true') return null; + + // ... rest of extraction +} +``` + +In Sitecore, create a `_Search` base template at the Foundation layer with an `excludeFromSearch` checkbox field. Add it as a base template on your Page template, and render the meta tag conditionally in your JSS layout. + +Optionally if you exclude the page from Sitemap it won't be crawled as well given we're using the sitemap for our crawling. + +### Step 3: Configure Widgets + +Widgets are the reusable search UI configurations — they define things like which facets are displayed, how results are sorted, and what boosting rules apply. You need at least two: + +- **PreviewSearch** — the typeahead/inline search widget that shows as users type +- **SearchResults** — the full results page widget + +Create each widget in **Widgets**, noting the `rfk_id` assigned to each. This ID is how your React components connect to the right widget configuration. + +For PreviewSearch, ensure your suggestions block is configured and pointing to the right attributes. For SearchResults, configure your facet fields (map them to the attributes you set up with Facet enabled). + +> ⚠️ Another important quirk: the widget **"Will be used in"** setting cannot be changed after creation (just like attributes). Set it correctly — typically "Search" for SearchResults and "Preview Search" for typeahead. + +--- + +## Part 2: Front-End Integration + +With the CEC configured and content indexed, it's time to wire up the front end. Here's the setup for a JSS Next.js application using `@sitecore-search/react`. + +### Installation + +```bash +npm install @sitecore-search/react @sitecore-search/ui styled-components +``` + +### Environment Variables + +You'll need different values for each environment tier. In your `.env.local` (and in Vercel's environment variable settings per deployment): + +```env +# Non-prod environments (dev, QA, UAT) +NEXT_PUBLIC_SEARCH_ENV=staging +NEXT_PUBLIC_SEARCH_CUSTOMER_KEY=your-non-prod-customer-key +NEXT_PUBLIC_SEARCH_API_KEY=your-non-prod-api-key + +# Production (set separately in Vercel prod environment) +# NEXT_PUBLIC_SEARCH_ENV=prod +# NEXT_PUBLIC_SEARCH_CUSTOMER_KEY=your-prod-customer-key +# NEXT_PUBLIC_SEARCH_API_KEY=your-prod-api-key +``` + +> 📝 `NEXT_PUBLIC_SEARCH_ENV` accepts specific values: `prod`, `staging`, `prodEu`, or `apse2`. It is *not* a free-text label — it maps to Sitecore's actual API endpoint regions. For Australian/APAC implementations, `apse2` is the relevant value. + +### WidgetsProvider Setup + +The `WidgetsProvider` is the root component that initialises the Search SDK and manages all communication between your widget components. It should wrap the parts of your application that use search — typically at the layout level. + +In your JSS `Layout.tsx`: + +```tsx +import { WidgetsProvider, PageController } from '@sitecore-search/react'; + +const searchConfig = { + env: process.env.NEXT_PUBLIC_SEARCH_ENV as string, + customerKey: process.env.NEXT_PUBLIC_SEARCH_CUSTOMER_KEY as string, + apiKey: process.env.NEXT_PUBLIC_SEARCH_API_KEY as string, +}; + +export default function Layout({ layoutData }: LayoutProps) { + return ( + <WidgetsProvider {...searchConfig}> + {/* Your existing layout markup */} + <Header /> + <main>{/* page content */}</main> + <Footer /> + </WidgetsProvider> + ); +} +``` + +### Building the PreviewSearch (Typeahead) Component + +The PreviewSearch widget is the typeahead experience in your site header. It's the most visible part of Search and the trickiest to get right in a multi-site setup. + +Create a Sitecore rendering in the CMS and a corresponding component in your JSS app: + +```tsx +// components/search/PreviewSearch.tsx +import { usePreviewSearch, widget } from '@sitecore-search/react'; +import { PreviewSearch as PreviewSearchUI } from '@sitecore-search/ui'; +import { useRouter } from 'next/router'; + +interface PreviewSearchProps { + rfkId: string; // Pass this from your Sitecore rendering parameters +} + +const PreviewSearchComponent = ({ rfkId }: PreviewSearchProps) => { + const router = useRouter(); + + const { + widgetRef, + actions: { onItemClick, onKeyphraseChange }, + queryResult: { data: { suggestion: { title_context_aware: suggestions = [] } = {} } = {} }, + } = usePreviewSearch({ + query: (query) => + query + .getRequest() + .setSearchQueryHighlightFragmentSize(100), + }); + + const handleSubmit = (value: string) => { + router.push(`/search?q=${encodeURIComponent(value)}`); + }; + + return ( + <PreviewSearchUI.Root ref={widgetRef}> + <PreviewSearchUI.Input + onChange={(e) => onKeyphraseChange({ keyphrase: e.target.value })} + onEnterKeyphrase={handleSubmit} + /> + {suggestions.length > 0 && ( + <PreviewSearchUI.Suggestions> + {suggestions.map((suggestion, index) => ( + <PreviewSearchUI.SuggestionItem + key={index} + onClick={() => handleSubmit(suggestion.text)} + > + {suggestion.text} + </PreviewSearchUI.SuggestionItem> + ))} + </PreviewSearchUI.Suggestions> + )} + </PreviewSearchUI.Root> + ); +}; + +// Widget wrapping connects this component to the CEC widget configuration +export const PreviewSearch = widget(PreviewSearchComponent, PreviewSearchUI.Default, 'preview-search'); +``` + +> 💡 The `rfk_id` you pass here must match the ID assigned to your widget in the CEC. For multi-site, you can either create separate widgets per site in the CEC and pass different `rfk_id` values per site, or use one shared widget and filter results using a source filter at query time. + +### Building the Search Results Page + +Create a `/search` page in your Next.js app and a Search Results component: + +```tsx +// components/search/SearchResults.tsx +import { useSearchResults, widget, FilterEqual } from '@sitecore-search/react'; +import { SearchResults as SearchResultsUI, Pagination, FacetList } from '@sitecore-search/ui'; +import { useRouter } from 'next/router'; + +interface SearchResultItem { + id: string; + title: string; + description: string; + url: string; + image_url?: string; + type?: string; + site_name?: string; +} + +const SearchResultsComponent = () => { + const router = useRouter(); + const keyphrase = (router.query.q as string) || ''; + + const { + widgetRef, + actions: { onResultsPerPageChange, onPageNumberChange, onFacetClick }, + state: { page, itemsPerPage }, + queryResult: { + data: { + total_item: totalItems = 0, + facet: facets = [], + content: results = [], + } = {}, + }, + } = useSearchResults<SearchResultItem>({ + query: (query) => { + query + .getRequest() + .setSearchQueryKeyphrase(keyphrase) + // Multi-site: filter to only show results from this site + .addSearchQueryFilter(new FilterEqual('site_name', process.env.NEXT_PUBLIC_SITE_NAME as string)); + }, + }); + + return ( + <div ref={widgetRef}> + <p>{totalItems} results for "{keyphrase}"</p> + + <div className="search-layout"> + {/* Facets sidebar */} + <aside> + {facets.map((facet) => ( + <FacetList + key={facet.name} + facet={facet} + onFacetClick={onFacetClick} + /> + ))} + </aside> + + {/* Results */} + <main> + {results.map((result) => ( + <article key={result.id}> + {result.image_url && <img src={result.image_url} alt={result.title} />} + <h3><a href={result.url}>{result.title}</a></h3> + <p>{result.description}</p> + </article> + ))} + + <Pagination + currentPage={page} + totalItems={totalItems} + itemsPerPage={itemsPerPage} + onPageChange={onPageNumberChange} + /> + </main> + </div> + </div> + ); +}; + +export const SearchResults = widget( + SearchResultsComponent, + SearchResultsUI.Default, + 'search-results' +); +``` + +Add `NEXT_PUBLIC_SITE_NAME` to your Vercel environment variables, set per-site/per-head deployment. This is the filter that scopes search results to the current site — essential in a multi-site setup where all sites share the same non-prod Search instance. + +### The `@sitecore-search/ui` Bundle Size Gotcha + +Before you commit to using the full `@sitecore-search/ui` package in production, be aware of a meaningful performance trade-off. Including the full UI kit has been reported to drop Lighthouse performance scores significantly, with "Reduce unused JavaScript" appearing as a red flag in audits. The package ships all widget templates together without deep tree-shaking support. + +Your options: + +1. **Use the UI kit but measure it.** Run a Lighthouse audit with and without the package. If your score drops below acceptable thresholds, move to option 2. +2. **Use only the SDK hooks (`@sitecore-search/react`) and build your own UI.** The hooks (`usePreviewSearch`, `useSearchResults`) are lean and give you full control. Pair with your existing Tailwind components. +3. **Hybrid approach.** Use the SDK hooks directly for components in your critical rendering path (header PreviewSearch), and use the UI kit for the lower-priority search results page where the performance impact is less significant. + +For most client builds, option 2 — using the SDK hooks with your own Tailwind-styled components — produces the best balance of performance and design consistency with the rest of your site. + +--- + +## Part 3: Multi-Site Source Scoping + +If you're following a proper multi-site setup (as covered in previous posts), you have multiple Next.js heads, each deployed to their own Vercel project. Here's how to manage Search across them cleanly. + +### One Source Per Site Per Environment Tier + +In the CEC, create a source for each site: + +**Non-Prod CEC sources:** +- `Site A — Non-Prod` → crawls `dev.site-a.com` +- `Site B — Non-Prod` → crawls `dev.site-b.com` + +**Prod CEC sources:** +- `Site A — Prod` → crawls `site-a.com` +- `Site B — Prod` → crawls `site-b.com` + +All non-prod environments (dev, QA, UAT) share the non-prod CEC and its sources, connected via the same non-prod API key. Each Vercel deployment has its own env var pointing to the appropriate CEC key. + +### Scoping Results Per Site at Query Time + +As shown in the Search Results component above, add a `FilterEqual` on `site_name` using the `NEXT_PUBLIC_SITE_NAME` environment variable. Set this per Vercel deployment: + +``` +# Site A Vercel project (all environments) +NEXT_PUBLIC_SITE_NAME=site-a + +# Site B Vercel project (all environments) +NEXT_PUBLIC_SITE_NAME=site-b +``` + +And in your document extractor, ensure `site_name` is populated consistently: + +```javascript +// In your CEC document extractor +const siteName = $('body').data('site-name') || 'default'; +return { ..., site_name: siteName }; +``` + +In your JSS layout, render the data attribute: + +```tsx +// _document.tsx or Layout.tsx +<body data-site-name={process.env.NEXT_PUBLIC_SITE_NAME}> +``` + +This creates a clean chain: Vercel env var → rendered HTML attribute → crawled by Search → indexed as `site_name` attribute → filtered at query time per-site. + +### Separate Widgets or Shared Widgets? + +For PreviewSearch, you can use the same CEC widget across all sites (the `rfk_id` is identical in both CEC instances) since the site scoping is handled at the source/filter level. However, if your sites have meaningfully different facet schemas or boosting rules, create separate widgets per site and pass the appropriate `rfk_id` from Sitecore rendering parameters. + +--- + +## Part 4: Environment Promotion — The Manual Problem + +This is the part of Sitecore Search that will catch you off guard if you're not prepared for it. + +**There is currently no automated way to export configuration from the Non-Prod CEC and import it into the Prod CEC.** This applies to sources, document extractors, widget settings, boosting rules, facet configuration — all of it. Each change made in Non-Prod must be manually replicated in Prod. + +The Sitecore team has confirmed this migration capability is on their roadmap, but until it ships, you need a disciplined process to manage it. + +### A Workable Promotion Workflow + +The approach that works best in practice combines source control discipline for extractor code with a structured release note process for everything else. + +**1. Version control your document extractor scripts** + +Even though extractor scripts don't run as part of your code deployment, keep them in your repository. Create a folder structure like: + +``` +/search-config + /extractors + site-a.js + site-b.js + /widgets + preview-search-config.md + search-results-config.md + CHANGELOG.md +``` + +When you modify an extractor in the Non-Prod CEC, also update the file in source control and commit it with a meaningful message. This gives you a versioned history and makes it obvious what needs to be replicated to Prod. + +**2. Maintain a Search Config CHANGELOG** + +For changes that can't be captured in code (widget facet settings, boosting rules, sort configuration), maintain a `CHANGELOG.md` in your `/search-config` folder: + +```markdown +## [Unreleased — Pending Prod Promotion] + +### Non-Prod CEC Changes +- Added `category` facet to SearchResults widget +- Boosted documents with type=article by 1.5x +- Updated site-a extractor to extract `author` field + +## [2025-03-15] — Promoted to Prod +- Added `type` facet to SearchResults widget +- Updated sitemap trigger URL for Site B +``` + +Tie this to your sprint/release process. Before any production deployment, the promotion checklist includes: apply all `[Unreleased]` Search config changes to the Prod CEC, then move them to a dated `Promoted to Prod` entry. + +**3. Use the CEC API Explorer to validate parity** + +The CEC's **Developer Resources → API Explorer** lets you fire search queries against both your Non-Prod and Prod instances and compare responses. After promoting changes, use this to verify your results, facets, and suggestions match expectations before signing off. + +**4. Synchronise crawls to your deployment timeline** + +When you deploy a significant change to content structure (new page types, new meta attributes, changed URL patterns), trigger a manual re-crawl in the CEC after deployment. Don't rely on the scheduled crawl to pick it up — especially for Prod. Navigate to **Sources → [Your Source] → Scan Now** after each significant content or structure deployment. + +### Non-Prod Source Targets and Environment Isolation + +One practical problem with sharing a single non-prod CEC across dev, QA, and UAT is that the Search sources point at specific URLs. If your dev environment is at `dev.site-a.com`, your CEC non-prod source crawls that URL. QA at `qa.site-a.com` is not crawled. + +A few options for managing this: + +**Option A — Accept it.** Dev gets the most up-to-date non-prod index (since its URL is the crawl target). QA and UAT use the same non-prod API key and index, which is "good enough" for testing the integration even if it's not crawling QA-specific content. + +**Option B — Multiple sources in one non-prod CEC.** Create a source per environment URL (`dev.site-a.com`, `qa.site-a.com`). All index into the same non-prod domain. Use an additional `env_name` attribute in your extractor to tag documents by source environment, and filter by it in your non-prod head deployments. + +**Option C — Treat non-prod Search as best-effort.** Accept that Search integration testing is done primarily against dev, and use QA/UAT for testing the UI integration (does the component render, do results appear) rather than validating the indexed content quality. Reserve content quality validation for production after a controlled go-live. + +Most teams end up with a mix of A and C in practice — and that's fine, as long as the team understands the limitation and doesn't treat non-prod search results as representative of what prod will show. + +--- + +## Wrapping Up + +Sitecore Search is a genuinely capable product and integrates well with a JSS/Next.js head, but it rewards teams who plan their CEC configuration carefully and establish a disciplined promotion process early. The key things to take away: + +- **Set up your attribute schema before you start indexing** — you can't change feature flags on existing attributes. +- **Use a shared non-prod CEC instance across all non-prod environments**, scoped by environment variable in your Vercel deployments. +- **Filter results by `site_name` at query time** — this is what keeps multi-site search clean. +- **Keep your document extractor scripts in source control** — they're your most important Search configuration artefact and the CEC offers no versioning. +- **Manually maintain a promotion changelog** until Sitecore ships native CEC config export/import. Be disciplined about it and tie it to your release process. +- **Measure the bundle impact of `@sitecore-search/ui`** before committing to it in production — consider using the SDK hooks with your own Tailwind components instead. + +The next natural step from here is wiring Sitecore Search analytics into Sitecore Personalize — the click and conversion events tracked by Search become audience signals you can act on in Personalize. That's a topic for a follow-up post. From c14878df67f047df72d2f75fb77aa42433f5384b Mon Sep 17 00:00:00 2001 From: Dave Goosem <davidgoosem@aceik.com.au> Date: Tue, 14 Apr 2026 08:23:49 +1000 Subject: [PATCH 4/8] fix long name/url --- ...rch-sdk-into-your-jss-nextjs-solution.mdx} | 150 +++++++++--------- 1 file changed, 76 insertions(+), 74 deletions(-) rename data/blog/{sitecore-search-end-to-end-integrating-the-search-sdk-into-your-jss-nextjs-solution.mdx => integrating-the-search-sdk-into-your-jss-nextjs-solution.mdx} (90%) diff --git a/data/blog/sitecore-search-end-to-end-integrating-the-search-sdk-into-your-jss-nextjs-solution.mdx b/data/blog/integrating-the-search-sdk-into-your-jss-nextjs-solution.mdx similarity index 90% rename from data/blog/sitecore-search-end-to-end-integrating-the-search-sdk-into-your-jss-nextjs-solution.mdx rename to data/blog/integrating-the-search-sdk-into-your-jss-nextjs-solution.mdx index c40e118..9df8928 100644 --- a/data/blog/sitecore-search-end-to-end-integrating-the-search-sdk-into-your-jss-nextjs-solution.mdx +++ b/data/blog/integrating-the-search-sdk-into-your-jss-nextjs-solution.mdx @@ -24,7 +24,7 @@ Before diving into implementation, it's worth being clear about where Sitecore S - **Sitecore Personalize** — behavioural targeting and experimentation - **Sitecore Search** — content discovery, typeahead, search results pages -Sitecore Search is independent of your CMS's GraphQL/Edge delivery. Rather than querying your content tree directly, it crawls your *live website* (or non-prod equivalent) and builds its own index. This is important to understand early — your Search index is populated from rendered web pages, not directly from Sitecore content items. That has implications for what you index, when you index it, and how your non-prod crawl targets are configured. +Sitecore Search is independent of your CMS's GraphQL/Edge delivery. Rather than querying your content tree directly, it crawls your _live website_ (or non-prod equivalent) and builds its own index. This is important to understand early — your Search index is populated from rendered web pages, not directly from Sitecore content items. That has implications for what you index, when you index it, and how your non-prod crawl targets are configured. --- @@ -60,7 +60,7 @@ The Customer Engagement Console (CEC) at `cec.sitecorecloud.io` is where all sea The CEC operates with a concept of separate domain instances — typically a **Non-Production** instance and a **Production** instance. Each is a completely separate tenant with its own sources, attributes, API keys, and widget configuration. -The licensing model for Sitecore Search typically provides one production instance and one non-production instance. The Search non-prod instance is used by *all* your non-prod environments (dev, QA, UAT) simultaneously — there is no per-environment isolation built in. Your production instance is used only by your live production environment. +The licensing model for Sitecore Search typically provides one production instance and one non-production instance. The Search non-prod instance is used by _all_ your non-prod environments (dev, QA, UAT) simultaneously — there is no per-environment isolation built in. Your production instance is used only by your live production environment. This gives you an architecture that looks like this: @@ -92,16 +92,16 @@ Before setting up sources or widgets, define your domain attributes. Think of th Sitecore ships a few default entities (Content, Product, Category, Store). For a typical JSS/content site you'll be working with the **Content** entity. Common attributes to set up: -| Attribute | Type | Features | -|---|---|---| -| `title` | String | Textual Relevance, Return in API Response | -| `description` | String | Textual Relevance, Return in API Response | -| `url` | String | Return in API Response | -| `image_url` | String | Return in API Response | -| `type` | String | Facets, Return in API Response | -| `category` | String | Facets, Suggestions, Return in API Response | -| `date` | Timestamp | Sorting, Return in API Response | -| `site_name` | String | Facets (critical for multi-site) | +| Attribute | Type | Features | +| ------------- | --------- | ------------------------------------------- | +| `title` | String | Textual Relevance, Return in API Response | +| `description` | String | Textual Relevance, Return in API Response | +| `url` | String | Return in API Response | +| `image_url` | String | Return in API Response | +| `type` | String | Facets, Return in API Response | +| `category` | String | Facets, Suggestions, Return in API Response | +| `date` | Timestamp | Sorting, Return in API Response | +| `site_name` | String | Facets (critical for multi-site) | > ⚠️ **Important:** The "Will be used in" feature settings on an attribute **cannot be changed after creation**. If you need to add Facet support to an attribute you've already created without it, you must delete it and recreate it. Plan your attribute schema carefully before you start indexing. @@ -136,24 +136,23 @@ A basic extractor for a JSS site might look like this: ```javascript function extract(request, response, document) { - const $ = document.content; + const $ = document.content // Pull from meta tags that your JSS head renders - const title = $('meta[property="og:title"]').attr('content') - || $('title').text() - || ''; + const title = $('meta[property="og:title"]').attr('content') || $('title').text() || '' - const description = $('meta[property="og:description"]').attr('content') - || $('meta[name="description"]').attr('content') - || ''; + const description = + $('meta[property="og:description"]').attr('content') || + $('meta[name="description"]').attr('content') || + '' - const imageUrl = $('meta[property="og:image"]').attr('content') || ''; + const imageUrl = $('meta[property="og:image"]').attr('content') || '' // Use a data attribute on your layout for structured type data - const type = $('body').data('page-type') || 'content'; + const type = $('body').data('page-type') || 'content' // Site name — critical for multi-site filtering - const siteName = $('body').data('site-name') || 'default'; + const siteName = $('body').data('site-name') || 'default' return { title, @@ -161,7 +160,7 @@ function extract(request, response, document) { image_url: imageUrl, type, site_name: siteName, - }; + } } ``` @@ -181,10 +180,10 @@ Then in your document extractor, check for it and return `null` to prevent index ```javascript function extract(request, response, document) { - const $ = document.content; + const $ = document.content - const exclude = $('meta[property="excludeFromSearch"]').attr('content'); - if (exclude === 'true') return null; + const exclude = $('meta[property="excludeFromSearch"]').attr('content') + if (exclude === 'true') return null // ... rest of extraction } @@ -235,7 +234,7 @@ NEXT_PUBLIC_SEARCH_API_KEY=your-non-prod-api-key # NEXT_PUBLIC_SEARCH_API_KEY=your-prod-api-key ``` -> 📝 `NEXT_PUBLIC_SEARCH_ENV` accepts specific values: `prod`, `staging`, `prodEu`, or `apse2`. It is *not* a free-text label — it maps to Sitecore's actual API endpoint regions. For Australian/APAC implementations, `apse2` is the relevant value. +> 📝 `NEXT_PUBLIC_SEARCH_ENV` accepts specific values: `prod`, `staging`, `prodEu`, or `apse2`. It is _not_ a free-text label — it maps to Sitecore's actual API endpoint regions. For Australian/APAC implementations, `apse2` is the relevant value. ### WidgetsProvider Setup @@ -244,13 +243,13 @@ The `WidgetsProvider` is the root component that initialises the Search SDK and In your JSS `Layout.tsx`: ```tsx -import { WidgetsProvider, PageController } from '@sitecore-search/react'; +import { WidgetsProvider, PageController } from '@sitecore-search/react' const searchConfig = { env: process.env.NEXT_PUBLIC_SEARCH_ENV as string, customerKey: process.env.NEXT_PUBLIC_SEARCH_CUSTOMER_KEY as string, apiKey: process.env.NEXT_PUBLIC_SEARCH_API_KEY as string, -}; +} export default function Layout({ layoutData }: LayoutProps) { return ( @@ -260,7 +259,7 @@ export default function Layout({ layoutData }: LayoutProps) { <main>{/* page content */}</main> <Footer /> </WidgetsProvider> - ); + ) } ``` @@ -272,31 +271,28 @@ Create a Sitecore rendering in the CMS and a corresponding component in your JSS ```tsx // components/search/PreviewSearch.tsx -import { usePreviewSearch, widget } from '@sitecore-search/react'; -import { PreviewSearch as PreviewSearchUI } from '@sitecore-search/ui'; -import { useRouter } from 'next/router'; +import { usePreviewSearch, widget } from '@sitecore-search/react' +import { PreviewSearch as PreviewSearchUI } from '@sitecore-search/ui' +import { useRouter } from 'next/router' interface PreviewSearchProps { - rfkId: string; // Pass this from your Sitecore rendering parameters + rfkId: string // Pass this from your Sitecore rendering parameters } const PreviewSearchComponent = ({ rfkId }: PreviewSearchProps) => { - const router = useRouter(); + const router = useRouter() const { widgetRef, actions: { onItemClick, onKeyphraseChange }, queryResult: { data: { suggestion: { title_context_aware: suggestions = [] } = {} } = {} }, } = usePreviewSearch({ - query: (query) => - query - .getRequest() - .setSearchQueryHighlightFragmentSize(100), - }); + query: (query) => query.getRequest().setSearchQueryHighlightFragmentSize(100), + }) const handleSubmit = (value: string) => { - router.push(`/search?q=${encodeURIComponent(value)}`); - }; + router.push(`/search?q=${encodeURIComponent(value)}`) + } return ( <PreviewSearchUI.Root ref={widgetRef}> @@ -317,11 +313,15 @@ const PreviewSearchComponent = ({ rfkId }: PreviewSearchProps) => { </PreviewSearchUI.Suggestions> )} </PreviewSearchUI.Root> - ); -}; + ) +} // Widget wrapping connects this component to the CEC widget configuration -export const PreviewSearch = widget(PreviewSearchComponent, PreviewSearchUI.Default, 'preview-search'); +export const PreviewSearch = widget( + PreviewSearchComponent, + PreviewSearchUI.Default, + 'preview-search' +) ``` > 💡 The `rfk_id` you pass here must match the ID assigned to your widget in the CEC. For multi-site, you can either create separate widgets per site in the CEC and pass different `rfk_id` values per site, or use one shared widget and filter results using a source filter at query time. @@ -332,34 +332,30 @@ Create a `/search` page in your Next.js app and a Search Results component: ```tsx // components/search/SearchResults.tsx -import { useSearchResults, widget, FilterEqual } from '@sitecore-search/react'; -import { SearchResults as SearchResultsUI, Pagination, FacetList } from '@sitecore-search/ui'; -import { useRouter } from 'next/router'; +import { useSearchResults, widget, FilterEqual } from '@sitecore-search/react' +import { SearchResults as SearchResultsUI, Pagination, FacetList } from '@sitecore-search/ui' +import { useRouter } from 'next/router' interface SearchResultItem { - id: string; - title: string; - description: string; - url: string; - image_url?: string; - type?: string; - site_name?: string; + id: string + title: string + description: string + url: string + image_url?: string + type?: string + site_name?: string } const SearchResultsComponent = () => { - const router = useRouter(); - const keyphrase = (router.query.q as string) || ''; + const router = useRouter() + const keyphrase = (router.query.q as string) || '' const { widgetRef, actions: { onResultsPerPageChange, onPageNumberChange, onFacetClick }, state: { page, itemsPerPage }, queryResult: { - data: { - total_item: totalItems = 0, - facet: facets = [], - content: results = [], - } = {}, + data: { total_item: totalItems = 0, facet: facets = [], content: results = [] } = {}, }, } = useSearchResults<SearchResultItem>({ query: (query) => { @@ -367,23 +363,23 @@ const SearchResultsComponent = () => { .getRequest() .setSearchQueryKeyphrase(keyphrase) // Multi-site: filter to only show results from this site - .addSearchQueryFilter(new FilterEqual('site_name', process.env.NEXT_PUBLIC_SITE_NAME as string)); + .addSearchQueryFilter( + new FilterEqual('site_name', process.env.NEXT_PUBLIC_SITE_NAME as string) + ) }, - }); + }) return ( <div ref={widgetRef}> - <p>{totalItems} results for "{keyphrase}"</p> + <p> + {totalItems} results for "{keyphrase}" + </p> <div className="search-layout"> {/* Facets sidebar */} <aside> {facets.map((facet) => ( - <FacetList - key={facet.name} - facet={facet} - onFacetClick={onFacetClick} - /> + <FacetList key={facet.name} facet={facet} onFacetClick={onFacetClick} /> ))} </aside> @@ -392,7 +388,9 @@ const SearchResultsComponent = () => { {results.map((result) => ( <article key={result.id}> {result.image_url && <img src={result.image_url} alt={result.title} />} - <h3><a href={result.url}>{result.title}</a></h3> + <h3> + <a href={result.url}>{result.title}</a> + </h3> <p>{result.description}</p> </article> ))} @@ -406,14 +404,14 @@ const SearchResultsComponent = () => { </main> </div> </div> - ); -}; + ) +} export const SearchResults = widget( SearchResultsComponent, SearchResultsUI.Default, 'search-results' -); +) ``` Add `NEXT_PUBLIC_SITE_NAME` to your Vercel environment variables, set per-site/per-head deployment. This is the filter that scopes search results to the current site — essential in a multi-site setup where all sites share the same non-prod Search instance. @@ -441,10 +439,12 @@ If you're following a proper multi-site setup (as covered in previous posts), yo In the CEC, create a source for each site: **Non-Prod CEC sources:** + - `Site A — Non-Prod` → crawls `dev.site-a.com` - `Site B — Non-Prod` → crawls `dev.site-b.com` **Prod CEC sources:** + - `Site A — Prod` → crawls `site-a.com` - `Site B — Prod` → crawls `site-b.com` @@ -522,11 +522,13 @@ For changes that can't be captured in code (widget facet settings, boosting rule ## [Unreleased — Pending Prod Promotion] ### Non-Prod CEC Changes + - Added `category` facet to SearchResults widget - Boosted documents with type=article by 1.5x - Updated site-a extractor to extract `author` field ## [2025-03-15] — Promoted to Prod + - Added `type` facet to SearchResults widget - Updated sitemap trigger URL for Site B ``` From 2bd024b8d481b39a2e65572daf48c7e27bd5ee53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:24:11 +0000 Subject: [PATCH 5/8] Bump the npm_and_yarn group across 1 directory with 7 updates Bumps the npm_and_yarn group with 7 updates in the / directory: | Package | From | To | | --- | --- | --- | | [next](https://github.com/vercel/next.js) | `15.5.10` | `15.5.15` | | [brace-expansion](https://github.com/juliangruber/brace-expansion) | `1.1.12` | `1.1.14` | | [minimatch](https://github.com/isaacs/minimatch) | `3.1.2` | `3.1.5` | | [picomatch](https://github.com/micromatch/picomatch) | `2.3.1` | `2.3.2` | | [picomatch](https://github.com/micromatch/picomatch) | `4.0.3` | `4.0.4` | | [flatted](https://github.com/WebReflection/flatted) | `3.3.3` | `3.4.2` | | [svgo](https://github.com/svg/svgo) | `3.3.2` | `3.3.3` | | [yaml](https://github.com/eemeli/yaml) | `2.8.2` | `2.8.3` | Updates `next` from 15.5.10 to 15.5.15 - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/compare/v15.5.10...v15.5.15) Updates `brace-expansion` from 1.1.12 to 1.1.14 - [Release notes](https://github.com/juliangruber/brace-expansion/releases) - [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.14) Updates `minimatch` from 3.1.2 to 3.1.5 - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5) Updates `picomatch` from 2.3.1 to 2.3.2 - [Release notes](https://github.com/micromatch/picomatch/releases) - [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2) Updates `picomatch` from 4.0.3 to 4.0.4 - [Release notes](https://github.com/micromatch/picomatch/releases) - [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2) Updates `flatted` from 3.3.3 to 3.4.2 - [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2) Updates `svgo` from 3.3.2 to 3.3.3 - [Release notes](https://github.com/svg/svgo/releases) - [Commits](https://github.com/svg/svgo/compare/v3.3.2...v3.3.3) Updates `yaml` from 2.8.2 to 2.8.3 - [Release notes](https://github.com/eemeli/yaml/releases) - [Commits](https://github.com/eemeli/yaml/compare/v2.8.2...v2.8.3) --- updated-dependencies: - dependency-name: next dependency-version: 15.5.15 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: brace-expansion dependency-version: 1.1.14 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: minimatch dependency-version: 3.1.5 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: picomatch dependency-version: 2.3.2 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: picomatch dependency-version: 4.0.4 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: flatted dependency-version: 3.4.2 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: svgo dependency-version: 3.3.3 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: yaml dependency-version: 2.8.3 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 156 +++++++++++++++++++++++++++------------------- package.json | 2 +- 2 files changed, 94 insertions(+), 64 deletions(-) diff --git a/package-lock.json b/package-lock.json index f8b9b25..9083663 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "github-slugger": "^2.0.0", "gray-matter": "^4.0.2", "image-size": "1.0.0", - "next": "15.5.10", + "next": "15.5.15", "next-contentlayer2": "0.5.8", "next-themes": "^0.3.0", "pliny": "0.4.1", @@ -3657,13 +3657,15 @@ } }, "node_modules/@next/env": { - "version": "15.5.10", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.15.tgz", + "integrity": "sha512-vcmyu5/MyFzN7CdqRHO3uHO44p/QPCZkuTUXroeUmhNP8bL5PHFEhik22JUazt+CDDoD6EpBYRCaS2pISL+/hg==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz", - "integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.15.tgz", + "integrity": "sha512-6PvFO2Tzt10GFK2Ro9tAVEtacMqRmTarYMFKAnV2vYMdwWc73xzmDQyAV7SwEdMhzmiRoo7+m88DuiXlJlGeaw==", "cpu": [ "arm64" ], @@ -3677,9 +3679,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz", - "integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.15.tgz", + "integrity": "sha512-G+YNV+z6FDZTp/+IdGyIMFqalBTaQSnvAA+X/hrt+eaTRFSznRMz9K7rTmzvM6tDmKegNtyzgufZW0HwVzEqaQ==", "cpu": [ "x64" ], @@ -3693,9 +3695,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz", - "integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.15.tgz", + "integrity": "sha512-eVkrMcVIBqGfXB+QUC7jjZ94Z6uX/dNStbQFabewAnk13Uy18Igd1YZ/GtPRzdhtm7QwC0e6o7zOQecul4iC1w==", "cpu": [ "arm64" ], @@ -3709,9 +3711,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz", - "integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.15.tgz", + "integrity": "sha512-RwSHKMQ7InLy5GfkY2/n5PcFycKA08qI1VST78n09nN36nUPqCvGSMiLXlfUmzmpQpF6XeBYP2KRWHi0UW3uNg==", "cpu": [ "arm64" ], @@ -3725,9 +3727,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz", - "integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.15.tgz", + "integrity": "sha512-nplqvY86LakS+eeiuWsNWvfmK8pFcOEW7ZtVRt4QH70lL+0x6LG/m1OpJ/tvrbwjmR8HH9/fH2jzW1GlL03TIg==", "cpu": [ "x64" ], @@ -3741,9 +3743,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz", - "integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.15.tgz", + "integrity": "sha512-eAgl9NKQ84/sww0v81DQINl/vL2IBxD7sMybd0cWRw6wqgouVI53brVRBrggqBRP/NWeIAE1dm5cbKYoiMlqDQ==", "cpu": [ "x64" ], @@ -3757,9 +3759,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz", - "integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.15.tgz", + "integrity": "sha512-GJVZC86lzSquh0MtvZT+L7G8+jMnJcldloOjA8Kf3wXvBrvb6OGe2MzPuALxFshSm/IpwUtD2mIoof39ymf52A==", "cpu": [ "arm64" ], @@ -3773,7 +3775,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.7", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.15.tgz", + "integrity": "sha512-nFucjVdwlFqxh/JG3hWSJ4p8+YJV7Ii8aPDuBQULB6DzUF4UNZETXLfEUk+oI2zEznWWULPt7MeuTE6xtK1HSA==", "cpu": [ "x64" ], @@ -3867,7 +3871,9 @@ } }, "node_modules/@npmcli/map-workspaces/node_modules/brace-expansion": { - "version": "2.0.2", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -3875,11 +3881,13 @@ } }, "node_modules/@npmcli/map-workspaces/node_modules/minimatch": { - "version": "9.0.5", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -4579,14 +4587,6 @@ "node": ">=4" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@types/concat-stream": { "version": "2.0.3", "dev": true, @@ -4862,7 +4862,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -4870,11 +4872,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -5078,7 +5082,9 @@ } }, "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -5454,7 +5460,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.12", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -7414,7 +7422,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -7685,7 +7695,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -7693,11 +7705,13 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -10492,7 +10506,9 @@ } }, "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -10547,7 +10563,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -10661,10 +10679,12 @@ } }, "node_modules/next": { - "version": "15.5.10", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.15.tgz", + "integrity": "sha512-VSqCrJwtLVGwAVE0Sb/yikrQfkwkZW9p+lL/J4+xe+G3ZA+QnWPqgcfH1tDUEuk9y+pthzzVFp4L/U8JerMfMQ==", "license": "MIT", "dependencies": { - "@next/env": "15.5.10", + "@next/env": "15.5.15", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -10677,14 +10697,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.7", - "@next/swc-darwin-x64": "15.5.7", - "@next/swc-linux-arm64-gnu": "15.5.7", - "@next/swc-linux-arm64-musl": "15.5.7", - "@next/swc-linux-x64-gnu": "15.5.7", - "@next/swc-linux-x64-musl": "15.5.7", - "@next/swc-win32-arm64-msvc": "15.5.7", - "@next/swc-win32-x64-msvc": "15.5.7", + "@next/swc-darwin-arm64": "15.5.15", + "@next/swc-darwin-x64": "15.5.15", + "@next/swc-linux-arm64-gnu": "15.5.15", + "@next/swc-linux-arm64-musl": "15.5.15", + "@next/swc-linux-x64-gnu": "15.5.15", + "@next/swc-linux-x64-musl": "15.5.15", + "@next/swc-win32-arm64-msvc": "15.5.15", + "@next/swc-win32-x64-msvc": "15.5.15", "sharp": "^0.34.3" }, "peerDependencies": { @@ -11245,7 +11265,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -11791,7 +11813,9 @@ } }, "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -12748,7 +12772,9 @@ "license": "MIT" }, "node_modules/sax": { - "version": "1.4.4", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -13473,17 +13499,19 @@ "license": "MIT" }, "node_modules/svgo": { - "version": "3.3.2", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", + "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", "dev": true, "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", - "picocolors": "^1.0.0" + "picocolors": "^1.0.0", + "sax": "^1.5.0" }, "bin": { "svgo": "bin/svgo" @@ -14599,7 +14627,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.2", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 1400193..233791a 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "github-slugger": "^2.0.0", "gray-matter": "^4.0.2", "image-size": "1.0.0", - "next": "15.5.10", + "next": "15.5.15", "next-contentlayer2": "0.5.8", "next-themes": "^0.3.0", "pliny": "0.4.1", From 8be97b96180c0e12a9241c5bf2afd7eb10568828 Mon Sep 17 00:00:00 2001 From: Dave Goosem <davidgoosem@aceik.com.au> Date: Tue, 14 Apr 2026 08:25:24 +1000 Subject: [PATCH 6/8] meta data updates --- app/tag-data.json | 40 ++++++++++++++++++- ...arch-sdk-into-your-jss-nextjs-solution.mdx | 4 +- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/app/tag-data.json b/app/tag-data.json index 6019344..f8a2961 100644 --- a/app/tag-data.json +++ b/app/tag-data.json @@ -1 +1,39 @@ -{"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} \ No newline at end of file +{ + "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, + "headless-cms": 1, + "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, + "sxa": 4, + "seo": 1, + "devops": 2, + "searchstax": 2, + "solr": 6, + "helix": 3, + "accessibility": 1, + "aspnet": 1, + "aws": 1 +} diff --git a/data/blog/integrating-the-search-sdk-into-your-jss-nextjs-solution.mdx b/data/blog/integrating-the-search-sdk-into-your-jss-nextjs-solution.mdx index 9df8928..b910540 100644 --- a/data/blog/integrating-the-search-sdk-into-your-jss-nextjs-solution.mdx +++ b/data/blog/integrating-the-search-sdk-into-your-jss-nextjs-solution.mdx @@ -1,8 +1,8 @@ --- title: 'Sitecore Search End-to-End: Integrating the Search SDK into Your JSS Next.js Headless Solution' date: '2026-04-13' -tags: [] -draft: true +tags: [Sitecore, Search, Sitecore Search] +draft: false summary: 'A comprehensive guide to integrating Sitecore Search into a JSS Next.js headless solution — covering CEC setup, SDK options, front-end integration, multi-site source scoping, and a disciplined environment promotion workflow.' layout: DaveLayout images: [] From 04189ac6819f40e8603ebd6c462bb4186ce12a85 Mon Sep 17 00:00:00 2001 From: Dave Goosem <davidgoosem@aceik.com.au> Date: Tue, 14 Apr 2026 10:43:29 +1000 Subject: [PATCH 7/8] 2026 mvp badge added --- layouts/DaveLayout.tsx | 8 ++++++++ .../Logos/2026-Sitecore_MVP_Technology.png | Bin 0 -> 27425 bytes 2 files changed, 8 insertions(+) create mode 100644 public/static/images/Logos/2026-Sitecore_MVP_Technology.png diff --git a/layouts/DaveLayout.tsx b/layouts/DaveLayout.tsx index 86c8a54..73a6fea 100644 --- a/layouts/DaveLayout.tsx +++ b/layouts/DaveLayout.tsx @@ -145,6 +145,14 @@ export default function PostLayout({ content, authorDetails, next, prev, childre </div> <footer> <div className="flex flex-wrap"> + <div className="w-1/4 pl-1 pt-1 sm:w-1/4 md:w-1/4 xl:w-1/2"> + <Image + src={'/static/images/Logos/2026-Sitecore_MVP_Technology.png'} + alt="2026 Sitecore Technology MVP" + width={500} + height={500} + /> + </div> <div className="w-1/4 pl-1 pt-1 sm:w-1/4 md:w-1/4 xl:w-1/2"> <Image src={'/static/images/Logos/2025-Sitecore-MVP-Technology.png'} diff --git a/public/static/images/Logos/2026-Sitecore_MVP_Technology.png b/public/static/images/Logos/2026-Sitecore_MVP_Technology.png new file mode 100644 index 0000000000000000000000000000000000000000..a5e592fc74423f91afd41319e1c937adf650bb8a GIT binary patch literal 27425 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ðL7K-+M@=VQ!i$#8 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 literal 0 HcmV?d00001 From 8d5b49d3cea17e12cd6b11a969236cfdf12a9f70 Mon Sep 17 00:00:00 2001 From: Dave Goosem <davidgoosem@aceik.com.au> Date: Tue, 14 Apr 2026 10:44:21 +1000 Subject: [PATCH 8/8] SEO/AEO updates along with some Claude config/context file tweaks --- .claude/commands/new-post.md | 2 + CLAUDE.md | 59 ++++++++++++++----- app/CLAUDE.md | 46 +++++++++++---- app/blog/[...slug]/page.tsx | 23 ++++++++ app/layout.tsx | 25 ++++++++ app/tag-data.json | 52 ++++++++-------- contentlayer.config.ts | 13 ++++ data/blog/CLAUDE.md | 58 +++++++++++++----- data/blog/federated-search.mdx | 2 +- .../indexing-external-data-with-sitecore.mdx | 2 +- ...ore-custom-facets-returns-single-words.mdx | 2 +- layouts/CLAUDE.md | 28 +++++++++ 12 files changed, 239 insertions(+), 73 deletions(-) create mode 100644 layouts/CLAUDE.md diff --git a/.claude/commands/new-post.md b/.claude/commands/new-post.md index d4cc1ad..b4f4382 100644 --- a/.claude/commands/new-post.md +++ b/.claude/commands/new-post.md @@ -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`. diff --git a/CLAUDE.md b/CLAUDE.md index 25f6fc4..28c1cda 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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) @@ -11,6 +12,7 @@ - **Analytics**: Google Analytics via Pliny ## Key Commands + ```bash yarn dev # start dev server yarn build # production build + postbuild (RSS, search index) @@ -18,29 +20,54 @@ 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. diff --git a/app/CLAUDE.md b/app/CLAUDE.md index 055345e..d443274 100644 --- a/app/CLAUDE.md +++ b/app/CLAUDE.md @@ -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. diff --git a/app/blog/[...slug]/page.tsx b/app/blog/[...slug]/page.tsx index cfc29ed..711541c 100644 --- a/app/blog/[...slug]/page.tsx +++ b/app/blog/[...slug]/page.tsx @@ -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, @@ -74,6 +75,9 @@ export async function generateMetadata({ description: post.summary, images: imageList, }, + alternates: { + canonical: canonicalUrl, + }, } } @@ -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 ( @@ -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> diff --git a/app/layout.tsx b/app/layout.tsx index f6092ae..df74e6b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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 @@ -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 /> diff --git a/app/tag-data.json b/app/tag-data.json index f8a2961..06fa45e 100644 --- a/app/tag-data.json +++ b/app/tag-data.json @@ -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 } diff --git a/contentlayer.config.ts b/contentlayer.config.ts index 4ea401c..be36895 100644 --- a/contentlayer.config.ts +++ b/contentlayer.config.ts @@ -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`, + }, + }, }), }, }, @@ -132,6 +144,7 @@ export const Authors = defineDocumentType(() => ({ export default makeSource({ contentDirPath: 'data', + contentDirExclude: ['blog/CLAUDE.md'], documentTypes: [Blog, Authors], mdx: { cwd: process.cwd(), diff --git a/data/blog/CLAUDE.md b/data/blog/CLAUDE.md index a50f111..d3698b3 100644 --- a/data/blog/CLAUDE.md +++ b/data/blog/CLAUDE.md @@ -1,45 +1,71 @@ # Blog Posts — Claude Code Context ## Creating a New Post + - File name: `kebab-case-title.mdx` in this directory (`data/blog/`) - Contentlayer picks up all `*.mdx` files here automatically — no registration needed ## Frontmatter Schema + ```yaml --- title: 'Your Post Title' date: 'YYYY-MM-DD' +lastmod: 'YYYY-MM-DD' tags: ['XMCloud', 'Sitecore', 'Next.js'] -draft: false +draft: true summary: 'One-paragraph summary displayed on listing pages and in RSS.' layout: DaveLayout -images: [] +images: ['/static/images/posts/YYYY/post-slug-name/og-image.png'] authors: ['default'] +canonicalUrl: '' --- ``` ### Field notes -| Field | Required | Notes | -|-------|----------|-------| -| `title` | yes | Displayed in `<h1>` and `<title>` | -| `date` | yes | ISO format `'YYYY-MM-DD'` | -| `tags` | yes | Array of strings; capitalise consistently (e.g. `'XMCloud'` not `'xmcloud'`) | -| `draft` | yes | `false` to publish, `true` to hide from listings | -| `summary` | yes | Also used for OG description | -| `layout` | no | Default: `DaveLayout`. Options: `DaveLayout`, `PostLayout`, `PostSimple`, `PostBanner` | -| `images` | no | Array of image paths or `[]`. First image used as OG image if provided | -| `authors` | no | Defaults to `['default']` (David Goosem) | + +| Field | Required | Notes | +| -------------- | -------- | ------------------------------------------------------------------------------------- | +| `title` | yes | Displayed in `<h1>` and `<title>` | +| `date` | yes | ISO format `'YYYY-MM-DD'` — original publish date, never change it | +| `lastmod` | no | ISO format — set when substantially updating a post; drives `dateModified` in JSON-LD | +| `tags` | yes | Array of strings; see tag list below — casing matters | +| `draft` | yes | `true` while writing, `false` to publish | +| `summary` | yes | Also used for OG description and RSS — write a complete sentence | +| `layout` | no | Default: `DaveLayout` — see `layouts/CLAUDE.md` for differences | +| `images` | no | **Must be non-empty before publishing.** First entry is the OG/social share image | +| `authors` | no | Defaults to `['default']` (David Goosem) | +| `canonicalUrl` | no | Only set if this post was originally published elsewhere | ## Images -Store post images at: + +Store images at: + ``` -public/static/images/posts/YYYY/post-slug-name/image1.png +public/static/images/posts/YYYY/post-slug-name/image.png ``` -Reference in MDX as: + +Reference in MDX body as: + ```md -![Alt text](/static/images/posts/YYYY/post-slug-name/image1.png 'Optional title') +![Alt text](/static/images/posts/YYYY/post-slug-name/image.png 'Optional title') +``` + +**The `images:` frontmatter field must be non-empty before setting `draft: false`.** It drives the OpenGraph/social share image — empty means the site falls back to the generic banner. Ideal OG dimensions: 1200×630px. + +If no purpose-built banner exists, promote the first meaningful body image: + +```yaml +images: ['/static/images/posts/YYYY/post-slug-name/image.png'] ``` +## Contentlayer Gotcha + +Contentlayer scans the entire `data/` directory. Any `.md` file here (including instruction files like this one) must be listed in `contentDirExclude` in `contentlayer.config.ts`, otherwise Contentlayer logs a warning and skips it. The current exclude list is at the top of `makeSource()`. + ## Common Tags (existing — keep casing consistent) + `XMCloud`, `Sitecore`, `JSS`, `SXA`, `SaaS`, `Next.js`, `Vercel`, `Azure`, `AWS`, `CI/CD`, `Accessibility`, `SEO`, `Search`, `Solr`, `Architecture`, `Helix` + +> ⚠️ **Tag casing is enforced by display** — use exactly the capitalisation above. In particular: `Solr` (not `SOLR`), `Next.js` (not `NextJS`), `XMCloud` (no space or hyphen). diff --git a/data/blog/federated-search.mdx b/data/blog/federated-search.mdx index 1394722..79fe389 100644 --- a/data/blog/federated-search.mdx +++ b/data/blog/federated-search.mdx @@ -1,7 +1,7 @@ --- title: Federated Search with External Data and Sitecore date: '2020-08-18' -tags: ['Sitecore', 'Helix', 'SOLR'] +tags: ['Sitecore', 'Helix', 'Solr'] draft: false summary: In this article we look at how you might develop a Federated Search with data using both Sitecore Content and numerous other external data points. layout: DaveLayout diff --git a/data/blog/indexing-external-data-with-sitecore.mdx b/data/blog/indexing-external-data-with-sitecore.mdx index a12d424..aff7467 100644 --- a/data/blog/indexing-external-data-with-sitecore.mdx +++ b/data/blog/indexing-external-data-with-sitecore.mdx @@ -1,7 +1,7 @@ --- title: Indexing External Data for use with Sitecore date: '2020-07-16' -tags: ['Sitecore', 'SOLR'] +tags: ['Sitecore', 'Solr'] draft: false summary: In this article we'll take a look at a realtively simple way in which you can utilise Sitecore to look after managing the indexing of external data for you using Solr as the Search Provider. layout: DaveLayout diff --git a/data/blog/sitecore-custom-facets-returns-single-words.mdx b/data/blog/sitecore-custom-facets-returns-single-words.mdx index 92fec1d..574cb81 100644 --- a/data/blog/sitecore-custom-facets-returns-single-words.mdx +++ b/data/blog/sitecore-custom-facets-returns-single-words.mdx @@ -1,7 +1,7 @@ --- title: Sitecore Custom Facets with 'text' field type returns single words from Solr date: '2020-08-25' -tags: ['Sitecore', 'SOLR'] +tags: ['Sitecore', 'Solr'] draft: false summary: This article describes an issue you may encounter when you are trying to use the faceting capability of Solr. It touches on the way you can use the Sitecore Search API to generate Solr queries which contain facets using Sitecore's FacetOn() LINQ to Sitecore capability. layout: DaveLayout diff --git a/layouts/CLAUDE.md b/layouts/CLAUDE.md new file mode 100644 index 0000000..06a863d --- /dev/null +++ b/layouts/CLAUDE.md @@ -0,0 +1,28 @@ +# Layouts — Claude Code Context + +## Post Layouts (used in blog post frontmatter) + +| Layout | Comments | Hero image | Author sidebar | Prev/Next | Use when | +| ------------ | ----------- | ---------------------- | -------------- | --------- | ----------------------------- | +| `DaveLayout` | Off | Yes — from `images[0]` | Yes | Yes | **Default for all new posts** | +| `PostLayout` | On (Giscus) | Yes — from `images[0]` | Yes | Yes | Want Giscus comments enabled | +| `PostBanner` | On (Giscus) | Full-bleed banner | No | No | Feature/visual-heavy posts | +| `PostSimple` | On (Giscus) | No | No | No | Minimal/short-form posts | + +`DaveLayout` is the default set in `app/blog/[...slug]/page.tsx`. Giscus is configured via env vars — see `data/siteMetadata.js`. + +## System Layouts (not for posts) + +| Layout | Used by | +| -------------------- | -------------------------------------------- | +| `ListLayout` | `app/blog/page.tsx` — paginated post listing | +| `ListLayoutWithTags` | `app/tags/[tag]/page.tsx` — filtered by tag | +| `AuthorLayout` | `app/about/page.tsx` — author profile | + +Do not reference these in blog post frontmatter. + +## Adding a New Layout + +1. Create `layouts/YourLayout.tsx` — follow the `LayoutProps` interface pattern from an existing layout +2. Register it in the `layouts` map in `app/blog/[...slug]/page.tsx` +3. It becomes available as a `layout:` value in post frontmatter