diff --git a/docs/application-architecture/_category_.json b/docs/application-architecture/_category_.json new file mode 100644 index 0000000..499c416 --- /dev/null +++ b/docs/application-architecture/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Application architecture", + "position": 2, + "link": { + "type": "generated-index", + "description": "Organising a Hanami application: containers and dependency injection, providers, and slices." + } +} diff --git a/docs/application-architecture/containers.md b/docs/application-architecture/containers.md new file mode 100644 index 0000000..a775b78 --- /dev/null +++ b/docs/application-architecture/containers.md @@ -0,0 +1,421 @@ +--- +sidebar_position: 1 +--- + +# Containers and components + +In Hanami, the application code you add to your `app` directory is automatically organised into a **container**, which forms the basis of a **component management system**. + +The **components** within that system are the objects you create to get things done within your application. For example, a HTTP action for responding to requests, a validation contract for verifying data, an operation for writing to the database, or a client that calls an external API. + +Ideally, each component in your application has a single responsibility. Very often, one component will need to use other components to achieve its work. When this happens, we call the latter components **dependencies**. + +Hanami is designed to make it easy to create applications that are systems of well-formed components with clear dependencies. + +Let's take a look at how this works in practice! + +Imagine we want our Bookshelf application to send welcome emails to new users. Assuming that we're already handling user sign ups, our task is now to create an operation for sending the welcome email. We're going to use an external mail delivery service, while sending email in both html and plain text. + +To acheive this, we first add two new components to our application: a _send welcome email operation_, and a _welcome email renderer_. + +On the file system, this looks like: + +```shell +app +├── operations +│   └── send_welcome_email.rb +└── renderers + └── welcome_email.rb +``` + +Sketching out a send welcome email operation component: + +```ruby title="app/operations/send_welcome_email.rb" +# frozen_string_literal: true + +module Bookshelf + module Operations + class SendWelcomeEmail + def call(name:, email_address:) + # Send a welcome email to the user here... + end + end + end +end +``` + +And a welcome email renderer component: + +```ruby title="app/renderers/welcome_email.rb" +# frozen_string_literal: true + +module Bookshelf + module Renderers + class WelcomeEmail + def render_html(name:) + "

Welcome to Bookshelf #{name}!

" + end + + def render_text(name:) + "Welcome to Bookshelf #{name}!" + end + end + end +end +``` + +When our application boots, Hanami will automatically register these classes as components in its __app container__, each under a __key__ based on their Ruby class name. + +This means that an instance of the `Bookshelf::Operations::SendWelcomeEmail` class is available in the container under the key `"operations.send_welcome_email"`, while an instance of `Bookshelf::Renderers::WelcomeEmail` is available under the key `"renderers.welcome_email"`. + +We can see this in the Hanami console if we boot our application and ask what keys are registered with the app container: + +```ruby +bundle exec hanami console + +bookshelf[development]> Hanami.app.boot +=> Bookshelf::App + +bookshelf[development]> Hanami.app.keys +=> ["notifications", + "settings", + "routes", + "inflector", + "logger", + "rack.monitor", + "operations.send_welcome_email", + "renderers.welcome_email"] + ``` + +To fetch our welcome email send operation from the container, we can ask for it by its `"operations.send_welcome_email"` key: + +```ruby +bookshelf[development]> Hanami.app["operations.send_welcome_email"] +=> # +``` + +Similarly we can fetch and call the renderer via the `"renderers.welcome_email"` key: + +```ruby +bookshelf[development]> Hanami.app["renderers.welcome_email"] +=> # + +bookshelf[development]> Hanami.app["renderers.welcome_email"].render_html(name: "Ada") +=> "

Welcome to Bookshelf Ada!

" +``` + +Most of the time however, you won't work with components directly through the container via `Hanami.app`. Instead, you'll work with components through the convenient __dependency injection__ system that having your components in a container supports. Let's see how that works! + +## Dependency injection + +Dependency injection is a software pattern where, rather than a component knowing how to instantiate its dependencies, those dependencies are instead provided to it. This means the dependencies can be abstract rather than hard coded, making the component more flexible, reusable and easier to test. + +To illustrate, here's an example of a send welcome email operation which _doesn't_ use dependency injection: + +```ruby title="app/operations/send_welcome_email.rb" +# frozen_string_literal: true + +require "acme_email/client" + +module Bookshelf + module Operations + class SendWelcomeEmail + def call(name:, email_address:) + email_client = AcmeEmail::Client.new + + email_renderer = Renderers::WelcomeEmail.new + + email_client.deliver( + to: email_address, + subject: "Welcome!", + text_body: email_renderer.render_text(name: name), + html_body: email_renderer.render_html(name: name) + ) + end + end + end +end +``` + +This component has two dependencies, each of which is a "hard coded" reference to a concrete Ruby class: + +- `AcmeEmail::Client`, used to send an email via the third party Acme Email service. +- `Renderers::WelcomeEmail`, used to render text and html versions of the welcome email. + +To make this send welcome email operation more resuable and easier to test, we could instead _inject_ its dependencies when we initialize it: + +```ruby title="app/operations/send_welcome_email.rb" +# frozen_string_literal: true + +require "acme_email/client" + +module Bookshelf + module Operations + class SendWelcomeEmail + attr_reader :email_client + attr_reader :email_renderer + + def initialize(email_client:, email_renderer:) + @email_client = email_client + @email_renderer = email_renderer + end + + def call(name:, email_address:) + email_client.deliver( + to: email_address, + subject: "Welcome!", + text_body: email_renderer.render_text(name: name), + html_body: email_renderer.render_html(name: name) + ) + end + end + end +end +``` + +As a result of injection, this component no longer has rigid dependencies - it's able to use any email client and email renderer it's provided. + +Hanami makes this style of dependency injection simple through its `Deps` mixin. Built into the component management system, and invoked through the use of `include Deps["key"]`, the `Deps` mixin allows a component to use any other component in its container as a dependency, while removing the need for any attr_reader or initializer boilerplate: + +```ruby title="app/operations/send_welcome_email.rb" +# frozen_string_literal: true + +module Bookshelf + module Operations + class SendWelcomeEmail + include Deps[ + "email_client", + "renderers.welcome_email" + ] + + def call(name:, email_address:) + email_client.deliver( + to: email_address, + subject: "Welcome!", + text_body: welcome_email.render_text(name: name), + html_body: welcome_email.render_html(name: name) + ) + end + end + end +end +``` + +## Injecting dependencies via `Deps` + +In the above example, the `Deps` mixin takes each given key and makes the relevant component from the app container available within the current component via an instance method. + +i.e. this code: + +```ruby +include Deps[ + "email_client", + "renderers.welcome_email" +] +``` + +makes the `"email_client"` component from the container available via an `#email_client` method, and the `"renderers.welcome_email"` component available via `#welcome_email`. + +By default, dependencies are made available under a method named after the last segment of their key. So `include Deps["renderers.welcome_email"]` allows us to call `#welcome_email` anywhere in our `SendWelcomeEmail` class access the welcome email renderer. + +We can see `Deps` in action in the console if we instantiate an instance of our send welcome email operation: + +```ruby +bookshelf[development]> Bookshelf::Operations::SendWelcomeEmail.new +=> #, + @welcome_email=#> +``` + +We can choose to provide different dependencies during initialization: + +```ruby +bookshelf[development]> Bookshelf::Operations::SendWelcomeEmail.new(email_client: "another client") +=> #> +``` + +This behaviour is particularly useful when testing, as you can substitute one or more components to test behaviour. + +In this unit test, we substitute each of the operation's dependencies in order to unit test its behaviour: + +```ruby title="spec/unit/operations/send_welcome_email_spec.rb" +RSpec.describe Bookshelf::Operations::SendWelcomeEmail, "#call" do + subject(:send_welcome_email) { + described_class.new(email_client: email_client, welcome_email: welcome_email) + } + + let(:email_client) { double(:email_client) } + let(:welcome_email) { double(:welcome_email) } + + before do + allow(welcome_email).to receive(:render_text).and_return("Welcome to Bookshelf Ada!") + allow(welcome_email).to receive(:render_html).and_return("

Welcome to Bookshelf Ada!

") + end + + it "sends a welcome email" do + expect(email_client).to receive(:deliver).with( + to: "ada@example.com", + subject: "Welcome!", + text_body: "Welcome to Bookshelf Ada!", + html_body: "

Welcome to Bookshelf Ada!

" + ) + + send_welcome_email.call(name: "Ada!", email_address: "ada@example.com") + end +end +``` + +Exactly which dependency to stub using RSpec mocks is up to you - if a depenency is left out of the constructor within the spec, then the real dependency is resolved from the container. This means that every test can decide exactly which dependencies to replace. + +## Renaming dependencies + +Sometimes you want to use a dependency under another name, either because two dependencies end with the same suffix, or just because it makes things clearer in a different context. + +This can be done by using the `Deps` mixin like so: + +```ruby title="app/operations/send_welcome_email.rb" +# frozen_string_literal: true + +module Bookshelf + module Operations + class SendWelcomeEmail + include Deps[ + "email_client", + email_renderer: "renderers.welcome_email" + ] + + def call(name:, email_address:) + email_client.deliver( + to: email_address, + subject: "Welcome!", + text_body: email_renderer.render_text(name: name), + html_body: email_renderer.render_html(name: name) + ) + end + end + end +end +``` + +Above, the welcome email renderer is now available via the `#email_renderer` method, rather than via `#welcome_email`. When testing, the renderer can now be substituted by providing `email_renderer` to the constructor: + +```ruby +subject(:send_welcome_email) { + described_class.new(email_client: mock_email_client, email_renderer: mock_email_renderer) +} +``` + +## Opting out of the container + +Sometimes it doesn’t make sense for something to be put in the container. For example, Hanami provides a base action class at `app/action.rb` from which all actions inherit. This type of class will never be used as a dependency by anything, and so registering it in the container doesn’t make sense. + +For once-off exclusions like this Hanami supports a magic comment: `# auto_register: false` + +```ruby title="app/action.rb" +# auto_register: false +# frozen_string_literal: true + +require "hanami/action" + +module Bookshelf + class Action < Hanami::Action + end +end +``` + +If you have a whole class of objects that shouldn't be placed in your container, you can configure your Hanami application to exclude an entire directory from auto registration by adjusting its `no_auto_register_paths` configuration. + +Here for example, the `app/structs` directory is excluded, meaning nothing in the `app/structs` directory will be registered with the container: + +```ruby title="config/app.rb" +# frozen_string_literal: true + +require "hanami" + +module Bookshelf + class App < Hanami::App + config.no_auto_register_paths << "structs" + end +end +``` + +A third alternative for classes you do not want to be registered in your container is to place them in the `lib` directory at the root of your project. + +For example, this `SlackNotifier` class can be used anywhere in your application, and is not registered in the container: + +```ruby title="lib/bookshelf/slack_notifier.rb" +module Bookshelf + class SlackNotifier + def self.notify(message) + # ... + end + end +end +``` + +```ruby title="app/operations/send_welcome_email.rb" +# frozen_string_literal: true + +module Bookshelf + module Operations + class SendWelcomeEmail + include Deps[ + "email_client", + "renderers.welcome_email" + ] + + def call(name:, email_address:) + email_client.deliver( + to: email_address, + subject: "Welcome!", + text_body: welcome_email.render_text(name: name), + html_body: welcome_email.render_html(name: name) + ) + + SlackNotifier.notify("Welcome email sent to #{email_address}") + end + end + end +end +``` + + + +:::tip Autoloading and the `lib` directory + +[Zeitwerk](https://github.com/fxn/zeitwerk) autoloading is in place for code you put in `lib/`, meaning that you do not need to use a `require` statement before using it. + +Code that you place in other directories under `lib` needs to be explicitly required before use. + +| Constant location | Usage | +|---------------------------------|--------------------------------------------| +| lib/bookshelf/slack_notifier.rb | Bookshelf::SlackNotifier | +| lib/my_redis/client.rb | require "my_redis/client"

