Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useStoryblokAsync equivalent for multiple stories (storyblokApi.get()) #547

Open
1 task done
papoms opened this issue Aug 29, 2023 · 14 comments
Open
1 task done

useStoryblokAsync equivalent for multiple stories (storyblokApi.get()) #547

papoms opened this issue Aug 29, 2023 · 14 comments
Labels
dx [Issue] Improvement or fix with impact on the developer experience. enhancement [Issue][PR] New feature pending-author [Issue] Awaiting further information or action from the issue author

Comments

@papoms
Copy link

papoms commented Aug 29, 2023

Description

Using the Storyblok-nuxt module i wish there was a composable similar to useStoryblokAsync but for requests that return multiple stories.

An Example would be fetching all Blog articles as described in: https://www.storyblok.com/tp/create-and-render-blog-articles-in-storyblok-and-nuxt#rendering-all-articles

const articles = ref(null)
const storyblokApi = useStoryblokApi()
const { data } = await storyblokApi.get('cdn/stories', {
  version: 'draft',
  starts_with: 'blog',
  is_startpage: false,
})
articles.value = data.stories

While that works, i love the benefit of useStoryblokAsync in the context of static site generation with npm run generate and the new experimental payload extraction feature of nuxt 3.

Would you be interested in including such a composable within this module?

Suggested solution or improvement

write a useStoryBlokAsyncGet Composable
generalize useStoryBlokAsync to allow queries for multiple stories (e.g. cache returned story OR stories)

Additional context

No response

Validations

@papoms papoms added enhancement [Issue][PR] New feature pending-author [Issue] Awaiting further information or action from the issue author pending-triage [Issue] Ticket is pending to be prioritised labels Aug 29, 2023
@papoms
Copy link
Author

papoms commented Aug 29, 2023

I just played around with the existing composable useAsyncStoryblok and changed

to use either data.story or data.stories by changing it to

		story.value = data.story ?? data.stories;

Calling it like that

const news =  await useAsyncStoryblok('', { version: 'draft', starts_with: 'news/' })

@codeflorist
Copy link

you can wrap your call to the storyblok-api inside a useAsyncData call, so it get's stored in Nuxt's payload:

const articles = ref(null)
const storyblokApi = useStoryblokApi()
const { data } = await useAsyncData(
	'articles',
	async () =>
		await storyblokApi.get('cdn/stories', {
                  version: 'draft',
                  starts_with: 'blog',
                  is_startpage: false,
                })
)
articles.value = data.stories

but i agree, that a shorthand composable would be very handy.

@Dawntraoz Dawntraoz added dx [Issue] Improvement or fix with impact on the developer experience. and removed pending-triage [Issue] Ticket is pending to be prioritised labels Aug 30, 2023
@Dawntraoz
Copy link
Contributor

Hi @papoms & @codeflorist, thanks for opening and giving insights into this feature request.

I will talk about this internally with my manager, but in the meantime, will you be willing to participate in developing the composable if we have the green light?

@bocooper-dev
Copy link

bocooper-dev commented Aug 30, 2023

you can wrap your call to the storyblok-api inside a useAsyncData call, so it get's stored in Nuxt's payload:

const articles = ref(null)
const storyblokApi = useStoryblokApi()
const { data } = await useAsyncData(
	'articles',
	async () =>
		await storyblokApi.get('cdn/stories', {
                  version: 'draft',
                  starts_with: 'blog',
                  is_startpage: false,
                })
)
articles.value = data.stories

but i agree, that a shorthand composable would be very handy.

@codeflorist With this your data object is structured a bit differently: data.value.data.stories

I thought this would work for me but I've run into a few problems over the past few weeks. Potentially cache related? Here are my problems:

  1. The page shows hydration errors on page refresh. You can even see the blog cards double in quantity with ugly broken CSS.
  2. i18n does not update on refresh, which I was able to fix by breaking out of SSG by passing a unique key to cv to invalidate cache.
  3. When calling the same stories on different pages (like a recent blog component vs all blogs on the blog home page), nuxt would serve me the 3 cached articles from the recent blog component when I wanted all of them on the blog home page. This too was mitigated by cache invalidated at the expense of SSG.
// RecentArticles.vue
<script setup>
defineProps({ blok: Object })

const articles = ref(null)
const storyblokApi = useStoryblokApi()
const { data } = await useAsyncData(
	'recent-articles',
	async () =>
		await storyblokApi.get('cdn/stories', {
			version: 'draft',
			starts_with: 'blog',
			is_startpage: 0,
			per_page: 3
		})
)
articles.value = data.value.data.stories
</script>
// AllArticles.vue (with cv and localization)
<script setup>
import { useI18n } from 'vue-i18n'
const { locale } = useI18n({
	useScope: 'global'
})

defineProps({ blok: Object })

const articles = ref(null)
const storyblokApi = useStoryblokApi()
const { data } = await useAsyncData(
	`blog-${new Date().getTime()}`,
	async () => await storyblokApi.get('cdn/stories', {
		version: 'draft',
		starts_with: 'blog',
		is_startpage: 0,
		language: locale.value,
		cv: new Date().getTime()
	})
)
articles.value = data.value.data.stories
</script>

@papoms @codeflorist @Dawntraoz Have you run into this issue?

@codeflorist
Copy link

@bocooper-dev

ad 1) if your are using useAsyncData and payloads, server rendered and client data should be the same and not produce hydration errors. Are you re-fetching data on the client, or does the problem only happen, when in Storyblok's visual editor? If yes, the effect you describe could be due to this vue issue. A workaround would be to do all client side updates inside onMounted()

ad 2) I think you need to include the language in the useAsyncData key e.g. like this:

useAsyncData(`blog-${locale.value}`,...

ad 3) Thats strange, since you are using different keys with useAsyncData in your example.

As an alternative to useAsyncData, you could utilize useState like this:

// RecentArticles.vue
<script setup>
defineProps({ blok: Object })

const articles = useState('recent-articles')
const storyblokApi = useStoryblokApi()
if (!articles.value) {
    const stories = await storyblokApi.get('cdn/stories', {
        version: 'draft',
        starts_with: 'blog',
        is_startpage: 0,
        per_page: 3
    })
    articles.value = stories
}
</script>

This doesn't seem to store it's data in the payload-files, but rather directly in the index.html and inside a default.<hash>.js, but imho that should be equivalent.

@codeflorist
Copy link

Hi @papoms & @codeflorist, thanks for opening and giving insights into this feature request.

I will talk about this internally with my manager, but in the meantime, will you be willing to participate in developing the composable if we have the green light?

@Dawntraoz I'll try to wrap up a pull request.

@nachoadjust
Copy link

how is pagination solved for the examples above? or is it not resolved?

@bocooper-dev
Copy link

bocooper-dev commented Nov 13, 2023

how is pagination solved for the examples above? or is it not resolved?

I ended up getting it all to work like this:

const storyblokApi = useStoryblokApi()
const { data } = await useAsyncData(
	`blog-list-${locale.value}-${page.value}`,
	async () => await storyblokApi.get('cdn/stories', {
		version: draft,
		starts_with: 'blog',
		is_startpage: false,
		language: locale.value,
		per_page: 10,
		page: page.value
	})
)

You need to set a unique key for the cache to retrieve the data per page, so:

`blog-list-${locale.value}-${page.value}`,

is adding the page.value string (e.g. '1') to the key of the cached data for that page. If you aren't using localization, then don't add locale.value to the key.

and blog-list is just arbitrary so you can name yours however you want.

@nachoadjust
Copy link

nachoadjust commented Nov 13, 2023

Thanks @bocooper-dev !
But how do you increment page.value, or were do you define it?
that's the part I am missing currently.

In my case I need all pages to resolve to a single promise. And then return the results.
(I have to fetch +500 stories and generate a single list with all of them without pagination)

