diff --git a/assets/css/main.css b/assets/css/main.css
index b5a0cb2..10d40fe 100644
--- a/assets/css/main.css
+++ b/assets/css/main.css
@@ -90,6 +90,12 @@ a:hover,
grid-gap: 1rem;
}
+@media (width < 600px) {
+ .nav-main__list > li:first-child {
+ margin-right: 6rem;
+ }
+}
+
.nav-main__logo {
width: 80px;
margin-bottom: calc(var(--gap-m) * -1);
@@ -101,12 +107,6 @@ a:hover,
bottom: 0;
}
-@media (width < 600px) {
- .nav-main__link--logo {
- z-index: -1;
- }
-}
-
.center-block {
display: block;
margin-inline: auto;
@@ -152,9 +152,11 @@ a:hover,
}
.button:hover {
+ --translate: 3px;
border-color: var(--color-brown);
- box-shadow: 0 5px 0 var(--color-brown);
+ box-shadow: var(--translate) var(--translate) 0 var(--color-brown);
text-decoration: none;
+ transform: translate(calc(-1 * var(--translate)), calc(-1 * var(--translate)));
}
.header {
@@ -178,7 +180,7 @@ a:hover,
.header .nav__list {
display: grid;
- grid-template-columns: 1fr repeat(4, auto);
+ grid-template-columns: 1fr repeat(5, auto);
}
@media (width < 600px) {
@@ -288,6 +290,11 @@ a:hover,
}
}
+.nav-docs__list {
+ list-style: "➔ ";
+ list-style-position: inside;
+}
+
/* Home */
.nav-home__list {
@@ -333,7 +340,7 @@ a:hover,
/* Docs */
.docs {
display: grid;
- grid-template-columns: 8rem auto;
+ grid-template-columns: 10rem auto;
gap: var(--gap-l);
}
@@ -348,6 +355,12 @@ a:hover,
}
}
+/* Tutorials */
+.tutorials ul {
+ list-style: "📚 ";
+ list-position: inside;
+}
+
/* Footer */
.footer {
text-align: center;
diff --git a/one.org b/one.org
index 396a03c..6019183 100644
--- a/one.org
+++ b/one.org
@@ -56,35 +56,23 @@ The same process is repeated for each action, such as clicking a button, submitt
- Everything is asynchronous by default.
- Don't learn anything new. If you know Python, you know how to use Django LiveView.
-Are you ready to create your first Realtime SPA? Let's go to the [[#/docs/quickstart/][Quickstart]].
+Are you ready to create your first Realtime SPA? Let's go to the [[#/tutorials/quickstart/][Quickstart]].
-* Quickstart
+* Install
:PROPERTIES:
:ONE: one-custom-default-doc
-:CUSTOM_ID: /docs/quickstart/
-:TITLE: Quickstart
-:DESCRIPTION: Get started with Django LiveView the easy way.
+:CUSTOM_ID: /docs/install/
+:TITLE: Install
+:DESCRIPTION: Install Django LiveView.
:END:
-Welcome to the Quickstart guide. Here you will learn how to create your first Realtime SPA using Django LiveView. I assume you have a basic understanding of Django and Python.
-
-All the steps are applied in a [[https://github.com/Django-LiveView/minimal-template][minimalist template]].
-
-** 1. Install Django
-
-Install Django, create a project and an app.
-
-** 2. Install LiveView
-
-Install django-liveview with ~pip~.
+You can install Django LiveView with ~pip~.
#+BEGIN_SRC sh
pip install django-liveview
#+END_SRC
-** 3. Modify the configuration
-
-Add ~liveview~ to your installed ~INSTALLED_APPS~.
+Then add ~liveview~ to your installed ~INSTALLED_APPS~ in your ~settings.py~.
#+BEGIN_SRC python
INSTALLED_APPS = [
@@ -94,264 +82,21 @@ INSTALLED_APPS = [
]
#+END_SRC
-Then indicate in which previously created App you want to implement LiveView.
+You need to previously have installed ~channels~ and ~daphne~.
+
+Now, in ~settings.py~, indicate in which previously created App you want to integrate LiveView.
#+BEGIN_SRC python
LIVEVIEW_APPS = ["website"]
#+END_SRC
-** 4. Migration
-
-Execute the migrations so that the LiveView tables are generated.
+Finally, execute the migrations so that the LiveView tables are generated.
#+BEGIN_SRC python
python manage.py migrate
#+END_SRC
-** 5. ASGI
-
-Modify the ASGI file, ~asgi.py~ to add the LiveView routing. In this example it is assumed that settings.py is inside core, in your case it may be different.
-
-#+BEGIN_SRC python
-import os
-import django
-
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
-django.setup()
-
-from channels.auth import AuthMiddlewareStack
-from django.core.asgi import get_asgi_application
-from channels.security.websocket import AllowedHostsOriginValidator
-from channels.routing import ProtocolTypeRouter, URLRouter
-from django.urls import re_path
-from liveview.consumers import LiveViewConsumer
-
-
-application = ProtocolTypeRouter(
- {
- # Django's ASGI application to handle traditional HTTP requests
- "http": get_asgi_application(),
- # WebSocket handler
- "websocket": AuthMiddlewareStack(
- AllowedHostsOriginValidator(
- URLRouter([re_path(r"^ws/liveview/$", LiveViewConsumer.as_asgi())])
- )
- ),
- }
-)
-#+END_SRC
-
-** 6. Create your first Action
-
-Place where the functions and logic of the business logic are stored. We will start by creating an action to generate a random number and print it.
-
-Create inside your App a folder called ~actions~, here will go all the actions for each page. Now we will create inside the folder a file named ~home.py~.
-
-#+BEGIN_SRC python
-# my-app/actions/home.py
-from liveview.context_processors import get_global_context
-from core import settings
-from liveview.utils import (
- get_html,
- update_active_nav,
- enable_lang,
- loading,
-)
-from channels.db import database_sync_to_async
-from django.templatetags.static import static
-from django.urls import reverse
-from django.utils.translation import gettext as _
-from random import randint
-
-template = "pages/home.html"
-
-# Database
-
-# Functions
-
-async def get_context(consumer=None):
- context = get_global_context(consumer=consumer)
- # Update context
- context.update(
- {
- "url": settings.DOMAIN_URL + reverse("home"),
- "title": _("Home") + " | Home",
- "meta": {
- "description": _("Home page of the website"),
- "image": f"{settings.DOMAIN_URL}{static('img/seo/og-image.jpg')}",
- },
- "active_nav": "home",
- "page": template,
- }
- )
- return context
-
-
-@enable_lang
-@loading
-async def send_page(consumer, client_data, lang=None):
- # Nav
- await update_active_nav(consumer, "home")
- # Main
- my_context = await get_context(consumer=consumer)
- html = await get_html(template, my_context)
- data = {
- "action": client_data["action"],
- "selector": "#main",
- "html": html,
- }
- data.update(my_context)
- await consumer.send_html(data)
-
-async def random_number(consumer, client_data, lang=None):
- my_context = await get_context(consumer=consumer)
- data = {
- "action": client_data["action"],
- "selector": "#output-random-number",
- "html": randint(0, 10),
- }
- data.update(my_context)
- await consumer.send_html(data)
-#+END_SRC
-
-There are several points in the above code to keep in mind.
-
-- ~template~ is the name of the template that will be rendered.
-- ~get_context()~ is a function that returns a dictionary with the context of the page.
-- ~send_page()~ is the function that will be executed when the page is loaded.
-- ~random_number()~ is the function that will be executed when the button is clicked.
-
-** 7. Create the base template
-
-Now we will create the base template, which will be the one that will be rendered when the page is loaded.
-
-Create a folder called ~templates~, or use your template folder, inside your App and inside it create another folder called ~layouts~. Now create a file called ~base.html~.
-
-#+BEGIN_SRC html
-{# my-app/templates/layouts/base.html #}
-{% load static i18n %}
-{% get_current_language as CURRENT_LANGUAGE %}
-
-
-
- {{ title }}
-
-
-
-
-
-
-
-
-
-
-
- {% include 'components/header.html' %}
-
- {% include page %}
-
-
-
-
-#+END_SRC
-
-In the future we will define ~main.js~, a minimal JavaScript to connect the events and the WebSockets client.
-
-** 8. Create the page template
-
-We will create the home page template, which will be the one that will be rendered when the page is loaded.
-
-Create a folder called ~pages~ in your template folder and inside it create a file called ~home.html~.
-
-#+BEGIN_SRC html
-{# my-app/templates/pages/home.html #}
-{% load static %}
-
-
-
-
-
-
-
-#+END_SRC
-
-As you can see, we have defined a button to launch the action of generating the random number (~button~) and the place where we will print the result (~output-random-number~).
-
-** 9. Create frontend
-
-Now we are going to create the frontend, the part where we will manage the JavaScript events and invoke the actions.
-
-Download [[https://github.com/Django-LiveView/assets/archive/refs/heads/main.zip][assets]] and unzip it in your static folder. You will be left with the following route: ~/static/js/~.
-
-** 10. Create View
-
-We will create the view that will render the page for the first time (like Server Side Rendering). The rest of the times will be rendered dynamically (like Single Page Application).
-
-In a normal Django application we would create a view, ~views.py~, similar to the following:
-
-#+BEGIN_SRC python
-# my-app/views.py
-from django.shortcuts import render
-
-# Create your views here.
-def home(request):
- return render(request, "pages/home.html")
-#+END_SRC
-
-With LiveView, on the other hand, you will have the following structure.
-
-#+BEGIN_SRC python
-# my-app/views.py
-from django.shortcuts import render
-from .actions.home import get_context as get_home_context
-
-from liveview.utils import get_html
-
-async def home(request):
- return render(request, "layouts/base.html", await get_home_context())
-#+END_SRC
-
-** 11. Create URL
-
-Finally, we will create the URL that will render the page.
-
-#+BEGIN_SRC python
-# my-app/urls.py
-from django.urls import path
-
-from .views import home
-
-urlpatterns = [
- path("", home, name="home"),
-]
-#+END_SRC
-
-** 12. Run the server
-
-Run the server.
-
-#+BEGIN_SRC sh
-python manage.py runserver
-#+END_SRC
-
-And open the browser at ~http://localhost:8000/~. You should see the home page with a button that generates a random number.
-
-#+ATTR_HTML: :class center-block image image--responsive
-[[#/img/quickstart/minimal-template.webp][Random number]]
+We strongly recommend that you follow the [[#/tutorials/quickstart/][Quickstart]] to see the installation in action.
* Actions
:PROPERTIES:
@@ -689,6 +434,54 @@ You can customize the history management system by editing the ~history~ control
If you want to disable it, remove `startHistory();` from ~assets/js/main.js~.
+* Internationalization
+:PROPERTIES:
+:ONE: one-custom-default-doc
+:CUSTOM_ID: /docs/internationalization/
+:TITLE: Internationalization
+:DESCRIPTION: Internationalization of Django LiveView.
+:END:
+
+Django LiveView uses the same internationalization system as Django. You can read more about it in the [[https://docs.djangoproject.com/en/dev/topics/i18n/][Django documentation]]. However, let's go deeper.
+
+Every time that the client sends a request for action, it sends the language that the user has set in the browser.
+
+#+BEGIN_SRC json
+ {
+ "action": "blog_list->send_page",
+ "data": {
+ "lang": "es"
+ }
+}
+#+END_SRC
+
+The ~lang~ attribute is extracted directly from the ~~ tag.
+
+#+BEGIN_SRC html
+ {% load static i18n %}
+ {% get_current_language as CURRENT_LANGUAGE %}
+
+#+END_SRC
+
+You can access the language with the ~lang~ parameter of the action using the ~@enable_lang~ decorator.
+
+#+BEGIN_SRC python
+ @enable_lang
+ @loading
+ async def send_page(consumer, client_data, lang=None):
+ print(lang)
+#+END_SRC
+
+Or you could read it with the ~lang~ key of the ~client_data~ parameter.
+
+ #+BEGIN_SRC python
+ @loading
+ async def send_page(consumer, client_data):
+ print(client_data["data"]["lang"])
+#+END_SRC
+
+You can read the tutorial [[#/tutorials/internationalize-with-subdomains/][Internationalize with subdomains]] to see how to create a multilingual website with subdomains.
+
* Deploy
:PROPERTIES:
:ONE: one-custom-default-doc
@@ -767,6 +560,348 @@ Yes, you can.
Only me and my free time.
+
+
+* Tutorials
+:PROPERTIES:
+:ONE: one-custom-default-tutorials
+:CUSTOM_ID: /tutorials/
+:TITLE: Tutorials
+:DESCRIPTION: Tutorials about Django LiveView.
+:END:
+
+** Tutorials
+
+Learn how to create a Django LiveView application step by step.
+
+- [[#/tutorials/quickstart/][Quickstart]]
+- [[#/tutorials/internationalize-with-subdomains/][Internationalize with subdomains]]
+
+* Quickstart
+:PROPERTIES:
+:ONE: one-custom-default-page
+:CUSTOM_ID: /tutorials/quickstart/
+:TITLE: Quickstart
+:DESCRIPTION: Get started with Django LiveView the easy way.
+:END:
+
+Welcome to the Quickstart guide. Here you will learn how to create your first Realtime SPA using Django LiveView. I assume you have a basic understanding of Django and Python.
+
+All the steps are applied in a [[https://github.com/Django-LiveView/minimal-template][minimalist template]].
+
+** 1. Install Django
+
+Install Django, create a project and an app.
+
+** 2. Install LiveView
+
+Install django-liveview with ~pip~.
+
+#+BEGIN_SRC sh
+pip install django-liveview
+#+END_SRC
+
+** 3. Modify the configuration
+
+Add ~liveview~ to your installed ~INSTALLED_APPS~.
+
+#+BEGIN_SRC python
+INSTALLED_APPS = [
+ "daphne",
+ "channels",
+ "liveview",
+]
+#+END_SRC
+
+Then indicate in which previously created App you want to implement LiveView.
+
+#+BEGIN_SRC python
+LIVEVIEW_APPS = ["website"]
+#+END_SRC
+
+** 4. Migration
+
+Execute the migrations so that the LiveView tables are generated.
+
+#+BEGIN_SRC python
+python manage.py migrate
+#+END_SRC
+
+** 5. ASGI
+
+Modify the ASGI file, ~asgi.py~ to add the LiveView routing. In this example it is assumed that settings.py is inside core, in your case it may be different.
+
+#+BEGIN_SRC python
+import os
+import django
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
+django.setup()
+
+from channels.auth import AuthMiddlewareStack
+from django.core.asgi import get_asgi_application
+from channels.security.websocket import AllowedHostsOriginValidator
+from channels.routing import ProtocolTypeRouter, URLRouter
+from django.urls import re_path
+from liveview.consumers import LiveViewConsumer
+
+
+application = ProtocolTypeRouter(
+ {
+ # Django's ASGI application to handle traditional HTTP requests
+ "http": get_asgi_application(),
+ # WebSocket handler
+ "websocket": AuthMiddlewareStack(
+ AllowedHostsOriginValidator(
+ URLRouter([re_path(r"^ws/liveview/$", LiveViewConsumer.as_asgi())])
+ )
+ ),
+ }
+)
+#+END_SRC
+
+** 6. Create your first Action
+
+Place where the functions and logic of the business logic are stored. We will start by creating an action to generate a random number and print it.
+
+Create inside your App a folder called ~actions~, here will go all the actions for each page. Now we will create inside the folder a file named ~home.py~.
+
+#+BEGIN_SRC python
+# my-app/actions/home.py
+from liveview.context_processors import get_global_context
+from core import settings
+from liveview.utils import (
+ get_html,
+ update_active_nav,
+ enable_lang,
+ loading,
+)
+from channels.db import database_sync_to_async
+from django.templatetags.static import static
+from django.urls import reverse
+from django.utils.translation import gettext as _
+from random import randint
+
+template = "pages/home.html"
+
+# Database
+
+# Functions
+
+async def get_context(consumer=None):
+ context = get_global_context(consumer=consumer)
+ # Update context
+ context.update(
+ {
+ "url": settings.DOMAIN_URL + reverse("home"),
+ "title": _("Home") + " | Home",
+ "meta": {
+ "description": _("Home page of the website"),
+ "image": f"{settings.DOMAIN_URL}{static('img/seo/og-image.jpg')}",
+ },
+ "active_nav": "home",
+ "page": template,
+ }
+ )
+ return context
+
+
+@enable_lang
+@loading
+async def send_page(consumer, client_data, lang=None):
+ # Nav
+ await update_active_nav(consumer, "home")
+ # Main
+ my_context = await get_context(consumer=consumer)
+ html = await get_html(template, my_context)
+ data = {
+ "action": client_data["action"],
+ "selector": "#main",
+ "html": html,
+ }
+ data.update(my_context)
+ await consumer.send_html(data)
+
+async def random_number(consumer, client_data, lang=None):
+ my_context = await get_context(consumer=consumer)
+ data = {
+ "action": client_data["action"],
+ "selector": "#output-random-number",
+ "html": randint(0, 10),
+ }
+ data.update(my_context)
+ await consumer.send_html(data)
+#+END_SRC
+
+There are several points in the above code to keep in mind.
+
+- ~template~ is the name of the template that will be rendered.
+- ~get_context()~ is a function that returns a dictionary with the context of the page.
+- ~send_page()~ is the function that will be executed when the page is loaded.
+- ~random_number()~ is the function that will be executed when the button is clicked.
+
+** 7. Create the base template
+
+Now we will create the base template, which will be the one that will be rendered when the page is loaded.
+
+Create a folder called ~templates~, or use your template folder, inside your App and inside it create another folder called ~layouts~. Now create a file called ~base.html~.
+
+#+BEGIN_SRC html
+{# my-app/templates/layouts/base.html #}
+{% load static i18n %}
+{% get_current_language as CURRENT_LANGUAGE %}
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+
+
+ {% include 'components/header.html' %}
+
+ {% include page %}
+
+
+
+
+#+END_SRC
+
+In the future we will define ~main.js~, a minimal JavaScript to connect the events and the WebSockets client.
+
+** 8. Create the page template
+
+We will create the home page template, which will be the one that will be rendered when the page is loaded.
+
+Create a folder called ~pages~ in your template folder and inside it create a file called ~home.html~.
+
+#+BEGIN_SRC html
+{# my-app/templates/pages/home.html #}
+{% load static %}
+
+
+
+
+
+
+
+#+END_SRC
+
+As you can see, we have defined a button to launch the action of generating the random number (~button~) and the place where we will print the result (~output-random-number~).
+
+** 9. Create frontend
+
+Now we are going to create the frontend, the part where we will manage the JavaScript events and invoke the actions.
+
+Download [[https://github.com/Django-LiveView/assets/archive/refs/heads/main.zip][assets]] and unzip it in your static folder. You will be left with the following route: ~/static/js/~.
+
+** 10. Create View
+
+We will create the view that will render the page for the first time (like Server Side Rendering). The rest of the times will be rendered dynamically (like Single Page Application).
+
+In a normal Django application we would create a view, ~views.py~, similar to the following:
+
+#+BEGIN_SRC python
+# my-app/views.py
+from django.shortcuts import render
+
+# Create your views here.
+def home(request):
+ return render(request, "pages/home.html")
+#+END_SRC
+
+With LiveView, on the other hand, you will have the following structure.
+
+#+BEGIN_SRC python
+# my-app/views.py
+from django.shortcuts import render
+from .actions.home import get_context as get_home_context
+
+from liveview.utils import get_html
+
+async def home(request):
+ return render(request, "layouts/base.html", await get_home_context())
+#+END_SRC
+
+** 11. Create URL
+
+Finally, we will create the URL that will render the page.
+
+#+BEGIN_SRC python
+# my-app/urls.py
+from django.urls import path
+
+from .views import home
+
+urlpatterns = [
+ path("", home, name="home"),
+]
+#+END_SRC
+
+** 12. Run the server
+
+Run the server.
+
+#+BEGIN_SRC sh
+python manage.py runserver
+#+END_SRC
+
+And open the browser at ~http://localhost:8000/~. You should see the home page with a button that generates a random number when clicked.
+
+#+ATTR_HTML: :class center-block image image--responsive
+[[#/img/quickstart/minimal-template.webp][Random number]]
+
+Congratulations! You have created your first Realtime SPA using Django LiveView.
+
+The next step is to create a more complex application. You can read other [[#/tutorials/][tutorials]] or go to the [[#/docs/install/][documentation]].
+
+* Internationalize with subdomains
+:PROPERTIES:
+:ONE: one-custom-default-page
+:CUSTOM_ID: /tutorials/internationalize-with-subdomains/
+:TITLE: Internationalize with subdomains
+:DESCRIPTION: Create a multilingual website with subdomains.
+:END:
+
+Here you will learn how to create a multilingual website using Django LiveView. We recommend using subdomains to define the language (~en.example.com~, ~es.example.com~...), instead of using prefixes in addresses (~example.com/en/blog/~, ~example.com/es/blog/~). They simplify SEO, maintain consistency in the Sitemap and are easy to test.
+
+** 1. Configure the subdomains
+
+** 2. Create the subdomains
+
+** 3. Configure the languages
+
+** 4. Redirection with Middleware
+
+** 5. First text
+
+** 6. Make messages
+
+** 7. Compile messages
+
+** 8. Selector of languages
+
* Source code
:PROPERTIES:
:ONE: one-custom-default-page
diff --git a/onerc.el b/onerc.el
index b59eafc..f72c819 100644
--- a/onerc.el
+++ b/onerc.el
@@ -97,13 +97,15 @@
(:li.nav-main__item
(:a.nav-main__link.nav-main__link--logo (@ :href "/") (:img.nav-main__logo (@ :alt "Django LiveView" :src "/img/logo.webp"))))
(:li.nav-main__item
- (:a.button.nav-main__link (@ :href "/docs/quickstart/") "Docs"))
+ (:a.button.nav-main__link (@ :href "/docs/install/") "Docs"))
(:li.nav-main__item
- (:a.button.nav-main__link (@ :href "/source-code/") "Source code"))
+ (:a.button.nav-main__link (@ :href "/tutorials/") "Tutorials"))
(:li.nav-main__item
(:a.button.nav-main__link (@ :href "https://django-liveview-demo.andros.dev/" :target "_blank") "Demo"))
(:li.nav-main__item
- (:a.button.nav-main__link (@ :href "/books/") "Books"))))))
+ (:a.button.nav-main__link (@ :href "/books/") "Books"))
+ (:li.nav-main__item
+ (:a.button.nav-main__link (@ :href "/source-code/") "Source code"))))))
,tree-content
(:footer.footer
(:p "Created with " (:i (@ :aria-label "love") "❤️") " by " (:a.link (@ :href "https://andros.dev/" :target "_blank") "Andros Fenollosa") " with " (:a.link (@ :href "https://one.tonyaldon.com/" :target "_blank") "one.el"))
@@ -168,7 +170,7 @@
(:nav.nav-docs
(:ul.nav__list.nav__list--docs.nav-docs__list
(:li.nav-docs__item
- (:a.nav-docs__link (@ :href "/docs/quickstart/") "Quickstart"))
+ (:a.nav-docs__link (@ :href "/docs/install/") "Install"))
(:li.nav-docs__item
(:a.nav-docs__link (@ :href "/docs/actions/") "Actions"))
(:li.nav-docs__item
@@ -177,6 +179,8 @@
(:a.nav-docs__link (@ :href "/docs/routing/") "Routing"))
(:li.nav-docs__item
(:a.nav-docs__link (@ :href "/docs/history/") "History"))
+ (:li.nav-docs__item
+ (:a.nav-docs__link (@ :href "/docs/internationalization/") "Internationalization"))
(:li.nav-docs__item
(:a.nav-docs__link (@ :href "/docs/deploy/") "Deploy"))
(:li.nav-docs__item
@@ -185,6 +189,22 @@
(:main.main.main--docs
,content))))))
+(defun one-custom-default-tutorials (page-tree pages _global)
+ "Default render function by tutorials page."
+ (let* ((title (org-element-property :raw-value page-tree))
+ (description (org-element-property :DESCRIPTION page-tree))
+ (path (org-element-property :CUSTOM_ID page-tree))
+ (content (org-export-data-with-backend
+ (org-element-contents page-tree)
+ 'one-ox nil))
+ (website-name (one-default-website-name pages))
+ (nav (one-default-nav path pages)))
+ (render-layout-html
+ title
+ description
+ (jack-html `(:main.main
+ (:section.tutorials
+ (:div.container ,content)))))))
;; Sitemap
(defun make-sitemap (pages tree global)