use-theme-editor
Version:
Zero configuration CSS variables based theme editor
689 lines • 189 kB
HTML
<!DOCTYPE html>
<!-- saved from url=(0043)https://remix.run/docs/en/v1/tutorials/blog -->
<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="theme-color" content="#121212"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><link rel="icon" href="https://remix.run/favicon-light.1.png" type="image/png" media="(prefers-color-scheme: light)"><link rel="icon" href="https://remix.run/favicon-dark.1.png" type="image/png" media="(prefers-color-scheme: dark)"><link rel="preconnect" href="https://fonts.googleapis.com/"><link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin="anonymous"><link href="./Remix _ Blog Tutorial (short)_files/css2" rel="stylesheet"><link rel="preload" as="font" href="https://remix.run/font/founders-grotesk-bold.woff2" crossorigin="anonymous"><link rel="preload" as="font" href="https://fonts.gstatic.com/s/inter/v8/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2" crossorigin="anonymous"><link rel="preload" as="font" href="https://fonts.gstatic.com/s/sourcecodepro/v20/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevWnsUnxg.woff2" crossorigin="anonymous"><link rel="stylesheet" href="./Remix _ Blog Tutorial (short)_files/tailwind-K775WQNR.css"><link rel="stylesheet" href="./Remix _ Blog Tutorial (short)_files/bailwind-QVE66GOL.css"><link rel="stylesheet" href="./Remix _ Blog Tutorial (short)_files/docs-DYGEVPUH.css"><link rel="stylesheet" href="./Remix _ Blog Tutorial (short)_files/style-7EPHLDZJ.css"><link rel="stylesheet" href="./Remix _ Blog Tutorial (short)_files/docsearch-LMN4YJJS.css"><meta content="en" name="docsearch:language"><meta content="v1" name="docsearch:version"><title>Remix | Blog Tutorial (short)</title><meta id="ConnectiveDocSignExtentionInstalled" name="ConnectiveDocSignExtentionInstalled" data-extension-version="1.0.6"></head><body x-comp="Body" class="min-h-screen flex flex-col w-full overflow-x-hidden bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-200 undefined"><div class="lg:flex lg:h-full px-6"><div class="lg:hidden"><div class="absolute top-6 right-6 flex gap-2 items-center"><div class="mr-2"><button type="button" class="DocSearch DocSearch-Button" aria-label="Search"><span class="DocSearch-Button-Container"><svg width="20" height="20" class="DocSearch-Search-Icon" viewBox="0 0 20 20"><path d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z" stroke="currentColor" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"></path></svg><span class="DocSearch-Button-Placeholder">Search</span></span><span class="DocSearch-Button-Keys"><kbd class="DocSearch-Button-Key"><svg width="15" height="15" class="DocSearch-Control-Key-Icon"><path d="M4.505 4.496h2M5.505 5.496v5M8.216 4.496l.055 5.993M10 7.5c.333.333.5.667.5 1v2M12.326 4.5v5.996M8.384 4.496c1.674 0 2.116 0 2.116 1.5s-.442 1.5-2.116 1.5M3.205 9.303c-.09.448-.277 1.21-1.241 1.203C1 10.5.5 9.513.5 8V7c0-1.57.5-2.5 1.464-2.494.964.006 1.134.598 1.24 1.342M12.553 10.5h1.953" stroke-width="1.2" stroke="currentColor" fill="none" stroke-linecap="square"></path></svg></kbd><kbd class="DocSearch-Button-Key">K</kbd></span></button></div><a href="https://remix.run/docs/en/v1"><svg x-comp="Wordmark" height="24" viewBox="0 0 659 165" fill="none" xmlns="http://www.w3.org/2000/svg"><title>Remix Logo</title><path fill-rule="evenodd" clip-rule="evenodd" d="M133.85 124.16C135.3 142.762 135.3 151.482 135.3 161H92.2283C92.2283 158.927 92.2653 157.03 92.3028 155.107C92.4195 149.128 92.5411 142.894 91.5717 130.304C90.2905 111.872 82.3473 107.776 67.7419 107.776H54.8021H0V74.24H69.7918C88.2407 74.24 97.4651 68.632 97.4651 53.784C97.4651 40.728 88.2407 32.816 69.7918 32.816H0V0H77.4788C119.245 0 140 19.712 140 51.2C140 74.752 125.395 90.112 105.665 92.672C122.32 96 132.057 105.472 133.85 124.16Z" fill="currentColor"></path><path d="M0 161V136H45.5416C53.1486 136 54.8003 141.638 54.8003 145V161H0Z" fill="currentColor"></path><path d="M654.54 47.1035H611.788L592.332 74.2395L573.388 47.1035H527.564L568.78 103.168L523.98 161.28H566.732L589.516 130.304L612.3 161.28H658.124L613.068 101.376L654.54 47.1035Z" fill="url(#paint0_linear)"></path><path d="M654.54 47.1035H611.788L592.332 74.2395L573.388 47.1035H527.564L568.78 103.168L523.98 161.28H566.732L589.516 130.304L612.3 161.28H658.124L613.068 101.376L654.54 47.1035Z" fill="currentColor"></path><path d="M229.43 120.576C225.59 129.536 218.422 133.376 207.158 133.376C194.614 133.376 184.374 126.72 183.35 112.64H263.478V101.12C263.478 70.1437 243.254 44.0317 205.11 44.0317C169.526 44.0317 142.902 69.8877 142.902 105.984C142.902 142.336 169.014 164.352 205.622 164.352C235.83 164.352 256.822 149.76 262.71 123.648L229.43 120.576ZM183.862 92.6717C185.398 81.9197 191.286 73.7277 204.598 73.7277C216.886 73.7277 223.542 82.4317 224.054 92.6717H183.862Z" fill="url(#paint1_linear)"></path><path d="M229.43 120.576C225.59 129.536 218.422 133.376 207.158 133.376C194.614 133.376 184.374 126.72 183.35 112.64H263.478V101.12C263.478 70.1437 243.254 44.0317 205.11 44.0317C169.526 44.0317 142.902 69.8877 142.902 105.984C142.902 142.336 169.014 164.352 205.622 164.352C235.83 164.352 256.822 149.76 262.71 123.648L229.43 120.576ZM183.862 92.6717C185.398 81.9197 191.286 73.7277 204.598 73.7277C216.886 73.7277 223.542 82.4317 224.054 92.6717H183.862Z" fill="currentColor"></path><path d="M385.256 66.5597C380.392 53.2477 369.896 44.0317 349.672 44.0317C332.52 44.0317 320.232 51.7117 314.088 64.2557V47.1037H272.616V161.28H314.088V105.216C314.088 88.0638 318.952 76.7997 332.52 76.7997C345.064 76.7997 348.136 84.9917 348.136 100.608V161.28H389.608V105.216C389.608 88.0638 394.216 76.7997 408.04 76.7997C420.584 76.7997 423.4 84.9917 423.4 100.608V161.28H464.872V89.5997C464.872 65.7917 455.656 44.0317 424.168 44.0317C404.968 44.0317 391.4 53.7597 385.256 66.5597Z" fill="url(#paint2_linear)"></path><path d="M385.256 66.5597C380.392 53.2477 369.896 44.0317 349.672 44.0317C332.52 44.0317 320.232 51.7117 314.088 64.2557V47.1037H272.616V161.28H314.088V105.216C314.088 88.0638 318.952 76.7997 332.52 76.7997C345.064 76.7997 348.136 84.9917 348.136 100.608V161.28H389.608V105.216C389.608 88.0638 394.216 76.7997 408.04 76.7997C420.584 76.7997 423.4 84.9917 423.4 100.608V161.28H464.872V89.5997C464.872 65.7917 455.656 44.0317 424.168 44.0317C404.968 44.0317 391.4 53.7597 385.256 66.5597Z" fill="currentColor"></path><path d="M478.436 47.104V161.28H519.908V47.104H478.436ZM478.18 36.352H520.164V0H478.18V36.352Z" fill="url(#paint3_linear)"></path><path d="M478.436 47.104V161.28H519.908V47.104H478.436ZM478.18 36.352H520.164V0H478.18V36.352Z" fill="currentColor"></path><defs><lineargradient id="paint0_linear" x1="591.052" y1="47.1035" x2="591.052" y2="161.28" gradientUnits="userSpaceOnUse"><stop stop-color="currentColor"></stop><stop offset="1" stop-color="currentColor" stop-opacity="0"></stop></lineargradient><lineargradient id="paint1_linear" x1="203.19" y1="44.0317" x2="203.19" y2="164.352" gradientUnits="userSpaceOnUse"><stop stop-color="currentColor"></stop><stop offset="1" stop-color="currentColor" stop-opacity="0"></stop></lineargradient><lineargradient id="paint2_linear" x1="368.744" y1="44.0317" x2="368.744" y2="161.28" gradientUnits="userSpaceOnUse"><stop stop-color="currentColor"></stop><stop offset="1" stop-color="currentColor" stop-opacity="0"></stop></lineargradient><lineargradient id="paint3_linear" x1="499.172" y1="0" x2="499.172" y2="161.28" gradientUnits="userSpaceOnUse"><stop stop-color="currentColor"></stop><stop offset="1" stop-color="currentColor" stop-opacity="0"></stop></lineargradient></defs></svg></a></div><details><summary class="pb-4 pt-6 cursor-pointer">Docs Navigation</summary><div><nav class="md-nav py-6 text-d-p-sm font-medium"><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="1"><li data-dir="" data-level="1"><span class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-1 md-nav-heading pt-0">Tutorials</span><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="2"><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2 md-nav-item--active" href="https://remix.run/docs/en/v1/tutorials/blog" aria-current="page">Blog Tutorial (short)</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/tutorials/jokes">App Tutorial (long)</a></li></ul></li><li data-dir="" data-level="1"><span class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-1 md-nav-heading">Pages</span><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="2"><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/pages/philosophy">Philosophy</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/pages/technical-explanation">Technical Explanation</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/pages/stacks">Remix Stacks</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/pages/contributing">Contributing</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/pages/community">Community</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/pages/faq">FAQs</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/pages/gotchas">Gotchas</a></li></ul></li><li data-dir="" data-level="1"><span class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-1 md-nav-heading">File Conventions</span><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="2"><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/file-conventions/entry.client">entry.client</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/file-conventions/entry.server">entry.server</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/file-conventions/remix-config">remix.config.js</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/file-conventions/root">root</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/file-conventions/routes-files">Route File Naming</a></li></ul></li><li data-dir="" data-level="1"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-1 md-nav-heading" href="https://remix.run/docs/en/v1/route">Route Module API</a><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="2"><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/action">action</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/catch-boundary">CatchBoundary</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/component">Component</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/error-boundary">ErrorBoundary</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/handle">handle</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/headers">headers</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/links">links</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/loader">loader</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/meta">meta</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/should-reload">unstable_shouldReload</a></li></ul></li><li data-dir="" data-level="1"><span class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-1 md-nav-heading">Components</span><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="2"><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/form">Form</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/link">Link</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/links">Links</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/live-reload">LiveReload</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/meta">Meta</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/nav-link">NavLink</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/outlet">Outlet</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/prefetch-page-links">PrefetchPageLinks</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/scripts">Scripts</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/scroll-restoration">ScrollRestoration</a></li></ul></li><li data-dir="" data-level="1"><span class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-1 md-nav-heading">Hooks</span><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="2"><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/hooks/use-action-data">useActionData</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/hooks/use-before-unload">useBeforeUnload</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/hooks/use-fetcher">useFetcher</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/hooks/use-fetchers">useFetchers</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/hooks/use-form-action">useFormAction</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/hooks/use-loader-data">useLoaderData</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/hooks/use-matches">useMatches</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/hooks/use-submit">useSubmit</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/hooks/use-transition">useTransition</a></li></ul></li><li data-dir="" data-level="1"><span class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-1 md-nav-heading">Utilities</span><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="2"><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/utils/cookies">Cookies</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/utils/json">json</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/utils/redirect">redirect</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/utils/sessions">Sessions</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/utils/unstable-create-file-upload-handler">unstable_createFileUploadHandler</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/utils/unstable-create-memory-upload-handler">unstable_createMemoryUploadHandler</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/utils/parse-multipart-form-data">unstable_parseMultipartFormData</a></li></ul></li><li data-dir="" data-level="1"><span class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-1 md-nav-heading">Other API</span><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="2"><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/other-api/dev">@remix-run/dev (CLI)</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/other-api/adapter">@remix-run/{adapter}</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/other-api/serve">@remix-run/serve</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/other-api/node">@remix-run/node</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/other-api/asset-imports">Asset Imports</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/other-api/react-router">React Router v6</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/other-api/fetch">Web Fetch API</a></li></ul></li><li data-dir="" data-level="1"><span class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-1 md-nav-heading">Guides</span><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="2"><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/accessibility">Accessibility</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/api-routes">API Routes</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/bff">Backend For Frontend</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/browser-support">Browser Support</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/data-loading">Data Loading</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/data-writes">Data Writes</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/deployment">Deployment</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/disabling-javascript">Disabling JavaScript</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/envvars">Environment Variables</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/errors">Error Handling</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/file-uploads">File Uploads</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/mdx">MDX</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/migrating-react-router-app">Migrating from React Router</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/constraints">Module Constraints</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/not-found">Not Found Handling</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/optimistic-ui">Optimistic UI</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/performance">Performance</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/resource-routes">Resource Routes</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/routing">Routing</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/styling">Styling</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/typescript">TypeScript</a></li></ul></li></ul></nav></div></details><hr class="mb-4"></div><div class="shrink-0 hidden lg:block"><div class="h-full max-h-screen overflow-x-hidden overflow-y-auto w-64 xl:w-80 2xl:w-96 sticky top-0 py-10 pl-6 pr-3 xl:pr-5 2xl:pr-6"><a href="https://remix.run/docs/en/v1"><svg x-comp="Wordmark" height="24" viewBox="0 0 659 165" fill="none" xmlns="http://www.w3.org/2000/svg"><title>Remix Logo</title><path fill-rule="evenodd" clip-rule="evenodd" d="M133.85 124.16C135.3 142.762 135.3 151.482 135.3 161H92.2283C92.2283 158.927 92.2653 157.03 92.3028 155.107C92.4195 149.128 92.5411 142.894 91.5717 130.304C90.2905 111.872 82.3473 107.776 67.7419 107.776H54.8021H0V74.24H69.7918C88.2407 74.24 97.4651 68.632 97.4651 53.784C97.4651 40.728 88.2407 32.816 69.7918 32.816H0V0H77.4788C119.245 0 140 19.712 140 51.2C140 74.752 125.395 90.112 105.665 92.672C122.32 96 132.057 105.472 133.85 124.16Z" fill="currentColor"></path><path d="M0 161V136H45.5416C53.1486 136 54.8003 141.638 54.8003 145V161H0Z" fill="currentColor"></path><path d="M654.54 47.1035H611.788L592.332 74.2395L573.388 47.1035H527.564L568.78 103.168L523.98 161.28H566.732L589.516 130.304L612.3 161.28H658.124L613.068 101.376L654.54 47.1035Z" fill="url(#paint0_linear)"></path><path d="M654.54 47.1035H611.788L592.332 74.2395L573.388 47.1035H527.564L568.78 103.168L523.98 161.28H566.732L589.516 130.304L612.3 161.28H658.124L613.068 101.376L654.54 47.1035Z" fill="currentColor"></path><path d="M229.43 120.576C225.59 129.536 218.422 133.376 207.158 133.376C194.614 133.376 184.374 126.72 183.35 112.64H263.478V101.12C263.478 70.1437 243.254 44.0317 205.11 44.0317C169.526 44.0317 142.902 69.8877 142.902 105.984C142.902 142.336 169.014 164.352 205.622 164.352C235.83 164.352 256.822 149.76 262.71 123.648L229.43 120.576ZM183.862 92.6717C185.398 81.9197 191.286 73.7277 204.598 73.7277C216.886 73.7277 223.542 82.4317 224.054 92.6717H183.862Z" fill="url(#paint1_linear)"></path><path d="M229.43 120.576C225.59 129.536 218.422 133.376 207.158 133.376C194.614 133.376 184.374 126.72 183.35 112.64H263.478V101.12C263.478 70.1437 243.254 44.0317 205.11 44.0317C169.526 44.0317 142.902 69.8877 142.902 105.984C142.902 142.336 169.014 164.352 205.622 164.352C235.83 164.352 256.822 149.76 262.71 123.648L229.43 120.576ZM183.862 92.6717C185.398 81.9197 191.286 73.7277 204.598 73.7277C216.886 73.7277 223.542 82.4317 224.054 92.6717H183.862Z" fill="currentColor"></path><path d="M385.256 66.5597C380.392 53.2477 369.896 44.0317 349.672 44.0317C332.52 44.0317 320.232 51.7117 314.088 64.2557V47.1037H272.616V161.28H314.088V105.216C314.088 88.0638 318.952 76.7997 332.52 76.7997C345.064 76.7997 348.136 84.9917 348.136 100.608V161.28H389.608V105.216C389.608 88.0638 394.216 76.7997 408.04 76.7997C420.584 76.7997 423.4 84.9917 423.4 100.608V161.28H464.872V89.5997C464.872 65.7917 455.656 44.0317 424.168 44.0317C404.968 44.0317 391.4 53.7597 385.256 66.5597Z" fill="url(#paint2_linear)"></path><path d="M385.256 66.5597C380.392 53.2477 369.896 44.0317 349.672 44.0317C332.52 44.0317 320.232 51.7117 314.088 64.2557V47.1037H272.616V161.28H314.088V105.216C314.088 88.0638 318.952 76.7997 332.52 76.7997C345.064 76.7997 348.136 84.9917 348.136 100.608V161.28H389.608V105.216C389.608 88.0638 394.216 76.7997 408.04 76.7997C420.584 76.7997 423.4 84.9917 423.4 100.608V161.28H464.872V89.5997C464.872 65.7917 455.656 44.0317 424.168 44.0317C404.968 44.0317 391.4 53.7597 385.256 66.5597Z" fill="currentColor"></path><path d="M478.436 47.104V161.28H519.908V47.104H478.436ZM478.18 36.352H520.164V0H478.18V36.352Z" fill="url(#paint3_linear)"></path><path d="M478.436 47.104V161.28H519.908V47.104H478.436ZM478.18 36.352H520.164V0H478.18V36.352Z" fill="currentColor"></path><defs><lineargradient id="paint0_linear" x1="591.052" y1="47.1035" x2="591.052" y2="161.28" gradientUnits="userSpaceOnUse"><stop stop-color="currentColor"></stop><stop offset="1" stop-color="currentColor" stop-opacity="0"></stop></lineargradient><lineargradient id="paint1_linear" x1="203.19" y1="44.0317" x2="203.19" y2="164.352" gradientUnits="userSpaceOnUse"><stop stop-color="currentColor"></stop><stop offset="1" stop-color="currentColor" stop-opacity="0"></stop></lineargradient><lineargradient id="paint2_linear" x1="368.744" y1="44.0317" x2="368.744" y2="161.28" gradientUnits="userSpaceOnUse"><stop stop-color="currentColor"></stop><stop offset="1" stop-color="currentColor" stop-opacity="0"></stop></lineargradient><lineargradient id="paint3_linear" x1="499.172" y1="0" x2="499.172" y2="161.28" gradientUnits="userSpaceOnUse"><stop stop-color="currentColor"></stop><stop offset="1" stop-color="currentColor" stop-opacity="0"></stop></lineargradient></defs></svg></a><div class="h-8"></div><button type="button" class="DocSearch DocSearch-Button" aria-label="Search"><span class="DocSearch-Button-Container"><svg width="20" height="20" class="DocSearch-Search-Icon" viewBox="0 0 20 20"><path d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z" stroke="currentColor" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"></path></svg><span class="DocSearch-Button-Placeholder">Search</span></span><span class="DocSearch-Button-Keys"><kbd class="DocSearch-Button-Key"><svg width="15" height="15" class="DocSearch-Control-Key-Icon"><path d="M4.505 4.496h2M5.505 5.496v5M8.216 4.496l.055 5.993M10 7.5c.333.333.5.667.5 1v2M12.326 4.5v5.996M8.384 4.496c1.674 0 2.116 0 2.116 1.5s-.442 1.5-2.116 1.5M3.205 9.303c-.09.448-.277 1.21-1.241 1.203C1 10.5.5 9.513.5 8V7c0-1.57.5-2.5 1.464-2.494.964.006 1.134.598 1.24 1.342M12.553 10.5h1.953" stroke-width="1.2" stroke="currentColor" fill="none" stroke-linecap="square"></path></svg></kbd><kbd class="DocSearch-Button-Key">K</kbd></span></button><div class="h-8"></div><nav class="md-nav"><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="1"><li data-dir="" data-level="1"><span class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-1 md-nav-heading pt-0">Tutorials</span><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="2"><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2 md-nav-item--active" href="https://remix.run/docs/en/v1/tutorials/blog" aria-current="page">Blog Tutorial (short)</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/tutorials/jokes">App Tutorial (long)</a></li></ul></li><li data-dir="" data-level="1"><span class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-1 md-nav-heading">Pages</span><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="2"><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/pages/philosophy">Philosophy</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/pages/technical-explanation">Technical Explanation</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/pages/stacks">Remix Stacks</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/pages/contributing">Contributing</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/pages/community">Community</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/pages/faq">FAQs</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/pages/gotchas">Gotchas</a></li></ul></li><li data-dir="" data-level="1"><span class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-1 md-nav-heading">File Conventions</span><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="2"><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/file-conventions/entry.client">entry.client</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/file-conventions/entry.server">entry.server</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/file-conventions/remix-config">remix.config.js</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/file-conventions/root">root</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/file-conventions/routes-files">Route File Naming</a></li></ul></li><li data-dir="" data-level="1"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-1 md-nav-heading" href="https://remix.run/docs/en/v1/route">Route Module API</a><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="2"><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/action">action</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/catch-boundary">CatchBoundary</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/component">Component</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/error-boundary">ErrorBoundary</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/handle">handle</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/headers">headers</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/links">links</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/loader">loader</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/meta">meta</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/route/should-reload">unstable_shouldReload</a></li></ul></li><li data-dir="" data-level="1"><span class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-1 md-nav-heading">Components</span><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="2"><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/form">Form</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/link">Link</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/links">Links</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/live-reload">LiveReload</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/meta">Meta</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/nav-link">NavLink</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/outlet">Outlet</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/prefetch-page-links">PrefetchPageLinks</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/scripts">Scripts</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/components/scroll-restoration">ScrollRestoration</a></li></ul></li><li data-dir="" data-level="1"><span class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-1 md-nav-heading">Hooks</span><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="2"><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/hooks/use-action-data">useActionData</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/hooks/use-before-unload">useBeforeUnload</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/hooks/use-fetcher">useFetcher</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/hooks/use-fetchers">useFetchers</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/hooks/use-form-action">useFormAction</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/hooks/use-loader-data">useLoaderData</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/hooks/use-matches">useMatches</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/hooks/use-submit">useSubmit</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/hooks/use-transition">useTransition</a></li></ul></li><li data-dir="" data-level="1"><span class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-1 md-nav-heading">Utilities</span><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="2"><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/utils/cookies">Cookies</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/utils/json">json</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/utils/redirect">redirect</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/utils/sessions">Sessions</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/utils/unstable-create-file-upload-handler">unstable_createFileUploadHandler</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/utils/unstable-create-memory-upload-handler">unstable_createMemoryUploadHandler</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/utils/parse-multipart-form-data">unstable_parseMultipartFormData</a></li></ul></li><li data-dir="" data-level="1"><span class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-1 md-nav-heading">Other API</span><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="2"><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/other-api/dev">@remix-run/dev (CLI)</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/other-api/adapter">@remix-run/{adapter}</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/other-api/serve">@remix-run/serve</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/other-api/node">@remix-run/node</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/other-api/asset-imports">Asset Imports</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/other-api/react-router">React Router v6</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/other-api/fetch">Web Fetch API</a></li></ul></li><li data-dir="" data-level="1"><span class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-1 md-nav-heading">Guides</span><ul class="md-nav-list text-[color:var(--hue-0750)] mb-3" data-level="2"><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/accessibility">Accessibility</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/api-routes">API Routes</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/bff">Backend For Frontend</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/browser-support">Browser Support</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/data-loading">Data Loading</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/data-writes">Data Writes</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/deployment">Deployment</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/disabling-javascript">Disabling JavaScript</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/envvars">Environment Variables</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/errors">Error Handling</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/file-uploads">File Uploads</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/mdx">MDX</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/migrating-react-router-app">Migrating from React Router</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/constraints">Module Constraints</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/not-found">Not Found Handling</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/optimistic-ui">Optimistic UI</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/performance">Performance</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/resource-routes">Resource Routes</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/routing">Routing</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/styling">Styling</a></li><li data-dir="" data-level="2"><a class="md-nav-item py-1 block text-m-p-sm lg:text-sm md-nav-item--level-2" href="https://remix.run/docs/en/v1/guides/typescript">TypeScript</a></li></ul></li></ul></nav></div></div><div class="lg:z-[1] grow lg:h-full"><div class="py-6 md:py-8 lg:py-10 lg:pr-6 lg:pl-3 xl:pl-5 2xl:pl-6"><div><div class="markdown has-toc"><div class="md-toc-container"><div class="md-toc-nav" role="navigation"><ul class="toc">
<li>
<p><a href="https://remix.run/docs/en/v1/tutorials/blog#quickstart">Quickstart</a></p>
<ul>
<li>
<p><a href="https://remix.run/docs/en/v1/tutorials/blog#prerequisites">Prerequisites</a></p>
</li>
<li>
<p><a href="https://remix.run/docs/en/v1/tutorials/blog#creating-the-project">Creating the project</a></p>
</li>
<li>
<p><a href="https://remix.run/docs/en/v1/tutorials/blog#your-first-route">Your First Route</a></p>
</li>
<li>
<p><a href="https://remix.run/docs/en/v1/tutorials/blog#loading-data">Loading Data</a></p>
</li>
<li>
<p><a href="https://remix.run/docs/en/v1/tutorials/blog#a-little-refactoring">A little refactoring</a></p>
</li>
<li>
<p><a href="https://remix.run/docs/en/v1/tutorials/blog#pulling-from-a-data-source">Pulling from a data source</a></p>
</li>
<li>
<p><a href="https://remix.run/docs/en/v1/tutorials/blog#dynamic-route-params">Dynamic Route Params</a></p>
</li>
<li>
<p><a href="https://remix.run/docs/en/v1/tutorials/blog#nested-routing">Nested Routing</a></p>
<ul>
<li><a href="https://remix.run/docs/en/v1/tutorials/blog#index-routes">Index Routes</a></li>
</ul>
</li>
<li>
<p><a href="https://remix.run/docs/en/v1/tutorials/blog#actions">Actions</a></p>
</li>
<li>
<p><a href="https://remix.run/docs/en/v1/tutorials/blog#progressive-enhancement">Progressive Enhancement</a></p>
</li>
<li>
<p><a href="https://remix.run/docs/en/v1/tutorials/blog#homework">Homework</a></p>
</li>
</ul>
</li>
</ul></div><div class="md-prose"><h1 id="quickstart"><a href="https://remix.run/docs/en/v1/tutorials/blog#quickstart" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Quickstart</h1><p>We're going to be short on words and quick on code in this quickstart. If you're looking to see what Remix is all about in 15 minutes, this is it.</p><p><docs-info>Work through this tutorial with Kent in <a target="_blank" rel="noopener noreferrer" href="https://rmx.as/egghead-course">this free Egghead.io course</a></docs-info></p><p>This tutorial uses TypeScript. Remix can definitely be used without TypeScript. We feel most productive when writing TypeScript, but if you'd prefer to skip the TypeScript syntax, feel free to write your code in JavaScript.</p><p><docs-info>💿 Hey I'm Derrick the Remix Compact Disc 👋 Whenever you're supposed to <em>do</em> something you'll see me</docs-info></p><h2 id="prerequisites"><a href="https://remix.run/docs/en/v1/tutorials/blog#prerequisites" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Prerequisites</h2><p>Click this button to create a <a href="https://gitpod.io/">Gitpod</a> workspace with the project set up and ready to run in VS Code or JetBrains either directly in the browser or on the desktop.</p><p><a href="https://gitpod.io/#https://github.com/remix-run/indie-stack"><img src="./Remix _ Blog Tutorial (short)_files/Gitpod-Ready--to--Code-blue" alt="Gitpod Ready-to-Code"></a></p><p>If you want to follow this tutorial locally on your own computer, it is important for you to have these things installed:</p><ul>
<li><a href="https://nodejs.org/">Node.js</a> version (^14.17.0, or >=16.0.0)</li>
<li><a href="https://www.npmjs.com/">npm</a> 7 or greater</li>
<li>A code editor (<a href="https://code.visualstudio.com/">VSCode</a> is a nice one)</li>
</ul><h2 id="creating-the-project"><a href="https://remix.run/docs/en/v1/tutorials/blog#creating-the-project" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Creating the project</h2><p><docs-warning>Make sure you are running at least Node v14 or greater</docs-warning></p><p>💿 Initialize a new Remix project. We'll call ours "blog-tutorial" but you can call it something else if you'd like.</p><pre><code class="language-sh">npx create-remix@latest --template remix-run/indie-stack blog-tutorial
</code></pre><pre><code>? Do you want me to run `npm install`? Yes
</code></pre><p>You can read more about the stacks available in <a href="https://remix.run/pages/stacks">the stacks docs</a>.</p><p>We're using <a href="https://github.com/remix-run/indie-stack">the Indie stack</a>, which is a full application ready to deploy to <a href="https://fly.io/">fly.io</a>. This includes development tools as well as production-ready authentication and persistence. Don't worry if you're unfamiliar with the tools used, we'll walk you through things as we go.</p><p><docs-info>Note, you can definitely start with "Just the basics" instead by running <code>npx create-remix@latest</code> without the <code>--template</code> flag. The generated project is much more minimal that way. However, some bits of the tutorial will be different for you and you'll have to configure things for deployment manually.</docs-info></p><p>💿 Now, open the project that was generated in your preferred editor and check the instructions in the <code>README.md</code> file. Feel free to read over this. We'll get to the deployment bit later in the tutorial.</p><p>💿 Let's start the dev server:</p><pre><code class="language-sh">npm run dev
</code></pre><p>💿 Open up <a href="http://localhost:3000/">http://localhost:3000</a>, the app should be running.</p><p>If you want, take a minute and poke around the UI a bit. Feel free to create an account and create/delete some notes to get an idea of what's available in the UI out of the box.</p><h2 id="your-first-route"><a href="https://remix.run/docs/en/v1/tutorials/blog#your-first-route" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Your First Route</h2><p>We're going to make a new route to render at the "/posts" URL. Before we do that, let's link to it.</p><p>💿 Add a link to posts in <code>app/routes/index.tsx</code></p><p>Go ahead and copy/paste this:</p><div><pre data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><<span style="color: var(--base08)">div</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">mx-auto mt-16 max-w-7xl text-center</span>">
</span><span class="codeblock-line" data-line-number="2"> <<span style="color: var(--base0A)">Link</span>
</span><span class="codeblock-line" data-line-number="3"> <span style="color: var(--base0D)">to</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">/posts</span>"
</span><span class="codeblock-line" data-line-number="4"> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">text-xl text-blue-600 underline</span>"
</span><span class="codeblock-line" data-line-number="5"> >
</span><span class="codeblock-line" data-line-number="6"> Blog Posts
</span><span class="codeblock-line" data-line-number="7"> </<span style="color: var(--base0A)">Link</span>>
</span><span class="codeblock-line" data-line-number="8"></<span style="color: var(--base08)">div</span>>
</span></code></pre></div><p>You can put it anywhere you like. I stuck it right above the icons of all the technologies used in the stack:</p><!-- TODO: once the website can deploy properly, update this to use our self-hosted version of this image --><!--  --><p><img src="./Remix _ Blog Tutorial (short)_files/160208939-34fe20ed-3146-4f4b-a68a-d82284339c47.png" alt="Screenshot of the app showing the blog post link"></p><p><docs-info>You may have noticed we're using <a href="https://tailwindcss.com/">tailwind</a> classes.</docs-info></p><p>The Remix Indie stack has <a href="https://tailwindcss.com/">tailwind</a> support pre-configured. If you'd prefer to not use tailwind, you're welcome to remove it and use something else. Learn more about your styling options with Remix in <a href="https://remix.run/docs/en/v1/guides/styling">the styling guide</a>.</p><p>Back in the browser go ahead and click the link. You should see a 404 page since we've not created this route yet. Let's create the route now:</p><p>💿 Create a new file in <code>app/routes/posts/index.tsx</code></p><pre><code class="language-sh">mkdir app/routes/posts
touch app/routes/posts/index.tsx
</code></pre><p><docs-info>Any time you see terminal commands to create files or folders, you can of course do that however you'd like, but using <code>mkdir</code> and <code>touch</code> is just a way for us to make it clear which files you should be creating.</docs-info></p><p>We could have named it just <code>posts.tsx</code> but we'll have another route soon and it'll be nice to put them by each other. An index route will render at the folder's path (just like index.html on a web server).</p><p>Now if you navigate to the <code>/posts</code> route, you'll get an error indicating there's no way to handle the request. That's because we haven't done anything in that route yet! Let's add a component and export it as the default:</p><p>💿 Make the posts component</p><div><pre data-filename="app/routes/posts/index.tsx" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">default</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">Posts</span>() {
</span><span class="codeblock-line" data-line-number="2"> <span style="color: var(--base0E)">return</span> (
</span><span class="codeblock-line" data-line-number="3"> <<span style="color: var(--base08)">main</span>>
</span><span class="codeblock-line" data-line-number="4"> <<span style="color: var(--base08)">h1</span>>Posts</<span style="color: var(--base08)">h1</span>>
</span><span class="codeblock-line" data-line-number="5"> </<span style="color: var(--base08)">main</span>>
</span><span class="codeblock-line" data-line-number="6"> );
</span><span class="codeblock-line" data-line-number="7">}
</span></code></pre></div><p>You might need to refresh the browser to see our new, bare-bones posts route.</p><h2 id="loading-data"><a href="https://remix.run/docs/en/v1/tutorials/blog#loading-data" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Loading Data</h2><p>Data loading is built into Remix.</p><p>If your web dev background is primarily in the last few years, you're probably used to creating two things here: an API route to provide data and a frontend component that consumes it. In Remix your frontend component is also its own API route and it already knows how to talk to itself on the server from the browser. That is, you don't have to fetch it.</p><p>If your background is a bit farther back than that with MVC web frameworks like Rails, then you can think of your Remix routes as backend views using React for templating, but then they know how to seamlessly hydrate in the browser to add some flair instead of writing detached jQuery code to dress up the user interactions. It's progressive enhancement realized in its fullest. Additionally, your routes are their own controller.</p><p>So let's get to it and provide some data to our component.</p><p>💿 Make the posts route "loader"</p><div><pre data-filename="app/routes/posts/index.tsx" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-highlight="true" data-line-number="1"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">json</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-highlight="true" data-line-number="2"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">useLoaderData</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/react</span>";
</span><span class="codeblock-line" data-line-number="3">
</span><span class="codeblock-line" data-highlight="true" data-line-number="4"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0A)">const</span> <span style="color: var(--base0D)">loader</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0E)">async</span> () <span style="color: var(--base0D)">=></span> {
</span><span class="codeblock-line" data-highlight="true" data-line-number="5"> <span style="color: var(--base0E)">return</span> <span style="color: var(--base0D)">json</span>({
</span><span class="codeblock-line" data-highlight="true" data-line-number="6"> posts: [
</span><span class="codeblock-line" data-highlight="true" data-line-number="7"> {
</span><span class="codeblock-line" data-highlight="true" data-line-number="8"> slug: "<span style="color: var(--base0B)">my-first-post</span>",
</span><span class="codeblock-line" data-highlight="true" data-line-number="9"> title: "<span style="color: var(--base0B)">My First Post</span>",
</span><span class="codeblock-line" data-highlight="true" data-line-number="10"> },
</span><span class="codeblock-line" data-highlight="true" data-line-number="11"> {
</span><span class="codeblock-line" data-highlight="true" data-line-number="12"> slug: "<span style="color: var(--base0B)">90s-mixtape</span>",
</span><span class="codeblock-line" data-highlight="true" data-line-number="13"> title: "<span style="color: var(--base0B)">A Mixtape I Made Just For You</span>",
</span><span class="codeblock-line" data-highlight="true" data-line-number="14"> },
</span><span class="codeblock-line" data-highlight="true" data-line-number="15"> ],
</span><span class="codeblock-line" data-highlight="true" data-line-number="16"> });
</span><span class="codeblock-line" data-highlight="true" data-line-number="17">};
</span><span class="codeblock-line" data-line-number="18">
</span><span class="codeblock-line" data-line-number="19"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">default</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">Posts</span>() {
</span><span class="codeblock-line" data-highlight="true" data-line-number="20"> <span style="color: var(--base0A)">const</span> { <span style="color: var(--base08)">posts</span> } <span style="color: var(--base0E)">=</span> <span style="color: var(--base0D)">useLoaderData</span><<span style="color: var(--base0E)">typeof</span> <span style="color: var(--base08)">loader</span>>();
</span><span class="codeblock-line" data-highlight="true" data-line-number="21"> <span style="color: var(--base08)">console</span>.<span style="color: var(--base0D)">log</span>(<span style="color: var(--base08)">posts</span>);
</span><span class="codeblock-line" data-line-number="22"> <span style="color: var(--base0E)">return</span> (
</span><span class="codeblock-line" data-line-number="23"> <<span style="color: var(--base08)">main</span>>
</span><span class="codeblock-line" data-line-number="24"> <<span style="color: var(--base08)">h1</span>>Posts</<span style="color: var(--base08)">h1</span>>
</span><span class="codeblock-line" data-line-number="25"> </<span style="color: var(--base08)">main</span>>
</span><span class="codeblock-line" data-line-number="26"> );
</span><span class="codeblock-line" data-line-number="27">}
</span></code></pre></div><p>Loaders are the backend "API" for their component and it's already wired up for you through <code>useLoaderData</code>. It's a little wild how blurry the line is between the client and the server in a Remix route. If you have your server and browser consoles both open, you'll note that they both logged our post data. That's because Remix rendered on the server to send a full HTML document like a traditional web framework, but it also hydrated in the client and logged there too.</p><p>💿 Render links to our posts</p><div><pre data-filename="app/routes/posts/index.tsx" data-nocopy="" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">json</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-highlight="true" data-line-number="2"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">Link</span>, <span style="color: var(--base08)">useLoaderData</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/react</span>";
</span><span class="codeblock-line" data-line-number="3">
</span><span class="codeblock-line" data-line-number="4"><span style="color: var(--base03)">// ...</span>
</span><span class="codeblock-line" data-line-number="5"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">default</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">Posts</span>() {
</span><span class="codeblock-line" data-line-number="6"> <span style="color: var(--base0A)">const</span> { <span style="color: var(--base08)">posts</span> } <span style="color: var(--base0E)">=</span> <span style="color: var(--base0D)">useLoaderData</span><<span style="color: var(--base0E)">typeof</span> <span style="color: var(--base08)">loader</span>>();
</span><span class="codeblock-line" data-line-number="7"> <span style="color: var(--base0E)">return</span> (
</span><span class="codeblock-line" data-line-number="8"> <<span style="color: var(--base08)">main</span>>
</span><span class="codeblock-line" data-line-number="9"> <<span style="color: var(--base08)">h1</span>>Posts</<span style="color: var(--base08)">h1</span>>
</span><span class="codeblock-line" data-highlight="true" data-line-number="10"> <<span style="color: var(--base08)">ul</span>>
</span><span class="codeblock-line" data-highlight="true" data-line-number="11"> <span style="color: var(--base0F)">{</span><span style="color: var(--base08)">posts</span>.<span style="color: var(--base0D)">map</span>((<span style="color: var(--base08)">post</span>) <span style="color: var(--base0D)">=></span> (
</span><span class="codeblock-line" data-highlight="true" data-line-number="12"> <<span style="color: var(--base08)">li</span> <span style="color: var(--base0D)">key</span><span style="color: var(--base0E)">=</span><span style="color: var(--base0F)">{</span><span style="color: var(--base08)">post</span>.<span style="color: var(--base08)">slug</span><span style="color: var(--base0F)">}</span>>
</span><span class="codeblock-line" data-highlight="true" data-line-number="13"> <<span style="color: var(--base0A)">Link</span>
</span><span class="codeblock-line" data-highlight="true" data-line-number="14"> <span style="color: var(--base0D)">to</span><span style="color: var(--base0E)">=</span><span style="color: var(--base0F)">{</span><span style="color: var(--base08)">post</span>.<span style="color: var(--base08)">slug</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-highlight="true" data-line-number="15"> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">text-blue-600 underline</span>"
</span><span class="codeblock-line" data-highlight="true" data-line-number="16"> >
</span><span class="codeblock-line" data-highlight="true" data-line-number="17"> <span style="color: var(--base0F)">{</span><span style="color: var(--base08)">post</span>.<span style="color: var(--base08)">title</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-highlight="true" data-line-number="18"> </<span style="color: var(--base0A)">Link</span>>
</span><span class="codeblock-line" data-highlight="true" data-line-number="19"> </<span style="color: var(--base08)">li</span>>
</span><span class="codeblock-line" data-highlight="true" data-line-number="20"> ))<span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-highlight="true" data-line-number="21"> </<span style="color: var(--base08)">ul</span>>
</span><span class="codeblock-line" data-line-number="22"> </<span style="color: var(--base08)">main</span>>
</span><span class="codeblock-line" data-line-number="23"> );
</span><span class="codeblock-line" data-line-number="24">}
</span></code></pre></div><p>Hey, that's pretty cool. We get a pretty solid degree of type safety even over a network request because it's all defined in the same file. Unless the network blows up while Remix fetches the data, you've got type safety in this component and its API (remember, the component is already its own API route).</p><h2 id="a-little-refactoring"><a href="https://remix.run/docs/en/v1/tutorials/blog#a-little-refactoring" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>A little refactoring</h2><p>A solid practice is to create a module that deals with a particular concern. In our case it's going to be reading and writing posts. Let's set that up now and add a <code>getPosts</code> export to our module.</p><p>💿 Create <code>app/models/post.server.ts</code></p><pre><code class="language-sh">touch app/models/post.server.ts
</code></pre><p>We're mostly gonna copy/paste stuff from our route:</p><div><pre data-filename="app/models/post.server.ts" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0A)">type</span> <span style="color: var(--base0A)">Post</span> <span style="color: var(--base0E)">=</span> {
</span><span class="codeblock-line" data-line-number="2"> <span style="color: var(--base08)">slug</span><span style="color: var(--base0E)">:</span> <span style="color: var(--base0C)">string</span>;
</span><span class="codeblock-line" data-line-number="3"> <span style="color: var(--base08)">title</span><span style="color: var(--base0E)">:</span> <span style="color: var(--base0C)">string</span>;
</span><span class="codeblock-line" data-line-number="4">};
</span><span class="codeblock-line" data-line-number="5">
</span><span class="codeblock-line" data-line-number="6"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">async</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">getPosts</span>()<span style="color: var(--base0E)">:</span> <span style="color: var(--base0A)">Promise</span><<span style="color: var(--base0A)">Array</span><<span style="color: var(--base0A)">Post</span>>> {
</span><span class="codeblock-line" data-line-number="7"> <span style="color: var(--base0E)">return</span> [
</span><span class="codeblock-line" data-line-number="8"> {
</span><span class="codeblock-line" data-line-number="9"> slug: "<span style="color: var(--base0B)">my-first-post</span>",
</span><span class="codeblock-line" data-line-number="10"> title: "<span style="color: var(--base0B)">My First Post</span>",
</span><span class="codeblock-line" data-line-number="11"> },
</span><span class="codeblock-line" data-line-number="12"> {
</span><span class="codeblock-line" data-line-number="13"> slug: "<span style="color: var(--base0B)">90s-mixtape</span>",
</span><span class="codeblock-line" data-line-number="14"> title: "<span style="color: var(--base0B)">A Mixtape I Made Just For You</span>",
</span><span class="codeblock-line" data-line-number="15"> },
</span><span class="codeblock-line" data-line-number="16"> ];
</span><span class="codeblock-line" data-line-number="17">}
</span></code></pre></div><p>Note that we're making the <code>getPosts</code> function <code>async</code> because even though it's not currently doing anything async it will soon!</p><p>💿 Update the posts route to use our new posts module:</p><div><pre data-filename="app/routes/posts/index.tsx" data-nocopy="" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">json</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-line-number="2"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">Link</span>, <span style="color: var(--base08)">useLoaderData</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/react</span>";
</span><span class="codeblock-line" data-line-number="3">
</span><span class="codeblock-line" data-line-number="4"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">getPosts</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">~/models/post.server</span>";
</span><span class="codeblock-line" data-line-number="5">
</span><span class="codeblock-line" data-line-number="6"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0A)">const</span> <span style="color: var(--base0D)">loader</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0E)">async</span> () <span style="color: var(--base0D)">=></span> {
</span><span class="codeblock-line" data-line-number="7"> <span style="color: var(--base0E)">return</span> <span style="color: var(--base0D)">json</span>({ posts: <span style="color: var(--base0E)">await</span> <span style="color: var(--base0D)">getPosts</span>() });
</span><span class="codeblock-line" data-line-number="8">};
</span><span class="codeblock-line" data-line-number="9">
</span><span class="codeblock-line" data-line-number="10"><span style="color: var(--base03)">// ...</span>
</span></code></pre></div><h2 id="pulling-from-a-data-source"><a href="https://remix.run/docs/en/v1/tutorials/blog#pulling-from-a-data-source" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Pulling from a data source</h2><p>With the Indie Stack, we've got a SQLite database already set up and configured for us, so let's update our Database Schema to handle SQLite. We're using <a href="https://prisma.io/">Prisma</a> to interact with the database, so we'll update that schema and Prisma will take care of updating our database to match the schema for us (as well as generating and running the necessary SQL commands for the migration).</p><p><docs-info>You do not have to use Prisma when using Remix. Remix works great with whatever existing database or data persistence services you're currently using.</docs-info></p><p>If you've never used Prisma before, don't worry, we'll walk you through it.</p><p>💿 First, we need to update our Prisma schema:</p><div><pre data-filename="prisma/schema.prisma" data-nocopy="" data-line-numbers="true" data-lang="prisma" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base03)">// Stick this at the bottom of that file:</span>
</span><span class="codeblock-line" data-line-number="2">
</span><span class="codeblock-line" data-line-number="3"><span style="color: var(--base0A)">model</span> <span style="color: var(--base0A)">Post</span> {
</span><span class="codeblock-line" data-line-number="4"> <span style="color: var(--base08)">slug</span> <span style="color: var(--base0C)">String</span> <span style="color: var(--base0D)">@id</span>
</span><span class="codeblock-line" data-line-number="5"> <span style="color: var(--base08)">title</span> <span style="color: var(--base0C)">String</span>
</span><span class="codeblock-line" data-line-number="6"> <span style="color: var(--base08)">markdown</span> <span style="color: var(--base0C)">String</span>
</span><span class="codeblock-line" data-line-number="7">
</span><span class="codeblock-line" data-line-number="8"> <span style="color: var(--base08)">createdAt</span> <span style="color: var(--base0C)">DateTime</span> <span style="color: var(--base0D)">@default</span>(<span style="color: var(--base0D)">now</span>())
</span><span class="codeblock-line" data-line-number="9"> <span style="color: var(--base08)">updatedAt</span> <span style="color: var(--base0C)">DateTime</span> <span style="color: var(--base0D)">@updatedAt</span>
</span><span class="codeblock-line" data-line-number="10">}
</span></code></pre></div><p>💿 Now let's tell Prisma to update our local database and TypeScript definitions to match this schema change:</p><pre><code class="language-sh">npx prisma db push
</code></pre><p>💿 Let's seed our database with a couple posts. Open <code>prisma/seed.ts</code> and add this to the end of the seed functionality (right before the <code>console.log</code>):</p><div><pre data-filename="prisma/seed.ts" data-line-numbers="true" data-lang="ts" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">const</span> <span style="color: var(--base08)">posts</span> <span style="color: var(--base0E)">=</span> [
</span><span class="codeblock-line" data-line-number="2"> {
</span><span class="codeblock-line" data-line-number="3"> slug: "<span style="color: var(--base0B)">my-first-post</span>",
</span><span class="codeblock-line" data-line-number="4"> title: "<span style="color: var(--base0B)">My First Post</span>",
</span><span class="codeblock-line" data-line-number="5"> markdown: `
</span><span class="codeblock-line" data-line-number="6"><span style="color: var(--base0B)"># This is my first post</span>
</span><span class="codeblock-line" data-line-number="7">
</span><span class="codeblock-line" data-line-number="8"><span style="color: var(--base0B)">Isn't it great?</span>
</span><span class="codeblock-line" data-line-number="9"><span style="color: var(--base0B)"> </span>`.<span style="color: var(--base0D)">trim</span>(),
</span><span class="codeblock-line" data-line-number="10"> },
</span><span class="codeblock-line" data-line-number="11"> {
</span><span class="codeblock-line" data-line-number="12"> slug: "<span style="color: var(--base0B)">90s-mixtape</span>",
</span><span class="codeblock-line" data-line-number="13"> title: "<span style="color: var(--base0B)">A Mixtape I Made Just For You</span>",
</span><span class="codeblock-line" data-line-number="14"> markdown: `
</span><span class="codeblock-line" data-line-number="15"><span style="color: var(--base0B)"># 90s Mixtape</span>
</span><span class="codeblock-line" data-line-number="16">
</span><span class="codeblock-line" data-line-number="17"><span style="color: var(--base0B)">- I wish (Skee-Lo)</span>
</span><span class="codeblock-line" data-line-number="18"><span style="color: var(--base0B)">- This Is How We Do It (Montell Jordan)</span>
</span><span class="codeblock-line" data-line-number="19"><span style="color: var(--base0B)">- Everlong (Foo Fighters)</span>
</span><span class="codeblock-line" data-line-number="20"><span style="color: var(--base0B)">- Ms. Jackson (Outkast)</span>
</span><span class="codeblock-line" data-line-number="21"><span style="color: var(--base0B)">- Interstate Love Song (Stone Temple Pilots)</span>
</span><span class="codeblock-line" data-line-number="22"><span style="color: var(--base0B)">- Killing Me Softly With His Song (Fugees, Ms. Lauryn Hill)</span>
</span><span class="codeblock-line" data-line-number="23"><span style="color: var(--base0B)">- Just a Friend (Biz Markie)</span>
</span><span class="codeblock-line" data-line-number="24"><span style="color: var(--base0B)">- The Man Who Sold The World (Nirvana)</span>
</span><span class="codeblock-line" data-line-number="25"><span style="color: var(--base0B)">- Semi-Charmed Life (Third Eye Blind)</span>
</span><span class="codeblock-line" data-line-number="26"><span style="color: var(--base0B)">- ...Baby One More Time (Britney Spears)</span>
</span><span class="codeblock-line" data-line-number="27"><span style="color: var(--base0B)">- Better Man (Pearl Jam)</span>
</span><span class="codeblock-line" data-line-number="28"><span style="color: var(--base0B)">- It's All Coming Back to Me Now (Céline Dion)</span>
</span><span class="codeblock-line" data-line-number="29"><span style="color: var(--base0B)">- This Kiss (Faith Hill)</span>
</span><span class="codeblock-line" data-line-number="30"><span style="color: var(--base0B)">- Fly Away (Lenny Kravits)</span>
</span><span class="codeblock-line" data-line-number="31"><span style="color: var(--base0B)">- Scar Tissue (Red Hot Chili Peppers)</span>
</span><span class="codeblock-line" data-line-number="32"><span style="color: var(--base0B)">- Santa Monica (Everclear)</span>
</span><span class="codeblock-line" data-line-number="33"><span style="color: var(--base0B)">- C'mon N' Ride it (Quad City DJ's)</span>
</span><span class="codeblock-line" data-line-number="34"><span style="color: var(--base0B)"> </span>`.<span style="color: var(--base0D)">trim</span>(),
</span><span class="codeblock-line" data-line-number="35"> },
</span><span class="codeblock-line" data-line-number="36">];
</span><span class="codeblock-line" data-line-number="37">
</span><span class="codeblock-line" data-line-number="38"><span style="color: var(--base0E)">for</span> (<span style="color: var(--base0E)">const</span> <span style="color: var(--base08)">post</span> <span style="color: var(--base0E)">of</span> <span style="color: var(--base08)">posts</span>) {
</span><span class="codeblock-line" data-line-number="39"> <span style="color: var(--base0E)">await</span> <span style="color: var(--base08)">prisma</span>.<span style="color: var(--base0D)">post</span>.<span style="color: var(--base0D)">upsert</span>({
</span><span class="codeblock-line" data-line-number="40"> where: { slug: <span style="color: var(--base08)">post</span>.<span style="color: var(--base08)">slug</span> },
</span><span class="codeblock-line" data-line-number="41"> update: <span style="color: var(--base08)">post</span>,
</span><span class="codeblock-line" data-line-number="42"> create: <span style="color: var(--base08)">post</span>,
</span><span class="codeblock-line" data-line-number="43"> });
</span><span class="codeblock-line" data-line-number="44">}
</span></code></pre></div><p><docs-info>Note that we're using <code>upsert</code> so you can run the seed script over and over without adding multiple versions of the same post every time.</docs-info></p><p>Great, let's get those posts into the database with the seed script:</p><pre><code>npx prisma db seed
</code></pre><p>💿 Let's generate a migration file for our schema changes - which will be required if you deploy your application rather than just running in dev mode locally.</p><pre><code class="language-sh">npx prisma migrate dev
</code></pre><p><docs-warning>You'll get the ability to name the migration name, ideally you can refer back to what the changes you made are, so I'd suggest <code>create-post-model</code> for the name.</docs-warning></p><p>💿 Now update the <code>app/models/post.server.ts</code> file to read from the SQLite database:</p><div><pre data-filename="app/models/post.server.ts" data-line-numbers="true" data-lang="ts" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">prisma</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">~/db.server</span>";
</span><span class="codeblock-line" data-line-number="2">
</span><span class="codeblock-line" data-line-number="3"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">async</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">getPosts</span>() {
</span><span class="codeblock-line" data-line-number="4"> <span style="color: var(--base0E)">return</span> <span style="color: var(--base08)">prisma</span>.<span style="color: var(--base0D)">post</span>.<span style="color: var(--base0D)">findMany</span>();
</span><span class="codeblock-line" data-line-number="5">}
</span></code></pre></div><p><docs-success>Notice we're able to remove the return type, but everything is still fully typed. The TypeScript feature of Prisma is one of its greatest strengths. Less manual typing, but still type safe!</docs-success></p><p><docs-info>The <code>~/db.server</code> import is importing the file at <code>app/db.server.ts</code>. The <code>~</code> is a fancy alias to the <code>app</code> directory so you don't have to worry about how many <code>../../</code>s to include in your import as you move files around.</docs-info></p><p>💿 Now that the Prisma client has been updated, we will need to restart our server. So stop the dev server and start it back up again with <code>npm run dev</code>.</p><p><docs-warning>You only need to ever do this when you change the Prisma schema and update the Prisma client. Normally you don't need to restart the dev server during development. Nice that it's so fast though right?</docs-warning></p><p>With the server up and running again, you should be able to go to <code>http://localhost:3000/posts</code> and the posts should still be there, but now they're coming from SQLite!</p><h2 id="dynamic-route-params"><a href="https://remix.run/docs/en/v1/tutorials/blog#dynamic-route-params" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Dynamic Route Params</h2><p>Now let's make a route to actually view the post. We want these URLs to work:</p><pre><code>/posts/my-first-post
/posts/90s-mixtape
</code></pre><p>Instead of creating a route for every single one of our posts, we can use a "dynamic segment" in the url. Remix will parse and pass to us so we can look up the post dynamically.</p><p>💿 Create a dynamic route at "app/routes/posts/$slug.tsx"</p><pre><code class="language-sh">touch app/routes/posts/\$slug.tsx
</code></pre><div><pre data-filename="app/routes/posts/$slug.tsx" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">default</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">PostSlug</span>() {
</span><span class="codeblock-line" data-line-number="2"> <span style="color: var(--base0E)">return</span> (
</span><span class="codeblock-line" data-line-number="3"> <<span style="color: var(--base08)">main</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">mx-auto max-w-4xl</span>">
</span><span class="codeblock-line" data-line-number="4"> <<span style="color: var(--base08)">h1</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">my-6 border-b-2 text-center text-3xl</span>">
</span><span class="codeblock-line" data-line-number="5"> Some Post
</span><span class="codeblock-line" data-line-number="6"> </<span style="color: var(--base08)">h1</span>>
</span><span class="codeblock-line" data-line-number="7"> </<span style="color: var(--base08)">main</span>>
</span><span class="codeblock-line" data-line-number="8"> );
</span><span class="codeblock-line" data-line-number="9">}
</span></code></pre></div><p>You can click one of your posts and should see the new page.</p><p>💿 Add a loader to access the params</p><div><pre data-filename="app/routes/posts/$slug.tsx" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-highlight="true" data-line-number="1"><span style="color: var(--base0E)">import</span> <span style="color: var(--base0E)">type</span> { <span style="color: var(--base08)">LoaderArgs</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-highlight="true" data-line-number="2"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">json</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-highlight="true" data-line-number="3"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">useLoaderData</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/react</span>";
</span><span class="codeblock-line" data-line-number="4">
</span><span class="codeblock-line" data-highlight="true" data-line-number="5"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0A)">const</span> <span style="color: var(--base0D)">loader</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0E)">async</span> ({ <span style="color: var(--base08)">params</span> }<span style="color: var(--base0E)">:</span> <span style="color: var(--base0A)">LoaderArgs</span>) <span style="color: var(--base0D)">=></span> {
</span><span class="codeblock-line" data-highlight="true" data-line-number="6"> <span style="color: var(--base0E)">return</span> <span style="color: var(--base0D)">json</span>({ slug: <span style="color: var(--base08)">params</span>.<span style="color: var(--base08)">slug</span> });
</span><span class="codeblock-line" data-highlight="true" data-line-number="7">};
</span><span class="codeblock-line" data-line-number="8">
</span><span class="codeblock-line" data-line-number="9"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">default</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">PostSlug</span>() {
</span><span class="codeblock-line" data-highlight="true" data-line-number="10"> <span style="color: var(--base0A)">const</span> { <span style="color: var(--base08)">slug</span> } <span style="color: var(--base0E)">=</span> <span style="color: var(--base0D)">useLoaderData</span><<span style="color: var(--base0E)">typeof</span> <span style="color: var(--base08)">loader</span>>();
</span><span class="codeblock-line" data-line-number="11"> <span style="color: var(--base0E)">return</span> (
</span><span class="codeblock-line" data-line-number="12"> <<span style="color: var(--base08)">main</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">mx-auto max-w-4xl</span>">
</span><span class="codeblock-line" data-line-number="13"> <<span style="color: var(--base08)">h1</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">my-6 border-b-2 text-center text-3xl</span>">
</span><span class="codeblock-line" data-highlight="true" data-line-number="14"> Some Post: <span style="color: var(--base0F)">{</span><span style="color: var(--base08)">slug</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="15"> </<span style="color: var(--base08)">h1</span>>
</span><span class="codeblock-line" data-line-number="16"> </<span style="color: var(--base08)">main</span>>
</span><span class="codeblock-line" data-line-number="17"> );
</span><span class="codeblock-line" data-line-number="18">}
</span></code></pre></div><p>The part of the filename attached to the <code>$</code> becomes a named key on the <code>params</code> object that comes into your loader. This is how we'll look up our blog post.</p><p>Now, let's actually get the post contents from the database by its slug.</p><p>💿 Add a <code>getPost</code> function to our post module</p><div><pre data-filename="app/models/post.server.ts" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">prisma</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">~/db.server</span>";
</span><span class="codeblock-line" data-line-number="2">
</span><span class="codeblock-line" data-line-number="3"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">async</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">getPosts</span>() {
</span><span class="codeblock-line" data-line-number="4"> <span style="color: var(--base0E)">return</span> <span style="color: var(--base08)">prisma</span>.<span style="color: var(--base0D)">post</span>.<span style="color: var(--base0D)">findMany</span>();
</span><span class="codeblock-line" data-line-number="5">}
</span><span class="codeblock-line" data-line-number="6">
</span><span class="codeblock-line" data-highlight="true" data-line-number="7"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">async</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">getPost</span>(<span style="color: var(--base08)">slug</span><span style="color: var(--base0E)">:</span> <span style="color: var(--base0C)">string</span>) {
</span><span class="codeblock-line" data-highlight="true" data-line-number="8"> <span style="color: var(--base0E)">return</span> <span style="color: var(--base08)">prisma</span>.<span style="color: var(--base0D)">post</span>.<span style="color: var(--base0D)">findUnique</span>({ where: { <span style="color: var(--base08)">slug</span> } });
</span><span class="codeblock-line" data-highlight="true" data-line-number="9">}
</span></code></pre></div><p>💿 Use the new <code>getPost</code> function in the route</p><div><pre data-filename="app/routes/posts/$slug.tsx" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">import</span> <span style="color: var(--base0E)">type</span> { <span style="color: var(--base08)">LoaderArgs</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-line-number="2"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">json</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-line-number="3"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">useLoaderData</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/react</span>";
</span><span class="codeblock-line" data-line-number="4">
</span><span class="codeblock-line" data-highlight="true" data-line-number="5"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">getPost</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">~/models/post.server</span>";
</span><span class="codeblock-line" data-line-number="6">
</span><span class="codeblock-line" data-line-number="7"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0A)">const</span> <span style="color: var(--base0D)">loader</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0E)">async</span> ({ <span style="color: var(--base08)">params</span> }<span style="color: var(--base0E)">:</span> <span style="color: var(--base0A)">LoaderArgs</span>) <span style="color: var(--base0D)">=></span> {
</span><span class="codeblock-line" data-highlight="true" data-line-number="8"> <span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">post</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0E)">await</span> <span style="color: var(--base0D)">getPost</span>(<span style="color: var(--base08)">params</span>.<span style="color: var(--base08)">slug</span>);
</span><span class="codeblock-line" data-highlight="true" data-line-number="9"> <span style="color: var(--base0E)">return</span> <span style="color: var(--base0D)">json</span>({ <span style="color: var(--base08)">post</span> });
</span><span class="codeblock-line" data-line-number="10">};
</span><span class="codeblock-line" data-line-number="11">
</span><span class="codeblock-line" data-line-number="12"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">default</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">PostSlug</span>() {
</span><span class="codeblock-line" data-highlight="true" data-line-number="13"> <span style="color: var(--base0A)">const</span> { <span style="color: var(--base08)">post</span> } <span style="color: var(--base0E)">=</span> <span style="color: var(--base0D)">useLoaderData</span><<span style="color: var(--base0E)">typeof</span> <span style="color: var(--base08)">loader</span>>();
</span><span class="codeblock-line" data-line-number="14"> <span style="color: var(--base0E)">return</span> (
</span><span class="codeblock-line" data-line-number="15"> <<span style="color: var(--base08)">main</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">mx-auto max-w-4xl</span>">
</span><span class="codeblock-line" data-line-number="16"> <<span style="color: var(--base08)">h1</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">my-6 border-b-2 text-center text-3xl</span>">
</span><span class="codeblock-line" data-highlight="true" data-line-number="17"> <span style="color: var(--base0F)">{</span><span style="color: var(--base08)">post</span>.<span style="color: var(--base08)">title</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="18"> </<span style="color: var(--base08)">h1</span>>
</span><span class="codeblock-line" data-line-number="19"> </<span style="color: var(--base08)">main</span>>
</span><span class="codeblock-line" data-line-number="20"> );
</span><span class="codeblock-line" data-line-number="21">}
</span></code></pre></div><p>Check that out! We're now pulling our posts from a data source instead of including it all in the browser as JavaScript.</p><p>Let's make TypeScript happy with our code:</p><div><pre data-filename="app/routes/posts/$slug.tsx" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">import</span> <span style="color: var(--base0E)">type</span> { <span style="color: var(--base08)">LoaderArgs</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-line-number="2"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">json</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-line-number="3"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">useLoaderData</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/react</span>";
</span><span class="codeblock-line" data-highlight="true" data-line-number="4"><span style="color: var(--base0E)">import</span> <span style="color: var(--base08)">invariant</span> <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">tiny-invariant</span>";
</span><span class="codeblock-line" data-line-number="5">
</span><span class="codeblock-line" data-line-number="6"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">getPost</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">~/models/post.server</span>";
</span><span class="codeblock-line" data-line-number="7">
</span><span class="codeblock-line" data-line-number="8"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0A)">const</span> <span style="color: var(--base0D)">loader</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0E)">async</span> ({ <span style="color: var(--base08)">params</span> }<span style="color: var(--base0E)">:</span> <span style="color: var(--base0A)">LoaderArgs</span>) <span style="color: var(--base0D)">=></span> {
</span><span class="codeblock-line" data-highlight="true" data-line-number="9"> <span style="color: var(--base0D)">invariant</span>(<span style="color: var(--base08)">params</span>.<span style="color: var(--base08)">slug</span>, `<span style="color: var(--base0B)">params.slug is required</span>`);
</span><span class="codeblock-line" data-line-number="10">
</span><span class="codeblock-line" data-line-number="11"> <span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">post</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0E)">await</span> <span style="color: var(--base0D)">getPost</span>(<span style="color: var(--base08)">params</span>.<span style="color: var(--base08)">slug</span>);
</span><span class="codeblock-line" data-highlight="true" data-line-number="12"> <span style="color: var(--base0D)">invariant</span>(<span style="color: var(--base08)">post</span>, `<span style="color: var(--base0B)">Post not found: </span>${<span style="color: var(--base08)">params</span>.<span style="color: var(--base08)">slug</span>}`);
</span><span class="codeblock-line" data-line-number="13">
</span><span class="codeblock-line" data-line-number="14"> <span style="color: var(--base0E)">return</span> <span style="color: var(--base0D)">json</span>({ <span style="color: var(--base08)">post</span> });
</span><span class="codeblock-line" data-line-number="15">};
</span><span class="codeblock-line" data-line-number="16">
</span><span class="codeblock-line" data-line-number="17"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">default</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">PostSlug</span>() {
</span><span class="codeblock-line" data-line-number="18"> <span style="color: var(--base0A)">const</span> { <span style="color: var(--base08)">post</span> } <span style="color: var(--base0E)">=</span> <span style="color: var(--base0D)">useLoaderData</span><<span style="color: var(--base0E)">typeof</span> <span style="color: var(--base08)">loader</span>>();
</span><span class="codeblock-line" data-line-number="19"> <span style="color: var(--base0E)">return</span> (
</span><span class="codeblock-line" data-line-number="20"> <<span style="color: var(--base08)">main</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">mx-auto max-w-4xl</span>">
</span><span class="codeblock-line" data-line-number="21"> <<span style="color: var(--base08)">h1</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">my-6 border-b-2 text-center text-3xl</span>">
</span><span class="codeblock-line" data-line-number="22"> <span style="color: var(--base0F)">{</span><span style="color: var(--base08)">post</span>.<span style="color: var(--base08)">title</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="23"> </<span style="color: var(--base08)">h1</span>>
</span><span class="codeblock-line" data-line-number="24"> </<span style="color: var(--base08)">main</span>>
</span><span class="codeblock-line" data-line-number="25"> );
</span><span class="codeblock-line" data-line-number="26">}
</span></code></pre></div><p>Quick note on that <code>invariant</code> for the params. Because <code>params</code> comes from the URL, we can't be totally sure that <code>params.slug</code> will be defined--maybe you change the name of the file to <code>$postId.ts</code>! It's good practice to validate that stuff with <code>invariant</code>, and it makes TypeScript happy too.</p><p>We also have an invariant for the post. We'll handle the <code>404</code> case better later. Keep going!</p><p>Now let's get that markdown parsed and rendered to HTML to the page. There are a lot of markdown parsers, we'll use "marked" for this tutorial because it's really easy to get working.</p><p>💿 Parse the markdown into HTML</p><pre><code class="language-sh">npm add marked
# if using typescript
npm add @types/marked -D
</code></pre><p>Now that <code>marked</code> has been installed, we will need to restart our server. So stop the dev server and start it back up again with <code>npm run dev</code>.</p><div><pre data-filename="app/routes/post/$slug.ts" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">import</span> <span style="color: var(--base0E)">type</span> { <span style="color: var(--base08)">LoaderArgs</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-line-number="2"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">json</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-line-number="3"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">useLoaderData</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/react</span>";
</span><span class="codeblock-line" data-highlight="true" data-line-number="4"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">marked</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">marked</span>";
</span><span class="codeblock-line" data-line-number="5"><span style="color: var(--base0E)">import</span> <span style="color: var(--base08)">invariant</span> <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">tiny-invariant</span>";
</span><span class="codeblock-line" data-line-number="6">
</span><span class="codeblock-line" data-line-number="7"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">getPost</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">~/models/post.server</span>";
</span><span class="codeblock-line" data-line-number="8">
</span><span class="codeblock-line" data-line-number="9"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0A)">const</span> <span style="color: var(--base0D)">loader</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0E)">async</span> ({ <span style="color: var(--base08)">params</span> }<span style="color: var(--base0E)">:</span> <span style="color: var(--base0A)">LoaderArgs</span>) <span style="color: var(--base0D)">=></span> {
</span><span class="codeblock-line" data-line-number="10"> <span style="color: var(--base0D)">invariant</span>(<span style="color: var(--base08)">params</span>.<span style="color: var(--base08)">slug</span>, `<span style="color: var(--base0B)">params.slug is required</span>`);
</span><span class="codeblock-line" data-line-number="11">
</span><span class="codeblock-line" data-line-number="12"> <span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">post</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0E)">await</span> <span style="color: var(--base0D)">getPost</span>(<span style="color: var(--base08)">params</span>.<span style="color: var(--base08)">slug</span>);
</span><span class="codeblock-line" data-line-number="13"> <span style="color: var(--base0D)">invariant</span>(<span style="color: var(--base08)">post</span>, `<span style="color: var(--base0B)">Post not found: </span>${<span style="color: var(--base08)">params</span>.<span style="color: var(--base08)">slug</span>}`);
</span><span class="codeblock-line" data-line-number="14">
</span><span class="codeblock-line" data-highlight="true" data-line-number="15"> <span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">html</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0D)">marked</span>(<span style="color: var(--base08)">post</span>.<span style="color: var(--base08)">markdown</span>);
</span><span class="codeblock-line" data-highlight="true" data-line-number="16"> <span style="color: var(--base0E)">return</span> <span style="color: var(--base0D)">json</span>({ <span style="color: var(--base08)">post</span>, <span style="color: var(--base08)">html</span> });
</span><span class="codeblock-line" data-line-number="17">};
</span><span class="codeblock-line" data-line-number="18">
</span><span class="codeblock-line" data-line-number="19"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">default</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">PostSlug</span>() {
</span><span class="codeblock-line" data-highlight="true" data-line-number="20"> <span style="color: var(--base0A)">const</span> { <span style="color: var(--base08)">post</span>, <span style="color: var(--base08)">html</span> } <span style="color: var(--base0E)">=</span> <span style="color: var(--base0D)">useLoaderData</span><<span style="color: var(--base0E)">typeof</span> <span style="color: var(--base08)">loader</span>>();
</span><span class="codeblock-line" data-line-number="21"> <span style="color: var(--base0E)">return</span> (
</span><span class="codeblock-line" data-line-number="22"> <<span style="color: var(--base08)">main</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">mx-auto max-w-4xl</span>">
</span><span class="codeblock-line" data-line-number="23"> <<span style="color: var(--base08)">h1</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">my-6 border-b-2 text-center text-3xl</span>">
</span><span class="codeblock-line" data-line-number="24"> <span style="color: var(--base0F)">{</span><span style="color: var(--base08)">post</span>.<span style="color: var(--base08)">title</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="25"> </<span style="color: var(--base08)">h1</span>>
</span><span class="codeblock-line" data-highlight="true" data-line-number="26"> <<span style="color: var(--base08)">div</span> <span style="color: var(--base0D)">dangerouslySetInnerHTML</span><span style="color: var(--base0E)">=</span><span style="color: var(--base0F)">{</span>{ __html: <span style="color: var(--base08)">html</span> }<span style="color: var(--base0F)">}</span> />
</span><span class="codeblock-line" data-line-number="27"> </<span style="color: var(--base08)">main</span>>
</span><span class="codeblock-line" data-line-number="28"> );
</span><span class="codeblock-line" data-line-number="29">}
</span></code></pre></div><p>Holy smokes, you did it. You have a blog. Check it out! Next, we're gonna make it easier to create new blog posts 📝</p><h2 id="nested-routing"><a href="https://remix.run/docs/en/v1/tutorials/blog#nested-routing" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Nested Routing</h2><p>Right now, our blog posts just come from seeding the database. Not a real solution, so we need a way to create a new blog post in the database. We're going to be using actions for that.</p><p>Let's make a new "admin" section of the app.</p><p>💿 First, let's add a link to the admin section on the posts index route:</p><div><pre data-filename="app/routes/posts/index.tsx" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base03)">// ...</span>
</span><span class="codeblock-line" data-line-number="2"><<span style="color: var(--base0A)">Link</span> <span style="color: var(--base0D)">to</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">admin</span>" <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">text-red-600 underline</span>">
</span><span class="codeblock-line" data-line-number="3"> Admin
</span><span class="codeblock-line" data-line-number="4"></<span style="color: var(--base0A)">Link</span>>
</span><span class="codeblock-line" data-line-number="5"><span style="color: var(--base03)">// ...</span>
</span></code></pre></div><p>Put that anywhere in the component. I stuck it right under the <code><h1></code>.</p><p><docs-info>Did you notice that the <code>to</code> prop is just "admin" and it linked to <code>/posts/admin</code>? With Remix, you get relative links.</docs-info></p><p>💿 Create an admin route within the <code>posts</code> directory:</p><pre><code class="language-sh">touch app/routes/posts/admin.tsx
</code></pre><div><pre data-filename="app/routes/posts/admin.tsx" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">json</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-line-number="2"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">Link</span>, <span style="color: var(--base08)">useLoaderData</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/react</span>";
</span><span class="codeblock-line" data-line-number="3">
</span><span class="codeblock-line" data-line-number="4"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">getPosts</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">~/models/post.server</span>";
</span><span class="codeblock-line" data-line-number="5">
</span><span class="codeblock-line" data-line-number="6"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0A)">const</span> <span style="color: var(--base0D)">loader</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0E)">async</span> () <span style="color: var(--base0D)">=></span> {
</span><span class="codeblock-line" data-line-number="7"> <span style="color: var(--base0E)">return</span> <span style="color: var(--base0D)">json</span>({ posts: <span style="color: var(--base0E)">await</span> <span style="color: var(--base0D)">getPosts</span>() });
</span><span class="codeblock-line" data-line-number="8">};
</span><span class="codeblock-line" data-line-number="9">
</span><span class="codeblock-line" data-line-number="10"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">default</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">PostAdmin</span>() {
</span><span class="codeblock-line" data-line-number="11"> <span style="color: var(--base0A)">const</span> { <span style="color: var(--base08)">posts</span> } <span style="color: var(--base0E)">=</span> <span style="color: var(--base0D)">useLoaderData</span><<span style="color: var(--base0E)">typeof</span> <span style="color: var(--base08)">loader</span>>();
</span><span class="codeblock-line" data-line-number="12"> <span style="color: var(--base0E)">return</span> (
</span><span class="codeblock-line" data-line-number="13"> <<span style="color: var(--base08)">div</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">mx-auto max-w-4xl</span>">
</span><span class="codeblock-line" data-line-number="14"> <<span style="color: var(--base08)">h1</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">my-6 mb-2 border-b-2 text-center text-3xl</span>">
</span><span class="codeblock-line" data-line-number="15"> Blog Admin
</span><span class="codeblock-line" data-line-number="16"> </<span style="color: var(--base08)">h1</span>>
</span><span class="codeblock-line" data-line-number="17"> <<span style="color: var(--base08)">div</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">grid grid-cols-4 gap-6</span>">
</span><span class="codeblock-line" data-line-number="18"> <<span style="color: var(--base08)">nav</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">col-span-4 md:col-span-1</span>">
</span><span class="codeblock-line" data-line-number="19"> <<span style="color: var(--base08)">ul</span>>
</span><span class="codeblock-line" data-line-number="20"> <span style="color: var(--base0F)">{</span><span style="color: var(--base08)">posts</span>.<span style="color: var(--base0D)">map</span>((<span style="color: var(--base08)">post</span>) <span style="color: var(--base0D)">=></span> (
</span><span class="codeblock-line" data-line-number="21"> <<span style="color: var(--base08)">li</span> <span style="color: var(--base0D)">key</span><span style="color: var(--base0E)">=</span><span style="color: var(--base0F)">{</span><span style="color: var(--base08)">post</span>.<span style="color: var(--base08)">slug</span><span style="color: var(--base0F)">}</span>>
</span><span class="codeblock-line" data-line-number="22"> <<span style="color: var(--base0A)">Link</span>
</span><span class="codeblock-line" data-line-number="23"> <span style="color: var(--base0D)">to</span><span style="color: var(--base0E)">=</span><span style="color: var(--base0F)">{</span><span style="color: var(--base08)">post</span>.<span style="color: var(--base08)">slug</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="24"> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">text-blue-600 underline</span>"
</span><span class="codeblock-line" data-line-number="25"> >
</span><span class="codeblock-line" data-line-number="26"> <span style="color: var(--base0F)">{</span><span style="color: var(--base08)">post</span>.<span style="color: var(--base08)">title</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="27"> </<span style="color: var(--base0A)">Link</span>>
</span><span class="codeblock-line" data-line-number="28"> </<span style="color: var(--base08)">li</span>>
</span><span class="codeblock-line" data-line-number="29"> ))<span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="30"> </<span style="color: var(--base08)">ul</span>>
</span><span class="codeblock-line" data-line-number="31"> </<span style="color: var(--base08)">nav</span>>
</span><span class="codeblock-line" data-line-number="32"> <<span style="color: var(--base08)">main</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">col-span-4 md:col-span-3</span>">
</span><span class="codeblock-line" data-line-number="33"> ...
</span><span class="codeblock-line" data-line-number="34"> </<span style="color: var(--base08)">main</span>>
</span><span class="codeblock-line" data-line-number="35"> </<span style="color: var(--base08)">div</span>>
</span><span class="codeblock-line" data-line-number="36"> </<span style="color: var(--base08)">div</span>>
</span><span class="codeblock-line" data-line-number="37"> );
</span><span class="codeblock-line" data-line-number="38">}
</span></code></pre></div><p>You should recognize several of the things we're doing in there from what we've done so far. With that, you should have a decent looking page with the posts on the left and a placeholder on the right.
Now, if you click on the Admin link, it'll take you to <a href="http://localhost:3000/posts/admin">http://localhost:3000/posts/admin</a>.</p><h3 id="index-routes"><a href="https://remix.run/docs/en/v1/tutorials/blog#index-routes" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Index Routes</h3><p>Let's fill in that placeholder with an index route for admin. Hang with us, we're introducing "nested routes" here where your route file nesting becomes UI component nesting.</p><p>💿 Create a folder for <code>admin.tsx</code>'s child routes, with an index inside</p><pre><code class="language-sh">mkdir app/routes/posts/admin
touch app/routes/posts/admin/index.tsx
</code></pre><div><pre data-filename="app/routes/posts/admin/index.tsx" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">Link</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/react</span>";
</span><span class="codeblock-line" data-line-number="2">
</span><span class="codeblock-line" data-line-number="3"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">default</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">AdminIndex</span>() {
</span><span class="codeblock-line" data-line-number="4"> <span style="color: var(--base0E)">return</span> (
</span><span class="codeblock-line" data-line-number="5"> <<span style="color: var(--base08)">p</span>>
</span><span class="codeblock-line" data-line-number="6"> <<span style="color: var(--base0A)">Link</span> <span style="color: var(--base0D)">to</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">new</span>" <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">text-blue-600 underline</span>">
</span><span class="codeblock-line" data-line-number="7"> Create a New Post
</span><span class="codeblock-line" data-line-number="8"> </<span style="color: var(--base0A)">Link</span>>
</span><span class="codeblock-line" data-line-number="9"> </<span style="color: var(--base08)">p</span>>
</span><span class="codeblock-line" data-line-number="10"> );
</span><span class="codeblock-line" data-line-number="11">}
</span></code></pre></div><p>If you refresh you're not going to see it yet. Every route inside of <code>app/routes/posts/admin/</code> can now render <em>inside</em> of <code>app/routes/posts/admin.tsx</code> when their URL matches. You get to control which part of the <code>admin.tsx</code> layout the child routes render.</p><p>💿 Add an outlet to the admin page</p><div><pre data-filename="app/routes/posts/admin.tsx" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">json</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-line-number="2"><span style="color: var(--base0E)">import</span> {
</span><span class="codeblock-line" data-line-number="3"> <span style="color: var(--base08)">Link</span>,
</span><span class="codeblock-line" data-highlight="true" data-line-number="4"> <span style="color: var(--base08)">Outlet</span>,
</span><span class="codeblock-line" data-line-number="5"> <span style="color: var(--base08)">useLoaderData</span>,
</span><span class="codeblock-line" data-line-number="6">} <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/react</span>";
</span><span class="codeblock-line" data-line-number="7">
</span><span class="codeblock-line" data-line-number="8"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">getPosts</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">~/models/post.server</span>";
</span><span class="codeblock-line" data-line-number="9">
</span><span class="codeblock-line" data-line-number="10"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0A)">const</span> <span style="color: var(--base0D)">loader</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0E)">async</span> () <span style="color: var(--base0D)">=></span> {
</span><span class="codeblock-line" data-line-number="11"> <span style="color: var(--base0E)">return</span> <span style="color: var(--base0D)">json</span>({ posts: <span style="color: var(--base0E)">await</span> <span style="color: var(--base0D)">getPosts</span>() });
</span><span class="codeblock-line" data-line-number="12">};
</span><span class="codeblock-line" data-line-number="13">
</span><span class="codeblock-line" data-line-number="14"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">default</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">PostAdmin</span>() {
</span><span class="codeblock-line" data-line-number="15"> <span style="color: var(--base0A)">const</span> { <span style="color: var(--base08)">posts</span> } <span style="color: var(--base0E)">=</span> <span style="color: var(--base0D)">useLoaderData</span><<span style="color: var(--base0E)">typeof</span> <span style="color: var(--base08)">loader</span>>();
</span><span class="codeblock-line" data-line-number="16"> <span style="color: var(--base0E)">return</span> (
</span><span class="codeblock-line" data-line-number="17"> <<span style="color: var(--base08)">div</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">mx-auto max-w-4xl</span>">
</span><span class="codeblock-line" data-line-number="18"> <<span style="color: var(--base08)">h1</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">my-6 mb-2 border-b-2 text-center text-3xl</span>">
</span><span class="codeblock-line" data-line-number="19"> Blog Admin
</span><span class="codeblock-line" data-line-number="20"> </<span style="color: var(--base08)">h1</span>>
</span><span class="codeblock-line" data-line-number="21"> <<span style="color: var(--base08)">div</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">grid grid-cols-4 gap-6</span>">
</span><span class="codeblock-line" data-line-number="22"> <<span style="color: var(--base08)">nav</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">col-span-4 md:col-span-1</span>">
</span><span class="codeblock-line" data-line-number="23"> <<span style="color: var(--base08)">ul</span>>
</span><span class="codeblock-line" data-line-number="24"> <span style="color: var(--base0F)">{</span><span style="color: var(--base08)">posts</span>.<span style="color: var(--base0D)">map</span>((<span style="color: var(--base08)">post</span>) <span style="color: var(--base0D)">=></span> (
</span><span class="codeblock-line" data-line-number="25"> <<span style="color: var(--base08)">li</span> <span style="color: var(--base0D)">key</span><span style="color: var(--base0E)">=</span><span style="color: var(--base0F)">{</span><span style="color: var(--base08)">post</span>.<span style="color: var(--base08)">slug</span><span style="color: var(--base0F)">}</span>>
</span><span class="codeblock-line" data-line-number="26"> <<span style="color: var(--base0A)">Link</span>
</span><span class="codeblock-line" data-line-number="27"> <span style="color: var(--base0D)">to</span><span style="color: var(--base0E)">=</span><span style="color: var(--base0F)">{</span><span style="color: var(--base08)">post</span>.<span style="color: var(--base08)">slug</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="28"> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">text-blue-600 underline</span>"
</span><span class="codeblock-line" data-line-number="29"> >
</span><span class="codeblock-line" data-line-number="30"> <span style="color: var(--base0F)">{</span><span style="color: var(--base08)">post</span>.<span style="color: var(--base08)">title</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="31"> </<span style="color: var(--base0A)">Link</span>>
</span><span class="codeblock-line" data-line-number="32"> </<span style="color: var(--base08)">li</span>>
</span><span class="codeblock-line" data-line-number="33"> ))<span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="34"> </<span style="color: var(--base08)">ul</span>>
</span><span class="codeblock-line" data-line-number="35"> </<span style="color: var(--base08)">nav</span>>
</span><span class="codeblock-line" data-line-number="36"> <<span style="color: var(--base08)">main</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">col-span-4 md:col-span-3</span>">
</span><span class="codeblock-line" data-line-number="37"> <<span style="color: var(--base0A)">Outlet</span> />
</span><span class="codeblock-line" data-line-number="38"> </<span style="color: var(--base08)">main</span>>
</span><span class="codeblock-line" data-line-number="39"> </<span style="color: var(--base08)">div</span>>
</span><span class="codeblock-line" data-line-number="40"> </<span style="color: var(--base08)">div</span>>
</span><span class="codeblock-line" data-highlight="true" data-line-number="41"> );
</span><span class="codeblock-line" data-line-number="42">}
</span></code></pre></div><p>Hang with us for a minute, index routes can be confusing at first. Just know that when the URL matches the parent route's path, the index will render inside the outlet.</p><p>Maybe this will help, let's add the "/posts/admin/new" route and see what happens when we click the link.</p><p>💿 Create the <code>app/routes/posts/admin/new.tsx</code> route</p><pre><code class="language-sh">touch app/routes/posts/admin/new.tsx
</code></pre><div><pre data-filename="app/routes/posts/admin/new.tsx" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">default</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">NewPost</span>() {
</span><span class="codeblock-line" data-line-number="2"> <span style="color: var(--base0E)">return</span> <<span style="color: var(--base08)">h2</span>>New Post</<span style="color: var(--base08)">h2</span>>;
</span><span class="codeblock-line" data-line-number="3">}
</span></code></pre></div><p>Now click the link from the index route and watch the <code><Outlet/></code> automatically swap out the index route for the "new" route!</p><h2 id="actions"><a href="https://remix.run/docs/en/v1/tutorials/blog#actions" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Actions</h2><p>We're gonna get serious now. Let's build a form to create a new post in our new "new" route.</p><p>💿 Add a form to the new route</p><div><pre data-filename="app/routes/posts/admin/new.tsx" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">Form</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/react</span>";
</span><span class="codeblock-line" data-line-number="2">
</span><span class="codeblock-line" data-line-number="3"><span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">inputClassName</span> <span style="color: var(--base0E)">=</span> `<span style="color: var(--base0B)">w-full rounded border border-gray-500 px-2 py-1 text-lg</span>`;
</span><span class="codeblock-line" data-line-number="4">
</span><span class="codeblock-line" data-line-number="5"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">default</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">NewPost</span>() {
</span><span class="codeblock-line" data-line-number="6"> <span style="color: var(--base0E)">return</span> (
</span><span class="codeblock-line" data-line-number="7"> <<span style="color: var(--base0A)">Form</span> <span style="color: var(--base0D)">method</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">post</span>">
</span><span class="codeblock-line" data-line-number="8"> <<span style="color: var(--base08)">p</span>>
</span><span class="codeblock-line" data-line-number="9"> <<span style="color: var(--base08)">label</span>>
</span><span class="codeblock-line" data-line-number="10"> Post Title:<span style="color: var(--base0F)">{</span>"<span style="color: var(--base0B)"> </span>"<span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="11"> <<span style="color: var(--base08)">input</span>
</span><span class="codeblock-line" data-line-number="12"> <span style="color: var(--base0D)">type</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">text</span>"
</span><span class="codeblock-line" data-line-number="13"> <span style="color: var(--base0D)">name</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">title</span>"
</span><span class="codeblock-line" data-line-number="14"> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span><span style="color: var(--base0F)">{</span><span style="color: var(--base08)">inputClassName</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="15"> />
</span><span class="codeblock-line" data-line-number="16"> </<span style="color: var(--base08)">label</span>>
</span><span class="codeblock-line" data-line-number="17"> </<span style="color: var(--base08)">p</span>>
</span><span class="codeblock-line" data-line-number="18"> <<span style="color: var(--base08)">p</span>>
</span><span class="codeblock-line" data-line-number="19"> <<span style="color: var(--base08)">label</span>>
</span><span class="codeblock-line" data-line-number="20"> Post Slug:<span style="color: var(--base0F)">{</span>"<span style="color: var(--base0B)"> </span>"<span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="21"> <<span style="color: var(--base08)">input</span>
</span><span class="codeblock-line" data-line-number="22"> <span style="color: var(--base0D)">type</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">text</span>"
</span><span class="codeblock-line" data-line-number="23"> <span style="color: var(--base0D)">name</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">slug</span>"
</span><span class="codeblock-line" data-line-number="24"> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span><span style="color: var(--base0F)">{</span><span style="color: var(--base08)">inputClassName</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="25"> />
</span><span class="codeblock-line" data-line-number="26"> </<span style="color: var(--base08)">label</span>>
</span><span class="codeblock-line" data-line-number="27"> </<span style="color: var(--base08)">p</span>>
</span><span class="codeblock-line" data-line-number="28"> <<span style="color: var(--base08)">p</span>>
</span><span class="codeblock-line" data-line-number="29"> <<span style="color: var(--base08)">label</span> <span style="color: var(--base0D)">htmlFor</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">markdown</span>">Markdown:</<span style="color: var(--base08)">label</span>>
</span><span class="codeblock-line" data-line-number="30"> <<span style="color: var(--base08)">br</span> />
</span><span class="codeblock-line" data-line-number="31"> <<span style="color: var(--base08)">textarea</span>
</span><span class="codeblock-line" data-line-number="32"> <span style="color: var(--base0D)">id</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">markdown</span>"
</span><span class="codeblock-line" data-line-number="33"> <span style="color: var(--base0D)">rows</span><span style="color: var(--base0E)">=</span><span style="color: var(--base0F)">{</span><span style="color: var(--base09)">20</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="34"> <span style="color: var(--base0D)">name</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">markdown</span>"
</span><span class="codeblock-line" data-line-number="35"> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span><span style="color: var(--base0F)">{</span>`${<span style="color: var(--base08)">inputClassName</span>}<span style="color: var(--base0B)"> font-mono</span>`<span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="36"> />
</span><span class="codeblock-line" data-line-number="37"> </<span style="color: var(--base08)">p</span>>
</span><span class="codeblock-line" data-line-number="38"> <<span style="color: var(--base08)">p</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">text-right</span>">
</span><span class="codeblock-line" data-line-number="39"> <<span style="color: var(--base08)">button</span>
</span><span class="codeblock-line" data-line-number="40"> <span style="color: var(--base0D)">type</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">submit</span>"
</span><span class="codeblock-line" data-line-number="41"> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">rounded bg-blue-500 py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400 disabled:bg-blue-300</span>"
</span><span class="codeblock-line" data-line-number="42"> >
</span><span class="codeblock-line" data-line-number="43"> Create Post
</span><span class="codeblock-line" data-line-number="44"> </<span style="color: var(--base08)">button</span>>
</span><span class="codeblock-line" data-line-number="45"> </<span style="color: var(--base08)">p</span>>
</span><span class="codeblock-line" data-line-number="46"> </<span style="color: var(--base0A)">Form</span>>
</span><span class="codeblock-line" data-line-number="47"> );
</span><span class="codeblock-line" data-line-number="48">}
</span></code></pre></div><p>If you love HTML like us, you should be getting pretty excited. If you've been doing a lot of <code><form onSubmit></code> and <code><button onClick></code> you're about to have your mind blown by HTML.</p><p>All you really need for a feature like this is a form to get data from the user and a backend action to handle it. And in Remix, that's all you have to do too.</p><p>Let's create the essential code that knows how to save a post first in our <code>post.ts</code> module.</p><p>💿 Add <code>createPost</code> anywhere inside of <code>app/models/post.server.ts</code></p><div><pre data-filename="app/models/post.server.ts" data-nocopy="" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base03)">// ...</span>
</span><span class="codeblock-line" data-line-number="2"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">async</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">createPost</span>(<span style="color: var(--base08)">post</span>) {
</span><span class="codeblock-line" data-line-number="3"> <span style="color: var(--base0E)">return</span> <span style="color: var(--base08)">prisma</span>.<span style="color: var(--base0D)">post</span>.<span style="color: var(--base0D)">create</span>({ data: <span style="color: var(--base08)">post</span> });
</span><span class="codeblock-line" data-line-number="4">}
</span></code></pre></div><p>💿 Call <code>createPost</code> from the new post route's action</p><div><pre data-filename="app/routes/posts/admin/new.tsx" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">import</span> <span style="color: var(--base0E)">type</span> { <span style="color: var(--base08)">ActionArgs</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-line-number="2"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">redirect</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-line-number="3"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">Form</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/react</span>";
</span><span class="codeblock-line" data-line-number="4">
</span><span class="codeblock-line" data-line-number="5"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">createPost</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">~/models/post.server</span>";
</span><span class="codeblock-line" data-line-number="6">
</span><span class="codeblock-line" data-line-number="7"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0A)">const</span> <span style="color: var(--base0D)">action</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0E)">async</span> ({ <span style="color: var(--base08)">request</span> }<span style="color: var(--base0E)">:</span> <span style="color: var(--base0A)">ActionArgs</span>) <span style="color: var(--base0D)">=></span> {
</span><span class="codeblock-line" data-line-number="8"> <span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">formData</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0E)">await</span> <span style="color: var(--base08)">request</span>.<span style="color: var(--base0D)">formData</span>();
</span><span class="codeblock-line" data-line-number="9">
</span><span class="codeblock-line" data-line-number="10"> <span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">title</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base08)">formData</span>.<span style="color: var(--base0D)">get</span>("<span style="color: var(--base0B)">title</span>");
</span><span class="codeblock-line" data-line-number="11"> <span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">slug</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base08)">formData</span>.<span style="color: var(--base0D)">get</span>("<span style="color: var(--base0B)">slug</span>");
</span><span class="codeblock-line" data-line-number="12"> <span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">markdown</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base08)">formData</span>.<span style="color: var(--base0D)">get</span>("<span style="color: var(--base0B)">markdown</span>");
</span><span class="codeblock-line" data-line-number="13">
</span><span class="codeblock-line" data-line-number="14"> <span style="color: var(--base0E)">await</span> <span style="color: var(--base0D)">createPost</span>({ <span style="color: var(--base08)">title</span>, <span style="color: var(--base08)">slug</span>, <span style="color: var(--base08)">markdown</span> });
</span><span class="codeblock-line" data-line-number="15">
</span><span class="codeblock-line" data-line-number="16"> <span style="color: var(--base0E)">return</span> <span style="color: var(--base0D)">redirect</span>("<span style="color: var(--base0B)">/posts/admin</span>");
</span><span class="codeblock-line" data-line-number="17">};
</span><span class="codeblock-line" data-line-number="18">
</span><span class="codeblock-line" data-line-number="19"><span style="color: var(--base03)">// ...</span>
</span></code></pre></div><p>That's it. Remix (and the browser) will take care of the rest. Click the submit button and watch the sidebar that lists our posts update automatically.</p><p>In HTML an input's <code>name</code> attribute is sent over the network and available by the same name on the request's <code>formData</code>. Oh, and don't forget, the <code>request</code> and <code>formData</code> objects are both straight out of the web specification. So if you want to learn more about either of them, head over to MDN! <a href="https://developer.mozilla.org/en-US/docs/Web/API/Request">Request</a> <a href="https://developer.mozilla.org/en-US/docs/Web/API/Request/formData">Request.formData</a>.</p><p>TypeScript is mad again, let's add some types.</p><p>💿 Add the types to <code>app/models/post.server.ts</code></p><div><pre data-filename="app/models/post.server.ts" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base03)">// ...</span>
</span><span class="codeblock-line" data-highlight="true" data-line-number="2"><span style="color: var(--base0E)">import</span> <span style="color: var(--base0E)">type</span> { <span style="color: var(--base08)">Post</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@prisma/client</span>";
</span><span class="codeblock-line" data-line-number="3">
</span><span class="codeblock-line" data-line-number="4"><span style="color: var(--base03)">// ...</span>
</span><span class="codeblock-line" data-line-number="5">
</span><span class="codeblock-line" data-line-number="6"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">async</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">createPost</span>(
</span><span class="codeblock-line" data-highlight="true" data-line-number="7"> <span style="color: var(--base08)">post</span><span style="color: var(--base0E)">:</span> <span style="color: var(--base0A)">Pick</span><<span style="color: var(--base0A)">Post</span>, "<span style="color: var(--base0B)">slug</span>" <span style="color: var(--base0E)">|</span> "<span style="color: var(--base0B)">title</span>" <span style="color: var(--base0E)">|</span> "<span style="color: var(--base0B)">markdown</span>">
</span><span class="codeblock-line" data-line-number="8">) {
</span><span class="codeblock-line" data-line-number="9"> <span style="color: var(--base0E)">return</span> <span style="color: var(--base08)">prisma</span>.<span style="color: var(--base0D)">post</span>.<span style="color: var(--base0D)">create</span>({ data: <span style="color: var(--base08)">post</span> });
</span><span class="codeblock-line" data-line-number="10">}
</span></code></pre></div><p>Whether you're using TypeScript or not, we've got a problem when the user doesn't provide values on some of these fields (and TS is still mad about that call to <code>createPost</code>).</p><p>Let's add some validation before we create the post.</p><p>💿 Validate if the form data contains what we need, and return the errors if not</p><div><pre data-filename="app/routes/posts/admin/new.tsx" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">import</span> <span style="color: var(--base0E)">type</span> { <span style="color: var(--base08)">ActionArgs</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-highlight="true" data-line-number="2"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">json</span>, <span style="color: var(--base08)">redirect</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-line-number="3"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">Form</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/react</span>";
</span><span class="codeblock-line" data-line-number="4">
</span><span class="codeblock-line" data-line-number="5"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">createPost</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">~/models/post.server</span>";
</span><span class="codeblock-line" data-line-number="6">
</span><span class="codeblock-line" data-line-number="7"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0A)">const</span> <span style="color: var(--base0D)">action</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0E)">async</span> ({ <span style="color: var(--base08)">request</span> }<span style="color: var(--base0E)">:</span> <span style="color: var(--base0A)">ActionArgs</span>) <span style="color: var(--base0D)">=></span> {
</span><span class="codeblock-line" data-line-number="8"> <span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">formData</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0E)">await</span> <span style="color: var(--base08)">request</span>.<span style="color: var(--base0D)">formData</span>();
</span><span class="codeblock-line" data-line-number="9">
</span><span class="codeblock-line" data-line-number="10"> <span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">title</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base08)">formData</span>.<span style="color: var(--base0D)">get</span>("<span style="color: var(--base0B)">title</span>");
</span><span class="codeblock-line" data-line-number="11"> <span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">slug</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base08)">formData</span>.<span style="color: var(--base0D)">get</span>("<span style="color: var(--base0B)">slug</span>");
</span><span class="codeblock-line" data-line-number="12"> <span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">markdown</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base08)">formData</span>.<span style="color: var(--base0D)">get</span>("<span style="color: var(--base0B)">markdown</span>");
</span><span class="codeblock-line" data-line-number="13">
</span><span class="codeblock-line" data-highlight="true" data-line-number="14"> <span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">errors</span> <span style="color: var(--base0E)">=</span> {
</span><span class="codeblock-line" data-highlight="true" data-line-number="15"> title: <span style="color: var(--base08)">title</span> <span style="color: var(--base0E)">?</span> <span style="color: var(--base09)">null</span> <span style="color: var(--base0E)">:</span> "<span style="color: var(--base0B)">Title is required</span>",
</span><span class="codeblock-line" data-highlight="true" data-line-number="16"> slug: <span style="color: var(--base08)">slug</span> <span style="color: var(--base0E)">?</span> <span style="color: var(--base09)">null</span> <span style="color: var(--base0E)">:</span> "<span style="color: var(--base0B)">Slug is required</span>",
</span><span class="codeblock-line" data-highlight="true" data-line-number="17"> markdown: <span style="color: var(--base08)">markdown</span> <span style="color: var(--base0E)">?</span> <span style="color: var(--base09)">null</span> <span style="color: var(--base0E)">:</span> "<span style="color: var(--base0B)">Markdown is required</span>",
</span><span class="codeblock-line" data-highlight="true" data-line-number="18"> };
</span><span class="codeblock-line" data-highlight="true" data-line-number="19"> <span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">hasErrors</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0A)">Object</span>.<span style="color: var(--base0D)">values</span>(<span style="color: var(--base08)">errors</span>).<span style="color: var(--base0D)">some</span>(
</span><span class="codeblock-line" data-highlight="true" data-line-number="20"> (<span style="color: var(--base08)">errorMessage</span>) <span style="color: var(--base0D)">=></span> <span style="color: var(--base08)">errorMessage</span>
</span><span class="codeblock-line" data-highlight="true" data-line-number="21"> );
</span><span class="codeblock-line" data-highlight="true" data-line-number="22"> <span style="color: var(--base0E)">if</span> (<span style="color: var(--base08)">hasErrors</span>) {
</span><span class="codeblock-line" data-highlight="true" data-line-number="23"> <span style="color: var(--base0E)">return</span> <span style="color: var(--base0D)">json</span>(<span style="color: var(--base08)">errors</span>);
</span><span class="codeblock-line" data-highlight="true" data-line-number="24"> }
</span><span class="codeblock-line" data-line-number="25">
</span><span class="codeblock-line" data-line-number="26"> <span style="color: var(--base0E)">await</span> <span style="color: var(--base0D)">createPost</span>({ <span style="color: var(--base08)">title</span>, <span style="color: var(--base08)">slug</span>, <span style="color: var(--base08)">markdown</span> });
</span><span class="codeblock-line" data-line-number="27">
</span><span class="codeblock-line" data-line-number="28"> <span style="color: var(--base0E)">return</span> <span style="color: var(--base0D)">redirect</span>("<span style="color: var(--base0B)">/posts/admin</span>");
</span><span class="codeblock-line" data-line-number="29">};
</span><span class="codeblock-line" data-line-number="30">
</span><span class="codeblock-line" data-line-number="31"><span style="color: var(--base03)">// ...</span>
</span></code></pre></div><p>Notice we don't return a redirect this time, we actually return the errors. These errors are available to the component via <code>useActionData</code>. It's just like <code>useLoaderData</code> but the data comes from the action after a form POST.</p><p>💿 Add validation messages to the UI</p><div><pre data-filename="app/routes/posts/admin/new.tsx" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">import</span> <span style="color: var(--base0E)">type</span> { <span style="color: var(--base08)">ActionArgs</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-line-number="2"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">redirect</span>, <span style="color: var(--base08)">json</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-highlight="true" data-line-number="3"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">Form</span>, <span style="color: var(--base08)">useActionData</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/react</span>";
</span><span class="codeblock-line" data-line-number="4">
</span><span class="codeblock-line" data-line-number="5"><span style="color: var(--base03)">// ...</span>
</span><span class="codeblock-line" data-line-number="6">
</span><span class="codeblock-line" data-line-number="7"><span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">inputClassName</span> <span style="color: var(--base0E)">=</span> `<span style="color: var(--base0B)">w-full rounded border border-gray-500 px-2 py-1 text-lg</span>`;
</span><span class="codeblock-line" data-line-number="8">
</span><span class="codeblock-line" data-line-number="9"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">default</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">NewPost</span>() {
</span><span class="codeblock-line" data-highlight="true" data-line-number="10"> <span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">errors</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0D)">useActionData</span><<span style="color: var(--base0E)">typeof</span> <span style="color: var(--base08)">action</span>>();
</span><span class="codeblock-line" data-line-number="11">
</span><span class="codeblock-line" data-line-number="12"> <span style="color: var(--base0E)">return</span> (
</span><span class="codeblock-line" data-line-number="13"> <<span style="color: var(--base0A)">Form</span> <span style="color: var(--base0D)">method</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">post</span>">
</span><span class="codeblock-line" data-line-number="14"> <<span style="color: var(--base08)">p</span>>
</span><span class="codeblock-line" data-line-number="15"> <<span style="color: var(--base08)">label</span>>
</span><span class="codeblock-line" data-line-number="16"> Post Title:<span style="color: var(--base0F)">{</span>"<span style="color: var(--base0B)"> </span>"<span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-highlight="true" data-line-number="17"> <span style="color: var(--base0F)">{</span><span style="color: var(--base08)">errors</span>?.<span style="color: var(--base08)">title</span> <span style="color: var(--base0E)">?</span> (
</span><span class="codeblock-line" data-highlight="true" data-line-number="18"> <<span style="color: var(--base08)">em</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">text-red-600</span>"><span style="color: var(--base0F)">{</span><span style="color: var(--base08)">errors</span>.<span style="color: var(--base08)">title</span><span style="color: var(--base0F)">}</span></<span style="color: var(--base08)">em</span>>
</span><span class="codeblock-line" data-highlight="true" data-line-number="19"> ) <span style="color: var(--base0E)">:</span> <span style="color: var(--base09)">null</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="20"> <<span style="color: var(--base08)">input</span> <span style="color: var(--base0D)">type</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">text</span>" <span style="color: var(--base0D)">name</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">title</span>" <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span><span style="color: var(--base0F)">{</span><span style="color: var(--base08)">inputClassName</span><span style="color: var(--base0F)">}</span> />
</span><span class="codeblock-line" data-line-number="21"> </<span style="color: var(--base08)">label</span>>
</span><span class="codeblock-line" data-line-number="22"> </<span style="color: var(--base08)">p</span>>
</span><span class="codeblock-line" data-line-number="23"> <<span style="color: var(--base08)">p</span>>
</span><span class="codeblock-line" data-line-number="24"> <<span style="color: var(--base08)">label</span>>
</span><span class="codeblock-line" data-line-number="25"> Post Slug:<span style="color: var(--base0F)">{</span>"<span style="color: var(--base0B)"> </span>"<span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-highlight="true" data-line-number="26"> <span style="color: var(--base0F)">{</span><span style="color: var(--base08)">errors</span>?.<span style="color: var(--base08)">slug</span> <span style="color: var(--base0E)">?</span> (
</span><span class="codeblock-line" data-highlight="true" data-line-number="27"> <<span style="color: var(--base08)">em</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">text-red-600</span>"><span style="color: var(--base0F)">{</span><span style="color: var(--base08)">errors</span>.<span style="color: var(--base08)">slug</span><span style="color: var(--base0F)">}</span></<span style="color: var(--base08)">em</span>>
</span><span class="codeblock-line" data-highlight="true" data-line-number="28"> ) <span style="color: var(--base0E)">:</span> <span style="color: var(--base09)">null</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="29"> <<span style="color: var(--base08)">input</span> <span style="color: var(--base0D)">type</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">text</span>" <span style="color: var(--base0D)">name</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">slug</span>" <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span><span style="color: var(--base0F)">{</span><span style="color: var(--base08)">inputClassName</span><span style="color: var(--base0F)">}</span> />
</span><span class="codeblock-line" data-line-number="30"> </<span style="color: var(--base08)">label</span>>
</span><span class="codeblock-line" data-line-number="31"> </<span style="color: var(--base08)">p</span>>
</span><span class="codeblock-line" data-line-number="32"> <<span style="color: var(--base08)">p</span>>
</span><span class="codeblock-line" data-line-number="33"> <<span style="color: var(--base08)">label</span> <span style="color: var(--base0D)">htmlFor</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">markdown</span>">
</span><span class="codeblock-line" data-line-number="34"> Markdown:<span style="color: var(--base0F)">{</span>"<span style="color: var(--base0B)"> </span>"<span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-highlight="true" data-line-number="35"> <span style="color: var(--base0F)">{</span><span style="color: var(--base08)">errors</span>?.<span style="color: var(--base08)">markdown</span> <span style="color: var(--base0E)">?</span> (
</span><span class="codeblock-line" data-highlight="true" data-line-number="36"> <<span style="color: var(--base08)">em</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">text-red-600</span>">
</span><span class="codeblock-line" data-highlight="true" data-line-number="37"> <span style="color: var(--base0F)">{</span><span style="color: var(--base08)">errors</span>.<span style="color: var(--base08)">markdown</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-highlight="true" data-line-number="38"> </<span style="color: var(--base08)">em</span>>
</span><span class="codeblock-line" data-highlight="true" data-line-number="39"> ) <span style="color: var(--base0E)">:</span> <span style="color: var(--base09)">null</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="40"> </<span style="color: var(--base08)">label</span>>
</span><span class="codeblock-line" data-line-number="41"> <<span style="color: var(--base08)">br</span> />
</span><span class="codeblock-line" data-line-number="42"> <<span style="color: var(--base08)">textarea</span>
</span><span class="codeblock-line" data-line-number="43"> <span style="color: var(--base0D)">id</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">markdown</span>"
</span><span class="codeblock-line" data-line-number="44"> <span style="color: var(--base0D)">rows</span><span style="color: var(--base0E)">=</span><span style="color: var(--base0F)">{</span><span style="color: var(--base09)">20</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="45"> <span style="color: var(--base0D)">name</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">markdown</span>"
</span><span class="codeblock-line" data-line-number="46"> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span><span style="color: var(--base0F)">{</span>`${<span style="color: var(--base08)">inputClassName</span>}<span style="color: var(--base0B)"> font-mono</span>`<span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="47"> />
</span><span class="codeblock-line" data-line-number="48"> </<span style="color: var(--base08)">p</span>>
</span><span class="codeblock-line" data-line-number="49"> <<span style="color: var(--base08)">p</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">text-right</span>">
</span><span class="codeblock-line" data-line-number="50"> <<span style="color: var(--base08)">button</span>
</span><span class="codeblock-line" data-line-number="51"> <span style="color: var(--base0D)">type</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">submit</span>"
</span><span class="codeblock-line" data-line-number="52"> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">rounded bg-blue-500 py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400 disabled:bg-blue-300</span>"
</span><span class="codeblock-line" data-line-number="53"> >
</span><span class="codeblock-line" data-line-number="54"> Create Post
</span><span class="codeblock-line" data-line-number="55"> </<span style="color: var(--base08)">button</span>>
</span><span class="codeblock-line" data-line-number="56"> </<span style="color: var(--base08)">p</span>>
</span><span class="codeblock-line" data-line-number="57"> </<span style="color: var(--base0A)">Form</span>>
</span><span class="codeblock-line" data-line-number="58"> );
</span><span class="codeblock-line" data-line-number="59">}
</span></code></pre></div><p>TypeScript is still mad, because someone could call our API with non-string values, so let's add some invariants to make it happy.</p><div><pre data-filename="app/routes/posts/admin/new.tsx" data-nocopy="" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base03)">//...</span>
</span><span class="codeblock-line" data-line-number="2"><span style="color: var(--base0E)">import</span> <span style="color: var(--base08)">invariant</span> <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">tiny-invariant</span>";
</span><span class="codeblock-line" data-line-number="3"><span style="color: var(--base03)">// ..</span>
</span><span class="codeblock-line" data-line-number="4">
</span><span class="codeblock-line" data-line-number="5"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0A)">const</span> <span style="color: var(--base0D)">action</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0E)">async</span> ({ <span style="color: var(--base08)">request</span> }<span style="color: var(--base0E)">:</span> <span style="color: var(--base0A)">ActionArgs</span>) <span style="color: var(--base0D)">=></span> {
</span><span class="codeblock-line" data-line-number="6"> <span style="color: var(--base03)">// ...</span>
</span><span class="codeblock-line" data-line-number="7"> <span style="color: var(--base0D)">invariant</span>(
</span><span class="codeblock-line" data-line-number="8"> <span style="color: var(--base0E)">typeof</span> <span style="color: var(--base08)">title</span> <span style="color: var(--base0E)">===</span> "<span style="color: var(--base0B)">string</span>",
</span><span class="codeblock-line" data-line-number="9"> "<span style="color: var(--base0B)">title must be a string</span>"
</span><span class="codeblock-line" data-line-number="10"> );
</span><span class="codeblock-line" data-line-number="11"> <span style="color: var(--base0D)">invariant</span>(
</span><span class="codeblock-line" data-line-number="12"> <span style="color: var(--base0E)">typeof</span> <span style="color: var(--base08)">slug</span> <span style="color: var(--base0E)">===</span> "<span style="color: var(--base0B)">string</span>",
</span><span class="codeblock-line" data-line-number="13"> "<span style="color: var(--base0B)">slug must be a string</span>"
</span><span class="codeblock-line" data-line-number="14"> );
</span><span class="codeblock-line" data-line-number="15"> <span style="color: var(--base0D)">invariant</span>(
</span><span class="codeblock-line" data-line-number="16"> <span style="color: var(--base0E)">typeof</span> <span style="color: var(--base08)">markdown</span> <span style="color: var(--base0E)">===</span> "<span style="color: var(--base0B)">string</span>",
</span><span class="codeblock-line" data-line-number="17"> "<span style="color: var(--base0B)">markdown must be a string</span>"
</span><span class="codeblock-line" data-line-number="18"> );
</span><span class="codeblock-line" data-line-number="19">
</span><span class="codeblock-line" data-line-number="20"> <span style="color: var(--base0E)">await</span> <span style="color: var(--base0D)">createPost</span>({ <span style="color: var(--base08)">title</span>, <span style="color: var(--base08)">slug</span>, <span style="color: var(--base08)">markdown</span> });
</span><span class="codeblock-line" data-line-number="21">
</span><span class="codeblock-line" data-line-number="22"> <span style="color: var(--base0E)">return</span> <span style="color: var(--base0D)">redirect</span>("<span style="color: var(--base0B)">/posts/admin</span>");
</span><span class="codeblock-line" data-line-number="23">};
</span></code></pre></div><h2 id="progressive-enhancement"><a href="https://remix.run/docs/en/v1/tutorials/blog#progressive-enhancement" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Progressive Enhancement</h2><p>For some real fun, <a href="https://developer.chrome.com/docs/devtools/javascript/disable">disable JavaScript</a> in your dev tools and try it out. Because Remix is built on the fundamentals of HTTP and HTML, this whole thing works without JavaScript in the browser 🤯 But that's not the point. What's cool about it is that this means our UI is resilient to network issues. But we really <em>like</em> having JavaScript in the browser and there are a lot of cool things we can do when we've got it, so make sure to re-enable JavaScript before continuing, because we're going to need it to <em>progressively enhance</em> the user experience next.</p><p>Let's slow this down and add some "pending UI" to our form.</p><p>💿 Slow down our action with a fake delay</p><div><pre data-filename="app/routes/posts/admin/new.tsx" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base03)">// ...</span>
</span><span class="codeblock-line" data-line-number="2"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0A)">const</span> <span style="color: var(--base0D)">action</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0E)">async</span> ({ <span style="color: var(--base08)">request</span> }<span style="color: var(--base0E)">:</span> <span style="color: var(--base0A)">ActionArgs</span>) <span style="color: var(--base0D)">=></span> {
</span><span class="codeblock-line" data-highlight="true" data-line-number="3"> <span style="color: var(--base03)">// TODO: remove me</span>
</span><span class="codeblock-line" data-highlight="true" data-line-number="4"> <span style="color: var(--base0E)">await</span> <span style="color: var(--base0E)">new</span> <span style="color: var(--base0A)">Promise</span>((<span style="color: var(--base08)">res</span>) <span style="color: var(--base0D)">=></span> <span style="color: var(--base0D)">setTimeout</span>(<span style="color: var(--base08)">res</span>, <span style="color: var(--base09)">1000</span>));
</span><span class="codeblock-line" data-line-number="5">
</span><span class="codeblock-line" data-line-number="6"> <span style="color: var(--base03)">// ...</span>
</span><span class="codeblock-line" data-line-number="7">};
</span><span class="codeblock-line" data-line-number="8"><span style="color: var(--base03)">//...</span>
</span></code></pre></div><p>💿 Add some pending UI with <code>useTransition</code></p><div><pre data-filename="app/routes/posts/admin/new.tsx" data-line-numbers="true" data-lang="tsx" style="color: var(--base05);background-color: var(--base00)"><code><span class="codeblock-line" data-line-number="1"><span style="color: var(--base0E)">import</span> <span style="color: var(--base0E)">type</span> { <span style="color: var(--base08)">ActionArgs</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-line-number="2"><span style="color: var(--base0E)">import</span> { <span style="color: var(--base08)">json</span>, <span style="color: var(--base08)">redirect</span> } <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/node</span>";
</span><span class="codeblock-line" data-line-number="3"><span style="color: var(--base0E)">import</span> {
</span><span class="codeblock-line" data-line-number="4"> <span style="color: var(--base08)">Form</span>,
</span><span class="codeblock-line" data-line-number="5"> <span style="color: var(--base08)">useActionData</span>,
</span><span class="codeblock-line" data-highlight="true" data-line-number="6"> <span style="color: var(--base08)">useTransition</span>,
</span><span class="codeblock-line" data-line-number="7">} <span style="color: var(--base0E)">from</span> "<span style="color: var(--base0B)">@remix-run/react</span>";
</span><span class="codeblock-line" data-line-number="8">
</span><span class="codeblock-line" data-line-number="9"><span style="color: var(--base03)">// ..</span>
</span><span class="codeblock-line" data-line-number="10">
</span><span class="codeblock-line" data-line-number="11"><span style="color: var(--base0E)">export</span> <span style="color: var(--base0E)">default</span> <span style="color: var(--base0D)">function</span> <span style="color: var(--base0D)">NewPost</span>() {
</span><span class="codeblock-line" data-line-number="12"> <span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">errors</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0D)">useActionData</span><<span style="color: var(--base0E)">typeof</span> <span style="color: var(--base08)">action</span>>();
</span><span class="codeblock-line" data-line-number="13">
</span><span class="codeblock-line" data-highlight="true" data-line-number="14"> <span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">transition</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0D)">useTransition</span>();
</span><span class="codeblock-line" data-highlight="true" data-line-number="15"> <span style="color: var(--base0A)">const</span> <span style="color: var(--base08)">isCreating</span> <span style="color: var(--base0E)">=</span> <span style="color: var(--base0A)">Boolean</span>(<span style="color: var(--base08)">transition</span>.<span style="color: var(--base08)">submission</span>);
</span><span class="codeblock-line" data-line-number="16">
</span><span class="codeblock-line" data-line-number="17"> <span style="color: var(--base0E)">return</span> (
</span><span class="codeblock-line" data-line-number="18"> <<span style="color: var(--base0A)">Form</span> <span style="color: var(--base0D)">method</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">post</span>">
</span><span class="codeblock-line" data-line-number="19"> <span style="color: var(--base0F)">{</span><span style="color: var(--base03)">/* ... */</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="20"> <<span style="color: var(--base08)">p</span> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">text-right</span>">
</span><span class="codeblock-line" data-line-number="21"> <<span style="color: var(--base08)">button</span>
</span><span class="codeblock-line" data-line-number="22"> <span style="color: var(--base0D)">type</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">submit</span>"
</span><span class="codeblock-line" data-line-number="23"> <span style="color: var(--base0D)">className</span><span style="color: var(--base0E)">=</span>"<span style="color: var(--base0B)">rounded bg-blue-500 py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400 disabled:bg-blue-300</span>"
</span><span class="codeblock-line" data-highlight="true" data-line-number="24"> <span style="color: var(--base0D)">disabled</span><span style="color: var(--base0E)">=</span><span style="color: var(--base0F)">{</span><span style="color: var(--base08)">isCreating</span><span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="25"> >
</span><span class="codeblock-line" data-highlight="true" data-line-number="26"> <span style="color: var(--base0F)">{</span><span style="color: var(--base08)">isCreating</span> <span style="color: var(--base0E)">?</span> "<span style="color: var(--base0B)">Creating...</span>" <span style="color: var(--base0E)">:</span> "<span style="color: var(--base0B)">Create Post</span>"<span style="color: var(--base0F)">}</span>
</span><span class="codeblock-line" data-line-number="27"> </<span style="color: var(--base08)">button</span>>
</span><span class="codeblock-line" data-line-number="28"> </<span style="color: var(--base08)">p</span>>
</span><span class="codeblock-line" data-line-number="29"> </<span style="color: var(--base0A)">Form</span>>
</span><span class="codeblock-line" data-line-number="30"> );
</span><span class="codeblock-line" data-line-number="31">}
</span></code></pre></div><p>Tada! You just implemented JavaScript-enabled progressive enhancement! 🥳 With what we've done, the experience is better than what the browser can do by itself. Lots of apps use JavaScript to <em>enable</em> the experience (and a select few actually do require JavaScript to work), but we've got a working experience as a baseline and just used JavaScript to <em>enhance</em> it.</p><h2 id="homework"><a href="https://remix.run/docs/en/v1/tutorials/blog#homework" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Homework</h2><p>That's it for today! Here are some bits of homework to implement if you wanna go deeper:</p><p><strong>Update/Delete posts:</strong> make an <code>/admin/$slug.tsx</code> page for your posts. This should open an edit page for the post that allows you to update the post or even delete it. The links are already there in the sidebar but they return 404! Create a new route that reads the posts, and puts them into the fields. All the code you need is already in <code>app/routes/posts/$slug.tsx</code> and <code>app/routes/posts/admin/new.tsx</code>. You just gotta put it together.</p><p><strong>Optimistic UI:</strong> You know how when you favorite a tweet, the heart goes red instantly and if the tweet is deleted it reverts back to empty? That's Optimistic UI: assume the request will succeed, and render what the user will see if it does. So your homework is to make it so when you hit "Create" it renders the post in the left nav and renders the "Create a New Post" link (or if you add update/delete do it for those too). You'll find this ends up being easier than you think even if it takes you a second to arrive there (and if you've implemented this pattern in the past, you'll find Remix makes this much easier). Learn more from <a href="https://remix.run/guides/optimistic-ui">the Optimistic UI guide</a>.</p><p><strong>Authenticated users only:</strong> Another cool bit of homework you could do is make it so only authenticated users can create posts. You've already got authentication all set up for you thanks to the Indie Stack. Tip, if you want to make it so you're the only one who can make posts, then simply check the user's email in your loaders and actions and if it's not yours redirect them <a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ">somewhere</a> 😈</p><p><strong>Customize the app:</strong> If you're happy with tailwind, keep it around, otherwise, check <a href="https://remix.run/docs/en/v1/guides/styling">the styling guide</a> to learn of other options. Remove the <code>Notes</code> model and routes, etc. Whatever you want to make this thing yours.</p><p><strong>Deploy the app:</strong> Check the README of your project. It has instructions you can follow to get your app deployed to Fly.io. Then you can actually start blogging!</p><p>We hope you love Remix! 💿 👋</p></div></div>
</div></div></div></div></div><script id="fathom-script" async="" data-site="IRVDGCHK" src="./Remix _ Blog Tutorial (short)_files/script.js.download" data-excluded-domains="localhost" data-spa="history"></script><script>window.__env = {"NODE_ENV":"production"};</script><script>
if (!window.history.state || !window.history.state.key) {
window.history.replaceState({ key: Math.random().toString(32).slice(2) }, null);
}
let STORAGE_KEY = "positions";
try {
let positions = JSON.parse(sessionStorage.getItem("positions") ?? '{}')
let storedY = positions[window.history.state.key];
if (typeof storedY === 'number') {
window.scrollTo(0, storedY)
}
} catch(error) {
console.error(error)
sessionStorage.removeItem(STORAGE_KEY)
}
</script><link rel="modulepreload" href="https://remix.run/build/_shared/chunk-AGZHEPHC.js"><link rel="modulepreload" href="https://remix.run/build/_shared/chunk-B5OVMZCJ.js"><link rel="modulepreload" href="https://remix.run/build/_shared/chunk-TOAMQMCD.js"><link rel="modulepreload" href="https://remix.run/build/_shared/chunk-LWYHHRIE.js"><link rel="modulepreload" href="https://remix.run/build/root-UPR33QGL.js"><link rel="modulepreload" href="https://remix.run/build/_shared/chunk-5Y7IMCQM.js"><link rel="modulepreload" href="https://remix.run/build/_shared/chunk-C6KB2UDE.js"><link rel="modulepreload" href="https://remix.run/build/_shared/chunk-T7BCCATT.js"><link rel="modulepreload" href="https://remix.run/build/routes/docs/$lang.$version-FU24IALM.js"><link rel="modulepreload" href="https://remix.run/build/_shared/chunk-CA3JSO4U.js"><link rel="modulepreload" href="https://remix.run/build/_shared/chunk-S6VOZMR7.js"><link rel="modulepreload" href="https://remix.run/build/_shared/chunk-GUUTIKDG.js"><link rel="modulepreload" href="https://remix.run/build/routes/docs/$lang.$version/$-7D7FOMSE.js">
<link rel="stylesheet" href="../../dist/main.css">
<script src="../../dist/bundle.js"></script>
</body></html>