Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ following command to generate the `CHANGELOG.md` file in each subpackage.
uv run towncrier build --config pyproject.toml --version v0.9.4
```

**Where changelogs are published:** the docs site renders every `CHANGELOG.md`
in the repo (repo root and `packages/*/`) under
[reflex.dev/docs/changelog/](https://reflex.dev/docs/changelog/). The
`reflex-enterprise` changelog is read from the installed `reflex-enterprise`
distribution at docs build time; it appears once the published wheel ships a
`CHANGELOG.md` and the docs app's lockfile picks up that version.

## ✅ Making a PR

Once you solve a current issue or improvement to Reflex, you can make a PR, and we will review the changes.
Expand Down
2 changes: 1 addition & 1 deletion docs/app/agent_files/_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ def _section_for_path(url_path: Path) -> str:
return "Skills"
if path.startswith("ai/"):
return "AI Builder"
return _format_title(path.split("/", maxsplit=1)[0])
return _format_title(path.split("/", maxsplit=1)[0].removesuffix(".md"))


def _ordered_sections(
Expand Down
109 changes: 109 additions & 0 deletions docs/app/reflex_docs/changelogs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Discovery of package changelogs surfaced on the docs site.

The monorepo packages manage their changelogs with towncrier: the main
``reflex`` changelog lives at the repo root and each subpackage ships its own
under ``packages/<name>/CHANGELOG.md``. The ``reflex-enterprise`` package is
developed in a separate repo, so its changelog is read from the installed
distribution instead (it only appears once the published wheel ships a
``CHANGELOG.md``).
"""

from importlib.metadata import PackageNotFoundError, distribution
from pathlib import Path

ENTERPRISE_PACKAGE = "reflex-enterprise"


def discover_repo_changelogs(repo_root: Path) -> dict[str, Path]:
"""Find the changelogs maintained in this repo.

Args:
repo_root: The repo checkout root (parent of the docs content tree).

Returns:
A mapping of package name to its CHANGELOG.md path — ``reflex`` for
the repo-root changelog plus one entry per subpackage that ships one.
"""
changelogs: dict[str, Path] = {}
root_changelog = repo_root / "CHANGELOG.md"
if root_changelog.is_file():
changelogs["reflex"] = root_changelog
for pkg_changelog in (repo_root / "packages").glob("*/CHANGELOG.md"):
changelogs[pkg_changelog.parent.name] = pkg_changelog
return changelogs


def find_distribution_changelog(package: str) -> Path | None:
"""Locate the CHANGELOG.md shipped with an installed distribution.

Args:
package: The distribution name (e.g. ``reflex-enterprise``).

Returns:
The path to the installed CHANGELOG.md, or None when the distribution
is not installed or does not ship one.
"""
try:
dist = distribution(package)
except PackageNotFoundError:
return None
for file in dist.files or ():
if file.name == "CHANGELOG.md":
path = Path(str(dist.locate_file(file)))
if path.is_file():
return path
Comment thread
masenf marked this conversation as resolved.
Outdated
return None


def discover_changelogs(repo_root: Path) -> dict[str, Path]:
"""Find all package changelogs to publish on the docs site.

Args:
repo_root: The repo checkout root.

Returns:
A mapping of package name to CHANGELOG.md path, with ``reflex`` first
and the remaining packages in alphabetical order.
"""
changelogs = discover_repo_changelogs(repo_root)
enterprise_changelog = find_distribution_changelog(ENTERPRISE_PACKAGE)
if enterprise_changelog is not None:
changelogs[ENTERPRISE_PACKAGE] = enterprise_changelog
return {
name: changelogs[name]
for name in sorted(changelogs, key=lambda name: (name != "reflex", name))
}


def changelog_page_title(package: str) -> str:
"""Return the display title for a package changelog page.

Args:
package: The package name.

Returns:
The page title.
"""
return "Reflex Changelog" if package == "reflex" else f"{package} Changelog"
Comment thread
masenf marked this conversation as resolved.


