diff --git a/README.md b/README.md index 458b81b..8abad35 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,25 @@ async def jobs(request): return await render_nextjs_page(request) ``` +#### Using `nextjs_page` with `stream=True` (Recommended) + +If you're using the [Next.js App Router](https://nextjs.org/docs/app) (introduced in Next.js 13+), you can enable streaming by setting the `stream=True` parameter in the `nextjs_page` function. This allows the HTML response to be streamed directly from the Next.js server to the client. This approach is particularly useful for server-side rendering with streaming support to show an [instant loading state](https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming#instant-loading-states) from the Next.js server while the content of a route segment loads. + +Here's an example: + +```python +from django_nextjs.views import nextjs_page + +urlpatterns = [ + path("/nextjs/page", nextjs_page(stream=True), name="nextjs_page"), +] +``` + +**Considerations:** + +- When using `stream_nextjs_page`, you cannot use a custom HTML template in Django, as the HTML is streamed directly from the Next.js server. +- The `stream` parameter will default to `True` in future releases. Currently, it is set to `False` for backward compatibility. To avoid breaking changes, we recommend explicitly setting `stream=False` if you are customizing HTML and do not want to use streaming. + ## Customizing the HTML Response You can modify the HTML code that Next.js returns in your Django code. diff --git a/django_nextjs/render.py b/django_nextjs/render.py index 2b534c5..22c593d 100644 --- a/django_nextjs/render.py +++ b/django_nextjs/render.py @@ -5,7 +5,7 @@ import aiohttp from asgiref.sync import sync_to_async from django.conf import settings -from django.http import HttpRequest, HttpResponse +from django.http import HttpRequest, HttpResponse, StreamingHttpResponse from django.middleware.csrf import get_token as get_csrf_token from django.template.loader import render_to_string from multidict import MultiMapping @@ -78,6 +78,11 @@ def _get_nextjs_response_headers(headers: MultiMapping[str]) -> Dict: "Vary", "Content-Type", "Set-Cookie", + "Link", + "Cache-Control", + "Connection", + "Date", + "Keep-Alive", ], ) @@ -150,3 +155,46 @@ async def render_nextjs_page( headers=headers, ) return HttpResponse(content=content, status=status, headers=response_headers) + + +async def stream_nextjs_page( + request: HttpRequest, + allow_redirects: bool = False, + headers: Union[Dict, None] = None, +): + """ + Stream a Next.js page response. + This function is used to stream the response from a Next.js server. + """ + page_path = quote(request.path_info.lstrip("/")) + params = [(k, v) for k in request.GET.keys() for v in request.GET.getlist(k)] + next_url = f"{NEXTJS_SERVER_URL}/{page_path}" + + session = aiohttp.ClientSession() + + try: + nextjs_response = await session.get( + next_url, + params=params, + allow_redirects=allow_redirects, + cookies=_get_nextjs_request_cookies(request), + headers=_get_nextjs_request_headers(request, headers), + ) + response_headers = _get_nextjs_response_headers(nextjs_response.headers) + + async def stream_nextjs_response(): + try: + async for chunk in nextjs_response.content.iter_any(): + yield chunk + finally: + await nextjs_response.release() + await session.close() + + return StreamingHttpResponse( + stream_nextjs_response(), + status=nextjs_response.status, + headers=response_headers, + ) + except: + await session.close() + raise diff --git a/django_nextjs/views.py b/django_nextjs/views.py index 59024c5..a431f9a 100644 --- a/django_nextjs/views.py +++ b/django_nextjs/views.py @@ -1,17 +1,24 @@ from typing import Dict, Union -from .render import render_nextjs_page +from .render import render_nextjs_page, stream_nextjs_page def nextjs_page( *, + stream: bool = False, template_name: str = "", context: Union[Dict, None] = None, using: Union[str, None] = None, allow_redirects: bool = False, headers: Union[Dict, None] = None, ): + if stream and (template_name or context or using): + raise ValueError("When 'stream' is set to True, you should not use 'template_name', 'context', or 'using'") + async def view(request, *args, **kwargs): + if stream: + return await stream_nextjs_page(request=request, allow_redirects=allow_redirects, headers=headers) + return await render_nextjs_page( request=request, template_name=template_name,