I have a problem with inconsistent behavior between the site generated by gatsby develop and gatsby build. The result is a site that works in development but not production.
A summary of my site:
A simple blog-like site (personal profiles instead of blog posts). The index
page is a list of people, and each item in that list links to that person's profile page.
I'm using Gatsby to build the site. My data (the personal profiles) are entries hosted on the Contentful headless CMS. I'm using the gatsby-source-contentful source plugin.
High level description of the problem
I cannot shuffle the order of the profile list items on the index page. The only behavior where my site goes beyond any basic gatsby tutorial is that I want to randomize the list of profiles on my index page (to give everyone a fair chance at being listed at the top).
gatsby build generates a static index page with the list in one permutation.
When loaded in a browser the ThumbList component re-shuffles those items to another permutation on render and some sub-elements are not properly managed by react and stay stuck as other elements shift. This leads, for example, to profile images paired with the wrong name.
The code
The following code is somewhat summarized for readability.
src/pages/index.js:
import React from "react"
import Layout from "../components/layout"
import ThumbList from "../components/thumbList"
import { graphql } from "gatsby"
export default ({data}) => {
// people are called "creators" in the app
const creatorData = data.allContentfulCreator.edges
const shuffledData = shuffle(creatorData.slice(0))
return (
<Layout>
<ThumbList data={shuffledData} />
</Layout>
)
}
const shuffle = (a) => {
// Fisher-Yates randomized array in-place shuffle algo
// ...
return a
}
export const query = graphql`
{
allContentfulCreator {
edges {
node {
id
slug
name
bio {
id
bio
}
mainImage {
file {
url
}
}
}
}
}
}
`
src/components/thumbList.js:
import React from "react"
import { Link } from "gatsby"
// A list of creator profile links, with name and picture thumbnail
export default ({data}) => {
return (
<div>
<ul>
{
data.map(({node}) => {
const creator = node
const link = "/" + creator.slug
const image = "https:" + creator.mainImage.file.url
return (
<li key={creator.id}>
<Link to={link}>
{creator.name}
</Link>
<img src={image} />
</li>
)
})
}
</ul>
</div>
)
}
The result of gatsby build is an index.html containing:
<ul>
<li>
<a href="/alice">
Alice
</a>
<img src="cdn.com/alice.jpg">
</li>
<li>
<a href="/bob">
Bob
</a>
<img src="cdn.com/bob.jpg">
</li>
<li>
<a href="/eve">
Eve
</a>
<img src="cdn.com/eve.jpg">
</li>
</ul>
However, when viewing the index page in the browser (via gatsby serve or a deployed version of the site) the live react ThumbList component again shuffles the data in its render method.
The result re-rendered html:
<ul>
<li>
<a href="/alice">
Bob
</a>
<img src="cdn.com/alice.jpg">
</li>
<li>
<a href="/bob">
Eve
</a>
<img src="cdn.com/bob.jpg">
</li>
<li>
<a href="/eve">
Alice
</a>
<img src="cdn.com/eve.jpg">
</li>
</ul>
Here only the text nodes are rearranged to match the new order (confirmed by console logging the array order), but the links and image elements remain stuck where they were in the static build. Now the names, images, and links are scrambled.
Two other things to note:
All works fine with gatsby develop. I guess it's because in development index.html is generated without its static content in the body - allowing react complete control over the DOM from the start with no
static scaffolding to confuse it.
Using the react inspector I see that the virtual DOM and the real DOM have gotten out of sync. React thinks it has correctly shuffled the list items. Inspector shows something like:
(very abbreviated for readability)
<ul>
<li key="165e2405">
<GatsbyLink to="/bob">
Bob
</GatsbyLink>
<img src="cdn.com/bob.jpg"></img>
</li>
<li key="067f9afc">
<GatsbyLink to="/eve">
Eve
</GatsbyLink>
<img src="cdn.com/eve.jpg"></img>
</li>
<li key="ca4b82bf">
<GatsbyLink to="/alice">
Alice
</GatsbyLink>
<img src="cdn.com/alice.jpg"></img>
</li>
</ul>
My questions
Is this just an un-Gatsby-like approach? This description of a "Hybrid app page" seems to imply that you can either have static or dynamic components. I suppose I'm trying to have it both ways: I want the
profiles fetched from contentful via graphql during build so it can be available via static HTML + pre-built json data files (e.g. /static/d/556/path---index-6a9-L7r5Sntxcv3RUIoHYIR3Qqm9Jmg.json), but
then I want to dynamically shuffle that data and rearrange the DOM on render. Is that not possible with Gatsby? Do I need to give up the pre-fetched data during build and just consider that a dynamic component and fetch the
data via the Contentful API in componentDidMount?
If this approach should be OK, what am I doing wrong?
If this approach is not idomatic, is there a way to modify (shuffle) the data queried via graphql at build time? I'd actually be happier if the data only shuffled at build time and did not re-shuffle at run-time in the browser - I could just automate the site to rebuild every hour or so and the site could be more static to the client.
I've been struggling with this too recently!
My solution is to render the shuffled content once the parent component mounts using ReactDOM's render method:
import React, { useRef, useEffect } from "react";
import { render } from "react-dom";
import shuffle from "../utils/shuffle";
const shuffledArray = shuffle(array.slice());
// The below should still be able to work with graphql fetched data
// as I think the array will be saved to a variable for use in the client,
// although in my case I haven't used it so can't be fully sure
const ShuffledJSXElements = () =>
shuffledArray.map(creator => (
<li key={creator.id}>
<Link to={link}>
{creator.name}
</Link>
<img src={image} />
</li>
));
const Page = () => {
const shuffledContentContainerRef = useRef();
useEffect(() => {
const ontainer = portfolioContainerRef.current;
render(<ShuffledJSXElements />, container);
}, []);
return (
<MainWrapper>
<StyledPortfolioGridWrapper ref={shuffledContentContainerRef} />
</MainWrapper>
);
};
export default Portfolio;
One frustrating thing about this is that the container element won't have an awareness of its own height before the content is rendered, so the layout might jump about a bit. A workaround for this is to use a min-height css property.
Related
What I'm trying to do
A simple way to render the content of a markdown file when it's passed as a string to another component using .compiledContent (or even using .rawContnent)? Or even a better way than this as obviously usually in Astro we can use the <Content /> Component, but from my knowledge, I can't pass a component or this functionality to another component without using a <slot /> in the parent component.
I have some JS for the parent component and using a <slot/> instead of passing the props to the component would change things, so hopefully looking for a solution with using this.
My setup
Data stored in /src/data/experience as markdown files with a year and a description formatted as markdown in the content section of each file
A component called Tabs.astro which takes props of headings and contents which are both lists of strings
A page /src/pages/experience.astro with the Tabs component in it which is displaying this data
I take the below code to get the data from the markdown files and pass the years and descriptions to the Tab component.
experience.astro
---
import Tabs from "../components/Tabs.astro";
const jobs = await Astro.glob("../data/experience/*.md");
const years = jobs.map((job) => job.frontmatter.year);
const descriptions = jobs.map((job) => job.compiledContent);
---
<!-- My component taking the data to be rendered -->
<Tabs headings={years} contents={descriptions} />
Tabs.astro
And the component renders the info like so
<!-- Tabs -->
<div class="tabs">
<ul class="tabs-header">
{
headings.map((heading) => (
<li>{heading}</li>
))
}
</ul>
<ul class="tabs-content">
{contents.map((content) => <li class="tab">{content}</li>)}
</ul>
</div>
My current solution
At the moment using .compiledContent gets me the correct HTML, however it is all in a string so the HTML doesn't actually render.
What I'm looking for
Is there a native way in Astro to pass markdown as a prop to a component?
If not is there a manual and recommended way in Astro to convert a markdown string and sanitise it to protect against XSS attacks? (if this is a risk in Astro when rendered statically?)
If not what are your most recommended ways to render markdown and sanitise it in JS?
Thanks so much for your time and help! I'm loving using Astro
p.s Also happy to concede and just use a <slot/> in my component if needed... ;)
Astro has a set:html directive you can use in combination with a Fragment like this
<Fragment set:html={post.compiledContent()}/>
After a bit of struggling with this myself, the current solution from the Astro docs for a single file without looping is the following.
Import your file with {Content as YourAliasName} from '../yourPath/yourFileName.md'
Then just use it as a tag <YourAliasName />
Example from the docs for reference:
---
import {Content as PromoBanner} from '../components/promoBanner.md';
---
<PromoBanner />
https://docs.astro.build/en/guides/markdown-content/#the-content-component
I'm trying to parse out the contents of a slot and render the contents in multiple places. The idea is to create a wizard that allows each step to be contained in a single component. Something like this:
Wizard Component Definition:
<ul>
<li v-for="icon in icons">{{icon}}<li>
</ul>
<section>
<ul>
<li v-for="body in bodies">{{body}}</li>
</ul>
</section>
Wizard Component Script
import {ref} from "vue";
export default {
setup(props, {slots}) {
const icons = ref([]);
const bodies = ref([]);
for (let item of slots.default()) {
// not sure if I need to call these, ex: item.children.icon()
icons.value.push(item.children.icon);
bodies.value.push(item.children.body);
}
return {icons, bodies};
}
}
Wizard Component Usage:
<wizard>
<wizard-page>
<template #icon>someIcon</template>
<template #body>someBody</template>
</wizard-page>
<wizard-page>
<template #icon>someIcon2</template>
<template #body>someBody2</template>
</wizard-page>
</wizard>
The obvious problem here is that everything is VNodes and doesn't just render nicely to the DOM. I've tried using render() and h() but those don't seem to be what I'm looking for. Also tried the v-html binding, but again, that isn't expecting a VNode.
I'm not sure if there's a better way to do this, or if I'm missing something simple here, but I'm not seeing an easy way to split apart a slot and render the contents in different places.
VUE 3
<component :is="body">
You can implement it in the following way
<ul>
<li v-for="body in bodies" :key="uniquekey">
<component :is="body" />
</li>
</ul>
link to docs:
https://vuejs.org/guide/essentials/component-basics.html#dynamic-components
Basically I'm trying to remake some simple web page that I have initially created with HTML and CSS to be working rather on React. I managed to redo the page to correctly display when it was moved into React, however I don't really understand why the navigation links that I have on top do not take me to the corresponding section on the same page anymore as well as why the external links to the project sites also stopped working.
Here is the project link code:
import React from "react";
export default function ProjectTile(props) {
return (
<div className="project-tile" id={props.id}>
<a href={props.href} target="_blank" id={props.link_id}>
<img
className="project_screenshot"
src={props.img_src}
alt={props.img_alt}
/>
<p className="project_name">
<span className="brackets"><</span> {props.title}{" "}
<span className="brackets">/></span>
</p>
</a>
</div>
);
}
All props are getting mapped and loaded from the array with corresponding data where each object looks like this:
{
id: "tribute_page",
link_id: "https://codepen.io/konstantinkrumin/full/PooYQbG",
img_src: "https://i.imgur.com/ynRuzOQ.png",
img_alt: "tribute_page_screenshot",
title: "Tribute Page"
}
The navigation links used are the following:
import React from "react";
export default function Navbar() {
return (
<nav id="navbar">
<a className="nav-link" href="#welcome-section">
About
</a>
<a className="nav-link" href="#projects">
Projects
</a>
<a className="nav-link" href="#contact">
Contact
</a>
</nav>
);
}
And each section they refer to have an id corresponding to the href indicated above.
Here if the link to this project on codesandbox
P.S. Everything used to work correctly when it was on HTML.
Also the contact links that seem to be set in similar way as project links are working.
Here are two things I think I found out:
In the ProjectTile.js file, replace href = {props.href} by href={props.link_id and now project opens in codepen.
About the jump link you have made in nav-bar, I think it's because of problem of codesandbox.
If you manage to make your url to https://op6gq.csb.app#projects instead of https://op6gq.csb.app/#projects. That's gonna work.
Or directly visiting https://op6gq.csb.app/#welcome-section jump link too works well.
It looks like there's no href prop. Sounds like what you want is something like
href={`#${props.id}`}
which would evaluate to href="#tribute_page" in this example.
You Have to try that your page url become:
https://op6gq.csb.app#welcome-section
Not:
https://op6gq.csb.app/#welcome-section
please attend to that / in address bar!
From Gatsby's official docs regarding Gatsby's Link component, it states that the Link component is used only for internal links, whereas for external links, one has to use the tag.
I'm building a Button component that has inbuilt props for links. The problem is right now I have to create 2 separate Button components for internal and external links due to the limitation.
My goal is to use one freeLink component that can be used as both internal and external links
I've tried creating a subcomponent (Button) for the button, but I'm unsure of the parent component (freeLink) which requires conditional rendering. The subcomponent is as of follows:
const Button = props => (
<button className={props.btnType}>
<span>{props.text}</span>
</button>
)
This is the visual logic to what I want to achieve:
For Internal links
<freeLink intLink="/about" btnType="btn-cta" text="Read about us">
</freeLink>
...which will render...
<Link to="/about">
<button className="btn-cta">
<span>Read about us</span>
</button>
</Link>
It is relatively similar for external links
<freeLink extLink="https://google.com" btnType="btn-cta" text="Visit Our Partner">
</freeLink>
...which will render...
<a href="https://google.com">
<button className="btn-cta">
<span>Visit Our Partner</span>
</button>
</a>
I'm quite new to Javascript, Gatsby and React so I'm unsure to how to apply a conditional rendering based on props applied.
Any advice, suggestion, or direction to how to code up the freeLink component is greatly appreciated.
P.S: I've seen Conditionally Use Gatsby Link in React Compoment but the chosen answer is too complicated for me to understand, and I don't have enough points to comment to ask for further elaboration.
You could try something simple like this:
const MyLink = (href, text, ...props) => {
if (href.startsWith("http") {
return <a href={href} {...props}>{text}</a>
} else {
return <Link href={href} {...props}>{text}</Link>
}
}
Your component could return different stuff based on weather you pass it a to or a href prop:
import { Link } from "gatsby"
const freeLink = props => {
if (props.to) return <Link {...props} />
return <a {...props} />
}`
I'm building a small react app containing a list of many programming languages with courses. Each list element will contain a background image provided by one "sprite" png image of their corresponding logo. Instead of giving each div an id to then add a background image and set the position of the image, I gave all of them the class languages for which I will set the background url() quickly then position after.
I've tried to directly set the languages class's background url from my external style-sheet but the link wont reach my public/images folder for some reason when It gets built with webpack. I've decided to settle with linking it with my component file instead.
The issue I'm coming across is when I try to set
const languageElements = document.getElementsByClassName("languages");
When calling console.log(languageElements), I see that it returns an HTMLCollection[] array for which I can see my elements clearly.
However, when I call console.log(languageElements[0]) it surprisingly returns undefined.
Am I missing something here? Does this something that has to do with react?
import React, { Component } from "react";
import "../styles/Center.scss";
class Center extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.log("Center Mounted Successfully.");
}
render() {
const languageElements = document.getElementsByClassName("languages");
console.log(languageElements);
languageElements[0].style.background = "url(../images/full-languages.png)";
return (
<>
<section>
<ul>
<li>
CS1022: <span className="languages cpp" />
</li>
<li>
CS1030:
<span className="languages c" />
<span className="languages cpp" />
</li>
<li>
CS1410:
<span className="languages html" />
<span className="languages css" />
<span className="languages js" />
</li>
<li>
CS2350: <span className="languages java" />
</li>
<li>
CS2420: <span className="languages java" />
</li>
<li>
CS2550: <span className="languages sql" />
</li>
</ul>
</section>
</>
);
}
}
export default Center;
I expect the use of the bracket operators to allow me to access each element within the HTMLCollection[] getElementsById() returns.
You should learn about React Component Lifecycle at
https://reactjs.org/docs/react-component.html
const languageElements = document.getElementsByClassName("languages");
This selector happened inside render() method, while nothing rendered, so languageElements will get empty. Try to put your code at componentDidMount()