MyRedis::Client | + +::: + +## Container compontent loading + +Hanami applications support a **prepared** state and a **booted** state. + +Whether your app is prepared or booted determines whether components in your app container are _lazily_ loaded on demand, or _eagerly_ loaded up front. + +### Hanami.prepare + +When you call `Hanami.prepare` (or use `require "hanami/prepare"`) Hanami will make its app available, but components within the app container will be **lazily loaded**. + +This is useful for minimizing load time. It's the default mode in the Hanami console and when running tests. + +### Hanami.boot + +When you call `Hanami.boot` (or use `require "hanami/boot"`) Hanami will go one step further and **eagerly load** all components up front. + +This is useful in contexts where you want to incur initialization costs at boot time, such as when preparing your application to serve web requests. It's the default when running via Hanami's puma setup (see `config.ru`). + +## What's next? + +Reading the above examples, you may have wondered where the `"email_client"` component came from, given it wasn't defined in the `app` directory. + +Hanami offers a mechanism called **providers** for adding components to a container outside of the auto-registration process. We'll look at providers next. diff --git a/docs/application-architecture/providers.md b/docs/application-architecture/providers.md new file mode 100644 index 0000000..9c756be --- /dev/null +++ b/docs/application-architecture/providers.md @@ -0,0 +1,145 @@ +--- +sidebar_position: 2 +--- + +# Providers + +Providers are a way to register components with your containers, outside of the automatic registration mechanism detailed in [containers and components](/docs/application-architecture/containers). + +Providers are useful when: + +- you want to register a specific instance of an object as a component, and have that very same instance be available as a dependency +- you need to set up a dependency that requires non-trivial configuration (often a third party library, or some library-like code in your `lib` directory) +- you want to take advantage of provider lifecycle methods (prepare, start and stop) + +Providers should be placed in the `config/providers` directory. Here's an example provider for that registers a client for an imagined third-party Acme Email delivery service. + +```ruby title="config/providers/email_client.rb" +# frozen_string_literal: true + +Hanami.app.register_provider(:email_client) do + prepare do + require "acme_email/client" + end + + start do + client = AcmeEmail::Client.new( + api_key: target["settings"].acme_api_key, + default_from: "no-reply@bookshelf.example.com" + ) + + register "email_client", client + end +end +``` + +The above provider creates an instance of Acme's email client, using an API key from our application's settings, then registers the client in the app container with the key `"email_client"`. + +The registered dependency can now become a dependency for other components, via `include Deps["email_client"]`: + +```ruby title="app/operations/send_welcome_email.rb" +# frozen_string_literal: true + +module Bookshelf + module Operations + class SendWelcomeEmail + include Deps[ + "email_client", + "renderers.welcome_email" + ] + + def call(name:, email_address:) + email_client.deliver( + to: email_address, + subject: "Welcome!", + text_body: welcome_email.render_text(name: name), + html_body: welcome_email.render_html(name: name) + ) + end + end + end +end +``` + +Every provider has a name (`Hanami.app.register_provider(:my_provider_name)`) and will usually register _one or more_ related components with the relevant container. + +Registered components can be any kind of object - they can be classes too. + +To register an item with the container, providers call `register`, which takes two arguments: the _key_ to be used, and the _item_ to register under it. + +```ruby title="config/providers/my_provider.rb" +# frozen_string_literal: true + +Hanami.app.register_provider(:my_provider) do + start do + register "my_thing", MyThing.new + register "another.thing", AnotherThing.new + register "thing", Thing + end +end +``` + +## Provider lifecycle + +Providers offer a three-stage lifecycle: `prepare`, `start`, and `stop`. Each has a distinct purpose: + +- prepare - basic setup code, here you can require third-party code, or code from your `lib` directory, and perform basic configuration +- start - code that needs to run for a component to be usable at runtime +- stop - code that needs to run to stop a component, perhaps to close a database connection, or purge some artifacts. + +```ruby title="config/providers/database.rb" +Hanami.app.register_provider(:database) do + prepare do + require "acme/db" + + register "database", Acme::DB.configure(target["settings"].database_url) + end + + start do + target["database"].establish_connection + end + + stop do + target["database"].close_connection + end +end +``` + +A provider's prepare and start steps will run as necessary when a component that the provider registers is used by another component at runtime. + +`Hanami.boot` calls `start` on each of your application’s providers, meaning each of your providers is started automatically when your application boots. Similarly, `Hanami.shutdown` can be invoked to call `stop` on each provider. + +You can also trigger lifecycle transitions directly by using `Hanami.app.prepare(:provider_name)`, `Hanami.app.start(:provider_name)` and `Hanami.app.stop(:provider_name)`. + +## Accessing the container via `#target` + +Within a provider, the `target` method (also available as `target_container`) can be used to access the app container. + +This is useful if your provider needs to use other components within the container, for example the application's settings or logger (via `target["settings]` and `target["logger"]`). It can also be used when a provider wants to ensure another provider has started before starting itself, via `target.start(:provider_name)`: + +```ruby title="config/providers/uploads_bucket" +Hanami.app.register_provider(:uploads_bucket) do + prepare do + require "aws-sdk-s3" + end + + start do + target.start(:metrics) + + uploads_bucket_name = target["settings"].uploads_bucket_name + + credentials = Aws::Credentials.new( + target["settings"].aws_access_key_id, + target["settings"].aws_secret_access_key, + ) + + uploads_bucket = Aws::S3::Resource.new(credentials: credentials).bucket(uploads_bucket_name) + + register "uploads_bucket", uploads_bucket + end +end +``` + +## Default providers + +Hanami ships with several providers. TODO. diff --git a/docs/application-architecture/slices.md b/docs/application-architecture/slices.md new file mode 100644 index 0000000..22c8145 --- /dev/null +++ b/docs/application-architecture/slices.md @@ -0,0 +1,276 @@ +--- +sidebar_position: 3 +--- + +# Slices + +In addition to the `app` directory, Hanami also supports organising your application code into **slices**. + +You can think of slices as distinct modules of your application. A typical case is to use slices to separate your business domains (for example billing, accounting or admin) or to have separate modules for a particular feature (API) or technical concern (search). + +Slices exist in the `slices` directory. +## Creating a slice + +Hanami provides a slice generator. To create an API slice, run `bundle exec hanami generate slice api`. + +This creates a directory in `slices`, adding some slice-specific classes like actions: + +```shell +bundle exec hanami generate slice api + +slices +└── api + ├── action.rb + └── actions +``` + +Simply creating a new directory in `slices` will also create a slice: + +```shell +mkdir -p slices/admin + +slices +└── admin +``` + +## Features of a slice + +Slices offer much of the same behaviour and features as Hanami's `app` folder. + +A Hanami slice: + +- has its own container +- can have its own providers (e.g. `slices/api/providers/my_provider.rb`) +- can include actions, routable from the application's router +- can import and export components from other slices +- can be prepared and booted independently of other slices +- can have its own slice-specific settings (e.g. `slices/api/config/settings.rb`) + +## Slice containers + +Like Hanami's `app` folder, components added to a Hanami slice are automatically organised into the slice's container. + +For example, suppose our Bookshelf application, which catalogues international books, needs an API to return the name, flag, and currency of a given country. We can create a countries show action in our API slice (by running `bundle exec hanami generate action countries.show --slice api` or by adding the file manually) that looks like: + +```ruby title="slices/api/actions/countries/show.rb" +# frozen_string_literal: true + +require "countries" + +module API + module Actions + module Countries + class Show < API::Action + include Deps[ + query: "queries.countries.show" + ] + + params do + required(:country_code).value(included_in?: ISO3166::Country.codes) + end + + def handle(request, response) + response.format = format(:json) + + halt 422, {error: "Unprocessable country code"}.to_json unless request.params.valid? + + result = query.call( + request.params[:country_code] + ) + + response.body = result.to_json + end + end + end + end +end +``` + +This action uses the countries gem to check that the provided country code (`request.params[:country_code]`) is a valid ISO3166 code and returns a 422 response if it isn't. + +If the code is valid, the action calls the countries show query (aliased here as `query` for readability). That class might look like: + +```ruby title="slices/api/queries/countries/show.rb" +# frozen_string_literal: true + +require "countries" + +module API + module Queries + module Countries + class Show + def call(country_code) + country = ISO3166::Country[country_code] + + { + name: country.iso_short_name, + flag: country.emoji_flag, + currency: country.currency_code + } + end + end + end + end +end +``` + +As an exercise, as with `Hanami.app` and its app container, we can boot the `API::Slice` to see what its container contains: + +```ruby +bundle exec hanami console + +bookshelf[development]> API::Slice.boot +=> API::Slice +bookshelf[development]> API::Slice.keys +=> ["settings", + "actions.countries.show", + "queries.countries.show", + "inflector", + "logger", + "notifications", + "rack.monitor", + "routes"] +``` + +We can call the query with a country code: + +``` +bookshelf[development]> API::Slice["queries.countries.show"].call("UA") +=> {:name=>"Ukraine", :flag=>"🇺🇦", :currency=>"UAH"} +``` + +To add a route for our action, we can add the below to our application's `config/routes.rb` file. This is done for you if you used the action generator. + +```ruby title="config/routes.rb" +# frozen_string_literal: true + +module Bookshelf + class Routes < Hanami::Routes + root { "Hello from Hanami" } + + slice :api, at: "/api" do + get "/countries/:country_code", to: "countries.show" + end + end +end +``` + +`slice :api` tells the router it can find actions for the routes within the block in the API slice. `at: "/api"` specifies an optional mount point, such the routes in the block each be mounted at `/api`. + +After running `bundle exec hanami server`, the endpoint can be hit via a `GET` request to `/api/countries/:country_code`: + +```shell +curl http://localhost:2300/api/countries/AU +{"name":"Australia","flag":"🇦🇺","currency":"AUD"} +``` + + +# Slice imports and exports + +Suppose that our bookshelf application uses a content delivery network (CDN) to serve book covers. While this makes these images fast to download, it does mean that book covers need to be purged from the CDN when they change, in order for freshly updated images to take their place. + +Images can be updated in one of two ways: the publisher of the book can sign in and upload a new image, or a Bookshelf staff member can use an admin interface to update an image on the publisher's behalf. + +In our bookshelf app, an `Admin` slice supports the latter functionality, and a `Publisher` slice the former. Both these slices want to trigger a CDN purge when a book cover is updated, but neither slice needs to know exactly how that's achieved. Instead, a `CDN` slice can manage this operation. + +```ruby title="slices/cdn/book_covers/purge.rb" +module CDN + module BookCovers + class Purge + def call(book_cover_path) + # "Purging logic here!" + end + end + end +end +``` + +Slices can be configured by creating a file at `config/slices/slice_name.rb`. + +To configure the `Admin` slice to import components from the CDN container (including the purge component above), we can create a `config/slices/admin.rb` file with the following configuration: + +```ruby title="config/slices/admin.rb" +module Admin + class Slice < Hanami::Slice + import from: :cdn + end +end +``` + +Let's see this import in action in the console, where we can see that the `Admin` slices's container now has a `"cdn.book_covers.purge"` component: + +```ruby +bundle exec hanami console + +bookshelf[development]> Admin::Slice.boot.keys +=> ["settings", + "cdn.book_covers.purge", + "inflector", + "logger", + "notifications", + "rack.monitor", + "routes"] +``` + +Using the purge operation from the `CDN` slice within the `Admin` slice component below is now as simple as using the `Deps` mixin: + +```ruby title="slices/admin/books/operations/update.rb" +module Admin + module Books + module Operations + class Update + include Deps[ + "repositories.book_repo", + "cdn.book_covers.purge" + ] + + def call(id, params) + # ... update the book using the book repository ... + + # If the update is successful, purge the book cover from the CDN + purge.call(book.cover_path) + end + end + end + end +end +``` + +It's also possible to import only specific components from another slice. Here for example, the `Publisher` slice imports strictly the purge operation, while also - for reasons of its own choosing - using the suffix `content_network` instead of `cdn`: + +```ruby title="config/slices/publisher.rb" +module Publisher + class Slice < Hanami::Slice + import keys: ["book_covers.purge"], from: :cdn, as: :content_network + end +end +``` + +In action in the console: + +```ruby +bundle exec hanami console + +bookshelf[development]> Publisher::Slice.boot.keys +=> ["settings", + "content_network.book_covers.purge", + "inflector", + "logger", + "notifications", + "rack.monitor", + "routes"] +``` + +Slices can also limit what they make available for export to other slices. + +Here, we configure the CDN slice to export only its purge component: + +```ruby title="config/slices/cdn.rb" +module CDN + class Slice < Hanami::Slice + export ["book_covers.purge"] + end +end +``` + +TODO: the rest of slices :) diff --git a/docs/http-handling/_category_.json b/docs/http-handling/_category_.json new file mode 100644 index 0000000..7b95667 --- /dev/null +++ b/docs/http-handling/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "HTTP handling", + "position": 3, + "link": { + "type": "generated-index", + "description": "Routing, actions and middlewares." + } +} diff --git a/docs/http-handling/actions.md b/docs/http-handling/actions.md new file mode 100644 index 0000000..c5fccd6 --- /dev/null +++ b/docs/http-handling/actions.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 2 +--- + +# Actions + +Hello from actions diff --git a/docs/http-handling/rack-middleware.md b/docs/http-handling/rack-middleware.md new file mode 100644 index 0000000..1b95ba0 --- /dev/null +++ b/docs/http-handling/rack-middleware.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 3 +--- + +# Rack middleware + +Rack middleware diff --git a/docs/http-handling/routing.md b/docs/http-handling/routing.md new file mode 100644 index 0000000..e77486e --- /dev/null +++ b/docs/http-handling/routing.md @@ -0,0 +1,255 @@ +--- +sidebar_position: 1 +--- + +# Routing and actions + +Hanami provides a fast, simple [router](https://github.com/hanami/router) for handling HTTP requests. + +Your application's routes are defined within the `Routes` class in the `config/routes.rb` file. + +```ruby title="config/routes.rb" +# frozen_string_literal: true + +module Bookshelf + class Routes < Hanami::Routes + root { "Hello from Hanami" } + end +end +``` + +## Adding a route + +Each route in Hanami's router is comprised of: + +- a HTTP method (i.e. `get`, `post`, `put`, `patch`, `delete`, `options` or `trace`) +- a path +- an endpoint to be invoked. + +Endpoints are usually actions within your application, but they can also be a block, a [Rack](https://github.com/rack/rack) application, or anything that responds to `#call`. + +```ruby title="Example routes" +get "/books", to: "books.index" # Invokes the Bookshelf::Actions:Books::Index action +post "/books", to: "books.create" # Invokes the Bookshelf::Actions:Books::Create action +get "/rack-app", to: RackApp.new +get "/my-lambda", to: ->(env) { [200, {}, ["A Rack compatible response"]] } +``` + +To add a full set of routes for viewing and managing books, you can either manually add the required routes to your `config/routes.rb` file, or use Hanami's action generator, which will generate actions in addition to adding routes for you. + +```shell title="Using Hanami's action generator" +bundle exec hanami generate action books.index +bundle exec hanami generate action books.show +bundle exec hanami generate action books.new +bundle exec hanami generate action books.create +bundle exec hanami generate action books.update +bundle exec hanami generate action books.destroy +``` + + +```ruby title="config/routes.rb" +# frozen_string_literal: true + +module Bookshelf + class Routes < Hanami::Routes + root { "Hello from Hanami" } + + get "/books", to: "books.index" + get "/books/:id", to: "books.show" + get "/books/new", to: "books.new" + post "/books", to: "books.create" + patch "/books/:id", to: "books.update" + delete "/books/:id", to: "books.destroy" + end +end +``` + + +## Root request routing + +A `root` method allows you to define a root route for handling `GET` requests to `"/"`. In a newly generated application, the root path calls a block which returns "Hello from Hanami". You can instead choose to invoke an action by specifying `root to: "my_action"`. For example, with the following configuration, the router will invoke the `"home"` action: + +```ruby title="config/routes.rb" +# frozen_string_literal: true + +module Bookshelf + class Routes < Hanami::Routes + root to: "home" + end +end +``` + +## Path matching + +The path component of a route supports matching on fixed strings, as well as matching with dynamic variables which can be accessed in Hanami actions via `request.params[:name]`, where `:name` matches the segment's name specified in the path. + +### Fixed paths + +The following route matches `GET` requests for `"/books"` exactly: + +```ruby +get "/books", to: "books.index" +``` + +### Dynamic segments + +The path `"/books/:id"` matches `GET` requests like `"/books/1"`: + +```ruby +get "/books/:id", to: "books.show" + +# GET /books/1 +# request.params[:id] <-- 1 +``` + +### Multiple dynamic segments + +Paths support multiple dynamic segments. For example, the path `"/books/:book_id/reviews/:id"` matches `GET` requests like `"/books/17/reviews/6"`: + +```ruby +get "/books/:book_id/reviews/:id", to: "book_reviews.show" + +# GET /books/17/reviews/6 +# request.params[:book_id] <-- 17 +# request.params[:id] <-- 6 +``` + +Here's what accessing these variables looks like in a Hanami action: + +```ruby title="Route, matching request and resulting params" +# Route: get "/books/:book_id/reviews/:id", to: "book_reviews.show" + +# Request: GET /books/17/reviews/6 + +module Bookshelf + module Actions + module BookReviews + class Show < Bookshelf::Action + def handle(request, response) + request.params[:book_id] # 17 + request.params[:id] # 6 + end + end + end + end +end +``` + +### Constraints + +Constraints can be added when matching variables. Contraints are regular expressions that must match in order for the route to match. They can be useful for ensuring that ids are digits: + +```ruby +get "/books/:id", id: /\d+/, to: "books.show" + +# GET /books/2 # matches +# GET /books/two # will not match +``` + +### Globbing and catch all routes + +Catch all routes can be added using globbing. These can be used to handle requests that do not match preceeding routes. + +For example, in the absence of an earlier matching route, `"/pages/*match"` will match requests for paths like `"/pages/2022/my-page"`: + +```ruby +get "/pages/*path", to: "page_catch_all" + +# GET /pages/2022/my-page will invoke the Bookshelf::Actions::PageCatchAll action +# request.params[:path] # <-- 2022/my-page +``` + +To create a catch all to handle all unmatched `GET` requests using a custom `"unmatched"` action, configure this route last: + +```ruby +get "/*path", to: "unmatched" +``` + +## Named routes + +Routes can be named using the `as` option. + +``` +get "/books", to: "books.index", as: :books +get "/books/:id", to: "books.show", as: :book +``` + +This enables `path` and `url` helpers, which can be accessed via the routes helper registered under `"routes"` within your application. + +```ruby +Hanami.app["routes"].path(:books) +=> "/books" + +Hanami.app["routes"].url(:books) +=> # +``` + +When a route requires variables, they can be passed to the helper: + +```ruby +Hanami.app["routes"].path(:book, id: 1) +=> "/books/1" + +Hanami.app["routes"].url(:book, id: 1) +=> # +``` + + +To set a base URL for the `url` helper, configure it in `config/app.rb`: + +```ruby title="config/app.rb" +# frozen_string_literal: true + +require "hanami" + +module Bookshelf + class App < Hanami::App + config.base_url = "https://bookshelf.example.com" + end +end +``` + +```ruby +Hanami.app["routes"].url(:book, id: 1) +=> # +``` + +## Scopes + +To nest a series of routes under a particular path, you can set a scope: + +```ruby +# frozen_string_literal: true + +module Bookshelf + class Routes < Hanami::Routes + scope "about" do + get "/contact-us", to: "content.contact_us" # <-- /about/contact-us + get "/faq", to: "content.faq" # <-- /about/faq + end + end +end +``` + + + +## Inspecting routes + +Hanami provides a `hanami routes` command to inspect your application's routes. Run `bundle exec hanami routes` on the command line to view current routes: + +```shell title="bundle exec hanami routes" +GET / home as :root +GET /books books.index +GET /books/:id books.show +GET /books/new books.new +POST /books books.create +PATCH /books/:id books.update +DELETE /books/:id books.destroy +``` + +TODO: the rest of routing :) + + + + +At the end, say - how does this action stuff wire up? Via the container. What is dat - let's see next... diff --git a/docs/intro.md b/docs/intro.md index c135225..b93f869 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -1,47 +1,231 @@ --- sidebar_position: 1 +title: Getting started +description: Getting started with Hanami 2 --- -# Hanami Tutorial +# Getting started -Let's discover **Docusaurus in less than 5 minutes**. +Hanami is a Ruby framework designed to create software that is well-architected, maintainable and a pleasure to work on. -## Getting Started +These guides aim to introduce you to the Hanami framework and demonstrate how its components fit together to produce a coherent application. -Get started by **creating a new site**. +Ideally, you already have some familiarity with web applications and the [Ruby language](https://www.ruby-lang.org/en/). -Or **try Docusaurus immediately** with **[docusaurus.new](https://docusaurus.new)**. +:::tip Hanami 2.0 is beta software -### What you'll need +Hanami 2 currently beta software. If you encounter an issue with these guides, please raise an issue or contribute a correction on our guides repository. If you encounter what you think is a bug with Hanami, please raise an issue on the [Hanami repository](https://github.com/hanami/hanami). -- [Node.js](https://nodejs.org/en/download/) version 16.14 or above: - - When installing Node.js, you are recommended to check all checkboxes related to dependencies. +::: -## Generate a new site +## Creating a Hanami application -Generate a new Docusaurus site using the **classic template**. +### Prerequisites -The classic template will automatically be added to your project after you run the command: +To create a Hanami application, you will need Ruby 3.0 or greater. Check your ruby version: ```bash -npm init docusaurus@latest my-website classic +ruby --version ``` -You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor. +If you need to install Ruby, follow with the instructions on [rubylang.org](https://www.ruby-lang.org/en/documentation/installation/). -The command also installs all necessary dependencies you need to run Docusaurus. +### Installing the gem -## Start your site +In order to create a Hanami application, first install the hanami gem: -Run the development server: +```bash +gem install hanami --pre +``` + +### Generating your first application + +Hanami provides a `hanami new` command for generating a new application. Let's use it to create a new application called "bookshelf": ```bash -cd my-website -npm run start +hanami new bookshelf +``` + +Here's what was generated for us: + +```shell title="Generated files" +cd bookshelf +tree . +. +├── Gemfile +├── Gemfile.lock +├── Guardfile +├── README.md +├── Rakefile +├── app +│   ├── action.rb +│   └── actions +├── config +│   ├── app.rb +│   ├── puma.rb +│   ├── routes.rb +│   └── settings.rb +├── config.ru +├── lib +│   ├── bookshelf +│   │   └── types.rb +│   └── tasks +└── spec + ├── requests + │   └── root_spec.rb + ├── spec_helper.rb + └── support + ├── requests.rb + └── rspec.rb + +9 directories, 16 files +``` + +As you can see, a new Hanami application has just 16 files in total. We'll look at each file as this guide progresses but for now let's get our new application running. + +In the bookshelf directory, run: + +```shell +hanami server +``` + +If all has gone well, you should see this output: + +``` +Puma starting in single mode... +* Puma version: 5.6.5 +* Min threads: 0 +* Max threads: 5 +* Environment: development +* PID: 31634 +* Listening on http://0.0.0.0:2300 +Use Ctrl-C to stop +``` + +Visit your application in the browser at [http://localhost:2300](http://localhost:2300) + +``` +open http://localhost:2300 +``` + +You should see "Hello from Hanami"! + +:::tip A note on bundle install + +You may have noticed that we did not run `bundle install` before starting our server. That's because the `hanami new` command runs `bundle install` for you. To opt out of this behaviour, run `hanami new --skip-bundle`. See `hanami new --help` for more options. + +::: + + +## Anatomy of a generated application + +Before we add our first functionality, let's take a brief look at some of the key files in your freshly generated application. + +### The `App` class + +As we'll explore in this guide, `config/app.rb` defines the `App` class - the core of your application. + +```ruby title="config/app.rb" +# frozen_string_literal: true + +require "hanami" + +module Bookshelf + class App < Hanami::App + end +end +``` + +This class allows you to configure application-level behaviours, while also providing a way to do things like booting your application, or starting or stopping your application's [providers](/docs/application-architecture/providers). + +It's also the interface some core components, such as your application's settings, via `Hanami.app["settings"]`, and its logger, via `Hanami.app["logger"]`. + +Read more about [Hanami's app class](#). + +### Routes + +Your application's routes are defined using the `Routes` class in `config/routes.rb`. + +```ruby title="config/routes.rb" +# frozen_string_literal: true + +module Bookshelf + class Routes < Hanami::Routes + root { "Hello from Hanami" } + + get "/books/:id", to: "books.show" + end +end +``` + +Routes in Hanami most commonly invoke what Hanami calls actions. For example, with the routes configuration above, a GET request to `/books/1` will call a `"books.show"` action, defined in your application's `app/actions` folder: + +```ruby title="app/actions/books/show.rb" +# frozen_string_literal: true + +module Bookshelf + module Actions + module Books + class Show < Bookshelf::Action + def handle(request, response) + # Show the book specified by request.params[:id] here + end + end + end + end +end +``` + +Read more about routes an actions in [HTTP handling](/docs/category/http-handling). + +### Settings + +Hanami provides a `Settings` class where you can define the custom settings that your application needs. + +```ruby title="config/settings.rb" +# frozen_string_literal: true + +require "bookshelf/types" + +module Bookshelf + class Settings < Hanami::Settings + # Define your app settings here, for example: + # + setting :my_flag, default: false, constructor: Types::Params::Bool + end +end +``` + +Settings read from the environment (i.e. the `my_flag` setting above takes its value from the `MY_FLAG` environment variable), and are available either through `Hanami.app["settings"]` or by injecting the `"settings"` component via Hanami's dependency injection mechanism (see [Dependency injection](#)). + +A **default** and a **constructor** can be optionally specified for each setting. The `Types::Params::Bool` constructor above will coerce any value found in the `MY_FLAG` environment variable into a boolean value. And because of the `default: false` argument, if the `MY_FLAG` environment variable is absent, `my_flag` will be `false`. + +This means that you can trust `Hanami.app["settings"].my_flag` to always return a boolean. + +Read more about [Hanami's settings](#). + + +## Commands + +Hanami ships with useful commands for things like starting a console, inspecting routes and generating code. + +To list available commands, run: + +```shell +bundle exec hanami --help + +Commands: + hanami console # App REPL + hanami generate [SUBCOMMAND] + hanami install + hanami middlewares # List all the registered middlewares + hanami routes # Inspect application + hanami server # Start Hanami server + hanami version ``` -The `cd` command changes the directory you're working with. In order to work with your newly created Docusaurus site, you'll need to navigate the terminal there. +We'll see most of these commands at play in this guide. For complete information on these commands, see the [Hanami reference](#). -The `npm run start` command builds your website locally and serves it through a development server, ready for you to view at http://localhost:3000/. +## What's next? -Open `docs/intro.md` (this page) and edit some lines: the site **reloads automatically** and displays your changes. +Now that we're a little acquainted, let's examine the structure of a Hanami application in more detail through [containers, providers and slices](/docs/category/application-architecture). diff --git a/docs/tutorial-basics/_category_.json b/docs/tutorial-basics/_category_.json deleted file mode 100644 index 2e6db55..0000000 --- a/docs/tutorial-basics/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "Tutorial - Basics", - "position": 2, - "link": { - "type": "generated-index", - "description": "5 minutes to learn the most important Docusaurus concepts." - } -} diff --git a/docs/tutorial-basics/congratulations.md b/docs/tutorial-basics/congratulations.md deleted file mode 100644 index 04771a0..0000000 --- a/docs/tutorial-basics/congratulations.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -sidebar_position: 6 ---- - -# Congratulations! - -You have just learned the **basics of Docusaurus** and made some changes to the **initial template**. - -Docusaurus has **much more to offer**! - -Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**. - -Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610) - -## What's next? - -- Read the [official documentation](https://docusaurus.io/) -- Modify your site configuration with [`docusaurus.config.js`](https://docusaurus.io/docs/api/docusaurus-config) -- Add navbar and footer items with [`themeConfig`](https://docusaurus.io/docs/api/themes/configuration) -- Add a custom [Design and Layout](https://docusaurus.io/docs/styling-layout) -- Add a [search bar](https://docusaurus.io/docs/search) -- Find inspirations in the [Docusaurus showcase](https://docusaurus.io/showcase) -- Get involved in the [Docusaurus Community](https://docusaurus.io/community/support) diff --git a/docs/tutorial-basics/create-a-blog-post.md b/docs/tutorial-basics/create-a-blog-post.md deleted file mode 100644 index ea472bb..0000000 --- a/docs/tutorial-basics/create-a-blog-post.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Create a Blog Post - -Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed... - -## Create your first Post - -Create a file at `blog/2021-02-28-greetings.md`: - -```md title="blog/2021-02-28-greetings.md" ---- -slug: greetings -title: Greetings! -authors: - - name: Joel Marcey - title: Co-creator of Docusaurus 1 - url: https://github.com/JoelMarcey - image_url: https://github.com/JoelMarcey.png - - name: Sébastien Lorber - title: Docusaurus maintainer - url: https://sebastienlorber.com - image_url: https://github.com/slorber.png -tags: [greetings] ---- - -Congratulations, you have made your first post! - -Feel free to play around and edit this post as much you like. -``` - -A new blog post is now available at [http://localhost:3000/blog/greetings](http://localhost:3000/blog/greetings). diff --git a/docs/tutorial-basics/create-a-document.md b/docs/tutorial-basics/create-a-document.md deleted file mode 100644 index ffddfa8..0000000 --- a/docs/tutorial-basics/create-a-document.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Create a Document - -Documents are **groups of pages** connected through: - -- a **sidebar** -- **previous/next navigation** -- **versioning** - -## Create your first Doc - -Create a Markdown file at `docs/hello.md`: - -```md title="docs/hello.md" -# Hello - -This is my **first Docusaurus document**! -``` - -A new document is now available at [http://localhost:3000/docs/hello](http://localhost:3000/docs/hello). - -## Configure the Sidebar - -Docusaurus automatically **creates a sidebar** from the `docs` folder. - -Add metadata to customize the sidebar label and position: - -```md title="docs/hello.md" {1-4} ---- -sidebar_label: 'Hi!' -sidebar_position: 3 ---- - -# Hello - -This is my **first Docusaurus document**! -``` - -It is also possible to create your sidebar explicitly in `sidebars.js`: - -```js title="sidebars.js" -module.exports = { - tutorialSidebar: [ - 'intro', - // highlight-next-line - 'hello', - { - type: 'category', - label: 'Tutorial', - items: ['tutorial-basics/create-a-document'], - }, - ], -}; -``` diff --git a/docs/tutorial-basics/create-a-page.md b/docs/tutorial-basics/create-a-page.md deleted file mode 100644 index 20e2ac3..0000000 --- a/docs/tutorial-basics/create-a-page.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Create a Page - -Add **Markdown or React** files to `src/pages` to create a **standalone page**: - -- `src/pages/index.js` → `localhost:3000/` -- `src/pages/foo.md` → `localhost:3000/foo` -- `src/pages/foo/bar.js` → `localhost:3000/foo/bar` - -## Create your first React Page - -Create a file at `src/pages/my-react-page.js`: - -```jsx title="src/pages/my-react-page.js" -import React from 'react'; -import Layout from '@theme/Layout'; - -export default function MyReactPage() { - return ( - -

