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

feat: create routing files via plop #244

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

jurgenbelien
Copy link
Contributor

@jurgenbelien jurgenbelien commented Jan 8, 2025

Changes

  • Refactors routing slug functions so they're reusable
  • Adds tests for said functions
  • Adds plop templates for routing files (both ts and fragment when creating pages
  • Expands plop questions to optionally create said files.

How to test

  1. Check out branch locally
  2. Create some new pages (both localized and non-localized)
  3. See that these files are a decent starting point now

Checklist

  • I have performed a self-review of my own code
  • I have made sure that my PR is easy to review (not too big, includes comments)
  • I have made updated relevant documentation files (in project README, docs/, etc)
  • I have added a decision log entry if the change affects the architecture or changes a significant technology
  • I have notified a reviewer

@jurgenbelien jurgenbelien requested a review from jbmoelker January 8, 2025 14:28
Copy link

cloudflare-workers-and-pages bot commented Jan 8, 2025

Deploying head-start with  Cloudflare Pages  Cloudflare Pages

Latest commit: d74d7b0
Status: ✅  Deploy successful!
Preview URL: https://77e094d2.head-start.pages.dev
Branch Preview URL: https://feat-routing-plopfile.head-start.pages.dev

View logs

when: (data) => !!data.name,
type: 'confirm',
name: 'separateRouting',
message: (data) => `Create separate file in routing for \`${plop.getHelper('pascalCase')(data.name)}\`?`,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment file routing is in @lib/routing/index.ts, choosingy will create a file akin to @lib/routing/page.ts.

@@ -14,9 +15,17 @@ export default function (plop) {
.map((param) => param.slice(1, -1)) // remove brackets
.filter((param) => param !== 'locale'); // remove locale param
});
plop.setHelper('trailingParam', (value) =>
Copy link
Contributor Author

@jurgenbelien jurgenbelien Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using this to find out if a developer creates a new file with a route that ends in a param, ie [slug]. Obviously that param might be any word between square brackets, but I left mapping that correctly as an exercise for the developer. I felt that asking how to handle a route that doesn't end in [slug] or remapping slug to the provided name would overcomplicate this functionality.

title
{{#each (routeParams route)}}
{{#if (eq (trailingParam ../route) this)}}
slug
Copy link
Contributor Author

@jurgenbelien jurgenbelien Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically I could've reassigned slug to the trailingParam, but I felt that to be overcomplicating it, and I must say I think it makes more sense to promote the use of [slug] as final param

Comment on lines +6 to +13
type LocalizedSlugs = {
_allSlugLocales?:
| {
locale?: SiteLocale | null;
value?: string;
}[]
| null;
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit of a weird type, but it is what's common between all route fragments with _allSlugLocales

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it correct that locale can be null but value can not?

return slug;
};
export const getPageSlugFromPath = (path: URL['pathname']) =>
getSlugFromPath(path) as string;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only for pages will this always return a string.

@jurgenbelien jurgenbelien force-pushed the feat/routing-plopfile branch from 0f9b0de to 81692ab Compare January 8, 2025 14:44
@jurgenbelien jurgenbelien requested a review from bazottie January 8, 2025 14:54
@jurgenbelien jurgenbelien marked this pull request as ready for review January 8, 2025 14:54
plop.setHelper('trailingParam', (value) =>
// Return the trailing param in a route
// If the last slug is not a param, this is undefined
value.split('/').pop()?.match(/\[(.*?)\]/)?.[1]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory routes can also contain multiple params i.e. [lang]-[version] or rest params [...pages].
If only using to check wether or not a param is used, you might want to use REGEX.test(value.split('/').pop())
Or if you want to access all params maintaining similar logic

Suggested change
value.split('/').pop()?.match(/\[(.*?)\]/)?.[1]
[...'[lang]-[ext]'.matchAll(/\[(.*?)\]/g)].map(match => match?.[1])

Which would return an array of params in the route name.
This will break implementations, you could return a joined string with a - or return a string if the array length is 1 to avoid regression in existing code from this PR.
This will still return ... if used

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, but then I'd say let's write it this way:

value.match(/\[([^]]*)\]\/?$/)?.[1]

Comment on lines +6 to +13
type LocalizedSlugs = {
_allSlugLocales?:
| {
locale?: SiteLocale | null;
value?: string;
}[]
| null;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it correct that locale can be null but value can not?

Comment on lines +6 to +13
type LocalizedSlugs = {
_allSlugLocales?:
| {
locale?: SiteLocale | null;
value?: string;
}[]
| null;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you find typing these nested interfaces weird, I suggest splitting them with context specific names

Suggested change
type LocalizedSlugs = {
_allSlugLocales?:
| {
locale?: SiteLocale | null;
value?: string;
}[]
| null;
};
interface SlugLocale {
locale?: SiteLocale | null;
value?: string;
}
interface LocalizedSlugs {
_allSlugLocales?: SlugLocale[] | null;
};

Copy link
Contributor Author

@jurgenbelien jurgenbelien Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant "weird" because it is somewhat convoluted and with many undefined and null possibilities, but based on our typegen from graphql. From that perspective I actually think it is clearer when it is not split up.

* - /page-1/?foo=bar
* - /grand-parent/parent-slug/page-1
*/
export const getSlugFromPath = (path: URL['pathname']) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL['pathname'] simply resolves to string so this seems somewhat redundant

Suggested change
export const getSlugFromPath = (path: URL['pathname']) => {
export const getSlugFromPath = (path: string) => {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It resolves to string, but I think labeling it as URL['pathname'] has semantic meaning to a developer looking at it.

Comment on lines 4 to 6
export type PageRouteForPath =
& Pick<PageRouteFragment, '_allSlugLocales' | 'parentPage'>
& { parentPage?: PageRouteForPath | null };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid intersecting imported generated and newly defined types with the same keys.
Extending a type or an interface is usually safer and helps with finding inconsistencies rather than intersecting
i.e.

export interface PageRouteForPath extends Pick<PageRouteFragment, '_allSlugLocales' | 'parentPage'> {
  parentPage?: PageRouteForPath | null
};

Or expanding on this when child keys are required, and we want to set them to optional

export interface PageRouteForPath extends Partial<Pick<PageRouteFragment, '_allSlugLocales' | 'parentPage'>> {
  parentPage?: PageRouteForPath | null
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I try to encourage type over interface because it is clearer when overridden. I don't feel your proposed interface is clearer tbh but I'm happy to hear in person the benefits of this.

Comment on lines -32 to +34
export const getParentSlugs = ({ page, locale }: { page: PageRouteForPath, locale?: SiteLocale }): MaybeSlug[] => {
export const getParentSlugs = ({ locale, page }: { locale?: SiteLocale, page: PageRouteForPath }): MaybeSlug[] => {
if (page.parentPage) {
const slug = page.parentPage._allSlugLocales?.find(slug => slug.locale === locale)?.value;
const slug = getLocalizedSlug<PageRouteForPath>({ locale, record: page.parentPage });
return [
...getParentSlugs({ page: page.parentPage, locale }),
...getParentSlugs({ locale, page: page.parentPage }),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think your editor is reordering some arguments here which makes it harder to review some changes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I purposely did that so the order is consistent among all those functions ;-)

Copy link
Member

@jbmoelker jbmoelker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jurgenbelien when I run this:

? [PLOP] Please choose a generator. page - create a new Page route
? Page route (e.g. `[locale]/search/`)? [locale]/events/[slug]/
? Model name in DatoCMS? (leave empty if none) Event
? Create separate file in routing for `Event`? Yes

My EventRoute.fragment.grapqhl has a weirdly nested slug field with sub properties like __typename. I'm not sure where it's coming from exactly. But I don't think this is right:

fragment EventRoute on EventRecord {
  __typename
  id
  title
  slug {
    __typename
    id
    title
    slug
    _allSlugLocales {
      locale
      value
    }
  }
}

@jurgenbelien
Copy link
Contributor Author

@jurgenbelien when I run this:

? [PLOP] Please choose a generator. page - create a new Page route
? Page route (e.g. `[locale]/search/`)? [locale]/events/[slug]/
? Model name in DatoCMS? (leave empty if none) Event
? Create separate file in routing for `Event`? Yes

My EventRoute.fragment.grapqhl has a weirdly nested slug field with sub properties like __typename. I'm not sure where it's coming from exactly. But I don't think this is right:

fragment EventRoute on EventRecord {
  __typename
  id
  title
  slug {
    __typename
    id
    title
    slug
    _allSlugLocales {
      locale
      value
    }
  }
}

No that's not right, but I think I do know why that's happening

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants