django-mvp-sphinx
Serve a project's Sphinx documentation inside its django-mvp application shell.
Scope & philosophy
django-mvp-sphinx serves a project's Sphinx documentation as pages of the project itself. The docs render through the project's own templates and theme, inside its application shell, and their table of contents becomes the app sidebar. Reading the user guide doesn't mean leaving the application.
It is written first for the people using the application: user guides, how-tos and reference for the site itself. Developer documentation should work too, but where the two pull in different directions, the user guide wins.
It serves a Sphinx JSON build (sphinx-build -b json). Serving never needs
Sphinx installed and never starts a build: the build is a file the project
produces before a request arrives, however it already does that. The package
adds a management command that runs the build when you ask for it. It does not
host documentation for several projects, keep old versions, or manage
translations.
When two designs conflict, the one that makes the docs look like the rest of the site beats the one that copies a Sphinx theme.
Quickstart
Five steps take a project from install to a documentation page in its own application shell, with the contents in the sidebar and a menu entry that leads to it. You write no template.
Before you start
You need a Django project on django-mvp whose application shell already works. This package renders inside django-mvp's layout and reads its colours from the theme django-mvp supplies, so it does nothing useful on its own.
You also need a Sphinx source directory for your user guide. The examples call
it docs/. If you don't have one yet, Sphinx's own
sphinx-quickstart
creates it.
The examples use yourproject and yourapp for your project's package and one
of its apps. Change those names and the paths to match your own.
1. Install
pip install django-mvp-sphinx
Then add it to INSTALLED_APPS, after mvp:
INSTALLED_APPS = [
# ...
"mvp",
"mvp_sphinx",
]
2. Add the Sphinx line
Add the package's extension to your Sphinx project's docs/conf.py:
extensions = ["mvp_sphinx.navigation"]
If conf.py already lists extensions, add this one to the list.
The extension writes the contents the sidebar shows. Without it the sidebar holds only the front page's entry, and every page is still served.
3. Build the docs
sphinx-build -b json docs docs/_build/json
Sphinx is needed where you run that command and nowhere else. The site that serves the pages reads the files the build left behind, never imports Sphinx and never starts a build, so Sphinx can stay out of your production requirements.
The output folder is yours to choose. A build made by a CI step, or one kept outside your project, works the same way once step 4 points at it. Until a build exists, every address under the prefix answers 404 and the rest of your site is unaffected.
Once the docs are mounted, python manage.py build_docs can run this build for
you. See
Building with a management command.
4. Mount the docs
Create the app once, in a module of its own, and give it the build's folder as
build_dir:
# yourproject/mounted.py
from django.conf import settings
from mvp_sphinx.mounted import DocumentationApp
docs = DocumentationApp(build_dir=settings.BASE_DIR / "docs" / "_build" / "json")
Then mount it in your URLs under whatever prefix you like:
# yourproject/urls.py
from mvp.mounted import mount
from yourproject.mounted import docs
urlpatterns = [
mount("docs/", docs),
]
5. Add the menu entry
Add the app's entry to your menu, in the menus.py of one of your installed
apps:
# yourapp/menus.py
from mvp.menus import AppMenu
from yourproject.mounted import docs
AppMenu.append(docs.menu_item())
django-flex-menus imports each installed app's menus module when Django
starts, which is why the file has to live in an installed app.
What you now have
Each page of the build answers under docs/, drawn by your own base.html
inside the application shell. The front page is at /docs/, the contents are in
the app sidebar, and your menu has an entry that leads to the front page. The tab
title carries the page's title and the app's name (Documentation unless you
change it), and the breadcrumbs lead back through the page's parents to the front
page.
Changing the docs
Edit your Sphinx source, run the command from step 3 again, and reload the page. The pages, the contents and the search follow on the next request, with no restart.
Using it
Everything below is optional. The quickstart is all a project needs to serve its documentation.
Building with a management command
To build the docs the way you run your other maintenance tasks, tell the app
where its Sphinx source is with source_dir, the directory that holds conf.py:
from django.conf import settings
from mvp_sphinx.mounted import DocumentationApp
docs = DocumentationApp(
build_dir=settings.BASE_DIR / "docs" / "_build" / "json",
source_dir=settings.BASE_DIR / "docs",
)
Then build it:
python manage.py build_docs
That runs Sphinx's JSON build from source_dir into build_dir, as
sphinx-build -b json would, so the build lands where the app already reads it. With no arguments the command builds every
mounted documentation app that has a source_dir, in the order they are mounted,
and leaves the others alone. Name one or more apps by namespace to build only
those:
python manage.py build_docs handbook
The command stops with an error, and a non-zero exit status, when Sphinx reports
a failed build, when a namespace belongs to no mounted documentation app, when a
named app has no source_dir, and when no mounted app has one. A failed build
stops the ones after it. --verbosity 0 prints warnings and errors only.
Sphinx has to be installed where you run the command, exactly as for
sphinx-build. Nothing else changes: the site still never imports Sphinx and
never starts a build to answer a request, so a page is only ever as new as the
last build you ran. source_dir is read by this command and nothing else, and an
app without one is served as before.
The contents in the sidebar
When sphinx-build -b json finishes, the extension you added in step 2 of the
quickstart writes a navigation.json
into the build with every page your toctrees list, hidden toctrees included.
The documentation app's menu, a DocumentationMenu, draws it as the sidebar
menu on every page of the docs.
- Each captioned toctree on your front page becomes a group named by its caption. An uncaptioned toctree puts its pages at the top level.
- A page with pages of its own opens as a group, and its first entry is the page itself.
- The front page has its own entry at the top, under its own title. A
navigation.jsonwritten before the extension recorded that title labels it "Overview". - A page no toctree lists stays out of the sidebar.
Only the JSON builder gets a file, and a build that failed writes nothing.
Rebuild the docs and the sidebar shows the change on the next request, with no restart.
The extension runs inside your Sphinx build and nowhere else, so the site that
serves the pages still doesn't need Sphinx. Without the line, or with a
navigation.json that can't be read, the sidebar holds only the front page
entry and every page is still served.
Adding your own entries to the sidebar
The contents come from the build, and you can add entries of your own beside
them. This matters most when the docs are your project's main app, mounted with
main=True: django-mvp then draws the documentation menu on every page that
belongs to no other app and leaves AppMenu out, so this menu is where links to
your other apps and pages go.
Add them to the documentation app's menu, in the menus.py of one of your
installed apps:
# yourapp/menus.py
from flex_menu import MenuItem
from yourproject.mounted import docs, studio
docs.menu.append(studio.menu_item())
docs.menu.insert(
MenuItem(name="home", view_name="home", extra_context={"label": "Home"}),
position=0,
)
- An entry you
appendorextendfollows the contents. - An entry you
insertahead of the front page's entry stays ahead of it. Inserted anywhere else, it follows the contents after the next rebuild. - Your entries keep their places when the docs are rebuilt. A rebuild replaces
only the entries that came from
navigation.json. - They behave like menu entries everywhere else: an entry whose
checkrefuses the request isn't drawn.
The page's own headings
On a wide screen, each page lists its own headings beside it, under "On this page", and the page's text widens into the room beside the list. The list is nested the way the page nests its sections, each entry links to its heading, and it stays in view below the top bar as the page scrolls. Sphinx already records that tree in the build, so there is nothing to configure and the site that serves the pages still doesn't need Sphinx.
- The page's title is not listed, and neither are the headings of other pages, so a front page that only holds a toctree lists nothing from the pages it links to. The sidebar keeps the contents.
- A page with no headings below its title shows no list at all.
- Sphinx's
:tocdepth:setting decides how deep the list goes.
PageView reads the tree with PageHeadings.from_toc(toc), which turns a
page's toc value from the build into nested dicts of title, anchor and
children. Call it yourself if you draw the headings in a template of your own.
Rebuild the docs and the list follows on the next request.
Every page also ends with links to the page before it and the page after it, in
the order Sphinx puts the pages in, each showing the title of the page it leads
to. The front page has no previous link, the last page has no next link, and a
page that no toctree lists has neither. The links stay inside the documentation
app, so a second app mounted elsewhere links under its own address. An
incremental Sphinx build only rewrites the pages that changed and the pages
whose toctrees changed, so a page's links follow a newly inserted neighbour once
that page is rebuilt. Run a full rebuild (-E) if in doubt.
Search
Every page of a documentation app has a search box. It lists the pages of that
docs build that contain all the words typed, in any order and any case, and finds
other forms of a word too, so lanterns finds a page that says "lantern". It
searches that one app's pages and never the rest of your site or another
documentation app. Adding a word narrows the list. Common words such as "the" are
ignored, as Sphinx ignores them.
The page whose title holds all the words comes first, then a page with a section heading that holds them, then the rest, each group in order of title. Every result shows the page's title and, when the page's text holds one of the words, a short passage around it. A result found by a section heading links to that section.
Nothing needs configuring. The search reads the search data Sphinx already writes
into every JSON build (searchindex.json), so there is no extra Sphinx setting and
no Sphinx where the site runs. The only extra dependency is snowballstemmer, the
stemmer Sphinx itself uses to build that data.
The box is an ordinary form that sends GET to the app's search/ address, for
example /docs/search/?q=lantern, so it works with scripts turned off and a search
can be bookmarked or shared. That address is where Sphinx's own search page would
be, so a :ref: link to search in your docs lands on it. Whoever may read the
pages may search them, under the same check.
The search/ address is reserved under every documentation app, so a folder of your
docs named search can't have its own index page there. Name that folder something
else.
The search data is read on each search, so a rebuild is searchable straight away. A build without it still serves its pages, and the results page says search is unavailable.
Naming the documentation
The app's name is what the tab title, the first breadcrumb and the menu entry
call the documentation. Give it your own when "Documentation" isn't right:
from django.conf import settings
from django.utils.translation import gettext_lazy as _
docs = DocumentationApp(
build_dir=settings.BASE_DIR / "docs" / "_build" / "json",
name=_("Administrator's handbook"),
)
Choosing who can read it
The documentation is open to everyone unless you say otherwise. To keep it for
signed-in people, import user_is_authenticated from flex_menu.checks and pass
it as check:
from django.conf import settings
from flex_menu.checks import user_is_authenticated
docs = DocumentationApp(
build_dir=settings.BASE_DIR / "docs" / "_build" / "json",
check=user_is_authenticated,
)
That is one import and one keyword. A reader the rule excludes gets no menu entry and no page. An anonymous visitor is sent to your sign-in page and comes back to the address they asked for once signed in. The rule covers every address under the prefix, including the images and downloads your pages link to, and it answers the same way whatever is behind the address, so a reader cannot tell which pages exist.
To keep it for a group or for holders of a permission, use the other two
checks from flex_menu.checks. Both are factories, so call them with their
arguments:
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from flex_menu.checks import user_has_any_permission, user_in_any_group
docs = DocumentationApp(
build_dir=settings.BASE_DIR / "docs" / "_build" / "json",
check=user_in_any_group("Support"),
)
tickets = DocumentationApp(
build_dir=settings.BASE_DIR / "tickets" / "_build" / "json",
name=_("Ticket handling"),
namespace="tickets",
check=user_has_any_permission("support.view_ticket"),
)
Any function of the request works too:
def staff_only(request):
return request.user.is_staff
handbook = DocumentationApp(
build_dir=settings.BASE_DIR / "handbook" / "_build" / "json",
namespace="handbook",
check=staff_only,
)
A signed-in reader the rule excludes gets your project's 403 page, which says
nothing about the documentation, and no menu entry. The rule alone decides.
Staff and superusers have no way in unless it admits them, and check=False
admits no one.
The rule is asked on every request, and often more than once in one, since the menu entry asks it as well as the page. Keep it quick and free of side effects. If it raises, the request is a server error: a page is never served on a guess. The menu entry asks the rule too, so the error also shows on every page that draws the entry, the sign-in page included. Write a rule that returns an answer.
A check that is not a function is read as yes or no. check="staff" is a
non-empty string, which is true, and admits everyone. To keep the docs for
staff, pass user_is_staff from flex_menu.checks, or a function of your own.
Two audiences are two apps. Each DocumentationApp has its own build, name,
namespace and rule, and each is mounted at its own prefix:
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from flex_menu.checks import user_is_staff
guide = DocumentationApp(
build_dir=settings.BASE_DIR / "guide" / "_build" / "json",
)
staff_guide = DocumentationApp(
build_dir=settings.BASE_DIR / "staff_guide" / "_build" / "json",
name=_("Staff guide"),
namespace="staff_guide",
check=user_is_staff,
)
from mvp.mounted import mount
urlpatterns = [
mount("guide/", guide),
mount("staff-guide/", staff_guide),
]
Each reader sees the entries their rules admit and is served or refused by each app's own rule. The rule is asked again on every request, so a reader who signs in or joins a group is admitted by their next request, with nothing to reset.
The rule is django-mvp's check, described in its
mounted apps guide.
Several documentation apps
Each build is one DocumentationApp with its own namespace (docs unless you
say otherwise), mounted at its own prefix, which may have several segments. Each
app serves only its own build and names only itself in its tabs and breadcrumbs.
handbook = DocumentationApp(
build_dir=settings.BASE_DIR / "handbook" / "_build" / "json",
name=_("Administrator's handbook"),
namespace="handbook",
)
urlpatterns = [
mount("docs/", docs),
mount("manuals/admin/", handbook),
]
Add handbook.menu_item() to your menu beside docs.menu_item() to give each
its own entry.
Addresses and files served
The images and downloads your pages link to (_images/ and _downloads/ in
the build) are served at the addresses the pages already use, with the file's
content type. Nothing else in the build is: not the search index, the page data,
the sources, the static files or Sphinx's pickles. An address that tries to climb
out of those two folders answers 404.
build_dir is read on every request. Rebuild the docs and reload the page to see
the change, with no restart. It doesn't have to exist when the site starts, so a
project that hasn't built its docs yet still boots. Until the build exists every
address under the prefix answers 404, the rest of the site is unaffected, and the
first request after the build appears is served.
Addresses behave like the rest of your site. A page address without its trailing
slash redirects permanently to the slashed address, query string kept, and an
address with no page answers your own 404 page. The bare prefix (/docs) is
redirected by Django's CommonMiddleware (APPEND_SLASH), as for any other
mount. A page file that is not valid JSON is a broken build and raises, so it is
a server error rather than a 404.
How pages look and the page template
A PageView renders each page, and it finds the page's data through a
DocsBuild, which only ever looks inside build_dir. To change how a page is
drawn, subclass PageView and pass it to your app as view_class.
Pages take your site's theme with nothing to configure: the colours are
django-mvp's own, so a page follows the light and dark themes and any theme your
project defines. The package's page template links two stylesheets, served
like the rest of your static files, and only pages the documentation app renders
load them: mvp_sphinx/content.css styles what Sphinx writes into the page, and
mvp_sphinx/page.css places the "On this page" list beside it. Sphinx's own
stylesheets are never used.
Reference entries, the documented functions, classes and other objects that
autodoc writes and that you write by hand with directives such as
.. py:function::, are styled from the same theme with nothing to configure.
Each signature sits in a bar in the code colours, its description hangs from a
rule beneath it, and entries inside other entries read as inside them. This
holds for any language Sphinx documents, and it makes no difference whether an
entry was generated or typed.
A heading you follow a link to lands 5rem below the top of the window, and the "On this page" list sticks at the same distance, so both clear django-mvp's top bar. django-mvp doesn't publish the bar's height, so if yours is taller, with a tray or a second row, set the distance in your own stylesheet:
:root {
--mvp-sphinx-header-clearance: 7rem;
}
Before a page is rendered, BodyRewriter adds what a stylesheet cannot. It
wraps each table in a scrolling region that takes keyboard focus and is named by
the table's caption, or "Table" when it has none, so a table wider than the
page scrolls sideways for a reader using only a keyboard and a screen reader
announces what the region holds. An equation set out on its own line gets the
same treatment, named "Equation" or, when it is numbered, "Equation (1)", so a
wide equation can be scrolled from the keyboard before and after it is typeset;
its number stays outside the region. It also names each heading link (the ¶ Sphinx
puts beside a section heading, a glossary term or a caption) with the link's own
title and the heading's text, such as "Link to this heading: Installing", so a
screen reader tells one link from the next. A reference entry's link is named by
the entry and not by its whole signature, such as "Link to this definition:
demo.links.page_address", so a page of entries reads as a list of names.
Everything else in the body reaches the page exactly as Sphinx wrote it.
PageView applies it and hands the result to the template as body, so a
PageView subclass gets it too; to use it elsewhere, call
BodyRewriter.rewrite(markup).
If you override mvp_sphinx/page.html, keep {{ block.super }} in its styles
block so both stylesheets still reach the page, render {{ body }} rather than
{{ page_data.body }} so your override keeps the rewrite, and keep the
mvp-sphinx-content class on the element that holds it, because every rule in
content.css is scoped to that class. An override of the content block also
takes over drawing "On this page" and the previous and next links, which the page
gets as headings, previous_page and next_page. BodyRewriter.rewrite returns
a plain string; PageView marks it safe because the docs build is your own, and a
caller of its own does the same:
{% extends "mvp_sphinx/page.html" %}
{% load static %}
{% block styles %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'yourproject/docs.css' %}">
{% endblock styles %}
Maths
Notation written with the math role or directive is typeset by
MathJax 4, in the reader's browser. Sphinx's own HTML
build uses the same library from the same place, and so does this package: the
page loads it from the jsDelivr CDN, at https://cdn.jsdelivr.net/npm/mathjax@4/tex-mml-chtml.js.
Typeset maths takes its colours from your theme and follows light and dark. Notation
MathJax cannot read, such as an unknown command, is shown in the theme's
error colour and not in MathJax's red.
Only a page that holds maths loads the library, together with the small settings
file mvp_sphinx/maths.js that tells it to look only inside what Sphinx marked
as maths, so nothing else on the page, your own shell included, is read as
notation. Every other page loads neither. It works for a docs build made with
Sphinx's default settings (html_math_renderer left alone, so notation reaches
the page as \(...\) and \[...\]). A build that renders maths as images shows
the images. The library is still loaded on those pages and finds nothing to
typeset.
If the reader's network has no outside access, or your site sends a content
security policy that does not allow cdn.jsdelivr.net, the library does not load.
(A policy has to allow that origin for scripts and fonts, and allow inline
styles, for typeset maths to show.)
The reader then sees the notation as written, in the code font, and the rest of
the page is unaffected.
The script runs in your own pages, with the reader's session, signed-in readers
included. The address follows MathJax's newest 4.x release and is loaded without
an integrity check, so what runs can change without a release of this package. If your project does not accept a third-party script, or needs the
library from another source, override mvp_sphinx/page.html and write your own
extra_js block, which is where the two script tags are.
Live examples
A live example puts a page of your own site into a documentation page. The page runs in a frame, so a reader can use it, and the code that makes it sits beside the frame. A reader can fill in a form and see what your site answers without leaving the documentation.
Write one with the live-example directive. The extension you already named in
conf.py provides it, so Sphinx needs no further setting. The argument is the
address of the page on your site, a path that starts with a single /. The
option :title: names the example, and the line below it names the file whose
code to show, relative to the documentation page's own file:
.. live-example:: /examples/contact/
:title: A contact form
../../examples/forms.py
Rebuild the docs and the page shows the frame with the file beside it. A page
with an example loads one more stylesheet, mvp_sphinx/example.css; other pages
load nothing new.
Each example has two links above the frame. "Start again" loads the example's address in the frame again, which puts the page back as it was when the reader arrived, and "Open on its own" opens the address as a page of its own. Both are plain links, and the tabs for several files are radio inputs, so all of them work with scripts off.
Several files, and parts of files
List more than one file, one to a line, and the reader gets a tab for each, in the
order you wrote them. Follow a file with first-last, or with one line number, to
show only those lines, counted from 1. The lines are shown with the indentation
they all share removed, so a method reads flush left:
.. live-example:: /examples/contact/
:title: A contact form
../../examples/forms.py
../../examples/views.py 12-25
/templates/contact.html
A path is relative to the documentation page's own file, or starts with / to
count from the documentation's source directory. A path may hold spaces. The last
word is read as lines only when it is a number or two numbers joined by a hyphen.
Line numbers do not follow edits to the file, so check them when you change it.
Each tab is named by its file. When two files of one example have the same name,
each tab adds the parent folders it needs to tell them apart, as a/forms.py and
b/forms.py. When one file is named twice with different lines, the lines are
added to the name, as views.py 12-25. A file named once keeps its plain name,
and an example with one file shows no tabs.
What the build reports
A marker the build cannot use is a Sphinx warning, at the line of the directive, so the message names the documentation page. The build then goes on without that part of the marker:
- An address that is not a path of your site. It must start with one
/and hold no scheme, host, backslash or space, sohttps://host/x,//host/x,/\host/xandhost/xare all refused. The page gets no example and none of its sources. - A source file that does not exist, or that is not UTF-8 text. The message names the file, and the example keeps its other sources.
- A range that is reversed, zero, or past the end of the file. The message names the file, and that source is left out. A last word that is not a number is part of the path, so it is reported as a file that does not exist.
- An example left with no source at all. The page gets no example.
Run Sphinx with -W and each of these fails the build, so a typo in an address
or a file that was renamed does not reach the published documentation.
Writing the example's page
The page in the frame is an ordinary page of your site, with an ordinary view.
It would show your application shell inside the frame, so give it a template
that extends mvp_sphinx/example.html and fills the content block:
{% extends "mvp_sphinx/example.html" %}
{% block content %}
<c-form method="post" :form-obj="form">
<c-button text="Send message" type="submit" variant="primary" />
</c-form>
{% endblock content %}
mvp_sphinx/example.html extends your own base.html, so the page keeps your
theme, stylesheets and scripts. It replaces only the app block, the one that
holds the shell, with a main element that holds content and the messages.
A page that extends a template with the shell still shows with the shell, inside
the frame.
A form in an example posts to the example's own address, so the answer appears
in the frame and the documentation page's address does not change. Have the view
redirect to request.path after a valid post, and a message added with
django.contrib.messages shows in the frame.
Letting your site frame itself
Django sends X-Frame-Options: DENY by default, and a browser then shows nothing
in the frame. Set this in your settings:
X_FRAME_OPTIONS = "SAMEORIGIN"
It lets pages of your site be framed by pages of your site and by nobody else.
It applies to every page, so it covers the sign-in page and the error pages
too, which is what lets an example show them. If your site sends a
Content-Security-Policy header with frame-ancestors, include 'self' in it.
The package cannot check either setting for you, and a frame that is empty or
shows the browser's own refusal usually means one of them is missing.
What the reader sees
The example is your site's own page at its own address, so your own rule for that address decides who sees what. A reader your site sends to sign in, or refuses, sees that answer in the frame. The documentation app's reader rule decides who reads the documentation page, and with it the source shown beside the frame.
The source is a copy made when the docs are built. Change the file and rebuild to refresh it. The page in the frame is always your site's current page.
If the site has no page at the address when a documentation page is served, the page says so in place of the frame, and the source is still shown.
Public surface
These are the names a project can use, grouped the way a project meets them.
Installed app and Sphinx extension
mvp_sphinxis the Django app. Add it toINSTALLED_APPSaftermvp.mvp_sphinx.navigationis the Sphinx extension. It writesnavigation.jsoninto a JSON build and adds thelive-exampledirective.
The documentation app
mvp_sphinx.mounted.DocumentationAppserves one docs build under the prefix you mount it at. Its keyword options arebuild_dir(required),source_dir,name,icon,namespace,view_classandcheck.menu_item(), inherited from django-mvp'sMountedApp, returns the entry to add to your own menus.menuis the app'sDocumentationMenu, which draws the contents as the app sidebar.- Its URL names are
<namespace>:front_page,<namespace>:page(which takespath) and<namespace>:search.
Management command
build_docsruns the Sphinx JSON build from each documentation app'ssource_dirinto itsbuild_dir. It takes the namespaces of the apps to build, and builds every app that has asource_dirwhen given none.
Views
mvp_sphinx.views.PageViewrenders a page. Subclass it and pass the subclass asview_classto change how a page is drawn. A subclass may overridetemplate_name,get_context_data(),get_page_title(),get_headings(),get_neighbour(key)(keyis"prev"or"next") andget_breadcrumbs().mvp_sphinx.views.SearchViewrenders the results of a search.
Building blocks
For a custom view or template:
mvp_sphinx.docs_build.DocsBuild(root)reads a docs build and never looks outside it.page(path)returns a page's data orNone,file(path)returns an image or download under_images/or_downloads/orNone, andnavigation()returns the entries ofnavigation.jsonorNone,front_page_title()returns the root document's title it records orNone, andnavigation_file()returns the whole file orNone.mvp_sphinx.menus.DocumentationMenuturns a build's navigation file into the sidebar menu.front_pageis the front page's entry,front_page_url(request)is the address it links to, andcontentslists the entries the last rebuild read from the file.mvp_sphinx.headings.PageHeadings, throughPageHeadings.from_toc(toc), turns a page'stocvalue into nested headings.mvp_sphinx.page_body.BodyRewriter, throughBodyRewriter.rewrite(markup), names table regions, equation regions and heading links in a page body.BodyRewriter.parse(markup)returns the parser itself:splice()gives the same rewritten body, andhas_mathssays whether the body holds maths.mvp_sphinx.page_body.Equationis the parser's own record of where one equation's number and notation sit while it reads, and nothing a project needs to build.mvp_sphinx.search.DocsSearch(build), given aDocsBuild, searches it.results(query)lists the pages that hold every word ofquery, best match first, each with itstitle,path,anchorandpassage, or returnsNonewhen the build has no usable search data.mvp_sphinx.search.PageText, throughPageText.text(markup), gives the text a reader sees in a page body, with its whitespace collapsed.mvp_sphinx.examples.LiveExamples, throughLiveExamples.parts(body), splits a rewritten page body into the markup and the live examples in it, in order. Each part is{"html": ...}or{"example": ...}, and an example holds itsid,title,address,availableandsources.PageViewpasses the result to the template asbody_parts.mvp_sphinx.examples.ExampleReaderis the parserpartsreads with, and nothing a project needs to build.
Templates a project may override
mvp_sphinx/page.htmldraws a page. It receivesbody,body_parts,has_examples,headings,previous_page,next_page,search_urlandpage_data. It has thetitle,stylesandcontentblocks, and it holds the page's body in an element with themvp-sphinx-contentclass.mvp_sphinx/example.htmlis the base for a page of your site that a documentation page shows as a live example. It extends yourbase.html, draws no shell, and has thecontentblock.mvp_sphinx/search.htmldraws the results. It receivesquery,resultsandsearch_url, and has thetitle,stylesandcontentblocks.resultsisNonewhen the build has no search data, and otherwise a list of the pages found, each with itstitle,passageandhref.
Components
Use these in your own templates as <c-mvp_sphinx.on_this_page /> and so on:
mvp_sphinx.on_this_pagedraws a page's headings. It takesheadings.mvp_sphinx.heading_listdraws nested headings as a list. It takesheadings.mvp_sphinx.page_linksdraws the links to the previous and next page. It takespreviousandnext.mvp_sphinx.search_formdraws the search box. It takesactionandquery.mvp_sphinx.live_exampledraws one live example, the frame and its source. It takesexample, one{"example": ...}part fromLiveExamples.parts.
Static files
mvp_sphinx/content.cssis the stylesheet page content uses, andmvp_sphinx/page.cssthe one that places "On this page" and sets--mvp-sphinx-header-clearance.mvp_sphinx/maths.jsholds the settings for the maths typesetting. A page with maths loads it before the library.mvp_sphinx/example.csssizes the live example's frame and its source. A page with an example loads it.
Settings
There are none. The package reads no Django setting, and everything is configured
on the DocumentationApp.
Anything not listed here is internal and may change without notice.
Contributing
Standards for this repository live in CONSTITUTION.md, and the vocabulary to use in issues and commits lives in CONTEXT.md.
uv sync
uv run pytest
uv run pre-commit install
The demo
demo/ is a Django project on django-mvp's application shell, for looking at this
package in a browser while you work on it. It serves a user guide of its own and a
second guide for staff, each through a documentation app, and the guides between
them hold every state this package draws.
The guides' builds are not committed, so build them before you start the server.
build_docs is the package's own
management command,
and it builds both of the demo's guides:
uv sync
uv run python manage.py migrate
uv run python manage.py seed_demo
uv run python manage.py build_docs
uv run python manage.py runserver
Then open http://127.0.0.1:8000/docs/ for the user guide. Its sidebar reaches every
page, and ends with an entry the demo adds itself: a link to the staff guide, drawn
for staff only. After you edit a page under demo/docs/, run build_docs docs and
reload, with no restart. Until a build exists, every address under /docs/ answers 404.
seed_demo made three accounts, all with the password password:
regular.user@example.comis signed in but not staff.staff.user@example.comis staff.super.user@example.comis a superuser, and staff too.
Only the staff and superuser accounts can open the staff guide at
http://127.0.0.1:8000/staff-guide/. Everyone else is asked to sign in or is shown
the forbidden page, and the sidebar has no entry for them. The command refuses to run
unless DEBUG is on.
License
MIT. See LICENSE.
Metadata
Release files for django-mvp-sphinx 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_mvp_sphinx-0.1.0.tar.gz | 60.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_mvp_sphinx-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 118.2 kB
Release files / django_mvp_sphinx-0.1.0.tar.gz
| Download URL | django_mvp_sphinx-0.1.0.tar.gz |
|---|---|
| Size | 60.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c99faaa0b768e0fb915b064453ca8827daad9fdd9d0b08f76619286d213a708c
|
|
BLAKE2b-256 checksum How to use checksums |
500ca8c8a5c8abc899d9ebe4cd14ae0096312ebf2e2ada62595e3b192a183630
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency logRelease files / django_mvp_sphinx-0.1.0-py3-none-any.whl
| Download URL | django_mvp_sphinx-0.1.0-py3-none-any.whl |
|---|---|
| Size | 57.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d2c9355b851f66274cfd00cc83854f83dc04af1688eafdce174948c73e4575b3
|
|
BLAKE2b-256 checksum How to use checksums |
85838d3c98375c1e59b3e0eb4fbd02bcc48484e7b995518aaa07148c81e5dd1e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency log