) {
- return (
-
- I like {loaderData}. // I like baseball.
-
- )
-}
-` }), _jsx(Paragraph, { children: "If you return a Response object, then that will \"take over\" and be returned from the route instead of your page. This is fine if you're creating a \"resource route\" (more on this below), but usually it's not what you want." }), _jsx(Paragraph, { children: "However, one thing that is more common is that you may want to return a redirect from a loader. You can do this with a Response object if you want, but Hwy exports a helper function that covers more edge cases and is built to work nicely with the HTMX request lifecycle." }), _jsx(CodeBlock, { language: "typescript", code: `
-import { redirect, type DataFunctionArgs } from 'hwy'
-
-export function loader({ c }: DataFunctionArgs) {
- return redirect({ c, to: '/login' })
-}
-` }), _jsxs(Paragraph, { children: ["You can also \"throw\" a ", _jsx(InlineCode, { children: "redirect" }), " if you want, which can be helpful in keeping your typescript types clean."] }), _jsx(AnchorHeading, { content: "Server components" }), _jsx(Paragraph, { children: "In addition to using loaders to load data in parallel before rendering any components, you can also load data inside your Hwy page components. Be careful with this, as it can introduce waterfalls, but if you are doing low-latency data fetching and prefer that pattern, it's available to you in Hwy." }), _jsx(CodeBlock, { language: "typescript", code: `
-// src/some-page.page.tsx
-
-export default async function ({ outlet }: PageProps) {
- const someData = await getSomeData()
-
- return (
-
- {JSON.stringify(someData)}
-
- {await outlet()}
-
- )
-}
- ` }), _jsx(Paragraph, { children: "You can also pass data to the child outlet if you want, and it will be available in the child page component's props. Here's how that would look in the parent page component:" }), _jsx(CodeBlock, { language: "typescript", code: `{await outlet({ someData })}` }), _jsxs(Paragraph, { children: ["And in the child component, you'll want to use", " ", _jsx(InlineCode, { children: `PageProps & { someData: SomeType }` }), " as your prop type."] }), _jsxs(Paragraph, { children: ["Because page components are async and let you fetch data inside them, be sure to always await your ", _jsx(InlineCode, { children: "outlet" }), " calls, like this: ", _jsx(InlineCode, { children: `{await outlet()}` }), ". If you don't, things might not render correctly."] }), _jsxs(Paragraph, { children: ["Another way of doing this would be to use Hono's", " ", _jsx(InlineCode, { children: "c.set('some-key', someData)" }), " feature. If you do that, any child component will be able to access the data without re-fetching via ", _jsx(InlineCode, { children: "c.get('some-key')" }), "."] }), _jsx(Paragraph, { children: "The world is your oyster!" }), _jsx(AnchorHeading, { content: "Page actions" }), _jsx(Paragraph, { children: "Page actions behave just like loaders, except they don't run until you call them (usually from a form submission). Loaders are for loading/fetching data, and actions are for mutations. Use actions when you want to log users in, mutate data in your database, etc." }), _jsxs(Paragraph, { children: ["Data returned from an action will be passed to the page component as the", " ", _jsx(InlineCode, { children: "actionData" }), " property on the", " ", _jsx(InlineCode, { children: "PageProps" }), " object. Unlike loaders, which are designed to run in parallel and pass different data to each nested component, actions are called individually and return the same", " ", _jsx(InlineCode, { children: "actionData" }), " to all nested components."] }), _jsx(Paragraph, { children: "Here is an example page with a login form. Note that this is highly simplified and not intended to be used in production. It is only intended to show how actions work." }), _jsx(CodeBlock, { language: "typescript", code: `
-import { DataFunctionArgs, PageProps } from 'hwy'
-import { extractFormData, logUserIn } from './pretend-lib.js'
-
-export async function action({ c }: DataFunctionArgs) {
- const { email, password } = await extractFormData({ c })
- return await logUserIn({ email, password })
-}
-
-export default function ({ actionData }: PageProps) {
- if (actionData?.success) return Success!
-
- return (
-
- )
-}
- ` }), _jsxs(Paragraph, { children: ["This form uses 100% standard html attributes, and it will be automatically progressively enhanced by HTMX (uses the", " ", _jsx(InlineCode, { children: "hx-boost" }), " feature). If JavaScript doesn't load for some reason, it will fall back to traditional web behavior (full-page reload)."] }), _jsx(AnchorHeading, { content: "Resource routes" }), _jsx(Paragraph, { children: "Remix has the concept of \"resource routes\", where you can define loaders and actions without defining a default export component, and then use them to build a view-less API." }), _jsxs(Paragraph, { children: ["In Hwy, you're probably better off just using your Hono server directly for this, as it's arguably more traditional, straightforward, and convenient. However, if you really want to use resource routes with Hwy's file-based routing, nothing is stopping you! You can do so by just making sure you return a fetch ", _jsx(InlineCode, { children: "Response" }), " object from your loader or action. For example:"] }), _jsx(CodeBlock, { language: "typescript", code: `
-// src/pages/api/example-resource-root.ts
-
-export function loader() {
- return new Response('Hello from resource route!')
-}
-` }), _jsxs(Paragraph, { children: ["All of that being said, Hwy is designed to work with HTMX-style hypermedia APIs, not JSON APIs. So if you return JSON from a resource route or a normal Hono endpoint, you'll be in charge of handling that on the client side. This will likely entail disabling HTMX on the form submission, doing an ", _jsx(InlineCode, { children: "e.preventDefault()" }), " in the form's onsubmit handler, and then doing a standard fetch request to the Hono endpoint. You can then parse the JSON response and do whatever you want with it."] }), _jsx(Paragraph, { children: "You probably don't need to do this, and if you think you do, I would challenge you to try using the hypermedia approach instead. If you still decide you need to use JSON, this is roughly the escape hatch." }), _jsx(AnchorHeading, { content: "Error boundaries" }), _jsxs(Paragraph, { children: ["Any Hwy page can export an ", _jsx(InlineCode, { children: "ErrorBoundary" }), " ", "component, which takes the same parameters as", " ", _jsx(InlineCode, { children: "loaders" }), " and ", _jsx(InlineCode, { children: "actions" }), ", as well as the error itself. The type for the ErrorBoundary component props is exported as ", _jsx(InlineCode, { children: "ErrorBoundaryProps" }), ". If an error is thrown in the page or any of its children, the error will be caught and passed to the nearest applicable parent error boundary component. You can also pass a default error boundary component that effectively wraps your outermost ", _jsx(InlineCode, { children: "rootOutlet" }), " (in", " ", _jsx(InlineCode, { children: "main.tsx" }), ") like so:"] }), _jsx(CodeBlock, { language: "typescript", code: `
-import type { ErrorBoundaryProps } from 'hwy'
-
-...
-
-{await rootOutlet({
- activePathData,
- c,
- fallbackErrorBoundary: (props: ErrorBoundaryProps) => {
- return {props.error.message}
- },
-})}
- ` }), _jsx(AnchorHeading, { content: "Hono middleware and variables" }), _jsx(Paragraph, { children: "You will very likely find yourself in a situation where there is some data you'd like to fetch before you even run your loaders, and you'd like that data to be available in all downstream loaders and page components. Here's how you might do it:" }), _jsx(CodeBlock, { language: "typescript", code: `
-app.use('*', async (c, next) => {
- const user = await getUserFromCtx(c)
-
- c.set('user', user)
-
- await next()
-})
-` }), _jsx(Paragraph, { children: "This isn't very typesafe though, so you'll want to make sure you create app-specific types. For this reason, all Hwy types that include the Hono Context object are generic, and you can pass your app-specific Hono Env type as a generic to the PageProps object. For example:" }), _jsx(CodeBlock, { language: "typescript", code: `
-import type { DataFunctionArgs } from 'hwy'
-
-type AppEnv = {
- Variables: {
- user: Awaited>
- }
-}
-
-type AppDataFunctionArgs = DataFunctionArgs
-
-export async function loader({ c }: AppDataFunctionArgs) {
- // this will be type safe!
- const user = c.get('user')
-}
-` }), _jsx(AnchorHeading, { content: "Main.tsx" }), _jsxs(Paragraph, { children: ["In your ", _jsx(InlineCode, { children: "main.tsx" }), " file, you'll have a handler that looks something like this."] }), _jsx(CodeBlock, { language: "typescript", code: `
-import {
- CssImports,
- rootOutlet,
- DevLiveRefreshScript,
- ClientScripts,
- getDefaultBodyProps,
- renderRoot,
-} from 'hwy'
-
-app.all('*', async (c, next) => {
- return await renderRoot(c, next, async ({ activePathData }) => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- {await rootOutlet({
- c,
- activePathData,
- fallbackErrorBoundary: () => {
- return Something went wrong!
- },
- })}
-
-
- )
- }
-})
- ` }), _jsxs(Paragraph, { children: ["The easiest way to get this set up correctly is to bootstrap your app with ", _jsx(InlineCode, { children: "npx create-hwy@latest" }), "."] }), _jsx(AnchorHeading, { content: "Document head (page metadata)" }), _jsxs(Paragraph, { children: ["Your document's ", _jsx(InlineCode, { children: "head" }), " is rendered via the", " ", _jsx(InlineCode, { children: `HeadElements` }), " component in your", " ", _jsx(InlineCode, { children: "main.tsx" }), " file, like this:"] }), _jsx(CodeBlock, { language: "typescript", code: `
-
- ` }), _jsx(Paragraph, { children: "As you can probably see, the \"defaults\" property takes an array of head elements. \"Title\" is a special one, in that it is just an object with a title key. Other elements are just objects with a tag and props key. The props key is an object of key-value pairs that will be spread onto the element." }), _jsxs(Paragraph, { children: ["The defaults you set here can be overridden at any Hwy page component by exporting a ", _jsx(InlineCode, { children: "head" }), " function. For example:"] }), _jsx(CodeBlock, { language: "typescript", code: `
-import { HeadFunction } from 'hwy'
-
-export const head: HeadFunction = (props) => {
- // props are the same as PageProps, but without the outlet
-
- return [
- { title: 'Some Child Page' },
- {
- tag: 'meta',
- props: {
- name: 'description',
- content:
- 'Description for some child page.',
- },
- },
- ]
-}
-` }), _jsxs(Paragraph, { children: ["This will override any conflicting head elements set either by an ancestor page component or by the root defaults. The", " ", _jsx(InlineCode, { children: "head" }), " function is passed all the same props as a page component, excluding ", _jsx(InlineCode, { children: "outlet" }), "."] }), _jsx(AnchorHeading, { content: "Styling" }), _jsxs(Paragraph, { children: ["Hwy includes built-in support for several CSS patterns, including a very convenient way to inline critical CSS. CSS is rendered into your app through the ", _jsx(InlineCode, { children: "CssImports" }), " component. That component in turn reads from the ", _jsx(InlineCode, { children: "src/styles" }), " ", "directory, which is where you should put your CSS files. Inside the styles directory, you can put two types of CSS files:", " ", _jsx(InlineCode, { children: "critical" }), " and ", _jsx(InlineCode, { children: "bundle" }), ". Any files that include ", _jsx(InlineCode, { children: ".critical." }), " in the filename will be concatenated (sorted alphabetically by file name), processed by esbuild, and inserted inline into the", " ", _jsx(InlineCode, { children: "head" }), " of your document. Any files that include", " ", _jsx(InlineCode, { children: ".bundle." }), " in the filename will similarly be concatenated (sorted alphabetically by file name), processed by esbuild, and inserted as a standard linked stylesheet in the", " ", _jsx(InlineCode, { children: "head" }), " of your document."] }), _jsxs(Paragraph, { children: ["It's also very easy to configure Tailwind, if that's your thing. To see how this works, spin up a new project with", " ", _jsx(InlineCode, { children: "npx create-hwy@latest" }), " and select \"Tailwind\" when prompted."] }), _jsx(Paragraph, { children: "And of course, if you don't like these patterns, you can just choose not to use them, and do whatever you want for styling instead!" }), _jsx(AnchorHeading, { content: "Deployment" }), _jsxs(Paragraph, { children: ["Hwy can be deployed to any Node-compatible runtime with filesystem read access. This includes more traditional Node app hosting like Render.com or Railway.app, or Vercel (Lambda), or Deno Deploy. This should also include Bun once that ecosystem becomes more stable and has more hosting options. Just choose your preferred deployment target when you run", " ", _jsx(InlineCode, { children: "npx create-hwy@latest" }), "."] }), _jsx(Paragraph, { children: "Cloudflare is a bit trickier, however, because Hwy reads from the filesystem at runtime. We may add support for this in the future through a specialized build step, but for now, it's not supported. This also means that Vercel edge functions are not supported, as they rely on Cloudflare Workers, which do not have runtime read access to the filesystem. Normal Vercel serverless, which runs on AWS Lambda under the hood, will work just fine." }), _jsx(AnchorHeading, { content: "Progressive enhancement" }), _jsxs(Paragraph, { children: ["When you included the ", _jsx(InlineCode, { children: "hx-boost" }), " attribute on the", " ", _jsx(InlineCode, { children: "body" }), " tag (included by default when you use", " ", _jsx(InlineCode, { children: "getDefaultBodyProps" }), "), anchor tags (links) and form submissions will be automatically progressively enhanced. For forms, include the traditional attributes, like this:", _jsx(CodeBlock, { language: "typescript", code: `