Feature: Implement Embedding Links in Titles of all Section Items (#2662)

* feat: add options.showLinkInTitle to baseItemSchema

Add itemOptionsSchema with showLinkInTitle boolean property to control
whether the website URL is rendered as a hyperlink on the title instead
of a separate link at the bottom. The field is optional for backwards
compatibility with existing resumes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: add hideLabelButton prop to URLInput

When hideLabelButton is true, the tag/label button is hidden from the
URL input. This is used when showLinkInTitle is enabled since the label
is not needed when the link is shown in the title.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: create LinkedTitle component for title-as-link rendering

Add a reusable component that conditionally renders the title as a
hyperlink when showLinkInTitle is true and a website URL is provided.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: add showLinkInTitle option to experience section

- Add Switch toggle in experience dialog for showLinkInTitle option
- Update URLInput to hide label button when showLinkInTitle is enabled
- Use LinkedTitle component in experience-item for conditional link rendering
- Hide bottom website link when showLinkInTitle is enabled

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: add showLinkInTitle option to all section items

- Update education, projects, awards, certifications, publications,
  volunteer, references, and profiles dialogs with Switch toggle
- Add LinkedTitle component usage in all corresponding item components
- Conditionally hide bottom website link when showLinkInTitle is enabled
- Add hideLabelButton prop to URLInput when showLinkInTitle is enabled

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: extract i18n strings for showLinkInTitle feature

Add "Show link in title" translation string to all locale catalogs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* update dependencies, fix an issue with glalie template and non-clickable links, fix better-auth type error

* remove unused export

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Amruth Pillai
2026-01-31 12:16:23 +01:00
committed by GitHub
parent 4d368ab6f6
commit 8d347f5162
82 changed files with 1085 additions and 193 deletions
@@ -2,6 +2,7 @@ import { TiptapContent } from "@/components/input/rich-input";
import type { SectionItem } from "@/schema/resume/data";
import { stripHtml } from "@/utils/string";
import { cn } from "@/utils/style";
import { LinkedTitle } from "../linked-title";
import { PageLink } from "../page-link";
type AwardsItemProps = SectionItem<"awards"> & {
@@ -15,7 +16,12 @@ export function AwardsItem({ className, ...item }: AwardsItemProps) {
<div className="section-item-header awards-item-header">
{/* Row 1 */}
<div className="flex items-start justify-between gap-x-2">
<strong className="section-item-title awards-item-title">{item.title}</strong>
<LinkedTitle
title={item.title}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title awards-item-title"
/>
<span className="section-item-metadata awards-item-date shrink-0 text-end">{item.date}</span>
</div>
@@ -31,9 +37,11 @@ export function AwardsItem({ className, ...item }: AwardsItemProps) {
</div>
{/* Website */}
<div className="section-item-website awards-item-website">
<PageLink {...item.website} label={item.website.label} />
</div>
{!item.options?.showLinkInTitle && (
<div className="section-item-website awards-item-website">
<PageLink {...item.website} label={item.website.label} />
</div>
)}
</div>
);
}
@@ -2,6 +2,7 @@ import { TiptapContent } from "@/components/input/rich-input";
import type { SectionItem } from "@/schema/resume/data";
import { stripHtml } from "@/utils/string";
import { cn } from "@/utils/style";
import { LinkedTitle } from "../linked-title";
import { PageLink } from "../page-link";
type CertificationsItemProps = SectionItem<"certifications"> & {
@@ -15,7 +16,12 @@ export function CertificationsItem({ className, ...item }: CertificationsItemPro
<div className="section-item-header certifications-item-header">
{/* Row 1 */}
<div className="flex items-start justify-between gap-x-2">
<strong className="section-item-title certifications-item-title">{item.title}</strong>
<LinkedTitle
title={item.title}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title certifications-item-title"
/>
<span className="section-item-metadata certifications-item-date shrink-0 text-end">{item.date}</span>
</div>
@@ -36,9 +42,11 @@ export function CertificationsItem({ className, ...item }: CertificationsItemPro
</div>
{/* Website */}
<div className="section-item-website certifications-item-website">
<PageLink {...item.website} label={item.website.label} />
</div>
{!item.options?.showLinkInTitle && (
<div className="section-item-website certifications-item-website">
<PageLink {...item.website} label={item.website.label} />
</div>
)}
</div>
);
}
@@ -2,6 +2,7 @@ import { TiptapContent } from "@/components/input/rich-input";
import type { SectionItem } from "@/schema/resume/data";
import { stripHtml } from "@/utils/string";
import { cn } from "@/utils/style";
import { LinkedTitle } from "../linked-title";
import { PageLink } from "../page-link";
type EducationItemProps = SectionItem<"education"> & {
@@ -15,7 +16,12 @@ export function EducationItem({ className, ...item }: EducationItemProps) {
<div className="section-item-header education-item-header mb-2">
{/* Row 1 */}
<div className="flex items-start justify-between gap-x-2">
<strong className="section-item-title education-item-title">{item.school}</strong>
<LinkedTitle
title={item.school}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title education-item-title"
/>
<span className="section-item-metadata education-item-degree-grade shrink-0 text-end">
{[item.degree, item.grade].filter(Boolean).join(" • ")}
</span>
@@ -38,9 +44,11 @@ export function EducationItem({ className, ...item }: EducationItemProps) {
</div>
{/* Website */}
<div className="section-item-website education-item-website">
<PageLink {...item.website} label={item.website.label} />
</div>
{!item.options?.showLinkInTitle && (
<div className="section-item-website education-item-website">
<PageLink {...item.website} label={item.website.label} />
</div>
)}
</div>
);
}
@@ -2,6 +2,7 @@ import { TiptapContent } from "@/components/input/rich-input";
import type { SectionItem } from "@/schema/resume/data";
import { stripHtml } from "@/utils/string";
import { cn } from "@/utils/style";
import { LinkedTitle } from "../linked-title";
import { PageLink } from "../page-link";
type ExperienceItemProps = SectionItem<"experience"> & {
@@ -15,7 +16,12 @@ export function ExperienceItem({ className, ...item }: ExperienceItemProps) {
<div className="section-item-header experience-item-header">
{/* Row 1 */}
<div className="flex items-start justify-between gap-x-2">
<strong className="section-item-title experience-item-title">{item.company}</strong>
<LinkedTitle
title={item.company}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title experience-item-title"
/>
<span className="section-item-metadata experience-item-location shrink-0 text-end">{item.location}</span>
</div>
@@ -34,9 +40,11 @@ export function ExperienceItem({ className, ...item }: ExperienceItemProps) {
</div>
{/* Website */}
<div className="section-item-website experience-item-website">
<PageLink {...item.website} label={item.website.label} />
</div>
{!item.options?.showLinkInTitle && (
<div className="section-item-website experience-item-website">
<PageLink {...item.website} label={item.website.label} />
</div>
)}
</div>
);
}
@@ -1,5 +1,6 @@
import type { SectionItem } from "@/schema/resume/data";
import { cn } from "@/utils/style";
import { LinkedTitle } from "../linked-title";
import { PageIcon } from "../page-icon";
import { PageLink } from "../page-link";
@@ -13,15 +14,22 @@ export function ProfilesItem({ className, ...item }: ProfilesItemProps) {
{/* Header */}
<div className="section-item-header profiles-item-header flex items-center gap-x-1.5">
<PageIcon icon={item.icon} className="section-item-icon profiles-item-icon" />
<strong className="section-item-title profiles-item-network">{item.network}</strong>
<LinkedTitle
title={item.network}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title profiles-item-network"
/>
</div>
{/* Website */}
<PageLink
{...item.website}
label={item.website.label || item.username}
className="section-item-website profiles-item-website"
/>
{!item.options?.showLinkInTitle && (
<PageLink
{...item.website}
label={item.website.label || item.username}
className="section-item-website profiles-item-website"
/>
)}
</div>
);
}
@@ -2,6 +2,7 @@ import { TiptapContent } from "@/components/input/rich-input";
import type { SectionItem } from "@/schema/resume/data";
import { stripHtml } from "@/utils/string";
import { cn } from "@/utils/style";
import { LinkedTitle } from "../linked-title";
import { PageLink } from "../page-link";
type ProjectsItemProps = SectionItem<"projects"> & {
@@ -15,7 +16,12 @@ export function ProjectsItem({ className, ...item }: ProjectsItemProps) {
<div className="section-item-header projects-item-header">
{/* Row 1 */}
<div className="flex items-start justify-between gap-x-2">
<strong className="section-item-title projects-item-title">{item.name}</strong>
<LinkedTitle
title={item.name}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title projects-item-title"
/>
<span className="section-item-metadata projects-item-period shrink-0 text-end">{item.period}</span>
</div>
</div>
@@ -28,9 +34,11 @@ export function ProjectsItem({ className, ...item }: ProjectsItemProps) {
</div>
{/* Website */}
<div className="section-item-website projects-item-website">
<PageLink {...item.website} label={item.website.label} />
</div>
{!item.options?.showLinkInTitle && (
<div className="section-item-website projects-item-website">
<PageLink {...item.website} label={item.website.label} />
</div>
)}
</div>
);
}
@@ -2,6 +2,7 @@ import { TiptapContent } from "@/components/input/rich-input";
import type { SectionItem } from "@/schema/resume/data";
import { stripHtml } from "@/utils/string";
import { cn } from "@/utils/style";
import { LinkedTitle } from "../linked-title";
import { PageLink } from "../page-link";
type PublicationsItemProps = SectionItem<"publications"> & {
@@ -15,7 +16,12 @@ export function PublicationsItem({ className, ...item }: PublicationsItemProps)
<div className="section-item-header publications-item-header">
{/* Row 1 */}
<div className="flex items-start justify-between gap-x-2">
<strong className="section-item-title publications-item-title">{item.title}</strong>
<LinkedTitle
title={item.title}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title publications-item-title"
/>
<span className="section-item-metadata publications-item-date shrink-0 text-end">{item.date}</span>
</div>
@@ -36,9 +42,11 @@ export function PublicationsItem({ className, ...item }: PublicationsItemProps)
</div>
{/* Website */}
<div className="section-item-website publications-item-website">
<PageLink {...item.website} label={item.website.label} />
</div>
{!item.options?.showLinkInTitle && (
<div className="section-item-website publications-item-website">
<PageLink {...item.website} label={item.website.label} />
</div>
)}
</div>
);
}
@@ -2,6 +2,7 @@ import { TiptapContent } from "@/components/input/rich-input";
import type { SectionItem } from "@/schema/resume/data";
import { stripHtml } from "@/utils/string";
import { cn } from "@/utils/style";
import { LinkedTitle } from "../linked-title";
import { PageLink } from "../page-link";
type ReferencesItemProps = SectionItem<"references"> & {
@@ -15,7 +16,12 @@ export function ReferencesItem({ className, ...item }: ReferencesItemProps) {
<div className="section-item-header references-item-header">
{/* Row 1 */}
<div className="flex items-start justify-between gap-x-2">
<strong className="section-item-title references-item-name">{item.name}</strong>
<LinkedTitle
title={item.name}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title references-item-name"
/>
</div>
{/* Row 2 */}
@@ -37,11 +43,13 @@ export function ReferencesItem({ className, ...item }: ReferencesItemProps) {
<span className="section-item-metadata references-item-phone inline-block">{item.phone}</span>
{/* Row 2 */}
<PageLink
{...item.website}
label={item.website.label}
className="section-item-website references-item-website"
/>
{!item.options?.showLinkInTitle && (
<PageLink
{...item.website}
label={item.website.label}
className="section-item-website references-item-website"
/>
)}
</div>
</div>
);
@@ -2,6 +2,7 @@ import { TiptapContent } from "@/components/input/rich-input";
import type { SectionItem } from "@/schema/resume/data";
import { stripHtml } from "@/utils/string";
import { cn } from "@/utils/style";
import { LinkedTitle } from "../linked-title";
import { PageLink } from "../page-link";
type VolunteerItemProps = SectionItem<"volunteer"> & {
@@ -15,7 +16,12 @@ export function VolunteerItem({ className, ...item }: VolunteerItemProps) {
<div className="section-item-header volunteer-item-header">
{/* Row 1 */}
<div className="flex items-start justify-between gap-x-2">
<strong className="section-item-title volunteer-item-title">{item.organization}</strong>
<LinkedTitle
title={item.organization}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title volunteer-item-title"
/>
<span className="section-item-metadata volunteer-item-period shrink-0 text-end">{item.period}</span>
</div>
@@ -33,9 +39,11 @@ export function VolunteerItem({ className, ...item }: VolunteerItemProps) {
</div>
{/* Website */}
<div className="section-item-website volunteer-item-website">
<PageLink {...item.website} label={item.website.label} />
</div>
{!item.options?.showLinkInTitle && (
<div className="section-item-website volunteer-item-website">
<PageLink {...item.website} label={item.website.label} />
</div>
)}
</div>
);
}
@@ -0,0 +1,20 @@
import { cn } from "@/utils/style";
type LinkedTitleProps = {
title: string;
website?: { url: string; label: string };
showLinkInTitle?: boolean;
className?: string;
};
export function LinkedTitle({ title, website, showLinkInTitle, className }: LinkedTitleProps) {
if (showLinkInTitle && website?.url) {
return (
<a href={website.url} target="_blank" rel="noopener" className={cn("inline-block", className)}>
<strong>{title}</strong>
</a>
);
}
return <strong className={className}>{title}</strong>;
}
@@ -38,7 +38,7 @@ export function ChikoritaTemplate({ pageIndex, pageLayout }: TemplateProps) {
<div className="template-chikorita page-content">
{/* Sidebar Background */}
{!fullWidth && (
<div className="page-sidebar-background absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color) ltr:end-0 rtl:start-0" />
<div className="page-sidebar-background pointer-events-none absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color) ltr:end-0 rtl:start-0" />
)}
{isFirstPage && <Header />}
+1 -1
View File
@@ -37,7 +37,7 @@ export function DitgarTemplate({ pageIndex, pageLayout }: TemplateProps) {
<div className="template-ditgar page-content">
{/* Sidebar Background */}
{(!fullWidth || isFirstPage) && (
<div className="page-sidebar-background absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20 ltr:start-0 rtl:end-0" />
<div className="page-sidebar-background pointer-events-none absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20 ltr:start-0 rtl:end-0" />
)}
<div className="flex">
+1 -1
View File
@@ -27,7 +27,7 @@ export function GengarTemplate({ pageIndex, pageLayout }: TemplateProps) {
<div className="template-gengar page-content">
{/* Sidebar Background */}
{(!fullWidth || isFirstPage) && (
<div className="page-sidebar-background absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20 ltr:start-0 rtl:end-0" />
<div className="page-sidebar-background pointer-events-none absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20 ltr:start-0 rtl:end-0" />
)}
<div className="flex">
+3 -3
View File
@@ -27,14 +27,14 @@ export function GlalieTemplate({ pageIndex, pageLayout }: TemplateProps) {
<div className="template-glalie page-content">
{/* Sidebar Background */}
{(!fullWidth || isFirstPage) && (
<div className="page-sidebar-background absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20 ltr:start-0 rtl:end-0" />
<div className="page-sidebar-background pointer-events-none absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20 ltr:start-0 rtl:end-0" />
)}
<div className="flex">
{(!fullWidth || isFirstPage) && (
<aside
data-layout="sidebar"
className="group page-sidebar flex w-(--page-sidebar-width) shrink-0 flex-col space-y-4 px-(--page-margin-x) pt-(--page-margin-y)"
className="group page-sidebar z-10 flex w-(--page-sidebar-width) shrink-0 flex-col space-y-4 px-(--page-margin-x) pt-(--page-margin-y)"
>
{isFirstPage && <Header />}
@@ -49,7 +49,7 @@ export function GlalieTemplate({ pageIndex, pageLayout }: TemplateProps) {
</aside>
)}
<main data-layout="main" className="group page-main">
<main data-layout="main" className="group page-main z-10">
<div className="space-y-4 px-(--page-margin-x) pt-(--page-margin-y)">
{main.map((section) => {
const Component = getSectionComponent(section, { sectionClassName });