#optional catch-all route?

15 messages · Page 1 of 1 (latest)

jagged ocean
#

Is there any such thing?

tawdry bayBOT
#

Hi @jagged ocean. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

In Astro, there isn't a concept specifically called "optional catch-all route," but you can achieve similar functionality using a combination of Astro's routing features.

Astro provides two types of dynamic routes:

  1. Named parameters: [param]
  2. Rest/spread parameters (catch-all): [...slug]

The catch-all route using spread parameters ([...slug]) will match any path at that position, including multiple segments. However, it's not optional by default - it requires at least one segment to match.

If you want to create a route that optionally catches all paths (including no path), you would need to handle this in your component logic. For example, in SSR mode, you could check if the slug parameter is undefined and handle that case accordingly:

---
const { slug } = Astro.params;
// If slug is undefined, it means we're at the root path
// If slug has a value, we're at a deeper path
---

For static sites, you would need to use getStaticPaths() to define all the paths you want to generate, including the root path if needed:

---
export async function getStaticPaths() {
  return [
    { params: { slug: undefined } }, // Matches the root path
    { params: { slug: 'products' } },
    { params: { slug: 'products/astro-handbook' } },
    // other paths...
  ];
}

const { slug } = Astro.params;
// Handle the case where slug is undefined (root path)
---

This approach is demonstrated in the dynamic routes documentation for SSR mode, where they show how to handle both the root path (where slug is undefined) and deeper paths.

#

Remember that Astro follows specific route priority rules, so more specific routes will take precedence over catch-all routes.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@daring cedar Kapa.ai is still learning and improving, please let me know how I did by reacting below.

jagged ocean
#

@tawdry bay

How update below code to ensure that I can catch routes under content collection pages and those coming from storyblok in the same catch-all route?

---
// @src/pages/[...slug].astro

import Base from '@layouts/Base.astro'
import StoryblokComponent from '@storyblok/astro/StoryblokComponent.astro'
import { getCollection, getEntry, render } from 'astro:content'
import { useStoryblokApi } from '@storyblok/astro'
import { _getPageData } from '@scripts/storyblok'
import { components } from '@scripts/components/components'

export async function getStaticPaths() {
    const localPages = await getCollection('pages')
    const localPaths = localPages.map((entry) => {
        const raw = entry.id.replace(/^pages\//, '')
        const parts = raw.split('/').filter(Boolean)
        return {
            params: {
                slug: raw === 'home' ? undefined : raw,
            },
        }
    })
    const api = useStoryblokApi()
    const links = await api.getAll('cdn/links', {
        version: import.meta.env.DEV ? 'draft' : 'published',
        resolve_links: 'url',
    })
    const ssrSkip = ['products', 'products/', 'settings']
    const storyPaths = Object.values(links)
        .filter((l) => !l.is_folder && !ssrSkip.includes(l.slug))
        .map((l) => {
            const parts = l.slug.split('/').filter(Boolean)
            return {
                params: {
                    slug: l.slug === 'home' ? undefined : l.slug,
                },
            }
        })

    return [...localPaths, ...storyPaths]
}

const { slug } = Astro.params
console.log(slug)
const path = slug

let data, Content, storyContent

try {
    const { entry } = Astro.props
    const rendered = await render(entry)
    data = entry.data.meta[0]
    Content = rendered.Content
} catch {
    const { content, meta } = await _getPageData(path)
    data = meta
    storyContent = content
}
---
tawdry bayBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

I'll help you update your catch-all route to handle both content collection pages and Storyblok pages. The main issue with your current code is that you're trying to access Astro.props.entry but you're not passing the entry as props in your getStaticPaths() function.

Here's how you can update your code:

---
// @src/pages/[...slug].astro

import Base from '@layouts/Base.astro'
import StoryblokComponent from '@storyblok/astro/StoryblokComponent.astro'
import { getCollection, getEntry, render } from 'astro:content'
import { useStoryblokApi } from '@storyblok/astro'
import { _getPageData } from '@scripts/storyblok'
import { components } from '@scripts/components/components'

export async function getStaticPaths() {
    const localPages = await getCollection('pages')
    const localPaths = localPages.map((entry) => {
        const raw = entry.id.replace(/^pages\//, '')
        const parts = raw.split('/').filter(Boolean)
        return {
            params: {
                slug: raw === 'home' ? undefined : raw,
            },
            props: {
                entry,
                source: 'collection'
            }
        }
    })
    const api = useStoryblokApi()
    const links = await api.getAll('cdn/links', {
        version: import.meta.env.DEV ? 'draft' : 'published',
        resolve_links: 'url',
    })
    const ssrSkip = ['products', 'products/', 'settings']
    const storyPaths = Object.values(links)
        .filter((l) => !l.is_folder && !ssrSkip.includes(l.slug))
        .map((l) => {
            const parts = l.slug.split('/').filter(Boolean)
            return {
                params: {
                    slug: l.slug === 'home' ? undefined : l.slug,
                },
                props: {
                    source: 'storyblok',
                    slug: l.slug
                }
            }
        })
#

return [...localPaths, ...storyPaths]
}

const { slug } = Astro.params
const { source, entry, slug: storyblokSlug } = Astro.props

let data, Content, storyContent

if (source === 'collection') {
// Handle content collection
const rendered = await render(entry)
data = entry.data.meta[0]
Content = rendered.Content
} else {
// Handle Storyblok content
const path = slug || 'home'
const { content, meta } = await _getPageData(path)
data = meta
storyContent = content
}

<!-- Your template here -->
<Base>
{source === 'collection' ? <Content /> : <StoryblokComponent blok={storyContent} />}
</Base>


Key changes made:

1. Added `props` to both the content collection and Storyblok paths in `getStaticPaths()`:
   - For content collections: passing the `entry` and a `source` identifier
   - For Storyblok: passing a `source` identifier and the `slug`

2. Destructured the props at the top of the component to access the source and relevant data

3. Used conditional logic based on the `source` to determine how to render the content

4. Added a simple template structure at the bottom that renders either the content collection's `Content` component or the Storyblok component based on the source

This approach follows the pattern shown in the [Astro documentation for generating routes from content collections](<https://docs.astro.build/en/guides/content-collections/#generating-routes-from-content>) and [Storyblok integration](<https://docs.astro.build/en/guides/cms/storyblok/#generating-pages>), combining them into a single catch-all route.

Note that you might need to adjust the template part at the bottom based on your actual layout requirements.

Is there anything specific about this implementation you'd like me to explain further?
#

@daring cedar Kapa.ai is still learning and improving, please let me know how I did by reacting below.

jagged ocean
#

@tawdry bay Nice...

But can you share the code in two parts so that they'll be formatted correctly -- the code fense part as js and then markup

tawdry bayBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

I'll share the updated code in two parts with proper formatting:

First, here's the JavaScript part with the frontmatter and logic:

// @src/pages/[...slug].astro
---
import Base from '@layouts/Base.astro'
import StoryblokComponent from '@storyblok/astro/StoryblokComponent.astro'
import { getCollection, getEntry, render } from 'astro:content'
import { useStoryblokApi } from '@storyblok/astro'
import { _getPageData } from '@scripts/storyblok'
import { components } from '@scripts/components/components'

export async function getStaticPaths() {
    const localPages = await getCollection('pages')
    const localPaths = localPages.map((entry) => {
        const raw = entry.id.replace(/^pages\//, '')
        const parts = raw.split('/').filter(Boolean)
        return {
            params: {
                slug: raw === 'home' ? undefined : raw,
            },
            props: {
                entry,
                source: 'collection'
            }
        }
    })
    const api = useStoryblokApi()
    const links = await api.getAll('cdn/links', {
        version: import.meta.env.DEV ? 'draft' : 'published',
        resolve_links: 'url',
    })
    const ssrSkip = ['products', 'products/', 'settings']
    const storyPaths = Object.values(links)
        .filter((l) => !l.is_folder && !ssrSkip.includes(l.slug))
        .map((l) => {
            const parts = l.slug.split('/').filter(Boolean)
            return {
                params: {
                    slug: l.slug === 'home' ? undefined : l.slug,
                },
                props: {
                    source: 'storyblok',
                    slug: l.slug
                }
            }
        })

    return [...localPaths, ...storyPaths]
}

const { slug } = Astro.params
const { source, entry, slug: storyblokSlug } = Astro.props

let data, Content, storyContent
#

if (source === 'collection') {
// Handle content collection
const rendered = await render(entry)
data = entry.data.meta[0]
Content = rendered.Content
} else {
// Handle Storyblok content
const path = slug || 'home'
const { content, meta } = await _getPageData(path)
data = meta
storyContent = content
}


And here's the markup part:

```astro
<Base>
  {source === 'collection' ? <Content /> : <StoryblokComponent blok={storyContent} />}
</Base>

This separation follows Astro's component structure as described in the Component Structure documentation, with the component script (JavaScript) separated from the component template (HTML).

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: