diff --git a/app/(main)/about/opengraph-image.tsx b/app/(main)/about/opengraph-image.tsx new file mode 100644 index 0000000..2bccb04 --- /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..bece186 --- /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(/over a decade/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..6b81545 --- /dev/null +++ b/app/(main)/about/page.tsx @@ -0,0 +1,129 @@ +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 over a decade in engineering, infrastructure, and practice improvement. Serving the Twin Cities.', + alternates: { canonical: '/about' }, + openGraph: { + title: 'Jordan Winters - Software Consultant, Minneapolis', + description: + 'Over a decade in engineering, infrastructure, 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 over a decade building software and + building the teams that build software. The short version: I led operations through an + organizational doubling at an ad-tech startup, spent five and a half years as a Staff + DevOps Engineer at NerdWallet (including steering a cloud cost journey that took over + $1.5M out of yearly spend), founded an Observability practice as an Engineering Manager at + a Fortune 500 financial services company, and took an AI-native startup from empty + repository to public beta in six months as its founding engineer. Bidwell Consulting is + where I now 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

+
    +
  • + Customer-focused solutions are easier to deliver when following the Product Operating + Model - teams do their best work when they can run real experiments and watch the + results land with customers. +
  • +
  • + Agile practice helps teams rapidly deliver value and execute more experiments - and + speed and stability reinforce each other along the way. +
  • +
  • + Writing and owning our code, in the tradition of DevOps, helps teams build better + software - you build it, you own it, and it stays healthy after launch. +
  • +
+
+ +
+

How I Work

+

+ Solo by design - the person you meet on the discovery call designs the system, writes the + code, and answers the hard questions himself. No juniors to hand you off to. +

+
+ +
+

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)/blog/agent-coordination/page.tsx b/app/(main)/blog/agent-coordination/page.tsx index c8989ff..bfeefa4 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 + {' '} + · {formatPostDate(post.publishedAt)}

+

+ I help teams put this into practice - see{' '} + + AI & agent engineering consulting + + . +

) } diff --git a/app/(main)/blog/page.tsx b/app/(main)/blog/page.tsx index 46f530f..49509d5 100644 --- a/app/(main)/blog/page.tsx +++ b/app/(main)/blog/page.tsx @@ -5,7 +5,8 @@ import { formatPostDate, posts } from './posts' export const metadata: Metadata = { title: 'Blog', - description: 'Updates and insights from the Bidwell Consulting team.', + description: + 'Notes on software engineering, AI coding agents, and engineering practice from Jordan Winters, a software consultant in Minneapolis.', alternates: { canonical: '/blog' }, } @@ -29,6 +30,13 @@ export default function BlogPage() { ))} +

