Browse Source

Documentation: GSoD - A complete tutorial on how to build your Portfolio site with Wagtail (#11217)

Co-authored-by: Meagen Voss <45881480+vossisboss@users.noreply.github.com>
Damilola Oladele 1 year ago
parent
commit
7e774971fa

+ 91 - 0
docs/advanced_tutorial/add_search.md

@@ -0,0 +1,91 @@
+# Add search to your site
+
+Using the Wagtail `start` command to start your project gives you a built-in search app. This built-in search app provides a simple search functionality for your site.
+
+However, you can customize your search template to suit your portfolio site. To customize your search template, go to your `search/templates/search.html` file and modify it as follows:
+
+```html+django
+{% extends "base.html" %}
+{% load static wagtailcore_tags %}
+
+{% block body_class %}template-searchresults{% endblock %}
+
+{% block title %}Search{% endblock %}
+
+{% block content %}
+<h1>Search</h1>
+
+<form action="{% url 'search' %}" method="get">
+    <input type="text" name="query"{% if search_query %} value="{{ search_query }}"{% endif %}>
+    <input type="submit" value="Search" class="button">
+</form>
+
+{% if search_results %}
+
+{# Add this paragraph to display the details of results found: #}
+<p>You searched{% if search_query %} for “{{ search_query }}”{% endif %}, {{ search_results.paginator.count }} result{{ search_results.paginator.count|pluralize }} found.</p>
+
+{# Replace the <ul> HTML element with the <ol> html element: #}
+<ol>
+    {% for result in search_results %}
+    <li>
+        <h4><a href="{% pageurl result %}">{{ result }}</a></h4>
+        {% if result.search_description %}
+        {{ result.search_description }}
+        {% endif %}
+    </li>
+    {% endfor %}
+</ol>
+
+{# Improve pagination by adding: #}
+{% if search_results.paginator.num_pages > 1 %}
+    <p>Page {{ search_results.number }} of {{ search_results.paginator.num_pages }}, showing {{ search_results|length }} result{{ search_results|pluralize }} out of {{ search_results.paginator.count }}</p>
+{% endif %}
+
+{% if search_results.has_previous %}
+<a href="{% url 'search' %}?query={{ search_query|urlencode }}&amp;page={{ search_results.previous_page_number }}">Previous</a>
+{% endif %}
+
+{% if search_results.has_next %}
+<a href="{% url 'search' %}?query={{ search_query|urlencode }}&amp;page={{ search_results.next_page_number }}">Next</a>
+{% endif %}
+
+{% elif search_query %}
+No results found
+{% endif %}
+{% endblock %}
+```
+
+Now, let's explain the customizations you made in the preceding template:
+
+1. You used `<p>You searched{% if search_query %} for “{{ search_query }}”{% endif %}, {{ search_results.paginator.count }} result{{ search_results.paginator.count|pluralize }} found.</p>` to display the search query, the number of results found. You also used it to display the plural form of "result" if more than one search result is found.
+
+2. You replaced the `<ul>` HTML element with the `<ol>` HTML element. The `<ol>` HTML element contains a loop iterating through each search result and displaying them as list items. Using `<ol>` gives you numbered search results.
+
+3. You improved the pagination in the template. `{% if search_results.paginator.num_pages > 1 %}` checks if there is more than one page of search results. If there is more than one page of search results, it displays the current page number, the total number of pages, the number of results on the current page, and the total number of results. `{% if search_results.has_previous %} and {% if search_results.has_next %}` checks if there are previous and next pages of search results. If they exist, it displays "Previous" and "Next" links with appropriate URLs for pagination.
+
+Now, you want to display your search across your site. One way to do this is to add it to your header. Go to your `mysite/templates/includes/header.html` file and modify it as follows:
+
+```html+django
+{% load wagtailcore_tags navigation_tags wagtailuserbar %}
+
+<header>
+    <a href="#main" class="skip-link">Skip to content</a>
+    {% get_site_root as site_root %}
+    <nav>
+        <p>
+          <a href="{% pageurl site_root %}">{{ site_root.title }}</a> |
+          {% for menuitem in site_root.get_children.live.in_menu %}
+            <a href="{% pageurl menuitem %}">{{ menuitem.title }}</a>{% if not forloop.last %} | {% endif %}
+          {% endfor %}
+
+          {# Display your search by adding this: #}
+          | <a href="/search/">Search</a>
+        </p>
+    </nav>
+
+    {% wagtailuserbar "top-right" %}
+</header>
+```
+
+Welldone! You now have a fully deployable portfolio site. The next section of this tutorial will walk you through how to deploy your site.

+ 320 - 0
docs/advanced_tutorial/create-footer_for_all_pages.md

@@ -0,0 +1,320 @@
+# Create a footer for all pages
+The next step is to create a footer for all pages of your portfolio site. You can display social media links and other information in your footer.
+
+## Add a base app
+
+Now, create a general-purpose app named `base`. To generate the `base` app, run the command, `python manage.py startapp base`.
+
+After generating the `base` app, you must install it on your site. In your `mysite/settings/base.py` file, add `"base"` to the `INSTALLED_APPS` list.
+
+```{note}
+You can structure your project in several ways aside from using a base app. For example, you can use your base app to manage configurations that are related or site-wide.
+```
+
+## Create navigation settings
+
+Now, go to your `base/models.py` and add the following lines of code:
+
+```python
+from django.db import models
+from modelcluster.models import ClusterableModel
+from wagtail.admin.panels import (
+    FieldPanel,
+    MultiFieldPanel,
+)
+from wagtail.contrib.settings.models import (
+    BaseGenericSetting,
+    register_setting,
+)
+
+@register_setting
+class NavigationSettings(BaseGenericSetting):
+    twitter_url = models.URLField(verbose_name="Twitter URL", blank=True)
+    github_url = models.URLField(verbose_name="GitHub URL", blank=True)
+    mastodon_url = models.URLField(verbose_name="Mastodon URL", blank=True)
+
+    panels = [
+        MultiFieldPanel(
+            [
+                FieldPanel("twitter_url"),
+                FieldPanel("github_url"),
+                FieldPanel("mastodon_url"),
+            ],
+            "Social settings",
+        )
+    ]
+```
+
+In the preceding code, the `register_setting` decorator registers your `NavigationSettings` models. You used the `BaseGenericSetting` base model class to define a settings model that applies to all web pages rather than just one page.
+
+Now, migrate your database by running the commands `python manage.py makemigrations` and `python manage.py migrate`. After migrating your database, reload your [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface). You'll get the error _'wagtailsettings' is not a registered namespace_. This is because you haven't install the [`wagtail.contrib.settings`](../reference/settings.md) module.
+
+The `wagtail.contrib.settings` module defines models that hold common settings across all your web pages. So, to successfully import the `BaseGenericSetting` and `register_setting`, you must install the `wagtail.contrib.settings` module on your site. To install `wagtail.contrib.settings`, go to your `mysite/settings/base.py` file and add `"wagtail.contrib.settings"` to the `INSTALLED_APPS` list.
+
+Also, you have to register the _settings_ context processor. Registering _settings_ context processor makes site-wide settings accessible in your templates. To register the _settings_ context processor, modify your `mysite/settings/base.py` file as follows:
+
+```python
+TEMPLATES = [
+    {
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
+        "DIRS": [
+            os.path.join(PROJECT_DIR, "templates"),
+        ],
+        "APP_DIRS": True,
+        "OPTIONS": {
+            "context_processors": [
+                "django.template.context_processors.debug",
+                "django.template.context_processors.request",
+                "django.contrib.auth.context_processors.auth",
+                "django.contrib.messages.context_processors.messages",
+
+                # Add this to register the _settings_ context processor:
+                "wagtail.contrib.settings.context_processors.settings",
+            ],
+        },
+    },
+]
+```
+
+(add_your_social_media_links)=
+
+## Add your social media links
+
+To add your social media links, reload your admin interface and click **Settings** from your [Sidebar](https://guide.wagtail.org/en-latest/how-to-guides/find-your-way-around/#the-sidebar). You can see your **Navigation Settings**. Clicking the **Navigation Settings** gives you a form to add your social media account links.
+
+## Display social media links
+
+You must provide a template to display the social media links you added through the admin interface.
+
+Create an `includes` folder in your `mysite/templates` folder. Then in your newly created `mysite/templates/includes` folder, create a `footer.html` file and add the following to it:
+
+```html+django
+<footer>
+    <p>Built with Wagtail</p>
+
+    {% with twitter_url=settings.base.NavigationSettings.twitter_url github_url=settings.base.NavigationSettings.github_url mastodon_url=settings.base.NavigationSettings.mastodon_url %}
+        {% if twitter_url or github_url or mastodon_url %}
+            <p>
+                Follow me on:
+                {% if github_url %}
+                    <a href="{{ github_url }}">GitHub</a>
+                {% endif %}
+                {% if twitter_url %}
+                    <a href="{{ twitter_url }}">Twitter</a>
+                {% endif %}
+                {% if mastodon_url %}
+                    <a href="{{ mastodon_url }}">Mastodon</a>
+                {% endif %}
+            </p>
+        {% endif %}
+    {% endwith %}
+</footer>
+```
+
+Now, go to your `mysite/templates/base.html` file and modify it as follows:
+
+```
+<body class="{% block body_class %}{% endblock %}">
+    {% wagtailuserbar %}
+
+    {% block content %}{% endblock %}
+
+    {# Add this to the file: #}
+    {% include "includes/footer.html" %}
+
+    {# Global javascript #}
+    <script type="text/javascript" src="{% static 'js/mysite.js' %}"></script>
+
+    {% block extra_js %}
+    {# Override this in templates to add extra javascript #}
+    {% endblock %}
+</body>
+```
+
+Now, reload your [homepage](http://127.0.0.1:8000). You'll see your social media links at the bottom of your homepage.
+
+# Create editable footer text with Wagtail Snippets
+
+Having only your social media links in your portfolio footer isn't ideal. You can add other items, like site credits and copyright notices, to your footer. One way to do this is to use the Wagtail [snippet](../topics/snippets/index.md) feature to create an editable footer text in your admin interface and display it in your site's footer.
+
+To add a footer text snippet to your admin interface, modify your `base/model.py` file as follows:
+
+```python
+from django.db import models
+from wagtail.admin.panels import (
+    FieldPanel,
+    MultiFieldPanel,
+
+    # import PublishingPanel:
+    PublishingPanel,
+)
+
+# import RichTextField:
+from wagtail.fields import RichTextField
+
+# import DraftStateMixin, PreviewableMixin, RevisionMixin, TranslatableMixin:
+from wagtail.models import (
+    DraftStateMixin,
+    PreviewableMixin,
+    RevisionMixin,
+    TranslatableMixin,
+)
+
+from wagtail.contrib.settings.models import (
+    BaseGenericSetting,
+    register_setting,
+)
+
+# import register_snippet:
+from wagtail.snippets.models import register_snippet
+
+# ...keep the definition of the NavigationSettings model and add the FooterText model:
+@register_snippet
+class FooterText(
+    DraftStateMixin,
+    RevisionMixin,
+    PreviewableMixin,
+    TranslatableMixin,
+    models.Model,
+):
+
+    body = RichTextField()
+
+    panels = [
+        FieldPanel("body"),
+        PublishingPanel(),
+    ]
+
+    def __str__(self):
+        return "Footer text"
+
+    def get_preview_template(self, request, mode_name):
+        return "base.html"
+
+    def get_preview_context(self, request, mode_name):
+        return {"footer_text": self.body}
+
+    class Meta(TranslatableMixin.Meta):
+        verbose_name_plural = "Footer Text"
+```
+
+In the preceding code, the `FooterText` class inherits from several `Mixins`, the `DraftStateMixin`, `RevisionMixin`, `PreviewableMixin`, and `TranslatableMixin`. In Django, `Mixins` are reusable pieces of code that defines additional functionality. They are implemented as Python classes, so you can inherit their methods and properties.
+
+Since your `FooterText` model is a Wagtail snippet, you must manually add `Mixins` to your model. This is because snippets aren't Wagtail `Pages` in their own right. Wagtail `Pages` don't require `Mixins` because they already have them.
+
+`DraftStateMixin` is an abstract model that you can add to any non-page Django model. You can use it for drafts or unpublished changes. The `DraftStateMixin` requires `RevisionMixin`.
+
+`RevisionMixin` is an abstract model that you can add to any non-page Django model to save revisions of its instances. Every time you edit a page, Wagtail creates a new `Revision` and saves it in your database. You can use `Revision` to find the history of all the changes that you make. `Revision` also provides a place to keep new changes before they go live.
+
+`PreviewableMixin` is a `Mixin` class that you can add to any non-page Django model to preview any changes made.
+
+`TranslatableMixin` is an abstract model you can add to any non-page Django model to make it translatable.
+
+Also, with Wagtail, you can set publishing schedules for changes you made to a Snippet. You can use the `PublishingPanel()` method to schedule `revisions` in your `FooterText`.
+
+The `__str__` method defines a human-readable string representation of an instance of the `FooterText` class. It returns the string "Footer text".
+
+The `get_preview_template` method determines the template for rendering the preview. It returns the template name _"base.html"_.
+
+The `get_preview_context` method defines the context data that you can use to render the preview template. It returns a key "footer_text" with the content of the body field as its value.
+
+The `Meta` class holds metadata about the model. It inherits from the `TranslatableMixin.Meta` class and sets the `verbose_name_plural` attribute to _"Footer Text"_.
+
+Now, migrate your database by running `python manage.py makemigrations` and `python manage.py migrate`. After migrating, restart your server and then reload your [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface). You can now find **Snippets** in your [Sidebar](https://guide.wagtail.org/en-latest/how-to-guides/find-your-way-around/).
+
+(add_footer_text)=
+
+## Add footer text
+
+To add your footer text, go to your [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface). Click **Snippets** in your [Sidebar](https://guide.wagtail.org/en-latest/how-to-guides/find-your-way-around/#the-sidebar) and add your footer text.
+
+## Display your footer text
+
+In this tutorial, you'll use a custom template tag to display your footer text.
+
+In your `base` folder, create a `templatetags` folder. Within your new `templatetags` folder, create the following files:
+- `__init__.py`
+- `navigation_tags.py`
+
+Leave your `base/templatetags/__init__.py` file blank and add the following to your `base/templatetags/navigation_tags.py` file:
+
+```python
+from django import template
+
+from base.models import FooterText
+
+register = template.Library()
+
+
+@register.inclusion_tag("base/includes/footer_text.html", takes_context=True)
+def get_footer_text(context):
+    footer_text = context.get("footer_text", "")
+
+    if not footer_text:
+        instance = FooterText.objects.filter(live=True).first()
+        footer_text = instance.body if instance else ""
+
+    return {
+        "footer_text": footer_text,
+    }
+```
+
+In the preceding code, you imported the `template` module. You can use it to create and render template tags and filters. Also, you imported the `FooterText` model from your `base/models.py` file.
+
+`register = template.Library()` creates an instance of the `Library` class from the template module. You can use this instance to register custom template tags and filters.
+
+`@register.inclusion_tag("base/includes/footer_text.html", takes_context=True)` is a decorator that registers an inclusion tag named `get_footer_text`. `"base/includes/footer_text.html"` is the template path that you'll use to render the inclusion tag. `takes_context=True ` indicates that the context of your `footer_text.html` template will be passed as an argument to your inclusion tag function.
+
+The `get_footer_text` inclusion tag function takes a single argument named `context`. `context` represents the template context where you'll use the tag.
+
+`footer_text = context.get("footer_text", "")` tries to retrieve a value from the context using the key `footer_text`. The `footer_text` variable stores any retrieved value. If there is no `footer_text` value within the context, then the variable stores an empty string `""`.
+
+The `if` statement in the `get_footer_text` inclusion tag function checks whether the `footer_text` exists within the context. If it doesn't, the `if` statement proceeds to retrieve the first published instance of the `FooterText` from the database. If a published instance is found, the statement extracts the `body` content from it. However, if there's no published instance available, it defaults to an empty string.
+
+Finally, the function returns a dictionary containing the `"footer_text"` key with the value of the retrieved `footer_text` content.
+You'll use this dictionary as context data when rendering your `footer_text` template.
+
+To use the returned dictionary in, create a `templates/base/includes` folder in your `base` folder. Then create a `footer_text.html` file in your `base/templates/base/includes/` folder and add the following to it:
+
+```html+django
+{% load wagtailcore_tags %}
+
+<div>
+    {{ footer_text|richtext }}
+</div>
+```
+
+Add your `footer_text` template to your footer by modifying your `mysite/templates/includes/footer.html` file:
+
+```html+django
+{# Load navigation_tags at the top of the file: #}
+{% load navigation_tags %}
+
+<footer>
+    <p>Built with Wagtail</p>
+
+    {% with twitter_url=settings.base.NavigationSettings.twitter_url github_url=settings.base.NavigationSettings.github_url mastodon_url=settings.base.NavigationSettings.mastodon_url %}
+        {% if twitter_url or github_url or mastodon_url %}
+            <p>
+                Follow me on:
+                {% if github_url %}
+                    <a href="{{ github_url }}">GitHub</a>
+                {% endif %}
+                {% if twitter_url %}
+                    <a href="{{ twitter_url }}">Twitter</a>
+                {% endif %}
+                {% if mastodon_url %}
+                    <a href="{{ mastodon_url }}">Mastodon</a>
+                {% endif %}
+            </p>
+        {% endif %}
+    {% endwith %}
+
+    {# Add footer_text: #}
+    {% get_footer_text %}
+</footer>
+```
+
+Now, restart your server and reload your [homepage](http://127.0.0.1:8000/). For more information on how to render your Wagtail snippets, read [Rendering snippets](../topics/snippets/rendering.md).
+
+Well done! 👏 You now have a footer across all pages of your portfolio site. In the next section of this tutorial, you'll learn how to set up a site menu for linking to your homepage and other pages as you add them.

+ 153 - 0
docs/advanced_tutorial/create_contact_page.md

@@ -0,0 +1,153 @@
+# Create contact page
+
+Having a contact page on your portfolio site will help you connect with potential clients, employers, or other professionals who are interested in your skills.
+
+In this section of the tutorial, you'll add a contact page to your portfolio site using Wagtail forms.
+
+Start by modifying your `base/models.py` file:
+
+```python
+from django.db import models
+
+# import parentalKey:
+from modelcluster.fields import ParentalKey
+
+# import FieldRowPanel and InlinePanel:
+from wagtail.admin.panels import (
+    FieldPanel,
+    FieldRowPanel,
+    InlinePanel,
+    MultiFieldPanel,
+    PublishingPanel,
+)
+
+from wagtail.fields import RichTextField
+from wagtail.models import (
+    DraftStateMixin,
+    PreviewableMixin,
+    RevisionMixin,
+    TranslatableMixin,
+)
+
+# import AbstractEmailForm and AbstractFormField:
+from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField
+
+# import FormSubmissionsPanel:
+from wagtail.contrib.forms.panels import FormSubmissionsPanel
+from wagtail.contrib.settings.models import (
+    BaseGenericSetting,
+    register_setting,
+)
+from wagtail.snippets.models import register_snippet
+
+
+# ... keep the definition of NavigationSettings and FooterText. Add FormField and FormPage:
+class FormField(AbstractFormField):
+    page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields')
+
+
+class FormPage(AbstractEmailForm):
+    intro = RichTextField(blank=True)
+    thank_you_text = RichTextField(blank=True)
+
+    content_panels = AbstractEmailForm.content_panels + [
+        FormSubmissionsPanel(),
+        FieldPanel('intro'),
+        InlinePanel('form_fields', label="Form fields"),
+        FieldPanel('thank_you_text'),
+        MultiFieldPanel([
+            FieldRowPanel([
+                FieldPanel('from_address'),
+                FieldPanel('to_address'),
+            ]),
+            FieldPanel('subject'),
+        ], "Email"),
+    ]
+```
+
+In the preceding code, your `FormField` model inherits from `AbstractFormField`. With`AbstractFormField`, you can define any form field type of your choice in the admin interface. `page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields')` defines a parent-child relationship between the `FormField` and `FormPage` models.
+
+On the other hand, your `FormPage` model inherits from `AbstractEmailForm`. Unlike `AbstractFormField`, `AbstractEmailForm` offers a form-to-email capability. Also, it defines the `to_address`, `from_address`, and `subject` fields. It expects a `form_fields` to be defined.
+
+After defining your `FormField` and `FormPage` models, you must create `form_page` and `form_page_landing` templates. The `form_page` template differs from a standard Wagtail template because it's passed a variable named `form` containing a Django `Form` object in addition to the usual `Page` variable. The `form_page_landing.html`, on the other hand, is a standard Wagtail template. Your site displays the `form_page_landing.html` after a user makes a successful form submission.
+
+Now, create a `base/templates/base/form_page.html` file and add the following to it:
+
+```html+django
+{% extends "base.html" %}
+{% load wagtailcore_tags %}
+
+{% block body_class %}template-formpage{% endblock %}
+
+{% block content %}
+    <h1>{{ page.title }}</h1>
+    <div>{{ page.intro|richtext }}</div>
+
+    <form class="page-form" action="{% pageurl page %}" method="POST">
+        {% csrf_token %}
+        {{ form.as_div }}
+        <button type="Submit">Submit</button>
+    </form>
+{% endblock content %}
+```
+
+Also, create a `base/templates/base/form_page_landing.html` file and add the following to it:
+
+```html+django
+{% extends "base.html" %}
+{% load wagtailcore_tags %}
+
+{% block body_class %}template-formpage{% endblock %}
+
+{% block content %}
+    <h1>{{ page.title }}</h1>
+    <div>{{ page.thank_you_text|richtext }}</div>
+{% endblock content %}
+```
+
+Now, you’ve added all the necessary lines of code and templates that you need to create a contact page on your portfolio website.
+
+Now, migrate your database by running `python manage.py makemigrations` and then `python manage.py migrate`.
+
+## Add your contact information
+
+To add contact information to your portfolio site, follow these steps:
+
+1. Create a **Form page** as a child page of **Home** by following these steps:  
+
+    a. Restart your server.  
+    b. Go to your admin interface.  
+    c. Click `Pages` in your [Sidebar](https://guide.wagtail.org/en-latest/how-to-guides/find-your-way-around/#the-sidebar).  
+    d. Click `Home`.  
+    e. Click the `...` icon at the top of the resulting page.  
+    f. Click `add child page`.  
+    g. Click `Form page`.  
+
+2. Add the necessary data.
+3. Publish your `Form Page`.
+
+## Style your contact page
+
+To style your contact page, add the following CSS to your `mysite/static/css/mysite.css` file:
+
+```css
+.page-form label {
+  display: block;
+  margin-top: 10px;
+  margin-bottom: 5px;
+}
+
+.page-form :is(textarea, input, select) {
+  width: 100%;
+  max-width: 500px;
+  min-height: 40px;
+  margin-top: 5px;
+  margin-bottom: 10px;
+}
+
+.page-form .helptext {
+  font-style: italic;
+}
+```
+
+In the next section of this tutorial, you'll learn how to add a portfolio page to your site.

+ 393 - 0
docs/advanced_tutorial/create_portfolio_page.md

@@ -0,0 +1,393 @@
+# Create a portfolio page
+
+A portfolio page is a web page that has your resume or Curriculum Vitae (CV). The page will give potential employers a chance to review your work experience.
+
+This tutorial shows you how to add a portfolio page to your portfolio site using the Wagtail StreamField. 
+
+First, let's explain what StreamField is.
+
+## What is StreamField?
+
+StreamField is a feature that was created to balance the need for developers to have well-structured data and the need for content creators to have editorial flexibility in how they create and organize their content.
+
+In traditional content management systems, there's often a compromise between structured content and giving editors the freedom to create flexible layouts. Typically, Rich Text fields are used to give content creators the tools they need to make flexible and versatile content. Rich Text fields can provide a WYSIWYG editor for formatting. However, Rich Text fields have limitations. 
+
+One of the limitations of Rich Text fields is the loss of semantic value. Semantic value in content denotes the underlying meaning or information conveyed by the structure and markup of content. When content lacks semantic value, it becomes more difficult to determine its intended meaning or purpose. For example, when editors use Rich Text fields to style text or insert multimedia, the content might not be semantically marked as such.
+
+So, StreamField gives editors more flexibility and addresses the limitations of Rich Text fields. StreamField is a versatile content management solution that treats content as a sequence of blocks. Each block represents different content types like paragraphs, images, and maps. Editors can arrange and customize these blocks to create complex and flexible layouts. Also, StreamField can capture the semantic meaning of different content types.
+
+## Create reusable custom blocks
+
+Now that you know what StreamField is, let's guide you through using it to add a portfolio page to your site.
+
+Start by adding a new app to your portfolio site by running the following command:
+
+```sh
+python manage.py startapp portfolio
+```
+
+Install your new portfolio app to your site by adding _"portfolio"_ to the `INSTALLED_APPS` list in your `mysite/settings/base.py` file.
+
+Now create a `base/blocks.py` file and add the following lines of code to it:
+
+```python
+from wagtail.blocks import (
+    CharBlock,
+    ChoiceBlock,
+    RichTextBlock,
+    StreamBlock,
+    StructBlock,
+)
+from wagtail.embeds.blocks import EmbedBlock
+from wagtail.images.blocks import ImageChooserBlock
+
+
+class ImageBlock(StructBlock):
+    image = ImageChooserBlock(required=True)
+    caption = CharBlock(required=False)
+    attribution = CharBlock(required=False)
+
+    class Meta:
+        icon = "image"
+        template = "base/blocks/image_block.html"
+
+
+class HeadingBlock(StructBlock):
+    heading_text = CharBlock(classname="title", required=True)
+    size = ChoiceBlock(
+        choices=[
+            ("", "Select a heading size"),
+            ("h2", "H2"),
+            ("h3", "H3"),
+            ("h4", "H4"),
+        ],
+        blank=True,
+        required=False,
+    )
+
+    class Meta:
+        icon = "title"
+        template = "base/blocks/heading_block.html"
+
+
+class BaseStreamBlock(StreamBlock):
+    heading_block = HeadingBlock()
+    paragraph_block = RichTextBlock(icon="pilcrow")
+    image_block = ImageBlock()
+    embed_block = EmbedBlock(
+        help_text="Insert a URL to embed. For example, https://www.youtube.com/watch?v=SGJFWirQ3ks",
+        icon="media",
+    )
+```
+
+In the preceding code, you created reusable Wagtail custom blocks for different content types in your general-purpose app. You can use these blocks across your site in any order. Let's take a closer look at each of these blocks.
+
+First, `ImageBlock` is a block that editors can use to add images to a StreamField section.
+
+```python
+class ImageBlock(StructBlock):
+    image = ImageChooserBlock(required=True)
+    caption = CharBlock(required=False)
+    attribution = CharBlock(required=False)
+    class Meta:
+        icon = "image"
+        template = "base/blocks/image_block.html"
+```
+
+`ImageBlock` inherits from `StructBlock`. `ImageBlock`inherits from `StructBlock`. With `StructBlock`, you can group several child blocks together under a single parent block. Your `ImageBlock` has three child blocks. The first child block, `Image`, uses the `ImageChooserBlock` field block type. With `ImageChooserBlock`, editors can select an existing image or upload a new one. Its `required` argument has a value of `true`, which means that you must provide an image for the block to work. The `caption` and `attribution` child blocks use the `CharBlock` field block type, which provides single-line text inputs for adding captions and attributions to your images. Your `caption` and `attribution` child blocks have their `required` attributes set to `false`. That means you can leave them empty in your [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface) if you want to.
+
+Just like `ImageBlock`, your `HeadingBlock` also inherits from `StructBlock`. It has two child blocks. Let's look at those.
+
+```python
+class HeadingBlock(StructBlock):
+    heading_text = CharBlock(classname="title", required=True)
+    size = ChoiceBlock(
+        choices=[
+            ("", "Select a heading size"),
+            ("h2", "H2"),
+            ("h3", "H3"),
+            ("h4", "H4"),
+        ],
+        blank=True,
+        required=False,
+    )
+    class Meta:
+        icon = "title"
+        template = "base/blocks/heading_block.html"
+```
+
+The first child block, `heading_text`, uses `CharBlock` for specifying the heading text, and it's required. The second child block, `size`, uses `ChoiceBlock` for selecting the heading size. It provides options for **h2**, **h3**, and **h4**. Both `blank=True` and `required=False` make the heading text optional in your [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface).
+
+Your `BaseStreamBlock` class inherits from `StreamBlock`. `StreamBlock` defines a set of child block types that you would like to include in all of the StreamField sections across a project. This class gives you a baseline collection of common blocks that you can reuse and customize for all the different page types where you use StreamField. For example, you will definitely want editors to be able to add images and paragraph text to all their pages, but you might want to create a special pull quote block that is only used on blog pages.
+
+
+```python
+class BaseStreamBlock(StreamBlock):
+    heading_block = HeadingBlock()
+    paragraph_block = RichTextBlock(icon="pilcrow")
+    image_block = ImageBlock()
+    embed_block = EmbedBlock(
+        help_text="Insert a URL to embed. For example, https://www.youtube.com/watch?v=SGJFWirQ3ks",
+        icon="media",
+    )
+```
+
+Your `BaseStreamBlock` has four child blocks. The `heading_block` uses the previously defined `HeadingBlock`. `paragraph_block` uses `RichTextBlock`, which provides a WYSIWYG editor for creating formatted text. `image_block` uses the previously defined `ImageBlock` class. `embed_block` is a block for embedding external content like videos. It uses the Wagtail `EmbedBlock`. To discover more field block types that you can use, read the [documentation on Field block types](field_block_types).
+
+Also, you defined a `Meta` class within your `ImageBlock` and `HeadingBlock` blocks. The `Meta` classes provide metadata for the blocks, including icons to visually represent them in the admin interface. The `Meta` classes also include custom templates for rendering your `ImageBlock` and `HeadingBlock` blocks.
+
+```note
+Wagtail provides built-in templates to render each block. However, you can override the built-in template with a custom template.
+```
+
+Finally, you must add the custom templates that you defined in the `Meta` classes of your `ImageBlock` and `HeadingBlock` blocks. 
+
+To add the custom template of your `ImageBlock`, create a `base/templates/base/blocks/image_block.html` file and add the following to it:
+
+```html+django
+{% load wagtailimages_tags %}
+
+<figure>
+    {% image self.image fill-600x338 loading="lazy" %}
+    <figcaption>{{ self.caption }} - {{ self.attribution }}</figcaption>
+</figure>
+```
+
+To add the custom template of your `HeadingBlock` block, create a `base/templates/base/blocks/heading_block.html` file and add the following to it:
+
+```html+django
+{% if self.size == 'h2' %}
+    <h2>{{ self.heading_text }}</h2>
+{% elif self.size == 'h3' %}
+    <h3>{{ self.heading_text }}</h3>
+{% elif self.size == 'h4' %}
+    <h4>{{ self.heading_text }}</h4>
+{% endif %}
+```
+
+```{note}
+You can also create a custom template for a child block. For example, to create a custom template for `embed_block`, create a `base/templates/base/blocks/embed_block.html` file and add the following to it:
+
+```html+django
+{{ self }}
+```
+
+```
+
+## Use the blocks you created in your portfolio app
+
+You can use the reusable custom blocks you created in your general-purpose `base` app across your site. However, it's conventional to define the blocks you want to use in a `blocks.py` file of the app you intend to use them in. Then you can import the blocks from your app's `blocks.py` file to use them in your `models.py` file.
+
+Now create a `portfolio/blocks.py` file and import the block you intend to use as follows:
+
+```python
+from base.blocks import BaseStreamBlock
+
+class PortfolioStreamBlock(BaseStreamBlock):
+    pass
+```
+
+The preceding code defines a custom block named `PortfolioStreamBlock`, which inherits from `BaseStreamBlock`. The pass statement indicates a starting point. Later in the tutorial, you'll add custom block definitions and configurations to the `PortfolioStreamBlock`.
+
+Now add the following to your 
+`portfolio/models.py` file:
+
+```python
+from wagtail.models import Page
+from wagtail.fields import StreamField
+from wagtail.admin.panels import FieldPanel
+
+from portfolio.blocks import PortfolioStreamBlock
+
+
+class PortfolioPage(Page):
+    parent_page_types = ["home.HomePage"]
+
+    body = StreamField(
+        PortfolioStreamBlock(),
+        blank=True,
+        use_json_field=True,
+        help_text="Use this section to list your projects and skills.",
+    )
+
+    content_panels = Page.content_panels + [
+        FieldPanel("body"),
+    ]
+```
+
+In the preceding code, you defined a Wagtail `Page` named `PortfolioPage`. `parent_page_types = ["home.HomePage"]` specifies that your Portfolio page can only be a child page of Home Page. Your `body` field is a `StreamField`, which uses the `PortfolioStreamBlock` custom block that you imported from your `portfolio/blocks.py` file. `blank=True` indicates that you can leave this field empty in your admin interface. `help_text` provides a brief description of the field to guide editors.
+
+Your next step is to create a template for your `PortfolioPage`. To do this, create a `portfolio/templates/portfolio/portfolio_page.html` file and add the following to it:
+
+```html+django
+{% extends "base.html" %}
+
+{% load wagtailcore_tags wagtailimages_tags %}
+
+{% block body_class %}template-portfolio{% endblock %}
+
+{% block content %}
+    <h1>{{ page.title }}</h1>
+
+    {{ page.body }}
+{% endblock %}
+```
+
+Now migrate your database by running `python manage.py makemigrations` and then `python manage.py migrate`.
+
+## Add more custom blocks
+
+To add more custom blocks to your `PortfolioPage`'s body, modify your `portfolio/blocks.py` file:
+
+```python
+# import CharBlock, ListBlock, PageChooserBlock, PageChooserBlock, RichTextBlock, and StructBlock: 
+from wagtail.blocks import (
+    CharBlock,
+    ListBlock,
+    PageChooserBlock,
+    RichTextBlock,
+    StructBlock,
+)
+
+# import ImageChooserBlock:
+from wagtail.images.blocks import ImageChooserBlock
+
+from base.blocks import BaseStreamBlock
+
+# add CardBlock:
+class CardBlock(StructBlock):
+    heading = CharBlock()
+    text = RichTextBlock(features=["bold", "italic", "link"])
+    image = ImageChooserBlock(required=False)
+
+    class Meta:
+        icon = "form"
+        template = "portfolio/blocks/card_block.html"
+
+# add FeaturedPostsBlock:
+class FeaturedPostsBlock(StructBlock):
+    heading = CharBlock()
+    text = RichTextBlock(features=["bold", "italic", "link"], required=False)
+    posts = ListBlock(PageChooserBlock(page_type="blog.BlogPage"))
+
+    class Meta:
+        icon = "folder-open-inverse"
+        template = "portfolio/blocks/featured_posts_block.html"
+
+class PortfolioStreamBlock(BaseStreamBlock):
+    # delete the pass statement
+
+    card = CardBlock(group="Sections")
+    featured_posts = FeaturedPostsBlock(group="Sections")
+```
+
+In the preceding code, `CardBlock` has three child blocks, `heading`, `text` and `image`. You are already familiar with the field block types used by the child pages.
+
+However, in your `FeaturedPostsBlock`, one of the child blocks, `posts`, uses `ListBlock`. `ListBlock` is a structural block type that you can use for multiple sub-blocks of the same type.  You used it with `PageChooserBlock` to select only the Blog Page type pages. To better understand structural block types, read the [Structural block types documentation](streamfield_staticblock).
+
+Furthermore, `icon = "form"` and `icon = "folder-open-inverse"` define custom block icons to set your blocks apart in the admin interface. For more information about block icons, read the [documentation on block icons](block_icons).
+
+You used `group="Sections"` in `card = CardBlock(group="Sections")` and `featured_posts = FeaturedPostsBlock(group="Sections")` to categorize your `card` and `featured_posts` child blocks together within a category named `section`.
+
+You probably know what your next step is. You have to create templates for your `CardBlock` and `FeaturedPostsBlock`.
+
+To create a template for `CardBlock`, create a `portfolio/templates/portfolio/blocks/card_block.html` file and add the following to it:
+
+```html+django
+{% load wagtailcore_tags wagtailimages_tags %}
+<div class="card">
+    <h3>{{ self.heading }}</h3>
+    <div>{{ self.text|richtext }}</div>
+    {% if self.image %}
+        {% image self.image width-480 %}
+    {% endif %}
+</div>
+```
+
+To create a template for `featured_posts_block`, create a `portfolio/templates/portfolio/blocks/featured_posts_block.html` file and add the following to it:
+
+```html+django
+{% load wagtailcore_tags %}
+<div>
+    <h2>{{ self.heading }}</h2>
+    {% if self.text %}
+        <p>{{ self.text|richtext }}</p>
+    {% endif %}
+
+    <div class="grid">
+        {% for page in self.posts %}
+            <div class="card">
+                <p><a href="{% pageurl page %}">{{ page.title }}</a></p>
+                <p>{{ page.specific.date }}</p>
+            </div>
+        {% endfor %}
+    </div>
+</div>
+```
+
+Finally, migrate your changes by running `python manage.py makemigrations` and then `python manage.py migrate`.
+
+(add_your_resume)=
+
+## Add your resume
+
+To add your resume to your portfolio site, follow these steps:
+
+1. Create a **Portfolio Page** as a child page of **Home** by following these steps:  
+
+    a. Restart your server.  
+    b. Go to your admin interface.  
+    c. Click `Pages` in your [Sidebar](https://guide.wagtail.org/en-latest/how-to-guides/find-your-way-around/#the-sidebar).  
+    d. Click `Home`.  
+    e. Click the `...` icon at the top of the resulting page.  
+    f. Click `add child page`.  
+    g. Click `Portfolio Page`.  
+
+2. Add your resume data by following these steps:  
+    a. Use "Resume" as your page title.  
+    b. Click **+** to expand your body section.  
+    c. Click **Paragraph block**.   
+    d. Copy and paste the following text in your new **Paragraph block**:  
+
+    ```txt
+    I'm a Wagtail Developer with a proven track record of developing and maintaining complex web applications. I have experience writing custom code to extend Wagtail applications, collaborating with other developers, and integrating third-party services and APIs.
+    ```  
+
+    e. Click **+** below your preceding Paragraph block, and then click **Paragraph block** to add a new Paragraph Block.  
+    f.  Type "/" in the input field of your new Paragraph block and then click **H2 Heading 2**.  
+    g. Use "Work Experience" as your Heading 2.   
+    h. Type "/" below your Heading 2 and click **H3 Heading 3**.  
+    i. Use the following as your Heading 3:  
+
+    ```
+    Wagtail developer at TopBox, United Kingdom
+    ```  
+
+    j. Type the following after your Heading 3:
+
+    ```txt
+    January 2022 to November 2023
+
+    - Developed and maintained a complex web application using Wagtail, resulting in a 25% increase in user engagement and a 20% increase in revenue within the first year.
+    - Wrote custom code to extend Wagtail applications, resulting in a 30% reduction in development time and a 15% increase in overall code quality.
+    - Collaborated with other developers, designers, and stakeholders to integrate third-party services and APIs, resulting in a 40% increase in application functionality and user satisfaction.
+    - Wrote technical documentation and participated in code reviews, providing feedback to other developers and improving overall code quality by 20%.
+    ```  
+
+    ```{note}
+    By starting your sentences with "-", you're writing out your work experience as a Bulletted list. You can achieve the same result by typing "/" in the input field of your Paragraph block and then clicking **Bulleted list**.
+    ```
+    
+    k. Click **+** below your Work experience.  
+    l. Click **Paragraph block** to add another Paragraph block.  
+    m. Type "/" in the input field of your new Paragraph block and then click **H2 Heading 2**.  
+    n. Use "Skills" as the Heading 2 of your new Paragraph block.  
+    o. Copy and paste the following after your Heading 2:  
+
+    ```text
+    Python, Django, Wagtail, HTML, CSS, Markdown, Open-source management, Trello, Git, GitHub
+    ```  
+
+3. Publish your `Portfolio Page`.
+
+Congratulations🎉! You now understand how to create complex flexible layouts with Wagtail StreamField. In the next tutorial, you'll learn how to add search functionality to your site.

+ 137 - 0
docs/advanced_tutorial/customize_homepage.md

@@ -0,0 +1,137 @@
+# Customize your home page
+
+When building your portfolio website, the first step is to set up and personalize your homepage. The homepage is your chance to make an excellent first impression and convey the core message of your portfolio. So your homepage should include the following features:
+
+1. **Introduction:** A concise introduction captures visitors' attention.
+2. **Biography:** Include a brief biography that introduces yourself. This section should mention your name, role, expertise, and unique qualities.
+3. **Hero Image:** This may be a professional headshot or other image that showcases your work and adds visual appeal.
+4. **Call to Action (CTA):** Incorporate a CTA that guides visitors to take a specific action, such as "View Portfolio," "Hire Me," or "Learn More".
+5. **Resume:** This is a document that provides a summary of your education, work experience, achievements, and qualifications.
+
+In this section, you'll learn how to add features **1** through **4** to your homepage. You'll add your resume or CV later in the tutorial.
+
+Now, modify your `home/models` file to include the following:
+
+```python
+from django.db import models
+
+from wagtail.models import Page
+from wagtail.fields import RichTextField
+
+# import MultiFieldPanel:
+from wagtail.admin.panels import FieldPanel, MultiFieldPanel
+
+
+class HomePage(Page):
+    # add the Hero section of HomePage:
+    image = models.ForeignKey(
+        "wagtailimages.Image",
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        related_name="+",
+        help_text="Homepage image",
+    )
+    hero_text = models.CharField(
+        blank=True,
+        max_length=255, help_text="Write an introduction for the site"
+    )
+    hero_cta = models.CharField(
+        blank=True,
+        verbose_name="Hero CTA",
+        max_length=255,
+        help_text="Text to display on Call to Action",
+    )
+    hero_cta_link = models.ForeignKey(
+        "wagtailcore.Page",
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        related_name="+",
+        verbose_name="Hero CTA link",
+        help_text="Choose a page to link to for the Call to Action",
+    )
+
+    body = RichTextField(blank=True)
+
+    # modify your content_panels:
+    content_panels = Page.content_panels + [
+        MultiFieldPanel(
+            [
+                FieldPanel("image"),
+                FieldPanel("hero_text"),
+                FieldPanel("hero_cta"),
+                FieldPanel("hero_cta_link"),
+            ],
+            heading="Hero section",
+        ),
+        FieldPanel('body'),
+    ]
+```
+
+You might already be familiar with the different parts of your `HomePage` model. The `image` field is a `ForeignKey` referencing Wagtail's built-in Image model for storing images. Similarly, `hero_cta_link` is a `ForeignKey` to `wagtailcore.Page`. The `wagtailcore.Page` is the base class for all other page types in Wagtail. This means all Wagtail pages inherit from `wagtailcore.Page`. For instance, your `class HomePage(page)` inherits from `wagtailcore.Page`.
+
+Using `on_delete=models.SET_NULL` ensures that if you remove an image or hero link from your admin interface, the `image` and `hero_cta_link` fields on your Homepage will be set to null, preserving their data entries. Read the [Django documentation](https://docs.djangoproject.com/en/4.2/ref/models/fields/#django.db.models.ForeignKey.on_delete) for more values for the `on-delete` attribute.
+
+The `related_name` attribute creates a relationship between related models. For example, if you want to access the HomePage's `image` from `wagtailimages.Image`, you can use the `related_name` attribute. When you use `related_name="+"`, you create a connection between models that doesn't create a reverse relationship for your `ForeignKey` fields. In other words, you're instructing Django to create a way to access the HomePage's `image` from `wagtailimages.Image` but not a way to access `wagtailimages.Image` from the HomePage's `image`.
+
+While `body` is a `RichTextField`, `hero_text` and `hero_cta` are `CharField`, a Django string field for storing short text.
+
+The [Your First Wagtail Tutorial](../getting_started/tutorial.md) already explained `content_panels`. [FieldPanel](field_panel) and [MultiPanel](multiFieldPanel) are types of Wagtail built-in [Panels](editing_api). They're both subclasses of the base Panel class and accept all of Wagtail's `Panel` parameters in addition to their own. While the `FieldPanel` provides a widget for basic Django model fields, `MultiFieldPanel` helps you decide the structure of the editing form. For example, you can group related fields.
+
+Now that you understand the different parts of your `HomePage` model, migrate your database by running `python manage.py makemigrations` and
+then `python manage.py migrate`
+
+After migrating your database, start your server by running
+`python manage.py runserver`.
+
+(add_content_to_your_homepage)=
+
+## Add content to your homepage
+
+To add content to your homepage through the admin interface, follow these steps:
+
+1. Log in to your [admin interface](http://127.0.0.1:8000/admin/), with your admin username and password.
+2. Click Pages.
+3. Click the **pencil**  icon beside **Home**.
+4. Choose an image, choose a page, and add data to the input fields.
+
+```Note
+You can choose your home page or blog index page to link to your Call to Action. You can choose a more suitable page later in the tutorial.
+```
+
+5. Publish your Home page.
+
+You have all the necessary data for your Home page now. You can visit your Home page by going to `http://127.0.0.1:8000` in your browser. You can't see all your data, right? That’s because you must modify your Homepage template to display the data.
+
+Replace the content of your `home/templates/home/home_page.html` file with the following:
+
+```html+django
+{% extends "base.html" %}
+{% load wagtailcore_tags wagtailimages_tags %}
+
+{% block body_class %}template-homepage{% endblock %}
+
+{% block content %}
+    <div>
+        <h1>{{ page.title }}</h1>
+        {% image page.image fill-480x320 %}
+        <p>{{ page.hero_text }}</p>
+        {% if page.hero_cta_link %}
+            <a href="{% pageurl page.hero_cta_link %}">
+                {% firstof page.hero_cta page.hero_cta_link.title %}
+            </a>
+        {% endif %}
+    </div>
+
+  {{ page.body|richtext }}
+{% endblock content %}
+```
+
+In your Homepage template, notice the use of `firstof` in line 13. It's helpful to use this tag when you have created a series of fallback options, and you want to display the first one that has a value. So, in your template, the `firstof` template tag displays `page.hero_cta` if it has a value. If `page.hero_cta` doesn't have a value, then it displays `page.hero_cta_link.title`.
+
+Congratulations! You've completed the first stage of your Portfolio website 🎉🎉🎉.
+
+<!-- 
+Ask Thibaud if the Resume page is downloadable.
+-->

+ 479 - 0
docs/advanced_tutorial/deployment.md

@@ -0,0 +1,479 @@
+# Deploy your site
+
+So far, you've been accessing your site locally. Now, it's time to deploy it.
+
+Deployment makes your site publicly accessible by moving it to a production server. Upon deployment, your site becomes accessible worldwide.
+
+In this section of the tutorial, you'll use two platforms to deploy your site. You'll host your site on [fly.io](https://fly.io) and serve your site's images on [Backblaze](https://www.backblaze.com).
+
+You can use fly.io to host your site and serve your images. However, storing your images on a platform other than the one hosting your site provides better performance, security, and reliability.
+
+```note
+In this tutorial, you'll see yourname several times. Replace it with a name of your choice.
+```
+
+## Setup Backblaze B2 Cloud Storage
+
+To serve your images, set up a Backblaze B2 storage following these steps:
+
+1. Visit the Backblaze [website](https://www.backblaze.com) in your browser.
+2. Click **Products** from the top navigation and then select **B2 Cloud Storage** from the dropdown.
+3. Sign up to Backblaze B2 Cloud Storage by following these steps:  
+
+    a. Enter your email address and password.  
+    b. Select the appropriate region.  
+    c. Click **Sign Up Now**.  
+
+4. Verify your email by following these steps:  
+
+    a. Go to **Account > My Settings** in your side navigation.  
+    b. Click **Verify Email** in the **Security section**.  
+    c. Enter your sign-up email address and then click send **Send Code**.  
+    d. Check your email inbox or spam folder for the verification email.  
+    e. Click the verification link or use the verification code.  
+
+5. Create a Bucket by going to **B2 Cloud Storage > Bucket** and clicking **Create a Bucket**.
+6. Go to **B2 Cloud Storage > Bucket** and then click **Create a Bucket**.
+7. Add your Bucket information as follows:
+
+| Bucket information | Instruction |
+| -------- | ------- |
+| Bucket Unique Name | Use a unique Bucket name. For example,_yourname-wagtail-portfolio_ |
+| Files in Bucket are | Select **Public** |
+| Default Encryption | Select **Disable** |
+| Object Lock | Select **Disable** |
+
+8. Click **Create a Bucket**.
+
+## Link your site to Backblaze B2 Cloud Storage
+
+After setting up your Backblaze B2 Cloud Storage, you must link it to your portfolio site.
+
+Start by creating a `.env.production` file at the root of your project directory. At this stage, your project directory should look like this:
+
+```txt
+mysite/
+├── base
+├── blog
+├── home
+├── media
+├── mysite
+├── portfolio
+├── search
+├── .dockerignore
+├── .gitignore
+├── .env.production
+├── Dockerfile
+├── manage.py
+├── mysite/
+└── requirements.txt
+```
+
+Now add the following environment variables to your `.env.production` file:
+
+```text
+AWS_STORAGE_BUCKET_NAME=
+AWS_S3_ENDPOINT_URL=https://
+AWS_S3_REGION_NAME=
+AWS_S3_ACCESS_KEY_ID=
+AWS_S3_SECRET_ACCESS_KEY=
+DJANGO_ALLOWED_HOSTS=
+DJANGO_CSRF_TRUSTED_ORIGINS=https://
+DJANGO_SETTINGS_MODULE=mysite.settings.production
+```
+
+### Fill in your Backblaze B2 bucket information
+
+The next step is to provide values for your environment variables. In your `.env.production` file, use your Backblaze B2 bucket information as values for your environment variables as follows:
+
+| Environment variable | Instruction |
+| -------- | ------- |
+| AWS_STORAGE_BUCKET_NAME | Use your Backblaze B2 bucket name |
+| AWS_S3_ENDPOINT_URL | Use the Backblaze B2 endpoint URL. For example, _https://s3.us-east-005.backblazeb2.com_ |
+| AWS_S3_REGION_NAME | Determine your bucket's region from the endpoint URL. For example, if your endpoint URL is _s3.us-east-005.backblazeb2.com_, then your bucket's region is _us-east-005_ |
+| AWS_S3_ACCESS_KEY_ID | Leave this empty for now |
+| AWS_S3_SECRET_ACCESS_KEY | Leave this empty for now |
+| DJANGO_ALLOWED_HOSTS | Leave this empty for now |
+| DJANGO_CSRF_TRUSTED_ORIGINS    | Use _https://_ |
+| DJANGO_SETTINGS_MODULE | Use _mysite.settings.production_ |
+
+In the preceding table, you didn't provide values for your `AWS_S3_ACCESS_KEY_ID`, `AWS_S3_SECRET_ACCESS_KEY`, and `DJANGO_ALLOWED_HOSTS`.
+
+To get values for your `AWS_S3_ACCESS_KEY_ID` and `AWS_S3_SECRET_ACCESS_KEY`, follow these steps:
+
+1. Log in to your Backblaze B2 account.
+2. Navigate to **Account > Application Keys**.
+3. Click **Add a New Application Key**.
+4. Configure the application key settings as follows:
+
+| Setting | Instruction |
+| -------- | ------- |
+| Name of Key | Provide a unique name |
+| Allow access to Buckets | Choose the Backblaze B2 bucket you created earlier |
+| Type of Access | Select **Read and Write** |
+| Allow List All Bucket Names | Leave this unticked |
+| File name prefix | Leave field empty |
+| Duration (seconds) | Leave field empty |
+
+5. Click **Create New Key**.
+
+Now, use your `keyID` as the value of `AWS_S3_ACCESS_KEY_ID` and `applicationKey` for `AWS_S3_SECRET_ACCESS_KEY` in your `.env.production` file:
+
+| Environment variable | Instruction |
+| -------- | ------- |
+| AWS_S3_ACCESS_KEY_ID | Use your **keyID** |
+| AWS_S3_SECRET_ACCESS_KEY | Use your **applicationKey** |
+
+At this stage, the content of your `.env.production` file looks like this:
+
+```txt
+AWS_STORAGE_BUCKET_NAME=yourname-wagtail-portfolio
+AWS_S3_ENDPOINT_URL=https://s3.us-east-005.backblazeb2.com
+AWS_S3_REGION_NAME=us-east-005
+AWS_S3_ACCESS_KEY_ID=your Backblaze keyID
+AWS_S3_SECRET_ACCESS_KEY=your Backblaze applicationKey
+DJANGO_ALLOWED_HOSTS=
+DJANGO_CSRF_TRUSTED_ORIGINS=https://
+DJANGO_SETTINGS_MODULE=mysite.settings.production
+```
+
+```note
+The Backblaze B2 storage uses _AWS_ and _S3_ because it works like Amazon Web Services’ S3.
+
+Do not commit or share your `.env.production `file. Anyone with the variables can access your site.
+
+If you lost your secret application key, create a new key following the preceding instructions.
+```
+
+For more information on how to set up your Backblaze B2 Cloud Storage, read the [Backblaze B2 Cloud Storage Documentation](https://www.backblaze.com/docs/cloud-storage/).
+
+## Set up Fly.io
+
+Now that you've linked your site to your Blackblaze storage, it's time to set up Fly.io to host your site.
+
+To set up your Fly.io account, follow these steps:
+
+1. Visit [Fly.io](https://fly.io/) in your browser.
+2. Click **Sign Up**.
+3. Sign up using your GitHub account, Google account, or the email option.
+4. Check your email inbox for the verification link to verify your email.
+
+```note
+If your email verification fails, go to your Fly.io [Dashboard](https://fly.io/dashboard) and try again.
+```
+
+5. Go to **Dashboard > Billing** and click **Add credit card** to add your credit card.
+
+```note
+Adding your credit card allows you to create a project in Fly.io. Fly.io won't charge you after adding your credit card.
+```
+
+6. [Install flyctl](https://fly.io/docs/hands-on/install-flyctl/) by navigating to your project directory and then running the following command in your terminal:
+
+On macOS:
+
+```sh
+# If you have the Homebrew package manager installed, run the following command:
+brew install flyctl
+
+# If you don't have the Homebrew package manager installed, run the following command:
+curl -L https://fly.io/install.sh | sh
+```
+
+On Linux:
+
+```sh
+curl -L https://fly.io/install.sh | sh
+```
+
+On Windows, navigate to your project directory on **PowerShell**, activate your environment and run the following command:
+
+```doscon
+pwsh -Command "iwr https://fly.io/install.ps1 -useb | iex"
+```
+
+```note
+If your get an error on Windows saying the term `pwsh` is  not recognized, install [PowerShell MSI](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.3#installing-the-msi-package) and then rerun the preceding Windows command.
+```
+
+7. [Sign in](https://fly.io/docs/hands-on/sign-in/) to your Fly.io by running the following command:
+
+```sh
+fly auth login
+```
+
+If you use Microsoft WSL, then run:
+
+```doscon
+ln -s /usr/bin/wslview /usr/local/bin/xdg-open
+```
+
+```note
+If you successfully install flyctl but get an error saying "`fly` is not recognized" or "flyctl: command not found error", then you must add flyctl to your PATH. For more information, read [Getting flyctl: command not found error post install](https://community.fly.io/t/getting-flyctl-command-not-found-error-post-install/4954/1).
+```
+
+8. Create your Fly.io project by running `fly launch` and answering the resulting prompt questions as follows:
+
+| Question | Instruction |
+| -------- | ------- |
+| Choose an app name | Enter a name of your choice. For example, _yourname-wagtail-portfolio_ |
+| Choose a region for deployment | Select the appropriate region. |
+| Overwrite ".../.dockerignore"?  | Enter _y_ |
+| Overwrite ".../Dockerfile"?  | Enter _y_ |
+| Would you like to set up a Postgresql database now?  | Enter _y_ |
+| Select configuration | select _Development - Single node, 1x shared CPU, 256MB RAM, 1GB disk_ if available. Otherwise, select the smallest configuration option |
+| Scale single node pg to zero after one hour?  | Enter _y_ |
+| Would you like to set up an Upstash Redis database now? | Enter _n_ |
+
+The `fly launch` command creates two new files, `Dockerfile` and `fly.toml`, in your project directory.
+
+If you use a third-party app terminal like the Visual Studio Code terminal, you may get an error creating your Postgres database. To rectify this error, follow these steps:
+
+1. Delete `fly.toml` file from your project directory.
+2. Go to your Fly.io account in your browser and click **Dashboard**.
+3. Click the created app in your **Apps** list.
+4. Click **Settings** in your side navigation. 
+5. Click **Delete app**.
+6. Enter the name your app.
+7. Click **Yes delete it**.
+8. Repeat steps 3, 4, 5, 6, and 7 for all apps in your **Apps** list.
+9. Run the `fly launch` command in your built-in terminal or PowerShell MSI on Windows.
+
+## Customize your site to use Fly.io
+
+Now, you must configure your portfolio site for the final deployment.
+
+Add the following to your `.gitignore` file to make Git ignore your environment files:
+
+```
+.env*
+```
+
+Also, add the following to your `.dockerignore` file to make Docker ignore your environment and media files:
+
+```
+.env*
+media
+```
+
+Configure your Fly.io to use `1` worker. This allows your site to work better with Fly.io's low memory allowance. To do this, modify the last line of your `Dockerfile` as follows:
+
+```
+CMD ["gunicorn", "--bind", ":8000", "--workers", "1", "mysite.wsgi"]
+```
+
+Also, check if your `fly.toml` file has the following:
+
+```toml
+[deploy]
+  release_command = "python manage.py migrate"
+```
+
+The `fly launch` command creates two new files, `Dockerfile` and `fly.toml`, in your project directory.
+
+```toml
+app = "yourname-wagtail-portfolio"
+primary_region = "lhr"
+console_command = "/code/manage.py shell"
+
+[build]
+
+# add the deploy command:
+[deploy]
+  release_command = "python manage.py migrate"
+
+[env]
+  PORT = "8000"
+
+[http_service]
+  internal_port = 8000
+  force_https = true
+  auto_stop_machines = true
+  auto_start_machines = true
+  min_machines_running = 0
+  processes = ["app"]
+
+[[statics]]
+  guest_path = "/code/static"
+  url_prefix = "/static/"
+```
+
+Now add your production dependencies by replacing the content of your `requirements.txt` file with the following:
+
+```txt
+Django>=4.2,<4.3
+wagtail==5.1.1
+gunicorn>=21.2.0,<22.0.0
+psycopg[binary]>=3.1.10,<3.2.0
+dj-database-url>=2.1.0,<3.0.0
+whitenoise>=5.0,<5.1
+django-storages[s3]>=1.14.0,<2.0.0
+```
+
+The preceding dependencies ensure that the necessary tools and libraries are in place to run your site successfully on the production server. The following are the explanations for the dependencies you may be unaware of:
+
+1. `gunicorn` is a web server that runs your site in Docker.
+2. `psycopg` is a PostgreSQL adapter that connects your site to a PostgreSQL database.
+3. `dj-database-url` is a package that simplifies your database configurations and connects to your site to a PostgreSQL database.
+4. `whitenoise` is a Django package that serves static files.
+5. `django-storages` is a Django library that handles your file storage and connects to your Backblaze B2 storage.
+
+Replace the content of your `mysite/settings/production.py` file with the following:
+
+```python
+import os
+import random
+import string
+import dj_database_url
+
+from .base import *
+
+DEBUG = False
+
+DATABASES = {
+    "default": dj_database_url.config(
+        conn_max_age=600,
+        conn_health_checks=True
+    )
+}
+
+SECRET_KEY = os.environ["SECRET_KEY"]
+
+SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
+
+SECURE_SSL_REDIRECT = True
+
+ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "*").split(",")
+
+CSRF_TRUSTED_ORIGINS = os.getenv("DJANGO_CSRF_TRUSTED_ORIGINS", "").split(",")
+
+EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
+
+MIDDLEWARE.append("whitenoise.middleware.WhiteNoiseMiddleware")
+STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
+
+if "AWS_STORAGE_BUCKET_NAME" in os.environ:
+    AWS_STORAGE_BUCKET_NAME = os.getenv("AWS_STORAGE_BUCKET_NAME")
+    AWS_S3_REGION_NAME = os.getenv("AWS_S3_REGION_NAME")
+    AWS_S3_ENDPOINT_URL = os.getenv("AWS_S3_ENDPOINT_URL")
+    AWS_S3_ACCESS_KEY_ID = os.getenv("AWS_S3_ACCESS_KEY_ID")
+    AWS_S3_SECRET_ACCESS_KEY = os.getenv("AWS_S3_SECRET_ACCESS_KEY")
+
+    INSTALLED_APPS.append("storages")
+
+    DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
+
+    AWS_S3_OBJECT_PARAMETERS = {
+        'CacheControl': 'max-age=86400',
+    }
+
+LOGGING = {
+    "version": 1,
+    "disable_existing_loggers": False,
+    "handlers": {
+        "console": {
+            "class": "logging.StreamHandler",
+        },
+    },
+    "loggers": {
+        "django": {
+            "handlers": ["console"],
+            "level": os.getenv("DJANGO_LOG_LEVEL", "INFO"),
+        },
+    },
+}
+
+WAGTAIL_REDIRECTS_FILE_STORAGE = "cache"
+
+try:
+    from .local import *
+except ImportError:
+    pass
+```
+
+The explanation of some of the code in your  `mysite/settings/production.py` file is as follows:
+
+1. `DEBUG = False` turns off debugging for the production environment. It's important for security and performance.
+2. `SECRET_KEY = os.environ["SECRET_KEY"]` retrieves the project's secret key from your environment variable.
+3. `SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")` ensures that Django can detect a secure HTTPS connection if you deploy your site behind a reverse proxy like Heroku.
+4. `SECURE_SSL_REDIRECT = True` enforces HTTPS redirect. This ensures that all connections to the site are secure.
+5. `ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "*").split(",")` defines the hostnames that can access your site. It retrieves its values from the `DJANGO_ALLOWED_HOSTS` environment variable. If no specific hosts are defined, it defaults to allowing all hosts.
+6. `EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"` configures your site to use the console email backend. You can configure this to use a proper email backend for sending emails.
+7. `WAGTAIL_REDIRECTS_FILE_STORAGE = "cache"` configures the file storage for Wagtail's redirects. Here, you set it to use cache.
+
+Now, complete the configuration of your environment variables by modifying your `.env.production` file as follows:
+
+| Environment variable | Instruction |
+| -------- | ------- |
+| DJANGO_ALLOWED_HOSTS | This must match your fly.io project name. For example, _yourname-wagtail-portfolio.fly.dev_ |
+| DJANGO_CSRF_TRUSTED_ORIGINS | This must match your project’s domain name. For example, _https://yourname-wagtail-portfolio.fly.dev_ |
+
+The content of your `.env.production` file should now look like this:
+
+```txt
+AWS_STORAGE_BUCKET_NAME=yourname-wagtail-portfolio
+AWS_S3_ENDPOINT_URL=https://s3.us-east-005.backblazeb2.com
+AWS_S3_REGION_NAME=us-east-005
+AWS_S3_ACCESS_KEY_ID=your Backblaze keyID
+AWS_S3_SECRET_ACCESS_KEY=your Backblaze applicationKey
+DJANGO_ALLOWED_HOSTS=yourname-wagtail-portfolio.fly.dev
+DJANGO_CSRF_TRUSTED_ORIGINS=https://yourname-wagtail-portfolio.fly.dev
+DJANGO_SETTINGS_MODULE=mysite.settings.production
+```
+
+Set the secrets for Fly.io to use by running:
+
+```sh
+flyctl secrets import < .env.production
+```
+
+On Windows, run the following command in your PowerShell MSI:
+
+```doscon
+Get-Content .env.production | flyctl secrets import
+```
+
+Finally, deploy your site to Fly.io by running the following command:
+
+```sh
+fly deploy --ha=false
+```
+
+Congratulations! Your site is now live. However, you must add content to it. Start by creating an admin user for your live site. Run the following command:
+
+```sh
+flyctl ssh console
+```
+
+Then run:
+
+```sh
+DJANGO_SUPERUSER_USERNAME=username DJANGO_SUPERUSER_EMAIL=mail@example.com DJANGO_SUPERUSER_PASSWORD=password python manage.py createsuperuser --noinput
+```
+
+```note
+Ensure you replace _username_, _mail@example.com_, and _password_ with a username, email address, and password of your choice.
+```
+
+For more information on how to set up your Django project on Fly.io, read [Django on Fly.io](https://fly.io/docs/django/).
+
+## Add content to your live site
+
+All this while, you've been adding content to your site in the local environment. Now that your site is live on a server, you must add content to the live site. To add content to your live site, go to ` https://yourname-wagtail-portfolio.fly.dev/admin/` in your browser and follow the steps in the following sub-sections of the tutorial:
+
+- [Add content to your homepage](add_content_to_your_homepage)
+- [Add your social media links](add_your_social_media_links)
+- [Add footer text](add_footer_text)
+- [Add pages to your site menu](add_pages_to_your_site_menu)
+- [Add your contact information]()
+- [Add your resume](add_your_resume)
+
+```{note}
+If you encounter errors while trying to access your live site in your browser, check your application logs in your Fly.io Dashboard. To check your application logs, click **Dashboard > Apps > yourname-wagtail-portfolio > Monitoring**
+```
+
+## Where next
+
+-   Read the Wagtail [topics](../topics/index) and [reference](../reference/index) documentation
+-   Learn how to implement [StreamField](../topics/streamfield) for freeform page content
+-   Browse through the [advanced topics](../advanced_topics/index) section and read [third-party tutorials](../advanced_topics/third_party_tutorials)

+ 34 - 0
docs/advanced_tutorial/index.md

@@ -0,0 +1,34 @@
+# Tutorial
+
+```{toctree}
+---
+maxdepth: 2
+---
+customize_homepage
+create-footer_for_all_pages
+set_up_site_menu
+style_your_site
+create_contact_page
+create_portfolio_page
+add_search
+deployment
+```
+
+
+
+Congratulations on completing [Your first Wagtail site](../getting_started/tutorial.md) tutorial! Now that you've completed the tutorial and built a blog site from scratch, you should have a solid understanding of the basic building blocks of a Wagtail website. We hope you enjoyed learning all about Wagtail.
+
+Now that you can build a blog site with Wagtail, why stop there? We created this tutorial to help you grow your Wagtail knowledge. 
+
+In this tutorial, you'll transform your blog site into a fully deployable portfolio site. So, you must complete the [Your First Wagtail Site](../getting_started/tutorial.md) tutorial before you begin this Tutorial.
+
+You'll learn the following in this tutorial:
+- How to add pagination to your Wagtail website
+- How to use Wagtail StreamField
+- How to use Wagtail documents
+- How to use snippets across multiple web pages
+- How to use Wagtail forms
+- How to implement the search feature in a Wagtail website
+- How to deploy a Wagtail website
+
+Now, let's dive in.

+ 89 - 0
docs/advanced_tutorial/set_up_site_menu.md

@@ -0,0 +1,89 @@
+# Set up site menu for linking to the homepage and other pages
+
+This section of the tutorial will teach you how to create a site menu to link to your homepage and other pages as you add them. The site menu will appear across all pages of your portfolio website, just like your footer.
+
+Start by creating a template tag in your `base/templatetags/navigation_tags.py` file:
+
+```python
+from django import template
+
+# import site:
+from wagtail.models import Site
+
+from base.models import FooterText
+
+register = template.Library()
+
+
+# ... keep the definition of get_footer_text and add the get_site_root template tag:
+@register.simple_tag(takes_context=True)
+def get_site_root(context):
+    return Site.find_for_request(context["request"]).root_page
+```
+
+In the preceding code, you used the `get_site_root` template tag to retrieve the root page of your site, which is your HomePage. 
+
+Now, create `mysite/templates/includes/header.html` file and add the following to it:
+
+```html+django
+{% load wagtailcore_tags navigation_tags %}
+
+<header>
+    {% get_site_root as site_root %}
+    <nav>
+        <p>
+        <a href="{% pageurl site_root %}">Home</a> |
+        {% for menuitem in site_root.get_children.live.in_menu %}
+            <a href="{% pageurl menuitem %}">{{ menuitem.title }}</a>
+        {% endfor %}
+        </p>
+    </nav>
+</header>
+```
+
+In the preceding template you loaded the `wagtailcore_tags` and `navigation_tags`. 
+With these tags, you can generate navigation menus for your Wagtail project.
+
+`{% get_site_root as site_root %}` retrieves your HomePage and assigns it to the variable `site_root`.
+
+`<a href="{% pageurl site_root %}">Home</a> |` creates a link to your HomePage by using the pageurl template tag with `site_root` as an argument. It generates a link to your HomePage, with the label **Home**, followed by a pipe symbol `|`, to separate the menu items.
+
+`{% for menuitem in site_root.get_children.live.in_menu %}` is a loop that iterates through the child pages of your HomePage that are live and included in the menu.
+
+Finally, add your `header` template to your `base` template by modifying your `mysite/templates/base.html` file:
+
+```html+django
+<body class="{% block body_class %}{% endblock %}">
+    {% wagtailuserbar %}
+
+    {# Add your header template to your base template: #}
+    {% include "includes/header.html" %}
+
+    {% block content %}{% endblock %}
+
+    {% include "includes/footer.html" %}
+
+    {# Global javascript #}
+        
+    <script type="text/javascript" src="{% static 'js/mysite.js' %}"></script>
+
+    {% block extra_js %}
+    {# Override this in templates to add extra javascript #}
+    {% endblock %}
+</body>
+```
+
+Now, if you restart your server and reload your homepage, you'll see your site menu with a link to your homepage labeled as **Home**.
+
+(add_pages_to_your_site_menu)=
+
+## Add pages to your site menu
+
+You can add any top-level page to the site menu by doing the following:
+1. Go to your admin interface.
+2. Go to any top-level page.
+3. Click **Promote**.
+4. Check the **Show in menus** checkbox.
+
+In the next section of this tutorial, we'll show you how to style your site and improve its user experience.
+<!-- Provide a diagram to illustrate the checking of the Show in Menu checkbox -->

+ 209 - 0
docs/advanced_tutorial/style_your_site.md

@@ -0,0 +1,209 @@
+# Style and improve user experience
+
+In this tutorial, you'll add a basic site theme to your portfolio site and improve its user experience. 
+
+## Add styles
+
+To style your site, navigate to your `mysite/static/css/mysite.css` file and add the following:
+
+```css
+*,
+::before,
+::after {
+  box-sizing: border-box;
+}
+
+html {
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, Roboto, "Helvetica Neue", Arial, sans-serif, Apple Color Emoji, "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+}
+
+body {
+  min-height: 100vh;
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 10px;
+  display: grid;
+  gap: 3vw;
+  grid-template-rows: min-content 1fr min-content;
+}
+
+a {
+  color: currentColor;
+}
+
+footer {
+  border-top: 2px dotted;
+  text-align: center;
+}
+
+header {
+  border-bottom: 2px dotted;
+}
+
+.template-homepage main {
+  text-align: center;
+}
+```
+
+Now, reload your portfolio site to reflect the styles.
+
+```note
+If your webpage's styles do not update after reloading, then you may need to clear your browser cache.
+```
+
+## Improve user experience
+
+There are several ways to improve the user experience of your portfolio site.
+
+Start by modifying your `mysite/templates/base.html` file as follows:
+
+```html+django
+{# Remove wagtailuserbar: #}
+{% load static wagtailcore_tags %}
+
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <title>
+            {% block title %}
+            {% if page.seo_title %}{{ page.seo_title }}{% else %}{{ page.title }}{% endif %}
+            {% endblock %}
+            {% block title_suffix %}
+            {% wagtail_site as current_site %}
+            {% if current_site and current_site.site_name %}- {{ current_site.site_name }}{% endif %}
+            {% endblock %}
+        </title>
+        {% if page.search_description %}
+        <meta name="description" content="{{ page.search_description }}" />
+        {% endif %}
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+
+        {# Force all links in the live preview panel to be opened in a new tab #}
+        {% if request.in_preview_panel %}
+        <base target="_blank">
+        {% endif %}
+
+        {# Add supported color schemes: #}
+        <meta name="color-scheme" content="light dark">
+
+        {# Add a favicon with inline SVG: #}
+        <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍩</text></svg>"/>
+
+        {# Global stylesheets #}
+        <link rel="stylesheet" type="text/css" href="{% static 'css/mysite.css' %}">
+
+        {% block extra_css %}
+        {# Override this in templates to add extra stylesheets #}
+        {% endblock %}
+    </head>
+
+    <body class="{% block body_class %}{% endblock %}">
+        {# Remove  wagtailuserbar: #}
+
+        {% include "includes/header.html" %}
+
+        {# Wrap your block content  within a <main> HTML5 tag: #}
+        <main>
+            {% block content %}{% endblock %}
+        </main>
+
+        {% include "includes/footer.html" %}
+
+        {# Global javascript #}
+        
+        <script type="text/javascript" src="{% static 'js/mysite.js' %}"></script>
+
+        {% block extra_js %}
+        {# Override this in templates to add extra javascript #}
+        {% endblock %}
+    </body>
+</html>
+```
+
+In the preceding template, you made the following modifications:
+1. You removed `wagtailuserbar` from your base template. You'll add the `wagtailuserbar` to your `header` template later in the tutorial. This change improves the user experience for keyboard and screen reader users.
+
+2. You Added `<meta name="color-scheme" content="light dark">` to inform the browser about the supported color schemes for your site. This makes your site adapt to both dark and light themes.
+
+3. You used the `<link>` tag to add a favicon to your portfolio site using inline SVG.
+
+4. You wrapped the `{% block content %}` and `{% endblock %}` tags with a `<main>` HTML5 tag. The `<main>` tag is a semantic HTML5 tag used to indicate the main content of a webpage.
+
+Also, you should dynamically get your HomePage's title to use in your site menu instead of hardcoding it in your template. Also, you should include the child pages of the Home page in your site menu if they have their 'Show in menus' option checked. Finally, you want to ensure that you add the `wagtailuserbar` that you removed from your `base` template to your `header` template. This will improve users' experience for keyboard and screen reader users. 
+
+To make the improvements mentioned in the preceding paragraph, modify your `mysite/templates/includes/header.html` as follows:
+
+```html+django
+{# Load wagtailuserbar: #}
+{% load wagtailcore_tags navigation_tags wagtailuserbar %}
+
+<header>
+    {% get_site_root as site_root %}
+    <nav>
+        <p>
+          <a href="{% pageurl site_root %}">{{ site_root.title }}</a> |
+          {% for menuitem in site_root.get_children.live.in_menu %}
+
+            {# Add the child pages of your HomePage that have their `Show in menu` checked #}
+            <a href="{% pageurl menuitem %}">{{ menuitem.title }}</a>{% if not forloop.last %} | {% endif %}
+
+          {% endfor %}
+        </p>
+    </nav>
+
+    {# Add wagtailuserbar: #}
+    {% wagtailuserbar "top-right" %}
+</header>
+```
+
+Another way you can improve user experience is by adding a skip link for keyboard users. A skip link is a web accessibility feature that enhances the browsing experience for keyboard navigators and screen readers. The skip link will let your users jump directly to the main content.
+
+To add a skip-link, add the following styles to your `mysite/static/css/mysite.css` file:
+
+```css
+.skip-link {
+  position: absolute;
+  top: -30px;
+}
+
+.skip-link:focus-visible {
+  top: 5px;
+}
+```
+
+After adding the styles, go to your `mysite/templates/base.html` file and add a unique identifier:
+
+```html+python
+{% include "includes/header.html" %}
+
+{# Add a unique identifier: #}
+<main id="main">
+  {% block content %}{% endblock %}
+</main>
+```
+
+Finally, go to your `mysite/templates/includes/header.html` and modify it as follows:
+
+```
+{% load wagtailcore_tags navigation_tags wagtailuserbar %}
+<header>
+  {# Add this: #}
+  <a href="#main" class="skip-link">Skip to content</a>
+  
+  {% get_site_root as site_root %}
+  <nav>
+    <p>
+      <a href="{% pageurl site_root %}">{{ site_root.title }}</a> |
+      {% for menuitem in site_root.get_children.live.in_menu %}
+        <a href="{% pageurl menuitem %}">{{ menuitem.title }}</a>{% if not forloop.last %} | {% endif %}
+      {% endfor %}
+    </p>
+  </nav>
+  {% wagtailuserbar "top-right" %}
+</header>
+```
+
+In the preceding template, you added an `<a> (anchor)` element to create a _Skip to content_ link. You set the `href` attribute to `#main`. The internal anchor links to your base template's `main` element.
+
+Well done! Now, you know how to style a Wagtail site. The next section will teach you how to create a contact page for your portfolio site.

+ 1 - 0
docs/getting_started/tutorial.md

@@ -967,6 +967,7 @@ Thank you for reading and welcome to the Wagtail community!
 
 ## Where next
 
+-   Read [Tutorial](../advanced_tutorial/index.md) to transform your blog site into a fully deployable portfolio site.
 -   Read the Wagtail [topics](../topics/index) and [reference](../reference/index) documentation
 -   Learn how to implement [StreamField](../topics/streamfield) for freeform page content
 -   Browse through the [advanced topics](../advanced_topics/index) section and read [third-party tutorials](../advanced_topics/third_party_tutorials)

+ 1 - 0
docs/index.rst

@@ -39,6 +39,7 @@ Index
    :titlesonly:
 
    getting_started/index
+   advanced_tutorial/index
    topics/index
    advanced_topics/index
    extending/index

+ 2 - 0
docs/reference/pages/panels.md

@@ -53,6 +53,8 @@ Here are some built-in panel types that you can use in your panel definitions. T
 
 ```
 
+(multiFieldPanel)=
+
 ### MultiFieldPanel
 
 ```{eval-rst}

+ 2 - 0
docs/reference/streamfield/blocks.md

@@ -52,6 +52,8 @@ All block definitions accept the following optional keyword arguments:
 -   `group`
     -   The group used to categorise this block. Any blocks with the same group name will be shown together in the editor interface with the group name as a heading.
 
+(field_block_types)=
+
 ## Field block types
 
 ```{eval-rst}

+ 2 - 0
docs/topics/streamfield.md

@@ -168,6 +168,8 @@ body = StreamField([
 ])
 ```
 
+(block_icons)=
+
 ### Block icons
 
 In the menu that content authors use to add new blocks to a StreamField, each block type has an associated icon. For StructBlock and other structural block types, a placeholder icon is used, since the purpose of these blocks is specific to your project. To set a custom icon, pass the option `icon` as either a keyword argument to `StructBlock`, or an attribute on a `Meta` class: