Skip to content

Commit 4908b7f

Browse files
committed
Add Content Switcher to articles and deep-dive item views
1 parent 5732aeb commit 4908b7f

14 files changed

Lines changed: 296 additions & 112 deletions

File tree

‎.cache/pages.json‎

Lines changed: 65 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -90,71 +90,71 @@
9090
"contact",
9191
{
9292
"deep-dive": [
93-
"alert-fatigue-reduction-triage-actionable-alerts/pdf",
94-
"api-deprecation-sunset-headers-consumer-migration/pdf",
95-
"api-gateway-metrics-traces-logs-debugging/pdf",
96-
"api-usage-metering-quotas-cost-attribution/pdf",
97-
"argocd-sync-failures-gitops-debugging-troubleshooting/pdf",
98-
"availability-targets-five-nines-cost-benefit-analysis/pdf",
99-
"backpressure-load-shedding-admission-control-overload/pdf",
100-
"blameless-postmortem-incident-analysis-systemic-causes/pdf",
101-
"blue-green-canary-deployment-strategy-comparison/pdf",
102-
"cdn-edge-caching-cache-keys-vary-headers/pdf",
103-
"chaos-engineering-failure-injection-low-cost-experiments/pdf",
104-
"ci-pipeline-caching-docker-layers-dependency-cache/pdf",
105-
"circuit-breaker-retry-budget-cascade-failure-prevention/pdf",
106-
"consumer-driven-contract-testing-pact-internal-apis/pdf",
107-
"container-vulnerability-scanning-ci-shift-left-security/pdf",
108-
"database-schema-migrations-continuous-deployment-zero-downtime/pdf",
109-
"dead-letter-queue-design-replay-debugging/pdf",
110-
"distributed-tracing-sampling-strategies-head-tail/pdf",
111-
"eol-runtime-upgrade-dependency-hell-migration/pdf",
112-
"ephemeral-preview-environments-cost-control-cleanup/pdf",
113-
"flaky-test-diagnosis-race-conditions-e2e-stabilization/pdf",
114-
"golden-paths-developer-experience-standardization-autonomy/pdf",
115-
"grafana-dashboard-hygiene-pruning-actionable-metrics/pdf",
116-
"helm-release-management-drift-detection-debugging/pdf",
117-
"idempotent-message-handlers-deduplication-retries/pdf",
118-
"internal-cli-kubectl-terraform-wrapper-abstraction/pdf",
119-
"internal-developer-portal-platform-self-service-actions/pdf",
120-
"internal-platform-api-versioning-deprecation-breaking-changes/pdf",
121-
"kubernetes-cluster-upgrade-playbook-risk-reduction/pdf",
122-
"kubernetes-cost-optimization-resource-sizing-spot-instances/pdf",
123-
"kubernetes-decision-framework-when-not-to-use/pdf",
124-
"kubernetes-dns-debugging-ndots-coredns-troubleshooting/pdf",
125-
"kubernetes-hpa-autoscaling-metrics-tuning-latency/pdf",
126-
"kubernetes-ingress-gateway-api-comparison-migration/pdf",
127-
"kubernetes-multi-cluster-fleet-management-configuration/pdf",
128-
"kubernetes-pod-resource-requests-limits-qos-classes/pdf",
129-
"kubernetes-secrets-external-secrets-operator-csi-vault/pdf",
130-
"legacy-code-testing-characterization-tests-seams/pdf",
131-
"monorepo-affected-builds-remote-caching-ci-optimization/pdf",
132-
"mtls-certificate-rotation-service-mesh-authentication/pdf",
133-
"nginx-haproxy-reverse-proxy-production-tuning/pdf",
134-
"on-call-rotation-small-teams-sustainable-coverage/pdf",
135-
"opa-conftest-policy-as-code-infrastructure-guardrails/pdf",
136-
"openapi-spec-documentation-sdk-generation-validation/pdf",
137-
"opentelemetry-span-design-granularity-overhead/pdf",
138-
"performance-testing-load-models-benchmark-accuracy/pdf",
139-
"platform-architecture-control-plane-data-plane-separation/pdf",
140-
"platform-engineering-metrics-lead-time-developer-friction/pdf",
141-
"postgresql-connection-pooling-saturation-sizing/pdf",
142-
"private-networking-dns-routing-tls-debugging/pdf",
143-
"prometheus-high-cardinality-metrics-label-design/pdf",
144-
"rate-limiting-token-bucket-leaky-bucket-implementation/pdf",
145-
"release-quality-gates-automated-deployment-validation/pdf",
146-
"reverse-engineering-documentation-legacy-systems/pdf",
147-
"service-catalog-metadata-schema-ownership-tracking/pdf",
148-
"service-decommissioning-scream-test-shutdown/pdf",
149-
"slo-error-budget-practical-guide/pdf",
150-
"slsa-build-provenance-artifact-signing-supply-chain/pdf",
151-
"strangler-fig-migration-complete-guide/pdf",
152-
"structured-logging-correlation-ids-log-schema-design/pdf",
153-
"symptom-based-alerting-runbooks-alert-design/pdf",
154-
"synthetic-test-data-pii-anonymization-fixtures/pdf",
155-
"terraform-module-design-defaults-versioning-interfaces/pdf",
156-
"terraform-state-locking-corruption-recovery-backend/pdf",
157-
"workload-identity-federation-keyless-cloud-authentication/pdf"
93+
"alert-fatigue-reduction-triage-actionable-alerts",
94+
"api-deprecation-sunset-headers-consumer-migration",
95+
"api-gateway-metrics-traces-logs-debugging",
96+
"api-usage-metering-quotas-cost-attribution",
97+
"argocd-sync-failures-gitops-debugging-troubleshooting",
98+
"availability-targets-five-nines-cost-benefit-analysis",
99+
"backpressure-load-shedding-admission-control-overload",
100+
"blameless-postmortem-incident-analysis-systemic-causes",
101+
"blue-green-canary-deployment-strategy-comparison",
102+
"cdn-edge-caching-cache-keys-vary-headers",
103+
"chaos-engineering-failure-injection-low-cost-experiments",
104+
"ci-pipeline-caching-docker-layers-dependency-cache",
105+
"circuit-breaker-retry-budget-cascade-failure-prevention",
106+
"consumer-driven-contract-testing-pact-internal-apis",
107+
"container-vulnerability-scanning-ci-shift-left-security",
108+
"database-schema-migrations-continuous-deployment-zero-downtime",
109+
"dead-letter-queue-design-replay-debugging",
110+
"distributed-tracing-sampling-strategies-head-tail",
111+
"eol-runtime-upgrade-dependency-hell-migration",
112+
"ephemeral-preview-environments-cost-control-cleanup",
113+
"flaky-test-diagnosis-race-conditions-e2e-stabilization",
114+
"golden-paths-developer-experience-standardization-autonomy",
115+
"grafana-dashboard-hygiene-pruning-actionable-metrics",
116+
"helm-release-management-drift-detection-debugging",
117+
"idempotent-message-handlers-deduplication-retries",
118+
"internal-cli-kubectl-terraform-wrapper-abstraction",
119+
"internal-developer-portal-platform-self-service-actions",
120+
"internal-platform-api-versioning-deprecation-breaking-changes",
121+
"kubernetes-cluster-upgrade-playbook-risk-reduction",
122+
"kubernetes-cost-optimization-resource-sizing-spot-instances",
123+
"kubernetes-decision-framework-when-not-to-use",
124+
"kubernetes-dns-debugging-ndots-coredns-troubleshooting",
125+
"kubernetes-hpa-autoscaling-metrics-tuning-latency",
126+
"kubernetes-ingress-gateway-api-comparison-migration",
127+
"kubernetes-multi-cluster-fleet-management-configuration",
128+
"kubernetes-pod-resource-requests-limits-qos-classes",
129+
"kubernetes-secrets-external-secrets-operator-csi-vault",
130+
"legacy-code-testing-characterization-tests-seams",
131+
"monorepo-affected-builds-remote-caching-ci-optimization",
132+
"mtls-certificate-rotation-service-mesh-authentication",
133+
"nginx-haproxy-reverse-proxy-production-tuning",
134+
"on-call-rotation-small-teams-sustainable-coverage",
135+
"opa-conftest-policy-as-code-infrastructure-guardrails",
136+
"openapi-spec-documentation-sdk-generation-validation",
137+
"opentelemetry-span-design-granularity-overhead",
138+
"performance-testing-load-models-benchmark-accuracy",
139+
"platform-architecture-control-plane-data-plane-separation",
140+
"platform-engineering-metrics-lead-time-developer-friction",
141+
"postgresql-connection-pooling-saturation-sizing",
142+
"private-networking-dns-routing-tls-debugging",
143+
"prometheus-high-cardinality-metrics-label-design",
144+
"rate-limiting-token-bucket-leaky-bucket-implementation",
145+
"release-quality-gates-automated-deployment-validation",
146+
"reverse-engineering-documentation-legacy-systems",
147+
"service-catalog-metadata-schema-ownership-tracking",
148+
"service-decommissioning-scream-test-shutdown",
149+
"slo-error-budget-practical-guide",
150+
"slsa-build-provenance-artifact-signing-supply-chain",
151+
"strangler-fig-migration-complete-guide",
152+
"structured-logging-correlation-ids-log-schema-design",
153+
"symptom-based-alerting-runbooks-alert-design",
154+
"synthetic-test-data-pii-anonymization-fixtures",
155+
"terraform-module-design-defaults-versioning-interfaces",
156+
"terraform-state-locking-corruption-recovery-backend",
157+
"workload-identity-federation-keyless-cloud-authentication"
158158
]
159159
},
160160
"hero",

‎src/components/Breadcrumbs/index.astro‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const breadcrumbSchema =
3737

3838
{
3939
breadcrumbs.length > 1 && (
40-
<div id="breadcrumbs" class="mt-4">
40+
<div class="min-w-0">
4141
<nav aria-label="Breadcrumbs" class="breadcrumbs sm:pl-4">
4242
<ol class="flex items-center space-x-2 text-sm sm:text-base text-primary-offset">
4343
{breadcrumbs.map((item, index) => (

‎src/components/Content/Layout/index.astro‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
import type { CollectionEntry } from 'astro:content'
1010
import { Picture } from 'astro:assets'
1111
import MarkdownLayout from '@layouts/MarkdownLayout.astro'
12-
import Shares from '@components/Social/Shares/index.astro'
1312
import Carousel from '@components/Carousel/index.astro'
13+
import ContentSwitcher from '@components/Content/Switcher/index.astro'
14+
import Shares from '@components/Social/Shares/index.astro'
1415
import WebMentions from '@components/WebMentions/index.astro'
1516
1617
export type Props = {
@@ -45,6 +46,11 @@ const { author, article, path, readingTime, section } = Astro.props
4546
tags={article.data.tags?.map(t => (typeof t === 'string' ? t : t.id)) ?? []}
4647
{...readingTime && { readingTime }}
4748
>
49+
{/** SLOT: breadcrumb-row */}
50+
<div class="" slot="breadcrumb-row">
51+
<ContentSwitcher path={path} />
52+
</div>
53+
4854
{/** SLOT: hero-image */}
4955
{
5056
article.data.cover && (

‎src/components/Content/Switcher/client/index.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,11 @@ export class ContentSwitcherElement extends LitElement {
6363
role="switch"
6464
aria-checked="${isDeepDive ? 'true' : 'false'}"
6565
aria-label="Toggle between Overview and Deep Dive"
66-
class="content-switcher-track group relative inline-flex h-7 w-12 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-trim-offset transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-secondary"
66+
class="content-switcher-track group relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-trim-offset transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-secondary"
6767
data-switcher-toggle
6868
>
6969
<span
70-
class="content-switcher-thumb pointer-events-none inline-block size-5 translate-x-0.5 rounded-full bg-page-base shadow-sm ring-0 transition-transform"
70+
class="content-switcher-thumb pointer-events-none inline-block size-3 translate-x-0.5 rounded-full bg-page-base shadow-sm ring-0 transition-transform"
7171
></span>
7272
</a>
7373

‎src/components/Content/Switcher/index.astro‎

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,40 @@
11
---
2+
import { parseContentPath } from './server'
3+
import { BuildError } from '@lib/errors/BuildError'
24
import './index.css'
35
46
export type Props = {
5-
/** The currently active variant, used to determine the position of the switcher thumb */
6-
currentVariant: 'overview' | 'deep-dive'
7-
/** Shared slug used to navigate between overview and deep-dive routes */
8-
slug: string
7+
/** The URL path component e.g. 'articles/my-article' */
8+
path?: string
9+
/** Legacy current variant for client rendering */
10+
currentVariant?: 'overview' | 'deep-dive'
11+
/** Legacy slug for client rendering */
12+
slug?: string
913
}
1014
11-
const { currentVariant, slug } = Astro.props
15+
const {
16+
path,
17+
currentVariant: legacyCurrentVariant,
18+
slug: legacySlug = '',
19+
} = Astro.props
20+
21+
let currentVariant: 'overview' | 'deep-dive'
22+
let slug: string
23+
24+
if (path) {
25+
const parsed = parseContentPath(path)
26+
currentVariant = parsed.currentVariant === 'articles' ? 'overview' : 'deep-dive'
27+
slug = parsed.slug
28+
} else if (legacyCurrentVariant) {
29+
currentVariant = legacyCurrentVariant
30+
slug = legacySlug
31+
} else {
32+
throw new BuildError('Content/Switcher: expected either `path` or `currentVariant` prop.', {
33+
phase: 'compilation',
34+
filePath: 'src/components/Content/Switcher/index.astro',
35+
tool: 'content-switcher',
36+
})
37+
}
1238
---
1339

1440
<content-switcher current-variant={currentVariant} slug={slug}></content-switcher>

‎src/components/Content/Switcher/index.css‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
}
88

99
.content-switcher-track[aria-checked='true'] .content-switcher-thumb {
10-
transform: translateX(1.25rem);
10+
transform: translateX(1.125rem);
1111
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { BuildError } from '@lib/errors/BuildError'
3+
import { parseContentPath } from '../index'
4+
5+
describe('parseContentPath', () => {
6+
it('parses an articles path into currentVariant and slug', () => {
7+
expect(parseContentPath('articles/my-article')).toEqual({
8+
currentVariant: 'articles',
9+
slug: 'my-article',
10+
})
11+
})
12+
13+
it('parses a deep-dive path into currentVariant and slug', () => {
14+
expect(parseContentPath('deep-dive/web-vitals')).toEqual({
15+
currentVariant: 'deep-dive',
16+
slug: 'web-vitals',
17+
})
18+
})
19+
20+
it('normalizes leading and trailing slashes', () => {
21+
expect(parseContentPath('/articles/my-article/')).toEqual({
22+
currentVariant: 'articles',
23+
slug: 'my-article',
24+
})
25+
})
26+
27+
it('normalizes extra whitespace around path', () => {
28+
expect(parseContentPath(' /deep-dive/platform-reliability/ ')).toEqual({
29+
currentVariant: 'deep-dive',
30+
slug: 'platform-reliability',
31+
})
32+
})
33+
34+
it('preserves nested slug segments', () => {
35+
expect(parseContentPath('articles/category/subcategory/my-article')).toEqual({
36+
currentVariant: 'articles',
37+
slug: 'category/subcategory/my-article',
38+
})
39+
})
40+
41+
it('returns an empty slug when only the variant is provided', () => {
42+
expect(parseContentPath('articles')).toEqual({
43+
currentVariant: 'articles',
44+
slug: '',
45+
})
46+
})
47+
48+
it('throws BuildError for unsupported variant', () => {
49+
expect(() => parseContentPath('services/my-service')).toThrow(BuildError)
50+
expect(() => parseContentPath('services/my-service')).toThrow(
51+
'Content/Switcher: invalid path variant.'
52+
)
53+
})
54+
55+
it('throws BuildError for an empty path', () => {
56+
const parse = () => parseContentPath(' ')
57+
58+
expect(parse).toThrow(BuildError)
59+
expect(parse).toThrow('Content/Switcher: invalid path variant.')
60+
})
61+
62+
it('includes detailed context in the thrown BuildError', () => {
63+
try {
64+
parseContentPath('/unknown/thing')
65+
throw new Error('Expected parseContentPath to throw BuildError')
66+
} catch (error) {
67+
const typedError = error as BuildError
68+
69+
expect(typedError).toBeInstanceOf(BuildError)
70+
expect(typedError.phase).toBe('compilation')
71+
expect(typedError.tool).toBe('content-switcher')
72+
expect(typedError.filePath).toBe('src/components/Content/Switcher/server/index.ts')
73+
expect(typedError.cause).toEqual({
74+
providedPath: '/unknown/thing',
75+
normalizedPath: 'unknown/thing',
76+
receivedVariant: 'unknown',
77+
expectedVariants: ['articles', 'deep-dive'],
78+
})
79+
}
80+
})
81+
})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { BuildError } from '@lib/errors/BuildError'
2+
type ContentVariant = 'articles' | 'deep-dive'
3+
4+
export const parseContentPath = (path: string) => {
5+
const normalizedPath = path.trim().replace(/^\/+|\/+$/g, '')
6+
const [variant, ...slugParts] = normalizedPath.split('/').filter(Boolean)
7+
const candidateVariant = variant ?? ''
8+
9+
const isSupportedVariant = (value: string): value is ContentVariant => {
10+
return value === 'articles' || value === 'deep-dive'
11+
}
12+
13+
if (!isSupportedVariant(candidateVariant)) {
14+
const errorDetails = {
15+
providedPath: path,
16+
normalizedPath,
17+
receivedVariant: variant ?? null,
18+
expectedVariants: ['articles', 'deep-dive'],
19+
}
20+
21+
throw new BuildError(new Error('Content/Switcher: invalid path variant.', { cause: errorDetails }), {
22+
phase: 'compilation',
23+
filePath: 'src/components/Content/Switcher/server/index.ts',
24+
tool: 'content-switcher',
25+
})
26+
}
27+
28+
return {
29+
currentVariant: candidateVariant,
30+
slug: slugParts.join('/'),
31+
}
32+
}

0 commit comments

Comments
 (0)