pngwn / MDsveX

A markdown preprocessor for Svelte.
https://mdsvex.pngwn.io
MIT License
2.27k stars 96 forks source link

Getting the frontmatter in sveltekit __layout.svelte #313

Open Madd0g opened 2 years ago

Madd0g commented 2 years ago

I tried using an mdsvex layout and not using one, in both cases the default __layout.svelte in the folder cannot access the frontmatter from the document.

Is there a way to pass it from the mdsvex layout (blog_layout.svelte) to the default __layout.svelte?

I'm kind of a beginner in svelte so I don't know if there's an easy svelte-level solution for this. I tried setContext() from the mdsvex layout and getContext in the default layout, but that didn't work because __layout.svelte runs first.

babichjacob commented 2 years ago

I tried setContext() from the mdsvex layout and getContext in the default layout, but that didn't work because __layout.svelte runs first.

Try making what's in context a store and set the value of the store from the mdsvex layout rather than setting context.

babichjacob commented 2 years ago

I needed to solve the same problem today and I took a different approach by using import.meta.glob in a SvelteKit layout to import the page component given by the path in load, from which you can destructure the metadata.

This is complicated, so I'll share a code sample later.

(Also be aware that using import.meta.glob and especially import.meta.globEager in a Svelte component is a bad idea by default and only a good idea in exceptions like knowing that you will only ever evaluate the import for one / very few modules).

Madd0g commented 2 years ago

with import.meta.glob you have to import *.md from the folder, right? There's no way of passing a variable to it?

it seems like the method in #122 would be a little more efficient to get the frontmatter out of the file? I tried both approaches and both achieve the goal. it just seems a little awkward, very roundabout way of getting this data that is already loaded...

I tried setContext() from the mdsvex layout and getContext in the default layout, but that didn't work because __layout.svelte runs first.

Try making what's in context a store and set the value of the store from the mdsvex layout rather than setting context.

I want to get the frontmatter in __layout.svelte when it initializes (or in the load() function), not sure how to accomplish that with a store it shares with the mdsvex layout.

Thanks

kasisoft commented 2 years ago

I had the same issue and solved it this way:

PS: The __layout.svelte should just contain the slot tag as the frontmatter is provided by mdsvex so you need to use these layout files.

Madd0g commented 2 years ago

@kasisoft when you say "in your layout add the following", you mean the mdsvex layout, right?

<script lang="ts">
    export let fm;
</script>

Do you have an example of how to pass fm to the default layout __layout.svelte?

kasisoft commented 2 years ago

Yeah I mean the layouts for mdsvex so I'm sorry for commenting as my solution does not apply to layout.svelte which is the request for this issue (obviously I got carried away here). I'm only using the mdsvex layout whereas layout.svelte is just a placeholder.

Madd0g commented 2 years ago

@kasisoft, no worries, I had the same exact requirement too (passing the entire frontmatter as an object), so I'm sure someone finds it useful too :)

boehs commented 2 years ago

Alright @Madd0g, I see you have two questions

Dear god I am doing the exact thing as you!! I made some progress

How to get metadata to __layout

For this, I made a file called stores.js in $lib containing:

import { writable } from "svelte/store";
export const meta = writable({});

, then for my [slug].svelte (the dynamic route for my blogposts) I did

<script context="module">
    import { meta } from '$lib/stores';
</script>
<script lang="ts">
    meta.set(metadata);
</script>

, finally in my __layout:

<script lang="ts">
    import { meta } from '$lib/stores';
    let metadata;
    meta.subscribe(value => {
        metadata = value;
    });
</script>

and boom!


but where am I getting my metadata from in [slug].svelte?

How to make a blog from external files

So far so good! I have an obsidian blog located at C:/Users/<redacted>/Personal/Notes/blog/, and my site is at /C:/Users/<redacted>/Personal/Code/Contrib/site, an obvious problem!

What I did was

  1. symlink /blog to /site/posts
    posts
    src
    configs
  2. create a script in $lib called getPosts.ts with the following contents
    export function getPosts({ page = 1, limit } = {}) {
    let posts = Object.entries(import.meta.globEager('/posts/**/*.md'))
      .map(([, post]) => ({ metadata: post.metadata, component: post.default }))
      // sort by date
      .sort((a, b) => {
        return new Date(a.metadata.date).getTime() < new Date(b.metadata.date).getTime() ? 1 : -1
      })
      console.log(posts)
    if (limit) {
      return posts.slice((page - 1) * limit, page * limit)
    }
    return posts
    }

    and in [slug].svelte:

    <script context="module">
    import { getPosts } from '$lib/getPosts'
    export async function load({ page: { params } }) {
    const { slug } = params
    const post = getPosts().find((post) => slug === post.metadata.slug)
    if (!post) {
      return {
        status: 404,
        error: 'Post not found'
      }
    }
    return {
      props: {
        metadata: post.metadata,
        component: post.component
      }
    }
    }
    </script>

    (your final result for [slug].svelte is here https://paste.sr.ht/~boehs/fd339a61521e4d4d96df595f6e5d04b800d0124c)

so, lets open up slug..... fuck, damn vite protecting us from ourself!

image

but, it's an easy fix.

add to svelte.config.js:

    kit: {
        // hydrate the <div id="svelte"> element in src/app.html
        target: '#content',
        adapter: adapter({
            pages: 'public',
            assets: 'public'
          }),
        vite: {
        server: {
            fs: {
                allow: [
                // search up for workspace root
                // your custom rules
                'C:\\Users\\<redacted>\\Personal\\Notes\\blog\\*'
                ]
            }
            }
        }
    }

Annnnnd OMG WHOLY SHIT ITS WORKING AAAA

image

Why did I bother doing this


Known issues:

The last 3 might be my problem though

Bug: Fixing your CSS

add import { searchForWorkspaceRoot } from 'vite' to svelte.config.js and add searchForWorkspaceRoot(process.cwd()), to config.kit.vite.server.fs.allow`

Catch

let posts;
let purgeNEEDED = true;

export function getPosts(page = 1, limit, purge = false) {
  if (purgeNEEDED || purge) {
    posts = Object.entries(import.meta.globEager('/posts/**/*.md'))
    // format
    .map(([, post]) => ({ metadata: post.metadata, component: post.default }))
    // sort by date
    .sort((a, b) => {
      return new Date(a.metadata.date).getTime() < new Date(b.metadata.date).getTime() ? 1 : -1
    })
    console.log('posts purged and list regenerated.')
    if (purgeNEEDED) purgeNEEDED = false;
  }
  console.log(posts.find((post) => 'Dunkin' === post.metadata.slug))
  if (limit) {
    return posts.slice((page - 1) * limit, page * limit)
  }
  return posts;
}
Madd0g commented 2 years ago

@boehs - nice! I did approximately the same things to get mine working (even down to symlinking from the obsidian folder, heh). Some of it does feel very dirty, but works and I'm making progress.

Weird things:

  1. My dev server (or the browser, or both) freezing in dev mode. I click a link and it never loads, never shows any errors. Browser tab frozen and need to be closed.
  2. Vite complains about dynamic imports at build time (not at dev time), it says don't import dynamically from the folder you're in. But still seemingly works?
  3. Some "global" CSS that works in dev gets wiped away by the build process. I still don't fully understand everything about CSS in svelte.
boehs commented 2 years ago

It's certainly dirty. If only I could figure out the 404s, the metadata sometimes working, and unrelated but the navbar sometimes being half blank. I think they are all connected and probably not having anything to do with the blog system but can't for the life of me figure it out. Oh well. I'll give your implementation a look over!

zmre commented 2 years ago

I feel like sveltekit's $page.stuff would be a perfect place to put the frontmatter metadata to make it available elsewhere and particularly on __layout pages. From the docs, "The combined stuff is available to components using the page store as $page.stuff, providing a mechanism for pages to pass data 'upward' to layouts."

Unfortunately, I couldn't find an elegant way to push the frontmatter into stuff.

My alternative approach is a fair bit simpler than most of the above and it just uses a regular store. There's a file, metadata.ts, that looks like this (I'm omitting my typescript definitions for brevity):

import { writable, type Writable } from 'svelte/store';
export const pageMeta: Writable<PageMeta> = writable({});

Next, I have an SEO component that gets called from pretty much every page layout. Here's my entire blog layout file -- most of the actual layout comes from blog/__layout.svelte which is why this is minimal:

<script>
import Seo from './SEO.svelte';
export let title;
export let description;
export let author;
export let canonical;
export let socialImage;
</script>

<Seo {title} {description} {socialImage} {author} {canonical} />
<slot />

Finally, here are the relevant bits of the SEO component:

<script lang="ts">
  import { pageMeta } from '../stores/metadata';
  export let title: string;
  export let description: string;
  // ...
  $pageMeta = { title, description, author, canonical, socialImage };
</script>

And lastly, in my chain of __layout.svelte files, I can just use $pageMeta.title and $pageMeta.description and such for various purposes, like a breadcrumb display. I'll be adding some things like a related field so I can create cards showing related blogs at the bottom of an existing one and those will be rendered by the blog layout as well.

I don't love basically making those globals, but I have good reasons for wanting most of my HTML to be in the relevant layout files and I think this is the best available option. Hope that helps someone.

(note: I accidentally posted this on a different issue first... sorry for anyone that's seeing it twice)

patricknelson commented 2 years ago

@mvasigh made a great example of how to pull frontmatter into a layout by using endpoints at https://github.com/mvasigh/sveltekit-mdsvex-blog using this __layout.svelte and it’s corresponding [slug].json endpoint. There’s also an example for enumerating a list of posts for a listing page as well (index.svelte and the endpoint posts/index.json.js).

These of course still use import.meta.glob. I’m new to Svelte myself so I’m not entirely aware of why doing this is considered bad practice. I’ll be using this for generating a static site anyway, so maybe the impact is lower (as it’s done at build time and not at request).

reesericci commented 2 years ago

@zmre Is there any way to have mdsvex add this script tag to every page with front matter?

<script context="module">
    export async function load() {
        return { stuff: metadata }
    }
</script>
zmre commented 2 years ago

Not to my knowledge, but I'm not a mdsvex expert.

furudean commented 2 years ago

Me and @pngwn had discussion about this today in the Svelte Kit discord and we came to the conclusion that exposing the frontmatter into stuff would be the best way forward. This would allow you to utilize Svelte Kit's __layout.svelte in place of mdsvex layouts without any painful drawbacks. (Having two separate layout conventions is something we want to avoid anyway!)

Making SK __layouts.svelte the de-facto standard for mdsvex allows more ergonomic access to load(), which previously required ugly hacks like putting this in every single .svx page:

<script context="module"> 
  import { load } from "./_load.js" 
  export { load }
</script>
zmre commented 2 years ago

Well, damn. This is what I was hoping for, but I recently stumbled on this:

https://github.com/sveltejs/kit/issues/4911

Which seems to suggest that stuff is not long for this world. Could we just create our own store that someone could subscribe to? Import the store from mdsvex?

furudean commented 2 years ago

@zmre it's definitely not set in stone that it's going away, but even if it does there will definitely be a solution for passing data "up" as a replacement. mdsvex should be able to hook into whatever that ends up being.

on a mdsvex store, i'm not sure how that would work. would it be possible from an mdsvex perspective to set the value of the store during server rendering? (why should this be a store anyway...?) @pngwn

maybe getContext()/setContext() could be used here? definitely straying a bit outside my territory at this point.

zmre commented 2 years ago

I expect you're right about them needing to replace it with something that can serve a similar purpose if it goes away.

I think context only works within a group of components in the hierarchy and doesn't work for flowing up to layouts.

braebo commented 1 year ago

~I noticed the frontmatter is no longer visible on import.meta.glob with the new +page.md routing system in sveltekit.~

Maybe I missed it, but the metadata field is there. You can even import it directly:

const meta = import.meta.glob('my/**/pattern.md', { import: 'metadata', eager: true })
rchrdnsh commented 1 year ago

hmmm... using `import: 'metadata' does not seem to work for me...keep getting this error:

Cannot read properties of undefined (reading 'title')

...dunno why, tho...

ctwhome commented 2 months ago

Still a very hacky way to get it but I do import all the markdown files (posts in my case) and the filter by the current URL. blog.svelte layout:

<script>
    import ProfilePicture from '$lib/components/ProfilePicture.svelte';
    import { page } from '$app/stores';

    // TODO:
    // this is very hacky, but only way for now to get the metadata of the md file directly
    // The reason is that the metadata is not available in the layout file so we have to get it from the glob and filter by the current route
    // This is a workaround until we have a better solution coming from MDSveX
    const posts = import.meta.glob('/src/routes/posts/**/*.md', { eager: true });
    const metadata = Object.entries(posts).filter(
        (post) => post[0] === '/src/routes' + $page.route.id + '/+page.md'
    )[0][1].metadata;
</script>

<div class="mx-auto prose py-10 px-3">
    <h1 class="text-4xl font-bold mb-5">
        {metadata.title}
    </h1>
    <!-- Render content of the Markdown file -->
    <slot />
</div>

in the svelte.config:

const config = {

    preprocess: [
                 ...
        mdsvex({
            extensions: ['.md', '.svx'],
            layout: {
                _: "/src/layouts/default.svelte", // Default layout for markdown files
                blog: "/src/layouts/blog.svelte",
            }
        }),
    ],