My React page

-

This is a React page

-
- ); -} -``` - -A new page is now available at [http://localhost:3000/my-react-page](http://localhost:3000/my-react-page). - -## Create your first Markdown Page - -Create a file at `src/pages/my-markdown-page.md`: - -```mdx title="src/pages/my-markdown-page.md" -# My Markdown page - -This is a Markdown page -``` - -A new page is now available at [http://localhost:3000/my-markdown-page](http://localhost:3000/my-markdown-page). diff --git a/docs/tutorial-basics/deploy-your-site.md b/docs/tutorial-basics/deploy-your-site.md deleted file mode 100644 index 1c50ee0..0000000 --- a/docs/tutorial-basics/deploy-your-site.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Deploy your site - -Docusaurus is a **static-site-generator** (also called **[Jamstack](https://jamstack.org/)**). - -It builds your site as simple **static HTML, JavaScript and CSS files**. - -## Build your site - -Build your site **for production**: - -```bash -npm run build -``` - -The static files are generated in the `build` folder. - -## Deploy your site - -Test your production build locally: - -```bash -npm run serve -``` - -The `build` folder is now served at [http://localhost:3000/](http://localhost:3000/). - -You can now deploy the `build` folder **almost anywhere** easily, **for free** or very small cost (read the **[Deployment Guide](https://docusaurus.io/docs/deployment)**). diff --git a/docs/tutorial-basics/markdown-features.mdx b/docs/tutorial-basics/markdown-features.mdx deleted file mode 100644 index 6b3aaaa..0000000 --- a/docs/tutorial-basics/markdown-features.mdx +++ /dev/null @@ -1,146 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Markdown Features - -Docusaurus supports **[Markdown](https://daringfireball.net/projects/markdown/syntax)** and a few **additional features**. - -## Front Matter - -Markdown documents have metadata at the top called [Front Matter](https://jekyllrb.com/docs/front-matter/): - -```text title="my-doc.md" -// highlight-start ---- -id: my-doc-id -title: My document title -description: My document description -slug: /my-custom-url ---- -// highlight-end - -## Markdown heading - -Markdown text with [links](./hello.md) -``` - -## Links - -Regular Markdown links are supported, using url paths or relative file paths. - -```md -Let's see how to [Create a page](/create-a-page). -``` - -```md -Let's see how to [Create a page](./create-a-page.md). -``` - -**Result:** Let's see how to [Create a page](./create-a-page.md). - -## Images - -Regular Markdown images are supported. - -You can use absolute paths to reference images in the static directory (`static/img/docusaurus.png`): - -```md -![Docusaurus logo](/img/docusaurus.png) -``` - -![Docusaurus logo](/img/docusaurus.png) - -You can reference images relative to the current file as well, as shown in [the extra guides](../tutorial-extras/manage-docs-versions.md). - -## Code Blocks - -Markdown code blocks are supported with Syntax highlighting. - - ```jsx title="src/components/HelloDocusaurus.js" - function HelloDocusaurus() { - return ( -

