diff --git a/components/LayerRenderer.tsx b/components/LayerRenderer.tsx
index 77483f34..863f4edd 100644
--- a/components/LayerRenderer.tsx
+++ b/components/LayerRenderer.tsx
@@ -44,7 +44,7 @@ import FilterableCollection from '@/components/FilterableCollection';
import LocaleSelector from '@/components/layers/LocaleSelector';
import { usePagesStore } from '@/stores/usePagesStore';
import { useSettingsStore } from '@/stores/useSettingsStore';
-import { generateLinkHref, type LinkResolutionContext } from '@/lib/link-utils';
+import { generateLinkHref, resolveLinkAttrs, type LinkResolutionContext } from '@/lib/link-utils';
import { collectEditorHiddenLayerIds, type HiddenLayerInfo } from '@/lib/animation-utils';
import AnimationInitializer from '@/components/AnimationInitializer';
import { transformLayerIdsForInstance, resolveVariableLinks } from '@/lib/resolve-components';
@@ -599,7 +599,6 @@ const LayerItem: React.FC<{
// Buttons with link settings render as directly instead of being
// wrapped in which is invalid HTML
const isButtonWithLink = layer.name === 'button'
- && !isEditMode
&& !isInsideForm
&& isValidLinkSettings(layer.variables?.link);
if (isButtonWithLink) {
@@ -1421,6 +1420,7 @@ const LayerItem: React.FC<{
paragraphClasses,
SWIPER_CLASS_MAP[layer.name],
isSlideChild && 'swiper-slide',
+ buttonNeedsFit && 'w-fit',
enableDragDrop && !isEditing && !isLockedByOther && 'cursor-default',
isDragging && 'opacity-30',
showProjection && 'outline outline-1 outline-dashed outline-blue-400 bg-blue-50/10',
@@ -1463,6 +1463,25 @@ const LayerItem: React.FC<{
return null;
}
+ // Shared link resolution context — only built once, reused by button links,
+ // layer links, and link wrappers. Skipped in edit mode (no resolution needed).
+ const layerLinkContext: LinkResolutionContext | undefined = isEditMode ? undefined : {
+ pages,
+ folders,
+ collectionItemSlugs,
+ collectionItemId: collectionLayerItemId,
+ pageCollectionItemId,
+ collectionItemData: collectionLayerData,
+ pageCollectionItemData: pageCollectionItemData || undefined,
+ isPreview,
+ locale: currentLocale,
+ translations,
+ getAsset,
+ anchorMap,
+ resolvedAssets,
+ layerDataMap: effectiveLayerDataMap,
+ };
+
// Render element-specific content
const renderContent = () => {
// Component instances in EDIT MODE: render component's layers directly
@@ -1623,65 +1642,15 @@ const LayerItem: React.FC<{
...(!isEditMode && { suppressHydrationWarning: true }),
};
- // When a button is rendered as , apply link attributes directly
- if (isButtonWithLink && layer.variables?.link) {
- const btnLinkSettings = layer.variables.link;
- const btnLinkContext: LinkResolutionContext = {
- pages,
- folders,
- collectionItemSlugs,
- collectionItemId: collectionLayerItemId,
- pageCollectionItemId,
- collectionItemData: collectionLayerData,
- pageCollectionItemData: pageCollectionItemData || undefined,
- isPreview,
- locale: currentLocale,
- translations,
- getAsset,
- anchorMap,
- resolvedAssets,
- layerDataMap: effectiveLayerDataMap,
- };
- const btnLinkHref = generateLinkHref(btnLinkSettings, btnLinkContext);
- if (btnLinkHref) {
- elementProps.href = btnLinkHref;
- elementProps.target = btnLinkSettings.target || '_self';
- const btnLinkRel = btnLinkSettings.rel || (btnLinkSettings.target === '_blank' ? 'noopener noreferrer' : undefined);
- if (btnLinkRel) elementProps.rel = btnLinkRel;
- if (btnLinkSettings.download) elementProps.download = btnLinkSettings.download;
+ // Apply link attributes for elements rendered as (buttons with links or layers)
+ if (htmlTag === 'a' && layer.variables?.link) {
+ if (isButtonWithLink) {
+ elementProps.role = 'button';
+ delete elementProps.type;
}
- elementProps.role = 'button';
- delete elementProps.type;
- }
-
- // When an layer has link settings, apply href/target/rel directly
- if (htmlTag === 'a' && !isButtonWithLink && !isEditMode && layer.variables?.link) {
- const aLinkSettings = layer.variables.link;
- if (isValidLinkSettings(aLinkSettings)) {
- const aLinkContext: LinkResolutionContext = {
- pages,
- folders,
- collectionItemSlugs,
- collectionItemId: collectionLayerItemId,
- pageCollectionItemId,
- collectionItemData: collectionLayerData,
- pageCollectionItemData: pageCollectionItemData || undefined,
- isPreview,
- locale: currentLocale,
- translations,
- getAsset,
- anchorMap,
- resolvedAssets,
- layerDataMap: effectiveLayerDataMap,
- };
- const aLinkHref = generateLinkHref(aLinkSettings, aLinkContext);
- if (aLinkHref) {
- elementProps.href = aLinkHref;
- elementProps.target = aLinkSettings.target || '_self';
- const aLinkRel = aLinkSettings.rel || (aLinkSettings.target === '_blank' ? 'noopener noreferrer' : undefined);
- if (aLinkRel) elementProps.rel = aLinkRel;
- if (aLinkSettings.download) elementProps.download = aLinkSettings.download;
- }
+ if (layerLinkContext && isValidLinkSettings(layer.variables.link)) {
+ const linkAttrs = resolveLinkAttrs(layer.variables.link, layerLinkContext);
+ if (linkAttrs) Object.assign(elementProps, linkAttrs);
}
}
@@ -2886,53 +2855,34 @@ const LayerItem: React.FC<{
// Don't wrap layers inside component instances (they're not directly editable)
let content = renderContent();
- // Wrap with link if layer has link settings (published mode only)
- // In edit mode, links are not interactive to allow layer selection
+ // Wrap with link if layer has link settings
// Skip for buttons — they render as directly (see isButtonWithLink)
// Skip for layers — they already render as and nesting inside is invalid HTML
const linkSettings = layer.variables?.link;
- const shouldWrapWithLink = !isEditMode
- && !isButtonWithLink
+ const shouldWrapWithLink = !isButtonWithLink
&& htmlTag !== 'a'
&& !subtreeHasInteractiveDescendants
&& isValidLinkSettings(linkSettings);
if (shouldWrapWithLink && linkSettings) {
- // Build link context for layer-level link resolution
- const layerLinkContext: LinkResolutionContext = {
- pages,
- folders,
- collectionItemSlugs,
- collectionItemId: collectionLayerItemId,
- pageCollectionItemId,
- collectionItemData: collectionLayerData,
- pageCollectionItemData: pageCollectionItemData || undefined,
- isPreview,
- locale: currentLocale,
- translations,
- getAsset,
- anchorMap,
- resolvedAssets,
- layerDataMap: effectiveLayerDataMap,
- };
- const linkHref = generateLinkHref(linkSettings, layerLinkContext);
-
- if (linkHref) {
- const linkTarget = linkSettings.target || '_self';
- const linkRel = linkSettings.rel || (linkTarget === '_blank' ? 'noopener noreferrer' : undefined);
- const linkDownload = linkSettings.download;
-
+ if (isEditMode) {
content = (
-
+
{content}
);
+ } else if (layerLinkContext) {
+ const linkAttrs = resolveLinkAttrs(linkSettings, layerLinkContext);
+ if (linkAttrs) {
+ content = (
+
+ {content}
+
+ );
+ }
}
}
diff --git a/lib/link-utils.ts b/lib/link-utils.ts
index 7568d51a..a9a40ea3 100644
--- a/lib/link-utils.ts
+++ b/lib/link-utils.ts
@@ -533,3 +533,29 @@ export function looksLikePhone(value: string): boolean {
const digitCount = (trimmed.match(/\d/g) || []).length;
return /^[\d\s\-()+.]*$/.test(trimmed) && digitCount >= 7;
}
+
+export interface ResolvedLinkAttrs {
+ href: string;
+ target: string;
+ rel?: string;
+ download?: boolean;
+}
+
+/** Resolve link settings to HTML anchor attributes (href, target, rel, download) */
+export function resolveLinkAttrs(
+ linkSettings: LinkSettings,
+ context: LinkResolutionContext
+): ResolvedLinkAttrs | null {
+ const href = generateLinkHref(linkSettings, context);
+ if (!href) return null;
+
+ const target = linkSettings.target || '_self';
+ const rel = linkSettings.rel || (target === '_blank' ? 'noopener noreferrer' : undefined);
+
+ return {
+ href,
+ target,
+ ...(rel && { rel }),
+ ...(linkSettings.download && { download: linkSettings.download }),
+ };
+}