diff --git a/assets/common/dark/squares_vertical_blog.svg b/assets/common/dark/squares_vertical_blog.svg
new file mode 100644
index 000000000..360dec19a
--- /dev/null
+++ b/assets/common/dark/squares_vertical_blog.svg
@@ -0,0 +1,78 @@
+
diff --git a/assets/common/light/squares_vertical_blog.svg b/assets/common/light/squares_vertical_blog.svg
new file mode 100644
index 000000000..0519c1bdf
--- /dev/null
+++ b/assets/common/light/squares_vertical_blog.svg
@@ -0,0 +1,78 @@
+
diff --git a/pcweb/components/icons/icons.py b/pcweb/components/icons/icons.py
index c7889244b..588542978 100644
--- a/pcweb/components/icons/icons.py
+++ b/pcweb/components/icons/icons.py
@@ -517,6 +517,27 @@
"""
+twitter_blog = """"""
+
+linkedin_blog = """"""
+
+link_blog = """"""
+
+reddit_blog = """"""
+
markdown = """"""
ICONS = {
@@ -598,6 +619,10 @@
"python-01": python_01,
"shield-key": shield_key,
"chart-up": chart_up,
+ "twitter_blog": twitter_blog,
+ "linkedin_blog": linkedin_blog,
+ "link_blog": link_blog,
+ "reddit_blog": reddit_blog,
"markdown": markdown,
}
diff --git a/pcweb/pages/blog/blog.py b/pcweb/pages/blog/blog.py
index a2589cce9..f40d32e82 100644
--- a/pcweb/pages/blog/blog.py
+++ b/pcweb/pages/blog/blog.py
@@ -3,6 +3,7 @@
from pcweb.components.icons.icons import get_icon
from pcweb.components.webpage.comps import h1_title
from pcweb.meta.meta import create_meta_tags
+from pcweb.templates.marketing_page import marketing_page
from pcweb.templates.webpage import webpage
from .page import page
@@ -160,7 +161,7 @@ def blogs():
# Get the docpage component.
route = f"/blog/{path}"
title = rx.utils.format.to_snake_case(path.rsplit("/", 1)[1].replace(".md", ""))
- comp = webpage(
+ comp = marketing_page(
path=route,
title=document.metadata["title"] + " · Reflex Blog",
description=document.metadata["description"],
diff --git a/pcweb/pages/blog/page.py b/pcweb/pages/blog/page.py
index dfe7b228b..404ad909a 100644
--- a/pcweb/pages/blog/page.py
+++ b/pcweb/pages/blog/page.py
@@ -1,12 +1,136 @@
import reflex as rx
+import reflex_ui as ui
+from reflex.experimental.client_state import ClientStateVar
-from pcweb.components.icons import get_icon
-from pcweb.components.webpage.comps import h1_title
+from pcweb.components.hosting_banner import HostingBannerState
+from pcweb.components.icons.icons import get_icon
+from pcweb.components.marketing_button import button
+from pcweb.constants import REFLEX_URL
from pcweb.flexdown import xd2 as xd
+from pcweb.templates.docpage import get_toc, right_sidebar_item_highlight
from .paths import blog_data
+def share_post_button(icon: str, href: str, aria_label: str) -> rx.Component:
+ return rx.el.a(
+ button(
+ get_icon(icon),
+ native_button=False,
+ variant="outline",
+ size="icon-sm",
+ aria_label=aria_label,
+ ),
+ target="_blank",
+ to=href,
+ )
+
+
+@rx.memo
+def copy_link_button(url: str):
+ copied = ClientStateVar.create("is_copied", default=False, global_ref=False)
+ return button(
+ rx.cond(
+ copied.value,
+ ui.icon(
+ "Tick02Icon",
+ ),
+ get_icon("link_blog"),
+ ),
+ variant="outline",
+ aria_label="Copy link to post",
+ size="icon-sm",
+ on_click=[
+ rx.call_function(copied.set_value(True)),
+ rx.set_clipboard(url),
+ ],
+ on_mouse_down=rx.call_function(copied.set_value(False)).debounce(1500),
+ )
+
+
+def table_of_contents(toc: list, path: str, page_url: str) -> rx.Component:
+ """Render the table of contents sidebar."""
+ if len(toc) < 2:
+ return rx.fragment()
+
+ return rx.el.nav(
+ rx.box(
+ rx.el.p(
+ "On This Page",
+ class_name="text-sm h-8 flex items-center justify-start font-[525] dark:text-m-slate-3 text-m-slate-12",
+ ),
+ rx.el.ul(
+ *[
+ (
+ rx.el.li(
+ rx.el.a(
+ text,
+ class_name="text-sm font-[525] text-m-slate-7 dark:text-m-slate-6 pl-4 py-1 block hover:text-m-slate-9 dark:hover:text-m-slate-5 transition-colors truncate",
+ href=path + "#" + text.lower().replace(" ", "-"),
+ ),
+ )
+ if level == 1
+ else (
+ rx.el.li(
+ rx.el.a(
+ text,
+ class_name="text-sm font-[525] text-m-slate-7 dark:text-m-slate-6 pl-4 py-1 block hover:text-m-slate-9 dark:hover:text-m-slate-5 transition-colors truncate",
+ href=path + "#" + text.lower().replace(" ", "-"),
+ ),
+ )
+ if level == 2
+ else rx.el.li(
+ rx.el.a(
+ text,
+ class_name="text-sm font-[525] text-m-slate-7 dark:text-m-slate-6 pl-8 py-1 block hover:text-m-slate-9 dark:hover:text-m-slate-5 transition-colors truncate",
+ href=path + "#" + text.lower().replace(" ", "-"),
+ ),
+ )
+ )
+ )
+ for level, text in toc
+ ],
+ id="toc-navigation",
+ class_name="flex flex-col gap-y-1 list-none shadow-[1.5px_0_0_0_var(--m-slate-4)_inset] dark:shadow-[1.5px_0_0_0_var(--m-slate-9)_inset]",
+ ),
+ rx.el.div(
+ rx.el.span(
+ "Share Post",
+ class_name="text-m-slate-12 dark:text-m-slate-3 font-[525] text-sm",
+ ),
+ rx.el.div(
+ share_post_button(
+ "twitter_blog",
+ f"https://twitter.com/intent/tweet?text={page_url}",
+ "Share on Twitter",
+ ),
+ share_post_button(
+ "linkedin_blog",
+ f"https://www.linkedin.com/feed/?shareActive=true&text={page_url}",
+ "Share on LinkedIn",
+ ),
+ share_post_button(
+ "reddit_blog",
+ f"https://www.reddit.com/submit?url={page_url}",
+ "Share on Reddit",
+ ),
+ copy_link_button(url=page_url),
+ class_name="flex flex-row gap-2",
+ ),
+ class_name="flex flex-col gap-5 mt-6",
+ ),
+ class_name="flex flex-col justify-start gap-y-4 overflow-y-auto",
+ ),
+ on_mount=rx.call_script(right_sidebar_item_highlight()),
+ class_name=ui.cn(
+ "sticky w-[14rem] shrink-0 hidden xl:block self-start max-lg:hidden",
+ rx.cond(
+ HostingBannerState.is_banner_visible, "top-[8.5rem]", "top-[6.5rem]"
+ ),
+ ),
+ )
+
+
def more_posts(current_post: dict) -> rx.Component:
from .blog import card_content
@@ -56,51 +180,89 @@ def more_posts(current_post: dict) -> rx.Component:
def page(document, route) -> rx.Component:
"""Create a page."""
meta = document.metadata
+ toc, _ = get_toc(document, route)
+ page_url = f"{REFLEX_URL.strip('/')}{route}"
return rx.el.section(
rx.el.article(
- rx.link(
- rx.box(
- get_icon("arrow_right", class_name="rotate-180"),
- "Back to Blog",
- class_name="box-border flex justify-center items-center gap-2 border-slate-5 bg-slate-1 hover:bg-slate-3 -mb-4 px-3 py-0.5 border rounded-full font-small text-secondary-11 transition-bg cursor-pointer",
- ),
- underline="none",
- href="/blog",
- ),
- rx.el.header(
- h1_title(title=meta["title"]),
- rx.el.h2(
- str(meta["description"]),
- class_name="font-md text-balance text-slate-10",
- ),
- rx.box(
- rx.text(
- meta["author"],
- ),
- rx.text(
- "·",
+ rx.el.div(
+ rx.el.div(
+ rx.el.a(
+ "Blog",
+ href="/blog",
+ class_name="text-m-slate-12 dark:text-m-slate-3 font-[575] hover:text-primary-10 dark:hover:text-primary-9 text-xs",
),
+ rx.el.div(class_name="w-4 h-px bg-m-slate-5 dark:bg-m-slate-10"),
rx.moment(
str(meta["date"]),
format="MMM DD, YYYY",
+ class_name="font-[475] text-m-slate-7 dark:text-m-slate-6 text-xs",
),
- class_name="flex items-center gap-2 !font-normal font-small text-nowrap text-secondary-11",
+ class_name="flex flex-row items-center gap-3 mb-6",
+ ),
+ rx.image(
+ src=f"/common/{rx.color_mode_cond('light', 'dark')}/squares_vertical_blog.svg",
+ alt="Squares Vertical Docs",
+ loading="lazy",
+ class_name="pointer-events-none w-auto h-[calc(100%-2rem)] absolute inset-y-4 left-2 max-lg:hidden",
+ ),
+ rx.image(
+ src=f"/common/{rx.color_mode_cond('light', 'dark')}/squares_vertical_blog.svg",
+ alt="Squares Vertical Docs",
+ loading="lazy",
+ class_name="pointer-events-none w-auto h-[calc(100%-2rem)] absolute inset-y-4 right-2 scale-x-[-1] max-lg:hidden",
),
- class_name="section-header",
+ rx.el.header(
+ rx.el.h1(
+ meta["title"],
+ class_name="lg:text-5xl text-3xl text-m-slate-12 dark:text-m-slate-3 font-[575] mb-6 text-center text-balance",
+ ),
+ rx.el.h2(
+ str(meta["description"]),
+ class_name="lg:text-base text-sm text-m-slate-7 dark:text-m-slate-6 font-[475] mb-8 text-center",
+ ),
+ rx.el.span(
+ meta["author"],
+ class_name="text-m-slate-12 dark:text-m-slate-3 text-sm font-[525]",
+ ),
+ class_name="flex flex-col justify-center items-center max-w-[45rem] mx-auto w-full",
+ ),
+ class_name="flex flex-col justify-center items-center max-w-[69rem] lg:border-x border-m-slate-4 dark:border-m-slate-9 lg:py-16 pb-8 w-full mx-auto relative",
),
- rx.image(
- src=f"{meta['image']}",
- alt=f"Image for blog post: {meta['title']}",
- loading="eager",
- custom_attrs={"fetchPriority": "high"},
- class_name="rounded-[1.125rem] w-auto object-contain max-w-full max-h-[40rem]",
+ rx.el.hr(
+ class_name="h-[1px] w-full bg-m-slate-4 dark:bg-m-slate-10",
),
- rx.box(
- xd.render(document, document.filename),
- class_name="flex flex-col gap-4 w-full max-w-2xl",
+ rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ rx.image(
+ src=f"{meta['image']}",
+ alt=f"Image for blog post: {meta['title']}",
+ loading="eager",
+ custom_attrs={"fetchPriority": "high"},
+ class_name="rounded-xl object-contain w-full h-auto mb-4",
+ ),
+ rx.el.div(
+ xd.render(document, document.filename),
+ class_name="flex flex-col gap-4 w-full max-w-2xl",
+ ),
+ class_name="flex flex-col gap-12 flex-1",
+ ),
+ table_of_contents(toc, route, page_url),
+ class_name="flex flex-row gap-24 max-w-[69rem] mx-auto w-full lg:py-24 py-12",
+ ),
+ rx.el.div(
+ more_posts(meta),
+ class_name="max-w-[69rem] mx-auto w-full",
+ ),
+ class_name="bg-gradient-to-b from-white-1 to-m-slate-1 dark:from-m-slate-11 dark:to-m-slate-12 w-full flex flex-col gap-12",
+ ),
+ ),
+ class_name=ui.cn(
+ "flex flex-col mx-auto max-lg:px-6 w-full relative",
+ rx.cond(
+ HostingBannerState.is_banner_visible,
+ "lg:pt-[7rem] pt-[11rem]",
+ "lg:pt-[4rem] pt-[8rem]",
),
- more_posts(meta),
- class_name="flex flex-col justify-center items-center gap-12 max-w-full",
),
- class_name="section-content",
)
diff --git a/pcweb/templates/docpage/docpage.py b/pcweb/templates/docpage/docpage.py
index 0fd567c3c..9d6ba0c6c 100644
--- a/pcweb/templates/docpage/docpage.py
+++ b/pcweb/templates/docpage/docpage.py
@@ -729,41 +729,36 @@ def wrapper(*args, **kwargs) -> rx.Component:
rx.el.li(
rx.el.a(
text,
- class_name="text-sm font-[525] text-m-slate-7 dark:text-m-slate-6 break-words pl-4 min-h-7 flex items-center",
+ class_name="text-sm font-[525] text-m-slate-7 dark:text-m-slate-6 pl-4 py-1 block hover:text-m-slate-9 dark:hover:text-m-slate-5 transition-colors truncate",
href=path
+ "#"
+ text.lower().replace(" ", "-"),
),
- class_name="min-h-8 flex items-center",
)
if level == 1
else (
rx.el.li(
rx.el.a(
text,
- class_name="text-sm font-[525] text-m-slate-7 dark:text-m-slate-6 break-words min-h-7 pl-4 flex items-center",
- underline="none",
+ class_name="text-sm font-[525] text-m-slate-7 dark:text-m-slate-6 pl-4 py-1 block hover:text-m-slate-9 dark:hover:text-m-slate-5 transition-colors truncate",
href=path
+ "#"
+ text.lower().replace(
" ", "-"
),
),
- class_name="min-h-8 flex items-center",
)
if level == 2
else rx.el.li(
rx.el.a(
text,
- underline="none",
- class_name="text-sm font-[525] text-m-slate-7 dark:text-m-slate-6 break-words min-h-7 pl-8 flex items-center",
+ class_name="text-sm font-[525] text-m-slate-7 dark:text-m-slate-6 pl-8 py-1 block hover:text-m-slate-9 dark:hover:text-m-slate-5 transition-colors truncate",
href=path
+ "#"
+ text.lower().replace(
" ", "-"
),
),
- class_name="min-h-8 flex items-center",
)
)
)
diff --git a/pcweb/templates/marketing_page.py b/pcweb/templates/marketing_page.py
new file mode 100644
index 000000000..3df41a651
--- /dev/null
+++ b/pcweb/templates/marketing_page.py
@@ -0,0 +1,90 @@
+import functools
+from typing import Callable
+
+import reflex as rx
+
+from pcweb.route import Route
+
+DEFAULT_TITLE = "The platform to build and scale enterprise apps"
+DEFAULT_DESCRIPTION = "Connect to all your company data and systems to build secure internal apps with AI. Deployed on prem with built-in governance and production-grade reliability, so technical and nontechnical teams can ship together."
+
+
+def marketing_page(
+ path: str,
+ title: str = DEFAULT_TITLE,
+ description: str | None = DEFAULT_DESCRIPTION,
+ image: str | None = None,
+ meta: list[dict[str, str]] | None = None,
+ props=None,
+ add_as_page=True,
+) -> Callable:
+ """A template that most pages on the reflex.dev site should use.
+
+ This template wraps the webpage with the navbar and footer.
+
+ Args:
+ path: The path of the page.
+ title: The title of the page.
+ description: The description of the page.
+ image: The image to use for social media.
+ meta: Additional meta tags to add to the page.
+ props: Props to apply to the template.
+ add_as_page: whether to add the route to the app pages.
+
+ Returns:
+ A wrapper function that returns the full webpage.
+ """
+ props = props or {}
+
+ def marketing_page(contents: Callable[[], Route]) -> Route:
+ """Wrapper to create a templated route.
+
+ Args:
+ contents: The function to create the page route.
+
+ Returns:
+ The templated route.
+ """
+
+ @functools.wraps(contents)
+ def wrapper(*children, **props) -> rx.Component:
+ """The template component.
+
+ Args:
+ children: The children components.
+ props: The props to apply to the component.
+
+ Returns:
+ The component with the template applied.
+ """
+ # Import here to avoid circular imports.
+ from pcweb.views.bottom_section.bottom_section import bottom_section
+ from pcweb.views.footer import footer
+ from pcweb.views.marketing_navbar import marketing_navbar
+
+ # Wrap the component in the template.
+ return rx.el.div(
+ marketing_navbar(),
+ rx.el.main(
+ rx.el.div(
+ contents(*children, **props),
+ bottom_section(),
+ footer(),
+ class_name="flex flex-col relative justify-center items-center w-full",
+ ),
+ class_name="flex flex-col w-full relative h-full justify-center items-center",
+ ),
+ class_name="flex flex-col w-full justify-center items-center relative dark:bg-m-slate-12 bg-m-slate-1",
+ )
+
+ return Route(
+ path=path,
+ title=title,
+ description=description,
+ image=image,
+ meta=meta,
+ component=wrapper,
+ add_as_page=add_as_page,
+ )
+
+ return marketing_page
diff --git a/pcweb/views/bottom_section/bottom_section.py b/pcweb/views/bottom_section/bottom_section.py
index 27f98ecdb..175afcf40 100644
--- a/pcweb/views/bottom_section/bottom_section.py
+++ b/pcweb/views/bottom_section/bottom_section.py
@@ -9,5 +9,5 @@ def bottom_section() -> rx.Component:
return rx.box(
newsletter(),
get_started(),
- class_name="flex flex-col items-center gap-20 lg:gap-32 pt-8 lg:pt-[6.5rem] w-[22rem] lg:w-[25rem]",
+ class_name="flex flex-col items-center gap-20 lg:gap-32 pt-8 lg:pt-[6.5rem] max-lg:max-w-[22rem] w-full lg:w-[25rem] overflow-hidden",
)