Closed milewskibogumil closed 2 months ago
Hi @milewskibogumil I've had a play on Stackblitz astro-portabletext issue #155 and there is a quirk with Astro.
Have a look at the <Heading />
component and you'll notice a child <span>
. With it, the styling works and without it the styling is lost.
The issue lies within the page
. You need to wrap the <Heading />
component with a <Layout />
component to make it stick.
I would however, approach rendering Portable Text content, in a different manner as <PortableText />
shouldn't be nested within a heading.
Extend astro-portabletext
Block component
/* BlockExt.astro */
---
import type { Props as $, Block as BlockType } from "astro-portabletext/types";
import { Block } from "astro-portabletext/components";
import Heading from "./Heading.astro"; // 👈 custom heading
export type Props = $<BlockType>;
const props = Astro.props;
// const styleIs = (style: string) => style === props.node.style;
const isHeading = /^h[1-6]$/.test(props.node.style);
const Cmp = isHeading ? (
Heading // custom heading
) : (
Block // fallback to `astro-portabletext`
)
---
<Cmp {...props}><slot /></Cmp>
/* Heading.astro */
---
import type { HTMLAttributes } from 'astro/types';
import type { Props as $, Block as BlockType } from "astro-portabletext/types";
type Props = $<Block>;
const { node } = Astro.props;
const HeadingTag = node.style;
---
<HeadingTag>
<slot />
</HeadingTag>
<style lang="scss">
h2 {
color: red;
span {
color: blue;
}
}
</style>
/* some page */
---
import { PortableText } from "astro-portabletext";
import Layout from "../layouts/Layout.astro";
import BlockExt from "path/to/BlockExt.astro";
const { portableTextContent } = await someQuery();
---
<Layout>
<PortableText value={portableTextContent} components={{ block: BlockExt }} />
</Layout>
You can go a step further by extending PortableText component.
I hope this helps, let me know your thoughts.
Tom, thanks for your suggestions!
Initially I get only the normal
Portable Text, not styled as Heading from my CMS. Then I do all the transformation (converting the simple <p>
to initial <h?>
.
The final code, which works as I intended, looks like this:
/* page.astro */
---
const { portableTextContent } = await someQuery();
---
<Heading level="h2" value={portableTextContent} />
<style lang="scss">
h2 {
color: red;
span {
color: blue;
}
}
</style>
/* Heading.astro */
---
import { PortableText } from 'astro-portabletext';
import Block from './Block.astro';
import type { PortableTextBlock } from '@portabletext/types';
import type { HTMLAttributes } from 'astro/types';
type Props = {
level: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
value: PortableTextBlock[];
} & HTMLAttributes<'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'>;
const { level, value, ...props } = Astro.props;
const HeadingTag = level;
---
<HeadingTag {...props}>
{
value.map((block: PortableTextBlock, index: number) =>
index > 0 ? (
<span {...props}>
<PortableText value={block} components={{ block: Block }} />
</span>
) : (
<PortableText value={block} components={{ block: Block }} />
),
)
}
</HeadingTag>
/* Block.astro */
<slot/>
And I have concerns only about Block.astro
, but in Astro it is probably impossible to do it any other way? In React it would be enough to use something just like: components={{ block: ({ children }) => children }}
I'll have to get back to you on this. The ideal is to use usePortableText however, this is not working as expected. I'll have to do some checking.
Okay, thank you @theisel! Waiting for more information about that.
Hi @milewskibogumil the issue lies in how the Portable Text is presented.
You have mentioned
Initially I get only the
normal
Portable Text, not styled as Heading from my CMS. Then I do all the transformation (converting the simple<p>
to initial<h?>
.
Out of interest which CMS are you using? If you are using Sanity, I would look into implementing decorators or annotations.
astro-portabletext
is simple, it hands over data to relevant components.
As a guide, have a look at the following code. I have removed all Typescript stuff to make it easier to read. You will notice that the span with text "One" has a mark.
Let me know how things go.
// pages/*.astro
---
import { PortableText } from "astro-portabletext";
import Layout from "../layouts/Layout.astro";
import BlockExt from "../components/BlockExt.astro";
import MarkExt from "../components/MarkExt.astro";
const value = [
{
"_type": "block",
"style": "h2",
"children": [
{
"_type": "span",
"text": "Heading "
},
{
"_type": "span",
"text": "One",
"marks": ["abc123"], // 👈 this is where the magic happens
},
],
"markDefs": [
{
"_type": "highlight",
"_key": "abc123"
}
]
},
];
const components = { block: BlockExt, mark: MarkExt };
---
<Layout>
<PortableText {value} {components} />
</Layout>
// BlockExt.astro
---
import { Block } from "astro-portabletext/components";
import Heading from "./Heading.astro"; // 👈 custom heading
const props = Astro.props;
const isHeading = /^h[1-6]$/.test(props.node.style);
const Cmp = isHeading ? (
Heading // custom heading
) : (
Block // fallback to `astro-portabletext`
)
---
<Cmp {...props}><slot /></Cmp>
// Heading.astro
---
const { node, index, isInline, ...rest } = Astro.props;
const HeadingTag = node.style;
---
<HeadingTag class="heading" {...rest}><slot /></HeadingTag>
<style>
.heading {
color: red;
}
</style>
// MarkExt.astro
---
import { Mark } from "astro-portabletext/components";
import Highlight from "./Highlight.astro"; // 👈 custom mark
const props = Astro.props;
const { node } = props;
const markTypeIs = (markType: string) => markType === node.markType;
const Cmp = markTypeIs('highlight') ? Highlight : Mark;
---
<Cmp {...props}><slot /></Cmp>
// Hightlight.astro
---
/*
You may need to do the following just in case of whitespace
const html = await Astro.slots.render('default').then((str) => str.trim(());
<span>{html}</span>
*/
---
<span><slot /></span>
<style>
span {
background: green;
color: white;
padding-inline: 0.25rem;
margin-inline: 0.25rem;
}
</style>
Closing this issue as there hasn't been any recent activity. If you have any further questions or updates, feel free to reopen or create a new issue.
Hello there!
I'm creating a reusable
Heading
element with Portable Text. The main functionality is to return just h1-h6 tag, with content inside, or<span>
inside the main element, if there will be an enter inside a Portable Text.Example:
<h2 data-astro-cid-someid>Some <strong>bold</strong> content</h2>
<h2 data-astro-cid-someid>Some content <span>another text line</span></h2>
Whole page looks like:
My
Heading.astro
component looks like:And
Block.astro
:The problem is that I cannot pass a
{...props}
from<Heading>
, to<span>
, and that means that scoped SCSS is not working.Any ideas how to solve that?