mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
Add support for meta tags (#14)
* feat: added support for head meta tags * doc: update tutorial * feat: add support to no css manifest entries * feat: update version to v0.5.0 * doc: add mention to add the imports field * feat: add filter to workspace package.json * fix: add types declarations for react-meta-tags
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono"
|
||||
version = "0.4.6"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "The react/rust fullstack framework"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono_lib"
|
||||
version = "0.4.6"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "The react/rust fullstack framework"
|
||||
@@ -32,7 +32,7 @@ regex = "1.10.5"
|
||||
either = "1.13.0"
|
||||
tower-http = {version = "0.5.2", features = ["fs"]}
|
||||
|
||||
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.4.6"}
|
||||
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.5.0"}
|
||||
# Match the same version used by axum
|
||||
tokio-tungstenite = "0.21.0"
|
||||
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
|
||||
|
||||
@@ -7,12 +7,19 @@ use std::path::PathBuf;
|
||||
|
||||
const VITE_MANIFEST_PATH: &str = "./out/client/.vite/manifest.json";
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BundleInfo {
|
||||
/// TODO: Add also the import field and load the dynamic
|
||||
/// values in the payload bundles.
|
||||
pub file: String,
|
||||
#[serde(default = "default_css_vector")]
|
||||
pub css: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_css_vector() -> Vec<String> {
|
||||
Vec::with_capacity(0)
|
||||
}
|
||||
|
||||
/// Manifest is the mapping between the vite output bundled files
|
||||
/// and the originals.
|
||||
/// Vite doc: https://vitejs.dev/config/build-options.html#build-manifest
|
||||
@@ -45,6 +52,73 @@ fn remap_manifest_keys(manifest: HashMap<String, BundleInfo>) -> HashMap<String,
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn correctly_parse_the_manifest_json() {
|
||||
let manifest_example = r#"{
|
||||
"../src/routes/index.tsx": {
|
||||
"file": "assets/index.js",
|
||||
"name": "index",
|
||||
"src": "../src/routes/index.tsx",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"client-main.tsx"
|
||||
],
|
||||
"css": [
|
||||
"assets/index.css"
|
||||
]
|
||||
},
|
||||
"meta-tags-lib": {
|
||||
"file": "assets/meta-lib.js",
|
||||
"name": "meta-tags-lib",
|
||||
"imports": [
|
||||
"client-main.tsx"
|
||||
]
|
||||
},
|
||||
"client-main.tsx": {
|
||||
"file": "assets/client-main.js",
|
||||
"name": "client-main",
|
||||
"src": "client-main.tsx",
|
||||
"isEntry": true,
|
||||
"dynamicImports": [
|
||||
"../src/routes/index.tsx",
|
||||
"../src/routes/pokemons/[pokemon].tsx"
|
||||
],
|
||||
"css": [
|
||||
"assets/client-main.css"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let parsed_manifest =
|
||||
serde_json::from_str::<HashMap<String, BundleInfo>>(manifest_example).unwrap();
|
||||
|
||||
let mut result = HashMap::new();
|
||||
result.insert(
|
||||
"../src/routes/index.tsx".to_string(),
|
||||
BundleInfo {
|
||||
file: "assets/index.js".to_string(),
|
||||
css: vec!["assets/index.css".to_string()],
|
||||
},
|
||||
);
|
||||
result.insert(
|
||||
"client-main.tsx".to_string(),
|
||||
BundleInfo {
|
||||
file: "assets/client-main.js".to_string(),
|
||||
css: vec!["assets/client-main.css".to_string()],
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(
|
||||
"meta-tags-lib".to_string(),
|
||||
BundleInfo {
|
||||
file: "assets/meta-lib.js".to_string(),
|
||||
css: Vec::new(),
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(parsed_manifest, result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_correctly_remap_the_manifest() {
|
||||
let mut parsed_manifest: HashMap<String, BundleInfo> = HashMap::new();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono_lib_macros"
|
||||
version = "0.4.6"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
description = "The react/rust fullstack framework"
|
||||
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
|
||||
|
||||
@@ -23,6 +23,7 @@ Typescript and Rust knowledge is not a requirement though!
|
||||
* [Create a stand-alone component](#create-a-stand-alone-component)
|
||||
* [Create the /pokemons/[pokemon] route](#create-the-pokemonspokemon-route)
|
||||
* [Error handling](#error-handling)
|
||||
* [Seo and meta tags](#seo-and-meta-tags)
|
||||
* [Handle redirections](#handle-redirections)
|
||||
* [Building for production](#building-for-production)
|
||||
* [Conclusion](#conclusion)
|
||||
@@ -547,6 +548,88 @@ async fn get_all_pokemons(_req: Request, fetch: Client) -> Response {
|
||||
If you now try to load a not existing pokemon (`http://localhost:3000/pokemons/tuono-pokemon`) you will
|
||||
correctly receive a 404 status code in the console.
|
||||
|
||||
## Seo and meta tags
|
||||
|
||||
The website now works and the http errors are meaningful but we should also take care to be meaningful
|
||||
for the web crawlers. The best way to do it is to enrich the meta tags like the `<title>` and the
|
||||
`<description>`.
|
||||
|
||||
To do so `tuono` exposes also the `<Head />` component useful exactly for handling this scenario. Let's update the `/` and the
|
||||
`/pokemons/[pokemon]` routes with this.
|
||||
|
||||
```diff
|
||||
// src/routes/index.tsx
|
||||
import type { TuonoProps } from "tuono";
|
||||
++ import { Head } from "tuono"
|
||||
|
||||
interface Pokemon {
|
||||
name: string
|
||||
}
|
||||
|
||||
interface IndexProps {
|
||||
results: Pokemon[]
|
||||
}
|
||||
|
||||
export default function IndexPage({
|
||||
data,
|
||||
}: TuonoProps<IndexProps>): JSX.Element {
|
||||
if (!data?.results) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
++ <Head>
|
||||
++ <title>Tuono tutorial</title>
|
||||
++ </Head>
|
||||
<header className="header">
|
||||
<a href="https://crates.io/crates/tuono" target="_blank">
|
||||
Crates
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/tuono" target="_blank">
|
||||
Npm
|
||||
</a>
|
||||
</header>
|
||||
<div className="title-wrap">
|
||||
<h1 className="title">
|
||||
TU<span>O</span>NO
|
||||
</h1>
|
||||
<div className="logo">
|
||||
<img src="rust.svg" className="rust" />
|
||||
<img src="react.svg" className="react" />
|
||||
</div>
|
||||
</div>
|
||||
<ul style={{ flexWrap: "wrap", display: "flex", gap: 10 }}>
|
||||
{data.results.map((pokemon) => {
|
||||
return pokemon.name;
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```diff
|
||||
// src/routes/pokemons/[pokemon].tsx
|
||||
-- import { TuonoProps } from "tuono";
|
||||
++ import { TuonoProps, Head } from "tuono";
|
||||
import PokemonView from "../../components/PokemonView";
|
||||
|
||||
export default function Pokemon({ data }: TuonoProps): JSX.Element {
|
||||
-- return <PokemonView pokemon={data} />;
|
||||
++ return (
|
||||
++ <>
|
||||
++ <Head>
|
||||
++ <title>Pokemon: ${data?.name}</title>
|
||||
++ </Head>
|
||||
++ <PokemonView pokemon={data} />
|
||||
++ </>
|
||||
++ )
|
||||
}
|
||||
```
|
||||
|
||||
The `Head` component takes as children any valid HTML meta tag.
|
||||
|
||||
## Handle redirections
|
||||
|
||||
What if there is a pokemon among all of them that should be considered the GOAT? What
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/routes/index.tsx
|
||||
import type { TuonoProps } from 'tuono'
|
||||
import { Head, type TuonoProps } from 'tuono'
|
||||
import PokemonLink from '../components/PokemonLink'
|
||||
|
||||
interface Pokemon {
|
||||
@@ -19,6 +19,9 @@ export default function IndexPage({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Tuono tutorial</title>
|
||||
</Head>
|
||||
<header className="header">
|
||||
<a href="https://crates.io/crates/tuono" target="_blank">
|
||||
Crates
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TuonoProps } from 'tuono'
|
||||
import { Head, type TuonoProps } from 'tuono'
|
||||
import PokemonView from '../../components/PokemonView'
|
||||
|
||||
interface Pokemon {
|
||||
@@ -11,5 +11,12 @@ interface Pokemon {
|
||||
export default function PokemonPage({
|
||||
data,
|
||||
}: TuonoProps<Pokemon>): JSX.Element {
|
||||
return <PokemonView pokemon={data} />
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Pokemon: {data?.name}</title>
|
||||
</Head>
|
||||
<PokemonView pokemon={data} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+7
-7
@@ -3,13 +3,13 @@
|
||||
"packageManager": "pnpm@9.1.1",
|
||||
"scripts": {
|
||||
"dev": "turbo dev --filter tuono",
|
||||
"build": "turbo build",
|
||||
"lint": "turbo lint",
|
||||
"format": "turbo format",
|
||||
"format:check": "turbo format:check",
|
||||
"types": "turbo types",
|
||||
"test": "turbo test",
|
||||
"test:watch": "turbo test:watch"
|
||||
"build": "turbo build --filter tuono",
|
||||
"lint": "turbo lint --filter tuono",
|
||||
"format": "turbo format --filter tuono",
|
||||
"format:check": "turbo format:check --filter tuono",
|
||||
"types": "turbo types --filter tuono",
|
||||
"test": "turbo test --filter tuono",
|
||||
"test:watch": "turbo test:watch --filter tuono"
|
||||
},
|
||||
"workspaces": [
|
||||
"tuono",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuono-fs-router-vite-plugin",
|
||||
"version": "0.4.6",
|
||||
"version": "0.5.0",
|
||||
"description": "Plugin for the tuono's file system router. Tuono is the react/rust fullstack framework",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuono-lazy-fn-vite-plugin",
|
||||
"version": "0.4.6",
|
||||
"version": "0.5.0",
|
||||
"description": "Plugin for the tuono's lazy fn. Tuono is the react/rust fullstack framework",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuono",
|
||||
"version": "0.4.6",
|
||||
"version": "0.5.0",
|
||||
"description": "The react/rust fullstack framework",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
@@ -88,6 +88,7 @@
|
||||
"@types/node": "^20.12.7",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"react-meta-tags": "^1.0.1",
|
||||
"tuono-fs-router-vite-plugin": "workspace:*",
|
||||
"tuono-lazy-fn-vite-plugin": "workspace:*",
|
||||
"vite": "^5.2.11",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Head from 'react-meta-tags'
|
||||
import {
|
||||
createRoute,
|
||||
createRootRoute,
|
||||
@@ -18,4 +19,5 @@ export {
|
||||
RouterProvider,
|
||||
dynamic,
|
||||
useRouter,
|
||||
Head,
|
||||
}
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
declare module 'react-meta-tags'
|
||||
declare module 'react-meta-tags/server'
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'fast-text-encoding' // Mandatory for React18
|
||||
import * as React from 'react'
|
||||
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
|
||||
import MetaTagsServer from 'react-meta-tags/server'
|
||||
import { MetaTagsContext } from 'react-meta-tags'
|
||||
import { RouterProvider, createRouter } from '../router'
|
||||
|
||||
type RouteTree = any
|
||||
@@ -42,16 +44,22 @@ export function serverSideRendering(routeTree: RouteTree) {
|
||||
const cssBundles = props.cssBundles as string[]
|
||||
const router = createRouter({ routeTree }) // Render the app
|
||||
|
||||
const metaTagsInstance = MetaTagsServer()
|
||||
|
||||
const app = renderToString(
|
||||
<RouterProvider router={router} serverProps={props} />,
|
||||
<MetaTagsContext extract={metaTagsInstance.extract}>
|
||||
<RouterProvider router={router} serverProps={props} />
|
||||
</MetaTagsContext>,
|
||||
)
|
||||
|
||||
const metaTags = metaTagsInstance.renderToString()
|
||||
|
||||
return `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Playground</title>
|
||||
${metaTags}
|
||||
${generateCssLinks(cssBundles, mode)}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user