From 9db78d71e05ca728f4f938570f5780b6249f8178 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Fri, 28 Jun 2024 18:49:19 +0200 Subject: [PATCH 1/6] docs: update canary for Fresh 2 --- .../getting-started/create-a-project.md | 64 +++++ docs/canary/getting-started/tutorial.md | 238 ++++++++++++++++++ docs/toc.ts | 7 +- www/static/docs/island-visualize.png | Bin 0 -> 16381 bytes 4 files changed, 304 insertions(+), 5 deletions(-) create mode 100644 docs/canary/getting-started/create-a-project.md create mode 100644 docs/canary/getting-started/tutorial.md create mode 100644 www/static/docs/island-visualize.png diff --git a/docs/canary/getting-started/create-a-project.md b/docs/canary/getting-started/create-a-project.md new file mode 100644 index 00000000000..4eb68557f0a --- /dev/null +++ b/docs/canary/getting-started/create-a-project.md @@ -0,0 +1,64 @@ +--- +description: | + Create a new Fresh project by running the Fresh project creation tool. This + scaffolds out the various files and folders a Fresh project needs. +--- + +New Fresh projects can be created by using the Fresh project creation tool. It +will scaffold out a new project with some example files to get you started. + +To create a new project, run: + +```sh Terminal +deno run -A -r jsr:@fresh/init +cd fresh-project +deno task dev +``` + +This will scaffold out the new project, then switch into the newly created +directory, and then start the development server. + +This will create a directory containing some files and directories. There are 4 +files that are strictly necessary to run a Fresh project: + +- **`dev.ts`**: This is the development entry point for your project. This is + the file that you run to start your project. +- **`main.ts`**: This is the production entry point for your project. It is the + file that you link to Deno Deploy. + +A **`deno.json`** file is also created in the project directory. This file does +two things: + +- It defines the "imports" field. This is an + [import map](https://docs.deno.com/runtime/manual/basics/import_maps) that is + used to manage dependencies for the project. This allows for easy importing + and updating of dependencies. +- It registers a "dev" [task](https://deno.land/manual/tools/task_runner) to run + the project without having to type a long `deno run` command. + +Two important folders are also created that contain your routes and islands +respectively: + +- **`routes/`**: This folder contains all of the routes in your project. The + names of each file in this folder correspond to the path where that page will + be accessed. Code inside of this folder is never directly shipped to the + client. You'll learn more about how routes work in the next section. +- **`islands/`**: This folder contains all of the interactive islands in your + project. The name of each file corresponds to the name of the island defined + in that file. Code inside of this folder can be run from both client and + server. You'll learn more about islands later in this chapter. + +Finally a **`static/`** folder is created that contains static files that are +automatically served "as is". We'll +[learn more about static files](../concepts/static-files) in a later chapter. + +> [info]: Fresh requires the following permissions to function: +> +> - **`--allow-net`**: Required to start the HTTP server. +> - **`--allow-read`**: Required to read (static) files from disk. +> - **`--allow-env`**: Required to read environment variables that can be used +> to configure your project. +> - **`--allow-run`**: Required to shell out to `deno` and `esbuild` under the +> hood generate the browser assets. +> +> The tasks defined in `deno.json` have `-A` which allows all permissions. diff --git a/docs/canary/getting-started/tutorial.md b/docs/canary/getting-started/tutorial.md new file mode 100644 index 00000000000..b42c057d686 --- /dev/null +++ b/docs/canary/getting-started/tutorial.md @@ -0,0 +1,238 @@ +--- +description: | + To start a Fresh project, just run `deno task start`. This will start the + project with default permission flags, in watch mode. +--- + +In this tutorial we're going to build a simple blog with an about page. The next +step after scaffolding out a new project, is to actually start it. To do this +you can run `deno task dev`. + +```sh Terminal +$ deno task dev +Watcher Process started. + πŸ‹ Fresh ready + Local: http://localhost:8000 +``` + +This will start Fresh in development mode. Visit http://localhost:8000 to view +your running project. Try changing some of the text in `routes/index.tsx` and +see how the page updates automatically when you save the file. + +## Your first route + +Routes encapsulate the logic for handling requests to a particular path in your +project. They can be used to handle API requests or render HTML pages. For now +we are going to do the latter. + +Routes are defined as files in the `routes/` directory. The file name of the +module is important: it is used to determine the path that the route will +handle. + +```sh +routes +β”œβ”€β”€ blog +| β”œβ”€β”€ [id].tsx # /blog/example-post, /blog/post2, etc +| └── search.tsx # /blog/search +| └── index.tsx # /blog +β”œβ”€β”€ about.tsx # /about +└── index.tsx # / +``` + +_We'll learn more about all available ways to configure routes in the +[Routing chapter](../concepts/routing)._ + +Let's create an `/about` route by creating a `routes/about.tsx` file. In this +file, we can declare a component that should be rendered every time a user +visits the page. This is done with JSX, an extension to JavaScript that allows +you to embed HTML-like tags in your TypeScript/JavaScript code. + +```tsx routes/about.tsx +export default function AboutPage() { + return ( +
+

About

+

This is the about page.

+
+ ); +} +``` + +If you now visit http://localhost:8000/about you'll see our new route in your +browser. + +> [info]: To learn more about JSX and Preact, you can follow +> [this tutorial](https://preactjs.com/tutorial) from the Preact documentation. + +### Adding a dynamic route + +Our page should also have a very basic blog. We want to have an index page where +we have an overview of all our blog posts and a detail page where we can view a +single blog post. But first, let's create a file that holds the actual content +of our blog posts. We'll create a file at the root of your project called +`blog-posts.ts`: + +```ts blog-posts.ts +export interface BlogPost { + /** The blog post title */ + title: string, + /** The blog post url segment */ + slug: string + /** The publish date */ + date: Date + /** The blog post content */ + content: string +} + +export const posts: BlogPost[] = [ + { + title: "My first post", + slug: "my-first-post", + date: new Date("2024-01-01") + content: "This is the content of my first post" + }, + { + title: "This is my second post", + slug: "my-second-post", + date: new Date("2024-03-24") + content: "This is the content of my second post" + } +] +``` + +With our post data in place we can start building the routes for our blog. Let's +start with the blog index page by creating a new route at +`routes/blog/index.tsx` + +```tsx routes/blog/index.tsx +import { posts } from "../../blog-posts.ts"; + +export default function BlogIndex() { + // Sort posts in reverse order + const sortedPosts = posts.sort((a, b) => a.getTime() - b.getTime()); + + return ( +
+

Blog

+ +
+ ); +} +``` + +If we visit http://localhost:8000/blog we'll see a list of our blog posts. But +when we click on a link we'll get a page not found error. Let's fix that by +creating a route for viewing a single post. To do so, create a +`routes/blog/[id].tsx` file. + +```tsx routes/blog/[id].tsx +import { HttpError, page } from "fresh"; +import { define } from "../utils/state.ts"; +import { type BlogPost, posts } from "../../blog-posts.ts"; + +// This is a handler that runs before rendering the page. It +// is typically used to prepare data, set HTTP headers, fetch +// additional data from other APIs and things like that. +export const handler = define.handler({ + GET(ctx) { + // Since this file is named "[id].tsx" there will + // be a parameter named id in `ctx.params` which contains + // that part of the url. + const slug = ctx.params.id; + const post = posts.find((post) => post.slug === slug); + + // If we didn't find a post, throw a 404 not found error + if (post === undefined) { + throw new HttpError(404); + } + + // We found a post, let's pass it to our component + return page({ post }); + }, +}); + +// Preact component that renders the blog post +export default define.page(function Post(props) { + const post = props.post; + + return ( +
+

