use next/link for internal links, even if absolute from the cms
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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} →
|
{block.linkText} →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user