References using keyOf

Using keyOf for references

What is keyOf?

The `s.keyOf()` schema type creates a reference to keys from a another Val module that is defined using `s.record()`. This enables safe references between your content, ensuring that links and references always point to existing content.

Setting up referenceable content

In order to refer to content using keyOf, you must first have a `s.record` to which you want to refer.

In the example below, we have defined a module of all authors by email.

// authors.val.ts
const authorsSchema = s.record(
  s.object({
    name: s.string(),
  })
);

export default c.define("/content/authors.val.ts", authorsSchema, {
  "freekh@val.build": { name: "Freddy" },
  "theo@val.build": { name: "Theddy"  },
});

Refering to content using keyOf

When you have a record of keys that you want to refer to, use s.keyOf. This will make it possible for content editors to select from a list of possible options, the keys of the record. It will also make it possible for Val to validate the reference.

import authorsVal from "./authors.val";

const pageSchema = s.object({
  title: s.string(),
  author: s.keyOf(authorsVal), // References authors
});

export default c.define("/content/page.val.ts", pageSchema, {
  title: "Welcome",
  author: "fre", // this will be validated by Val
});

Recursive keyOf

When using keyOf, you might end up in a situation where you have mutally recursive schemas. A consequence of this is that typescript will no longer be able to infer the types of the modules that are mutally recursive.

This can occour when you use the page router.

To avoid this, you can use the "getter syntax" on all keyOf related types.

import { s } from "@/val.config";
import { DecodeVal, Schema, t } from "@valbuild/next";
import blogsPageVal from "./blogs/[slug]/page.val";
import supportArticlePageVal from "./support/[slug]/page.val";

export const linkButtonSchema = s.object({
  label: s.string(),
  link: s.union(
    "type",
    s.object({
      type: s.literal("support-article"),
      get href(): Schema<string> {
        return s.keyOf(supportArticlePageVal);
      },
    }),
    s.object({
      type: s.literal("blog"),
      get href(): Schema<string> {
        return s.keyOf(blogsPageVal);
      },
    }),
  ),
});

export type LinkButton = DecodeVal<t.inferSchema<typeof linkButtonSchema>>;

By using the the "getter syntax" you are now able to use the linkButton in the blogs / support article pages.

import { s, c, nextAppRouter } from "@/val.config";
import { LinkButton, linkButtonSchema } from "../../linkButton.val";
import { Schema } from "@valbuild/next";

const blogSchema = s.object({
  // ...
  get cta(): Schema<LinkButton> {
    return linkButtonSchema;
  },
});

export default c.define(
  "/app/(main)/blogs/[slug]/page.val.ts",
  s.record(blogSchema).router(nextAppRouter),
  {
    "/blogs/my-blog": {
      cta: {
        label: "My blog",
        link: {
          type: "blog",
          href: "/blogs/my-blog",
        },
      },
    },
  },
);