{post.title}

+

{post.content}

+
+ ); +}); +``` + +### Adding a search page + +asd + +## Your first island + +Every page in Fresh is server-rendered only by default. But not every page of a +website is completely static. Often you'll want some form of interactivity on +the page. Fresh does this via the so called +["Island architecture"](https://jasonformat.com/islands-architecture/). + +An island is an area on the page that should be interactive and run some client-side JavaScript code + +In our case we're going to add an interactive counter to our about page. To do +so, we're going to create a new file at `islands/Counter.tsx`. It is important +that the file is inside the `islands/` directory as Fresh uses that to know +which files need to also run in the browser. + +In this file we're going to export a Preact component that shows a counter and +has two buttons to either increment or decrement the counter value. + +```tsx islands/Counter.tsx +import { useSignal } from "@preact/signals"; + +export function Counter() { + const count = useSignal(0); + + return ( +
+

Counter value: {count}

+ + +
+ ); +} +``` + +> [info]: A signal is a value that when updated will ensure that the UI is +> automatically updated in the most optimal way. Read more about signals here: +> https://preactjs.com/blog/introducing-signals + +### Using our Counter island + +Let's integrate our counter into our about page. This can be done by importing +the `Counter` component and adding it to the JSX ``. + +```tsx routes/about.tsx +import { Counter } from "../islands/Counter.tsx"; + +export default function AboutPage() { + return ( +
+

About

+

This is the about page.

+

Here is a counter

+ +
+ ); +} +``` + +If you now visit http://localhost:8000/about you'll see our newly created +counter on the page. Click the "increment" and "decrement" button and see the +number update in the UI. diff --git a/docs/toc.ts b/docs/toc.ts index 02f8f2b5a99..2a358130d27 100644 --- a/docs/toc.ts +++ b/docs/toc.ts @@ -29,13 +29,10 @@ const toc: RawTableOfContents = { title: "Getting Started", link: "latest", pages: [ - ["create-a-project", "Create a project", "link:latest"], - ["running-locally", "Running locally", "link:latest"], - ["create-a-route", "Create a route", "link:latest"], + ["create-a-project", "Create a project", "link:canary"], + ["tutorial", "Tutorial", "link:canary"], ["dynamic-routes", "Dynamic routes", "link:latest"], - ["custom-handlers", "Custom handlers", "link:latest"], ["form-submissions", "Form submissions", "link:latest"], - ["adding-interactivity", "Adding interactivity", "link:latest"], ["deploy-to-production", "Deploy to production", "link:latest"], ], }, diff --git a/www/static/docs/island-visualize.png b/www/static/docs/island-visualize.png new file mode 100644 index 0000000000000000000000000000000000000000..493048d563db8a97409663844ce712ad85965b9a GIT binary patch literal 16381 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 Date: Fri, 28 Jun 2024 19:12:46 +0200 Subject: [PATCH 2/6] WIP --- .../getting-started/create-a-project.md | 68 +++++++++---------- docs/canary/getting-started/tutorial.md | 4 +- docs/toc.ts | 1 - 3 files changed, 35 insertions(+), 38 deletions(-) diff --git a/docs/canary/getting-started/create-a-project.md b/docs/canary/getting-started/create-a-project.md index 4eb68557f0a..0b8c47a7905 100644 --- a/docs/canary/getting-started/create-a-project.md +++ b/docs/canary/getting-started/create-a-project.md @@ -7,50 +7,48 @@ description: | New Fresh projects can be created by using the Fresh project creation tool. It will scaffold out a new project with some example files to get you started. +> [info]: Make sure to have the [latest Deno](https://deno.com/) version +> installed before continuing. + To create a new project, run: ```sh Terminal deno run -A -r jsr:@fresh/init cd fresh-project -deno task dev ``` -This will scaffold out the new project, then switch into the newly created -directory, and then start the development server. - -This will create a directory containing some files and directories. There are 4 -files that are strictly necessary to run a Fresh project: - -- **`dev.ts`**: This is the development entry point for your project. This is - the file that you run to start your project. -- **`main.ts`**: This is the production entry point for your project. It is the - file that you link to Deno Deploy. - -A **`deno.json`** file is also created in the project directory. This file does -two things: - -- It defines the "imports" field. This is an - [import map](https://docs.deno.com/runtime/manual/basics/import_maps) that is - used to manage dependencies for the project. This allows for easy importing - and updating of dependencies. -- It registers a "dev" [task](https://deno.land/manual/tools/task_runner) to run - the project without having to type a long `deno run` command. - -Two important folders are also created that contain your routes and islands -respectively: +This will scaffold out the new project, then switch ito the newly created +directory. The folder structure of the newly created Fresh project will look +roughly like this: + +```sh +fresh-project/ +β”œβ”€β”€ components/ # Place components that should be re-used here +| └── Button.tsx # A re-usable button component +β”œβ”€β”€ islands/ # Client-side components to run in the browser +| └── Counter.tsx # An example Counter island component +β”œβ”€β”€ routes/ # Place all your routes here +| β”œβ”€β”€ _app.tsx # App wrapper template, the outer HTML structure +| | # that will always be included on every page. +| β”œβ”€β”€ api/ +| | └── [name].tsx # /api/:name route example that responds with +| | # plain text and the name you pass in the url +| └── index.tsx # / Route +β”œβ”€β”€ static/ # Place static files (images, videos, etc) here +| └── ... +β”œβ”€β”€ deno.json # Contains project dependencies, tasks, etc +β”œβ”€β”€ dev.ts # Development entry point +└── main.tsx # Production entry point (use this for Deno Deploy) +``` -- **`routes/`**: This folder contains all of the routes in your project. The - names of each file in this folder correspond to the path where that page will - be accessed. Code inside of this folder is never directly shipped to the - client. You'll learn more about how routes work in the next section. -- **`islands/`**: This folder contains all of the interactive islands in your - project. The name of each file corresponds to the name of the island defined - in that file. Code inside of this folder can be run from both client and - server. You'll learn more about islands later in this chapter. +The most important fields in the `deno.json` file are the `"imports"` and +`"tasks"` field. -Finally a **`static/`** folder is created that contains static files that are -automatically served "as is". We'll -[learn more about static files](../concepts/static-files) in a later chapter. +- `"imports"`: An + [import map](https://docs.deno.com/runtime/manual/basics/import_maps) for + managing dependencies. +- `"tasks"`: Registers [tasks](https://deno.land/manual/tools/task_runner) for + your project. Run `deno task` to view all available tasks. > [info]: Fresh requires the following permissions to function: > diff --git a/docs/canary/getting-started/tutorial.md b/docs/canary/getting-started/tutorial.md index b42c057d686..8e8a21c42c9 100644 --- a/docs/canary/getting-started/tutorial.md +++ b/docs/canary/getting-started/tutorial.md @@ -30,8 +30,8 @@ module is important: it is used to determine the path that the route will handle. ```sh -routes -β”œβ”€β”€ blog +routes/ +β”œβ”€β”€ blog/ | β”œβ”€β”€ [id].tsx # /blog/example-post, /blog/post2, etc | └── search.tsx # /blog/search | └── index.tsx # /blog diff --git a/docs/toc.ts b/docs/toc.ts index 2a358130d27..920fa8c2e0b 100644 --- a/docs/toc.ts +++ b/docs/toc.ts @@ -31,7 +31,6 @@ const toc: RawTableOfContents = { pages: [ ["create-a-project", "Create a project", "link:canary"], ["tutorial", "Tutorial", "link:canary"], - ["dynamic-routes", "Dynamic routes", "link:latest"], ["form-submissions", "Form submissions", "link:latest"], ["deploy-to-production", "Deploy to production", "link:latest"], ], From dce62307050ecab15f8aaaac8ff583f3dea70f3f Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Wed, 3 Jul 2024 12:49:41 +0200 Subject: [PATCH 3/6] WIP --- docs/canary/build-and-deploy/build.md | 38 +++++++++++++ docs/canary/build-and-deploy/deno-deploy.md | 21 ++++++++ docs/canary/build-and-deploy/docker.md | 54 +++++++++++++++++++ .../getting-started/create-a-project.md | 14 +++-- docs/canary/getting-started/introduction.md | 30 +++++++++++ docs/canary/getting-started/tutorial.md | 54 +++++++++++++++++-- docs/toc.ts | 16 +++--- 7 files changed, 212 insertions(+), 15 deletions(-) create mode 100644 docs/canary/build-and-deploy/build.md create mode 100644 docs/canary/build-and-deploy/deno-deploy.md create mode 100644 docs/canary/build-and-deploy/docker.md create mode 100644 docs/canary/getting-started/introduction.md diff --git a/docs/canary/build-and-deploy/build.md b/docs/canary/build-and-deploy/build.md new file mode 100644 index 00000000000..119c20edb06 --- /dev/null +++ b/docs/canary/build-and-deploy/build.md @@ -0,0 +1,38 @@ +--- +description: | + Building your Fresh app for production. +--- + +Fresh comes with built-in capabilities to compress assets to improve page load +speed. + +## Building + +You can build your Fresh app by invoking the `deno task build` script. This will +run the `dev.ts` file, which will then call Fresh's `Builder` class and generate +optimized bundles for the browser. Put any additional tasks that should be done +to prepare your app for deployment here. You can check if the build task was +invoke by checking for `Deno.args.includes("build")`. + +Here is an example what the `dev.ts` file looks like for the Fresh documentation +website: + +```ts dev.ts +import { Builder } from "fresh/dev"; +import { app } from "./main.ts"; +import { tailwind } from "@fresh/plugin-tailwind"; + +const builder = new Builder({ target: "safari12" }); +tailwind(builder, app, {}); + +if (Deno.args.includes("build")) { + await builder.build(app); +} else { + await builder.listen(app); +} +``` + +## Preview your app + +You can preview your production app locally by running the `deno task start` or +running `deno run -A main.ts` directly. diff --git a/docs/canary/build-and-deploy/deno-deploy.md b/docs/canary/build-and-deploy/deno-deploy.md new file mode 100644 index 00000000000..d08c36067fa --- /dev/null +++ b/docs/canary/build-and-deploy/deno-deploy.md @@ -0,0 +1,21 @@ +--- +description: | + Deploy Fresh to Deno Deploy which is an optimized edge platform. +--- + +[Deno Deploy](https://deno.com/deploy) is a hassle-free deployment platform for +serverless TypeScript/JavaScript applications created by us. Fresh works best +with that. + +## Creating a new Deploy project + +Head over to your [Deno Deploy Dashboard](https://dash.deno.com/projects) and +click on the "New Project" button. Select the GitHub repository of your project +and the branch you want to deploy from. The default branch is typically `main` +for production deployments. Follow the rest of the wizard and finish deployment +by clicking the "Deploy Project" button at the bottom. Each time new code lands +in the `main` branch, a new deployment will be made to update your website +automatically. + +> [info]: Deno deploy will automatically integrate with GitHub to create preview +> deployments for every PR. diff --git a/docs/canary/build-and-deploy/docker.md b/docs/canary/build-and-deploy/docker.md new file mode 100644 index 00000000000..89d55704650 --- /dev/null +++ b/docs/canary/build-and-deploy/docker.md @@ -0,0 +1,54 @@ +--- +description: | + Deploy Fresh via Docker yourself. +--- + +You can deploy Fresh to any platform that can run Docker containers. Docker is a +tool to containerize projects and portably run them on any supported platform. + +## Creating a Docker image + +When packaging your Fresh app for Docker, it is important that you set the +`DENO_DEPLOYMENT_ID` environment variable in your container. This variable needs +to be set to an opaque string ID that represents the version of your application +that is currently being run. This could be a Git commit hash, or a hash of all +files in your project. It is critical for the function of Fresh that this ID +changes when _any_ file in your project changes - if it doesn't, incorrect +caching **will** cause your project to not function correctly. + +Here is an example `Dockerfile` for a Fresh project: + +```dockerfile Dockerfile +# Pick the latest deno version here +FROM denoland/deno:1.44.4 + +ARG GIT_REVISION +ENV DENO_DEPLOYMENT_ID=${GIT_REVISION} + +WORKDIR /app + +COPY . . +RUN deno cache main.ts + +EXPOSE 8000 + +CMD ["run", "-A", "main.ts"] +``` + +To build your Docker image inside of a Git repository: + +```sh Terminal +$ docker build --build-arg GIT_REVISION=$(git rev-parse HEAD) -t my-fresh-app . +``` + +Then run your Docker container: + +```sh Terminal +$ docker run -t -i -p 80:8000 my-fresh-app +``` + +To deploy to a cloud provider, push it to a container registry and follow their +documentation. + +- [Amazon Web Services](https://docs.aws.amazon.com/AmazonECS/latest/userguide/create-container-image.html#create-container-image-push-ecr) +- [Google Cloud](https://cloud.google.com/container-registry/docs/pushing-and-pulling) diff --git a/docs/canary/getting-started/create-a-project.md b/docs/canary/getting-started/create-a-project.md index 0b8c47a7905..e6bca1f149a 100644 --- a/docs/canary/getting-started/create-a-project.md +++ b/docs/canary/getting-started/create-a-project.md @@ -18,8 +18,12 @@ cd fresh-project ``` This will scaffold out the new project, then switch ito the newly created -directory. The folder structure of the newly created Fresh project will look -roughly like this: +directory. + +## Project Structure + +The folder structure of the newly created Fresh project will look roughly like +this: ```sh fresh-project/ @@ -50,7 +54,9 @@ The most important fields in the `deno.json` file are the `"imports"` and - `"tasks"`: Registers [tasks](https://deno.land/manual/tools/task_runner) for your project. Run `deno task` to view all available tasks. -> [info]: Fresh requires the following permissions to function: +> [info]: The commands defined in the `"tasks"` section in `deno.json` pass `-A` +> to `deno run` which enables all permissions. Fresh requires the following +> permissions to function: > > - **`--allow-net`**: Required to start the HTTP server. > - **`--allow-read`**: Required to read (static) files from disk. @@ -58,5 +64,3 @@ The most important fields in the `deno.json` file are the `"imports"` and > to configure your project. > - **`--allow-run`**: Required to shell out to `deno` and `esbuild` under the > hood generate the browser assets. -> -> The tasks defined in `deno.json` have `-A` which allows all permissions. diff --git a/docs/canary/getting-started/introduction.md b/docs/canary/getting-started/introduction.md new file mode 100644 index 00000000000..5d024485f9f --- /dev/null +++ b/docs/canary/getting-started/introduction.md @@ -0,0 +1,30 @@ +--- +description: | + Fresh is a full stack modern web framework for JavaScript and TypeScript + developers, designed to make it trivial to create high-quality, performant, + and personalized web applications. +--- + +Fresh is an approchable full stack web framework, designed to make it trivial to +create high-quality, performant, and personalized web applications. You can use +it to create your home page, a blog, a large web application like GitHub or +Twitter, or anything else you can think of. + +Fresh is built around the +[island architecture](https://jasonformat.com/islands-architecture/) where every +page is rendered on the server and only the JavaScript for the interactive areas +on your page - so called islands - is shipped to the browser. This allows Fresh +to minimize the amount of JavaScript that is sent to the browser to make your +websites load faster. + +Fresh uses [Preact](https://preactjs.com) under the hood, which is a super tiny +react-like framework. Whenever possible, we use standard +[Web APIs](https://developer.mozilla.org/en-US/docs/Web/API) instead of +re-inventing the wheel. + +## Project goals + +The Fresh project was created and is maintained by the [Deno](https://deno.com/) +company with the goal of beign one of the fastest web frameworks and showing how +Deno can be used to built highly optimized websites. Many experiences gained +here in Fresh directly help making Deno better. diff --git a/docs/canary/getting-started/tutorial.md b/docs/canary/getting-started/tutorial.md index 8e8a21c42c9..c89811dcca5 100644 --- a/docs/canary/getting-started/tutorial.md +++ b/docs/canary/getting-started/tutorial.md @@ -135,7 +135,7 @@ creating a route for viewing a single post. To do so, create a ```tsx routes/blog/[id].tsx import { HttpError, page } from "fresh"; -import { define } from "../utils/state.ts"; +import { define } from "../../utils/state.ts"; import { type BlogPost, posts } from "../../blog-posts.ts"; // This is a handler that runs before rendering the page. It @@ -172,9 +172,52 @@ export default define.page(function Post(props) { }); ``` -### Adding a search page +## Adding a search page for our blog -asd +To build our search page, we'll create a standard +[`
`](https://developer.mozilla.org/en-US/docs/Learn/Forms/Your_first_form) +that upon submission adds the search query in the URL. On the server we'll pull +out that value of the incoming request, and search if any of our blog posts +contains the query string. + +Since handlers run before rendering the components they are the perfect place to +trigger the search. Once we've filtered down the result list, we can pass it to +our component via the `page()` function. + +```tsx routes/blog/search.tsx +import { page } from "fresh"; +import { define } from "../../utils/state.ts"; +import { posts } from "../../blog-posts.ts" + +export const handler = define.handler({ + GET(ctx) { + const query = ctx.url.searchParams.get("q") || ""; + const results = posts.filter((post) => post.title.includes(query)); + return page({ results, query }); + }, +}); + +export default define.page(props => { + const { results, query } = props.data; + return ( +
+ + + + +
    + {results.map((post) => { + return ( +
  • + {post.title} +
  • + ) + })} +
+
+ ); +} +``` ## Your first island @@ -236,3 +279,8 @@ export default function AboutPage() { If you now visit http://localhost:8000/about you'll see our newly created counter on the page. Click the "increment" and "decrement" button and see the number update in the UI. + +## Deploying + +Congratulations, you've built your first Fresh app! You can now continue to +[deploy it](#) to publish it on the internet. diff --git a/docs/toc.ts b/docs/toc.ts index 920fa8c2e0b..f94a07a675f 100644 --- a/docs/toc.ts +++ b/docs/toc.ts @@ -21,25 +21,19 @@ const toc: RawTableOfContents = { "the-canary-version": { title: "The canary version", }, - introduction: { - title: "Introduction", - link: "latest", - }, "getting-started": { title: "Getting Started", link: "latest", pages: [ + ["introduction", "Introduction", "link:canary"], ["create-a-project", "Create a project", "link:canary"], ["tutorial", "Tutorial", "link:canary"], - ["form-submissions", "Form submissions", "link:latest"], - ["deploy-to-production", "Deploy to production", "link:latest"], ], }, concepts: { title: "Concepts", link: "latest", pages: [ - ["architecture", "Architecture", "link:latest"], ["server-components", "Server Components", "link:latest"], ["routing", "Routing", "link:latest"], ["routes", "Routes", "link:latest"], @@ -59,6 +53,14 @@ const toc: RawTableOfContents = { ["server-configuration", "Server configuration", "link:latest"], ], }, + "build-and-deploy": { + title: "Build and Deploy", + pages: [ + ["build", "Building your app", "link:canary"], + ["deno-deploy", "Deno Deploy", "link:canary"], + ["docker", "Docker Image", "link:canary"], + ], + }, integrations: { title: "Integrations", link: "latest", From 8d8737e003b2d34d4a61615904bdba5cae273aa8 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Wed, 3 Jul 2024 12:51:01 +0200 Subject: [PATCH 4/6] WIP --- .../canary/getting-started/create-a-project.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/canary/getting-started/create-a-project.md b/docs/canary/getting-started/create-a-project.md index e6bca1f149a..a925799abe6 100644 --- a/docs/canary/getting-started/create-a-project.md +++ b/docs/canary/getting-started/create-a-project.md @@ -28,18 +28,18 @@ this: ```sh fresh-project/ β”œβ”€β”€ components/ # Place components that should be re-used here -| └── Button.tsx # A re-usable button component +β”‚ └── Button.tsx # A re-usable button component β”œβ”€β”€ islands/ # Client-side components to run in the browser -| └── Counter.tsx # An example Counter island component +β”‚ └── Counter.tsx # An example Counter island component β”œβ”€β”€ routes/ # Place all your routes here -| β”œβ”€β”€ _app.tsx # App wrapper template, the outer HTML structure -| | # that will always be included on every page. -| β”œβ”€β”€ api/ -| | └── [name].tsx # /api/:name route example that responds with -| | # plain text and the name you pass in the url -| └── index.tsx # / Route +β”‚ β”œβ”€β”€ _app.tsx # App wrapper template, the outer HTML structure +β”‚ β”‚ # that will always be included on every page. +β”‚ β”œβ”€β”€ api/ +β”‚ β”‚ └── [name].tsx # /api/:name route example that responds with +β”‚ β”‚ # plain text and the name you pass in the url +β”‚ └── index.tsx # / Route β”œβ”€β”€ static/ # Place static files (images, videos, etc) here -| └── ... +β”‚ └── ... β”œβ”€β”€ deno.json # Contains project dependencies, tasks, etc β”œβ”€β”€ dev.ts # Development entry point └── main.tsx # Production entry point (use this for Deno Deploy) From 07f351082f5126a829dacfdb8ae5fe70d0f816c3 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Wed, 3 Jul 2024 13:19:37 +0200 Subject: [PATCH 5/6] WIP --- docs/canary/concepts/app-wrapper.md | 79 ++++++++++ docs/canary/concepts/forms.md | 113 ++++++++++++++ docs/canary/concepts/islands.md | 228 ++++++++++++++++++++++++++++ docs/canary/concepts/layouts.md | 103 +++++++++++++ docs/canary/concepts/routes.md | 173 +++++++++++++++++++++ docs/toc.ts | 10 +- 6 files changed, 701 insertions(+), 5 deletions(-) create mode 100644 docs/canary/concepts/app-wrapper.md create mode 100644 docs/canary/concepts/forms.md create mode 100644 docs/canary/concepts/islands.md create mode 100644 docs/canary/concepts/layouts.md create mode 100644 docs/canary/concepts/routes.md diff --git a/docs/canary/concepts/app-wrapper.md b/docs/canary/concepts/app-wrapper.md new file mode 100644 index 00000000000..d558c832446 --- /dev/null +++ b/docs/canary/concepts/app-wrapper.md @@ -0,0 +1,79 @@ +--- +description: | + Add a global app wrapper to provide common meta tags or context for application routes. +--- + +An app wrapper is defined in an `_app.tsx` file in `routes/` folder and is +typically used to create the outer structure of an HTML document. It must +contain a default export that is a regular Preact component. Only one such +wrapper is allowed per application. + +The component to be wrapped is received via props, in addition to a few other +things. This allows for the introduction of a global container functioning as a +template which can be conditioned based on state and params. Note that any state +set by middleware is available via `props.state`. + +```tsx routes/_app.tsx +import { define } from "../utils/state.ts"; + +export default define.page(function App({ Component, state }) { + // do something with state here + return ( + + + + + My Fresh app + + + + + + ); +}); +``` + +## Async app wrapper + +Similar to routes and layouts, the app wrapper can be made asynchronous. This +changes the function signature so that the first argument is the `Request` +instance and the second one is the `FreshContext`. + +```tsx routes/_app.tsx +import { define } from "../utils/state.ts"; + +export default define.page(async function App(props) { + const data = await somehowLoadData(); + + return ( + + + + + My Fresh app + + +

Hello {data.name}

+ + + + ); +}); +``` + +## Disabling the app wrapper + +Rendering the app wrapper can be skipped on a route or layout basis. To do that, +set `skipAppWrapper: true` to the layout or route config. + +```tsx routes/my-special-route.tsx +import { RouteConfig } from "fresh"; + +export const config: RouteConfig = { + skipAppWrapper: true, // Skip the app wrapper during rendering +}; + +export default define.page(function Page() { + // ... +}); +``` diff --git a/docs/canary/concepts/forms.md b/docs/canary/concepts/forms.md new file mode 100644 index 00000000000..4ba4fb5394f --- /dev/null +++ b/docs/canary/concepts/forms.md @@ -0,0 +1,113 @@ +--- +description: | + Robustly handle user inputs using HTML `
` elements client side, and form + submission handlers server side. +--- + +For stronger resiliency and user experience, Fresh relies on native browser +support for form submissions with the HTML `` element. + +In the browser, a `` submit will send an HTML action (usually `GET` or +`POST`) to the server, which responds with a new page to render. + +## POST request with `application/x-www-form-urlencoded` + +Forms typically submit as a `GET` request with data encoded in the URL's search +parameters, or as a `POST` request with either an +`application/x-www-form-urlencoded` or `multipart/form-data` body. + +This example demonstrates how to handle `application/x-www-form-urlencoded` +`` submissions: + +```tsx routes/subscribe.tsx +import { define } from "../utils/state.ts"; + +export const handler = define.handler({ + async POST(req, ctx) { + const form = await req.formData(); + const email = form.get("email")?.toString(); + + // Add email to list. + + // Redirect user to thank you page. + const headers = new Headers(); + headers.set("location", "/thanks-for-subscribing"); + return new Response(null, { + status: 303, // See Other + headers, + }); + }, +}); + +export default define.page(function Subscribe() { + return ( + + + + + ); +}); +``` + +When the user submits the form, Deno will retrieve the `email` value using the +request's `formData()` method, add the email to a list, and redirect the user to +a thank you page. + +## Handling file uploads + +File uploads can be handled in a very similar manner to the example above. Note +that this time, we have to explicitly declare the form's encoding to be +`multipart/form-data`. + +```tsx routes/subscribe.tsx +import { page } from "fresh"; + +export const handler = { + async GET(req, ctx) { + return page({ data: { message: null } }); + }, + async POST(req, ctx) { + const form = await req.formData(); + const file = form.get("my-file") as File; + + if (!file) { + return page({ + data: { + message: `Please try again`, + }, + }); + } + + const name = file.name; + const contents = await file.text(); + + console.log(contents); + + return page({ + data: { + message: `${name} uploaded!`, + }, + }); + }, +}; + +export default define.page(function Upload(props) { + const { message } = props.data; + return ( + <> +
+ + +
+ {message ?

{message}

: null} + + ); +}); +``` + +## A note of caution + +These examples are simplified to demonstrate how Deno and Fresh handle HTTP +requests. In the Real Worldβ„’, you'll want to validate your data (_especially the +file type_) and protect against cross-site request forgery. Consider yourself +warned. diff --git a/docs/canary/concepts/islands.md b/docs/canary/concepts/islands.md new file mode 100644 index 00000000000..9a23b768e05 --- /dev/null +++ b/docs/canary/concepts/islands.md @@ -0,0 +1,228 @@ +--- +description: | + Islands enable client side interactivity in Fresh. They are hydrated on the client in addition to being rendered on the server. +--- + +Islands enable client side interactivity in Fresh. Islands are isolated Preact +components that are rendered on the server and then hydrated on the client. This +is different from all other components in Fresh, as they are usually rendered on +the server only. + +Islands are defined by creating a file in the `islands/` folder in a Fresh +project. The name of this file must be a PascalCase or kebab-case name of the +island. + +```tsx islands/my-island.tsx +import { useSignal } from "@preact/signals"; + +export default function MyIsland() { + const count = useSignal(0); + + return ( +
+ Counter is at {count}.{" "} + +
+ ); +} +``` + +An island can be used in a page like a regular Preact component. Fresh will take +care of automatically re-hydrating the island on the client. + +```tsx route/index.tsx +import MyIsland from "../islands/my-island.tsx"; + +export default function Home() { + return ; +} +``` + +## Passing JSX to islands + +Islands support passing JSX elements via the `children` property. + +```tsx islands/my-island.tsx +import { useSignal } from "@preact/signals"; +import { ComponentChildren } from "preact"; + +interface Props { + children: ComponentChildren; +} + +export default function MyIsland({ children }: Props) { + const count = useSignal(0); + + return ( +
+ Counter is at {count}.{" "} + + {children} +
+ ); +} +``` + +This allows you to pass static content rendered by the server to an island in +the browser. + +```tsx routes/index.tsx +import MyIsland from "../islands/my-island.tsx"; + +export default function Home() { + return ( + +

This text is rendered on the server

+
+ ); +} +``` + +You can also create shared components in your `components/` directory, which can +be used in both static content and interactive islands. When these components +are used within islands, interactivity can be added, such as `onClick` handlers +(using an `onClick` handler on a button outside of an island will not fire). + +```tsx islands/my-island.tsx +import { useSignal } from "@preact/signals"; +import { ComponentChildren } from "preact"; +import Card from "../components/Card.tsx"; +import Button from "../components/Button.tsx"; + +interface Props { + children: ComponentChildren; +} + +export default function MyIsland({ children }: Props) { + const count = useSignal(0); + + return ( + + Counter is at {count}.{" "} + + {children} + + ); +} +``` + +## Passing other props to islands + +Passing props to islands is supported, but only if the props are serializable. +Fresh can serialize the following types of values: + +- Primitive types `string`, `boolean`, `bigint`, and `null` +- Most `number`s, including `Infinity`, `-Infinity`, and `NaN` +- Plain objects with string keys and serializable values +- Arrays, Sets and Maps containing serializable values +- `Uint8Array` +- JSX Elements (restricted to `props.children`) +- `Date` instsances +- `RegExp` instsances +- Preact Signals (if the inner value is serializable) + +Circular references are supported. If an object or signal is referenced multiple +times, it is only serialized once and the references are restored upon +deserialization. Passing complex objects like `Date`, custom classes, or +functions is not supported. + +During server side rendering, Fresh annotates the HTML with special comments +that indicate where each island will go. This gives the code sent to the client +enough information to put the islands where they are supposed to go without +requiring hydration for the static children of interactive islands. No +Javascript is sent to the client when no interactivity is needed. + +```html + +
+ Counter is at 0. + + +

This text is rendered on the server

+ +
+ +``` + +### Nesting islands + +Islands can be nested within other islands as well. In that scenario they act +like a normal Preact component, but still receive the serialized props if any +were present. + +```tsx islands/other-island.tsx +import { useSignal } from "@preact/signals"; +import { ComponentChildren } from "preact"; + +interface Props { + children: ComponentChildren; + foo: string; +} + +function randomNumber() { + return Math.floor(Math.random() * 100); +} + +export default function MyIsland({ children, foo }: Props) { + const number = useSignal(randomNumber()); + + return ( +
+

String from props: {foo}

+

+ + {" "} + number is: {number}. +

+
+ ); +} +``` + +In essence, Fresh allows you to mix static and interactive parts in your app in +a way that's most optimal for your use app. We'll keep sending only the +JavaScript that is needed for the islands to the browser. + +```tsx route/index.tsx +import MyIsland from "../islands/my-island.tsx"; +import OtherIsland from "../islands/other-island.tsx"; + +export default function Home() { + return ( +
+ + + +

Some more server rendered text

+
+ ); +} +``` + +## Rendering islands on client only + +When using client-only APIs, like `EventSource` or `navigator.getUserMedia`, +this component will not run on the server as it will produce an error like: + +``` +An error occurred during route handling or page rendering. ReferenceError: EventSource is not defined + at Object.MyIsland (file:///Users/someuser/fresh-project/islandsmy-island.tsx:6:18) + at m (https://esm.sh/v129/preact-render-to-string@6.2.0/X-ZS8q/denonext/preact-render-to-string.mjs:2:2602) + at m (https://esm.sh/v129/preact-render-to-string@6.2.0/X-ZS8q/denonext/preact-render-to-string.mjs:2:2113) + .... +``` + +Use the `IS_BROWSER` flag as a guard to fix the issue: + +```tsx islands/my-island.tsx +import { IS_BROWSER } from "fresh"; + +export function MyIsland() { + // Return any prerenderable JSX here which makes sense for your island + if (!IS_BROWSER) return
; + + // All the code which must run in the browser comes here! + // Like: EventSource, navigator.getUserMedia, etc. + return
; +} +``` diff --git a/docs/canary/concepts/layouts.md b/docs/canary/concepts/layouts.md new file mode 100644 index 00000000000..c5f5edf09b0 --- /dev/null +++ b/docs/canary/concepts/layouts.md @@ -0,0 +1,103 @@ +--- +description: | + Add a layout to provide common meta tags, context for application sub routes, and common layout. +--- + +An layout is defined in a `_layout.tsx` file in any sub directory (at any level) +under the `routes/` folder. It must contain a default export that is a regular +Preact component. Only one such layout is allowed per sub directory. + +```txt Project structure +└── routes + Β Β  β”œβ”€β”€ sub + Β Β  β”‚ β”œβ”€β”€ page.tsx + Β Β  β”‚Β Β  └── index.tsx + β”œβ”€β”€ other + β”‚ β”œβ”€β”€ _layout.tsx # will be applied on top of `routes/_layout.tsx` + β”‚ └── page.tsx + β”œβ”€β”€ _layout.tsx # will be applied to all routes + └── _app.tsx +``` + +The component to be wrapped is received via props, in addition to a few other +things. This allows for the introduction of a global container functioning as a +template which can be conditioned based on state and params. Note that any state +set by middleware is available via `props.state`. + +```tsx routes/sub/_layout.tsx +import { define } from "../../utils/state.ts"; + +export default define.page(function Layout({ Component, state }) { + // do something with state here + return ( +
+ +
+ ); +}); +``` + +## Async layouts + +In case you need to fetch data asynchronously before rendering the layout, you +can use an async layout to do so. + +```tsx routes/sub/_layout.tsx +import { define } from "../../utils/state.ts"; + +export default define.page(async function Layout(props) { + // do something with state here + const data = await somehowLoadData(); + + return ( +
+

{data.greeting}

+ +
+ ); +}); +``` + +## Opting out of layout inheritance + +Sometimes you want to opt out of the layout inheritance mechanism for a +particular route. This can be done via route configuration. Picture a directory +structure like this: + +```txt Project structure +└── routes + Β Β  β”œβ”€β”€ sub + Β Β  β”‚ β”œβ”€β”€ _layout_.tsx + Β Β  β”‚ β”œβ”€β”€ special.tsx # should not inherit layouts + Β Β  β”‚Β Β  └── index.tsx + └── _layout.tsx +``` + +To make `routes/sub/special.tsx` opt out of rendering layouts we can set +`skipInheritedLayouts: true`. + +```tsx routes/sub/special.tsx +import { RouteConfig } from "fresh"; + +export const config: RouteConfig = { + skipInheritedLayouts: true, // Skip already inherited layouts +}; + +export default function MyPage() { + return

