Open Madd0g opened 3 years ago
I tried
setContext()
from the mdsvex layout andgetContext
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 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).
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 andgetContext
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
I had the same issue and solved it this way:
<script lang="ts">
export let fm;
</script>
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.
@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
?
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.
@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 :)
Alright @Madd0g, I see you have two questions
Dear god I am doing the exact thing as you!! I made some progress
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
?
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
/blog
to /site/posts
posts
src
configs
$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!
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
Why did I bother doing this
Known issues:
The last 3 might be my problem though
add import { searchForWorkspaceRoot } from 'vite'
to svelte.config.js
and add searchForWorkspaceRoot(process.cwd()),
to config.kit.vite.server.fs.allow`
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;
}
@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:
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!
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)
@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).
@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>
Not to my knowledge, but I'm not a mdsvex expert.
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>
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?
@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.
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.
~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 })
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...
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",
}
}),
],
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 andgetContext
in the default layout, but that didn't work because__layout.svelte
runs first.