new_starr_app — Frontend
React + Vite CSR frontend for the STARR LMS platform.
Page Metadata
Metadata (<title>, Open Graph, Twitter Card) is managed with react-helmet-async.
The system has two layers:
- Global fallback — a
<Helmet>inApp.jsxalways renders default tags. Any page that does not supply its own metadata falls back to this. - Per-page override — each page renders a
<PageMeta />component anywhere in its JSX tree. react-helmet-async lets the deepest/last<Helmet>win, so the page always overrides the fallback.
This approach fixes the useNavigate() stale-title bug: when you navigate away from a page, its <PageMeta> unmounts and the global fallback immediately takes over.
The component
// src/contexts/MetadataContext.jsx
import { PageMeta } from '@/contexts/MetadataContext'
Props
| Prop | Type | Default |
|---|---|---|
title |
string | undefined |
'STARR | Philproperties' |
description |
string | undefined |
app default description |
keywords |
string | undefined |
app default keywords |
ogImage |
string | undefined |
app default OG image URL |
ogType |
string |
'website' |
Every prop is optional. Any omitted prop falls back to the app-level default.
Passing title={undefined} explicitly is the same as omitting it — the fallback title renders.
Static pages
Pages with no data dependency just pass a hardcoded string.
import { PageMeta } from '@/contexts/MetadataContext'
export default function CourseList() {
return (
<section className="bg-muted/60 h-full">
<PageMeta
title="Courses - STARR"
description="Browse and manage your training courses."
/>
{/* rest of page */}
</section>
)
}
Dynamic pages — data from context
When the page title comes from an API response, pass the derived string.
Use undefined (not a hardcoded fallback string) while the data is loading — this lets the global default render during the loading state and then automatically updates when data arrives.
import { PageMeta } from '@/contexts/MetadataContext'
import { useCourses } from '@/contexts/AdminCoursesContext'
export default function ViewCourse() {
const { course } = useCourses()
return (
<section className="bg-muted/60 min-h-full">
{/* undefined while course is null → global default renders */}
<PageMeta
title={course ? `${course.title} - STARR` : undefined}
description={course?.description}
/>
{/* rest of page */}
</section>
)
}
Dynamic pages — data in local state
Same pattern when the title comes from local useState instead of context.
import { PageMeta } from '@/contexts/MetadataContext'
export default function EditUnit() {
const [unitTitle, setUnitTitle] = useState('')
// ... fetch and setUnitTitle on load
return (
<section className="bg-muted/60 h-full">
<PageMeta title={unitTitle ? `Edit: ${unitTitle} - STARR` : undefined} />
{/* rest of page */}
</section>
)
}
Placement rules
- Drop
<PageMeta>as the first child of the root element in the page's mainreturn. - Do not add it to early/loading returns (spinner-only returns). The global fallback covers those.
- Do not wrap the entire page in
<PageMeta>— it is an inline element, not a wrapper.
// CORRECT — inline inside root element
return (
<section>
<PageMeta title="..." />
<div>...</div>
</section>
)
// WRONG — do not use as a wrapper
return (
<PageMeta title="...">
<section>...</section>
</PageMeta>
)
Title convention
| Page type | Pattern | Example |
|---|---|---|
| List | {Entity} - STARR |
Courses - STARR |
| Archived list | Archived {Entity} - STARR |
Archived Courses - STARR |
| Create | Add {Entity} - STARR |
Add Course - STARR |
| View (dynamic) | {name} - STARR |
Intro to Sales - STARR |
| Edit (dynamic) | Edit: {name} - STARR |
Edit: Intro to Sales - STARR |
| Sub-list | {SubEntity} – {parentName} - STARR |
Units – Intro to Sales - STARR |
| Nested action | {Action} – {parentName} - STARR |
Page Builder – Lesson 1 - STARR |
Adding metadata to a new page — checklist
- Import
PageMetafrom@/contexts/MetadataContext. - Identify what data (if any) drives the title — context state, local state, or nothing.
- Place
<PageMeta title="..." />as the first child of the root element in the mainreturn. - Use the title convention above.
- Pass
descriptionif the page has meaningful content to describe (e.g. a course description). - Pass
title={undefined}(or omittitle) while data is still loading.
MetadataProvider (legacy)
Login.jsx and Register.jsx still use the old wrapper pattern via MetadataProvider. This is a backward-compat shim that internally renders <PageMeta {...value} /> and then its children. It works but should not be used for new pages — use the inline <PageMeta /> pattern instead.
// Old pattern (Login / Register only — do not copy for new pages)
<MetadataProvider value={{ title: 'Login - Philproperties', description: '...' }}>
<LoginContent />
</MetadataProvider>
// New pattern (use this for all new pages)
<section>
<PageMeta title="Login - Philproperties" description="..." />
<LoginContent />
</section>