Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
22 changes: 3 additions & 19 deletions docs/app/agent_files/_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from reflex.constants import Dirs
from reflex_base.config import get_config
from reflex_base.plugins import CommonContext, Plugin
from reflex_site_shared.utils.url import public_url
from typing_extensions import Unpack

MCP_DOC_PATHS = {
Expand Down Expand Up @@ -116,29 +117,12 @@ def _extract_markdown_title(source: str) -> str | None:

def _llms_url_for_path(url_path: Path) -> str:
"""Return the public URL for a generated markdown asset."""
config = get_config()
deploy_url = config.deploy_url.removesuffix("/") if config.deploy_url else ""
frontend_path = (config.frontend_path or "").strip("/")
base_url = deploy_url
if frontend_path:
base_url = f"{base_url}/{frontend_path}" if base_url else f"/{frontend_path}"
return (
f"{base_url}/{url_path.as_posix()}" if base_url else f"/{url_path.as_posix()}"
)
return public_url(f"/{url_path.as_posix()}")


def _docs_home_url() -> str:
"""Return the public URL for the docs home."""
config = get_config()
deploy_url = config.deploy_url.removesuffix("/") if config.deploy_url else ""
frontend_path = (config.frontend_path or "").strip("/")
if deploy_url and frontend_path:
return f"{deploy_url}/{frontend_path}/"
if deploy_url:
return f"{deploy_url}/"
if frontend_path:
return f"/{frontend_path}/"
return "/"
return public_url("/")


def _strip_first_heading(source: str) -> str:
Expand Down
61 changes: 50 additions & 11 deletions docs/app/reflex_docs/pages/docs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import os
import re
from collections import defaultdict, namedtuple
from functools import lru_cache
from pathlib import Path
from types import SimpleNamespace

import reflex as rx
from reflex_components_core.core.cond import Cond
from reflex_docgen.markdown import parse_document
from reflex_docgen.markdown import FrontMatter, parse_document

# External Components
from reflex_pyplot import pyplot as pyplot
Expand Down Expand Up @@ -73,14 +74,36 @@ def build_nested_namespace(
return parent_namespace


# Leading YAML frontmatter block, mirroring reflex_docgen's parser.
_FRONTMATTER_BLOCK_RE = re.compile(r"\A---\n.*?\n---\n", re.DOTALL)


@lru_cache(maxsize=None)
def _frontmatter_for(filepath: str) -> FrontMatter | None:
"""Parse a doc's frontmatter once per file (cached for the process lifetime).

Only the frontmatter block is fed to the parser so this stays cheap even
when called for every doc at startup; the body is parsed separately by the
rendering pipeline. Read failures yield None (like docs without
frontmatter) so a missing/unreadable file can't abort route registration.
"""
try:
source = Path(filepath).read_text(encoding="utf-8")
except OSError:
return None
block = _FRONTMATTER_BLOCK_RE.match(source)
if block is None:
return None
return parse_document(block.group(0)).frontmatter


def get_components_from_frontmatter(filepath: str) -> list:
"""Extract component tuples from a doc's frontmatter."""
source = Path(filepath).read_text(encoding="utf-8")
doc = parse_document(source)
if doc.frontmatter is None:
fm = _frontmatter_for(filepath)
if fm is None:
return []
components = []
for comp_str in doc.frontmatter.components:
for comp_str in fm.components:
if component := SPECIAL_COMPONENT_DOCS.get(comp_str):
components.append((component, comp_str))
continue
Expand All @@ -98,11 +121,16 @@ def get_components_from_frontmatter(filepath: str) -> list:

def get_previews_from_frontmatter(filepath: str) -> dict[str, str]:
"""Extract component preview sources from a doc's frontmatter."""
source = Path(filepath).read_text(encoding="utf-8")
doc = parse_document(source)
if doc.frontmatter is None:
fm = _frontmatter_for(filepath)
if fm is None:
return {}
return {p.name: p.source for p in doc.frontmatter.component_previews}
return {p.name: p.source for p in fm.component_previews}


def get_image_from_frontmatter(filepath: str) -> str | None:
"""Resolve a per-page social preview image from frontmatter, if any."""
fm = _frontmatter_for(filepath)
return fm.image if fm is not None else None


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -327,14 +355,21 @@ def extract_doc_description(


def make_docpage(
route: str, title: str, doc_virtual: str, render_fn, description: str | None = None
route: str,
title: str,
doc_virtual: str,
render_fn,
description: str | None = None,
image: str | None = None,
):
"""Wrap a render function as a docpage, setting module metadata."""
doc_path = Path(doc_virtual)
render_fn.__module__ = ".".join(doc_path.parts[:-1])
render_fn.__name__ = doc_path.stem
render_fn.__qualname__ = doc_path.stem
return docpage(set_path=route, t=title, description=description)(render_fn)
return docpage(set_path=route, t=title, description=description, image=image)(
render_fn
)


CHANGELOG_VIRTUAL_PREFIX = "docs/changelog/"
Expand Down Expand Up @@ -366,6 +401,7 @@ def handle_library_doc(
"""Handle docs/library/** docs — component API reference via multi_docs."""
clist = [title, *get_components_from_frontmatter(actual_path)]
previews = get_previews_from_frontmatter(actual_path)
image = get_image_from_frontmatter(actual_path)
ll_actual_path = actual_path.replace(".md", "-ll.md")
ll_clist: list | None = None
if os.path.exists(ll_actual_path):
Expand Down Expand Up @@ -405,6 +441,7 @@ def handle_library_doc(
title=display_title,
ll_component_list=ll_clist,
description=description,
image=image,
source=source,
)

Expand Down Expand Up @@ -443,12 +480,14 @@ def comp(_actual=actual_path, _virtual=virtual_doc, _content=doc_text):
return ((toc, doc_content), body)

description = extract_doc_description(doc_text)
image = get_image_from_frontmatter(actual_path)
return make_docpage(
resolved.route,
resolved.display_title,
virtual_doc,
comp,
description=description,
image=image,
)


Expand Down
10 changes: 7 additions & 3 deletions docs/app/reflex_docs/pages/docs/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -902,8 +902,9 @@ def multi_docs(
previews: dict[str, str],
component_list: list,
title: str,
ll_component_list: list | None = None,
description: str | None = None,
image: str | None = None,
ll_component_list: list | None = None,
source: str | None = None,
):
components = [
Expand Down Expand Up @@ -965,7 +966,7 @@ def links(current_page, ll_doc_exists, path):
)
return rx.fragment()

@docpage(set_path=path, t=title, description=description)
@docpage(set_path=path, t=title, description=description, image=image)
def out():
toc = get_docgen_toc(actual_path)
# Reuse the source already read by the caller to avoid a second read.
Expand Down Expand Up @@ -1009,7 +1010,10 @@ def out():
)

@docpage(
set_path=path + "low", t=title + " (Low Level)", description=ll_description
set_path=path + "low",
t=title + " (Low Level)",
description=ll_description,
image=image,
)
def ll():
ll_virtual = virtual_path.replace(".md", "-ll.md")
Expand Down
9 changes: 3 additions & 6 deletions docs/app/reflex_docs/reflex_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,13 @@ def _canonical_url(path: str) -> str:


# Add the pages to the app.
_DEFAULT_PREVIEW = f"{REFLEX_ASSETS_CDN}previews/index_preview.webp"
for route in routes:
# print(f"Adding route: {route}")
if _check_whitelisted_path(route.path):
# Normalize image to CDN URL when it's a relative path
image_url = (
f"{REFLEX_ASSETS_CDN}previews/index_preview.webp"
if route.image is None
else to_cdn_image_url(route.image)
or f"{REFLEX_ASSETS_CDN}previews/index_preview.webp"
)
to_cdn_image_url(route.image) if route.image else None
) or _DEFAULT_PREVIEW

# Build a complete, page-specific set of SEO meta tags (description,
# Open Graph, Twitter card, canonical) from the route's own title and
Expand Down
3 changes: 3 additions & 0 deletions docs/app/reflex_docs/templates/docpage/docpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ def docpage(
page_title: str | None = None,
pseudo_right_bar: bool = False,
description: str | None = None,
image: str | None = None,
):
"""A template that most pages on the reflex.dev site should use.

Expand All @@ -256,6 +257,7 @@ def docpage(
description: The meta description for the page. If None, a descriptive
fallback derived from the page title is used so the page always has
a non-empty, page-specific meta description.
image: Social-preview image (relative path or absolute URL).

Returns:
A wrapper function that returns the full webpage.
Expand Down Expand Up @@ -465,6 +467,7 @@ def wrapper(*args, **kwargs) -> rx.Component:
path=path,
title=seo_title,
description=seo_description,
image=image,
component=wrapper,
)

Expand Down
52 changes: 17 additions & 35 deletions docs/app/tests/test_agent_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,21 @@
)


def _patch_config(monkeypatch, deploy_url: str, frontend_path: str = "/docs"):
"""Patch the site config everywhere it is read.

Public URLs are built via ``reflex_site_shared.utils.url.public_url``, which
reads the config through its own module-level ``get_config`` import, so
patching only ``agent_files._plugin.get_config`` is not enough.
"""
config = SimpleNamespace(deploy_url=deploy_url, frontend_path=frontend_path)
monkeypatch.setattr("agent_files._plugin.get_config", lambda: config)
monkeypatch.setattr("reflex_site_shared.utils.url.get_config", lambda: config)


def test_generate_llms_txt_groups_docs_at_public_root(monkeypatch):
"""The docs mount exposes public-root llms.txt as /docs/llms.txt."""
monkeypatch.setattr(
"agent_files._plugin.get_config",
lambda: SimpleNamespace(
deploy_url="https://reflex.dev",
frontend_path="/docs",
),
)
_patch_config(monkeypatch, deploy_url="https://reflex.dev")

path, content = generate_llms_txt([
MarkdownIndexEntry(
Expand Down Expand Up @@ -127,13 +133,7 @@ def test_generate_llms_txt_groups_docs_at_public_root(monkeypatch):

def test_generate_markdown_file_content_adds_agent_directive(monkeypatch, tmp_path):
"""Generated markdown pages advertise the docs index and markdown access."""
monkeypatch.setattr(
"agent_files._plugin.get_config",
lambda: SimpleNamespace(
deploy_url="http://localhost:3000",
frontend_path="/docs",
),
)
_patch_config(monkeypatch, deploy_url="http://localhost:3000")
source = tmp_path / "overview.md"
source.write_text(
"# Overview\n\nBuild full-stack apps in Python.\n",
Expand Down Expand Up @@ -161,13 +161,7 @@ def test_generate_markdown_file_content_appends_component_props_table(
monkeypatch, tmp_path
):
"""Component docs markdown includes generated API reference props tables."""
monkeypatch.setattr(
"agent_files._plugin.get_config",
lambda: SimpleNamespace(
deploy_url="https://reflex.dev",
frontend_path="/docs",
),
)
_patch_config(monkeypatch, deploy_url="https://reflex.dev")
source = tmp_path / "button.md"
source.write_text(
"---\n"
Expand Down Expand Up @@ -217,13 +211,7 @@ def test_generate_markdown_file_content_appends_component_props_table(

def test_generate_dynamic_api_reference_files(monkeypatch):
"""Dynamic API reference pages have generated markdown assets."""
monkeypatch.setattr(
"agent_files._plugin.get_config",
lambda: SimpleNamespace(
deploy_url="https://reflex.dev",
frontend_path="/docs",
),
)
_patch_config(monkeypatch, deploy_url="https://reflex.dev")

raw_files = generate_dynamic_api_reference_files()
files = dict(raw_files)
Expand Down Expand Up @@ -281,13 +269,7 @@ def test_section_for_root_level_markdown_strips_extension():

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(
"agent_files._plugin.get_config",
lambda: SimpleNamespace(
deploy_url="https://reflex.dev",
frontend_path="/docs",
),
)
_patch_config(monkeypatch, deploy_url="https://reflex.dev")
introduction = tmp_path / "introduction.md"
introduction.write_text(
"# Introduction\n\nBuild full-stack apps in Python.\n",
Expand Down
40 changes: 40 additions & 0 deletions docs/app/tests/test_frontmatter_meta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Unit tests for the frontmatter metadata helpers in reflex_docs.pages.docs."""

from reflex_docs.pages.docs import _frontmatter_for, get_image_from_frontmatter


def test_frontmatter_for_extracts_fields(tmp_path):
"""Frontmatter fields are parsed from a doc with a body."""
doc = tmp_path / "page.md"
doc.write_text(
"---\ntitle: Page\nimage: /previews/page.webp\n---\n\n# Page\n\nBody prose.\n",
encoding="utf-8",
)
fm = _frontmatter_for(str(doc))
assert fm is not None
assert fm.title == "Page"
assert fm.image == "/previews/page.webp"


def test_frontmatter_for_none_without_frontmatter(tmp_path):
"""Docs without a frontmatter block yield None."""
doc = tmp_path / "plain.md"
doc.write_text("# Plain\n\nNo frontmatter here.\n", encoding="utf-8")
assert _frontmatter_for(str(doc)) is None


def test_frontmatter_for_unreadable_file_returns_none(tmp_path):
"""A failed read yields None instead of aborting route registration."""
assert _frontmatter_for(str(tmp_path / "missing.md")) is None


def test_get_image_from_frontmatter(tmp_path):
"""The image helper returns the frontmatter image or None."""
with_image = tmp_path / "with_image.md"
with_image.write_text(
"---\nimage: /previews/foo.webp\n---\n\n# T\n", encoding="utf-8"
)
without_image = tmp_path / "without_image.md"
without_image.write_text("---\ntitle: T\n---\n\n# T\n", encoding="utf-8")
assert get_image_from_frontmatter(str(with_image)) == "/previews/foo.webp"
assert get_image_from_frontmatter(str(without_image)) is None
1 change: 1 addition & 0 deletions packages/reflex-docgen/news/6464.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added typed `description` and `image` frontmatter fields to `FrontMatter`, parsed from docs and preserved by the markdown writer, for per-page SEO metadata.
Loading
Loading