From 6d8a04dfb26f5fb37505a62cf7915e8c7372714f Mon Sep 17 00:00:00 2001 From: Valerio Ageno <51341197+Valerioageno@users.noreply.github.com> Date: Thu, 17 Oct 2024 08:20:01 +0200 Subject: [PATCH] Move tutorial into web documentation (#54) * chore: move tutorial into web documentation * fix: update ci-documentation tuono version * fix: rollback documentation website version to v0.10.1 * temporaly remove 'dynamic' keyword * feat: update tuono documentation to v0.10.3 * chore: format --------- Co-authored-by: Valerio Ageno --- .github/workflows/ci-documentation.yml | 2 +- .github/workflows/deploy-documentation.yml | 2 +- apps/documentation/Cargo.toml | 2 +- apps/documentation/package.json | 2 +- .../mdx-provider/mdx-pre/mdx-pre.tsx | 2 +- .../sidebar/sidebar-link.module.css | 2 +- .../src/components/sidebar/sidebar.tsx | 56 +++++- .../documentation/tutorial/api-fetching.mdx | 105 ++++++++++++ .../documentation/tutorial/components.mdx | 129 ++++++++++++++ .../documentation/tutorial/conclusion.mdx | 24 +++ .../documentation/tutorial/dynamic-routes.mdx | 159 ++++++++++++++++++ .../documentation/tutorial/error-handling.mdx | 102 +++++++++++ .../routes/documentation/tutorial/index.mdx | 43 +++++ .../routes/documentation/tutorial/intro.mdx | 17 -- .../documentation/tutorial/overview.mdx | 66 ++++++++ .../documentation/tutorial/production.mdx | 33 ++++ .../documentation/tutorial/redirections.mdx | 49 ++++++ .../src/routes/documentation/tutorial/seo.mdx | 96 +++++++++++ 18 files changed, 862 insertions(+), 29 deletions(-) create mode 100644 apps/documentation/src/routes/documentation/tutorial/api-fetching.mdx create mode 100644 apps/documentation/src/routes/documentation/tutorial/components.mdx create mode 100644 apps/documentation/src/routes/documentation/tutorial/conclusion.mdx create mode 100644 apps/documentation/src/routes/documentation/tutorial/dynamic-routes.mdx create mode 100644 apps/documentation/src/routes/documentation/tutorial/error-handling.mdx create mode 100644 apps/documentation/src/routes/documentation/tutorial/index.mdx delete mode 100644 apps/documentation/src/routes/documentation/tutorial/intro.mdx create mode 100644 apps/documentation/src/routes/documentation/tutorial/overview.mdx create mode 100644 apps/documentation/src/routes/documentation/tutorial/production.mdx create mode 100644 apps/documentation/src/routes/documentation/tutorial/redirections.mdx create mode 100644 apps/documentation/src/routes/documentation/tutorial/seo.mdx diff --git a/.github/workflows/ci-documentation.yml b/.github/workflows/ci-documentation.yml index b4d12da3..03917464 100644 --- a/.github/workflows/ci-documentation.yml +++ b/.github/workflows/ci-documentation.yml @@ -34,7 +34,7 @@ jobs: uses: actions-rust-lang/setup-rust-toolchain@v1 - name: Install tuono - run: cargo install tuono@0.10.1 + run: cargo install tuono@0.10.3 - name: Install pnpm run: npm i -g pnpm diff --git a/.github/workflows/deploy-documentation.yml b/.github/workflows/deploy-documentation.yml index 175bd6ac..a5ce3390 100644 --- a/.github/workflows/deploy-documentation.yml +++ b/.github/workflows/deploy-documentation.yml @@ -31,7 +31,7 @@ jobs: uses: actions-rust-lang/setup-rust-toolchain@v1 - name: Install tuono - run: cargo install tuono@0.10.2 + run: cargo install tuono@0.10.3 - name: Install pnpm run: npm i -g pnpm diff --git a/apps/documentation/Cargo.toml b/apps/documentation/Cargo.toml index 27508dc9..8d7bce43 100644 --- a/apps/documentation/Cargo.toml +++ b/apps/documentation/Cargo.toml @@ -8,7 +8,7 @@ name = "tuono" path = ".tuono/main.rs" [dependencies] -tuono_lib = "0.10.1" +tuono_lib = "0.10.3" glob = "0.3.1" time = { version = "0.3", features = ["macros"] } serde = { version = "1.0.202", features = ["derive"] } diff --git a/apps/documentation/package.json b/apps/documentation/package.json index f3c6dcee..92def63a 100644 --- a/apps/documentation/package.json +++ b/apps/documentation/package.json @@ -17,7 +17,7 @@ "clsx": "^2.1.1", "react": "18.3.1", "react-dom": "18.3.1", - "tuono": "0.10.2" + "tuono": "0.10.3" }, "devDependencies": { "@types/mdx": "^2.0.13", diff --git a/apps/documentation/src/components/mdx-provider/mdx-pre/mdx-pre.tsx b/apps/documentation/src/components/mdx-provider/mdx-pre/mdx-pre.tsx index 1918378c..7a91e396 100644 --- a/apps/documentation/src/components/mdx-provider/mdx-pre/mdx-pre.tsx +++ b/apps/documentation/src/components/mdx-provider/mdx-pre/mdx-pre.tsx @@ -9,7 +9,7 @@ export default function MdxPre({ children }: PreProps): JSX.Element { ) diff --git a/apps/documentation/src/components/sidebar/sidebar-link.module.css b/apps/documentation/src/components/sidebar/sidebar-link.module.css index 9bd68cae..b90273f4 100644 --- a/apps/documentation/src/components/sidebar/sidebar-link.module.css +++ b/apps/documentation/src/components/sidebar/sidebar-link.module.css @@ -2,5 +2,5 @@ border-radius: 8px; margin-top: 0.25rem; line-height: 1.25rem; - font-weight: bold; + font-weight: 500; } diff --git a/apps/documentation/src/components/sidebar/sidebar.tsx b/apps/documentation/src/components/sidebar/sidebar.tsx index ec2c3fc4..78ccc582 100644 --- a/apps/documentation/src/components/sidebar/sidebar.tsx +++ b/apps/documentation/src/components/sidebar/sidebar.tsx @@ -4,13 +4,58 @@ import SidebarLink from './sidebar-link' export default function Sidebar({ close }: { close: () => void }): JSX.Element { return ( -

Tutorial

-

Documentation

+ > + + + + + + + + + + void }): JSX.Element { + Tutorial - API fetching + + +import Breadcrumbs, { Element } from '../../../components/breadcrumbs' + + + + + + +# API fetching + +The goal is to use the [PokeAPI](https://pokeapi.co/docs/v2) to list all the pokemons of the first generation (the best one btw) and then reserve a \_\_ page for each one separately. + +## Fetch all the pokemons + +To start let’s fetch all of them in the root page; since we want to render them on the server side we gonna need to implement the logic in the `index.rs` file. + +Clear the `index.rs` file and paste: + +```rust +// src/routes/index.rs +use serde::{Deserialize, Serialize}; +use tuono_lib::reqwest::Client; +use tuono_lib::{Props, Request, Response}; + +const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151"; + +#[derive(Debug, Serialize, Deserialize)] +struct Pokemons { + results: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +struct Pokemon { + name: String, + url: String, +} + +#[tuono_lib::handler] +async fn get_all_pokemons(_req: Request, fetch: Client) -> Response { + return match fetch.get(ALL_POKEMON).send().await { + Ok(res) => { + let data = res.json::().await.unwrap(); + Response::Props(Props::new(data)) + } + Err(_err) => Response::Props(Props::new("{}")), + }; +} +``` + +Now the pokemons are correctly fetched and hydrated on the client side so we can actually use them. Clear the `index.tsx` file and paste: + +```tsx +// src/routes/index.tsx +import type { TuonoProps } from 'tuono' + +interface Pokemon { + name: string +} + +interface IndexProps { + results: Pokemon[] +} + +export default function IndexPage({ + data, +}: TuonoProps): JSX.Element { + if (!data?.results) { + return <> + } + + return ( + <> +
+ + Crates + + + Npm + +
+
+

+ TUONO +

+
+ + +
+
+
    + {data.results.map((pokemon) => { + return pokemon.name + })} +
+ + ) +} +``` + +Refresh now the browser! A bit ugly but all the pokemons are finally printed on screen! diff --git a/apps/documentation/src/routes/documentation/tutorial/components.mdx b/apps/documentation/src/routes/documentation/tutorial/components.mdx new file mode 100644 index 00000000..45c84230 --- /dev/null +++ b/apps/documentation/src/routes/documentation/tutorial/components.mdx @@ -0,0 +1,129 @@ +import { Head } from 'tuono' + + + Tutorial - Components + + +import Breadcrumbs, { Element } from '../../../components/breadcrumbs' + + + + + + +# Components + +## Creating a stand-alone component + +Let’s then create the button needed for displaying the list of pokemons. + +Create the following file `src/components/PokemonLink.tsx` and fill the content with: + +```tsx +// src/components/PokemonLink.tsx +import { Link } from 'tuono' + +interface Pokemon { + name: string +} + +export default function PokemonLink({ + pokemon, + id, +}: { + pokemon: Pokemon + id: number +}): JSX.Element { + return ( + + {pokemon.name} + + + ) +} +``` + +Now that the link is done let’s import it in the `index.tsx` file + +```diff +// src/routes/index.tsx + +++ import PokemonLink from '../components/PokemonLink' + +// ... +
    +-- {pokemons.map((pokemon) => { +-- return pokemon.name; +++ {pokemons.map((pokemon, i) => { +++ return ; + })} +
+// ... +``` + +Now the links work. Clicking on any of them we get redirected to the 404 page because we haven’t yet implemented the `pokemons/[pokemon]` page. +As previously said CSS modules are enabled out of the box so let’s make those links a little bit nicer. + +Create alongside the `PokemonLink.tsx` component the CSS module `PokemonLink.module.css` and copy the following content into it: + +```css +/* src/components/PokemonLink.module.css */ + +.link { + width: 100%; + max-width: 216px; + position: relative; + background: white; + margin-bottom: 10px; + border: solid #f0f0f0 1px; + text-decoration: none; + color: black; + padding: 5px 5px 5px 15px; + border-radius: 10px; + display: flex; + justify-content: space-between; + transition: 0.2s; + align-items: center; +} + +.link:hover { + box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px; +} + +.link img { + width: 70px; + background: white; + border-radius: 50%; +} +``` + +> 💡 SASS is supported out of the box. Just install the processor in the devDependencies `npm i -D sass` and run again `tuono dev` + +Then import the styles into the `PokemonLink` component as following: + +```diff +// src/components/PokemonLink.tsx +import { Link } from "tuono"; +import type { Pokemon } from "./../types/pokemon"; +++ import styles from './PokemonLink.module.css' + +export default function PokemonLink({ + pokemon, + id, +}: { + pokemon: Pokemon; + id: number; +}): JSX.Element { + return ( +-- +++ + {pokemon.name} + + + ); +} +``` diff --git a/apps/documentation/src/routes/documentation/tutorial/conclusion.mdx b/apps/documentation/src/routes/documentation/tutorial/conclusion.mdx new file mode 100644 index 00000000..a2854fd4 --- /dev/null +++ b/apps/documentation/src/routes/documentation/tutorial/conclusion.mdx @@ -0,0 +1,24 @@ +import { Head } from 'tuono' + + + Tutorial - Conclusion + + +import Breadcrumbs, { Element } from '../../../components/breadcrumbs' + + + + + + +# Conclusion + +That’s it! You just created a multi thread full stack application with rust and react. + +The project is still under heavy development and many features are not ready yet but +I hope you got the taste of what is like working with rust and react in the same stack. + +As I mentioned in the introduction I'd love to hear what you thought about the framework and the tutorial - feel free to reach me +at [valerioageno@yahoo.it](mailto:valerioageno@yahoo.it) or in Twitter (X) DMs [@valerioageno](https://twitter.com/valerioageno). + +Ciao diff --git a/apps/documentation/src/routes/documentation/tutorial/dynamic-routes.mdx b/apps/documentation/src/routes/documentation/tutorial/dynamic-routes.mdx new file mode 100644 index 00000000..b93b13e8 --- /dev/null +++ b/apps/documentation/src/routes/documentation/tutorial/dynamic-routes.mdx @@ -0,0 +1,159 @@ +import { Head } from 'tuono' + + + Tutorial - __ routes + + +import Breadcrumbs, { Element } from '../../../components/breadcrumbs' + + + + + + +# \_\_ routes + +## Create the pokemon route + +The homepage is ready. We have the full list of pokemons and they are all links. Great! + +Now we want to make those links actually pointing to a real page. Let’s create the \_\_ route. + +Create the folder `routes/pokemons` and then create the two files `[pokemon].tsx` and `[pokemon].rs`. + +These two will handle every requests that points to `http://localhost:3000/pokemons/bulbasaur..mew`. + +Let’s first work on the server side file. Paste into the new `[pokemon].rs` file the following code: + +```rust +// src/routes/pokemons/[pokemon].rs +use serde::{Deserialize, Serialize}; +use tuono_lib::reqwest::Client; +use tuono_lib::{Props, Request, Response}; + +const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon"; + +#[derive(Debug, Serialize, Deserialize)] +struct Pokemon { + name: String, + id: u16, + weight: u16, + height: u16, +} + +#[tuono_lib::handler] +async fn get_pokemon(req: Request, fetch: Client) -> Response { + // The param `pokemon` is defined in the route filename [pokemon].rs + let pokemon = req.params.get("pokemon").unwrap(); + + return match fetch.get(format!("{POKEMON_API}/{pokemon}")).send().await { + Ok(res) => { + let data = res.json::().await.unwrap(); + Response::Props(Props::new(data)) + } + Err(_err) => Response::Props(Props::new("{}")) + }; +} +``` + +Then let’s work on the frontend. Paste into the `[pokemon].tsx` file the following code: + +```tsx +import { TuonoProps } from 'tuono' +import PokemonView from '../../components/PokemonView' + +export default function Pokemon({ data }: TuonoProps): JSX.Element { + return +} +``` + +The browser should complain that the component `PokemonView` does not exist. Let’s create it then! + +```tsx +// components/PokemonView.tsx +import { Link } from 'tuono' +import styles from './PokemonView.module.css' + +interface Pokemon { + name: string + id: string + weight: number + height: number +} + +export default function PokemonView({ + pokemon, +}: { + pokemon?: Pokemon +}): JSX.Element { + return ( +
+ + Back + + {pokemon?.name && ( +
+
+

{pokemon.name}

+
+
Weight:
+
{pokemon.weight}lbs
+
+
+
Height:
+
{pokemon.height}ft
+
+
+ +
+ )} +
+ ) +} +``` + +
+ +```css +/* components/PokemonView.module.css */ +.back-btn { + background-color: white; + border-radius: 10px; + padding: 7px 15px; + color: black; + text-decoration: none; + border: solid #f0f0f0 1px; + font-size: 20px; +} + +.back-btn:hover { + box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px; +} + +.pokemon { + display: flex; + justify-content: space-between; + margin-top: 20px; +} + +.name { + font-size: 50px; + font-weight: 700; +} + +.pokemon img { + width: 400px; +} + +.spec { + display: flex; + font-size: 18px; + margin-top: 10px; +} + +.label { + font-weight: 700; +} +``` diff --git a/apps/documentation/src/routes/documentation/tutorial/error-handling.mdx b/apps/documentation/src/routes/documentation/tutorial/error-handling.mdx new file mode 100644 index 00000000..8af27efc --- /dev/null +++ b/apps/documentation/src/routes/documentation/tutorial/error-handling.mdx @@ -0,0 +1,102 @@ +import { Head } from 'tuono' + + + Tutorial - Error handling + + +import Breadcrumbs, { Element } from '../../../components/breadcrumbs' + + + + + + +# Error handling + +With the current setup all the routes always return a `200 Success` http status no matter the response type. + +In order to return a more meaningful status code to the browser the `Props` struct can be initialized with also the +`Props::new_with_status()` method. + +Let's see how it works! + +```diff +// src/routes/pokemons/[pokemon].rs +use serde::{Deserialize, Serialize}; +-- use tuono_lib::reqwest::Client; +++ use tuono_lib::reqwest::{Client, StatusCode}; +use tuono_lib::{Props, Request, Response}; + +const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon"; + +#[derive(Debug, Serialize, Deserialize)] +struct Pokemon { + name: String, + id: u16, + weight: u16, + height: u16, +} + +#[tuono_lib::handler] +async fn get_pokemon(req: Request, fetch: Client) -> Response { + // The param `pokemon` is defined in the route filename [pokemon].rs + let pokemon = req.params.get("pokemon").unwrap(); + + return match fetch.get(format!("{POKEMON_API}/{pokemon}")).send().await { + Ok(res) => { +++ if res.status() == StatusCode::NOT_FOUND { +++ return Response::Props(Props::new_with_status("{}", StatusCode::NOT_FOUND)); +++ } + + let data = res.json::().await.unwrap(); + Response::Props(Props::new(data)) + } +-- Err(_err) => Response::Props(Props::new( +++ Err(_err) => Response::Props(Props::new_with_status( +++ "{}", +++ StatusCode::INTERNAL_SERVER_ERROR, + )), + }; +} +``` + +
+ +```diff +// src/routes/index.rs +use serde::{Deserialize, Serialize}; +-- use tuono_lib::reqwest::Client; +++ use tuono_lib::reqwest::{Client, StatusCode}; +use tuono_lib::{Props, Request, Response}; + +const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151"; + +#[derive(Debug, Serialize, Deserialize)] +struct Pokemons { + results: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +struct Pokemon { + name: String, + url: String, +} + +#[tuono_lib::handler] +async fn get_all_pokemons(_req: Request, fetch: Client) -> Response { + return match fetch.get(ALL_POKEMON).send().await { + Ok(res) => { + let data = res.json::().await.unwrap(); + Response::Props(Props::new(data)) + } +-- Err(_err) => Response::Props(Props::new( +++ Err(_err) => Response::Props(Props::new_with_status( +++ "{}", // Return empty JSON +++ StatusCode::INTERNAL_SERVER_ERROR, + )), + }; +} +``` + +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. diff --git a/apps/documentation/src/routes/documentation/tutorial/index.mdx b/apps/documentation/src/routes/documentation/tutorial/index.mdx new file mode 100644 index 00000000..9e3405f7 --- /dev/null +++ b/apps/documentation/src/routes/documentation/tutorial/index.mdx @@ -0,0 +1,43 @@ +import { Head } from 'tuono' + + + Tuono - Tutorial + + +import Breadcrumbs, { Element } from '../../../components/breadcrumbs' + + + + + +# Tutorial + +This tutorial is meant for giving you a sneak peek of the framework and is intended to evolve during the +development - be sure to have [installed](/documentation/installation) the [latest version](https://crates.io/crates/tuono). + +This tutorial is not meant for people that don't know React - in that case I suggest you to first read the +[React doc](https://react.dev/); Typescript and Rust knowledge is not a requirement though! + +If you prefer to just read the code rather than writing it you can download the finished tutorial project with: + +```bash +$ tuono new tutorial --template tutorial +``` + +> I'd love to hear your thoughts about the framework and the tutorial - feel free to reach me at [valerioageno@yahoo.it](mailto:valerioageno@ahoo.it) +> or on Twitter (X) DM [@valerioageno](https://twitter.com/valerioageno) + +## Table of contents + +- Project scaffold +- Start the dev environment +- The “/” route +- Tutorial introduction +- Fetch all the pokemons +- Create a stand-alone component +- Create the /pokemons/[pokemon] route +- Error handling +- Seo and meta tags +- Handle redirections +- Building for production +- Conclusion diff --git a/apps/documentation/src/routes/documentation/tutorial/intro.mdx b/apps/documentation/src/routes/documentation/tutorial/intro.mdx deleted file mode 100644 index e0c82c44..00000000 --- a/apps/documentation/src/routes/documentation/tutorial/intro.mdx +++ /dev/null @@ -1,17 +0,0 @@ -import { Head } from 'tuono' - - - Tuono - Tutorial intro - - -import Breadcrumbs, { Element } from '../../../components/breadcrumbs' - - - - - -# Tutorial - -You can find the tutorial [here](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md). - -Other tasks have the priority right now. The tutorial will be ported here as soon as possible. diff --git a/apps/documentation/src/routes/documentation/tutorial/overview.mdx b/apps/documentation/src/routes/documentation/tutorial/overview.mdx new file mode 100644 index 00000000..c75bf1e9 --- /dev/null +++ b/apps/documentation/src/routes/documentation/tutorial/overview.mdx @@ -0,0 +1,66 @@ +import { Head } from 'tuono' + + + Tutorial - Scaffold new project + + +import Breadcrumbs, { Element } from '../../../components/breadcrumbs' + + + + + + +# Development setup + +## Scaffold a new project + +To setup a new fresh project you just need to run the following command: + +``` +$ tuono new tuono-tutorial +``` + +Get into the project folder and install the dependencies with: + +``` +$ npm install +``` + +Open it with your favourite code editor. + +The project will have the following structure: + +``` +├── package.json +├── public +├── src +│ ├── routes +│ └── styles +├── Cargo.toml +├── README.md +└── tsconfig.json +``` + +`public/`: put here all the files you want to be public + +`src/routes/`: All the files in this folder are considered routes. All the routes are server side rendered out of the box. +To add server side capabilities just create a rust file with the same name as the route (i.e. `about.tsx` → `about.rs`). + +`src/styles/`: In this folder there is the `global.css` file that stores all the global styles. For the rest of the project you can use +CSS modules. + +## Start the dev environment + +To start the development environment you just need to run the following command within the project folder: + +``` +$ tuono dev +``` + +The first time might take a little bit because it will install all the rust’s dependencies. All the other execution will be pretty quick! + +> 💡 The tuono dev development script is currently under strong optimization improvements. +> In case you face any error delete the cache `.tuono` folder and run it again! + +Then open [http://localhost:3000/](http://localhost:3000) on the browser. diff --git a/apps/documentation/src/routes/documentation/tutorial/production.mdx b/apps/documentation/src/routes/documentation/tutorial/production.mdx new file mode 100644 index 00000000..d600e0e0 --- /dev/null +++ b/apps/documentation/src/routes/documentation/tutorial/production.mdx @@ -0,0 +1,33 @@ +import { Head } from 'tuono' + + + Tutorial - Building for production + + +import Breadcrumbs, { Element } from '../../../components/breadcrumbs' + + + + + + +# Production build + +The source now is ready to be released. Both server and client have been managed in a unoptimized way +to easy the development experience. To build the project to the production state just run: + +```shell +$ tuono build +``` + +This command just created the final assets within the `out` directory. To run then the prodiction server +run: + +```shell +$ cargo run --release +``` + +Check again [`http://localhost:3000/`](http://localhost:3000/) - This environment now has all the +optimizations ready to unleash the power of a rust server that seamessly renders a React application!🚀 + +> Note: The `out` directory is not standalone. You can't rely just on it to run the production server. diff --git a/apps/documentation/src/routes/documentation/tutorial/redirections.mdx b/apps/documentation/src/routes/documentation/tutorial/redirections.mdx new file mode 100644 index 00000000..5def6e49 --- /dev/null +++ b/apps/documentation/src/routes/documentation/tutorial/redirections.mdx @@ -0,0 +1,49 @@ +import { Head } from 'tuono' + + + Tutorial - Redirections + + +import Breadcrumbs, { Element } from '../../../components/breadcrumbs' + + + + + + +# Redirections + +## Handle redirections + +What if there is a pokemon among all of them that should be considered the GOAT? What +we are going to do right now is creating a new route `/pokemons/GOAT` that points to the best +pokemon of the first generation. + +First let's create a new route by just creating an new file `/pokemons/GOAT.rs` and pasting the following code: + +```rs +// src/routes/pokemons/GOAT.rs +use tuono_lib::{reqwest::Client, Request, Response}; + +#[tuono_lib::handler] +async fn redirect_to_goat(_: Request, _: Client) -> Response { + // Of course the GOAT is mewtwo - feel free to select your favourite 😉 + Response::Redirect("/pokemons/mewtwo".to_string()) +} +``` + +Now let's create the button in the home page to actually point to it! + +```diff +// src/routes/index.tsx + +
    +++ + {data.results.map((pokemon, i) => { + return + })} +
+``` + +Now at [http://localhost:3000/](http:/localhost:3000/) you will find a new link at the beginning of the list. +Click on it and see the application automatically redirecting you to your favourite pokemon's route! diff --git a/apps/documentation/src/routes/documentation/tutorial/seo.mdx b/apps/documentation/src/routes/documentation/tutorial/seo.mdx new file mode 100644 index 00000000..a35e49a4 --- /dev/null +++ b/apps/documentation/src/routes/documentation/tutorial/seo.mdx @@ -0,0 +1,96 @@ +import { Head } from 'tuono' + + + Tutorial - SEO and meta tags + + +import Breadcrumbs, { Element } from '../../../components/breadcrumbs' + + + + + + +# 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.