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

Getting the frontmatter in sveltekit __layout.svelte #313

Open
Tracked by #584 ...
Madd0g opened this issue Oct 3, 2021 · 21 comments
Open
Tracked by #584 ...

Getting the frontmatter in sveltekit __layout.svelte #313

Madd0g opened this issue Oct 3, 2021 · 21 comments
Labels
assigned Whether or not this bug has been assigned some to some other issues as a subtask or pre-req

Comments

@Madd0g
Copy link

Madd0g commented Oct 3, 2021

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Author

Madd0g commented Oct 4, 2021

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
Copy link

kasisoft commented Oct 10, 2021

I had the same issue and solved it this way:

  • In your layout add the following section in order to access the frontmatter (I've chosen the name 'fm' to refer to the complete frontmatter):
<script lang="ts">
    export let fm;
</script>
  • Then in your svelte.config.js you override the frontmatter parser (in mdsvex; using js-yaml to parse):
frontmatter: {
    marker:'-',
    type: 'yaml',
    parse: (frontmatter, messages) => {
         try {
             let content = yaml.load(frontmatter);
             return { 'fm': content, ...content };
         } catch (e) {
              messages.push(e.message);
         }
    }
}

I'm just recreating the frontmatter result while adding the whole content to a variable declared in my layouts (fm). You obviously can just select the variables you need but I prefer to have the whole thing.
Hope that helps or that someone else has a better solution.

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
Copy link
Author

Madd0g commented Oct 10, 2021

@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
Copy link

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
Copy link
Author

Madd0g commented Oct 10, 2021

@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
Copy link

boehs commented Oct 19, 2021

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
  1. 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:

  • This whole thing is a bad idea probably
  • Adapters won't work probably wtf they do
  • Maybe I should catch the blogposts instead of calling the func over and over
  • If I reload the page 3 times for some reason it 404's until I restart the dev server. I don't know why this was not related to my parser
  • For some reason my metadata won't work right
  • For some reason my css is nonexistent for some reason why

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
Copy link
Author

Madd0g commented Oct 20, 2021

@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
Copy link

boehs commented Oct 20, 2021

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
Copy link

zmre commented Mar 10, 2022

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
Copy link

@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
Copy link

reesericci commented May 11, 2022

@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
Copy link

zmre commented May 12, 2022

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

@furudean
Copy link

furudean commented May 27, 2022

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
Copy link

zmre commented May 27, 2022

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

sveltejs/kit#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
Copy link

furudean commented May 27, 2022

@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
Copy link

zmre commented May 27, 2022

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
Copy link

braebo commented Aug 30, 2022

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
Copy link

rchrdnsh commented Nov 3, 2022

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...

@pngwn pngwn added the assigned Whether or not this bug has been assigned some to some other issues as a subtask or pre-req label Feb 23, 2024
This was referenced Feb 23, 2024
@ctwhome
Copy link

ctwhome commented Apr 17, 2024

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",
			}
		}),
	],

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
assigned Whether or not this bug has been assigned some to some other issues as a subtask or pre-req
Projects
None yet
Development

No branches or pull requests