From 78990d3c08baed26ed45db1215e6df86eb7f9f1e Mon Sep 17 00:00:00 2001 From: Valerio Ageno <51341197+Valerioageno@users.noreply.github.com> Date: Sat, 13 Jul 2024 12:09:56 +0200 Subject: [PATCH] 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 --- crates/tuono/Cargo.toml | 2 +- crates/tuono_lib/Cargo.toml | 4 +- crates/tuono_lib/src/manifest.rs | 76 ++++++++++++++++- crates/tuono_lib_macros/Cargo.toml | 2 +- docs/tutorial.md | 83 +++++++++++++++++++ examples/tutorial/src/routes/index.tsx | 5 +- .../src/routes/pokemons/[pokemon].tsx | 11 ++- package.json | 14 ++-- packages/fs-router-vite-plugin/package.json | 2 +- packages/lazy-fn-vite-plugin/package.json | 2 +- packages/tuono/package.json | 3 +- packages/tuono/src/index.ts | 2 + packages/tuono/src/react-meta-tags.d.ts | 2 + packages/tuono/src/ssr/index.tsx | 12 ++- 14 files changed, 200 insertions(+), 20 deletions(-) create mode 100644 packages/tuono/src/react-meta-tags.d.ts diff --git a/crates/tuono/Cargo.toml b/crates/tuono/Cargo.toml index 79a7c4f7..23618a9b 100644 --- a/crates/tuono/Cargo.toml +++ b/crates/tuono/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuono" -version = "0.4.6" +version = "0.5.0" edition = "2021" authors = ["V. Ageno "] description = "The react/rust fullstack framework" diff --git a/crates/tuono_lib/Cargo.toml b/crates/tuono_lib/Cargo.toml index 567ca8f9..e65843b2 100644 --- a/crates/tuono_lib/Cargo.toml +++ b/crates/tuono_lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuono_lib" -version = "0.4.6" +version = "0.5.0" edition = "2021" authors = ["V. Ageno "] 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"] } diff --git a/crates/tuono_lib/src/manifest.rs b/crates/tuono_lib/src/manifest.rs index a89da9d8..5e047de8 100644 --- a/crates/tuono_lib/src/manifest.rs +++ b/crates/tuono_lib/src/manifest.rs @@ -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, } +fn default_css_vector() -> Vec { + 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) -> HashMap>(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 = HashMap::new(); diff --git a/crates/tuono_lib_macros/Cargo.toml b/crates/tuono_lib_macros/Cargo.toml index f95c2632..19245bf8 100644 --- a/crates/tuono_lib_macros/Cargo.toml +++ b/crates/tuono_lib_macros/Cargo.toml @@ -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"] diff --git a/docs/tutorial.md b/docs/tutorial.md index d40309e6..0a6d8e90 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -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 `` 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 +++ +
+ + Crates + + + Npm + +
+
+

+ TUONO +

+
+ + +
+
+
    + {data.results.map((pokemon) => { + return pokemon.name; + })} +
+ + ); +} +``` + +```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 ; +++ return ( +++ <> +++ +++ Pokemon: ${data?.name} +++ +++ +++ +++ ) +} +``` + +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 diff --git a/examples/tutorial/src/routes/index.tsx b/examples/tutorial/src/routes/index.tsx index 806d0732..3fc04a11 100644 --- a/examples/tutorial/src/routes/index.tsx +++ b/examples/tutorial/src/routes/index.tsx @@ -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 ( <> + + Tuono tutorial +
Crates diff --git a/examples/tutorial/src/routes/pokemons/[pokemon].tsx b/examples/tutorial/src/routes/pokemons/[pokemon].tsx index b480a6fe..41af6910 100644 --- a/examples/tutorial/src/routes/pokemons/[pokemon].tsx +++ b/examples/tutorial/src/routes/pokemons/[pokemon].tsx @@ -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): JSX.Element { - return + return ( + <> + + Pokemon: {data?.name} + + + + ) } diff --git a/package.json b/package.json index 4033a42f..9b4361c0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/fs-router-vite-plugin/package.json b/packages/fs-router-vite-plugin/package.json index 8f165ada..2a70b74b 100644 --- a/packages/fs-router-vite-plugin/package.json +++ b/packages/fs-router-vite-plugin/package.json @@ -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", diff --git a/packages/lazy-fn-vite-plugin/package.json b/packages/lazy-fn-vite-plugin/package.json index 5db1c33c..a84a569e 100644 --- a/packages/lazy-fn-vite-plugin/package.json +++ b/packages/lazy-fn-vite-plugin/package.json @@ -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", diff --git a/packages/tuono/package.json b/packages/tuono/package.json index 4ff91bd5..770b9f93 100644 --- a/packages/tuono/package.json +++ b/packages/tuono/package.json @@ -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", diff --git a/packages/tuono/src/index.ts b/packages/tuono/src/index.ts index 33c1c9a5..2018cc9d 100644 --- a/packages/tuono/src/index.ts +++ b/packages/tuono/src/index.ts @@ -1,3 +1,4 @@ +import Head from 'react-meta-tags' import { createRoute, createRootRoute, @@ -18,4 +19,5 @@ export { RouterProvider, dynamic, useRouter, + Head, } diff --git a/packages/tuono/src/react-meta-tags.d.ts b/packages/tuono/src/react-meta-tags.d.ts new file mode 100644 index 00000000..85eaf7fc --- /dev/null +++ b/packages/tuono/src/react-meta-tags.d.ts @@ -0,0 +1,2 @@ +declare module 'react-meta-tags' +declare module 'react-meta-tags/server' diff --git a/packages/tuono/src/ssr/index.tsx b/packages/tuono/src/ssr/index.tsx index 428e578d..23b71bbd 100644 --- a/packages/tuono/src/ssr/index.tsx +++ b/packages/tuono/src/ssr/index.tsx @@ -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( - , + + + , ) + const metaTags = metaTagsInstance.renderToString() + return ` - Playground + ${metaTags} ${generateCssLinks(cssBundles, mode)}