Jacob Paris
← Back to all content

Serving files as routes with Remix

I needed to host some legal documents on a website, but I also wanted to be able to provide a plain text source so that the legal team could make their own modifications.

Then, if they have any changes, all I have to do is upload the new file instead of changing specific lines piece by piece.

The result of this is a /terms-of-service route that renders a well formatted article and a /terms-of-service.md route that shows the raw markdown source.

Create a layout route __markdown.tsx

import {Link, Outlet, useLocation} from 'remix'
export default function MarkdownLayout() {
const location = useLocation()
return (
<article className="mx-auto max-w-prose">
<div className="pb-12 mx-auto my-6 prose text-gray-500 prose-sky">
<Outlet />
View printable

Create a dynamic route that serves the file __markdown/$file.tsx

import ReactDOMServer from 'react-dom/server'
import {
} from 'remix'
const allowedFiles = [
export const loader: LoaderFunction = async ({
}) => {
if (!allowedFiles.includes(params.file)) {
return redirect('/')
const files = {
'terms-of-sale': import(
'terms-of-use': import(
'privacy-policy': import(
'financial-disclosure': import(
const file = await files[params.file].then(
(mod) => mod.default,
return ReactDOMServer.renderToString(file())
export default function PostSlug() {
const __html = useLoaderData()
return (
<div dangerouslySetInnerHTML={{__html}} />
Professional headshot

Hey there! I'm a developer, designer, and digital nomad building cool things with Remix, and I'm also writing Moulton, the Remix Community Newsletter

About once per month, I send an email with:

  • New guides and tutorials
  • Upcoming talks, meetups, and events
  • Cool new libraries and packages
  • What's new in the latest versions of Remix

Stay up to date with everything in the Remix community by entering your email below.

Unsubscribe at any time.