React app loading lots of markdown/html fragments with images

Hi,

I’m building my first app in Ionic with React and Capacitor. It consists of a single page with navigation based on accordion and carousel components. I’ve set up a skeleton app that looks very promising. The actual content is available as a set of 100+ markdown files (or if that would work out better, converted to html fragments). They contain images, also 100+ in number.

The problem has two levels: 1) importing the content files and 2) importing the images.

A component like react-markdown will render a markdown string, but how to load it from a local file? And what if there’s not one file, but a folder full of files, which I’d like to load dynamically based on a list of filenames? And if that works out, how to make sure the the images that it refers to can also be packaged as local files?

I did quite some reading, and so far it looks like this is not a real React use case. I did find a few solutions, but they all relied on webpack (and its require.context() feature), but that’s not coming with Ionic, if I’m right?

Maybe I should follow a completely different approach, but note that my site is 100% static, so starting with databases to store my content is overkill. Also, I like the fact that I have the content source as markdown, for if I need to edit it.

What direction should I follow? As a fallback, I was thinking of storing all markdown and images as strings (base64 for the images) in a big json container, but that’s quite silly and hard to maintain, not?

Best regards,
Vic

PS. the entry form guides me to an older post: https://forum.ionicframework.com/t/best-practice-for-pre-loading-an-all-offline-app-with-data-600-images/91765/9. Using the File plugin. Just to get my head around, if this works, how should I make sure that all my markdown and image files are packaged? Just putting them in src/ without an import or require in the code won’t work, will it? The post says ‘store them in your assets folder’, but is this public/assets or build/assets? Must I manually copy and maintain the files there? A bit of background info about the packaging process would be helpful and I didn’t find it so far…

Simplest option I can think of is load the content files by making HTTP requests using e.g. fetch. Just put them somewhere in the public folder.

Thanks!

In the meanwhile I made progress on my own :slight_smile: . I now can render my markdown, including images, using require.context (which turns out to be part of Ionic after all). Here’s my code:

import React, { useState, useEffect } from 'react';
import { IonContent } from '@ionic/react';
import './Home.css';
import ReactMarkdown from 'react-markdown';

// utilitary function to create a dictionary of packaged files 
// based on the output of require.context()
const importAll = (r: any, cache: {[key: string]: string}) => r.keys().forEach(
  (key: string) => cache[key] = r(key)
);

// create a dictionary of packaged markdownfiles, by './<source filename>'
const markdownFiles: {[key: string]: string} = {};
importAll(
  require.context('../data/markdown/', true, /\.markdown$/),
  markdownFiles
);
// create a dictionary of packaged image files, by './<source filename>'
const imageFiles: {[key: string]: string} = {};
importAll(
  require.context('../images/', true, /\.(png|gif|jpg)$/),
  imageFiles
);

// utilitary function to convert './<source filename>' to '<packaged filename>'
const transformImageUri = (uri: string) => imageFiles[uri];

function Home () {
 
  // state is a dictionary of the markdown content, by './<source filename>'
  const [markdownCache, setMarkdownCache] = useState<{[key: string]: string}>({});

  // fetch the content of the markdown files and store it as state
  useEffect(() => {
    let cache: {[key: string]: string} = {};
    function fetchMarkdown() {
      let promises = Object.keys(markdownFiles)
        .map(async (markdownFile: string) => {
          let markdownResponse = await fetch(markdownFiles[markdownFile]);
          return markdownResponse.text().then(text => { 
            cache[markdownFile] = text;
          });
      });
      return Promise.all(promises);
    }
    fetchMarkdown().then(() => { 
      setMarkdownCache(cache);
    });
  }, []);
  
  return (
    <IonContent>
      {/* render all markdown files; note that the image file 
          uri's must be in the form './<source filename>', 
          regardless the relative path in the source tree */}
      {Object.keys(markdownCache).map((markdownFile, index: number) => (
          <ReactMarkdown 
            key={index} 
            transformImageUri={transformImageUri} 
            source={markdownCache[markdownFile].replace(/\\n/g, "\n")} 
            escapeHtml={false}
          />
      ))}
    </IonContent>
  );
};

export default Home;

It may be made more concise and making more elegant use of arrow-functions and await async, but I’ve only very recently developing my javascript skills to newer standards…

I think I will be creating my own custom markdown component, which will have the fetching on board, so I don’t need the global cache dictionary, and use that component in a navigation skeleton built of some fancy off-the-shelf react components.

Best regards,
Vic