en un clic
add-page
// Add a new server-rendered page to the Liftosaur website. Use when creating new public-facing pages with routing, SSR, and client hydration.
// Add a new server-rendered page to the Liftosaur website. Use when creating new public-facing pages with routing, SSR, and client hydration.
Fix a production exception from Rollbar in an interactive Claude Code session. Fetches the occurrence data, analyzes the error, and either implements a fix or adds the error to the ignore list. Use when given a Rollbar occurrence ID.
Guidelines for writing React Native code in Liftosaur. Use when migrating web components to RN primitives, fixing RN performance issues, or building new RN screens.
Write, debug, and integrate Lezer grammars for the Liftosaur project. Use when creating or modifying .grammar files, evaluators, syntax highlighting, or CodeMirror integration.
Write idiomatic Liftoscript code for weightlifting programs. Use when creating or editing program files in programs/builtin/.
Writing style guide for prose content - program descriptions, technique pages, blog posts, and any user-facing text. Use when writing or editing descriptive content.
Capture important architectural decisions, bug patterns, feature designs, or other critical findings into the project knowledge base. Use proactively when significant technical decisions are made, non-obvious bugs are resolved, new subsystems are designed, or important product features are discussed.
| name | add-page |
| description | Add a new server-rendered page to the Liftosaur website. Use when creating new public-facing pages with routing, SSR, and client hydration. |
Create a new server-rendered page at the specified route. Requirements: $ARGUMENTS
Before creating files, read these existing examples to understand the patterns:
src/pages/allExercises/allExercisesHtml.tsx — simple HTML wrapper examplesrc/pages/allExercises/allExercisesContent.tsx — simple content componentsrc/allExercises.tsx — client entry hydrationlambda/allExercises.tsx — lambda render functionlambda/index.ts for the route registration pattern (search for getAllExercisesEndpoint)webpack.config.js for the entry point patternEvery public page uses 4 layers. For a page at /myroute with entry name mypage:
lambda/myPage.tsx)import { h } from "preact";
import { IAccount } from "../src/models/account";
import { MyPageHtml } from "../src/pages/myPage/myPageHtml";
import { renderPage } from "./render";
export function renderMyPageHtml(client: Window["fetch"], account?: IAccount): string {
return renderPage(<MyPageHtml client={client} account={account} />);
}
src/pages/myPage/myPageHtml.tsx)Wraps content in <Page>. Key props:
css={["mypage"]} / js={["mypage"]} — must match webpack entry keymaxWidth={1200} — page max widthtitle, canonical, description, ogUrl — SEO metadataaccount — optional user account for nav stateurl="/myroute" — highlights nav item in TopNavMenudata={data} — serialized for hydration (exclude client from data!)import { h, JSX } from "preact";
import { Page } from "../../components/page";
import { IAccount } from "../../models/account";
import { MyPageContent } from "./myPageContent";
interface IProps {
client: Window["fetch"];
account?: IAccount;
}
export function MyPageHtml(props: IProps): JSX.Element {
const { client, ...data } = props;
return (
<Page
css={["mypage"]} js={["mypage"]} maxWidth={1200}
title="Page Title | Liftosaur"
canonical="https://www.liftosaur.com/myroute"
description="Page description"
ogUrl="https://www.liftosaur.com/myroute"
account={props.account} data={data} client={client} url="/myroute"
>
<MyPageContent client={client} {...data} />
</Page>
);
}
src/pages/myPage/myPageContent.tsx)The interactive Preact component. Export the props interface (needed by Layer 4).
src/myPage.tsx)import { h } from "preact";
import { PageWrapper } from "./components/pageWrapper";
import { IMyPageContentProps, MyPageContent } from "./pages/myPage/myPageContent";
import { HydrateUtils } from "./utils/hydrate";
function main(): void {
HydrateUtils.hydratePage<IMyPageContentProps>((pageWrapperProps, data) => (
<PageWrapper {...pageWrapperProps}>
<MyPageContent {...data} client={window.fetch.bind(window)} />
</PageWrapper>
));
}
main();
webpack.config.js)Add to entry in mainConfig:
mypage: ["./src/myPage.tsx", "./src/index.css"],
lambda/index.ts)Add import:
import { renderMyPageHtml } from "./myPage";
Add endpoint + handler (near similar endpoints):
const getMyPageEndpoint = Endpoint.build("/myroute");
const getMyPageHandler: RouteHandler<IPayload, APIGatewayProxyResult, typeof getMyPageEndpoint> = async ({ payload }) => {
const { di } = payload;
let account: IAccount | undefined;
const userResult = await getUserAccount(payload, { withPrograms: true });
if (userResult.success) {
account = userResult.data.account;
}
return {
statusCode: 200,
body: renderMyPageHtml(di.fetch, account),
headers: { "content-type": "text/html" },
};
};
Register in router chain (~line 2700):
.get(getMyPageEndpoint, getMyPageHandler)
_redirects and _redirects_staging)Add redirect entries so the CDN (Netlify) proxies the route to the Lambda backend:
In _redirects:
/myroute https://api3.liftosaur.com/myroute 200
In _redirects_staging:
/myroute https://api3-dev.liftosaur.com/myroute 200
Place the entry before any parameterized routes for the same prefix (e.g. /myroute before /myroute/:id).
src/components/topNavMenu.tsx)If page should appear in nav, update getMenuItems().
PageWrapper (don't set nowrapper on <Page>)src/components/scrollableTabs.tsx): Tabbed contentsrc/components/selectLink.tsx): Dropdown link with bottom sheetsrc/components/exerciseImage.tsx): Exercise illustrationssrc/components/markdown.tsx): Renders markdownsrc/components/icons/src/models/builtinPrograms.ts): Metadata for builtin programslambda/dao/programDao.ts): Fetches program data from CDNdata prop on <Page> is serialized into a hidden #data div. Keep it lightweight.Settings.build() creates default settings for public pages without user context.npx tsc --noEmit after creating all files to verify no TypeScript errors.