+ Subscribe via{' '} + + RSS + + . +

) } diff --git a/app/(main)/blog/posts.ts b/app/(main)/blog/posts.ts index 1dae616..4e7061e 100644 --- a/app/(main)/blog/posts.ts +++ b/app/(main)/blog/posts.ts @@ -1,11 +1,13 @@ /** - * Blog post registry — the single source of truth for post metadata. + * Blog post registry - the single source of truth for post metadata. * Pure data with no imports: app/sitemap.ts consumes this and must stay * executable by tsx (health-check generation) outside the Next bundler. */ export interface Post { slug: string title: string + /** Search-targeted when it should differ from the on-page h1. */ + seoTitle: string description: string /** ISO date (YYYY-MM-DD) the post was first published. Never derived from build time. */ publishedAt: string @@ -14,7 +16,9 @@ export interface Post { export const agentCoordinationPost: Post = { slug: 'agent-coordination', title: 'Agent Coordination Structure', - description: 'A framework for managing AI agents within the Bidwell ecosystem.', + seoTitle: 'Coordinating AI Coding Agents', + description: + 'A practical structure for coordinating AI coding agents - critical directives, artifact handoffs, and personas.', publishedAt: '2025-11-27', } 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(<CareerGuidanceRedirect />) -describe('Career Guidance Page', () => { - it('renders the hero section with main heading', () => { - render(<CareerGuidancePage />) - - 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(<CareerGuidancePage />) - - const heroCta = screen.getByRole('link', { name: /get in touch on linkedin/i }) - expect(heroCta).toHaveAttribute('href', LINKEDIN_URL) - }) - - it('displays all service offerings', () => { - render(<CareerGuidancePage />) - - 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(<CareerGuidancePage />) - - 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(<CareerGuidancePage />) - - 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(<CareerGuidancePage />) - - 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(<CareerGuidancePage />) - - 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(<CareerGuidancePage />) - - 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(<CareerGuidancePage />) - - 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(<CareerGuidancePage />) - - // 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(<CareerGuidancePage />) - - expect(container.querySelector('#services')).toBeInTheDocument() - expect(container.querySelector('#contact')).toBeInTheDocument() - }) + it('offers a visible link to the destination', () => { + render(<CareerGuidanceRedirect />) - it('is accessible', async () => { - const { container } = render(<CareerGuidancePage />) - 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 ( - <section> - <JsonLd data={faqSchema(faqs)} /> - {/* Hero Section */} - <div className='mb-12'> - <h1 className='mb-4 text-2xl font-semibold tracking-tighter'>Career Guidance</h1> - <p className='mb-4 text-neutral-700 dark:text-neutral-300'> - 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. - </p> - <div className='mt-6 text-center'> - <a - href='https://www.linkedin.com/in/wintersjordan/' - target='_blank' - rel='noopener noreferrer' - className='inline-block px-6 py-3 bg-black dark:bg-white text-white dark:text-black font-semibold rounded-lg hover:bg-neutral-800 dark:hover:bg-neutral-200 transition-colors' - > - Get In Touch On LinkedIn - </a> - </div> - </div> - - {/* Services Section */} - <div - id='services' - className='mb-12 pb-12 border-b border-neutral-200 dark:border-neutral-800' - > - <h2 className='mb-8 text-2xl font-semibold tracking-tight'>Services Offered</h2> - <div className='grid gap-8 md:grid-cols-2'> - {services.map(service => ( - <div - key={service.title} - className='p-6 border border-neutral-200 dark:border-neutral-800 rounded-lg hover:border-neutral-400 dark:hover:border-neutral-600 transition-colors' - > - <h3 className='mb-3 text-xl font-semibold'>{service.title}</h3> - <p className='mb-4 text-neutral-700 dark:text-neutral-300'>{service.description}</p> - <ul className='space-y-2'> - {service.benefits.map(benefit => ( - <li - key={benefit} - className='flex items-start text-sm text-neutral-600 dark:text-neutral-400' - > - <svg - className='w-5 h-5 mr-2 mt-0.5 flex-shrink-0 text-green-600 dark:text-green-400' - fill='none' - stroke='currentColor' - viewBox='0 0 24 24' - > - <title>Checkmark - - - {benefit} - - ))} - - - ))} - - - - {/* Engagement Options */} -
-

Ways to Work Together

-
- {engagementOptions.map(option => ( -
-

{option.name}

-
- {option.duration} -
-

{option.description}

-
    - {option.features.map(feature => ( -
  • - - Checkmark - - - {feature} -
  • - ))} -
- - Reach Out - -
- ))} -
-
- - {/* FAQ Section */} -
-

Frequently Asked Questions

-
- {faqs.map(faq => ( -
-

{faq.question}

-

{faq.answer}

-
- ))} -
-
+export const metadata = redirectMetadata('/career-guidance') - {/* Contact Section */} -
-

Get in Touch

-

- The best way to reach me is through LinkedIn. Send a message and we can set up a time to - talk. -

- - Connect on LinkedIn - -
- - ) +export default function CareerGuidanceRedirect() { + return } diff --git a/app/(main)/career-guidance/opengraph-image.tsx b/app/(main)/contact/opengraph-image.tsx similarity index 64% rename from app/(main)/career-guidance/opengraph-image.tsx rename to app/(main)/contact/opengraph-image.tsx index e35091a..3f19fcf 100644 --- a/app/(main)/career-guidance/opengraph-image.tsx +++ b/app/(main)/contact/opengraph-image.tsx @@ -1,10 +1,10 @@ import { OG_SIZE, ogCard } from '../../../lib/og-image' export const dynamic = 'force-static' -export const alt = 'Career Guidance' +export const alt = 'Contact Bidwell Consulting' export const size = OG_SIZE export const contentType = 'image/png' export default function OpengraphImage() { - return ogCard('Career Guidance', 'Bidwell Consulting') + 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..08a446d --- /dev/null +++ b/app/(main)/contact/page.test.tsx @@ -0,0 +1,60 @@ +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 email-primary and LinkedIn CTAs', () => { + render() + + expect(screen.getByRole('heading', { level: 1, name: /contact/i })).toBeInTheDocument() + expect(screen.getByRole('link', { name: /^email me$/i })).toHaveAttribute( + 'href', + 'mailto:jordan@bidwell.info' + ) + 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 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..76c3be4 --- /dev/null +++ b/app/(main)/contact/page.tsx @@ -0,0 +1,73 @@ +import { siteConfig } from 'lib/site-config' +import type { Metadata } from 'next' +import type React from 'react' + +export const metadata: Metadata = { + title: 'Contact', + description: + 'Reach Bidwell Consulting - email jordan@bidwell.info or message me on LinkedIn. Based in Minneapolis, serving the Twin Cities metro and remote clients.', + alternates: { canonical: '/contact' }, + openGraph: { + title: 'Contact Bidwell Consulting - Minneapolis, MN', + description: + 'Email jordan@bidwell.info, 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

+

+ Email is best:{' '} + + {siteConfig.email} + + . LinkedIn works too - I read everything. +

+ +
+ +
+

Free Discovery Call

+

+ Every engagement starts with a free 30-minute conversation - 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..c8de5fb 100644 --- a/app/(main)/opengraph-image.tsx +++ b/app/(main)/opengraph-image.tsx @@ -8,9 +8,9 @@ export const contentType = 'image/png' /** * Card for the home page (and any (main) route without its own image). * Required here: the home page defines metadata.openGraph, which replaces - * the inherited root openGraph — including the root segment's file image — + * the inherited root openGraph - including the root segment's file image - * 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..5bcaad6 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(/over a decade/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 eaf6e79..1d9d6b6 100644 --- a/app/(main)/page.tsx +++ b/app/(main)/page.tsx @@ -1,25 +1,25 @@ +import { siteConfig } from 'lib/site-config' 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 - infrastructure, 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 - infrastructure, AI engineering, and practice improvement for Twin Cities teams.', type: 'website', }, } @@ -27,28 +27,93 @@ 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 - over a decade of building and + leading behind it. +

+ +
+ + Book a free discovery call + +
-

Expertise & Services

- +

{service.title}

+

+ {service.description} +

+ + ))} +
+

+ Or browse{' '} + + all services + + . +

+ + +
+

AI-Native Consulting

+

+ I coordinate coding agents to ship real software every day, not just advise on it, and I + publish{' '} + + how I coordinate AI coding agents + + . Bring AI into your engineering practice with someone who has already made it work in his + 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 + + , email{' '} + + {siteConfig.email} + + , or reach out on{' '} + + LinkedIn + + . +

) 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..9fe918c --- /dev/null +++ b/app/(main)/services/ai-consulting/page.tsx @@ -0,0 +1,192 @@ +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 measuring.", + }, + { + 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. I architected the agentic pipelines and multimodal ingestion backend for an AI-native platform that went from empty repository to public beta in six months, and 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. I've contributed production LLM patterns inside a regulated financial-services environment, so compliance-grade data boundaries are familiar ground.", + }, + { + 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. Most recently I took an AI-native + platform from empty repository to public beta in six months, with a cost architecture that + held 90% gross margin on AI-heavy workloads. +

+ +
+

+ 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 and unit + economics from day one, so you know whether it works in production and what it costs per + request - not just whether it demos. +

+
+ +
+

AI Agent Development

+

+ Agents earn their keep on multi-step work: triaging queues, reconciling records, turning + documents and images into structured, queryable data. I design them with explicit + boundaries and human checkpoints wherever a mistake would actually cost you something. +

+
+ +
+

AI-Native Engineering Teams

+

+ Coding agents are the biggest change to software delivery in a decade, and most teams get + 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 - so agents raise your team's output instead of your + incident count. Building that structure is what I do: I've founded an enterprise + Observability practice and stood up the delivery engine for a founding team, then handed + both off running. +

+
+ +
+

How I Approach AI Projects

+

+ I approach AI projects pragmatically. It's 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 + + . If the system around the model needs building too, see{' '} + + software consulting + + . +

+
+ +
+

Start a Conversation

+

+ Tell me about the workflow you think AI could carry.{' '} + + Book a free discovery call + {' '} + - 30 minutes, no commitment. +

+
+
+ ) +} diff --git a/app/(main)/services/career-coaching/opengraph-image.tsx b/app/(main)/services/career-coaching/opengraph-image.tsx new file mode 100644 index 0000000..94275e9 --- /dev/null +++ b/app/(main)/services/career-coaching/opengraph-image.tsx @@ -0,0 +1,10 @@ +import { OG_SIZE, ogCard } from '../../../../lib/og-image' + +export const dynamic = 'force-static' +export const alt = 'Tech Career Coaching' +export const size = OG_SIZE +export const contentType = 'image/png' + +export default function OpengraphImage() { + return ogCard('Tech Career Coaching', 'Bidwell Consulting · Minneapolis') +} diff --git a/app/(main)/services/career-coaching/page.test.tsx b/app/(main)/services/career-coaching/page.test.tsx new file mode 100644 index 0000000..f693195 --- /dev/null +++ b/app/(main)/services/career-coaching/page.test.tsx @@ -0,0 +1,145 @@ +import { axeTest, render, screen } from 'lib/test-utils' +import { describe, expect, it } from 'vitest' +import CareerCoachingPage from './page' + +const LINKEDIN_URL = 'https://www.linkedin.com/in/wintersjordan/' + +describe('Career Coaching Page', () => { + it('renders the hero section with main heading', () => { + render() + + expect( + screen.getByRole('heading', { level: 1, name: /tech career coaching/i }) + ).toBeInTheDocument() + expect(screen.getByText(/coach engineers and tech professionals/i)).toBeInTheDocument() + }) + + it('states the Minneapolis base without making location a blocker', () => { + render() + + expect(screen.getAllByText(/based in minneapolis/i).length).toBeGreaterThanOrEqual(1) + expect(screen.getByText(/location never gets in the way/i)).toBeInTheDocument() + }) + + it('displays the hero call-to-action with a discovery-call alternative', () => { + render() + + const heroCta = screen.getByRole('link', { name: /get in touch on linkedin/i }) + expect(heroCta).toHaveAttribute('href', LINKEDIN_URL) + + const discoveryLinks = screen.getAllByRole('link', { name: /book a free discovery call/i }) + expect(discoveryLinks.length).toBeGreaterThanOrEqual(1) + for (const link of discoveryLinks) { + expect(link).toHaveAttribute('href', '/contact') + } + }) + + 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 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('includes FAQ section with the Twin Cities question', () => { + render() + + expect(screen.getByRole('heading', { name: /frequently asked questions/i })).toBeInTheDocument() + expect(screen.getByText(/how long are sessions\?/i)).toBeInTheDocument() + expect( + screen.getByText(/do you offer career coaching in the twin cities\?/i) + ).toBeInTheDocument() + }) + + it('renders contact section with LinkedIn and an about link', () => { + render() + + expect(screen.getByRole('heading', { name: /get in touch/i })).toBeInTheDocument() + expect(screen.getByRole('link', { name: /connect on linkedin/i })).toBeInTheDocument() + expect(screen.getByRole('link', { name: /more about my background/i })).toHaveAttribute( + 'href', + '/about' + ) + }) + + it('opens external links safely in a new tab', () => { + render() + + const externalLinks = screen + .getAllByRole('link') + .filter(link => (link.getAttribute('href') ?? '').startsWith('http')) + expect(externalLinks.length).toBeGreaterThan(0) + for (const link of externalLinks) { + expect(link).toHaveAttribute('href', LINKEDIN_URL) + expect(link).toHaveAttribute('target', '_blank') + expect(link).toHaveAttribute('rel', 'noopener noreferrer') + } + }) + + it('renders breadcrumb navigation to the services hub', () => { + render() + + const breadcrumb = screen.getByRole('navigation', { name: /breadcrumb/i }) + expect(breadcrumb).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'Services' })).toHaveAttribute('href', '/services') + }) + + it('emits Service and FAQPage structured data', () => { + const { container } = render() + + const schemas = Array.from( + container.querySelectorAll('script[type="application/ld+json"]') + ).map(script => JSON.parse(script.textContent || '{}')) + const types = schemas.map(schema => schema['@type']) + expect(types).toContain('Service') + expect(types).toContain('FAQPage') + expect(types).toContain('BreadcrumbList') + }) + + it('has proper heading hierarchy', () => { + render() + + const h1Elements = screen.getAllByRole('heading', { level: 1 }) + expect(h1Elements).toHaveLength(1) + + 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('is accessible', async () => { + const { container } = render() + await axeTest(container) + }) + + it('has locally-targeted metadata in the solo voice', async () => { + const { metadata } = await import('./page') + expect(metadata.title).toBe('Tech Career Coaching in Minneapolis') + expect(metadata.description).toContain('Career coaching for software engineers') + expect(metadata.description).toContain('Minneapolis') + expect(metadata.description).not.toMatch(/\bwe\b/i) + expect(metadata.alternates?.canonical).toBe('/services/career-coaching') + expect(metadata.keywords).toContain('tech career coach') + }) +}) diff --git a/app/(main)/services/career-coaching/page.tsx b/app/(main)/services/career-coaching/page.tsx new file mode 100644 index 0000000..0ac917c --- /dev/null +++ b/app/(main)/services/career-coaching/page.tsx @@ -0,0 +1,293 @@ +import { siteConfig } from 'lib/site-config' +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: 'Tech Career Coaching in Minneapolis', + description: + 'Career coaching for software engineers and tech professionals: resumes, interviews, transitions. Minneapolis-based, remote-friendly. Free 30-minute call.', + keywords: [ + 'tech career coach', + 'career coaching for software engineers', + 'tech resume review', + 'interview prep coaching', + 'career transition', + 'engineering leadership coach', + ], + alternates: { canonical: '/services/career-coaching' }, + openGraph: { + title: 'Tech Career Coaching - Minneapolis', + description: + 'Career coaching for software engineers and tech professionals, from a Minneapolis-based consultant.', + 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.', + }, + { + question: 'Do you offer career coaching in the Twin Cities?', + answer: + "Yes - I'm based in Minneapolis. Clients in the metro can meet in person when it helps; for everyone else, video works great.", + }, +] + +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 CareerCoachingPage(): React.JSX.Element { + return ( +
+ + + + {/* Hero Section */} +
+

Tech Career Coaching

+

+ With over a decade in software and professional development, I coach engineers and tech + professionals navigating transitions, preparing for interviews, or figuring out their next + move. +

+

+ Based in Minneapolis - most coaching happens over video, so location never gets in the + way. +

+
+ + Get In Touch On LinkedIn + +

+ or{' '} + + book a free discovery call + +

+
+
+ + {/* Services Section */} +
+

Services Offered

+
+ {services.map(service => ( +
+

{service.title}

+

{service.description}

+
    + {service.benefits.map(benefit => ( +
  • + + Checkmark + + + {benefit} +
  • + ))} +
+
+ ))} +
+
+ + {/* Engagement Options */} +
+

Ways to Work Together

+
+ {engagementOptions.map(option => ( +
+

{option.name}

+
+ {option.duration} +
+

{option.description}

+
    + {option.features.map(feature => ( +
  • + + Checkmark + + + {feature} +
  • + ))} +
+ + Reach Out + +
+ ))} +
+
+ + {/* FAQ Section */} +
+

Frequently Asked Questions

+
+ {faqs.map(faq => ( +
+

{faq.question}

+

{faq.answer}

+
+ ))} +
+
+ + {/* Contact Section */} +
+

Get in Touch

+

+ Email{' '} + + {siteConfig.email} + + , reach me through LinkedIn, or{' '} + + book a free discovery call + {' '} + and we can set up a time to talk. +

+ + Connect on LinkedIn + +

+ If you want the context first:{' '} + + more about my background + + . +

+
+
+ ) +} 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..6fa7540 --- /dev/null +++ b/app/(main)/services/engineering-practice-improvement/page.test.tsx @@ -0,0 +1,92 @@ +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(/founded\s+an enterprise observability practice/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..a86d5ab --- /dev/null +++ b/app/(main)/services/engineering-practice-improvement/page.tsx @@ -0,0 +1,183 @@ +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 has founded and run this work inside real organizations.', + 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, and the recommendations come from shipping software rather than a certification curriculum. 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, and 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 the work I keep coming back to. I've founded + an enterprise Observability practice, rebuilt scrum processes that lifted sprint goal + completion by more than 30%, and stood up the delivery engine that carried a founding team + from first commit to public beta in six months. If your team is busy but delivery feels + slow, the problem is rarely effort - it's friction, and I know where to find it. +

+ +
+

+ 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. At one client that meant migrating a fragmented logging + estate to a single pane of glass while cutting ingest costs enough to make the migration + pay for itself. +

+
+ +
+

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. I've run this playbook at + scale: sprint goal completion up more than 30%, mid-sprint additions cut in half. And the + AI-era addition: teams with their practices in order can adopt coding agents safely and go + genuinely faster. Teams without them 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. + Everything else runs remote. Fully distributed org - the engagement runs remote just as + well. +

+
+ +
+

Frequently Asked Questions

+
+ {faqs.map(faq => ( +
+

{faq.question}

+

{faq.answer}

+
+ ))} +
+
+ +
+

Related Services

+

+ If AI adoption is part of the change, see{' '} + + AI & agent engineering + + . For growth support for the engineers themselves, see{' '} + + career coaching + + . +

+
+ +
+

Start a Conversation

+

+ Tell me where delivery feels stuck and we'll dig into the friction together.{' '} + + 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..92dc120 --- /dev/null +++ b/app/(main)/services/page.tsx @@ -0,0 +1,75 @@ +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', + description: + 'Software consulting, AI & agent engineering, engineering practice improvement, and tech career coaching from a Minneapolis consultant. Free discovery call.', + alternates: { canonical: '/services' }, +} + +export default function ServicesPage(): React.JSX.Element { + return ( +
+ + +

Consulting Services

+ +
+

How I Can Help

+

+ I'm Jordan Winters - an independent consultant with over a decade 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. +

+
+ {serviceEntries.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 one point of contact and no handoffs. +

+
+ +
+

Start With a Conversation

+

+ Start with the{' '} + + free 30-minute discovery call + {' '} + and I'll point you to the right one. +

+
+
+ ) +} 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..438fc41 --- /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(/over a decade/i)).toBeInTheDocument() + }) + + it('covers development, architecture, and rescue offerings', () => { + render() + + expect(screen.getByRole('heading', { name: /zero-to-one foundations/i })).toBeInTheDocument() + expect( + screen.getByRole('heading', { name: /architecture & technical strategy/i }) + ).toBeInTheDocument() + expect( + screen.getByRole('heading', { name: /infrastructure modernization & cost/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..e0be05d --- /dev/null +++ b/app/(main)/services/software-consulting/page.tsx @@ -0,0 +1,190 @@ +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, cloud infrastructure, and 0-to-1 platform builds for Twin Cities teams. Free discovery call.', + keywords: [ + 'software consultant minneapolis', + 'software consulting twin cities', + 'cloud infrastructure consultant', + 'devops consulting', + 'software architecture consultant', + ], + alternates: { canonical: '/services/software-consulting' }, + openGraph: { + title: 'Software Consulting in Minneapolis - Bidwell Consulting', + description: + 'Hands-on software consulting: architecture, cloud infrastructure, and 0-to-1 platform builds for Twin Cities teams.', + type: 'website', + }, +} + +const faqs = [ + { + question: 'What does a software consultant actually do?', + answer: + 'Whatever moves your software forward: designing systems, building infrastructure, writing code, and advising your team on hard technical decisions. You work directly with me, so 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 modernize our legacy infrastructure?', + answer: + "Yes, and I have the track record: I've cut legacy application costs 70% by moving brittle hand-run deployments onto a self-scaling container platform, and cut build and deploy times by more than 75% on core products. Incremental modernization, not a risky rewrite.", + }, + { + question: 'Do you write the code yourself or just advise?', + answer: + 'Both, and you choose the mix. Some clients want the platform built and handed over; others want architecture reviews and a senior sounding board. Either way I stay hands-on with the tools.', + }, + { + question: 'Do you work with startups and small businesses?', + answer: + 'Yes. I spent 2026 as a founding engineer taking a startup from empty repository to public beta in six months. Small teams are where one experienced consultant beats an agency: no account managers, no handoffs, and scope shaped to what you can 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 over a decade of building + and leading: staff-level DevOps at a national fintech, platform builds from scratch, and + cloud architecture that holds up under real traffic and real budgets. I design systems and I + write the code. +

+ +
+

Zero-to-One Foundations

+

+ If you're going from 0 to 1, I build the core infrastructure and development + environment for rapid iteration: cloud architecture on AWS or GCP, Terraform-managed with + environment isolation, CI/CD, observability, and cost models from day one. Most recently I + did exactly this as a founding engineer - empty repository to public beta in six months. +

+
+ +
+

+ Architecture & Technical Strategy +

+

+ Architecture reviews before you commit a year of budget. Build-versus-buy calls with the + trade-offs stated plainly. System design that starts from your constraints: team size, + budget, and what you can operate at 2 a.m. Not whatever's trending. +

+
+ +
+

+ Infrastructure Modernization & Cost +

+

+ Brittle deploys, sprawling cloud bills, legacy systems everyone is afraid to touch - this + is home turf. I've commodified compute hosting onto self-scaling container platforms + (cutting legacy costs 70%), shrunk build and deploy times by more than 75%, and steered a + multi-year cloud cost journey that took over $1.5M out of yearly spend. Modernization pays + for itself when you sequence it right. +

+
+ +
+

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, + I'll come to you anywhere in the Minneapolis-St. Paul metro: architecture sessions, + whiteboarding, team workshops. 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

+

+ For LLM and coding-agent work, see{' '} + + AI & agent engineering + + . If the team is 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/(wide)/experiments/claude-agentic-framework/opengraph-image.tsx b/app/(wide)/experiments/claude-agentic-framework/opengraph-image.tsx index 6018a6a..88b21b1 100644 --- a/app/(wide)/experiments/claude-agentic-framework/opengraph-image.tsx +++ b/app/(wide)/experiments/claude-agentic-framework/opengraph-image.tsx @@ -6,5 +6,5 @@ export const size = OG_SIZE export const contentType = 'image/png' export default function OpengraphImage() { - return ogCard('Claude Agentic Framework', 'The Governed Swarm — Bidwell Consulting') + return ogCard('Claude Agentic Framework', 'The Governed Swarm - Bidwell Consulting') } diff --git a/app/(wide)/experiments/claude-agentic-framework/page.tsx b/app/(wide)/experiments/claude-agentic-framework/page.tsx index 4465c7e..3dfa63b 100644 --- a/app/(wide)/experiments/claude-agentic-framework/page.tsx +++ b/app/(wide)/experiments/claude-agentic-framework/page.tsx @@ -7,7 +7,7 @@ const githubUrl = 'https://github.com/dralgorhythm/claude-agentic-framework' export const metadata: Metadata = { title: 'Claude Agentic Framework', description: - 'The Governed Swarm — a drop-in operating system for Claude Code that turns one AI assistant into a coordinated, quality-gated engineering team.', + 'The Governed Swarm - a drop-in operating system for Claude Code that turns one AI assistant into a coordinated, quality-gated engineering team.', keywords: [ 'claude agentic framework', 'claude code', @@ -32,13 +32,13 @@ const terminalLines = [ }, { prompt: '/swarm-execute', result: '5 workers finished · quality gates green' }, { prompt: '/swarm-review', result: 'security · architecture · tests · approved' }, - { prompt: 'git push', result: "shipped — work isn't done until it's pushed" }, + { prompt: 'git push', result: "shipped - work isn't done until it's pushed" }, ] const principles = [ { title: 'Speed and stability reinforce each other', - body: "Quality gates aren't a tax on velocity — they're what makes sustained velocity possible. Every change passes tests, lint, types, and build before it lands, so the next change starts on solid ground.", + body: "Quality gates aren't a tax on velocity - they're what makes sustained velocity possible. Every change passes tests, lint, types, and build before it lands, so the next change starts on solid ground.", }, { title: 'AI amplifies existing discipline', @@ -55,36 +55,36 @@ const cycle = [ step: '01', name: 'Think', commands: ['/architect', '/swarm-plan'], - body: 'One planning agent studies the goal, records decisions as durable artifacts — PRDs, ADRs, plans — and decomposes the work into small, parallelizable tasks.', + body: 'One planning agent studies the goal, records decisions as durable artifacts - PRDs, ADRs, plans - and decomposes the work into small, parallelizable tasks.', }, { step: '02', name: 'Build', commands: ['/swarm-execute'], - body: 'Focused workers implement in parallel. Each gets a self-contained prompt, a bounded turn budget, and a model tier matched to its job — and no worker can spawn workers of its own.', + body: 'Focused workers implement in parallel. Each gets a self-contained prompt, a bounded turn budget, and a model tier matched to its job - and no worker can spawn workers of its own.', }, { step: '03', name: 'Review', commands: ['/swarm-review'], - body: "Adversarial reviewers attack the diff from independent angles — correctness, security, architecture, tests. Run it more than once; it's cheaper than an incident.", + body: "Adversarial reviewers attack the diff from independent angles - correctness, security, architecture, tests. Run it more than once; it's cheaper than an incident.", }, { step: '04', name: 'Ship', commands: ['git push'], - body: 'Tests, linter, type checker, and build must pass before every commit — and the work is not done until the push succeeds.', + body: 'Tests, linter, type checker, and build must pass before every commit - and the work is not done until the push succeeds.', }, ] const inTheBox = [ { title: '10 commands', - body: 'Six single-agent expert modes — architect, builder, QA engineer, security auditor, UI/UX designer, code auditor — plus four swarm orchestrators for planning, execution, review, and research.', + body: 'Six single-agent expert modes - architect, builder, QA engineer, security auditor, UI/UX designer, code auditor - plus four swarm orchestrators for planning, execution, review, and research.', }, { title: '5 worker types', - body: 'Explorer, builder, reviewer, researcher, and architect workers. Model tiers are pinned in each agent’s frontmatter — premium reasoning where judgment matters, cheaper models for mechanical work.', + body: 'Explorer, builder, reviewer, researcher, and architect workers. Model tiers are pinned in each agent’s frontmatter - premium reasoning where judgment matters, cheaper models for mechanical work.', }, { title: '24 skills', @@ -92,7 +92,7 @@ const inTheBox = [ }, { title: 'Layered rules', - body: 'Golden-path tech strategy, code-quality standards, a debugging protocol, and security requirements — roughly 5k tokens, loaded into every session automatically.', + body: 'Golden-path tech strategy, code-quality standards, a debugging protocol, and security requirements - roughly 5k tokens, loaded into every session automatically.', }, { title: 'Fail-soft safety hooks', @@ -110,7 +110,7 @@ const ladder = [ level: 'Prose rules', strength: 'Advisory', accent: 'border-neutral-300 dark:border-neutral-700', - body: 'CLAUDE.md and the rules directory are read at the start of every session. They set direction — but nothing mechanically checks compliance.', + body: 'CLAUDE.md and the rules directory are read at the start of every session. They set direction - but nothing mechanically checks compliance.', }, { rung: '2', @@ -143,10 +143,10 @@ const stats = [ ] const qualityGates = [ - 'tsc --noEmit — strict type-check', - 'biome check — lint and format', - 'vitest run — unit tests with axe accessibility assertions', - 'next build — full static export', + 'tsc --noEmit - strict type-check', + 'biome check - lint and format', + 'vitest run - unit tests with axe accessibility assertions', + 'next build - full static export', ] function CommandChip({ children }: { children: string }): React.JSX.Element { @@ -172,7 +172,7 @@ export default function ClaudeAgenticFrameworkPage(): React.JSX.Element {

The Governed Swarm

A drop-in template for Claude Code that turns a single AI assistant into a coordinated - engineering team — with the guardrails to trust what it ships. + engineering team - with the guardrails to trust what it ships.

@@ -220,7 +220,7 @@ export default function ClaudeAgenticFrameworkPage(): React.JSX.Element {

An unsupervised coding agent is a firehose: fast, confident, and indifferent to your - standards. The interesting problem isn't getting AI to write code — it's making the + standards. The interesting problem isn't getting AI to write code - it's making the output trustworthy. The framework's bet: treat the agent like an engineering organization, not an autocomplete.

@@ -308,7 +308,7 @@ export default function ClaudeAgenticFrameworkPage(): React.JSX.Element {

Prose instructions are suggestions to a language model. The framework's sharpest idea is - admitting that — and pushing anything that must be true down the ladder until a machine + admitting that - and pushing anything that must be true down the ladder until a machine checks it.

@@ -360,7 +360,7 @@ export default function ClaudeAgenticFrameworkPage(): React.JSX.Element { This site is the lab

- bidwell.info runs on the framework it's describing — v3.1.0, checked into this + bidwell.info runs on the framework it's describing - v3.1.0, checked into this repository's .claude directory. The experiments hub you arrived from was specified in a PRD, architected in an ADR, and built and reviewed by the swarm. @@ -404,7 +404,7 @@ export default function ClaudeAgenticFrameworkPage(): React.JSX.Element {

The recommended install is a raw drop-in: clone the framework and run its init script against your project. It sets up the .claude{' '} - directory — commands, skills, agents, rules, hooks — and leaves the rest of your repo + directory - commands, skills, agents, rules, hooks - and leaves the rest of your repo alone.

@@ -417,7 +417,7 @@ cd your-project

- Read the init script before you run it — the framework would tell you to do the same. + Read the init script before you run it - the framework would tell you to do the same. Prefer a lighter footprint? Install it as a Claude Code plugin instead:{' '} /plugin install agentic-framework@agentic-framework

diff --git a/app/(wide)/experiments/global-anxiety-map/page.tsx b/app/(wide)/experiments/global-anxiety-map/page.tsx index f805576..d7cbc14 100644 --- a/app/(wide)/experiments/global-anxiety-map/page.tsx +++ b/app/(wide)/experiments/global-anxiety-map/page.tsx @@ -9,8 +9,8 @@ export default function GlobalAnxietyMapPage() {

The Sentiment Geoscope

- The Global Anxiety Map is a geospatial visualization of negative sentiment—specifically - "Anxiety" and "Uncertainty"—extracted from global news media. It acts as a "Geiger + The Global Anxiety Map is a geospatial visualization of negative sentiment - specifically + "Anxiety" and "Uncertainty" - extracted from global news media. It acts as a "Geiger counter" for geopolitical stability, highlighting regions where the language of fear is spiking.

diff --git a/app/(wide)/experiments/live-order-book/page.tsx b/app/(wide)/experiments/live-order-book/page.tsx index df0b7c5..b9e8d03 100644 --- a/app/(wide)/experiments/live-order-book/page.tsx +++ b/app/(wide)/experiments/live-order-book/page.tsx @@ -9,7 +9,7 @@ export default function LiveOrderBookPage() {

The Market Depth Crumble

- This concept renders the limit order book as a physical structure—two opposing walls of + This concept renders the limit order book as a physical structure - two opposing walls of "Buy" and "Sell" volume. As trades occur, they are visualized as projectiles smashing into these walls, physically destroying the blocks, helping traders intuitively feel the "sell pressure" or "support." diff --git a/app/components/footer.test.tsx b/app/components/footer.test.tsx index a3a3174..dd2260f 100644 --- a/app/components/footer.test.tsx +++ b/app/components/footer.test.tsx @@ -1,43 +1,80 @@ -import { render, screen } from '@testing-library/react' -import { axeTest } from 'lib/test-utils' +import { axeTest, render, screen } from 'lib/test-utils' +import { describe, expect, it } from 'vitest' import Footer from './footer' describe('Footer Component', () => { - it('renders navigation links', () => { + it('links all four services', () => { render(