Hello, Docusaurus!

- ) - } - ``` - -```jsx title="src/components/HelloDocusaurus.js" -function HelloDocusaurus() { - return

Hello, Docusaurus!

; -} -``` - -## Admonitions - -Docusaurus has a special syntax to create admonitions and callouts: - - :::tip My tip - - Use this awesome feature option - - ::: - - :::danger Take care - - This action is dangerous - - ::: - -:::tip My tip - -Use this awesome feature option - -::: - -:::danger Take care - -This action is dangerous - -::: - -## MDX and React Components - -[MDX](https://mdxjs.com/) can make your documentation more **interactive** and allows using any **React components inside Markdown**: - -```jsx -export const Highlight = ({children, color}) => ( - { - alert(`You clicked the color ${color} with label ${children}`) - }}> - {children} - -); - -This is Docusaurus green ! - -This is Facebook blue ! -``` - -export const Highlight = ({children, color}) => ( - { - alert(`You clicked the color ${color} with label ${children}`); - }}> - {children} - -); - -This is Docusaurus green ! - -This is Facebook blue ! diff --git a/docs/tutorial-extras/_category_.json b/docs/tutorial-extras/_category_.json deleted file mode 100644 index a8ffcc1..0000000 --- a/docs/tutorial-extras/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Tutorial - Extras", - "position": 3, - "link": { - "type": "generated-index" - } -} diff --git a/docs/tutorial-extras/img/docsVersionDropdown.png b/docs/tutorial-extras/img/docsVersionDropdown.png deleted file mode 100644 index 97e4164..0000000 Binary files a/docs/tutorial-extras/img/docsVersionDropdown.png and /dev/null differ diff --git a/docs/tutorial-extras/img/localeDropdown.png b/docs/tutorial-extras/img/localeDropdown.png deleted file mode 100644 index e257edc..0000000 Binary files a/docs/tutorial-extras/img/localeDropdown.png and /dev/null differ diff --git a/docs/tutorial-extras/manage-docs-versions.md b/docs/tutorial-extras/manage-docs-versions.md deleted file mode 100644 index e12c3f3..0000000 --- a/docs/tutorial-extras/manage-docs-versions.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Manage Docs Versions - -Docusaurus can manage multiple versions of your docs. - -## Create a docs version - -Release a version 1.0 of your project: - -```bash -npm run docusaurus docs:version 1.0 -``` - -The `docs` folder is copied into `versioned_docs/version-1.0` and `versions.json` is created. - -Your docs now have 2 versions: - -- `1.0` at `http://localhost:3000/docs/` for the version 1.0 docs -- `current` at `http://localhost:3000/docs/next/` for the **upcoming, unreleased docs** - -## Add a Version Dropdown - -To navigate seamlessly across versions, add a version dropdown. - -Modify the `docusaurus.config.js` file: - -```js title="docusaurus.config.js" -module.exports = { - themeConfig: { - navbar: { - items: [ - // highlight-start - { - type: 'docsVersionDropdown', - }, - // highlight-end - ], - }, - }, -}; -``` - -The docs version dropdown appears in your navbar: - -![Docs Version Dropdown](./img/docsVersionDropdown.png) - -## Update an existing version - -It is possible to edit versioned docs in their respective folder: - -- `versioned_docs/version-1.0/hello.md` updates `http://localhost:3000/docs/hello` -- `docs/hello.md` updates `http://localhost:3000/docs/next/hello` diff --git a/docs/tutorial-extras/translate-your-site.md b/docs/tutorial-extras/translate-your-site.md deleted file mode 100644 index caeaffb..0000000 --- a/docs/tutorial-extras/translate-your-site.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Translate your site - -Let's translate `docs/intro.md` to French. - -## Configure i18n - -Modify `docusaurus.config.js` to add support for the `fr` locale: - -```js title="docusaurus.config.js" -module.exports = { - i18n: { - defaultLocale: 'en', - locales: ['en', 'fr'], - }, -}; -``` - -## Translate a doc - -Copy the `docs/intro.md` file to the `i18n/fr` folder: - -```bash -mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/ - -cp docs/intro.md i18n/fr/docusaurus-plugin-content-docs/current/intro.md -``` - -Translate `i18n/fr/docusaurus-plugin-content-docs/current/intro.md` in French. - -## Start your localized site - -Start your site on the French locale: - -```bash -npm run start -- --locale fr -``` - -Your localized site is accessible at [http://localhost:3000/fr/](http://localhost:3000/fr/) and the `Getting Started` page is translated. - -:::caution - -In development, you can only use one locale at a same time. - -::: - -## Add a Locale Dropdown - -To navigate seamlessly across languages, add a locale dropdown. - -Modify the `docusaurus.config.js` file: - -```js title="docusaurus.config.js" -module.exports = { - themeConfig: { - navbar: { - items: [ - // highlight-start - { - type: 'localeDropdown', - }, - // highlight-end - ], - }, - }, -}; -``` - -The locale dropdown now appears in your navbar: - -![Locale Dropdown](./img/localeDropdown.png) - -## Build your localized site - -Build your site for a specific locale: - -```bash -npm run build -- --locale fr -``` - -Or build your site to include all the locales at once: - -```bash -npm run build -``` diff --git a/docusaurus.config.js b/docusaurus.config.js index 092332b..b57de0c 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -68,9 +68,9 @@ const config = { type: 'doc', docId: 'intro', position: 'left', - label: 'Tutorial', + label: 'Guides', }, - {to: '/blog', label: 'Blog', position: 'left'}, + {to: '/blog', label: 'Reference', position: 'left'}, { href: 'https://github.com/hanami/hanami', label: 'GitHub', @@ -85,7 +85,7 @@ const config = { title: 'Docs', items: [ { - label: 'Tutorial', + label: 'Guides', to: '/docs/intro', }, ], @@ -126,6 +126,7 @@ const config = { prism: { theme: lightCodeTheme, darkTheme: darkCodeTheme, + additionalLanguages: ['ruby'], }, }), };