Deno, Fresh framework - html template manipulation

2.8k Views Asked by At

I was recently playing with new Fresh framework for deno and it was all great but at some point I realized that there is no possibility to add any additional meta data to page head tag. Basically I want to do 2 things:

  1. Add title tag
  2. Link some css file from my static dir

Do you have any idea how to achieve this? In the ideal world I would want the possibility to provide my own html template, or at least have some flexible way to manipulate provided fixed template. I did find some code snippet in Fresh source file which is basically before-mentioned fixed html template, but unfortunately it doesn't look customizable to me - only variable element would be opts.headComponents, but I'm unsure if I can affect it.

export interface TemplateOptions {
  bodyHtml: string;
  headComponents: ComponentChildren[];
  imports: (readonly [string, string])[];
  styles: string[];
  preloads: string[];
  lang: string;
}

export function template(opts: TemplateOptions): string {
  const page = (
    <html lang={opts.lang}>
      <head>
        <meta charSet="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        {opts.preloads.map((src) => <link rel="modulepreload" href={src} />)}
        {opts.imports.map(([src, nonce]) => (
          <script src={src} nonce={nonce} type="module"></script>
        ))}
        <style
          id="__FRSH_STYLE"
          dangerouslySetInnerHTML={{ __html: opts.styles.join("\n") }}
        />
        {opts.headComponents}
      </head>
      <body dangerouslySetInnerHTML={{ __html: opts.bodyHtml }} />
    </html>
  );

  return "<!DOCTYPE html>" + renderToString(page);
}
3

There are 3 best solutions below

2
On BEST ANSWER

I found the answer in some fresh issue. Edition of head and body tag can be done through JSX, just like this:

/** @jsx h */
import {h} from 'preact';
import {PageProps} from '$fresh/src/server/types.ts';
import {tw} from '@twind';

export const Head = () => (
    <head>
        <title>My title</title>
    </head>
);

export default function LoginPage(props: PageProps) {
    return (
        <div class={tw`w-screen h-screen flex justify-center bg-green-900`}>
            <Head />
        </div>
    )
}

Guess that way of doing things comes from preact or SSR? From my point of view it's a bit strange - injecting head/body into some divs, not really intuitive.

Edit

Found even better solution, because the answer presented earlier still caused meta tags to be put inside body tag.

We can use _app.tsx which is basically a wrapper for each rendered route and use special Head component from fresh library. And this is the correct way to populate opts.headComponents field from the fixed html template (presented in question). Unfortunately, there seems to be no documentation for that. Example taken from fresh tests:

/** @jsx h */
/** @jsxFrag Fragment */
import { Fragment, h } from "preact";
import { Head } from "$fresh/runtime.ts";
import { AppProps } from "$fresh/server.ts";

export default function App(props: AppProps) {
  return (
    <>
      <Head>
        <meta name="description" content="Hello world!" />
      </Head>
      <props.Component />
    </>
  );
}
0
On

You can import the Head component from fresh and whatever you use as the children will be appended to the <head> element of the page you return.

// HTML Head to import CSS and fonts
import { Head } from "$fresh/runtime.ts";

It's a bad practice to do this on _app.tsx because that runs before EVERY request and if you want to have good SEO, you will be setting title, description, and OG tags dynamically, which you only know after your request hits the route you are returning data from.

One option that is an excellent middle ground between both options is creating a DefaultHead component based on Fresh's Head, giving it whatever needs to be added to every request and then accepted as props.children whatever you want to set dynamically.

/components/DefaultHead.tsx

//? HTML Head to import CSS and fonts
import { Head } from "$fresh/runtime.ts";
import { toChildArray } from "preact";
import { JSX } from "preact/jsx-runtime";

//? Define required properties for this Head
interface HeadOptions {
  title: string;
  description: string;
  link?: string;
  children: JSX.Element | JSX.Element[];
}

//? Creates and exports the Head to be used on all pages
export function DefaultHead(options: HeadOptions) {
  return (
    <>
      <Head>
        <title>{options.title}</title>
        <meta property="og:title" content={options.title} />
        <meta property="og:site_name" content={your-website-name-here} />
        <meta property="og:description" content={options.description} />
        <meta property="description" content={options.description} />
        <meta property="og:type" content="blog" />
        <meta property="og:image" content={/path/to/your-website-image-link-for-social-media-here.png}
        <meta
          property="og:url"
          content={options.link}
        />
        // You can also import here fonts that are used throughout all the website
        <link rel="stylesheet" href="/base.css" /> // Default CSS file that has base styles for every page (example: page font-family, background-color, etc)
        {...toChildArray(options.children)} //
      </Head>
    </>
  );
}

For ease of use, this code requires importing toChildArray so that the props.children is always an array. If you don't want to import this, you will need to manually check it within the <Head>, or just make sure you are always either passing a single JSX.Element or an array of them.

Then you can import this DefaultHead on every route and dynamically pass in the properties you want to set:

/routes/literally-any-route.tsx

//? Import DefaultHead with default metadata
import { DefaultHead } from "../components/DefaultHead.tsx";

export default function Home() {
  return (
    <>
      <DefaultHead
        title='page title you want to set'
        description="page description you want to set"
        link="page link you want to show on social media"
      >
        // put here all the elements you want to add to this <DefaultHead>
        <link rel="stylesheet" href="/page.css" />
        <link rel="stylesheet" href="/another-page.css" />
      </DefaultHead>
      // everything else for the body of your response goes here...
    </>
  );
}
2
On

You can use the edited answer by @Harrel. However, if you wish to add dynamic content to <head> on different pages, like <title>, you can use the <Head> component in individual pages as well. The children of <Head> component will stack because that component is implemented as a context as we can see from the source code

enter image description here

You can put <title> in each page like this

enter image description here

and meta tags and styleshees in _app.tsx like this

enter image description here

Hope this helps someone.