def normalize_changelog(source: str, title: str) -> str:
"""Give changelog markdown a canonical top-level heading.

Towncrier-generated changelogs have no top-level heading, while
Keep-a-Changelog files (e.g. reflex-enterprise) start with a generic
``# Changelog``. Replace any existing H1 with *title* so every changelog
page renders consistently.

Args:
source: The raw changelog markdown.
title: The canonical page title.

Returns:
The normalized markdown.
"""
lines = source.lstrip().splitlines()
if lines and lines[0].startswith("# "):
del lines[0]
body = "\n".join(lines).strip("\n")
return f"# {title}\n\n{body}\n"
Comment thread
greptile-apps[bot] marked this conversation as resolved.
15 changes: 15 additions & 0 deletions docs/app/reflex_docs/docgen_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,21 @@ def render_markdown(text: str) -> rx.Component:
return transformer.transform(doc)


def render_markdown_with_toc(text: str) -> tuple[list[tuple[int, str]], rx.Component]:
"""Render a plain markdown text string, also extracting its TOC headings.

Args:
text: The markdown source.

Returns:
A ``(toc, body)`` tuple where ``toc`` is a list of ``(level, text)``
heading tuples and ``body`` is the rendered component.
"""
doc = parse_document(text)
toc = [(h.level, _spans_to_plaintext(h.children)) for h in doc.headings]
return toc, ReflexDocTransformer().transform(doc)


def render_inline_markdown(text: str, class_name: str = "") -> rx.Component:
"""Render a short markdown string inline (links, code spans, emphasis).

Expand Down
50 changes: 49 additions & 1 deletion docs/app/reflex_docs/pages/docs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,16 @@
from reflex_pyplot import pyplot as pyplot
from reflex_site_shared.route import Route

from reflex_docs.docgen_pipeline import get_docgen_toc, render_docgen_document
from reflex_docs.changelogs import (
changelog_page_title,
discover_changelogs,
normalize_changelog,
)
from reflex_docs.docgen_pipeline import (
get_docgen_toc,
render_docgen_document,
render_markdown_with_toc,
)
from reflex_docs.pages.docs.component import multi_docs
from reflex_docs.pages.library_previews import components_previews_pages
from reflex_docs.templates.docpage import docpage
Expand Down Expand Up @@ -203,6 +212,26 @@ def make_docpage(route: str, title: str, doc_virtual: str, render_fn):
return docpage(set_path=route, t=title)(render_fn)


CHANGELOG_VIRTUAL_PREFIX = "docs/changelog/"


def handle_changelog_doc(doc: str, actual_path: str, resolved: ResolvedDoc):
"""Handle docs/changelog/** docs — package changelogs pulled from outside the docs tree.

Changelog markdown ships without a meaningful top-level heading, so the
canonical page title is normalized in and the table of contents is limited
to version headings.
"""

def comp(_actual=actual_path, _title=resolved.display_title):
source = normalize_changelog(Path(_actual).read_text(encoding="utf-8"), _title)
toc, body = render_markdown_with_toc(source)
toc = [(level, text) for level, text in toc if level <= 2]
return ((toc, source), body)

return make_docpage(resolved.route, resolved.display_title, doc, comp)


def handle_library_doc(
doc: str,
actual_path: str,
Expand Down Expand Up @@ -240,6 +269,9 @@ def get_component_docgen(virtual_doc: str, actual_path: str, title: str):
if virtual_doc.startswith("docs/library"):
return handle_library_doc(virtual_doc, actual_path, title, resolved)

if virtual_doc.startswith(CHANGELOG_VIRTUAL_PREFIX):
return handle_changelog_doc(virtual_doc, actual_path, resolved)

def comp(_actual=actual_path, _virtual=virtual_doc):
toc = get_docgen_toc(_actual)
doc_content = Path(_actual).read_text(encoding="utf-8")
Expand All @@ -253,6 +285,22 @@ def comp(_actual=actual_path, _virtual=virtual_doc):
return make_docpage(resolved.route, resolved.display_title, virtual_doc, comp)


# Package changelogs live outside the docs tree — the towncrier-managed ones
# at the repo root (CHANGELOG.md and packages/*/CHANGELOG.md) and the
# reflex-enterprise one inside the installed distribution. Reach up and pull
# them in as regular docs under docs/changelog/, with the main reflex
# changelog served at the section index.
changelog_packages: dict[str, str] = {} # package name → route
for _package, _changelog_path in discover_changelogs(_docs_dir.parent).items():
_virtual = (
f"{CHANGELOG_VIRTUAL_PREFIX}index.md"
if _package == "reflex"
else f"{CHANGELOG_VIRTUAL_PREFIX}{_package}.md"
)
all_docs[_virtual] = str(_changelog_path)
manual_titles[_virtual] = changelog_page_title(_package)
changelog_packages[_package] = doc_route_from_path(_virtual)

