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