From b3339d9acd257d3c69e3815c7336549ef0d2f86e Mon Sep 17 00:00:00 2001
From: Jordan Winters
Date: Tue, 21 Jul 2026 17:19:23 -0500
Subject: [PATCH 1/8] feat(seo): move career coaching to
/services/career-coaching with redirect stub
Nests the fourth service under /services/* and takes the searched slug
('career coaching'; 'guidance' is a zero-volume phrase). Safe to move
now: months of 404 plus the homepage-canonical bug left the old URL with
minimal equity. Retitled for 'tech career coach minneapolis', local
hero sentence + Twin Cities FAQ, discovery-call CTAs to /contact,
Service schema + breadcrumb, first-person voice in metadata.
New reusable static-redirect pattern (GitHub Pages cannot 301):
lib/redirects.ts registry + RedirectPage stub emitting an instant meta
refresh with canonical pointing at the destination. The old URL keeps a
stub that is never sitemapped or internally linked; the metadata-policy
test now computes each page's route, requires canonical === route for
real pages, and requires stubs to canonicalize to their registered
destination. e2e verifies the browser lands on the new page.
Co-Authored-By: Claude Fable 5
---
.../career-guidance/opengraph-image.tsx | 10 -
app/(main)/career-guidance/page.test.tsx | 129 +-------
app/(main)/career-guidance/page.tsx | 249 +--------------
app/(main)/page.tsx | 2 +-
.../career-coaching/opengraph-image.tsx | 10 +
.../services/career-coaching/page.test.tsx | 145 +++++++++
app/(main)/services/career-coaching/page.tsx | 288 ++++++++++++++++++
app/components/redirect-page.tsx | 27 ++
app/config/nav.ts | 2 +-
app/metadata-policy.test.ts | 35 ++-
app/sitemap.test.ts | 5 +-
app/sitemap.ts | 2 +-
e2e/seo.spec.ts | 13 +-
lib/redirects.ts | 30 ++
.../next-image-export-optimizer-hashes.json | 6 +-
15 files changed, 570 insertions(+), 383 deletions(-)
delete mode 100644 app/(main)/career-guidance/opengraph-image.tsx
create mode 100644 app/(main)/services/career-coaching/opengraph-image.tsx
create mode 100644 app/(main)/services/career-coaching/page.test.tsx
create mode 100644 app/(main)/services/career-coaching/page.tsx
create mode 100644 app/components/redirect-page.tsx
create mode 100644 lib/redirects.ts
diff --git a/app/(main)/career-guidance/opengraph-image.tsx b/app/(main)/career-guidance/opengraph-image.tsx
deleted file mode 100644
index e35091a..0000000
--- a/app/(main)/career-guidance/opengraph-image.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { OG_SIZE, ogCard } from '../../../lib/og-image'
-
-export const dynamic = 'force-static'
-export const alt = 'Career Guidance'
-export const size = OG_SIZE
-export const contentType = 'image/png'
-
-export default function OpengraphImage() {
- return ogCard('Career Guidance', 'Bidwell Consulting')
-}
diff --git a/app/(main)/career-guidance/page.test.tsx b/app/(main)/career-guidance/page.test.tsx
index 315718f..71041df 100644
--- a/app/(main)/career-guidance/page.test.tsx
+++ b/app/(main)/career-guidance/page.test.tsx
@@ -1,124 +1,27 @@
-import { axeTest, render, screen } from 'lib/test-utils'
+import { render, screen } from 'lib/test-utils'
import { describe, expect, it } from 'vitest'
-import CareerGuidancePage from './page'
+import CareerGuidanceRedirect from './page'
-const LINKEDIN_URL = 'https://www.linkedin.com/in/wintersjordan/'
+describe('career-guidance redirect stub', () => {
+ it('meta-refreshes instantly to the new URL', () => {
+ render()
-describe('Career Guidance Page', () => {
- it('renders the hero section with main heading', () => {
- render()
-
- expect(screen.getByRole('heading', { level: 1, name: /career guidance/i })).toBeInTheDocument()
- expect(screen.getByText(/career coaching/i)).toBeInTheDocument()
- })
-
- it('displays the hero call-to-action', () => {
- render()
-
- const heroCta = screen.getByRole('link', { name: /get in touch on linkedin/i })
- expect(heroCta).toHaveAttribute('href', LINKEDIN_URL)
- })
-
- it('displays all service offerings', () => {
- render()
-
- expect(screen.getByRole('heading', { name: /services offered/i })).toBeInTheDocument()
- expect(screen.getByRole('heading', { name: /1-on-1 coaching/i })).toBeInTheDocument()
- expect(screen.getByRole('heading', { name: /resume & linkedin review/i })).toBeInTheDocument()
- expect(screen.getByRole('heading', { name: /interview prep/i })).toBeInTheDocument()
- expect(screen.getByRole('heading', { name: /career transitions/i })).toBeInTheDocument()
- })
-
- it('displays service benefits', () => {
- render()
-
- expect(screen.getByText(/personalized advice/i)).toBeInTheDocument()
- expect(screen.getByText(/ats optimization/i)).toBeInTheDocument()
- expect(screen.getByText(/mock interviews/i)).toBeInTheDocument()
- expect(screen.getByText(/transition planning/i)).toBeInTheDocument()
- })
-
- it('displays engagement options with durations', () => {
- render()
-
- expect(screen.getByRole('heading', { name: /ways to work together/i })).toBeInTheDocument()
- expect(screen.getByRole('heading', { name: /discovery call/i })).toBeInTheDocument()
- expect(screen.getByRole('heading', { name: /^single session$/i })).toBeInTheDocument()
- expect(screen.getByRole('heading', { name: /ongoing mentorship/i })).toBeInTheDocument()
- expect(screen.getByText('30 minutes')).toBeInTheDocument()
- expect(screen.getByText('60 minutes')).toBeInTheDocument()
- expect(screen.getByText('3 months')).toBeInTheDocument()
- })
-
- it('renders a reach-out link for every engagement option', () => {
- render()
-
- const reachOutLinks = screen.getAllByRole('link', { name: /reach out/i })
- expect(reachOutLinks).toHaveLength(3)
- for (const link of reachOutLinks) {
- expect(link).toHaveAttribute('href', LINKEDIN_URL)
- }
+ const refresh = document.head.querySelector('meta[http-equiv="refresh"]')
+ expect(refresh?.getAttribute('content')).toBe('0;url=/services/career-coaching')
})
- it('includes FAQ section with all questions', () => {
- render()
-
- expect(screen.getByRole('heading', { name: /frequently asked questions/i })).toBeInTheDocument()
- expect(screen.getByText(/how long are sessions\?/i)).toBeInTheDocument()
- expect(screen.getByText(/not sure what you need\?/i)).toBeInTheDocument()
- expect(screen.getByText(/packages available\?/i)).toBeInTheDocument()
- expect(screen.getByText(/industry focus\?/i)).toBeInTheDocument()
- expect(screen.getByText(/remote work\?/i)).toBeInTheDocument()
- })
-
- it('renders contact section', () => {
- render()
-
- expect(screen.getByRole('heading', { name: /get in touch/i })).toBeInTheDocument()
- expect(screen.getByRole('link', { name: /connect on linkedin/i })).toBeInTheDocument()
- })
-
- it('opens all external links safely in a new tab', () => {
- render()
-
- const links = screen.getAllByRole('link')
- expect(links.length).toBeGreaterThan(0)
- for (const link of links) {
- expect(link).toHaveAttribute('href', LINKEDIN_URL)
- expect(link).toHaveAttribute('target', '_blank')
- expect(link).toHaveAttribute('rel', 'noopener noreferrer')
- }
- })
-
- it('has proper heading hierarchy', () => {
- render()
-
- // Should have exactly one h1
- const h1Elements = screen.getAllByRole('heading', { level: 1 })
- expect(h1Elements).toHaveLength(1)
-
- // One h2 per main section: services, engagement, FAQ, contact
- const h2Elements = screen.getAllByRole('heading', { level: 2 })
- expect(h2Elements).toHaveLength(4)
- })
-
- it('exposes anchor targets for deep links', () => {
- const { container } = render()
-
- expect(container.querySelector('#services')).toBeInTheDocument()
- expect(container.querySelector('#contact')).toBeInTheDocument()
- })
+ it('offers a visible link to the destination', () => {
+ render()
- it('is accessible', async () => {
- const { container } = render()
- await axeTest(container)
+ expect(screen.getByRole('heading', { level: 1, name: /moved/i })).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: /the new page/i })).toHaveAttribute(
+ 'href',
+ '/services/career-coaching'
+ )
})
- it('has proper metadata', async () => {
+ it('canonicalizes to the destination so equity consolidates there', async () => {
const { metadata } = await import('./page')
- expect(metadata.title).toBe('Career Guidance')
- expect(metadata.description).toContain('career coaching')
- expect(metadata.keywords).toContain('career coach')
- expect(metadata.keywords).toContain('career guidance')
+ expect(metadata.alternates?.canonical).toBe('/services/career-coaching')
})
})
diff --git a/app/(main)/career-guidance/page.tsx b/app/(main)/career-guidance/page.tsx
index 246f79f..65fdcbc 100644
--- a/app/(main)/career-guidance/page.tsx
+++ b/app/(main)/career-guidance/page.tsx
@@ -1,247 +1,8 @@
-import { faqSchema } from 'lib/structured-data'
-import type { Metadata } from 'next'
-import type React from 'react'
-import JsonLd from '../../components/structured-data'
+import { redirectMetadata } from 'lib/redirects'
+import RedirectPage from '../../components/redirect-page'
-export const metadata: Metadata = {
- title: 'Career Guidance',
- description:
- 'We provide career coaching and guidance for tech professionals. Help with career transitions, resume reviews, interview prep, and leadership development.',
- keywords: [
- 'career coach',
- 'career guidance',
- 'career mentoring',
- 'professional development',
- 'career transition',
- 'resume optimization',
- 'interview preparation',
- 'leadership development',
- ],
- alternates: { canonical: '/career-guidance' },
- openGraph: {
- title: 'Career Guidance | Bidwell Consulting',
- description: 'We provide career coaching and guidance for tech professionals.',
- type: 'website',
- },
-}
-
-const services = [
- {
- title: '1-on-1 Coaching',
- description: 'Dedicated time to work through your career questions and challenges.',
- benefits: ['Personalized advice', 'Goal setting', 'Accountability check-ins'],
- },
- {
- title: 'Resume & LinkedIn Review',
- description: 'Honest feedback on your professional materials.',
- benefits: ['ATS optimization', 'Profile improvements', 'Positioning strategy'],
- },
- {
- title: 'Interview Prep',
- description: 'Practice interviews with real feedback.',
- benefits: ['Mock interviews', 'Behavioral coaching', 'Technical prep'],
- },
- {
- title: 'Career Transitions',
- description: 'Guidance for changing roles or industries.',
- benefits: ['Skills assessment', 'Transition planning', 'Network strategy'],
- },
-]
-
-const faqs = [
- {
- question: 'How long are sessions?',
- answer: '60 minutes. Long enough to dig in, short enough to respect your time.',
- },
- {
- question: 'Not sure what you need?',
- answer: "Start with a free 30-minute call. We'll figure it out together.",
- },
- {
- question: 'Packages available?',
- answer: 'Yes. Single sessions or ongoing engagements. Reach out to discuss.',
- },
- {
- question: 'Industry focus?',
- answer: 'I specialize in tech and software, but the principles apply broadly.',
- },
- {
- question: 'Remote work?',
- answer: 'Definitely. I can help with remote job searches and distributed team dynamics.',
- },
-]
-
-const engagementOptions = [
- {
- name: 'Discovery Call',
- duration: '30 minutes',
- description: "Let's talk about what you're working on. No commitment, just a conversation.",
- features: ['Initial conversation', 'Goal discussion', "See if we're a good fit"],
- },
- {
- name: 'Single Session',
- duration: '60 minutes',
- description: 'Focused time on a specific challenge or question.',
- features: ['1-on-1 time', 'Targeted guidance', 'Action items'],
- },
- {
- name: 'Ongoing Mentorship',
- duration: '3 months',
- description: 'Regular check-ins for those who want sustained support.',
- features: ['Weekly sessions', 'Resume review', 'Interview prep', 'Email support'],
- },
-]
-
-export default function CareerGuidancePage(): React.JSX.Element {
- return (
-
-
- {/* Hero Section */}
-
-
Career Guidance
-
- With nearly 20 years of experience in professional development, I offer career coaching
- for those navigating transitions, preparing for interviews, or figuring out their next
- move.
-
+ Curious who you'd be working with?{' '}
+
+ More about my background
+
+ .
+
+
+
+ )
+}
diff --git a/app/components/redirect-page.tsx b/app/components/redirect-page.tsx
new file mode 100644
index 0000000..bdf15d9
--- /dev/null
+++ b/app/components/redirect-page.tsx
@@ -0,0 +1,27 @@
+import Link from 'next/link'
+import type React from 'react'
+
+import { resolveRedirect } from '../../lib/redirects'
+
+/**
+ * Stub body for a moved URL. React hoists the meta refresh into ;
+ * the visible link covers crawlers and anyone with auto-refresh disabled.
+ */
+export default function RedirectPage({ from }: { from: string }): React.JSX.Element {
+ const to = resolveRedirect(from)
+ return (
+ <>
+
+
+
This page has moved
+
+ If you are not redirected automatically, continue to{' '}
+
+ the new page
+
+ .
+
+
+ >
+ )
+}
diff --git a/app/config/nav.ts b/app/config/nav.ts
index 2f5efa8..28bcc3b 100644
--- a/app/config/nav.ts
+++ b/app/config/nav.ts
@@ -15,7 +15,7 @@ export const navItems: Record = {
name: 'experiments',
},
- '/career-guidance': {
+ '/services/career-coaching': {
name: 'coaching',
},
}
diff --git a/app/metadata-policy.test.ts b/app/metadata-policy.test.ts
index a8d6937..48e3583 100644
--- a/app/metadata-policy.test.ts
+++ b/app/metadata-policy.test.ts
@@ -1,4 +1,5 @@
///
+import { redirects } from 'lib/redirects'
import type { Metadata } from 'next'
/**
@@ -9,6 +10,9 @@ import type { Metadata } from 'next'
* which suppressed indexing of all sub-pages.
* 2. Titles, descriptions, and canonicals are unique — 12 experiment stubs
* once shared one inherited title/description.
+ *
+ * Redirect stubs (lib/redirects.ts) are the one sanctioned exception: their
+ * canonical deliberately points at the destination page.
*/
const pageModules = import.meta.glob('./**/page.tsx', { eager: true }) as Record<
string,
@@ -18,11 +22,24 @@ const pageModules = import.meta.glob('./**/page.tsx', { eager: true }) as Record
// Modules only — importing them would execute ImageResponse; paths suffice.
const ogImageFiles = Object.keys(import.meta.glob('./**/opengraph-image.tsx'))
-const pages = Object.entries(pageModules).map(([path, module]) => ({
+// './(main)/services/career-coaching/page.tsx' -> '/services/career-coaching'
+function routeOf(path: string): string {
+ const route = path
+ .replace(/\([^)]+\)\//g, '')
+ .replace(/^\.\//, '/')
+ .replace(/\/page\.tsx$/, '')
+ return route === '' ? '/' : route
+}
+
+const allPages = Object.entries(pageModules).map(([path, module]) => ({
path,
+ route: routeOf(path),
metadata: module.metadata,
}))
+const redirectStubs = allPages.filter(page => page.route in redirects)
+const pages = allPages.filter(page => !(page.route in redirects))
+
describe('metadata policy', () => {
it('discovers the full page inventory (route groups included)', () => {
expect(pages.length).toBeGreaterThanOrEqual(18)
@@ -31,18 +48,22 @@ describe('metadata policy', () => {
})
it.each(
- pages.map(page => [page.path, page.metadata] as const)
- )('%s exports complete metadata', (_path, metadata) => {
+ pages.map(page => [page.path, page.route, page.metadata] as const)
+ )('%s exports complete metadata', (_path, route, metadata) => {
expect(metadata).toBeDefined()
expect(typeof metadata?.title).toBe('string')
expect((metadata?.title as string).length).toBeGreaterThan(0)
expect(typeof metadata?.description).toBe('string')
expect((metadata?.description as string).length).toBeGreaterThan(0)
- const canonical = metadata?.alternates?.canonical
- expect(typeof canonical).toBe('string')
- expect(canonical as string).toMatch(/^\//)
- expect(canonical as string).not.toMatch(/.\/$/)
+ expect(metadata?.alternates?.canonical).toBe(route)
+ })
+
+ it('canonicalizes every redirect stub to its registered destination', () => {
+ expect(redirectStubs.length).toBe(Object.keys(redirects).length)
+ for (const stub of redirectStubs) {
+ expect(stub.metadata?.alternates?.canonical).toBe(redirects[stub.route])
+ }
})
it('has a unique canonical per page', () => {
diff --git a/app/sitemap.test.ts b/app/sitemap.test.ts
index 4696cc9..b5e78b3 100644
--- a/app/sitemap.test.ts
+++ b/app/sitemap.test.ts
@@ -23,10 +23,11 @@ describe('sitemap', () => {
}
})
- it('includes the career-guidance sales page', async () => {
+ it('includes the career-coaching sales page but never its redirect stub', async () => {
const urls = (await entries()).map(route => route.url)
- expect(urls).toContain('https://bidwell.info/career-guidance')
+ expect(urls).toContain('https://bidwell.info/services/career-coaching')
+ expect(urls).not.toContain('https://bidwell.info/career-guidance')
})
it('includes active experiments and excludes noindexed coming-soon stubs', async () => {
diff --git a/app/sitemap.ts b/app/sitemap.ts
index 56e5fc2..cb8bac2 100644
--- a/app/sitemap.ts
+++ b/app/sitemap.ts
@@ -17,7 +17,7 @@ export const dynamic = 'force-static'
export default async function sitemap() {
const routes = [
{ url: baseUrl, priority: 1.0 },
- { url: `${baseUrl}/career-guidance`, priority: 0.8 },
+ { url: `${baseUrl}/services/career-coaching`, priority: 0.9 },
{ url: `${baseUrl}/blog`, priority: 0.7 },
...posts.map(post => ({
url: `${baseUrl}/blog/${post.slug}`,
diff --git a/e2e/seo.spec.ts b/e2e/seo.spec.ts
index 4cea01f..c4682c8 100644
--- a/e2e/seo.spec.ts
+++ b/e2e/seo.spec.ts
@@ -60,11 +60,22 @@ test.describe('SEO invariants', () => {
test('sitemap lists indexable pages only', async ({ request }) => {
const xml = await (await request.get('/sitemap.xml')).text()
- expect(xml).toContain('https://bidwell.info/career-guidance')
+ expect(xml).toContain('https://bidwell.info/services/career-coaching')
+ expect(xml).not.toContain('https://bidwell.info/career-guidance')
expect(xml).toContain('https://bidwell.info/experiments/claude-agentic-framework')
expect(xml).not.toContain('sonic-weather')
})
+ test('the moved career-guidance URL serves a redirect stub', async ({ page }) => {
+ await page.goto('/career-guidance', { waitUntil: 'commit' })
+
+ // The 0s meta refresh fires immediately; landing on the destination IS the pass.
+ await page.waitForURL('**/services/career-coaching')
+ await expect(
+ page.getByRole('heading', { level: 1, name: /tech career coaching/i })
+ ).toBeVisible()
+ })
+
test('entity graph renders ProfessionalService, Person, and WebSite JSON-LD', async ({
page,
}) => {
diff --git a/lib/redirects.ts b/lib/redirects.ts
new file mode 100644
index 0000000..285da00
--- /dev/null
+++ b/lib/redirects.ts
@@ -0,0 +1,30 @@
+import type { Metadata } from 'next'
+
+/**
+ * Client-side redirects for moved URLs — GitHub Pages cannot serve 301s.
+ * Each entry keeps a stub page at the old path emitting an instant meta
+ * refresh plus a canonical pointing at the destination, which search
+ * engines treat as a redirect signal. Stubs are never sitemapped or
+ * internally linked, and entries are permanent — never delete one or
+ * reuse an old path.
+ */
+export const redirects: Record = {
+ '/career-guidance': '/services/career-coaching',
+}
+
+export function resolveRedirect(from: string): string {
+ const to = redirects[from]
+ if (!to) {
+ throw new Error(`No redirect registered for ${from}`)
+ }
+ return to
+}
+
+export function redirectMetadata(from: string): Metadata {
+ const to = resolveRedirect(from)
+ return {
+ title: `Moved to ${to}`,
+ description: `This page has moved to ${to}.`,
+ alternates: { canonical: to },
+ }
+}
diff --git a/public/images/next-image-export-optimizer-hashes.json b/public/images/next-image-export-optimizer-hashes.json
index 8b53e84..441d0ff 100644
--- a/public/images/next-image-export-optimizer-hashes.json
+++ b/public/images/next-image-export-optimizer-hashes.json
@@ -1,4 +1,4 @@
{
- "/apple-icon.2op_3_9qhojyb.png": "PPJAMIBvTZwzudZNELxW9ijdr747DHs7IkCoXyI3lUU=",
- "/icon.18nryn5uzzn91.png": "m124whsoQxa0sD-w9gCvFg-ETvv0-7nX4LSj5iimMp8="
-}
+ "/apple-icon.2op_3_9qhojyb.png": "PPJAMIBvTZwzudZNELxW9ijdr747DHs7IkCoXyI3lUU=",
+ "/icon.18nryn5uzzn91.png": "m124whsoQxa0sD-w9gCvFg-ETvv0-7nX4LSj5iimMp8="
+}
\ No newline at end of file
From 3ee62973e962da2e209d423f41695dda0dade401 Mon Sep 17 00:00:00 2001
From: Jordan Winters
Date: Tue, 21 Jul 2026 17:23:32 -0500
Subject: [PATCH 2/8] feat(seo): services hub + three Twin Cities service pages
/services hub (four offering cards, solo positioning, local intro) plus
dedicated pages for the three consulting lines, each targeting one
primary query: software-consulting ('software consultant minneapolis'),
ai-consulting ('ai consulting minneapolis'), and
engineering-practice-improvement ('engineering process improvement
consultant'). Every page: distinct 600-900 word copy in first person, a
deliberately different local angle (onsite architecture sessions / past-
the-demo-phase AI adoption / embedded practice change), six real-search
FAQs with FAQPage schema, Service schema, breadcrumb, own OG card,
related-services triangle, and discovery-call CTA. The AI page cites the
agent-coordination essay as proof of practice. Sitemap gains all four
routes in the same PR that ships them (health-check contract).
Co-Authored-By: Claude Fable 5
---
.../ai-consulting/opengraph-image.tsx | 10 +
.../services/ai-consulting/page.test.tsx | 98 +++++++++
app/(main)/services/ai-consulting/page.tsx | 189 ++++++++++++++++++
.../opengraph-image.tsx | 10 +
.../page.test.tsx | 94 +++++++++
.../engineering-practice-improvement/page.tsx | 178 +++++++++++++++++
app/(main)/services/opengraph-image.tsx | 10 +
app/(main)/services/page.test.tsx | 65 ++++++
app/(main)/services/page.tsx | 97 +++++++++
.../software-consulting/opengraph-image.tsx | 10 +
.../software-consulting/page.test.tsx | 94 +++++++++
.../services/software-consulting/page.tsx | 187 +++++++++++++++++
app/sitemap.test.ts | 6 +-
app/sitemap.ts | 4 +
14 files changed, 1051 insertions(+), 1 deletion(-)
create mode 100644 app/(main)/services/ai-consulting/opengraph-image.tsx
create mode 100644 app/(main)/services/ai-consulting/page.test.tsx
create mode 100644 app/(main)/services/ai-consulting/page.tsx
create mode 100644 app/(main)/services/engineering-practice-improvement/opengraph-image.tsx
create mode 100644 app/(main)/services/engineering-practice-improvement/page.test.tsx
create mode 100644 app/(main)/services/engineering-practice-improvement/page.tsx
create mode 100644 app/(main)/services/opengraph-image.tsx
create mode 100644 app/(main)/services/page.test.tsx
create mode 100644 app/(main)/services/page.tsx
create mode 100644 app/(main)/services/software-consulting/opengraph-image.tsx
create mode 100644 app/(main)/services/software-consulting/page.test.tsx
create mode 100644 app/(main)/services/software-consulting/page.tsx
diff --git a/app/(main)/services/ai-consulting/opengraph-image.tsx b/app/(main)/services/ai-consulting/opengraph-image.tsx
new file mode 100644
index 0000000..78e6624
--- /dev/null
+++ b/app/(main)/services/ai-consulting/opengraph-image.tsx
@@ -0,0 +1,10 @@
+import { OG_SIZE, ogCard } from '../../../../lib/og-image'
+
+export const dynamic = 'force-static'
+export const alt = 'AI Consulting & Agent Engineering'
+export const size = OG_SIZE
+export const contentType = 'image/png'
+
+export default function OpengraphImage() {
+ return ogCard('AI Consulting & Agent Engineering', 'Bidwell Consulting · Minneapolis')
+}
diff --git a/app/(main)/services/ai-consulting/page.test.tsx b/app/(main)/services/ai-consulting/page.test.tsx
new file mode 100644
index 0000000..54ab9f9
--- /dev/null
+++ b/app/(main)/services/ai-consulting/page.test.tsx
@@ -0,0 +1,98 @@
+import { axeTest, render, screen } from 'lib/test-utils'
+import { describe, expect, it } from 'vitest'
+import AiConsultingPage from './page'
+
+describe('AI Consulting Page', () => {
+ it('leads with the agent-engineering h1 and practices-what-it-sells proof', () => {
+ render()
+
+ expect(
+ screen.getByRole('heading', { level: 1, name: /ai consulting & agent engineering/i })
+ ).toBeInTheDocument()
+ expect(
+ screen.getByRole('link', { name: /how i coordinate ai coding agents/i })
+ ).toHaveAttribute('href', '/blog/agent-coordination')
+ })
+
+ it('covers implementation, agents, and AI-native teams', () => {
+ render()
+
+ expect(
+ screen.getByRole('heading', { name: /ai implementation & llm integration/i })
+ ).toBeInTheDocument()
+ expect(screen.getByRole('heading', { name: /ai agent development/i })).toBeInTheDocument()
+ expect(
+ screen.getByRole('heading', { name: /ai-native engineering teams/i })
+ ).toBeInTheDocument()
+ expect(screen.getByRole('heading', { name: /how i approach ai projects/i })).toBeInTheDocument()
+ })
+
+ it('has a distinct Twin Cities paragraph about the local adoption moment', () => {
+ render()
+
+ expect(
+ screen.getByRole('heading', { name: /ai work from the twin cities/i })
+ ).toBeInTheDocument()
+ expect(screen.getByText(/past the demo phase/i)).toBeInTheDocument()
+ })
+
+ it('answers real AI-adoption questions in the FAQ', () => {
+ render()
+
+ expect(
+ screen.getByText(/can you build custom ai agents for our workflows\?/i)
+ ).toBeInTheDocument()
+ expect(screen.getByText(/is our data safe when we use ai tools\?/i)).toBeInTheDocument()
+ expect(
+ screen.getByText(/do we need custom ai or can we use off-the-shelf tools\?/i)
+ ).toBeInTheDocument()
+ })
+
+ it('links the related-services triangle and the contact CTA', () => {
+ render()
+
+ expect(screen.getByRole('link', { name: /engineering practice improvement/i })).toHaveAttribute(
+ 'href',
+ '/services/engineering-practice-improvement'
+ )
+ expect(screen.getByRole('link', { name: /software consulting/i })).toHaveAttribute(
+ 'href',
+ '/services/software-consulting'
+ )
+ expect(screen.getByRole('link', { name: /book a free discovery call/i })).toHaveAttribute(
+ 'href',
+ '/contact'
+ )
+ })
+
+ it('emits Service, FAQPage, and breadcrumb structured data', () => {
+ const { container } = render()
+
+ const types = Array.from(container.querySelectorAll('script[type="application/ld+json"]')).map(
+ script => JSON.parse(script.textContent || '{}')['@type']
+ )
+ expect(types).toContain('Service')
+ expect(types).toContain('FAQPage')
+ expect(types).toContain('BreadcrumbList')
+ })
+
+ it('has exactly one h1 and a coherent h2 set', () => {
+ render()
+
+ expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1)
+ expect(screen.getAllByRole('heading', { level: 2 })).toHaveLength(8)
+ })
+
+ it('is accessible', async () => {
+ const { container } = render()
+ await axeTest(container)
+ })
+
+ it('targets "ai consulting minneapolis" in metadata', async () => {
+ const { metadata } = await import('./page')
+ expect(metadata.title).toBe('AI Consulting in Minneapolis')
+ expect(metadata.description).toContain('LLM integration')
+ expect(metadata.description).toContain('Minneapolis')
+ expect(metadata.alternates?.canonical).toBe('/services/ai-consulting')
+ })
+})
diff --git a/app/(main)/services/ai-consulting/page.tsx b/app/(main)/services/ai-consulting/page.tsx
new file mode 100644
index 0000000..5017456
--- /dev/null
+++ b/app/(main)/services/ai-consulting/page.tsx
@@ -0,0 +1,189 @@
+import { faqSchema, serviceSchema } from 'lib/structured-data'
+import type { Metadata } from 'next'
+import Link from 'next/link'
+import type React from 'react'
+import Breadcrumb from '../../../components/breadcrumb'
+import JsonLd from '../../../components/structured-data'
+
+export const metadata: Metadata = {
+ title: 'AI Consulting in Minneapolis',
+ description:
+ 'AI consulting in Minneapolis — LLM integration, AI agent development, and coding-agent workflows for Twin Cities teams. Book a free discovery call.',
+ keywords: [
+ 'ai consulting minneapolis',
+ 'ai implementation consultant',
+ 'ai agent development',
+ 'llm integration consultant',
+ 'ai consulting twin cities',
+ ],
+ alternates: { canonical: '/services/ai-consulting' },
+ openGraph: {
+ title: 'AI Consulting & Agent Engineering — Minneapolis',
+ description:
+ 'LLM integration, AI agent development, and coding-agent workflows — from a consultant who works this way daily.',
+ type: 'website',
+ },
+}
+
+const faqs = [
+ {
+ question: 'What does an AI implementation consultant do?',
+ answer:
+ 'Turns "we should be doing something with AI" into a working system: picking the use cases worth automating, integrating LLMs into your product or workflow, building the evaluation and guardrails around them, and getting your team comfortable operating it all.',
+ },
+ {
+ question: 'How do we start using AI in our business?',
+ answer:
+ "Start with one workflow that's high-volume, low-risk, and measurable — not a moonshot. We scope it in a discovery call, ship a working pilot, measure honestly, and only then widen. Most failed AI projects skipped the 'measure honestly' step.",
+ },
+ {
+ question: 'Do we need custom AI or can we use off-the-shelf tools?',
+ answer:
+ 'Usually off-the-shelf models with custom integration — the value is rarely in training your own model, and almost always in how well the model is wired into your data, your process, and your guardrails. I'll tell you plainly when a SaaS tool already solves your problem.',
+ },
+ {
+ question: 'Can you build custom AI agents for our workflows?',
+ answer:
+ 'Yes — agents that draft, triage, reconcile, or coordinate real business processes, with human checkpoints where the cost of a mistake is real. I publish how I coordinate my own coding agents, so you can inspect the approach before you hire it.',
+ },
+ {
+ question: 'Is our data safe when we use AI tools?',
+ answer:
+ 'It can be, if you choose deliberately: which providers see what data, what gets retained, what stays local, and what never leaves your systems. Data-boundary design is part of every AI engagement I take — not an afterthought.',
+ },
+ {
+ question: 'Can you train our engineering team on AI coding tools?',
+ answer:
+ 'Yes. I help teams adopt Copilot- and Claude-class tools with the practices that make them safe: review discipline, quality gates, and workflows that amplify judgment instead of replacing it.',
+ },
+]
+
+export default function AiConsultingPage(): React.JSX.Element {
+ return (
+
+
+
+
+
+
+ AI Consulting & Agent Engineering
+
+
+ Plenty of consultants advise on AI. I build with it daily — including the multi-agent
+ workflows that produce my own work — and I publish{' '}
+
+ how I coordinate AI coding agents
+ {' '}
+ so you can judge the approach before you pay for it. The goal is always the same: AI that
+ ships, with guardrails, not demos that die in a slide deck.
+
+
+
+
+ AI Implementation & LLM Integration
+
+
+ Wiring large language models into products and workflows that already exist: document
+ processing, support triage, search and retrieval over your own data, drafting and
+ summarization inside the tools your team lives in. Scoped around evaluation from day one,
+ so you know whether it works — not just whether it demos.
+
+
+
+
+
AI Agent Development
+
+ Agents earn their keep on multi-step work: triaging queues, reconciling records,
+ coordinating handoffs between systems. I design them with explicit boundaries and human
+ checkpoints where mistakes are expensive — autonomy where it's safe, oversight where
+ it isn't.
+
+
+
+
+
AI-Native Engineering Teams
+
+ Coding agents are the biggest change to software delivery in a decade, and most teams are
+ getting mixed results because they adopted the tool without the practice. I help
+ engineering teams put the structure around Copilot- and Claude-class tools — review
+ discipline, quality gates, coordination patterns — that turns them from chaos amplifiers
+ into force multipliers.
+
+
+
+
+
How I Approach AI Projects
+
+ Pragmatically. AI is spectacular at some jobs and confidently wrong at others, and the
+ expensive failures come from not knowing which is which. I start small, measure real
+ outcomes, design the data boundaries deliberately, and tell you when the right answer is
+ “don't use AI for this.”
+
+
+
+
+
AI Work From the Twin Cities
+
+ Twin Cities companies are past the demo phase — the question now is what AI actually
+ changes for their business. I meet Minneapolis–St. Paul clients in person for working
+ sessions and leadership briefings; the engineering itself runs remote, same as any modern
+ delivery.
+
+
+
+
+
Frequently Asked Questions
+
+ {faqs.map(faq => (
+
+
{faq.question}
+
{faq.answer}
+
+ ))}
+
+
+
+
+
Related Services
+
+ Making AI adoption stick is a practice problem — see{' '}
+
+ engineering practice improvement
+
+ . Need the system around the model built too? See{' '}
+
+ software consulting
+
+ .
+
+
+
+
+
Start a Conversation
+
+ Have a workflow you suspect AI could carry?{' '}
+
+ Book a free discovery call
+ {' '}
+ — 30 minutes, no commitment.
+
+
+
+ )
+}
diff --git a/app/(main)/services/engineering-practice-improvement/opengraph-image.tsx b/app/(main)/services/engineering-practice-improvement/opengraph-image.tsx
new file mode 100644
index 0000000..571c074
--- /dev/null
+++ b/app/(main)/services/engineering-practice-improvement/opengraph-image.tsx
@@ -0,0 +1,10 @@
+import { OG_SIZE, ogCard } from '../../../../lib/og-image'
+
+export const dynamic = 'force-static'
+export const alt = 'Engineering Practice Improvement'
+export const size = OG_SIZE
+export const contentType = 'image/png'
+
+export default function OpengraphImage() {
+ return ogCard('Engineering Practice Improvement', 'Bidwell Consulting · Minneapolis')
+}
diff --git a/app/(main)/services/engineering-practice-improvement/page.test.tsx b/app/(main)/services/engineering-practice-improvement/page.test.tsx
new file mode 100644
index 0000000..0d4dfce
--- /dev/null
+++ b/app/(main)/services/engineering-practice-improvement/page.test.tsx
@@ -0,0 +1,94 @@
+import { axeTest, render, screen } from 'lib/test-utils'
+import { describe, expect, it } from 'vitest'
+import EngineeringPracticeImprovementPage from './page'
+
+describe('Engineering Practice Improvement Page', () => {
+ it('leads with the consulting h1 and the day-job credential', () => {
+ render()
+
+ expect(
+ screen.getByRole('heading', {
+ level: 1,
+ name: /engineering practice improvement consulting/i,
+ })
+ ).toBeInTheDocument()
+ expect(
+ screen.getByText(/lead engineering practice\s+improvement for a living/i)
+ ).toBeInTheDocument()
+ })
+
+ it('covers scope, engagement shape, and measurement', () => {
+ render()
+
+ expect(
+ screen.getByRole('heading', { name: /what practice improvement covers/i })
+ ).toBeInTheDocument()
+ expect(screen.getByRole('heading', { name: /how an engagement runs/i })).toBeInTheDocument()
+ expect(screen.getByRole('heading', { name: /measuring what matters/i })).toBeInTheDocument()
+ })
+
+ it('has a distinct Twin Cities paragraph about embedded on-site time', () => {
+ render()
+
+ expect(
+ screen.getByRole('heading', { name: /working with twin cities teams/i })
+ ).toBeInTheDocument()
+ expect(screen.getByText(/embed on-site/i)).toBeInTheDocument()
+ })
+
+ it('answers agile-vs-practice and measurement questions in the FAQ', () => {
+ render()
+
+ expect(screen.getByText(/is this the same as agile coaching\?/i)).toBeInTheDocument()
+ expect(screen.getByText(/how do you measure developer productivity\?/i)).toBeInTheDocument()
+ expect(screen.getByText(/can you help us adopt ai coding tools safely\?/i)).toBeInTheDocument()
+ })
+
+ it('links the related services and the contact CTA', () => {
+ render()
+
+ expect(screen.getByRole('link', { name: /ai & agent engineering/i })).toHaveAttribute(
+ 'href',
+ '/services/ai-consulting'
+ )
+ expect(screen.getByRole('link', { name: /career coaching/i })).toHaveAttribute(
+ 'href',
+ '/services/career-coaching'
+ )
+ expect(screen.getByRole('link', { name: /book a free discovery call/i })).toHaveAttribute(
+ 'href',
+ '/contact'
+ )
+ })
+
+ it('emits Service, FAQPage, and breadcrumb structured data', () => {
+ const { container } = render()
+
+ const types = Array.from(container.querySelectorAll('script[type="application/ld+json"]')).map(
+ script => JSON.parse(script.textContent || '{}')['@type']
+ )
+ expect(types).toContain('Service')
+ expect(types).toContain('FAQPage')
+ expect(types).toContain('BreadcrumbList')
+ })
+
+ it('has exactly one h1 and a coherent h2 set', () => {
+ render()
+
+ expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1)
+ expect(screen.getAllByRole('heading', { level: 2 })).toHaveLength(7)
+ })
+
+ it('is accessible', async () => {
+ const { container } = render()
+ await axeTest(container)
+ })
+
+ it('targets "engineering process improvement consultant" in metadata', async () => {
+ const { metadata } = await import('./page')
+ expect(metadata.title).toBe('Engineering Practice Improvement')
+ expect(metadata.description).toContain('process improvement')
+ expect(metadata.description).toContain('Twin Cities')
+ expect(metadata.alternates?.canonical).toBe('/services/engineering-practice-improvement')
+ })
+})
diff --git a/app/(main)/services/engineering-practice-improvement/page.tsx b/app/(main)/services/engineering-practice-improvement/page.tsx
new file mode 100644
index 0000000..1fd1afc
--- /dev/null
+++ b/app/(main)/services/engineering-practice-improvement/page.tsx
@@ -0,0 +1,178 @@
+import { faqSchema, serviceSchema } from 'lib/structured-data'
+import type { Metadata } from 'next'
+import Link from 'next/link'
+import type React from 'react'
+import Breadcrumb from '../../../components/breadcrumb'
+import JsonLd from '../../../components/structured-data'
+
+export const metadata: Metadata = {
+ title: 'Engineering Practice Improvement',
+ description:
+ 'Engineering process improvement consulting in Minneapolis — DevOps, agile delivery, and developer productivity for Twin Cities teams. Free discovery call.',
+ keywords: [
+ 'engineering process improvement consultant',
+ 'devops consulting twin cities',
+ 'agile consulting minneapolis',
+ 'developer productivity consultant',
+ 'engineering effectiveness',
+ ],
+ alternates: { canonical: '/services/engineering-practice-improvement' },
+ openGraph: {
+ title: 'Engineering Practice Improvement — Minneapolis',
+ description:
+ 'DevOps, agile delivery, and developer productivity consulting from a practitioner who leads this work for a living.',
+ type: 'website',
+ },
+}
+
+const faqs = [
+ {
+ question: 'What is engineering practice improvement?',
+ answer:
+ "Making the way your team builds software measurably better: how work flows from idea to production, how quality is enforced, how ownership is shared, and how fast you can safely ship. It's the discipline of removing the friction your engineers complain about in private.",
+ },
+ {
+ question: 'Is this the same as agile coaching?',
+ answer:
+ "It overlaps, but no. I'm an engineer first — the recommendations come from shipping software, not from a certification deck. Ceremony that doesn't serve delivery gets cut, not added. If a standup isn't earning its fifteen minutes, we kill the standup.",
+ },
+ {
+ question: 'How do you measure developer productivity?',
+ answer:
+ 'Flow and outcome metrics — lead time, deploy frequency, change-failure rate, recovery time — plus the qualitative signal your engineers already have. What I refuse to do is rank humans by lines of code or ticket counts; vanity metrics rot trust and change nothing.',
+ },
+ {
+ question: 'How long does a process improvement engagement take?',
+ answer:
+ 'An assessment runs a couple of weeks. Real practice change sticks over one to three months of embedded work — long enough to change habits, short enough to stay honest. You should see the first measurable improvement inside the first month.',
+ },
+ {
+ question: 'Can you help us adopt AI coding tools safely?',
+ answer:
+ "Yes — it's become the most common reason teams call. Coding agents amplify whatever discipline you already have, so the practices come first: review standards, quality gates, and coordination patterns. I publish how I run my own agent workflows.",
+ },
+ {
+ question: 'Do you work with distributed or remote teams?',
+ answer:
+ 'Constantly. Practice improvement is mostly about how work and decisions flow, and that flows through the same tools whether your team sits in one room or five time zones.',
+ },
+]
+
+export default function EngineeringPracticeImprovementPage(): React.JSX.Element {
+ return (
+
+
+
+
+
+
+ Engineering Practice Improvement Consulting
+
+
+ Helping engineering teams ship faster is my day job — I lead Engineering Practice
+ Improvement for a living, and I consult on the same craft. If your team is busy but delivery
+ feels slow, the problem is rarely effort. It's friction, and friction can be found and
+ removed.
+
+
+
+
+ What Practice Improvement Covers
+
+
+ The whole path from idea to production: DevOps practices and pipeline health, agile
+ delivery that serves outcomes instead of ceremony, code ownership and review culture, and
+ the product operating model that connects engineering work to things customers notice.
+ Teams should run experiments, own what they ship, and see their work matter.
+
+
+
+
+
How an Engagement Runs
+
+ Assess: a few weeks watching how work actually flows — not how the wiki
+ says it flows. Recommend: a short, ranked list of changes with the
+ reasoning attached, never a hundred-page deck. Embed: I work alongside
+ the team while the changes take hold, because practice change that arrives by memo
+ doesn't survive the quarter.
+
+
+
+
+
Measuring What Matters
+
+ Lead time, deploy frequency, change-failure rate, time to recover — measured before and
+ after, so improvement is a fact rather than a feeling. And the AI-era addition: teams that
+ get their practices right can adopt coding agents safely and go genuinely faster; teams
+ that don't just produce defects at higher velocity.
+
+
+
+
+
Working With Twin Cities Teams
+
+ Practice change sticks when someone is in the room. For Minneapolis–St. Paul teams I embed
+ on-site for the moments that matter — working sessions, retros, delivery reviews — and run
+ the rest remotely. Fully distributed org? The whole engagement runs remote just as well.
+
+
+
+
+
Frequently Asked Questions
+
+ {faqs.map(faq => (
+
+
{faq.question}
+
{faq.answer}
+
+ ))}
+
+
+
+
+
Related Services
+
+ Adopting AI as part of the change? See{' '}
+
+ AI & agent engineering
+
+ . Want growth support for the engineers themselves? See{' '}
+
+ career coaching
+
+ .
+
+
+
+
+
Start a Conversation
+
+ Curious where your delivery friction actually is?{' '}
+
+ Book a free discovery call
+ {' '}
+ — 30 minutes, no commitment.
+
+
+
+ )
+}
diff --git a/app/(main)/services/opengraph-image.tsx b/app/(main)/services/opengraph-image.tsx
new file mode 100644
index 0000000..88724b0
--- /dev/null
+++ b/app/(main)/services/opengraph-image.tsx
@@ -0,0 +1,10 @@
+import { OG_SIZE, ogCard } from '../../../lib/og-image'
+
+export const dynamic = 'force-static'
+export const alt = 'Consulting Services'
+export const size = OG_SIZE
+export const contentType = 'image/png'
+
+export default function OpengraphImage() {
+ return ogCard('Consulting Services', 'Bidwell Consulting · Minneapolis')
+}
diff --git a/app/(main)/services/page.test.tsx b/app/(main)/services/page.test.tsx
new file mode 100644
index 0000000..7d76fd9
--- /dev/null
+++ b/app/(main)/services/page.test.tsx
@@ -0,0 +1,65 @@
+import { axeTest, render, screen } from 'lib/test-utils'
+import { describe, expect, it } from 'vitest'
+import ServicesPage from './page'
+
+describe('Services Hub Page', () => {
+ it('renders the hub heading and the solo local intro', () => {
+ render()
+
+ expect(
+ screen.getByRole('heading', { level: 1, name: /consulting services/i })
+ ).toBeInTheDocument()
+ expect(screen.getByText(/based in minneapolis/i)).toBeInTheDocument()
+ expect(screen.getByText(/twin cities metro/i)).toBeInTheDocument()
+ })
+
+ it('links all four offerings as cards', () => {
+ render()
+
+ expect(screen.getByRole('link', { name: /software consulting/i })).toHaveAttribute(
+ 'href',
+ '/services/software-consulting'
+ )
+ expect(screen.getByRole('link', { name: /ai & agent engineering/i })).toHaveAttribute(
+ 'href',
+ '/services/ai-consulting'
+ )
+ expect(screen.getByRole('link', { name: /engineering practice improvement/i })).toHaveAttribute(
+ 'href',
+ '/services/engineering-practice-improvement'
+ )
+ expect(screen.getByRole('link', { name: /career coaching/i })).toHaveAttribute(
+ 'href',
+ '/services/career-coaching'
+ )
+ })
+
+ it('describes who the practice serves and offers the discovery call', () => {
+ render()
+
+ expect(screen.getByRole('heading', { name: /who i work with/i })).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: /free 30-minute discovery call/i })).toHaveAttribute(
+ 'href',
+ '/contact'
+ )
+ })
+
+ it('has exactly one h1 and a coherent h2 set', () => {
+ render()
+
+ expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1)
+ expect(screen.getAllByRole('heading', { level: 2 })).toHaveLength(3)
+ })
+
+ it('is accessible', async () => {
+ const { container } = render()
+ await axeTest(container)
+ })
+
+ it('has hub metadata with a self-canonical', async () => {
+ const { metadata } = await import('./page')
+ expect(metadata.title).toBe('Software & AI Consulting Services')
+ expect(metadata.description).toContain('Minneapolis')
+ expect(metadata.alternates?.canonical).toBe('/services')
+ })
+})
diff --git a/app/(main)/services/page.tsx b/app/(main)/services/page.tsx
new file mode 100644
index 0000000..0df4b70
--- /dev/null
+++ b/app/(main)/services/page.tsx
@@ -0,0 +1,97 @@
+import type { Metadata } from 'next'
+import Link from 'next/link'
+import type React from 'react'
+import Breadcrumb from '../../components/breadcrumb'
+
+export const metadata: Metadata = {
+ title: 'Software & AI Consulting Services',
+ description:
+ 'Software consulting, AI & agent engineering, engineering practice improvement, and tech career coaching from a Minneapolis consultant. Free discovery call.',
+ alternates: { canonical: '/services' },
+}
+
+const services = [
+ {
+ title: 'Software Consulting',
+ href: '/services/software-consulting',
+ description: 'Architecture, custom software development, and rescue work — hands-on.',
+ },
+ {
+ title: 'AI & Agent Engineering',
+ href: '/services/ai-consulting',
+ description: 'Pragmatic AI adoption: LLM integration, custom agents, coding-agent workflows.',
+ },
+ {
+ title: 'Engineering Practice Improvement',
+ href: '/services/engineering-practice-improvement',
+ description: 'Ship faster with healthier DevOps, agile delivery, and review culture.',
+ },
+ {
+ title: 'Career Coaching',
+ href: '/services/career-coaching',
+ description: '1-on-1 coaching for engineers and tech leaders — resumes, interviews, moves.',
+ },
+]
+
+export default function ServicesPage(): React.JSX.Element {
+ return (
+
+
+
+
Consulting Services
+
+
+
How I Can Help
+
+ I'm Jordan Winters — an independent consultant with nearly 20 years in software.
+ Every engagement is with me directly: the person you talk to on the discovery call is the
+ person who does the work. I'm based in Minneapolis and work with companies across the
+ Twin Cities metro, and remotely everywhere else.
+
+
+ {services.map(service => (
+
+
{service.title}
+
+ {service.description}
+
+
+ Learn more
+
+
+ ))}
+
+
+
+
+
Who I Work With
+
+ Founders and CTOs of small-to-mid companies who need senior software judgment without an
+ agency's overhead. Engineering leaders at larger organizations who want delivery to
+ feel less stuck. And individual engineers working on their own careers. I'm one
+ person by design — which means honest scope, direct communication, and no handoffs.
+
+
+
+
+
Start With a Conversation
+
+ Not sure which of these fits? That's what the{' '}
+
+ free 30-minute discovery call
+ {' '}
+ is for.
+
+
+
+ )
+}
diff --git a/app/(main)/services/software-consulting/opengraph-image.tsx b/app/(main)/services/software-consulting/opengraph-image.tsx
new file mode 100644
index 0000000..d5eaa36
--- /dev/null
+++ b/app/(main)/services/software-consulting/opengraph-image.tsx
@@ -0,0 +1,10 @@
+import { OG_SIZE, ogCard } from '../../../../lib/og-image'
+
+export const dynamic = 'force-static'
+export const alt = 'Software Consulting in Minneapolis'
+export const size = OG_SIZE
+export const contentType = 'image/png'
+
+export default function OpengraphImage() {
+ return ogCard('Software Consulting', 'Bidwell Consulting · Minneapolis')
+}
diff --git a/app/(main)/services/software-consulting/page.test.tsx b/app/(main)/services/software-consulting/page.test.tsx
new file mode 100644
index 0000000..5eaa22b
--- /dev/null
+++ b/app/(main)/services/software-consulting/page.test.tsx
@@ -0,0 +1,94 @@
+import { axeTest, render, screen } from 'lib/test-utils'
+import { describe, expect, it } from 'vitest'
+import SoftwareConsultingPage from './page'
+
+describe('Software Consulting Page', () => {
+ it('leads with the Minneapolis-targeted h1 and hands-on positioning', () => {
+ render()
+
+ expect(
+ screen.getByRole('heading', { level: 1, name: /software consulting in minneapolis/i })
+ ).toBeInTheDocument()
+ expect(screen.getByText(/nearly 20 years/i)).toBeInTheDocument()
+ })
+
+ it('covers development, architecture, and rescue offerings', () => {
+ render()
+
+ expect(
+ screen.getByRole('heading', { name: /custom software development/i })
+ ).toBeInTheDocument()
+ expect(
+ screen.getByRole('heading', { name: /architecture & technical strategy/i })
+ ).toBeInTheDocument()
+ expect(screen.getByRole('heading', { name: /rescue & modernization/i })).toBeInTheDocument()
+ expect(screen.getByRole('heading', { name: /how engagements work/i })).toBeInTheDocument()
+ })
+
+ it('has a distinct Twin Cities paragraph about onsite collaboration', () => {
+ render()
+
+ expect(
+ screen.getByRole('heading', { name: /working together in the twin cities/i })
+ ).toBeInTheDocument()
+ expect(screen.getByText(/anywhere in the minneapolis–st\. paul metro/i)).toBeInTheDocument()
+ })
+
+ it('answers the cost question honestly in the FAQ', () => {
+ render()
+
+ expect(
+ screen.getByText(/how much does a software consultant cost in minneapolis\?/i)
+ ).toBeInTheDocument()
+ expect(screen.getByText(/what does a software consultant actually do\?/i)).toBeInTheDocument()
+ expect(screen.getByText(/do you work onsite in the twin cities\?/i)).toBeInTheDocument()
+ })
+
+ it('links the related-services triangle and the contact CTA', () => {
+ render()
+
+ expect(screen.getByRole('link', { name: /ai & agent engineering/i })).toHaveAttribute(
+ 'href',
+ '/services/ai-consulting'
+ )
+ expect(screen.getByRole('link', { name: /engineering practice improvement/i })).toHaveAttribute(
+ 'href',
+ '/services/engineering-practice-improvement'
+ )
+ expect(screen.getByRole('link', { name: /book a free discovery call/i })).toHaveAttribute(
+ 'href',
+ '/contact'
+ )
+ })
+
+ it('emits Service, FAQPage, and breadcrumb structured data', () => {
+ const { container } = render()
+
+ const types = Array.from(container.querySelectorAll('script[type="application/ld+json"]')).map(
+ script => JSON.parse(script.textContent || '{}')['@type']
+ )
+ expect(types).toContain('Service')
+ expect(types).toContain('FAQPage')
+ expect(types).toContain('BreadcrumbList')
+ })
+
+ it('has exactly one h1 and a coherent h2 set', () => {
+ render()
+
+ expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1)
+ expect(screen.getAllByRole('heading', { level: 2 })).toHaveLength(8)
+ })
+
+ it('is accessible', async () => {
+ const { container } = render()
+ await axeTest(container)
+ })
+
+ it('targets "software consultant minneapolis" in metadata', async () => {
+ const { metadata } = await import('./page')
+ expect(metadata.title).toBe('Software Consulting in Minneapolis')
+ expect(metadata.description).toContain('Minneapolis')
+ expect(metadata.description).toContain('Twin Cities')
+ expect(metadata.alternates?.canonical).toBe('/services/software-consulting')
+ })
+})
diff --git a/app/(main)/services/software-consulting/page.tsx b/app/(main)/services/software-consulting/page.tsx
new file mode 100644
index 0000000..561f362
--- /dev/null
+++ b/app/(main)/services/software-consulting/page.tsx
@@ -0,0 +1,187 @@
+import { faqSchema, serviceSchema } from 'lib/structured-data'
+import type { Metadata } from 'next'
+import Link from 'next/link'
+import type React from 'react'
+import Breadcrumb from '../../../components/breadcrumb'
+import JsonLd from '../../../components/structured-data'
+
+export const metadata: Metadata = {
+ title: 'Software Consulting in Minneapolis',
+ description:
+ 'Hands-on software consulting in Minneapolis — architecture, custom software development, and legacy rescue for Twin Cities teams. Free discovery call.',
+ keywords: [
+ 'software consultant minneapolis',
+ 'software consulting twin cities',
+ 'custom software development',
+ 'software architecture consultant',
+ 'legacy software modernization',
+ ],
+ alternates: { canonical: '/services/software-consulting' },
+ openGraph: {
+ title: 'Software Consulting in Minneapolis — Bidwell Consulting',
+ description:
+ 'Hands-on software consulting — architecture, custom development, and legacy rescue for Twin Cities teams.',
+ type: 'website',
+ },
+}
+
+const faqs = [
+ {
+ question: 'What does a software consultant actually do?',
+ answer:
+ 'Whatever moves your software forward: designing systems, writing code, reviewing architecture, untangling legacy projects, or advising your team on hard technical decisions. Unlike an agency, you work directly with me — the person doing the thinking is the person doing the work.',
+ },
+ {
+ question: 'How much does a software consultant cost in Minneapolis?',
+ answer:
+ 'Independent consultants in the Twin Cities typically run from the low hundreds per hour, with fixed-scope projects priced by outcome rather than time. The honest answer depends on scope and urgency — the free discovery call exists so I can give you a real number instead of a range.',
+ },
+ {
+ question: 'Can you take over an existing or legacy codebase?',
+ answer:
+ "Yes — rescue work is a specialty. I start with an assessment of what's actually there, stabilize the riskiest parts, and lay out a modernization path you can execute incrementally instead of betting on a rewrite.",
+ },
+ {
+ question: 'Do you write the code yourself or just advise?',
+ answer:
+ 'Both, and you choose the mix. Some clients want working software delivered; others want architecture reviews and a sounding board for their team. Either way I stay hands-on enough that my advice survives contact with a compiler.',
+ },
+ {
+ question: 'Do you work with startups and small businesses?',
+ answer:
+ 'Yes. Small teams are where a single experienced consultant beats an agency: no account managers, no handoffs, and scope shaped to what you can actually maintain after I leave.',
+ },
+ {
+ question: 'Do you work onsite in the Twin Cities?',
+ answer:
+ 'For architecture sessions, whiteboarding, and workshops — gladly, anywhere in the metro. Day-to-day build work usually runs remote, which keeps it efficient for both of us.',
+ },
+]
+
+export default function SoftwareConsultingPage(): React.JSX.Element {
+ return (
+
+
+
+
+
+
+ Software Consulting in Minneapolis
+
+
+ I'm Jordan Winters, an independent software consultant with nearly 20 years of building
+ and leading. I work hands-on: I design systems, write code, and leave your team with
+ software — and decisions — that hold up. No account managers, no junior handoffs.
+
+
+
+
Custom Software Development
+
+ Prototypes that prove an idea, internal tools that remove a bottleneck, integrations that
+ make two systems finally talk, MVPs scoped to learn something real. I build custom
+ software sized for a solo expert to deliver well — which is exactly the size where an
+ agency's overhead stops making sense.
+
+
+
+
+
+ Architecture & Technical Strategy
+
+
+ Architecture reviews before you commit a year of budget. Build-versus-buy calls with the
+ trade-offs stated plainly. Technical due diligence when you're acquiring or
+ investing. System design that starts from your constraints — team size, budget, and what
+ you can operate at 2 a.m. — instead of whatever is trending.
+
+
+
+
+
Rescue & Modernization
+
+ Inherited a codebase nobody understands? Watched a project stall past its second deadline?
+ I take over troubled and legacy systems, stabilize what's risky, and chart an
+ incremental modernization path — because “rewrite it” is usually the most
+ expensive sentence in software.
+
+
+
+
+
How Engagements Work
+
+ Three shapes: a fixed-scope assessment when you need clarity before
+ committing, a project when you need something built, or an ongoing{' '}
+ advisory arrangement when your team needs a senior technical voice on
+ call. Every engagement starts with a free 30-minute discovery call and a written scope —
+ you'll always know what you're paying for and what done looks like.
+
+
+
+
+
+ Working Together in the Twin Cities
+
+
+ I'm based in Minneapolis. For the parts of software work that go better in a room —
+ architecture sessions, whiteboarding, team workshops — I'll come to you anywhere in
+ the Minneapolis–St. Paul metro. The day-to-day build work runs remote, so distance never
+ slows a project down.
+
+
+
+
+
Frequently Asked Questions
+
+ {faqs.map(faq => (
+
+
{faq.question}
+
{faq.answer}
+
+ ))}
+
+
+
+
+
Related Services
+
+ Building with LLMs or coding agents? See{' '}
+
+ AI & agent engineering
+
+ . Team shipping slower than it should? See{' '}
+
+ engineering practice improvement
+
+ .
+
+
+
+
+
Start a Conversation
+
+ Tell me what you're building or what's stuck.{' '}
+
+ Book a free discovery call
+ {' '}
+ — 30 minutes, no commitment.
+
+
+
+ )
+}
diff --git a/app/sitemap.test.ts b/app/sitemap.test.ts
index b5e78b3..7e6165f 100644
--- a/app/sitemap.test.ts
+++ b/app/sitemap.test.ts
@@ -23,9 +23,13 @@ describe('sitemap', () => {
}
})
- it('includes the career-coaching sales page but never its redirect stub', async () => {
+ it('includes the services hub and all four service pages, never the redirect stub', async () => {
const urls = (await entries()).map(route => route.url)
+ expect(urls).toContain('https://bidwell.info/services')
+ expect(urls).toContain('https://bidwell.info/services/software-consulting')
+ expect(urls).toContain('https://bidwell.info/services/ai-consulting')
+ expect(urls).toContain('https://bidwell.info/services/engineering-practice-improvement')
expect(urls).toContain('https://bidwell.info/services/career-coaching')
expect(urls).not.toContain('https://bidwell.info/career-guidance')
})
diff --git a/app/sitemap.ts b/app/sitemap.ts
index cb8bac2..f88cc5d 100644
--- a/app/sitemap.ts
+++ b/app/sitemap.ts
@@ -17,6 +17,10 @@ export const dynamic = 'force-static'
export default async function sitemap() {
const routes = [
{ url: baseUrl, priority: 1.0 },
+ { url: `${baseUrl}/services`, priority: 0.8 },
+ { url: `${baseUrl}/services/software-consulting`, priority: 0.9 },
+ { url: `${baseUrl}/services/ai-consulting`, priority: 0.9 },
+ { url: `${baseUrl}/services/engineering-practice-improvement`, priority: 0.9 },
{ url: `${baseUrl}/services/career-coaching`, priority: 0.9 },
{ url: `${baseUrl}/blog`, priority: 0.7 },
...posts.map(post => ({
From 5066c1fe22f5def6c2567e8ebce9aa8c6752b9de Mon Sep 17 00:00:00 2001
From: Jordan Winters
Date: Tue, 21 Jul 2026 17:26:29 -0500
Subject: [PATCH 3/8] feat(seo): homepage rewrite + about + contact pages
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Home becomes the metro category page: 'Twin Cities Software Consulting'
title (keyword-first for a weak-brand domain), 'Software Consulting in
the Twin Cities' h1, founder hero linking /about, discovery-call CTA,
four service cards from a shared config (also consumed by the hub), an
AI-native differentiator section citing the agent-coordination essay,
and exactly one local trust line. /about is the E-E-A-T anchor — Jordan
Winters, Minneapolis, beliefs, solo working model, proof links — and the
Person schema url now points there. /contact ships LinkedIn-primary
(email lands in the gated commit) with the free-discovery-call framing
and the NAP line. Sitemap gains /about and /contact.
Co-Authored-By: Claude Fable 5
---
app/(main)/about/opengraph-image.tsx | 10 ++
app/(main)/about/page.test.tsx | 73 ++++++++++++++
app/(main)/about/page.tsx | 124 ++++++++++++++++++++++++
app/(main)/contact/opengraph-image.tsx | 10 ++
app/(main)/contact/page.test.tsx | 56 +++++++++++
app/(main)/contact/page.tsx | 60 ++++++++++++
app/(main)/opengraph-image.tsx | 2 +-
app/(main)/page.test.tsx | 96 ++++++++++++++----
app/(main)/page.tsx | 120 +++++++++++++++++------
app/(main)/services/page.tsx | 26 +----
app/components/structured-data.test.tsx | 1 +
app/config/services.ts | 29 ++++++
app/sitemap.test.ts | 7 ++
app/sitemap.ts | 2 +
lib/structured-data.ts | 2 +-
15 files changed, 545 insertions(+), 73 deletions(-)
create mode 100644 app/(main)/about/opengraph-image.tsx
create mode 100644 app/(main)/about/page.test.tsx
create mode 100644 app/(main)/about/page.tsx
create mode 100644 app/(main)/contact/opengraph-image.tsx
create mode 100644 app/(main)/contact/page.test.tsx
create mode 100644 app/(main)/contact/page.tsx
create mode 100644 app/config/services.ts
diff --git a/app/(main)/about/opengraph-image.tsx b/app/(main)/about/opengraph-image.tsx
new file mode 100644
index 0000000..6ff7a6d
--- /dev/null
+++ b/app/(main)/about/opengraph-image.tsx
@@ -0,0 +1,10 @@
+import { OG_SIZE, ogCard } from '../../../lib/og-image'
+
+export const dynamic = 'force-static'
+export const alt = 'Jordan Winters — Software Consultant, Minneapolis'
+export const size = OG_SIZE
+export const contentType = 'image/png'
+
+export default function OpengraphImage() {
+ return ogCard('Jordan Winters', 'Software Consultant · Minneapolis')
+}
diff --git a/app/(main)/about/page.test.tsx b/app/(main)/about/page.test.tsx
new file mode 100644
index 0000000..6b09968
--- /dev/null
+++ b/app/(main)/about/page.test.tsx
@@ -0,0 +1,73 @@
+import { axeTest, render, screen } from 'lib/test-utils'
+import { describe, expect, it } from 'vitest'
+import AboutPage from './page'
+
+describe('About Page', () => {
+ it('names Jordan Winters with the Minneapolis base and experience claim', () => {
+ render()
+
+ expect(
+ screen.getByRole('heading', { level: 1, name: /about jordan winters/i })
+ ).toBeInTheDocument()
+ expect(screen.getByText(/live and work in minneapolis/i)).toBeInTheDocument()
+ expect(screen.getByText(/nearly 20 years/i)).toBeInTheDocument()
+ })
+
+ it('links the public proof surfaces safely', () => {
+ render()
+
+ const linkedin = screen.getByRole('link', { name: /linkedin/i })
+ expect(linkedin).toHaveAttribute('href', 'https://www.linkedin.com/in/wintersjordan/')
+ const github = screen.getByRole('link', { name: /github/i })
+ expect(github).toHaveAttribute('href', 'https://github.com/dralgorhythm')
+ for (const link of [linkedin, github]) {
+ expect(link).toHaveAttribute('target', '_blank')
+ expect(link).toHaveAttribute('rel', 'noopener noreferrer')
+ }
+ expect(screen.getByRole('link', { name: /coordinating ai coding agents/i })).toHaveAttribute(
+ 'href',
+ '/blog/agent-coordination'
+ )
+ })
+
+ it('states beliefs and the solo working model', () => {
+ render()
+
+ expect(screen.getByRole('heading', { name: /what i believe/i })).toBeInTheDocument()
+ expect(screen.getByRole('heading', { name: /how i work/i })).toBeInTheDocument()
+ expect(screen.getByText(/solo, by design/i)).toBeInTheDocument()
+ })
+
+ it('links all four services and the contact page from Work With Me', () => {
+ render()
+
+ expect(screen.getByRole('link', { name: /software consulting/i })).toHaveAttribute(
+ 'href',
+ '/services/software-consulting'
+ )
+ expect(screen.getByRole('link', { name: /career coaching/i })).toHaveAttribute(
+ 'href',
+ '/services/career-coaching'
+ )
+ expect(screen.getByRole('link', { name: /get in touch/i })).toHaveAttribute('href', '/contact')
+ })
+
+ it('has exactly one h1 and a coherent h2 set', () => {
+ render()
+
+ expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1)
+ expect(screen.getAllByRole('heading', { level: 2 })).toHaveLength(5)
+ })
+
+ it('is accessible', async () => {
+ const { container } = render()
+ await axeTest(container)
+ })
+
+ it('has entity-page metadata', async () => {
+ const { metadata } = await import('./page')
+ expect(metadata.title).toBe('About Jordan Winters')
+ expect(metadata.description).toContain('Minneapolis-based software consultant')
+ expect(metadata.alternates?.canonical).toBe('/about')
+ })
+})
diff --git a/app/(main)/about/page.tsx b/app/(main)/about/page.tsx
new file mode 100644
index 0000000..f828c41
--- /dev/null
+++ b/app/(main)/about/page.tsx
@@ -0,0 +1,124 @@
+import type { Metadata } from 'next'
+import Link from 'next/link'
+import type React from 'react'
+import { serviceEntries } from '../../config/services'
+
+export const metadata: Metadata = {
+ title: 'About Jordan Winters',
+ description:
+ 'Jordan Winters is a Minneapolis-based software consultant with nearly 20 years in engineering, architecture, and practice improvement. Serving the Twin Cities.',
+ alternates: { canonical: '/about' },
+ openGraph: {
+ title: 'Jordan Winters — Software Consultant, Minneapolis',
+ description:
+ 'Nearly 20 years in engineering, architecture, and practice improvement. Based in Minneapolis, serving the Twin Cities.',
+ type: 'profile',
+ },
+}
+
+export default function AboutPage(): React.JSX.Element {
+ return (
+
+
About Jordan Winters
+
+
+
Hi, I'm Jordan
+
+ I live and work in Minneapolis and I've spent nearly 20 years building software and
+ building the teams that build software — as an engineer, an architect, and a leader of
+ Engineering Practice Improvement. Bidwell Consulting is where I do that work directly for
+ companies across the Twin Cities metro, and remotely everywhere else.
+
+
+ You can verify most of this the same way I would:{' '}
+
+ LinkedIn
+ {' '}
+ for the career,{' '}
+
+ GitHub
+ {' '}
+ for the code, and my essay on{' '}
+
+ coordinating AI coding agents
+ {' '}
+ for how I think.
+
+
+
+
+
What I Believe
+
+
+ Engineering exists to serve outcomes — teams do their best work when they can run real
+ experiments and see the results land with customers.
+
+
+ Speed and stability reinforce each other. Quality gates aren't a tax on velocity;
+ they're what makes sustained velocity possible.
+
+
+ You build it, you own it. Ownership — not process theater — is what keeps software
+ healthy after the launch party.
+
+
+
+
+
+
How I Work
+
+ Solo, by design. The person you meet on the discovery call is the person who designs the
+ system, writes the code, and answers the hard questions — there are no juniors to hand you
+ off to. That keeps scope honest, communication direct, and quality personal.
+
+
+
+
+
Beyond Work
+
+ When I'm not shipping software I'm usually making music — you can hear what that
+ sounds like on{' '}
+
+ SoundCloud
+
+ . Minneapolis is home, and the coffee-meeting offer is real.
+
+
+
+
+
Work With Me
+
+ {serviceEntries.map(service => (
+
+
+ {service.title}
+
+
+ ))}
+
+
+ Or just{' '}
+
+ get in touch
+ {' '}
+ — the discovery call is free.
+
+
+
+ )
+}
diff --git a/app/(main)/contact/opengraph-image.tsx b/app/(main)/contact/opengraph-image.tsx
new file mode 100644
index 0000000..3f19fcf
--- /dev/null
+++ b/app/(main)/contact/opengraph-image.tsx
@@ -0,0 +1,10 @@
+import { OG_SIZE, ogCard } from '../../../lib/og-image'
+
+export const dynamic = 'force-static'
+export const alt = 'Contact Bidwell Consulting'
+export const size = OG_SIZE
+export const contentType = 'image/png'
+
+export default function OpengraphImage() {
+ return ogCard('Contact', 'Bidwell Consulting · Minneapolis, MN')
+}
diff --git a/app/(main)/contact/page.test.tsx b/app/(main)/contact/page.test.tsx
new file mode 100644
index 0000000..a20da9a
--- /dev/null
+++ b/app/(main)/contact/page.test.tsx
@@ -0,0 +1,56 @@
+import { axeTest, render, screen } from 'lib/test-utils'
+import { describe, expect, it } from 'vitest'
+import ContactPage from './page'
+
+describe('Contact Page', () => {
+ it('renders the heading with the LinkedIn CTA', () => {
+ render()
+
+ expect(screen.getByRole('heading', { level: 1, name: /contact/i })).toBeInTheDocument()
+ const cta = screen.getByRole('link', { name: /message me on linkedin/i })
+ expect(cta).toHaveAttribute('href', 'https://www.linkedin.com/in/wintersjordan/')
+ expect(cta).toHaveAttribute('target', '_blank')
+ expect(cta).toHaveAttribute('rel', 'noopener noreferrer')
+ })
+
+ it('explains the free discovery call honestly', () => {
+ render()
+
+ expect(screen.getByRole('heading', { name: /free discovery call/i })).toBeInTheDocument()
+ expect(screen.getByText(/no commitment, no pitch/i)).toBeInTheDocument()
+ })
+
+ it('tells prospects what to include', () => {
+ render()
+
+ expect(screen.getByRole('heading', { name: /what to include/i })).toBeInTheDocument()
+ expect(screen.getByText(/rough timeline/i)).toBeInTheDocument()
+ })
+
+ it('shows the NAP line exactly once', () => {
+ render()
+
+ expect(
+ screen.getAllByText(/bidwell consulting · minneapolis, mn · serving the twin cities metro/i)
+ ).toHaveLength(1)
+ })
+
+ it('has exactly one h1 and a coherent h2 set', () => {
+ render()
+
+ expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1)
+ expect(screen.getAllByRole('heading', { level: 2 })).toHaveLength(3)
+ })
+
+ it('is accessible', async () => {
+ const { container } = render()
+ await axeTest(container)
+ })
+
+ it('has navigational metadata with the local line', async () => {
+ const { metadata } = await import('./page')
+ expect(metadata.title).toBe('Contact')
+ expect(metadata.description).toContain('Minneapolis')
+ expect(metadata.alternates?.canonical).toBe('/contact')
+ })
+})
diff --git a/app/(main)/contact/page.tsx b/app/(main)/contact/page.tsx
new file mode 100644
index 0000000..a3b3408
--- /dev/null
+++ b/app/(main)/contact/page.tsx
@@ -0,0 +1,60 @@
+import type { Metadata } from 'next'
+import type React from 'react'
+
+export const metadata: Metadata = {
+ title: 'Contact',
+ description:
+ 'Reach Bidwell Consulting — message me on LinkedIn or book a free 30-minute discovery call. Based in Minneapolis, serving the Twin Cities metro and remote clients.',
+ alternates: { canonical: '/contact' },
+ openGraph: {
+ title: 'Contact Bidwell Consulting — Minneapolis, MN',
+ description:
+ 'Message me on LinkedIn or book a free 30-minute discovery call. Minneapolis-based, remote-friendly.',
+ type: 'website',
+ },
+}
+
+export default function ContactPage(): React.JSX.Element {
+ return (
+
+
Contact
+
+
+
Get in Touch
+
+ The fastest way to reach me is a message on LinkedIn — I read everything.
+
+ Every engagement starts with a free 30-minute conversation — no commitment, no pitch.
+ We'll talk through what you're working on and whether I'm the right person
+ to help. If I'm not, I'll say so and point you somewhere better.
+
+
+
+
+
What to Include
+
+
What you're building or what's stuck — a few sentences is plenty
+
Rough timeline: exploring, this quarter, or on fire
+
How you'd like to work: project, assessment, or ongoing advice
+
+
+
+
+ Bidwell Consulting · Minneapolis, MN · Serving the Twin Cities metro · Remote-friendly
+
+
+ )
+}
diff --git a/app/(main)/opengraph-image.tsx b/app/(main)/opengraph-image.tsx
index ea6a715..77b24e3 100644
--- a/app/(main)/opengraph-image.tsx
+++ b/app/(main)/opengraph-image.tsx
@@ -12,5 +12,5 @@ export const contentType = 'image/png'
* so the image file must be colocated in this segment.
*/
export default function OpengraphImage() {
- return ogCard('Bidwell Consulting', 'Software Engineering & Organizational Consulting')
+ return ogCard('Bidwell Consulting', 'Software Consulting in the Twin Cities')
}
diff --git a/app/(main)/page.test.tsx b/app/(main)/page.test.tsx
index e0f5319..d506599 100644
--- a/app/(main)/page.test.tsx
+++ b/app/(main)/page.test.tsx
@@ -1,29 +1,90 @@
import { axeTest, render, screen } from 'lib/test-utils'
+import { describe, expect, it } from 'vitest'
import HomePage from './page'
-describe('Home Page Integration', () => {
- it('renders the main value proposition', () => {
+describe('Home Page', () => {
+ it('leads with the Twin Cities h1 and the founder hero', () => {
render()
expect(
- screen.getByRole('heading', { level: 1, name: /bidwell consulting/i })
+ screen.getByRole('heading', { level: 1, name: /software consulting in the twin cities/i })
).toBeInTheDocument()
- expect(screen.getByText(/welcome to bidwell consulting/i)).toBeInTheDocument()
- expect(screen.getByText(/thoughts on problem solving/i)).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: /jordan winters/i })).toHaveAttribute('href', '/about')
+ expect(screen.getByText(/nearly 20 years/i)).toBeInTheDocument()
})
- it('displays key business services', () => {
+ it('shows the primary discovery-call CTA', () => {
render()
- expect(screen.getByText(/Software engineering/i)).toBeInTheDocument()
- expect(screen.getByText(/Organizational consulting/i)).toBeInTheDocument()
- expect(screen.getByText(/Full-stack problem solving/i)).toBeInTheDocument()
- expect(screen.getByText(/Career coaching/i)).toBeInTheDocument()
+ const ctas = screen.getAllByRole('link', { name: /book a free discovery call/i })
+ expect(ctas.length).toBeGreaterThanOrEqual(1)
+ for (const cta of ctas) {
+ expect(cta).toHaveAttribute('href', '/contact')
+ }
})
- it('includes the expertise section', () => {
+ it('links all four service cards plus the hub', () => {
render()
- expect(screen.getByRole('heading', { name: /expertise & services/i })).toBeInTheDocument()
+
+ expect(screen.getByRole('link', { name: /software consulting/i })).toHaveAttribute(
+ 'href',
+ '/services/software-consulting'
+ )
+ expect(screen.getByRole('link', { name: /ai & agent engineering/i })).toHaveAttribute(
+ 'href',
+ '/services/ai-consulting'
+ )
+ expect(screen.getByRole('link', { name: /engineering practice improvement/i })).toHaveAttribute(
+ 'href',
+ '/services/engineering-practice-improvement'
+ )
+ expect(screen.getByRole('link', { name: /career coaching/i })).toHaveAttribute(
+ 'href',
+ '/services/career-coaching'
+ )
+ expect(screen.getByRole('link', { name: /all services/i })).toHaveAttribute('href', '/services')
+ })
+
+ it('differentiates with AI-native proof linking the essay', () => {
+ render()
+
+ expect(screen.getByRole('heading', { name: /ai-native consulting/i })).toBeInTheDocument()
+ expect(
+ screen.getByRole('link', { name: /how i coordinate ai coding agents/i })
+ ).toHaveAttribute('href', '/blog/agent-coordination')
+ })
+
+ it('carries exactly one local trust line', () => {
+ render()
+
+ expect(
+ screen.getByText(/based in minneapolis and serving the twin cities metro/i)
+ ).toBeInTheDocument()
+ })
+
+ it('keeps the solo first-person voice (no "our team")', () => {
+ const { container } = render()
+
+ expect(container.textContent).not.toMatch(/our team/i)
+ })
+
+ it('opens external links safely', () => {
+ render()
+
+ const externalLinks = screen
+ .getAllByRole('link')
+ .filter(link => (link.getAttribute('href') ?? '').startsWith('http'))
+ for (const link of externalLinks) {
+ expect(link).toHaveAttribute('target', '_blank')
+ expect(link).toHaveAttribute('rel', 'noopener noreferrer')
+ }
+ })
+
+ it('has exactly one h1 and a coherent h2 set', () => {
+ render()
+
+ expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1)
+ expect(screen.getAllByRole('heading', { level: 2 })).toHaveLength(3)
})
it('is accessible', async () => {
@@ -31,10 +92,11 @@ describe('Home Page Integration', () => {
await axeTest(container)
})
- it('has a valid heading hierarchy', () => {
- render()
- // Should have exactly one h1
- const h1Elements = screen.getAllByRole('heading', { level: 1 })
- expect(h1Elements).toHaveLength(1)
+ it('targets "software consulting twin cities" keyword-first', async () => {
+ const { metadata } = await import('./page')
+ expect(metadata.title).toBe('Twin Cities Software Consulting')
+ expect(metadata.description).toContain('Minneapolis')
+ expect(metadata.description).toContain('Twin Cities')
+ expect(metadata.alternates?.canonical).toBe('/')
})
})
diff --git a/app/(main)/page.tsx b/app/(main)/page.tsx
index db5ae85..54fdcae 100644
--- a/app/(main)/page.tsx
+++ b/app/(main)/page.tsx
@@ -1,25 +1,24 @@
import type { Metadata } from 'next'
+import Link from 'next/link'
import type React from 'react'
+import { serviceEntries } from '../config/services'
export const metadata: Metadata = {
- title: 'Bidwell Consulting',
+ title: 'Twin Cities Software Consulting',
description:
- 'Expert software engineering and organizational consulting firm - solving problems, designing systems, and optimizing processes.',
+ 'Independent software consultant in Minneapolis — architecture, AI engineering, and practice improvement for Twin Cities teams. Book a free discovery call.',
keywords: [
+ 'software consulting twin cities',
+ 'software consultant minneapolis',
+ 'technology consulting minneapolis',
+ 'independent software consultant',
'bidwell consulting',
- 'software engineering services',
- 'organizational consulting',
- 'portfolio',
- 'technical consulting',
- 'system architecture',
- 'business optimization',
- 'full-stack development',
],
alternates: { canonical: '/' },
openGraph: {
- title: 'Bidwell Consulting',
+ title: 'Bidwell Consulting — Software Consulting in the Twin Cities',
description:
- 'Expert software engineer and organizational consultant, specializing in problem solving, system design, and process optimization. This portfolio site showcases innovative technical solutions and demonstrates our approach to complex problem solving.',
+ 'Independent software consultant in Minneapolis — architecture, AI engineering, and practice improvement for Twin Cities teams.',
type: 'website',
},
}
@@ -27,28 +26,89 @@ export const metadata: Metadata = {
export default function Page(): React.JSX.Element {
return (
-
Bidwell Consulting
+
+ Software Consulting in the Twin Cities
+
-
-
- Welcome to Bidwell Consulting, I look forward to working with you! Here, you can find my
- thoughts on problem solving.
-
+
+ I'm{' '}
+
+ Jordan Winters
+
+ , an independent software consultant in Minneapolis. I help companies design systems, ship
+ software, and build engineering practices that hold up — drawing on nearly 20 years of
+ building and leading.
+
+
+
+
+ Book a free discovery call
+
+
-
Expertise & Services
-
-
Software engineering and system architecture
-
Organizational consulting and process optimization
+ I don't just advise on AI — I work this way every day, coordinating coding agents to
+ deliver real software, and I publish{' '}
+
+ how I coordinate AI coding agents
+
+ . If you want AI in your engineering practice, you want someone who has already made it
+ work in their own.
+
+
+ Based in Minneapolis and serving the Twin Cities metro — in person when it helps, remote
+ when it doesn't.
+
+
+
+
+
Start a Conversation
+
+ Tell me what you're building, what's stuck, or where you want to be.{' '}
+
+ Book a free discovery call
+ {' '}
+ or reach out on{' '}
+
+ LinkedIn
+
+ .
+
)
diff --git a/app/(main)/services/page.tsx b/app/(main)/services/page.tsx
index 0df4b70..de26def 100644
--- a/app/(main)/services/page.tsx
+++ b/app/(main)/services/page.tsx
@@ -2,6 +2,7 @@ import type { Metadata } from 'next'
import Link from 'next/link'
import type React from 'react'
import Breadcrumb from '../../components/breadcrumb'
+import { serviceEntries } from '../../config/services'
export const metadata: Metadata = {
title: 'Software & AI Consulting Services',
@@ -10,29 +11,6 @@ export const metadata: Metadata = {
alternates: { canonical: '/services' },
}
-const services = [
- {
- title: 'Software Consulting',
- href: '/services/software-consulting',
- description: 'Architecture, custom software development, and rescue work — hands-on.',
- },
- {
- title: 'AI & Agent Engineering',
- href: '/services/ai-consulting',
- description: 'Pragmatic AI adoption: LLM integration, custom agents, coding-agent workflows.',
- },
- {
- title: 'Engineering Practice Improvement',
- href: '/services/engineering-practice-improvement',
- description: 'Ship faster with healthier DevOps, agile delivery, and review culture.',
- },
- {
- title: 'Career Coaching',
- href: '/services/career-coaching',
- description: '1-on-1 coaching for engineers and tech leaders — resumes, interviews, moves.',
- },
-]
-
export default function ServicesPage(): React.JSX.Element {
return (
@@ -54,7 +32,7 @@ export default function ServicesPage(): React.JSX.Element {
Twin Cities metro, and remotely everywhere else.
- {services.map(service => (
+ {serviceEntries.map(service => (
{
expect(schema['@type']).toBe('Person')
expect(schema['@id']).toBe('https://bidwell.info/#person')
expect(schema.name).toBe('Jordan Winters')
+ expect(schema.url).toBe('https://bidwell.info/about')
expect(schema.worksFor).toEqual({ '@id': 'https://bidwell.info/#organization' })
expect(schema.sameAs).toContain('https://www.linkedin.com/in/wintersjordan/')
})
diff --git a/app/config/services.ts b/app/config/services.ts
new file mode 100644
index 0000000..17b4f0d
--- /dev/null
+++ b/app/config/services.ts
@@ -0,0 +1,29 @@
+/** The four consulting offerings — single source for home cards, the services hub, and the footer. */
+export interface ServiceEntry {
+ title: string
+ href: string
+ description: string
+}
+
+export const serviceEntries: ServiceEntry[] = [
+ {
+ title: 'Software Consulting',
+ href: '/services/software-consulting',
+ description: 'Architecture, custom software development, and rescue work — hands-on.',
+ },
+ {
+ title: 'AI & Agent Engineering',
+ href: '/services/ai-consulting',
+ description: 'Pragmatic AI adoption: LLM integration, custom agents, coding-agent workflows.',
+ },
+ {
+ title: 'Engineering Practice Improvement',
+ href: '/services/engineering-practice-improvement',
+ description: 'Ship faster with healthier DevOps, agile delivery, and review culture.',
+ },
+ {
+ title: 'Career Coaching',
+ href: '/services/career-coaching',
+ description: '1-on-1 coaching for engineers and tech leaders — resumes, interviews, moves.',
+ },
+]
diff --git a/app/sitemap.test.ts b/app/sitemap.test.ts
index 7e6165f..1d11ba0 100644
--- a/app/sitemap.test.ts
+++ b/app/sitemap.test.ts
@@ -34,6 +34,13 @@ describe('sitemap', () => {
expect(urls).not.toContain('https://bidwell.info/career-guidance')
})
+ it('includes the about and contact pages', async () => {
+ const urls = (await entries()).map(route => route.url)
+
+ expect(urls).toContain('https://bidwell.info/about')
+ expect(urls).toContain('https://bidwell.info/contact')
+ })
+
it('includes active experiments and excludes noindexed coming-soon stubs', async () => {
const urls = (await entries()).map(route => route.url)
diff --git a/app/sitemap.ts b/app/sitemap.ts
index f88cc5d..a1305b3 100644
--- a/app/sitemap.ts
+++ b/app/sitemap.ts
@@ -22,6 +22,8 @@ export default async function sitemap() {
{ url: `${baseUrl}/services/ai-consulting`, priority: 0.9 },
{ url: `${baseUrl}/services/engineering-practice-improvement`, priority: 0.9 },
{ url: `${baseUrl}/services/career-coaching`, priority: 0.9 },
+ { url: `${baseUrl}/about`, priority: 0.6 },
+ { url: `${baseUrl}/contact`, priority: 0.7 },
{ url: `${baseUrl}/blog`, priority: 0.7 },
...posts.map(post => ({
url: `${baseUrl}/blog/${post.slug}`,
diff --git a/lib/structured-data.ts b/lib/structured-data.ts
index b7a48c3..61f4064 100644
--- a/lib/structured-data.ts
+++ b/lib/structured-data.ts
@@ -77,7 +77,7 @@ export function personSchema(): WithContext {
'@id': PERSON_ID,
name: siteConfig.founder.name,
jobTitle: siteConfig.founder.jobTitle,
- url: baseUrl,
+ url: absoluteUrl('/about'),
worksFor: { '@id': ORGANIZATION_ID },
sameAs: [...siteConfig.founder.sameAs],
knowsAbout: [
From ba66221a3492f4cb0e697d86e4ce898aa50b9482 Mon Sep 17 00:00:00 2001
From: Jordan Winters
Date: Tue, 21 Jul 2026 17:30:15 -0500
Subject: [PATCH 4/8] feat(seo): nav/footer architecture, sitewide positioning,
llms.txt + RSS
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Nav becomes home/services/coaching/blog/about/contact (experiments moves
to the footer — 12 of 13 are noindexed stubs). The footer grows three
link columns (Services x4 / Site / Connect) plus one plain NAP line,
fixing the sitewide orphaning of a footer that previously held only
social links. Root default title/description flip to the Twin Cities
positioning (deliberately held out of PR1), authors credit Jordan
Winters, and the WebSite/Organization schema description follows via
siteConfig. Blog: index description drops 'the team' voice; the post's
title tag targets 'Coordinating AI Coding Agents' via a new seoTitle
field (h1 unchanged), gains a byline linking /about, repoints its intro
Engineering-Practice-Improvement link from LinkedIn to the service page,
and closes with an ai-consulting pointer. New AI/feed discovery
surfaces: public/llms.txt and a hand-rolled static /rss.xml (linked from
the layout head and blog index).
Co-Authored-By: Claude Fable 5
---
app/(main)/blog/agent-coordination/page.tsx | 23 +++--
app/(main)/blog/page.tsx | 10 +-
app/(main)/blog/posts.ts | 6 +-
app/components/footer.test.tsx | 68 ++++++++----
app/components/footer.tsx | 109 +++++++++++++-------
app/components/nav.test.tsx | 13 +++
app/config/nav.ts | 16 ++-
app/layout.tsx | 29 +++---
app/rss.xml/route.ts | 43 ++++++++
e2e/seo.spec.ts | 12 +++
lib/site-config.ts | 3 +-
public/llms.txt | 20 ++++
12 files changed, 269 insertions(+), 83 deletions(-)
create mode 100644 app/rss.xml/route.ts
create mode 100644 public/llms.txt
diff --git a/app/(main)/blog/agent-coordination/page.tsx b/app/(main)/blog/agent-coordination/page.tsx
index c8989ff..213fe44 100644
--- a/app/(main)/blog/agent-coordination/page.tsx
+++ b/app/(main)/blog/agent-coordination/page.tsx
@@ -1,12 +1,13 @@
import { siteConfig } from 'lib/site-config'
import { blogPostingSchema } from 'lib/structured-data'
import type { Metadata } from 'next'
+import Link from 'next/link'
import Breadcrumb from '../../../components/breadcrumb'
import JsonLd from '../../../components/structured-data'
import { formatPostDate, agentCoordinationPost as post } from '../posts'
export const metadata: Metadata = {
- title: post.title,
+ title: post.seoTitle,
description: post.description,
alternates: { canonical: `/blog/${post.slug}` },
openGraph: {
@@ -33,19 +34,18 @@ export default function AgentCoordinationPost() {
{post.title}
- {formatPostDate(post.publishedAt)}
+
+ Jordan Winters
+ {' '}
+ · Engineering Practice Improvement · {formatPostDate(post.publishedAt)}