stackblitz / tutorialkit

TutorialKit by StackBlitz - Create interactive tutorials powered by the WebContainer API
https://tutorialkit.dev
MIT License
504 stars 48 forks source link

Extending the content schema (for an RSS feed, meta-tags etc.) #327

Closed eric-burel closed 1 week ago

eric-burel commented 2 months ago

Is your feature request related to a problem?

Hi, Since I am "building in public" and crafting lessons as I go, I'd like to setup an RSS feed for some lessons.

In order to feed it, I would need to alter lessons metadata:

I would also need a way to get the lesson URL.

Describe the solution you'd like.

I'd like to either have an RSS feed out-of-the-box or be able to craft one in userland.

Describe alternatives you've considered.

Since RSS is a specific but common problematic, it could make sense to add the relevant to add the field I've listed into the contentSchema, to make it easy to implement a feed.

I would also need an helper to get a lessons link, which is the reverse operation from what generateStaticRoutes does to compute valid slugs. Perhaps we could precompute links when we build the "tutorial" collections?

The other alternative is to allow passing a custom schema, so the frontmatter of lessons can be extended freely provided it keeps the tutorial kit mandatory fields around. I would still need a helper to get the lesson URL.

Additional context

I don't seem to have a typed result when I run getCollection("tutorial"), perhaps Astro system that types collections cannot cross packages?

AriPerkkio commented 2 months ago

RSS feed support is not something we are prioritizing right now. But we will accept a pull request for this, if proposed changes are not too complex.

eric-burel commented 2 months ago

I'll try to work on it later on. Related to PR #342, I would also like to be able to finetune meta tags for each lesson. This would require being able to pass an image URL through the lesson metadata, and it would be great to be able to do that in user-land without having to extend the framework.

eric-burel commented 1 week ago

Closing as this is now supported via PR #378 I've implemented a feed based on this, the trickiest part was to convert content ids into slugs based on the existing logic from getContent, I've ended up with this quick and dirty implem (but working fine):

/**
 * Dictionary of lesson id to actual slug
 * Not super efficient but will be statically rendered anyway so don't bother
 * { "1_patterns/2_chapter/3_lesson/content.mdx": "part-slug/chapter-slug/lesson-slug" }
 * The key is "lesson.id" in the collection
 * The value is the slug used as URL in the [...slug] page of tutorial kit
 * @returns
 */
async function getSlugs() {
  const chapters = await getCollection(
    "tutorial",
    ({ data }) => data.type === "chapter"
  );
  const parts = await getCollection(
    "tutorial",
    ({ data }) => data.type === "part"
  );
  const lessons = await getCollection(
    "tutorial",
    ({ data }) => data.type === "lesson"
  );
  // p/1_foobar/2_baz/3_qux
  // => we want to turn that into tutorial-slug/part-slug/chapter-slug/lesson-slug
  let ids: Array<string> = lessons.map((l) => l.id);
  const idsToSlugs = new Map<string, string>(ids.map((id) => [id, id]));
  idsToSlugs.forEach((slug, id) => {
    idsToSlugs.set(id, slug.replace("1-patterns", "p"));
    // remove the dangling "/content.mdx"
    idsToSlugs.set(id, slug.replace("/content.mdx", ""));
  });
  parts.forEach((pt) => {
    // id is the actual filename, we want the slug
    // 1_patterns/meta.md => 1_patterns
    const partId = pt.id.split("/")[0];
    idsToSlugs.forEach((slug, id) => {
      // 3_part => part-3-slug
      idsToSlugs.set(id, slug.replace(partId, pt.slug));
    });
  });
  chapters.forEach((ch) => {
    // 1_patterns/2_chapter/meta.md => 2_chapter
    const chapterId = ch.id.split("/")[1];
    idsToSlugs.forEach((slug, id, m) => {
      // 2_chapter => chapter-2-slug
      idsToSlugs.set(id, slug.replace(chapterId, ch.slug));
    });
  });
  lessons.forEach((l) => {
    // 1_patterns/2_chapter/meta.md => 2_chapter
    const lessonId = l.id.split("/")[2];
    idsToSlugs.forEach((slug, id, m) => {
      // 2_chapter => chapter-2-slug
      idsToSlugs.set(id, slug.replace(lessonId, l.slug));
    });
  });
  return Object.fromEntries(idsToSlugs.entries());
}

Now you get a map of each "id" -> "URL". I'll post another issue on that specifically.