@flamingo-stack/openframe-frontend-core 0.0.256 → 0.0.257

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/{chunk-JK25REDL.js → chunk-2VZQJIVA.js} +3 -3
  2. package/dist/{chunk-IG4QRNYZ.cjs → chunk-4MXBBBHB.cjs} +108 -11
  3. package/dist/chunk-4MXBBBHB.cjs.map +1 -0
  4. package/dist/{chunk-CMWRZECW.cjs → chunk-AIQ4DGO7.cjs} +34 -34
  5. package/dist/{chunk-CMWRZECW.cjs.map → chunk-AIQ4DGO7.cjs.map} +1 -1
  6. package/dist/{chunk-S7IX7YU4.js → chunk-CBIQDPHK.js} +4 -49
  7. package/dist/chunk-CBIQDPHK.js.map +1 -0
  8. package/dist/{chunk-TSZBJVYK.cjs → chunk-EB76VWM4.cjs} +100 -49
  9. package/dist/chunk-EB76VWM4.cjs.map +1 -0
  10. package/dist/{chunk-6F5CGBVT.cjs → chunk-FBH73RRM.cjs} +1053 -1098
  11. package/dist/chunk-FBH73RRM.cjs.map +1 -0
  12. package/dist/{chunk-7RLV3U5C.js → chunk-IJHORDN5.js} +100 -3
  13. package/dist/chunk-IJHORDN5.js.map +1 -0
  14. package/dist/{chunk-ISALYOFX.cjs → chunk-LCYDTEEK.cjs} +19 -19
  15. package/dist/{chunk-ISALYOFX.cjs.map → chunk-LCYDTEEK.cjs.map} +1 -1
  16. package/dist/{chunk-QXSRQVRO.js → chunk-LDEXTWMK.js} +8 -2
  17. package/dist/chunk-LDEXTWMK.js.map +1 -0
  18. package/dist/{chunk-U6UNAC32.cjs → chunk-N5UOJC3V.cjs} +8 -2
  19. package/dist/chunk-N5UOJC3V.cjs.map +1 -0
  20. package/dist/{chunk-V5L4D454.js → chunk-NAOTZAKM.js} +54 -3
  21. package/dist/chunk-NAOTZAKM.js.map +1 -0
  22. package/dist/{chunk-V4Q5PIBV.cjs → chunk-NNDLCSGR.cjs} +27 -27
  23. package/dist/{chunk-V4Q5PIBV.cjs.map → chunk-NNDLCSGR.cjs.map} +1 -1
  24. package/dist/{chunk-HCMVLEC3.js → chunk-RUMSS73U.js} +4 -4
  25. package/dist/{chunk-46DHPQY6.js → chunk-YMZZ5THD.js} +3 -3
  26. package/dist/components/chat/index.cjs +4 -4
  27. package/dist/components/chat/index.js +3 -3
  28. package/dist/components/contact/index.cjs +5 -5
  29. package/dist/components/contact/index.js +4 -4
  30. package/dist/components/features/index.cjs +4 -4
  31. package/dist/components/features/index.js +3 -3
  32. package/dist/components/index.cjs +101 -98
  33. package/dist/components/index.cjs.map +1 -1
  34. package/dist/components/index.d.ts +1 -0
  35. package/dist/components/index.d.ts.map +1 -1
  36. package/dist/components/index.js +16 -13
  37. package/dist/components/index.js.map +1 -1
  38. package/dist/components/navigation/index.cjs +4 -4
  39. package/dist/components/navigation/index.js +3 -3
  40. package/dist/components/onboarding-guides/index.cjs +40 -26
  41. package/dist/components/onboarding-guides/index.cjs.map +1 -1
  42. package/dist/components/onboarding-guides/index.js +17 -3
  43. package/dist/components/onboarding-guides/index.js.map +1 -1
  44. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts +8 -1
  45. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  46. package/dist/components/related-content/index.cjs +5 -5
  47. package/dist/components/related-content/index.js +4 -4
  48. package/dist/components/shared/article-author-byline.d.ts +58 -0
  49. package/dist/components/shared/article-author-byline.d.ts.map +1 -0
  50. package/dist/components/social-icon-row.d.ts +4 -2
  51. package/dist/components/social-icon-row.d.ts.map +1 -1
  52. package/dist/components/tickets/index.cjs +75 -75
  53. package/dist/components/tickets/index.js +5 -5
  54. package/dist/components/ui/button/button.d.ts +1 -1
  55. package/dist/components/ui/button/button.d.ts.map +1 -1
  56. package/dist/components/ui/file-manager/index.cjs +20 -20
  57. package/dist/components/ui/file-manager/index.js +1 -1
  58. package/dist/components/ui/index.cjs +4 -4
  59. package/dist/components/ui/index.js +3 -3
  60. package/dist/index.cjs +8 -8
  61. package/dist/index.cjs.map +1 -1
  62. package/dist/index.js +7 -7
  63. package/dist/types/entity-author.d.ts +3 -0
  64. package/dist/types/entity-author.d.ts.map +1 -1
  65. package/dist/utils/content-href.d.ts.map +1 -1
  66. package/dist/utils/index.cjs +6 -1
  67. package/dist/utils/index.cjs.map +1 -1
  68. package/dist/utils/index.js +6 -1
  69. package/dist/utils/index.js.map +1 -1
  70. package/package.json +1 -1
  71. package/src/components/index.ts +5 -0
  72. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +22 -0
  73. package/src/components/shared/article-author-byline.tsx +187 -0
  74. package/src/components/social-icon-row.tsx +25 -8
  75. package/src/components/ui/button/button.tsx +6 -0
  76. package/src/types/entity-author.ts +3 -0
  77. package/src/utils/__tests__/content-href.test.ts +3 -0
  78. package/src/utils/content-href.ts +5 -0
  79. package/dist/chunk-6F5CGBVT.cjs.map +0 -1
  80. package/dist/chunk-7RLV3U5C.js.map +0 -1
  81. package/dist/chunk-IG4QRNYZ.cjs.map +0 -1
  82. package/dist/chunk-QXSRQVRO.js.map +0 -1
  83. package/dist/chunk-S7IX7YU4.js.map +0 -1
  84. package/dist/chunk-TSZBJVYK.cjs.map +0 -1
  85. package/dist/chunk-U6UNAC32.cjs.map +0 -1
  86. package/dist/chunk-V5L4D454.js.map +0 -1
  87. /package/dist/{chunk-JK25REDL.js.map → chunk-2VZQJIVA.js.map} +0 -0
  88. /package/dist/{chunk-HCMVLEC3.js.map → chunk-RUMSS73U.js.map} +0 -0
  89. /package/dist/{chunk-46DHPQY6.js.map → chunk-YMZZ5THD.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.256",
3
+ "version": "0.0.257",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -90,5 +90,10 @@ export * from './shared/legal-document'
90
90
  // Detail Page Skeleton
91
91
  export { DetailPageSkeleton, type DetailPageSkeletonProps } from './shared/detail-page-skeleton'
92
92
 
93
+ // Author byline card (end-of-article author description) — embeddable via
94
+ // embed-shims + optional-runtime avatar proxying; the hub wraps it to inject
95
+ // its platform-aware fallbackBio default.
96
+ export { ArticleAuthorByline, type ArticleAuthorBylineProps } from './shared/article-author-byline'
97
+
93
98
  // Priority UI components that exist in main components directory
94
99
  // Note: These are re-exported from ./ui already, no need to duplicate
@@ -32,6 +32,7 @@ import { getCaptionsUrl } from '../features/captions-url'
32
32
  import { SimpleMarkdownRenderer } from '../ui/simple-markdown-renderer'
33
33
  import { EntityTagBadges } from '../features/entity-tag-badges'
34
34
  import { LoadError } from '../ui/error-state'
35
+ import { ArticleAuthorByline } from '../shared/article-author-byline'
35
36
  import { EntityAuthorCard } from '../chat/entity-cards/entity-author-card'
36
37
  import { OnboardingGuideCard } from '../chat/entity-cards/onboarding-guide-card'
37
38
  import { useChatRuntime } from '../../contexts/chat-runtime-context'
@@ -53,6 +54,13 @@ export interface OnboardingGuideDetailViewProps {
53
54
  /** Link target for the author name in the metadata grid — the host
54
55
  * computes it (public author page; absent ⇒ plain text). */
55
56
  authorHref?: string
57
+ /** Bio paragraph for the end-of-article author byline. Defaults to the
58
+ * guide payload's `author.about` (hubs that hydrate it can omit this). */
59
+ authorBio?: string | null
60
+ /** Byline fallback paragraph when the bio is empty — the host passes its
61
+ * platform-aware copy (the lib has no config awareness). Absent ⇒ the
62
+ * byline renders nothing below the name when the bio is empty. */
63
+ fallbackBio?: string | null
56
64
  /** Optional markdown renderer override. Defaults to lib
57
65
  * `<SimpleMarkdownRenderer>`. */
58
66
  MarkdownRenderer?: ComponentType<{ content: string }>
@@ -69,6 +77,8 @@ export interface OnboardingGuideDetailViewProps {
69
77
 
70
78
  export function OnboardingGuideDetailView({
71
79
  authorHref,
80
+ authorBio,
81
+ fallbackBio,
72
82
  initialData,
73
83
  slug,
74
84
  guideEndpoint,
@@ -190,6 +200,18 @@ export function OnboardingGuideDetailView({
190
200
  />
191
201
  )}
192
202
 
203
+ {/* End-of-article author byline (avatar + linked name + bio) — the
204
+ author DESCRIPTION block every article-shaped detail page renders.
205
+ Hidden when the guide has no author (the byline returns null). */}
206
+ <ArticleAuthorByline
207
+ author={guide.author?.full_name ?? null}
208
+ avatar={guide.author?.avatar_url}
209
+ jobTitle={guide.author?.job_title}
210
+ bio={authorBio ?? guide.author?.about}
211
+ href={authorHref}
212
+ fallbackBio={fallbackBio}
213
+ />
214
+
193
215
  {/* Related — same-section, ordered by step. */}
194
216
  {related.length > 0 && (
195
217
  <div className="space-y-4 pt-8 border-t border-ods-border">
@@ -0,0 +1,187 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Shared "author byline" card used by article-shaped detail pages
5
+ * (blog post, product release, onboarding guide, investor update).
6
+ *
7
+ * MOVED from the hub (`components/shared/article-author-byline.tsx`) so any
8
+ * consuming app can embed it; the hub keeps a thin wrapper that pre-binds its
9
+ * host-specific injections (the platform-aware `fallbackBio` default derived
10
+ * from `getAppConfig()`).
11
+ *
12
+ * Embed-readiness contract:
13
+ * - `Link` / `Image` render through the embed-shims (plain `<a>` / `<img>`
14
+ * in non-Next hosts; the real Next primitives once the host registers
15
+ * them at app init).
16
+ * - The avatar is proxied through the OPTIONAL ambient `ChatRuntime`
17
+ * (`endpoints.imageProxyUrlPrefix`, same config `useProxiedImageUrl`
18
+ * reads) — when no runtime is mounted the raw URL renders as-is, so the
19
+ * component never throws outside a provider. Hosts can also inject
20
+ * `proxyImageUrl` to bypass the runtime entirely.
21
+ * - `fallbackBio` is a PLAIN prop (no app-config import): the lib has no
22
+ * platform awareness; pass copy or leave it absent to render nothing.
23
+ *
24
+ * Render order: avatar → name + job title + date → bio (when present).
25
+ * Returns null when `author` is empty (no card rendered).
26
+ */
27
+
28
+ import React from 'react'
29
+ import { Calendar, User } from 'lucide-react'
30
+ import Image from '../../embed-shims/next-image'
31
+ import Link from '../../embed-shims/next-link'
32
+ import { cn } from '../../utils/cn'
33
+ import { formatBioText } from '../../utils/format'
34
+ import { getProxiedImageUrl } from '../../utils/image-proxy'
35
+ import { useChatRuntime } from '../../contexts/chat-runtime-context'
36
+
37
+ export interface ArticleAuthorBylineProps {
38
+ /** Author display name. Required — block is hidden when null/empty. */
39
+ author: string | null
40
+ /** Avatar URL. Falls back to a placeholder when null. */
41
+ avatar?: string | null
42
+ /** Optional bio paragraph rendered below the name. */
43
+ bio?: string | null
44
+ /** Optional job title rendered immediately under the name. */
45
+ jobTitle?: string | null
46
+ /** Optional published date (ISO string or Date). Rendered next to the name. */
47
+ publishedAt?: string | Date | null
48
+ /** Optional link target for the author name (e.g. the author page). */
49
+ href?: string | null
50
+ /**
51
+ * Fallback paragraph when `bio` is empty. Plain copy — the lib has no
52
+ * platform/config awareness, so hosts that want a branded default
53
+ * ("Contributing author on the {platform} platform") pass it explicitly
54
+ * (the hub wrapper derives it from `getAppConfig()`). Absent/null ⇒
55
+ * nothing renders below the name when `bio` is empty.
56
+ */
57
+ fallbackBio?: string | null
58
+ /** Avatar size variant. `md` = 56px (default), `lg` = 64px. */
59
+ size?: 'md' | 'lg'
60
+ /**
61
+ * Host-injected avatar-URL mapper. Wins over the ambient-runtime proxy
62
+ * resolution. Use when the host proxies images outside the `ChatRuntime`
63
+ * config (e.g. a bespoke CDN rewrite).
64
+ */
65
+ proxyImageUrl?: (url: string) => string
66
+ className?: string
67
+ }
68
+
69
+ function formatDate(value: string | Date): string {
70
+ const d = typeof value === 'string' ? new Date(value) : value
71
+ if (Number.isNaN(d.getTime())) return ''
72
+ // `timeZone: 'UTC'` keeps the SSR (server = UTC) and client (user's local tz)
73
+ // renders identical — without it a published_at near a midnight boundary
74
+ // formats to a different day on each side, triggering a React #418 hydration
75
+ // text mismatch. Matches the convention in blog-metadata.tsx / investor-update.
76
+ return d.toLocaleDateString('en-US', {
77
+ month: 'long',
78
+ day: 'numeric',
79
+ year: 'numeric',
80
+ timeZone: 'UTC',
81
+ })
82
+ }
83
+
84
+ export function ArticleAuthorByline({
85
+ author,
86
+ avatar,
87
+ bio,
88
+ jobTitle,
89
+ publishedAt,
90
+ href,
91
+ fallbackBio,
92
+ size = 'md',
93
+ proxyImageUrl,
94
+ className,
95
+ }: ArticleAuthorBylineProps) {
96
+ // Optional runtime — `useChatRuntime` returns null outside a provider, so
97
+ // the byline works in bare embeds (raw avatar URL) and proxies whenever the
98
+ // host mounted a runtime with `imageProxyUrlPrefix` (the hub always does).
99
+ const runtime = useChatRuntime()
100
+ if (!author) return null
101
+
102
+ const proxiedAvatar = avatar
103
+ ? proxyImageUrl
104
+ ? proxyImageUrl(avatar)
105
+ : (getProxiedImageUrl(avatar, {
106
+ proxyPrefix: runtime?.endpoints.imageProxyUrlPrefix,
107
+ skipDomains: runtime?.endpoints.imageProxySkipDomains,
108
+ directHttps: true,
109
+ }) ?? avatar)
110
+ : ''
111
+
112
+ // Class-driven sizing (md = 56px, lg = 64px). The numeric pair feeds only
113
+ // the Image element's intrinsic width/height attributes (required by the
114
+ // Next image shim) — layout comes from the classes.
115
+ const avatarSizeClass = size === 'lg' ? 'w-16 h-16' : 'w-14 h-14'
116
+ const avatarIconClass = size === 'lg' ? 'w-8 h-8' : 'w-7 h-7'
117
+ const avatarDim = size === 'lg' ? 64 : 56
118
+ const formattedBio = formatBioText(bio ?? null)
119
+ const dateLabel = publishedAt ? formatDate(publishedAt) : ''
120
+
121
+ return (
122
+ <div
123
+ className={cn(
124
+ 'bg-ods-card border border-ods-border rounded-lg p-6',
125
+ 'flex flex-col md:flex-row gap-4 items-start',
126
+ className,
127
+ )}
128
+ >
129
+ {/* Avatar */}
130
+ <div className="flex-shrink-0">
131
+ {avatar ? (
132
+ <Image
133
+ src={proxiedAvatar || avatar}
134
+ alt={author}
135
+ width={avatarDim}
136
+ height={avatarDim}
137
+ className={cn('rounded-full border-2 border-ods-border object-cover', avatarSizeClass)}
138
+ />
139
+ ) : (
140
+ <div
141
+ className={cn(
142
+ 'rounded-full border-2 border-ods-border bg-ods-bg flex items-center justify-center',
143
+ avatarSizeClass,
144
+ )}
145
+ >
146
+ <User className={cn('text-ods-text-secondary', avatarIconClass)} />
147
+ </div>
148
+ )}
149
+ </div>
150
+
151
+ {/* Name + meta + bio */}
152
+ <div className="flex-1 min-w-0">
153
+ <div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
154
+ <h3 className="text-h5 text-ods-text-primary">
155
+ {href ? (
156
+ <Link href={href} className="hover:text-ods-accent transition-colors">
157
+ {author}
158
+ </Link>
159
+ ) : (
160
+ author
161
+ )}
162
+ </h3>
163
+ {dateLabel && (
164
+ <span className="inline-flex items-center gap-1 font-body text-body-sm text-ods-text-secondary">
165
+ <Calendar className="h-3 w-3" />
166
+ {dateLabel}
167
+ </span>
168
+ )}
169
+ </div>
170
+ {jobTitle && (
171
+ <p className="font-body text-body-sm text-ods-text-secondary mt-0.5">
172
+ {jobTitle}
173
+ </p>
174
+ )}
175
+ {formattedBio ? (
176
+ <p className="font-body text-body-md text-ods-text-secondary leading-relaxed mt-2">
177
+ {formattedBio}
178
+ </p>
179
+ ) : fallbackBio ? (
180
+ <p className="font-body text-body-md text-ods-text-secondary italic mt-2">
181
+ {fallbackBio}
182
+ </p>
183
+ ) : null}
184
+ </div>
185
+ </div>
186
+ )
187
+ }
@@ -19,8 +19,10 @@ interface SocialIconRowProps {
19
19
  className?: string;
20
20
  links?: SocialLink[];
21
21
  variant?: "accent" | "outline" | "transparent" | "destructive" | null | undefined;
22
- /** Natural icon-button sizing (w-fit container, no flex-1 stretch) for
23
- * page-level rows. Default false: buttons stretch across the container
22
+ /** Quiet metadata row for page-level identity/share slots: 32px ghost
23
+ * icon buttons (size="icon-sm", 16px glyphs), gap-2, w-fit container,
24
+ * variant defaulting to "transparent" (an explicit `variant` still wins).
25
+ * Default false: 44/48px buttons stretching across the container —
24
26
  * the original card-width behavior (TMCG member cards, footers). */
25
27
  compact?: boolean;
26
28
  }
@@ -78,17 +80,32 @@ function renderSocialIcon(platform: string) {
78
80
  }
79
81
  }
80
82
 
81
- export function SocialIconRow({ className = '', links = defaultLinks, variant = 'outline', compact = false }: SocialIconRowProps) {
83
+ export function SocialIconRow({ className = '', links = defaultLinks, variant, compact = false }: SocialIconRowProps) {
84
+ // ── Compact design rationale ──────────────────────────────────────────
85
+ // Page-level identity/share rows read as METADATA, not CTAs. The major
86
+ // design systems converge on one recipe for this slot: a ~32px ghost icon
87
+ // button with a ~16px glyph, tight 8px gap, transparent at rest, subtle
88
+ // background state-layer on hover (Carbon "ghost" sm, Primer "invisible"
89
+ // medium, shadcn ghost+icon-sm — all 32px; Material 3 "standard" icon
90
+ // button = state-layer hover). Author headers on content platforms
91
+ // (Medium / dev.to / Substack) use the same quiet treatment. Hence
92
+ // compact: size="icon-sm" + variant defaulting to "transparent" — the
93
+ // hover affordance comes from the bg state layer (hover:bg-ods-bg-hover
94
+ // inside the variant) because the brand icons carry fixed fills, not
95
+ // currentColor. An explicit `variant` prop still wins (e.g. outline
96
+ // chips). Non-compact keeps the legacy outline default + full-width
97
+ // stretch untouched.
98
+ const resolvedVariant = variant !== undefined ? variant : (compact ? 'transparent' : 'outline');
82
99
  return (
83
- <div className={`flex flex-row gap-3 ${compact ? 'w-fit' : 'w-full'} ${className}`}>
100
+ <div className={`flex flex-row ${compact ? 'gap-2 w-fit' : 'gap-3 w-full'} ${className}`}>
84
101
  {links.map((link, index) => {
85
102
  const ariaLabel = link.label || link.platform;
86
103
  return link.onClick ? (
87
104
  <Button
88
105
  key={index}
89
106
  type="button"
90
- variant={variant}
91
- size="icon"
107
+ variant={resolvedVariant}
108
+ size={compact ? 'icon-sm' : 'icon'}
92
109
  className={compact ? undefined : 'flex-1'}
93
110
  aria-label={ariaLabel}
94
111
  onClick={link.onClick}
@@ -101,8 +118,8 @@ export function SocialIconRow({ className = '', links = defaultLinks, variant =
101
118
  // no asChild/<a> nesting.
102
119
  <Button
103
120
  key={index}
104
- variant={variant}
105
- size="icon"
121
+ variant={resolvedVariant}
122
+ size={compact ? 'icon-sm' : 'icon'}
106
123
  className={compact ? undefined : 'flex-1'}
107
124
  href={link.href}
108
125
  openInNewTab
@@ -31,6 +31,12 @@ const buttonVariants = cva(
31
31
  small: "p-[var(--spacing-system-xs)] text-h5 h-6 md:h-8",
32
32
  "small-legacy": "py-[var(--spacing-system-xs)] px-[var(--spacing-system-m)] h-10 text-[14px] font-bold", // Temporary alias for "small" to avoid breaking changes in AnnouncementBar's CTA button; will be removed in the future
33
33
  icon: "p-[var(--spacing-system-sf)] h-11 w-11 md:h-12 md:w-12 [&_svg]:h-4 [&_svg]:w-4 md:[&_svg]:h-6 md:[&_svg]:w-6",
34
+ // Quiet 32px icon target with a 16px glyph, fixed across breakpoints
35
+ // (Carbon ghost sm / Primer medium / shadcn icon-sm all pin 32px;
36
+ // ≥ the 24px WCAG 2.5.8 target floor). For icon actions that read as
37
+ // metadata rather than CTAs — author-page social rows, share rows.
38
+ // Pair with variant="transparent" for the ghost treatment.
39
+ "icon-sm": "p-[var(--spacing-system-xxs)] h-8 w-8 [&_svg]:h-4 [&_svg]:w-4",
34
40
  },
35
41
  fullWidth: {
36
42
  true: "w-full",
@@ -9,6 +9,9 @@ export interface EntityAuthor {
9
9
  full_name: string | null
10
10
  avatar_url: string | null
11
11
  job_title?: string | null
12
+ /** Author bio paragraph (`profiles.about`) — feeds the end-of-article
13
+ * `<ArticleAuthorByline bio>` on every detail page with an author. */
14
+ about?: string | null
12
15
  /** Public author-page slug — present only when the hub's public-author
13
16
  * gate passes (`is_real` profiles only). Feeds /authors/[slug] links. */
14
17
  slug?: string
@@ -132,6 +132,9 @@ describe('DEFAULT_CONTENT_SUFFIXES', () => {
132
132
  webinar: 'webinars',
133
133
  podcast: 'podcasts',
134
134
  event: 'events',
135
+ // Hub-side home of the segment: AUTHORS_PATH in lib/utils/breadcrumbs.ts
136
+ // (PUBLIC_URL_PATHS.author derives from it).
137
+ author: 'authors',
135
138
  })
136
139
  })
137
140
  })
@@ -44,6 +44,11 @@ export const DEFAULT_CONTENT_SUFFIXES: Record<string, string> = {
44
44
  webinar: 'webinars',
45
45
  podcast: 'podcasts',
46
46
  event: 'events',
47
+ // Author profile pages (`/authors/<slug>`) — identified by the profile
48
+ // slug. Mirrors the hub's `PUBLIC_URL_PATHS.author`, which derives from
49
+ // `AUTHORS_PATH` in the hub's `lib/utils/breadcrumbs.ts` (the hub-side
50
+ // SSOT for the segment).
51
+ author: 'authors',
47
52
  }
48
53
 
49
54
  /** Input to the unified content-href seam. ONE object covers both callers: