nuxt / content

The file-based CMS for your Nuxt application, powered by Markdown and Vue components.
https://content.nuxt.com
MIT License
3.07k stars 623 forks source link

Resolve assets in markdown content #106

Open TixieSalander opened 4 years ago

TixieSalander commented 4 years ago

I would like to know if it's possible to have the webpack assets resolution inside of markdown content ? To display webpack handeled's images.

Reproduction link: https://codesandbox.io/s/dawn-snowflake-fv7l4?file=/content/post.md

1) I have an image in the assets folder 2) I link the image in the md file ![](~/assets/photo.jpg) (or <img src="~/assets/photo.jpg"> in html) the same way I would have in a component template

I'm not interested in putting files in the static folder since there is no compression and no hash for cache-busting.

dtmzr commented 4 years ago

As far as I know, this is currently not supported out of the box.

Maybe you can solve this with a component that dynamically loads the image for you, something like this? https://codesandbox.io/s/nuxt-content-106-hwhbv?file=/content/post.md

TixieSalander commented 4 years ago

Yeah I thought about a solution like that, but not really compliant with content management tools :/

atinux commented 4 years ago

Hi there,

The main issue here is that @nuxt/content is independent of Webpack, meaning that if you change anything in content/ you can directly run nuxt export (Nuxt 2.13) without having to wait for Webpack to build.

We are thinking of a way to support assets/images/ with Webpack dynamic import so it can still support nuxt export without having to build again (expect if you add an image in assets/images/)

TixieSalander commented 4 years ago

I see it was removed from the todo list. Any update on the situation of this feature request?

MattFaz commented 4 years ago

Bumping, would love an update seeing as it has been removed from the To do?

benjamincanac commented 4 years ago

Don't pay too much attention to the todo list, we use it internally to plan the next release.

We have considered this issue and will be working on it soon.

atinux commented 4 years ago

The tricky part is to not involve Webpack for it when using images since it would force a rebuild and loose te speed of content update. This is why it is not an such easy task 😁

dtmzr commented 4 years ago

@Atinux might overwriting the html img tag by a custom component with the same attributes work for this case? Maybe even as an opt in? 🤔

atinux commented 4 years ago

Indeed your solution was quite good actually, if you can open a PR so we can work on it it would be great :)

Spunkie commented 4 years ago

The tricky part is to not involve Webpack

@Atinux Maybe I'm missing something but why wouldn't I want webpack involved doing it's thing? Wouldn't I want webpack to rerun if content involving a webpack asset gets updated?

atinux commented 4 years ago

The idea behing content is that you can change your Markdown without rebuilding your entire Nuxt app, making it faster redeployment.

Tracking assets is a bit more tricky in that case since Content does not depend of Webpack at all.

dtmzr commented 4 years ago

I fiddled around with it today and think I understood the way in which it could be implemented.

I would be really interested in your thoughts about the API design. Should we support both, markdown images and HTML tags?

![image alt text](~/assets/localimage.jpg)
<img src="~/assets/localimage.jpg" alt="image alt text>

I have a few doubts about using the default tags and attributes because in this case, we would've to check if the developer is trying to load a local asset or remote image.
Maybe we could avoid this check by adding a custom attribute:

<img local-src="~/assets/localimage.jpg" alt="image alt text>
dr1ss commented 4 years ago

Yeah both should be supported, this will give more control since you can't do basic things like center an image with markdown

davydnorris commented 4 years ago

Is it possible to store the images under the content directory itself instead of in the assets directory? Then you could create a structure like:

content/articles/my-new-article/index.md content/articles/my-new-article/img/pic1.jpg content/articles/my-new-article/img/pic2.jpg content/articles/my-new-article/img/pic3.jpg

which would keep all the content together.

dr1ss commented 4 years ago

Yes but this should not be the only solution.. again to have more control pointing to a specific folder is useful in case someone wants to use a CDN/reverse proxy to serve the images.

MuhaddiMu commented 4 years ago

@davydnorris suggestion was helpful to me

I created a global Image component to work around with that and stored my images inside the /content/blog/my-blog-slug/images/

<template>
  <img :src="imgSrc()" :alt="alt" />
</template>

<script>
export default {
  props: {
    src: {
      type: String,
      required: true
    },
    alt: {
      type: String,
      required: true
    }
  },
  methods: {
    imgSrc() {
      try {
        return require(`~/content${this.src}`)
      } catch (error) {
        return null
      }
    }
  }
}
</script>

Make sure to register this component globally. Inside components/global/Image.vue and set the components:true in nuxt.config.js

<image
          v-if="blog.Featured_Image"
          :src="blog.dir + '/images/' + blog.Featured_Image"
          :alt="blog.Title"
        >
</image>

Inside Markdown:

<image src="/blog/my-blog-slug/images/Scripts.png" alt="Scripts"></image>

I hope this will be helpful to someone. 🙌

davydnorris commented 4 years ago

I did the same thing but I am getting warnings when I compile the files of the form:

WARNING in ./content/blog/2020-02-15-blog-number-3/index.md 1:2
Module parse failed: Assigning to rvalue (1:2)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> ---
| title: 'Blog #3 Headline'
| description: Lorem Ipsum type stuff
 @ . sync ^\.\/content.*$ ./content/blog/2020-02-15-blog-number-3/index.md
 @ ./node_modules/babel-loader/lib??ref--2-0!./node_modules/vue-loader/lib??vue-loader-options!./components/BlogImage.vue?vue&type=script&lang=js&
 @ ./components/BlogImage.vue?vue&type=script&lang=js&
 @ ./components/BlogImage.vue
 @ ./node_modules/babel-loader/lib??ref--2-0!./node_modules/vue-loader/lib??vue-loader-options!./pages/blog/_slug.vue?vue&type=script&lang=js&
 @ ./pages/blog/_slug.vue?vue&type=script&lang=js&
 @ ./pages/blog/_slug.vue
 @ ./.nuxt/router.js
 @ ./.nuxt/index.js
 @ ./.nuxt/client.js
 @ multi ./.nuxt/client.js

I thought I could ignore the warning, however when I then try to run export or dev, the process hangs and never completes, and I see the above warning in the generated javascript instead of the actual content.

I'm not a webpack/babel config file expert, but it looks like the error is from webpack when it goes looking for the image files. I assume you could write a rule to exclude the md files somehow? But then would that mess up the content generation?

Any help would be greatly appreciated

MuhaddiMu commented 4 years ago

Unfortunately, this is the only solution worked for me. I have the same warnings. However, it doesn't affect the build process.

davydnorris commented 4 years ago

OK so I have made progress...

I couldn't work out why my images were being converted during the build but their paths weren't - then I realised that the images I happened to be using were IMG_xxxx.JPG and I had them in the code as IMG_xxxx.jpg. As soon as I made them exactly the same they all got picked up.

Still trying to get the custom image component inside the .md to work - @MuhaddiMu did you register yours as a global?

MuhaddiMu commented 4 years ago

@davydnorris My Image component is registered globally. I have a working example, you can check that too by cloning https://github.com/MuhaddiMu/Portfolio

davydnorris commented 4 years ago

Many thanks - I have now got it working 🥇

The components directive doesn't seem to do exactly what it says in the guide. I had set up a subdirectory specifically for blog components, called 'content', and had registered that in my nuxt config with:

components: [
  '~/components/content'
]

but it didn't seem to work. I then moved the components into the top level and set components:true, but still no luck.

It finally worked when I copied your example exactly and added a 'global' subdirectory with components:true.

Thank you for all your assistance!!

djmtype commented 4 years ago

Besides images used in HTML and markdown, what about images referenced in frontmatter? I've had to add these to static/images in order for this to work instead of assets.

img: /images/journey-ux.jpg
alt: Rocket soaring through space
davydnorris commented 4 years ago

This were actually the first ones I got to work. I used the same approach as above but could use a normal component in my _slug.vue and preview components:

<template>
  <div>
    <v-img
      :src="require(`@/content${post.dir}/img/${post.image}`)"
      :alt="post.title"
      height="400"
    />
    <h1>{{ post.title }}</h1>
    <p>
      {{ post.description }}
    </p>
    <nuxt-content
      :document="post"
    />
  </div>
</template>

<script>
export default {
  async asyncData ({ params, error, $content }) {
    try {
      const postPath = `/blog/${params.slug}`;
      const [post] = await $content('blog', { deep: true })
        .where({ dir: postPath })
        .fetch();
      return { post };
    } catch (err) {
      error({
        statusCode: 404,
        message: 'Page could not be found'
      });
    }
  }
};
</script>

in your case you've already included the image subdirectory so just adjust the template accordingly

lucien144 commented 4 years ago

@davydnorris Regarding the Module parse failed: Assigning to rvalue (1:2) error. Using ignore-loader solved this for me:

// nuxt.config.js
// ...
build: {
    extend(config, { isDev, isClient }) {
        config.module.rules.push({
            test: /\.md$/i,
            loader: 'ignore-loader'
        })
    }
}
// ...
davydnorris commented 4 years ago

Many thanks @lucien144 - that's exactly what I was hoping someone would know! Will try that tomorrow

TixieSalander commented 4 years ago

My original point was to be able using the webpack asset management for images (compression, cache-busting), while writing posts on content management tools. For that, it's needs to work with vanilla , and not some custom vuejs elements.

ZeanQin commented 4 years ago

Hi there,

The main issue here is that @nuxt/content is independent of Webpack, meaning that if you change anything in content/ you can directly run nuxt export (Nuxt 2.13) without having to wait for Webpack to build.

We are thinking of a way to support assets/images/ with Webpack dynamic import so it can still support nuxt export without having to build again (expect if you add an image in assets/images/)

What about linking to assets that are not images? For example, linking to a local pdf file with [A file](/assets/sample.pdf) in the markdown? It's not mentioned on this thread but I presume it's essentially the same issue?

renoirb commented 4 years ago

Yes. Assets from Markdown written with fully qualified URL ~/images/ to be resolvable either from Vue pages/ or components/ components as equal to from a Markdown file. I'm assuming we could have the place where to store those as configurable. Either from static, or from content, or assets. So we avoid having more than one notation for the same resource and the resource resolver in its context (remark's toTHML, or WebPack, or vite) to pick which ever its running.

Defite commented 3 years ago

So many ugly crutches just to render image from Markdown, oh God. Nuxt is so 'intuitive' that I had less time to do it by myself with 11ty or at my self made ssg on Python.

Defite commented 3 years ago
extend(config, { isDev, isClient }) {
      config.module.rules.push({
          test: /\.md$/i,
          loader: 'ignore-loader'
      })
  }

now page with image doesn't render at all, thank you very much!

davydnorris commented 3 years ago

My original point was to be able using the webpack asset management for images (compression, cache-busting), while writing posts on content management tools. For that, it's needs to work with vanilla , and not some custom vuejs elements.

@TixieBorg - I've found that being able to use custom vuejs within the markdown has been amazingly useful. By wrapping my images in a Vuejs component I have got total control of the size, positioning, wrapping, etc. This is not available in markdown.

Webpack still operates on the images and does everything you need, but as part of Vuejs, not markdown.

I have to say, I absolutely love nuxt/content - it's exactly what I needed for my purpose.

davydnorris commented 3 years ago

@Defite - not sure what you have done, but that rule works perfectly for me.

Note that the rule is not required, it's just there to shut up any warnings - I still got a working site without it. If you remove it, does your page work?

Defite commented 3 years ago

@Defite - not sure what you have done, but that rule works perfectly for me.

Note that the rule is not required, it's just there to shut up any warnings - I still got a working site without it. If you remove it, does your page work?

I tried different variants with images placed in:

In my markdown file I use it like this:

<v-img src="post.jpg" />

Every time I get post.jpg in resulting code or page doesn't render =(

lucien144 commented 3 years ago

@Defite Well, some error log would be helpful... Looks like you are using some image component without specifying path (Vuetify perhaps?) so I would suspect the problem is in the component and not in the Nuxt's content module. Using simple img tag or markdown's ![]() works fine?

Defite commented 3 years ago

@Defite Well, some error log would be helpful... Looks like you are using some image component without specifying path (Vuetify perhaps?) so I would suspect the problem is in the component and not in the Nuxt's content module. Using simple img tag or markdown's ![]() works fine?

Simple img tag (inside global component VImg) works fine. But when I try to put there <nuxt-image> I got ERROR Cannot read property 'validate' of undefined and this in image source: /_image/ipx/local/_/_/_nuxt/assets/images/my.png. So may be I must use nuxt-image in Markdown?

I'll provide repo to show current state of my test.

Defite commented 3 years ago

Here is repo https://github.com/Defite/nuxt-image-test.

https://github.com/Defite/nuxt-image-test/blob/master/components/global/VImg.vue I included both working and not working variants. Simple img is working ok.

https://github.com/Defite/nuxt-image-test/blob/master/content/blog/image-post.md - markdown file

Defite commented 3 years ago

So many ugly crutches just to render image from Markdown, oh God. Nuxt is so 'intuitive' that I had less time to do it by myself with 11ty or at my self made ssg on Python.

And sorry for being toxic, I'm just too tired of recreating simple solution third time for another framework. My understanding of world is just too idealistic, I just wanna to be sure that all things inside one ecosystem work perfectly. And it was really disappointing to realize that it's not.

davydnorris commented 3 years ago

@Defite - I am using Vuetify, with Vuejs, with nuxt, with nuxt/content...

If I can get it working, with some problems, but nothing that took more than a few hours of effort, and with the end result that's perhaps even better than out-of-the-box...

You can do it my friend :-)

davydnorris commented 3 years ago

Or do it in Python as you said - I know both, and for what I need I am really happy here

Defite commented 3 years ago

So many ugly crutches just to render image from Markdown, oh God. Nuxt is so 'intuitive' that I had less time to do it by myself with 11ty or at my self made ssg on Python.

And sorry for being toxic, I'm just too tired of recreating simple solution third time for another framework. My understanding of world is just too idealistic, I just wanna to be sure that all things inside one ecosystem work perfectly. And it was really disappointing to realize that it's not.

PS: I am 54... if you're older than me - get with the young kids and get off the punch cards. If you're younger than me, then omg shut the eff up and actually code something- us oldies got 20 more languages than you!!! ;-)

Well, every day I code literally tons of code. I just don't understand why the exact product is not working. Sure, it's opensource and bla bla, but why? People who made it didn't think about how it really must work?

Ok, it's offtop. Thanks, I got ya.

atinux commented 3 years ago

@Defite <nuxt-image> is not ready yet, we are actively working on the stable version, see https://github.com/nuxt/image/pull/99

Please stay polite, we all do our best to make it as good as possible.

Defite commented 3 years ago

@Defite <nuxt-image> is not ready yet, we are actively working on the stable version, see nuxt/image#99

Please stay polite, we all do our best to make it as good as possible.

Great news. And again, sorry for being toxic here. Rough day.

davydnorris commented 3 years ago

I am using Vuetify like you. This is what I have in my site - pretty much exactly what is further up the thread

BlogImage.vue

<template>
  <v-img
    :src="imgSrc()"
    :alt="alt"
  />
</template>

<script>
export default {
  props: {
    dir: {
      type: String,
      required: true
    },
    src: {
      type: String,
      required: true
    },
    alt: {
      type: String,
      required: true
    }
  },
  methods: {
    imgSrc () {
      try {
        return require(`~/content${this.dir}/img/${this.src}`);
      } catch (e) {
        console.log(e);
      }
    }
  }
};
</script>

This is sitting in the components/global folder.

Then I have arranged my blog so that all images are stored under an img folder, and within the markdown I have

## Displaying content
<blog-image dir="/blog/2019-12-20-blog-number-2" src="IMG_6264.JPG" alt="text" />
Display stuff

And everything works for me. I have since added props to size and position the image as well

Defite commented 3 years ago

I am using Vuetify like you. This is what I have in my site - pretty much exactly what is further up the thread

BlogImage.vue

<template>
  <v-img
    :src="imgSrc()"
    :alt="alt"
  />
</template>

<script>
export default {
  props: {
    dir: {
      type: String,
      required: true
    },
    src: {
      type: String,
      required: true
    },
    alt: {
      type: String,
      required: true
    }
  },
  methods: {
    imgSrc () {
      try {
        return require(`~/content${this.dir}/img/${this.src}`);
      } catch (e) {
        console.log(e);
      }
    }
  }
};
</script>

This is sitting in the components/global folder.

Then I have arranged my blog so that all images are stored under an img folder, and within the markdown I have

## Displaying content
<blog-image dir="/blog/2019-12-20-blog-number-2" src="IMG_6264.JPG" alt="text" />
Display stuff

And everything works for me. I have since added props to size and position the image as well

I'm not using Vuetify)

davydnorris commented 3 years ago

OK well, whatever you're using substitute the appropriate image component, or just use img

NozomuIkuta commented 3 years ago

Here are what I tried and found.

Demo (CodeSandbox)

First, I created a rehype plugin, which is inspired by a comment by @dtmzr.

@Atinux might overwriting the html img tag by a custom component with the same attributes work for this case? Maybe even as an opt in? 🤔

This rehype plugin detects img nodes in AST and replaces them with app-image node, when src attribute starts with ".", "~", or "@" (cf. how vue-loader converts paths to require function call).

Second, I defined a global component AppImage in Nuxt app (i.e. ~/components/global/AppImage.vue). AppImage accepts 2 props, src and alt and renders a single img element. While value of alt prop is directly bound to alt attribute of the img element, value of src prop is modified before being binding to require function call.

AppImage removes the part ~/assets/ from src value and concatenates them again when calling require function in the template block. In this way, webpack can detect require with expression and can be aware of files in ~/assets/ directories in the bundle.

Actually, this point is where everyone faced at many warning messages like @davydnorris reported:

I did the same thing but I am getting warnings when I compile the files of the form:

WARNING in ./content/blog/2020-02-15-blog-number-3/index.md 1:2
Module parse failed: Assigning to rvalue (1:2)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> ---
| title: 'Blog #3 Headline'
| description: Lorem Ipsum type stuff
 @ . sync ^\.\/content.*$ ./content/blog/2020-02-15-blog-number-3/index.md
[omit]

webpack has to include all the files whose path possibly match the expression in require function call to handle all the cases. This is why files which don't have the associated loaders are warned.

You can see it in developer tool that a map of file paths which webpack is aware of. Screenshot below describes the case where only files which can match "~/assets/**/*" are managed by webpack.

Screen Shot 2020-12-23 at 5 02 11

My implementation is just a proof of concept, so there would be many points to improve, for example:

There are also some limitations. One is that changes on images are not watched and do not trigger app rebuilding. To see the newest images, let webpack require the images again (page refreshing, I guess).


@Atinux

I am thinking of creating a draft PR for further discussion, which introduces another package to Nuxt Content repository (i.e. packages/rehype-assets).

Perhaps, have you already found some solution for this issue?

sts-ryan-holton commented 3 years ago

I'm trying to get images to work, for me, images work on initial page load if coming at a URL from Google, but as soon as you refresh the page, the images loaded using a Nuxt JS component via my *.md files disappear, how can I fix this?

Each article, md file has:

<dynamic-image image-alt="how does website downtime affect a business" file="blogs/articles/actual-costs-of-website-downtime/business-affects.jpg"></dynamic-image>

Which is importing the component, and the component looks like...

<template>
  <b-img :src="dynamicImage" :alt="imageAlt" fluid class="mt-1 mb-3 rounded"></b-img>
</template>

<script>
export default {
  props: {
    file: {
      type: String,
      required: true
    },
    imageAlt: {
      type: String,
      default: 'FREE Domain Monitor'
    }
  },
  computed: {
    dynamicImage() {
      return require(`~/assets/images/content/${this.file}`)
    }
  }
}
</script>
el-pol commented 3 years ago

In the docs, it should be mentioned that images in markdown are not supported yet. It's no fun to go through all the setup just to realize at the end of it that they don't work. This is such a basic feature.

EDIT: found a good workaround here https://gilberttanner.com/blog/creating-a-blog-with-nuxt-content#gettingimagestowork

florincosta commented 3 years ago

I did the same thing but I am getting warnings when I compile the files of the form:

WARNING in ./content/blog/2020-02-15-blog-number-3/index.md 1:2
Module parse failed: Assigning to rvalue (1:2)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> ---
| title: 'Blog #3 Headline'
| description: Lorem Ipsum type stuff
 @ . sync ^\.\/content.*$ ./content/blog/2020-02-15-blog-number-3/index.md
 @ ./node_modules/babel-loader/lib??ref--2-0!./node_modules/vue-loader/lib??vue-loader-options!./components/BlogImage.vue?vue&type=script&lang=js&
 @ ./components/BlogImage.vue?vue&type=script&lang=js&
 @ ./components/BlogImage.vue
 @ ./node_modules/babel-loader/lib??ref--2-0!./node_modules/vue-loader/lib??vue-loader-options!./pages/blog/_slug.vue?vue&type=script&lang=js&
 @ ./pages/blog/_slug.vue?vue&type=script&lang=js&
 @ ./pages/blog/_slug.vue
 @ ./.nuxt/router.js
 @ ./.nuxt/index.js
 @ ./.nuxt/client.js
 @ multi ./.nuxt/client.js

I thought I could ignore the warning, however when I then try to run export or dev, the process hangs and never completes, and I see the above warning in the generated javascript instead of the actual content.

I'm not a webpack/babel config file expert, but it looks like the error is from webpack when it goes looking for the image files. I assume you could write a rule to exclude the md files somehow? But then would that mess up the content generation?

Any help would be greatly appreciated

To make the WARNING go away you MUST add the image ext '.jpg' or '.gif' or ..., to the require call, this will filter out all the other files like '.md' and only targeted image file will be parsed. It's also very IMPORTANT to start the require with '~/assets/' or '~/content/' including all path separators, for example require(`~/assets/${this.file}.jpg`) , please read here require-with-expression

Here is my solution for requiring images from assets and content in markdown files without using html component notation in markdown, only standard markdown image notation.

Codesandbox

Dir structure: assets/logo1.svg content/post1/index.md content/post1/logo1.svg content/post1/img/logo2.svg content/post2/img/logo.svg

In 'index.md' file: Content relative ![Logo](./logo1.svg "Logo Text") ![Logo](./image/logo2.svg "Logo Text")

Content absolute ![Logo](~/content/post2/img/logo.svg "Logo Text")

Assets absolute ![Logo](~/assets/logo1.svg "Logo Text")

Solution:

  1. Use rehype to convert all markdown img tags in content-img
  2. Have a global component named content-img(must be located in components/global/ContentImg.vue to be loaded for '.md') that knows how to handle the require paths(described above) from the '.md' files
  3. Enjoy images in content.

All code is located in above code sandbox, see plugins/rehype-content-image and components/global/ContentImg

NickSto commented 3 years ago

@davydnorris

imgSrc () {
  try {
    return require(`~/content${this.dir}/img/${this.src}`);
  } catch (e) {
    console.log(e);
  }
## Displaying content
<blog-image dir="/blog/2019-12-20-blog-number-2" src="IMG_6264.JPG" alt="text" />
Display stuff

I see you're still passing in the directory path manually. Is there any way to avoid that and have imgSrc() figure out the directory of the .md file itself? I'd really love to be able to just say <v-img src="image.jpg"/>.