XPine

This website runs on a custom full stack framework I created called XPine.

XPine combines JSX with Alpine.js for a simpler, easier development experience. It also includes a static site generator, hot module reload, SPA page navigation, page context, and more.

You can check out the XPine NPM package here.

Here's a breakdown of how XPine works.

Routing, page setup, and using Alpine.js

XPine uses page based routing. Render an HTML page using JSX components, for example the path /src/page/about.tsx will route to /about and /src/page/index.tsx will route to /

<span class="hljs-keyword">import</span> { <span class="hljs-title class_">WrapperProps</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;xpine/dist/types&#x27;</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title class_">Base</span> <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;../components/Base&#x27;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config = {
  <span class="hljs-title function_">data</span>(<span class="hljs-params"></span>) {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">title</span>: <span class="hljs-string">&#x27;Home page&#x27;</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">&#x27;The description&#x27;</span>,
    }
  },
  <span class="hljs-title function_">wrapper</span>(<span class="hljs-params">{ req, children, data, routePath }: <span class="hljs-title class_">WrapperProps</span></span>) {
    <span class="hljs-keyword">return</span> (
      <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">Base</span>
        <span class="hljs-attr">title</span>=<span class="hljs-string">{data?.title</span> || &#x27;<span class="hljs-attr">My</span> <span class="hljs-attr">awesome</span> <span class="hljs-attr">website</span>&#x27;}
        <span class="hljs-attr">description</span>=<span class="hljs-string">{data?.description}</span>
        <span class="hljs-attr">req</span>=<span class="hljs-string">{req}</span>
      &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Home page wrapper<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
        {children}
      <span class="hljs-tag">&lt;/<span class="hljs-name">Base</span>&gt;</span></span>
    )
  },
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">Home</span>(<span class="hljs-params"></span>) {
  <span class="hljs-keyword">return</span> (
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">x-data</span>=<span class="hljs-string">&quot;HomePageData&quot;</span> <span class="hljs-attr">x-on:click</span>=<span class="hljs-string">&quot;logMessage&quot;</span>&gt;</span>
      Hello world
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}

&lt;script /&gt;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">HomePageData</span>(<span class="hljs-params"></span>) {
  <span class="hljs-keyword">return</span> {
    <span class="hljs-title function_">logMessage</span>(<span class="hljs-params"></span>) {
      <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">&#x27;Hello world&#x27;</span>);
    }
  };
}
  • src/components/Base.tsx
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">JsxElement</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;typescript&#x27;</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">ServerRequest</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;xpine/dist/types&#x27;</span>;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">BaseProps</span> = {
  <span class="hljs-attr">head</span>?: <span class="hljs-title class_">JsxElement</span>;
  <span class="hljs-attr">title</span>: <span class="hljs-built_in">string</span>;
  <span class="hljs-attr">description</span>?: <span class="hljs-built_in">string</span>;
  <span class="hljs-attr">req</span>?: <span class="hljs-title class_">ServerRequest</span>;
  <span class="hljs-attr">children</span>?: <span class="hljs-title class_">JsxElement</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">Base</span>(<span class="hljs-params">{
  head,
  title,
  description,
  children,
}: <span class="hljs-title class_">BaseProps</span></span>) {
  <span class="hljs-keyword">return</span> (
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">&quot;en&quot;</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">&quot;UTF-8&quot;</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">&quot;viewport&quot;</span> <span class="hljs-attr">content</span>=<span class="hljs-string">&quot;width=device-width, initial-scale=1.0&quot;</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">&quot;robots&quot;</span> <span class="hljs-attr">content</span>=<span class="hljs-string">&quot;index,follow&quot;</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">&quot;description&quot;</span> <span class="hljs-attr">content</span>=<span class="hljs-string">{description</span> || `${<span class="hljs-attr">title</span>} <span class="hljs-attr">page</span>`} /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>{title || &#x27;My Website&#x27;}<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">&quot;stylesheet&quot;</span> <span class="hljs-attr">href</span>=<span class="hljs-string">&quot;/styles/global.css&quot;</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">defer</span> <span class="hljs-attr">src</span>=<span class="hljs-string">&quot;/scripts/app.js&quot;</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
        {head}
      <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">&quot;xpine-root&quot;</span>&gt;</span>
          {children}
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span></span>
  );
}

Dynamic routing

Create dynamic routes with paths similar to this /src/pages/[pathA]/[pathB]

Express API endpoints

Create regular Express routes by using .ts file extensions in the /src/pages folder. Specify the HTTP method by naming the file something like /src/pages/api/my-endpoint.POST.ts

<span class="hljs-keyword">import</span> { <span class="hljs-title class_">PageProps</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;xpine/dist/types&#x27;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">myEndpoint</span>(<span class="hljs-params">{ res }: <span class="hljs-title class_">PageProps</span></span>) {
  res.<span class="hljs-title function_">status</span>(<span class="hljs-number">200</span>).<span class="hljs-title function_">json</span>({
    <span class="hljs-attr">message</span>: <span class="hljs-string">&#x27;Hello World!&#x27;</span>,
  });
}

Static Site Generation

Generate path specific static pages by specifying in the config of either the page's file, such as /src/pages/about.tsx with a config export:

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config = {
  <span class="hljs-attr">staticPaths</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-title function_">data</span>(<span class="hljs-params"></span>) {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">title</span>: <span class="hljs-string">&#x27;My title&#x27;</span>
    }
  }
}

or a +config.ts file.

Configs

Configs can be nested. Create a +config.ts file in a directory and all subfolders will inherit that config unless overridden by their own +config.ts files. Want to apply static paths to an entire folder except for a single folder? In that folder you can create a +config.ts file like this:

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">staticPaths</span>: <span class="hljs-literal">false</span>,
}

Dynamic Static Pages

You can also create dynamic static pages by using a function in the staticPaths folder. For example, a directory named /src/[pathA]/[pathB]/[pathC]/[pathD].tsx might have a configuration file like this:

<span class="hljs-keyword">import</span> { <span class="hljs-title class_">ServerRequest</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;xpine/dist/types&#x27;</span>;
<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;axios&#x27;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config = {
  <span class="hljs-title function_">staticPaths</span>(<span class="hljs-params"></span>) {
    <span class="hljs-keyword">return</span> [
      {
        <span class="hljs-attr">pathA</span>: <span class="hljs-string">&#x27;my-path-a2&#x27;</span>,
        <span class="hljs-attr">pathB</span>: <span class="hljs-string">&#x27;my-path-b2&#x27;</span>,
        <span class="hljs-attr">pathC</span>: <span class="hljs-string">&#x27;my-path-c2&#x27;</span>,
        <span class="hljs-attr">pathD</span>: <span class="hljs-string">&#x27;2&#x27;</span>
      }
    ]
  },
  <span class="hljs-keyword">async</span> <span class="hljs-title function_">data</span>(<span class="hljs-params"><span class="hljs-attr">req</span>: <span class="hljs-title class_">ServerRequest</span></span>) {
    <span class="hljs-keyword">const</span> url = <span class="hljs-string">`https://jsonplaceholder.typicode.com/posts/<span class="hljs-subst">${req.params.pathD}</span>`</span>;
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> axios.<span class="hljs-title function_">get</span>(url);
      <span class="hljs-keyword">return</span> {
        ...data,
        ...req.<span class="hljs-property">params</span>,
      };
    } <span class="hljs-keyword">catch</span> (err) {
      <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">&#x27;could not fetch&#x27;</span>, url);
      <span class="hljs-keyword">return</span> {
        ...req.<span class="hljs-property">params</span>,
        <span class="hljs-attr">data</span>: {},
      }
    }
  }
}

Context

Create app context, useful for things like Navbars. In /src/context.tsx:

<span class="hljs-keyword">import</span> { createContext } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;xpine&#x27;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">NavbarContext</span>(<span class="hljs-params"></span>) {
  <span class="hljs-keyword">const</span> navbar = <span class="hljs-title function_">createContext</span>([]);
  <span class="hljs-keyword">return</span> {
    navbar,
  }
}

then in a page, say /src/pages/about.tsx, you can add to the NavbarContext like this:

<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">xpineOnLoad</span>(<span class="hljs-params"></span>) {
  context.<span class="hljs-title function_">addToArray</span>(<span class="hljs-string">&#x27;navbar&#x27;</span>, <span class="hljs-string">&#x27;My awesome context 1&#x27;</span>, <span class="hljs-number">2</span>);
  context.<span class="hljs-title function_">addToArray</span>(<span class="hljs-string">&#x27;navbar&#x27;</span>, <span class="hljs-string">&#x27;My awesome context 2&#x27;</span>, <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>(<span class="hljs-string">&#x27;January 11, 2024&#x27;</span>));
  context.<span class="hljs-title function_">addToArray</span>(<span class="hljs-string">&#x27;navbar&#x27;</span>, <span class="hljs-string">&#x27;My awesome context 3&#x27;</span>, <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>(<span class="hljs-string">&#x27;January 10, 2024&#x27;</span>));
  context.<span class="hljs-title function_">addToArray</span>(<span class="hljs-string">&#x27;navbar&#x27;</span>, <span class="hljs-string">&#x27;My awesome context 4&#x27;</span>, <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>(<span class="hljs-string">&#x27;January 30, 2024&#x27;</span>));
}

Context is sorted by array position then by date. You can then use context in your component like this:

<span class="hljs-keyword">import</span> { context } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;xpine&#x27;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">Navbar</span>(<span class="hljs-params"></span>) {
  <span class="hljs-keyword">const</span> navbar = context.<span class="hljs-title function_">get</span>(<span class="hljs-string">&#x27;navbar&#x27;</span>);
  <span class="hljs-keyword">return</span> (
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      {navbar.map(item =&gt; {
        return <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>{item}<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      })}
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}

XPine has many other features I made for my own use, such as the importEnv function to dynamically import environment variables with AWS Secrets Manager. Additionally, auth with JWT can be set up using the signUser and verifyUser functions.

One of my favorite utilities is the CustomEvent breakpoint-change, which can be listened to on the front end for when a breakpoint value changes.

Having fine grained control over how my code works and being able to transpile code into however I want allows me to create the framework I specifically want. I like how minimal yet powerful Alpine.js is and JSX is my favorite templating engine.