Skip to content

Commit a3e7b1c

Browse files
committed
feat: added head component
1 parent e88471c commit a3e7b1c

32 files changed

Lines changed: 323 additions & 200 deletions
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
title: How to create a head component?
3+
---
4+
5+
This page shows you how to create your own [head component](../reference/head-component.md)
6+
so that you can set any metadata on your pages.
7+
8+
## Steps
9+
10+
### Create a head component
11+
12+
A head is a component that:
13+
14+
* accept a layoutProps
15+
* and return a Head child component
16+
17+
Example of minimal implementation:
18+
19+
```tsx
20+
export default function MetaRobots({page}: LayoutProps) {
21+
let robots = page?.frontmatter?.robots;
22+
return (
23+
<meta name="robots" content={robots ? robots : "index, follow"}/>
24+
)
25+
}
26+
```
27+
28+
### Register it
29+
30+
Interact expects all head files to be stored in the [heads directory](../reference/directory-layout.md) (
31+
default to `src/components/heads`)
32+
as `jsx` or `tsx` files
33+
34+
* Save your head component at: `src/components/heads/MetaRobots.tsx`
35+
* Restart Interact and you should see your `Head` component in the HTML page

apps/site/pages/reference/directory-layout.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ A typical interact project is composed of the following paths:
1111
| [runtime](runtime-directory.md) | `.interact` | A directory that contains temporary runtime information such as cache, schema |
1212
| [build](build.md) | `dist` | A directory where the [build result](build.md) is stored (static website and handler) |
1313
| [config file](conf.md) | `interact.config.json` | the [configuration file](conf.md) |
14+
| [heads](head-component.md) | `src/components/heads` | a directory that contains your [head components](head-component.md) |
1415
| `images` | `src/images` | a directory that contains [raster image](../components/raster.md) or [Svg](../components/svg.md) |
1516
| [markdown components](markdown-component.md) | `src/components/markdown` | a directory that contains your custom markdown component |
1617
| [layouts](layout.md) | `src/components/layouts` | a directory that contains your custom [layouts](layout.md) |
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
title: Head component
3+
description: A head component is a component that is rendered in the HTML HEAD.
4+
---
5+
6+
A `head` is a [component](component.md) that is rendered in the [HTML Head node](head.md).
7+
8+
## How to create a Head component
9+
10+
See [How to create a head component](../howto/add-a-head.md)
11+
12+
## Not a Client Component Restriction
13+
14+
Head component cannot be [client component](../reference/rsc.md#client-component)
15+
otherwise you get the [fatal rsc error](../reference/rsc.md#only-plain-objects-error)
16+
17+
## Default
18+
19+
The default one can be seen in the [main/src/resources/components/heads directory](https://github.com/combostrap/interact/tree/main/src/resources/components/heads)
20+
21+
If you want to override the default ones, you should create a head with the same name and store it at:
22+
`@/components/heads`.
23+
24+
They:
25+
26+
* are and should be [server component](rsc.md)
27+
* accepts the layout props (page and context)

apps/site/pages/reference/head.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
---
2-
title: Head component
2+
title: How to add a Head element
3+
description: How to add a Head element in Interact
34
---
45

5-
## Features
6+
A `head element` is a HTML element that is rendered in
7+
the [HTML Head node](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/head).
8+
9+
## How to add a Head element
10+
11+
### By creating a Head React Component
12+
13+
You can create a specialized component called a [head component](head-component.md)
14+
that are rendered into the `head` html element.
615

716
### Hoisting
817

9-
We support head elements hoisting.
18+
We support also head elements hoisting.
1019

11-
If your component returns elements with type (`meta`,`script`, `link`, `style`), they
12-
will be moved into the `head` html element.
20+
If your [page](page.md) returns elements with type (`meta`,`script`, `link`, `style`),
21+
they are moved into the `head` html element.
1322

apps/site/pages/reference/layout.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,4 @@ If you want to override the default ones, you should create a partial with the s
6565
They:
6666

6767
* are and should be [server component](rsc.md)
68-
* accepts the context props (page and request)
68+
* accepts the layout props (page and context)

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"./types": "./src/types/index.d.ts",
3030
"./components/layouts/*": "./src/resources/components/layouts/*.tsx",
3131
"./components/partials/*": "./src/resources/components/partials/*.tsx",
32+
"./components/heads/*": "./src/resources/components/heads/*.tsx",
3233
"./components/pages/*": "./src/resources/components/pages/*.tsx",
3334
"./components/*": "./src/resources/components/interact/*.tsx",
3435
"./vite/*": "./dist/interact/vite/*.js",

src/interact/cli/shared/vite.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import Inspect from 'vite-plugin-inspect'
1818
import outlineNumberingStyleSheet from "../../vite/outlineNumberingStylesheet.js";
1919
import middlewareProvider from "../../vite/middlewareProvider.js";
2020
import layoutProvider from "../../vite/layoutProvider.js";
21+
import headProvider from "../../vite/headProvider.js";
2122
import tailwindcss from "@tailwindcss/vite"
2223
import globalStylesheet from "../../vite/globalStylesheet.js";
2324
import {setGlobalsConf} from "../../vite/globalConf.js";
@@ -302,6 +303,8 @@ export async function resolveViteConfig(
302303
mdxRollup({command}),
303304
// Component provider (provide the MdxComponent for mdx)
304305
mdxComponentProvider(),
306+
// Head provider (provide the HTML head dynamically)
307+
headProvider(),
305308
// Layout provider (provide the layouts dynamically)
306309
layoutProvider(),
307310
// Pages (after layout)

src/interact/config/configSchema.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ const PathsSchema = z.object({
9797
// https://vite.dev/guide/assets#the-public-directory
9898
publicDirectory: z.coerce.string<string>().describe("The path of the public directory").default("public"),
9999
layoutsDirectory: z.coerce.string<string>().describe("The path of the layout directory").default("src/components/layouts"),
100+
headsDirectory: z.coerce.string<string>().describe("The path of the head directory").default("src/components/heads"),
100101
mdComponentsDirectory: z.coerce.string<string>().describe("The path of the markdown components directory").default("src/components/markdowns"),
101102
middlewaresDirectory: z.coerce.string<string>().describe("The path of the middlewares directory").default("src/middlewares"),
102103
configDirectory: z.coerce.string<string>().describe("The path of the config directory").default("config"),
@@ -172,7 +173,7 @@ const ComponentSchema = z.object({
172173
// No file system path, it's derived thanks to import, and it does not work well with vite and import
173174
// as they don't handle symlink well
174175
importPath: z.coerce.string<string>().optional(),
175-
type: z.enum(["layout", "markdown"]),
176+
type: z.enum(["layout", "markdown", "head"]),
176177
props: z.record(z.string(), z.unknown()).optional(),
177178
});
178179
let ComponentsConfigSetSchema = z.record(

src/interact/config/interactConfigHandler.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ class InteractConfigHandler {
219219
pagesDirectory: this.#qualifiedDirectoryPath(rootDirectory, finalConfigData.paths.pagesDirectory),
220220
publicDirectory: this.#qualifiedDirectoryPath(rootDirectory, finalConfigData.paths.publicDirectory),
221221
imagesDirectory: this.#qualifiedDirectoryPath(rootDirectory, finalConfigData.paths.imagesDirectory),
222+
headsDirectory: this.#qualifiedDirectoryPath(rootDirectory, finalConfigData.paths.headsDirectory),
222223
layoutsDirectory: this.#qualifiedDirectoryPath(rootDirectory, finalConfigData.paths.layoutsDirectory),
223224
mdComponentsDirectory: this.#qualifiedDirectoryPath(rootDirectory, finalConfigData.paths.mdComponentsDirectory),
224225
middlewaresDirectory: this.#qualifiedDirectoryPath(rootDirectory, finalConfigData.paths.middlewaresDirectory),
@@ -254,14 +255,23 @@ class InteractConfigHandler {
254255
* (partials are not needed as the user import them from layout component)
255256
* Ie: import Header from "@/components/partials/Header";
256257
*/
257-
const types: ('layout' | 'markdown')[] = ['layout', 'markdown'];
258+
const types: ('layout' | 'markdown' | 'head')[] = ['layout', 'markdown', 'head'];
258259
for (const type of types) {
259260
let projectPath;
260-
if (type === 'layout') {
261-
projectPath = finalConfigData.paths.layoutsDirectory
262-
} else {
263-
projectPath = finalConfigData.paths.mdComponentsDirectory
261+
switch (type) {
262+
case 'layout':
263+
projectPath = finalConfigData.paths.layoutsDirectory
264+
break;
265+
case 'markdown':
266+
projectPath = finalConfigData.paths.mdComponentsDirectory
267+
break;
268+
case "head":
269+
projectPath = finalConfigData.paths.headsDirectory;
270+
break;
271+
default:
272+
throw new Error(`Internal Unknown type '${type}'`);
264273
}
274+
265275
const layoutDirectories = [`${finalConfigData.paths.interactResourcesDirectory}/components/${type}s`, projectPath];
266276

267277
for (const [i, layoutDirectory] of layoutDirectories.entries()) {
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import type {InteractConfig} from "../config/interactConfig.js";
2+
import path from "path";
3+
4+
/**
5+
* This function was created to print without any quote
6+
* so that an object can be added to JavaScript virtual module
7+
* It works for object but also for any other basic type
8+
*/
9+
export function toJsString(value: any, indent = 0): any {
10+
const pad = ' '.repeat(indent + 2);
11+
const closePad = ' '.repeat(indent);
12+
13+
if (value === null) return 'null';
14+
if (value === undefined) return 'undefined';
15+
if (typeof value === 'string') return value; // no quotes
16+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
17+
if (Array.isArray(value)) {
18+
// @ts-ignore
19+
const items = value.map(v => `${pad}${toJsString(v, indent + 2)}`);
20+
return `[\n${items.join(',\n')}\n${closePad}]`;
21+
}
22+
if (typeof value === 'object') {
23+
24+
// @ts-ignore
25+
const entries = Object.entries(value).map(
26+
// @ts-ignore
27+
([k, v]) => {
28+
/**
29+
* The key value may be my-component
30+
* The Javascript string result should be then 'my-component'
31+
*/
32+
if (k.includes("-")) {
33+
k = `'${k}'`
34+
}
35+
return `${pad}${k}: ${toJsString(v, indent + 2)}`
36+
}
37+
);
38+
return `{\n${entries.join(',\n')}\n${closePad}}`;
39+
}
40+
return String(value);
41+
}
42+
43+
44+
export function getComponentsAndImportArray(interactConfig: InteractConfig, type: "layout" | "markdown" | "head") {
45+
let imports = [];
46+
let components: Record<string, string> = {};
47+
48+
// component may be registered multiple time
49+
// for instance, code is registered for the pre element and itself as Code,
50+
// but it should be exported only once
51+
let exports = new Set<string>();
52+
for (const [key, value] of Object.entries(interactConfig.components)) {
53+
54+
/**
55+
* Head
56+
*/
57+
if (value.type != type) {
58+
continue;
59+
}
60+
61+
/**
62+
* Import Path
63+
*/
64+
let importPath = value.importPath;
65+
if (importPath == null) {
66+
throw new Error(`Import ${importPath} not defined for the layout component ${key}`);
67+
}
68+
69+
/**
70+
* Import name
71+
* Cannot come from the path "./pages/404.js",404 is a number and is not valid as component name but valid as path
72+
*/
73+
let importName = key;
74+
75+
/**
76+
* Map
77+
*/
78+
let layoutKey = key.toLowerCase();
79+
components[layoutKey] = importName;
80+
81+
if (!exports.has(importName)) {
82+
83+
/**
84+
* We export all component so that they can be used
85+
*/
86+
exports.add(importName);
87+
88+
/**
89+
* Relative to Absolute
90+
*/
91+
if (importPath.startsWith("./")) {
92+
importPath = path.resolve(interactConfig.paths.rootDirectory, importPath);
93+
}
94+
95+
/**
96+
* Import statement
97+
*/
98+
imports.push(`import ${importName} from ${JSON.stringify(importPath)};`);
99+
100+
}
101+
102+
103+
}
104+
return {
105+
components: components,
106+
imports: imports,
107+
exports: exports
108+
}
109+
}

0 commit comments

Comments
 (0)