# Build doc_markdown_sources mapping
for _virtual, _actual in all_docs.items():
if _virtual.endswith("-style.md"):
Expand Down
4 changes: 1 addition & 3 deletions docs/app/reflex_docs/templates/docpage/docpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,7 @@ def docpage_footer(path: rx.Var[str]) -> rx.Component:
[
footer_link("Home", "/"),
footer_link("Blog", "/blog"),
footer_link(
"Changelog", "https://github.com/reflex-dev/reflex/releases"
),
footer_link("Changelog", "/changelog/"),
],
),
footer_link_flex(
Expand Down
15 changes: 13 additions & 2 deletions docs/app/reflex_docs/templates/docpage/sidebar/sidebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
)
from .sidebar_items.learn import backend, frontend, hosting, learn
from .sidebar_items.recipes import recipes
from .sidebar_items.reference import api_reference
from .sidebar_items.reference import api_reference, changelog_items
from .state import SideBarBase, SideBarItem

SIDEBAR_ICON_MAP = {
Expand Down Expand Up @@ -318,6 +318,7 @@ def append_to_items(items, flat_items):
+ mcp_items
+ skills_items
+ api_reference
+ changelog_items
+ enterprise_items,
flat_items,
)
Expand Down Expand Up @@ -451,6 +452,7 @@ def sidebar_comp(
html_lib_index: rx.vars.ArrayVar[list[int]],
graphing_libs_index: rx.vars.ArrayVar[list[int]],
api_reference_index: rx.vars.ArrayVar[list[int]],
changelog_index: rx.vars.ArrayVar[list[int]],
recipes_index: rx.vars.ArrayVar[list[int]],
enterprise_usage_index: rx.vars.ArrayVar[list[int]],
enterprise_component_index: rx.vars.ArrayVar[list[int]],
Expand Down Expand Up @@ -485,7 +487,7 @@ def sidebar_comp(
)

is_library = url.contains("library") | url.contains("/mcp-")
is_api_reference = url.contains("api-reference")
is_api_reference = url.contains("api-reference") | url.startswith("/changelog/")
is_enterprise = url.contains("enterprise")
is_default_docs = ~is_library & ~is_api_reference & ~is_enterprise

Expand Down Expand Up @@ -652,6 +654,14 @@ def sidebar_comp(
api_reference_index,
url,
),
create_sidebar_section(
"Changelog",
"/changelog/",
changelog_items,
changelog_index,
url,
connected_line=True,
),
class_name="m-0 p-0 flex flex-col items-start gap-8 w-full list-none list-style-none",
)
enterprise_content = rx.el.ul(
Expand Down Expand Up @@ -761,6 +771,7 @@ def sidebar(url=None, width: str = "100%") -> rx.Component:
html_lib_index=calculate_index(html_lib, normalized_url),
graphing_libs_index=calculate_index(graphing_libs, normalized_url),
api_reference_index=calculate_index(api_reference, normalized_url),
changelog_index=calculate_index(changelog_items, normalized_url),
recipes_index=calculate_index(recipes, normalized_url),
enterprise_usage_index=calculate_index(
enterprise_usage_items, normalized_url
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
from ..state import SideBarItem
from .item import create_item


def get_sidebar_items_changelog():
from reflex_docs.pages.docs import changelog_packages

return [
SideBarItem(names=package, link=route)
for package, route in changelog_packages.items()
]


def get_sidebar_items_api_reference():
from reflex_docs.pages.docs import api_reference, apiref

Expand All @@ -24,3 +34,4 @@ def get_sidebar_items_api_reference():


api_reference = get_sidebar_items_api_reference()
changelog_items = get_sidebar_items_changelog()
8 changes: 8 additions & 0 deletions docs/app/tests/test_agent_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,14 @@ def test_generate_dynamic_api_reference_files(monkeypatch):
)


def test_section_for_root_level_markdown_strips_extension():
"""Root-level docs (e.g. the changelog index) get a clean section name."""
from agent_files._plugin import _section_for_path

assert _section_for_path(Path("changelog.md")) == "Changelog"
assert _section_for_path(Path("changelog/reflex-base.md")) == "Changelog"


def test_generate_llms_full_txt_stitches_markdown_docs(monkeypatch, tmp_path):
"""llms-full.txt contains full Markdown page bodies with source URLs."""
monkeypatch.setattr(
Expand Down
Loading
Loading