feat(theme): add top placement for sections

- Add `placement: top|bottom` to the section frontmatter
- Override `DocContent` to render hoisted bands under the page title
- Derive the zebra stripe from list position so split bands stay continuous
- Opt marketing bands out of doc typography with `rp-not-doc`
- Point the legal and privacy contact address at legal@onixbyte.com
- Condense the products pages' body copy into their section frontmatter
This commit is contained in:
zihluwang committed 2026-09-25 18:04:49 +08:00
1 parent d21b8f6b2f
commit 9d095eba1b
15 files changed
+145 -54

No files matched your search

+1 -1
View File
@@ -75,7 +75,7 @@ those rights are not affected by anything in this policy.
Questions about this policy, or about how we handle data: Questions about this policy, or about how we handle data:
**business@onixbyte.com** **legal@onixbyte.com**
Changsha Onix Byte Technology Co., Ltd Changsha Onix Byte Technology Co., Ltd
Hunan Province, China Hunan Province, China
+1 -1
View File
@@ -81,7 +81,7 @@ that means you accept the revised terms.
## Contact ## Contact
**business@onixbyte.com** **legal@onixbyte.com**
Changsha Onix Byte Technology Co., Ltd Changsha Onix Byte Technology Co., Ltd
Hunan Province, China Hunan Province, China
-19
View File
@@ -40,22 +40,3 @@ sections:
Alongside client work we build tools of our own. We keep the number small: each Alongside client work we build tools of our own. We keep the number small: each
one exists because we needed it ourselves and suspected others did too. one exists because we needed it ourselves and suspected others did too.
## Free to use, nothing to install
Every tool here runs entirely in your browser. There is no account to create and
no upload step — whatever you paste in stays on your machine.
That is a deliberate choice rather than a limitation. We use these tools on real
configuration files and real API responses, and a tool that phones home is a
tool we would not be able to use.
## Dev Lab
A JSON viewer built for data that is too large and too nested to read by hand.
Paste a response, query it with JSONPath, and the matching fields are
highlighted in place. Matches can be copied out in one click.
It also carries a few smaller utilities we kept reaching for.
**[Open Dev Lab →](https://dev-lab.onixbyte.com)**
+1 -1
View File
@@ -56,7 +56,7 @@ localStorage 键),但未经律师审阅。正式依赖前请交由法务检
关于本政策,或关于我们如何处理数据: 关于本政策,或关于我们如何处理数据:
**business@onixbyte.com** **legal@onixbyte.com**
长沙曜珀科技有限公司 长沙曜珀科技有限公司
中国湖南省 中国湖南省
+1 -1
View File
@@ -57,7 +57,7 @@ OnixByte 名称、标识及本网站的设计归我们所有。我们发布的
## 联系方式 ## 联系方式
**business@onixbyte.com** **legal@onixbyte.com**
长沙曜珀科技有限公司 长沙曜珀科技有限公司
中国湖南省 中国湖南省
-14
View File
@@ -37,17 +37,3 @@ sections:
--- ---
除了客户项目,我们也做自己的工具。数量刻意保持得很少:每一件都是因为我们自己需要,并且认为别人可能也需要。 除了客户项目,我们也做自己的工具。数量刻意保持得很少:每一件都是因为我们自己需要,并且认为别人可能也需要。
## 免费使用,无需安装
这里的每一个工具都完全在您的浏览器中运行。不需要注册账号,也没有任何上传步骤 —— 您粘贴进去的内容始终留在您自己的机器上。
这是刻意为之,不是能力所限。我们自己在真实的配置文件和真实的接口返回上使用这些工具,而一个会把数据传出去的工具,我们自己就不会用。
## Dev Lab
为「大到没法用肉眼看」的嵌套 JSON 而做的查看器。粘贴一段返回数据,用 JSONPath 查询,命中的字段会就地高亮,结果可一键复制。
除此之外,它还带了一些我们日常反复需要的小工具。
**[打开 Dev Lab →](https://dev-lab.onixbyte.com)**
+1
View File
@@ -24,6 +24,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.4", "@eslint/js": "^9.39.4",
"@mdx-js/react": "3.1.1",
"@rspress/plugin-sitemap": "2.0.11", "@rspress/plugin-sitemap": "2.0.11",
"@tailwindcss/postcss": "^4.3.0", "@tailwindcss/postcss": "^4.3.0",
"@types/node": "^22.8.1", "@types/node": "^22.8.1",
+3
View File
@@ -173,6 +173,9 @@ importers:
'@eslint/js': '@eslint/js':
specifier: ^9.39.4 specifier: ^9.39.4
version: 9.39.4 version: 9.39.4
'@mdx-js/react':
specifier: 3.1.1
version: 3.1.1(@types/react@19.2.14)(react@19.2.6)
'@rspress/plugin-sitemap': '@rspress/plugin-sitemap':
specifier: 2.0.11 specifier: 2.0.11
version: 2.0.11(@rspress/core@2.0.11(@rspack/core@2.0.3(@swc/helpers@0.5.21))(@types/mdast@4.0.4)(@types/react@19.2.14)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0)) version: 2.0.11(@rspress/core@2.0.11(@rspack/core@2.0.3(@swc/helpers@0.5.21))(@types/mdast@4.0.4)(@types/react@19.2.14)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))
+69
View File
@@ -0,0 +1,69 @@
import type { ComponentProps } from "react"
import { MDXProvider } from "@mdx-js/react"
import { Content, usePage, useSite } from "@rspress/core/runtime"
import {
Callout,
DocContent as OriginalDocContent,
FallbackHeading,
getCustomMDXComponent,
useScrollAfterNav,
} from "@rspress/core/theme-original"
import { HomeSections } from "./HomeSections"
/** Taken from upstream so the two cannot drift apart on an Rspress upgrade. */
type DocContentProps = ComponentProps<typeof OriginalDocContent>
/**
* The page title, rendered when the page does not supply an `# h1` of its own.
* Mirrors the private `FallbackTitle` inside `@rspress/core`'s `DocContent`.
*/
function FallbackTitle() {
const { site } = useSite()
const { page } = usePage()
const { headingTitle, title } = page
if (site.themeConfig.fallbackHeadingTitle === false || headingTitle) return null
return <FallbackHeading level={1} title={title} />
}
/**
* Mirrors `@rspress/core`'s `DocContent`, with one addition: the bands a page
* marks `placement: top` render between the page title and the article body.
*
* Rspress has no slot for that position. `beforeDocContent` sits above the
* title, and `afterDocContent` and the `Layout`'s `afterDoc` both sit below the
* whole article — which is why sections used to land at the foot of the
* products page. Replacing this component is the only way in, so it is a
* deliberate copy of upstream and is worth re-diffing whenever Rspress is
* upgraded. Everything below comes straight from upstream: `useScrollAfterNav`
* restores the scroll position after a client-side navigation, and the MDX
* component map is layered as defaults, then the caller's overrides, then the
* placeholder the compiled MDX uses for callouts.
*
* Every page rendering through `DocLayout` goes through here, so be careful:
* a mistake affects blogs and project pages as much as the marketing ones.
*/
export function DocContent({
components,
isOverviewPage = false,
beforeDocContent,
afterDocContent,
}: DocContentProps) {
useScrollAfterNav()
const mdxComponents = {
...getCustomMDXComponent(),
...components,
$$$callout$$$: Callout,
}
return (
<MDXProvider components={mdxComponents}>
{beforeDocContent}
{!isOverviewPage && <FallbackTitle />}
<HomeSections placement="top" />
<Content />
{afterDocContent}
</MDXProvider>
)
}
+28 -7
View File
@@ -4,7 +4,7 @@ import { FeatureGrid } from "./sections/FeatureGrid"
import { TechStack } from "./sections/TechStack" import { TechStack } from "./sections/TechStack"
import { Cases } from "./sections/Cases" import { Cases } from "./sections/Cases"
import { CallToAction } from "./sections/CallToAction" import { CallToAction } from "./sections/CallToAction"
import { hasContent, type Section } from "./sections/types" import { hasContent, isHoisted, type Section } from "./sections/types"
/** Type guard for a single entry of `frontmatter.sections`. */ /** Type guard for a single entry of `frontmatter.sections`. */
function isSection(value: unknown): value is Section { function isSection(value: unknown): value is Section {
@@ -33,27 +33,48 @@ function renderBody(section: Section) {
} }
} }
interface HomeSectionsProps {
/**
* Which bands this instance renders. Omit it to render all of them in
* declaration order, which is what the home page wants — it has no article
* for a band to sit on either side of.
*/
placement?: "top" | "bottom"
}
/** /**
* Renders `frontmatter.sections` in declaration order. * Renders `frontmatter.sections` in declaration order.
* *
* Attached to the `Layout` `afterHero` (home) and `afterDoc` (products, * Attached to the `Layout` `afterFeatures` (home) and `afterDoc` (products,
* services) slots. Returns `null` for any page without a `sections` array, so * services) slots, and to the `DocContent` override for hoisted bands. Returns
* it is inert on blog posts, project pages and every other doc page. * `null` for any page without a `sections` array, so it is inert on blog posts,
* project pages and every other doc page.
*/ */
export function HomeSections() { export function HomeSections({ placement }: HomeSectionsProps = {}) {
const { frontmatter } = useFrontmatter() const { frontmatter } = useFrontmatter()
const raw = frontmatter?.sections const raw = frontmatter?.sections
if (!Array.isArray(raw)) return null if (!Array.isArray(raw)) return null
const sections = raw.filter(isSection) const sections = raw.filter(isSection)
if (sections.length === 0) return null // The stripe alternates over the page's whole `sections` list rather than
// over whatever this instance happens to render: hoisting a band must not
// flip the background of every band below it.
const visible = sections
.map((section, index) => ({ section, index }))
.filter(({ section }) => {
if (placement === undefined) return true
return placement === "top" ? isHoisted(section) : !isHoisted(section)
})
if (visible.length === 0) return null
return ( return (
<div className="rp-site-sections"> <div className="rp-site-sections">
{sections.map((section, index) => ( {visible.map(({ section, index }) => (
<SectionBand <SectionBand
key={section.id ?? `${section.type}-${index}`} key={section.id ?? `${section.type}-${index}`}
section={section} section={section}
tinted={index % 2 === 1}
hasContent={hasContent(section)}> hasContent={hasContent(section)}>
{renderBody(section)} {renderBody(section)}
</SectionBand> </SectionBand>
+11 -3
View File
@@ -6,6 +6,8 @@ interface SectionBandProps {
section: Section section: Section
/** Whether the section body produced any content. */ /** Whether the section body produced any content. */
hasContent: boolean hasContent: boolean
/** Whether this band takes the soft background in the page's zebra stripe. */
tinted: boolean
children: ReactNode children: ReactNode
} }
@@ -13,15 +15,21 @@ interface SectionBandProps {
* Shell for a marketing band: the `<section>` wrapper, its heading and its * Shell for a marketing band: the `<section>` wrapper, its heading and its
* subtitle. Renders nothing at all when the section has neither content nor a * subtitle. Renders nothing at all when the section has neither content nor a
* heading, so an unfilled page degrades to just its hero. * heading, so an unfilled page degrades to just its hero.
*
* `rp-not-doc` opts the band out of the doc typography, which applies to
* everything inside `.rp-doc` and would otherwise restyle our headings and
* paragraphs. It is inert for bands rendered outside the article, as on the
* home page.
*/ */
export function SectionBand({ section, hasContent, children }: SectionBandProps) { export function SectionBand({ section, hasContent, tinted, children }: SectionBandProps) {
const title = str(section.title) const title = str(section.title)
const subtitle = str(section.subtitle) const subtitle = str(section.subtitle)
const className = `rp-site-section rp-not-doc${tinted ? " rp-site-section--tinted" : ""}`
if (!hasContent && !title && !subtitle) { if (!hasContent && !title && !subtitle) {
if (!SHOW_COPY_PLACEHOLDERS) return null if (!SHOW_COPY_PLACEHOLDERS) return null
return ( return (
<section id={section.id} className="rp-site-section"> <section id={section.id} className={className}>
<div className="mx-auto max-w-5xl px-6"> <div className="mx-auto max-w-5xl px-6">
<div className="rounded-lg border border-dashed border-(--rp-c-divider) p-8 text-center text-sm text-(--rp-c-text-3)"> <div className="rounded-lg border border-dashed border-(--rp-c-divider) p-8 text-center text-sm text-(--rp-c-text-3)">
TODO: {section.type} TODO: {section.type}
@@ -32,7 +40,7 @@ export function SectionBand({ section, hasContent, children }: SectionBandProps)
} }
return ( return (
<section id={section.id} className="rp-site-section"> <section id={section.id} className={className}>
<div className="mx-auto max-w-5xl px-6"> <div className="mx-auto max-w-5xl px-6">
{(title || subtitle) && ( {(title || subtitle) && (
<header className="mb-10 text-center"> <header className="mb-10 text-center">
-3
View File
@@ -3,9 +3,6 @@ import { arr, str, type TechStackGroup, type TechStackItem } from "./types"
/** /**
* Technology matrix: named groups of technologies, each with an optional * Technology matrix: named groups of technologies, each with an optional
* emoji/icon and link. * emoji/icon and link.
*
* TODO(copy): populate `frontmatter.sections` with real entries. Groups with no
* items are skipped, so a half-filled list still renders sensibly.
*/ */
export function TechStack({ groups }: { groups?: TechStackGroup[] }) { export function TechStack({ groups }: { groups?: TechStackGroup[] }) {
const visible = (arr<TechStackGroup>(groups) ?? []) const visible = (arr<TechStackGroup>(groups) ?? [])
+15
View File
@@ -50,6 +50,16 @@ interface SectionBase {
id?: string id?: string
title?: string title?: string
subtitle?: string subtitle?: string
/**
* Where the band sits relative to the article body. `"top"` hoists it above
* the body, directly under the page title; `"bottom"` — the default — leaves
* it below the article.
*
* Only doc pages can honour this, because only they have an article to sit
* between. The home page renders every band below the features grid, in
* declaration order, whatever this says.
*/
placement?: "top" | "bottom"
} }
export type Section = export type Section =
@@ -100,3 +110,8 @@ export function hasContent(section: Section): boolean {
) )
} }
} }
/** Whether a section asked to be hoisted above the article body. */
export function isHoisted(section: Section): boolean {
return section.placement === "top"
}
+7 -2
View File
@@ -19,11 +19,16 @@
padding: 0; padding: 0;
} }
/* Full-bleed marketing sections, driven by `frontmatter.sections`. */ /* Marketing sections, driven by `frontmatter.sections`. A band is full-bleed
when it renders below the article and article-width when `placement: top`
puts it inside the doc column. */
.rp-site-section { .rp-site-section {
padding-block: var(--rp-site-section-gap); padding-block: var(--rp-site-section-gap);
} }
.rp-site-section:nth-child(even) { /* Zebra striping. Chosen by `HomeSections` from the band's position in the
page's whole `sections` list rather than by `:nth-child`, so that bands
split across the top and bottom slots keep one continuous stripe. */
.rp-site-section--tinted {
background: var(--rp-c-bg-soft); background: var(--rp-c-bg-soft);
} }
+7 -2
View File
@@ -8,6 +8,7 @@ import { Tags } from "./components/Tags"
import { Author } from "./components/Author" import { Author } from "./components/Author"
import { SiteFooter } from "./components/SiteFooter" import { SiteFooter } from "./components/SiteFooter"
import { HomeSections } from "./components/HomeSections" import { HomeSections } from "./components/HomeSections"
import { DocContent } from "./components/DocContent"
function DocLayout(props: DocLayoutProps) { function DocLayout(props: DocLayoutProps) {
return ( return (
@@ -44,6 +45,10 @@ function DocLayout(props: DocLayoutProps) {
* than part of the prose column, so sections placed there stay full-bleed. * than part of the prose column, so sections placed there stay full-bleed.
* `HomeSections` renders nothing without the frontmatter, and the two slots are * `HomeSections` renders nothing without the frontmatter, and the two slots are
* never both live on the same page. * never both live on the same page.
*
* A band marked `placement: top` is not rendered here at all — `DocContent`
* picks it up and slots it between the page title and the article body, and
* `afterDoc` renders the remaining bands. See `components/DocContent.tsx`.
*/ */
function Layout(props: LayoutProps) { function Layout(props: LayoutProps) {
return ( return (
@@ -64,7 +69,7 @@ function Layout(props: LayoutProps) {
afterDoc={ afterDoc={
<> <>
{props.afterDoc} {props.afterDoc}
<HomeSections /> <HomeSections placement="bottom" />
</> </>
} }
/> />
@@ -72,5 +77,5 @@ function Layout(props: LayoutProps) {
} }
export * from "@rspress/core/theme-original" export * from "@rspress/core/theme-original"
export { DocLayout, Layout } export { DocLayout, Layout, DocContent }
export { Kbd } from "./components/Kbd" export { Kbd } from "./components/Kbd"