use next/link for internal links, even if absolute from the cms

This commit is contained in:
2026-07-10 03:24:00 +02:00
parent ca397070ea
commit 3892624382
12 changed files with 92 additions and 53 deletions
@@ -2,6 +2,7 @@ import { AssociationFragment } from "@/gql/graphql";
import { Image } from "@/components/general/Image";
import styles from './associationHeader.module.scss';
import { Breadcrumb } from "../general/Breadcrumb";
import { RichText } from "@/components/general/RichText";
import { Icon } from "../general/Icon";
export const AssociationHeader = ({
@@ -15,10 +16,9 @@ export const AssociationHeader = ({
<Breadcrumb link="/foreninger" text={association.associationType ? association.associationType : "Foreninger"} />
<h1>{association.title}</h1>
{association.lead && (
<div
className="lead"
dangerouslySetInnerHTML={{ __html: association.lead }}
/>
<div className="lead">
<RichText content={association.lead} />
</div>
)}
{association.websiteUrl && (
<a className="button" href={association.websiteUrl} target="_blank">
+7 -8
View File
@@ -3,6 +3,7 @@ import {
type ContactSectionBlockFragment,
type ContactSubsectionBlockFragment,
} from "@/gql/graphql";
import { RichText } from "@/components/general/RichText";
import styles from "./contactSection.module.scss";
import { Blocks } from "./Blocks";
@@ -37,10 +38,9 @@ export const ContactSectionBlock = ({
<section className={styles.contactSection}>
<h2 className={styles.heading}>{block.title}</h2>
{block.text && (
<p
className={styles.intro}
dangerouslySetInnerHTML={{ __html: block.text }}
/>
<div className={styles.intro}>
<RichText content={block.text} />
</div>
)}
<Blocks blocks={block.blocks} />
</section>
@@ -56,10 +56,9 @@ export const ContactSubsectionBlock = ({
<section className={styles.contactSubsection}>
<h3 className={styles.heading}>{block.title}</h3>
{block.text && (
<p
className={styles.intro}
dangerouslySetInnerHTML={{ __html: block.text }}
/>
<div className={styles.intro}>
<RichText content={block.text} />
</div>
)}
<Blocks blocks={block.blocks} />
</section>
+4 -4
View File
@@ -1,5 +1,6 @@
import { graphql } from "@/gql";
import { type FactBoxBlockFragment } from "@/gql/graphql";
import { RichText } from "@/components/general/RichText";
import styles from "./factBoxBlock.module.scss";
const FactBoxBlockFragmentDefinition = graphql(`
@@ -23,10 +24,9 @@ export const FactBoxBlock = ({
className={styles.factBox}
data-background-color={block.backgroundColor ?? ""}
>
<div
className={styles.factBoxContent}
dangerouslySetInnerHTML={{ __html: block.factBoxBody }}
/>
<div className={styles.factBoxContent}>
<RichText content={block.factBoxBody} />
</div>
</div>
</section>
);
+9 -2
View File
@@ -2,7 +2,9 @@ import { graphql, unmaskFragment } from "@/gql";
import { type FeaturedBlockFragment } from "@/gql/graphql";
import Link from "next/link";
import { Image } from "@/components/general/Image";
import { RichText } from "@/components/general/RichText";
import { ImageFragmentDefinition } from "@/lib/common";
import { internalHref } from "@/lib/links";
import styles from "./featuredBlock.module.scss";
const FeaturedBlockFragmentDefinition = graphql(`
@@ -44,6 +46,9 @@ export const FeaturedBlock = ({
);
// TODO: fetch image from target page
const pageUrl = block.featuredPage.url;
const featuredHref = pageUrl ? internalHref(pageUrl) ?? pageUrl : "#";
return (
<div
className={styles.featuredBlock}
@@ -51,8 +56,10 @@ export const FeaturedBlock = ({
>
<div className={styles.text}>
<h2>{block.title}</h2>
<div dangerouslySetInnerHTML={{ __html: block.featuredBlockText }} />
<Link href={block.featuredPage.url ?? "#"}>
<div>
<RichText content={block.featuredBlockText} />
</div>
<Link href={featuredHref}>
{block.linkText} &rarr;
</Link>
</div>
+7 -12
View File
@@ -1,5 +1,6 @@
import { graphql, unmaskFragment } from "@/gql";
import { type ScheduleBlockFragment } from "@/gql/graphql";
import { RichText } from "@/components/general/RichText";
import styles from "./scheduleBlock.module.scss";
const ScheduleItemFragmentDefinition = graphql(`
@@ -45,19 +46,13 @@ export const ScheduleBlock = ({
<div key={index} className={styles.item}>
<dt className={styles.time}>{scheduleItem.time}</dt>
<dd className={styles.body}>
<div
className={styles.title}
dangerouslySetInnerHTML={{
__html: scheduleItem.scheduleItemTitle,
}}
/>
<div className={styles.title}>
<RichText content={scheduleItem.scheduleItemTitle} />
</div>
{scheduleItem.description && (
<div
className={styles.description}
dangerouslySetInnerHTML={{
__html: scheduleItem.description,
}}
/>
<div className={styles.description}>
<RichText content={scheduleItem.description} />
</div>
)}
</dd>
</div>
+4 -4
View File
@@ -5,6 +5,7 @@ import { EventDetails } from "@/components/events/EventDetails";
import { EventHeader } from "@/components/events/EventHeader";
import { BgPig } from "@/components/general/BgPig";
import { PageContent } from "@/components/general/PageContent";
import { RichText } from "@/components/general/RichText";
import { getEventPig } from "@/lib/event";
const eventBySlugQuery = graphql(`
@@ -45,10 +46,9 @@ export function EventPageView({ event }: EventPageViewProps) {
<EventHeader event={event} />
<EventDetails event={event} />
{event.lead && (
<div
className="lead event-lead"
dangerouslySetInnerHTML={{ __html: event.lead }}
/>
<div className="lead event-lead">
<RichText content={event.lead} />
</div>
)}
<PageContent blocks={event.body} />
</main>
+4 -1
View File
@@ -1,3 +1,4 @@
import { RichText } from "@/components/general/RichText";
import styles from "./pageHeader.module.scss";
export const PageHeader = ({
@@ -13,7 +14,9 @@ export const PageHeader = ({
<div className={`${styles.pageHeader} ${align && styles[align]}`}>
<h1 className={styles.title}>{heading}</h1>
{lead && (
<div className="lead" dangerouslySetInnerHTML={{ __html: lead }} />
<div className="lead">
<RichText content={lead} />
</div>
)}
</div>
);
+22 -9
View File
@@ -4,26 +4,39 @@ import parse, {
type DOMNode,
type HTMLReactParserOptions,
} from "html-react-parser";
import Link from "next/link";
import { ButtonLink } from "@/components/general/ButtonLink";
import { internalHref } from "@/lib/links";
/**
* Renders CMS rich text (HTML) as React, swapping inline button markers
* (`<a class="button">` from the button-link Draftail feature) for the
* ButtonLink component. Everything else renders as plain HTML.
* Renders CMS rich text (HTML) as React. Everything renders as plain HTML,
* except:
*
* - inline button markers (`<a class="button">` from the button-link Draftail
* feature) become the ButtonLink component
* - links pointing at this site become next/link
*/
const options: HTMLReactParserOptions = {
replace: (node) => {
if (
node instanceof Element &&
node.name === "a" &&
node.attribs.class?.split(/\s+/).includes("button")
) {
if (!(node instanceof Element) || node.name !== "a") {
return;
}
const href = node.attribs.href ?? "#";
const internal = internalHref(href);
if (node.attribs.class?.split(/\s+/).includes("button")) {
return (
<ButtonLink href={node.attribs.href ?? "#"}>
<ButtonLink href={internal ?? href}>
{domToReact(node.children as DOMNode[], options)}
</ButtonLink>
);
}
if (internal) {
return (
<Link href={internal}>
{domToReact(node.children as DOMNode[], options)}
</Link>
);
}
},
};
+4 -4
View File
@@ -4,6 +4,7 @@ import { getClient } from "@/app/client";
import { Breadcrumb } from "@/components/general/Breadcrumb";
import { ImageFigure } from "@/components/general/Image";
import { PageContent } from "@/components/general/PageContent";
import { RichText } from "@/components/general/RichText";
import { formatDate } from "@/lib/date";
const newsBySlugQuery = graphql(`
@@ -47,10 +48,9 @@ export function NewsPageView({ news }: NewsPageViewProps) {
/>
<h1 className="news-title">{news.title}</h1>
{news.lead && (
<div
className="lead"
dangerouslySetInnerHTML={{ __html: news.lead }}
/>
<div className="lead">
<RichText content={news.lead} />
</div>
)}
{featuredImage && (
<ImageFigure
+4 -4
View File
@@ -2,6 +2,7 @@ import { graphql, unmaskFragment } from "@/gql";
import { type SponsorFragment } from "@/gql/graphql";
import { Image } from "../general/Image";
import { ImageFragmentDefinition } from "@/lib/common";
import { RichText } from "@/components/general/RichText";
import styles from "./sponsorList.module.scss";
export const SponsorFragmentDefinition = graphql(`
@@ -35,10 +36,9 @@ const SponsorItem = ({ sponsor }: { sponsor: SponsorFragment }) => {
<div className={styles.text}>
<h2>{name}</h2>
{text && (
<p
className={styles.sponsorText}
dangerouslySetInnerHTML={{ __html: text }}
/>
<div className={styles.sponsorText}>
<RichText content={text} />
</div>
)}
{website && (
<p className={styles.website}>
+4 -1
View File
@@ -1,3 +1,4 @@
import { RichText } from "@/components/general/RichText";
import styles from "./studioHeader.module.scss";
export const StudioHeader = ({
@@ -54,7 +55,9 @@ export const StudioHeader = ({
</div>
</div>
{lead && (
<div className={`lead ${styles.lead}`} dangerouslySetInnerHTML={{ __html: lead }} />
<div className={`lead ${styles.lead}`}>
<RichText content={lead} />
</div>
)}
</div>
);
+19
View File
@@ -0,0 +1,19 @@
const INTERNAL_ORIGINS = [
process.env.URL,
"https://neuf.no",
].filter(Boolean) as string[];
/** Root-relative href if the URL points at this site, else null. */
export function internalHref(href: string): string | null {
if (href.startsWith("/")) {
return href;
}
const origin = INTERNAL_ORIGINS.find(
(o) => href === o || href.startsWith(o + "/"),
);
if (!origin) {
return null;
}
const url = new URL(href);
return url.pathname + url.search + url.hash;
}