Hello world

; +} +``` + +You can skip already inherited layouts inside a layout file: + +```tsx routes/special/_layout.tsx +import { RouteConfig } from "fresh"; + +export const config: RouteConfig = { + skipInheritedLayouts: true, // Skip already inherited layouts +}; + +export default function MyPage() { + return

Hello world

; +} +``` diff --git a/docs/canary/concepts/routes.md b/docs/canary/concepts/routes.md new file mode 100644 index 00000000000..46134253d9c --- /dev/null +++ b/docs/canary/concepts/routes.md @@ -0,0 +1,173 @@ +--- +description: | + Routes are the basic building block of Fresh applications. They are used to define the behaviour the application when a given path is requested. +--- + +At their core, routes describe how a request for a given path should be handled, +and what the response should be. To do this, routes have two main parts: the +handler, and the component. A route can have either one, or both, but never +neither. + +The handler is a function that is called for every request to the route. It +needs to return a response that is then sent to the client. The response could +be anything: a plain text string, a JSON object, an HTML page, a WebSocket +connection, a streaming file, or pretty much anything else. The handler is +passed a `render` function that it can call to invoke rendering a component. + +The component is the template for a page. It is a JSX element that is rendered +on the server. The page component gets passed props that can be used by it to +determine exactly what should be rendered. By default components receive props +consisting of: the request URL, the matching route (as a string), the matches +from the URL pattern match, any state set by middleware, and any data passed to +the handler's `render` function. + +## Handler route + +Let's look at a basic route that returns a plain text string: + +```tsx routes/plain.tsx +import { define } from "../utils/state.ts"; + +export const handler = define.handler({ + GET() { + return new Response("Hello World"); + }, +}; +``` + +To define a handler, one needs to export a `handler` function or object from the +route module. If the handler is an object, each key in the object is the name of +the HTTP method that the handler should be called for. For example the `GET` +handler above is called for `GET` requests. If the handler is a function, it is +called for all requests regardless of the method. If an HTTP method does not +have a corresponding handler, a 405 HTTP error is returned. + +## Component route + +Now, let's render some HTML using the route component: + +```tsx routes/html.tsx +import { define } from "../utils/state.ts"; + +export default define.page(function Page(props) { + return
You are on the page '{props.url.href}'.
; +}); +``` + +The page component needs to be the default export of the route module. It is +passed props that can be used to render the page. + +As you can see in the second example, if no handler is explicitly defined a +default handler is used that just renders out the page component if present. You +can also override the default handler though to modify how exactly rendering +should work. + +## Mixed handler and component route + +In the below example, a custom handler is used to add a custom header to the +response after rendering the page component. + +```tsx routes/html.tsx +import { HandlerContext, Handlers, PageProps } from "$fresh/server.ts"; + +export const handler = define.handler({ + async GET() { + const res = await ctx.next(); + res.headers.set("X-Custom-Header", "Hello World"); + return res; + }, +}); + +export default define.page(function Page(props) { + return
You are on the page '{props.url.href}'.
; +}); +``` + +## Async route components + +Having a separate route handler and component function is nice, when you want to +test these in isolation, but can become a bit cumbersome to maintain. They +require some additional indirection of declaring an interface for the component +`Data` when you're passing it around through `ctx.render()`. + +```tsx routes/page.tsx +import { page } from "fresh"; +import { define } from "../utils/state.ts"; + +export const handler = define.handler({ + async GET() { + const foo = await loadFooValue(); + return page({ foo }); + }, +}); + +export default define.page(function MyPage(props) { + return

foo is: {props.data.foo}

; +}); +``` + +When a route has both a component and a `GET` handler, they are typically very +closely coupled. With async route components you can merge the two together and +avoid having to create the `Data` interface boilerplate. + +```tsx routes/page.tsx +import { define } from "../utils/state.ts"; + +// Async route component +export default define.page(async function MyPage(props) { + const value = await loadFooValue(); + return

foo is: {value}

; +}); +``` + +The code gets a little shorter with async route components. Conceptually, you +can think of async route components inlining the `GET` handler into the +component function. Note, that you can still add additional HTTP handlers in the +same file like before. + +```tsx routes/page.tsx +import { define } from "../utils/state.ts"; + +export const handler = define.handler({ + async POST(ctx) { + // ... do something here + }, +}); + +export default define.page(async function MyPage() { + const value = await loadFooValue(); + return

foo is: {value}

; +}); +``` + +### Returning Response objects + +Quite often a route handler needs to render a 404 page or bail out of rendering +in another manner. This can be done by returning a `Response` object. + +```tsx route/page.tsx +import { HttpError } from "fresh"; +import { define } from "../utils/state.ts"; + +// Async route component +export default define.page(async function MyPage(props) { + const value = await loadFooValue(); + + // Return 404 if `value` is null + if (value === null) { + throw new HttpError(404); + } + + // Returning a response object directly works too + if (value === "redirect") { + const headers = new Headers(); + headers.set("location", "/some-other-page"); + return new Response(null, { + status: 302, + headers, + }); + } + + return

foo is: {value}

; +}); +``` diff --git a/docs/toc.ts b/docs/toc.ts index f94a07a675f..1322d1b6eb9 100644 --- a/docs/toc.ts +++ b/docs/toc.ts @@ -36,11 +36,11 @@ const toc: RawTableOfContents = { pages: [ ["server-components", "Server Components", "link:latest"], ["routing", "Routing", "link:latest"], - ["routes", "Routes", "link:latest"], - ["app-wrapper", "App wrapper", "link:latest"], - ["layouts", "Layouts", "link:latest"], - ["forms", "Forms", "link:latest"], - ["islands", "Interactive islands", "link:latest"], + ["routes", "Routes", "link:canary"], + ["app-wrapper", "App wrapper", "link:canary"], + ["layouts", "Layouts", "link:canary"], + ["forms", "Forms", "link:canary"], + ["islands", "Interactive islands", "link:canary"], ["static-files", "Static files", "link:latest"], ["middleware", "Middlewares", "link:latest"], ["error-pages", "Error pages", "link:latest"], From b80e5337a285ee86a25e4fcfa920a673fb30e1ac Mon Sep 17 00:00:00 2001 From: Liigo Zhuang Date: Wed, 3 Jul 2024 19:25:34 +0800 Subject: [PATCH 6/6] docs: change `main.tsx` -> `main.ts` (#2574) --- docs/canary/getting-started/create-a-project.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/canary/getting-started/create-a-project.md b/docs/canary/getting-started/create-a-project.md index a925799abe6..2fb5462d0d5 100644 --- a/docs/canary/getting-started/create-a-project.md +++ b/docs/canary/getting-started/create-a-project.md @@ -42,7 +42,7 @@ fresh-project/ β”‚ └── ... β”œβ”€β”€ deno.json # Contains project dependencies, tasks, etc β”œβ”€β”€ dev.ts # Development entry point -└── main.tsx # Production entry point (use this for Deno Deploy) +└── main.ts # Production entry point (use this for Deno Deploy) ``` The most important fields in the `deno.json` file are the `"imports"` and