At the moment i have to do some horrible looping in order to get all the pages for a certain section (like blog)
It looks more or less like this:

async function useStoryblokGetStoriesAsync(url, apiOptions) {
  const storyblokApiInstance = useStoryblokApi()
  let currentPage = 1
  let results = []

  const { data } = await useAsyncData(
    `${cv}${url}${page.value}`,
    async () => {
      while (fetchingStories) {
        const rs = await storyblokApiInstance.get('cdn/stories', {
         ...apiOptions,
          version: 'draft',
          cv: myGetCacheVersion(),
          per_page: 10,
          page: currentPage,
          excluding_fields: apiOptions.excluding_fields || 'pageBody',
        })

        // ...some more things here

        const currentPageStories = rs.data.stories || []

        results = results.concat(currentPageStories)

        currentPage++
      }

      return results
    },
  )

  return data.value
}

This is the only way I've been able to loop through pages, but TBH I am not happy with it.
If you have a better, more concise idea, I am open to suggestions.
Thanks again!!!

@codeflorist
Copy link

@nachoadjust

you should be able to simply use storyblokApiInstance.getAll() (instead of get()) to get all stories without the need of any pagination-handling. the function directly returns the stories-array though - so rs will be the finished array instead of rs.data.stories.

here is how i do it ( using useState instead of useAsyncData and with multilanguage):

const articles = useState<NewsArticle[]>('articles-' + currentLocale.value)

if (!articles.value) {
	const stories = await storyblokApi.getAll('cdn/stories', {
		starts_with: 'articles/',
		sort_by: 'first_published_at:desc',
		language: currentLocale.value,
	})
	articles.value = stories
}

@bocooper-dev
Copy link

@nachoadjust
Copy link

nachoadjust commented Nov 14, 2023

Thank you big time @bocooper-dev @codeflorist !!

I ended up implementing it this way:

/**
 * Fetches Storyblok stories based on the provided URL or configuration.
 *
 * @param {string | StoryConfig} url - The URL or Storyblok configuration to fetch the story.
 * @param {ISbStoriesParams} apiOptions - Additional API options for fetching the story (optional).
 * @returns {Promise<ISbStoryData[]>} - The fetched story data.
 */
export async function useStoryblokGetStoriesAsync(
  url: string,
  apiOptions: ISbStoriesParams = {},
): Promise<ISbStoryData[]> {
  const version = getStoryVersion()
  const cv = getCacheVersion()

  // @ts-expect-error: Nuxt doesn't offer an explicit export for useState and auto imports it.
  const stories = useState<ISbStoryData[]>(`${cv}${url}`)

  const storyblokApiInstance = useStoryblokApi()

  if (!stories.value) {
    const rs = await storyblokApiInstance.getAll('cdn/stories', {
      version,
      cv,
      excluding_fields: apiOptions.excluding_fields || 'pageBody',
      ...apiOptions,
    })

    stories.value = rs
  }

  return stories.value
}

This worked fine on dev environment, tho I am getting 429 Your rate limit has been reached... errors when building for Prod (when building prerender routes). But I am not sure the problem is related to the code here.

I will also try the suggestion in the link

@Dawntraoz
Copy link
Contributor

Impressive findings, you all, @nachoadjust, @bocooper-dev & @codeflorist, thanks a lot for this. You made my day! It's so cool to see you all collaborating and helping each other 🤩

Would you be up for a quick sync call? So we can include the solution in the SDK for everyone? Let me know 😍

@nachoadjust
Copy link

Hi @Dawntraoz !
Thank you for the invitation!
I'd be happy to connect!

About the code:
Tho the code works for me I am not sure we can include the solution in the SDK yet.
I am encountering some problems (error handling mostly) that I don't know if they are related to the code here or to some other issue in my codebase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dx [Issue] Improvement or fix with impact on the developer experience. enhancement [Issue][PR] New feature pending-author [Issue] Awaiting further information or action from the issue author
Projects
None yet
Development

No branches or pull requests

5 participants