How to get post's url address for simple component

Hello everyone,

I’m working on a simple component which should display last 5 posts’ images from particular category (in my case category id=91). When user clicks on image, related post should be opened.

How to get proper url address of a post. This one which I found in item.link goes to origin. Which is wrong.
origin url:
https://wp.example.com/2016/shinjuku-gyoen-national-garden/
desired url:
http://localhost:3000/2016/shinjuku-gyoen-national-garden/

Could somebody point me what’s the proper way to achieve this?

Here is my code:

import React, { useEffect, useState } from "react";
import { connect, styled } from "frontity";

const FeaturedPosts = ({ state, actions, libraries }) => {
  const [ featuredPosts, setFeaturedPosts ] = useState( [] );
  useEffect(() => {
    libraries.source.api.get({
        endpoint: "posts",
        params: { _embed: true, categories: '91', per_page: 5 }
    })
    .then(response => {
        response.json().then( data => {
          console.log(data);
          setFeaturedPosts( data );
        });
    });
  }, []);
  return (
    <div>
        {featuredPosts.length > 0 && (
            <FeaturedPostsContainer>
                {featuredPosts.map(item => {
                  const date = new Date(item.date);
                  const imageUrl = item._embedded["wp:featuredmedia"][0].source_url;
                  const url = item.link;
                    return (
                      <MyLink key={item.id} href={url}>
                        <MiniImage src={imageUrl} />
                        <DateAndDescription>
                          <span>{date.toDateString()}</span>
                        </DateAndDescription>
                      </MyLink>
                    )
                })}
            </FeaturedPostsContainer>
        )}
    </div>
  );
}

export default connect(FeaturedPosts);

const FeaturedPostsContainer = styled.div`
  display: flex;
`;

const MyLink = styled.a`
  display: block;
`;

const DateAndDescription = styled.div`
  display: none;
`;

const MiniImage = styled.img`
  max-width: 100px;
  max-height: 100px;
`;

I had a similar requirement when working with menus. @luisherranz pointed out to me manually stripping the domain wasn’t the correct way to do it and pointed me to the URL helper in Frontity.

Instead of using item.link, run it through URL and use url.pathname.

2 Likes

Thank you Chris, this is exactly what I was looking for.

Do you have an idea, how to refactor this component to allow server side rendering?
Currently it’s only working on client side, becasue I’m using useEffect.

edit:
so far, I was able to achive this with those changes:
I’ve added beforeSSR action to my theme config:

  actions: {
    theme: {
		beforeSSR: async ({ state, actions, libraries }) => {
		//fetch only for home page
		if (state.router.link == "/") {
			await actions.source.fetch("/category/slider-images/");
		}
      }
	}
  },

and here is my component:

const FeaturedPosts = ({ state, actions }) => {
  const data = state.source.get("/category/slider-images/");
  const posts = data.items.map(
    ({ type, id }) => state.source[type][id]
  );
  
  return (
    <>
      {posts.map(p => <a href={p.link}>{p.title.rendered}</a>)}
    </>
  );
}
1 Like

Hey @LiamMcKoy, great work :slight_smile:

That’s exactly right, you should use actions.source.fetch("/category/slider-images/") instead of doing the fetch yourself.

Actually, when you use actions.source.fetch we strip the domain from item.link so you don’t need to use URL and do it yourself.

And the beforeSSR action you posted is the perfect way to tell Frontity: “don’t do the React rendering in the server until this data is on the state”.

Apart from that, I’d add a useEffect to your <FeaturedPosts> component to populate it when the initial URL was not "/":

const FeaturedPosts = ({ state, actions }) => {
  const data = state.source.get("/category/slider-images/");

  useEffect(() => {
    // Fetch the category in the client if it hasn't been fetched yet.
    actions.source.fetch("/category/slider-images/");
  }, []);

  // Return a loader if data is not ready.
  if (!data.isReady) return <div>loading...</div>;

  // Once the data is ready, return the items.
  const posts = data.items.map(
    ({ type, id }) => state.source[type][id]
  );
  return (
    <>
      {posts.map(p => <a href={p.link}>{p.title.rendered}</a>)}
    </>
  );
}

Thank you @luisherranz

I’ve found some weird behavior with my handler, why in state.source.data[“sliderMenu/”] this additional slash is needed? slider menu handler is registered with pattern “sliderMenu”, but when I’ve tried without this slash, state.source.data[“sliderMenu”] is undefined. Here is my handler, which works fine, but to be honest I would expect this should work well without this slash suffix.

export const sliderManuHandler = {
  name: "sliderMenu",
  priority: 10,
  pattern: "sliderMenu", 
  func: async ({ state, libraries }) => {
    const response = await libraries.source.api.get({
      endpoint: "posts",
      params: { _embed: true, categories: '91', per_page: 5 }
    });

    const data = await response.json();

    //can I use populate, what is the difference?
    //const data = await libraries.source.populate({ response, state });

    //why "sliderMenu" doesn't work? why this additional slash is needed?
    Object.assign(state.source.data["sliderMenu/"], {
      data,
      isSliderMenu: true,
    });
  }
}

You are absolutely right.

@David maybe we should actually remove the ending slash instead of adding it to avoid this problem with non-URL data?

EDIT: Or maybe just don’t add it if it doesn’t start with /.

By the way:

  1. You can use frontity.state.source.data in the browser console to take a look at the data stored there.
  2. You can get route and use it to populate data, like this:
export const sliderManuHandler = {
  name: "sliderMenu",
  priority: 10,
  pattern: "sliderMenu", 
  func: async ({ state, libraries, route }) => {
    const response = await libraries.source.api.get({
      endpoint: "posts",
      params: { _embed: true, categories: '91', per_page: 5 }
    });

    ...

    Object.assign(state.source.data[route], {
      data,
      isSliderMenu: true,
    });
  }
}
  1. libraries.source.populate is used to store all the normalized data in the state (posts, authors, tags, categories… and so on).

The ending slash is not needed, it is just added when normalizing the link or whatever you pass to actions.source.fetch(), in order to replicate the same behavior as WordPress.

I agree with not adding the slash for non-URL data, though. Maybe it is better to normalize only URLs, and leave the rest at it is, right @luisherranz?

Anyway, @LiamMcKoy, in case you want to get some object from state.source.data you can also use state.source.get() (it normalizes the input). In your case, for example, it could be

Object.assign(state.source.get("sliderMenu"), {
  data,
  isSliderMenu: true,
});

What’s the difference between “not adding the slash for non-URL data” and “normalizing only URLs”?

When wp-source normalizes a URL it does more things apart from adding a ending slash, as it could have search params or a hash property.

Ok!