This file provides guidance to AI coding agents when working with code in this repository.
We're a startup. We move fast, iterate quickly, and embrace change. When implementing features:
- Favor pragmatic solutions over perfect architecture
- Code will likely change - don't over-engineer
- A/B experiments are common (GrowthBook integration)
- Test coverage isn't a goal - write tests that validate functionality, not hit metrics
Control flow:
- Use early returns instead of if-else blocks for cleaner, flatter code
- Handle the errors or checks first and return early then proceed with happy path at the end of code block
Invariant handling:
- Do not silently ignore impossible states (for example, no-op rollback fallbacks in mutation/cache flows)
- Fail fast with a clear thrown error message when an internal invariant is violated
Drag-and-drop UI:
- Do not render owner-visible empty drag containers or empty categories unless the product requirement explicitly asks for visible empty drop targets
- Any drag overlay or tooltip that reads async query data must defensively handle
undefined/empty arrays without crashing
This is a pnpm monorepo containing the daily.dev application suite:
| Package | Purpose |
|---|---|
packages/webapp |
Next.js web application (main daily.dev site) |
packages/extension |
Browser extension (Chrome/Opera) built with Webpack |
packages/shared |
Shared React components, hooks, utilities, and design system |
packages/storybook |
Component documentation and development environment |
packages/eslint-config |
Shared ESLint configuration |
packages/eslint-rules |
Custom ESLint rules including color consistency enforcement |
packages/prettier-config |
Shared Prettier configuration |
- Node.js v24.14 (see
package.jsonvoltaandpackageManagerproperties, also.nvmrc) - pnpm 9.14.4 for package management (see
package.jsonpackageManagerproperty) - TypeScript across all packages
- React 18.3.1 with Next.js 15 for webapp (Pages Router, NOT App Router/Server Components)
- TanStack Query v5 for server state and data fetching
- GraphQL with graphql-request for API communication
- Tailwind CSS with custom design system
- Jest for testing
- GrowthBook for feature flags and A/B experiments
// Buttons (variants: Primary, Secondary, Tertiary, Float, Subtle, Option, Quiz)
import { Button, ButtonVariant, ButtonSize } from '@dailydotdev/shared/src/components/buttons/Button';
import { ClickableText } from '@dailydotdev/shared/src/components/buttons/ClickableText';
// Typography
import { Typography, TypographyType, TypographyColor } from '@dailydotdev/shared/src/components/typography/Typography';
// Form Fields
import { TextField } from '@dailydotdev/shared/src/components/fields/TextField';
import { Switch } from '@dailydotdev/shared/src/components/fields/Switch';
import { Checkbox } from '@dailydotdev/shared/src/components/fields/Checkbox';
import { Radio } from '@dailydotdev/shared/src/components/fields/Radio';
// Layout & Utilities
import { FlexCol, FlexRow } from '@dailydotdev/shared/src/components/utilities';
import Link from '@dailydotdev/shared/src/components/utilities/Link';
// Icons (500+ available)
import { PlusIcon, ShareIcon, UpvoteIcon } from '@dailydotdev/shared/src/components/icons';
import { IconSize } from '@dailydotdev/shared/src/components/Icon';
// Modals
import { LazyModal } from '@dailydotdev/shared/src/components/modals/common/types';
// Feedback
import { Loader } from '@dailydotdev/shared/src/components/Loader';
import Toast from '@dailydotdev/shared/src/components/notifications/Toast';
import { Tooltip } from '@dailydotdev/shared/src/components/tooltip/Tooltip';// Most commonly used
import { useViewSize, ViewSize } from '@dailydotdev/shared/src/hooks';
import { useLazyModal } from '@dailydotdev/shared/src/hooks/useLazyModal';
import { useToastNotification } from '@dailydotdev/shared/src/hooks/useToastNotification';
import { useConditionalFeature } from '@dailydotdev/shared/src/hooks/useConditionalFeature';
import { useFeedLayout } from '@dailydotdev/shared/src/hooks/useFeedLayout';
import { usePrompt } from '@dailydotdev/shared/src/hooks/usePrompt';
// Actions & State
import { useActions, usePlusSubscription } from '@dailydotdev/shared/src/hooks';
import useFeedSettings from '@dailydotdev/shared/src/hooks/useFeedSettings';import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext';
import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext';
import { useSettingsContext } from '@dailydotdev/shared/src/contexts/SettingsContext';
import { useNotificationContext } from '@dailydotdev/shared/src/contexts/NotificationsContext';import type { Post, PostType } from '@dailydotdev/shared/src/graphql/posts';
import type { Source, SourceType } from '@dailydotdev/shared/src/graphql/sources';
import { gqlClient } from '@dailydotdev/shared/src/graphql/common';
import { ActionType } from '@dailydotdev/shared/src/graphql/actions';import type { LoggedUser } from '@dailydotdev/shared/src/lib/user';
import { LogEvent, Origin } from '@dailydotdev/shared/src/lib/log';
import { AuthTriggers } from '@dailydotdev/shared/src/lib/auth';
import { webappUrl } from '@dailydotdev/shared/src/lib/constants';
import classed from '@dailydotdev/shared/src/lib/classed';
// String utilities
import { stripHtmlTags, capitalize, formatKeyword } from '@dailydotdev/shared/src/lib/strings';import { useForm, FormProvider } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
// Controlled components (use within FormProvider)
import ControlledTextField from '@dailydotdev/shared/src/components/fields/ControlledTextField';
import ControlledTextarea from '@dailydotdev/shared/src/components/fields/ControlledTextarea';
import ControlledSwitch from '@dailydotdev/shared/src/components/fields/ControlledSwitch';IMPORTANT - Zod Type Inference:
- ALWAYS use
z.inferto derive TypeScript types from Zod schemas - NEVER manually define types that duplicate Zod schema structure
// ❌ WRONG: Manual type definition that duplicates schema
const userSchema = z.object({
name: z.string(),
age: z.number(),
});
interface User {
name: string;
age: number;
}
// ✅ RIGHT: Infer type from schema
const userSchema = z.object({
name: z.string(),
age: z.number(),
});
export type User = z.infer<typeof userSchema>;This ensures type safety, reduces duplication, and keeps types automatically in sync with schemas.
# Setup
nvm use # Use correct Node version from .nvmrc
npm i -g [email protected]
pnpm install
# Development
pnpm --filter webapp dev # Run webapp (HTTPS)
pnpm --filter webapp dev:notls # Run webapp (HTTP)
pnpm --filter extension dev:chrome # Run Chrome extension
pnpm --filter storybook dev # Run Storybook
# Testing & Linting
pnpm --filter <package> test # Run tests
pnpm --filter <package> lint # Run linter
pnpm --filter <package> lint:fix # Fix lint issues
# Building for production
pnpm --filter webapp build # Build webapp
pnpm --filter extension build:chrome # Build Chrome extensionIMPORTANT: When running Jest manually with pnpm exec jest or a direct file path, set NODE_ENV=test so React and Testing Library do not run under a production build.
IMPORTANT: Do NOT run build commands while the dev server is running - it will break hot reload. Only run builds at the end to verify your work compiles successfully. During development, rely on the dev server's hot reload and TypeScript/ESLint checks instead.
IMPORTANT: For changed .ts/.tsx files, run node ./scripts/typecheck-strict-changed.js or the package's strict tsc command before finishing. Do not add context-specific props to shared primitives when the behavior can be scoped in the parent list/container.
IMPORTANT: When changing SEO, gating, or noindex logic, preserve existing undefined/nullable behavior unless the requirement explicitly changes it, and verify field names against the typed GraphQL model instead of ticket prose.
Is it used by both webapp AND extension?
├── Yes → packages/shared/
│ ├── Is it a React component? → src/components/
│ ├── Is it a custom hook? → src/hooks/
│ ├── Is it a GraphQL query/mutation? → src/graphql/
│ ├── Is it a complex feature with multiple files? → src/features/
│ ├── Is it a React context? → src/contexts/
│ └── Is it a utility function? → src/lib/
├── No, webapp only → packages/webapp/
│ ├── Is it a page? → pages/
│ ├── Is it a layout? → components/layouts/
│ └── Is it webapp-specific logic? → components/ or hooks/
└── No, extension only → packages/extension/src/
├── Is it for new tab? → newtab/
├── Is it for companion widget? → companion/
└── Is it background logic? → background/
| Use Case | Solution |
|---|---|
| Server data (API responses) | TanStack Query |
| Global app state (user, settings) | React Context |
| Local/UI state | useState |
| Form state | react-hook-form + Zod validation |
Note: TanStack Query v5 uses isPending for mutations (not isLoading).
- Colors: Food-themed palette (burger, cheese, avocado, bacon, etc.)
- Use semantic tokens:
text-primary,bg-surface-primary, not raw colors - Typography: Use
typo-*classes (typo-title1, typo-body, typo-callout) - Responsive: mobileL, mobileXL, tablet, laptop, laptopL, desktop
- ESLint enforces
no-custom-colorrule - use design system tokens - For dismissible banners/cards, default to the shared
CloseButtonicon pattern used elsewhere; do not introduce a separate full-widthDismissbutton unless the request explicitly calls for text dismiss UI.
We write tests to validate functionality, not to achieve coverage metrics:
- Focus on user interactions with React Testing Library
- Mock API responses with
nock - Test files live next to source:
Component.spec.tsx - Run tests:
pnpm --filter <package> test - For hover/tooltip changes on navigation, verify the real interactive hover target is wrapped by the tooltip component. Do not treat a native
titlefallback as a substitute for the requested tooltip behavior unless the user explicitly asks for that fallback.
GrowthBook is integrated for A/B testing. Define features in packages/shared/src/lib/featureManagement.ts:
export const featureMyFlag = new Feature('my_flag', false);Use useConditionalFeature with shouldEvaluate to gate evaluation — only evaluate the flag when the component would otherwise render (e.g., user is authenticated and Plus). This avoids unnecessary GrowthBook evaluations:
import { useConditionalFeature } from '@dailydotdev/shared/src/hooks';
import { featureMyFlag } from '../../lib/featureManagement';
const shouldEvaluate = isAuthReady && isPlus;
const { value: isEnabled } = useConditionalFeature({
feature: featureMyFlag,
shouldEvaluate,
});
const showComponent = shouldEvaluate && isEnabled;When removing a feature flag, do not assume the gated UI should become always-on. Match the product request explicitly: either delete the gated behavior entirely or keep it permanently, and remove dead code/tests for the discarded path.
pnpm-workspace.yaml- Monorepo workspace packagespackages/webapp/next.config.ts- Next.js configurationpackages/shared/tailwind.config.ts- Base Tailwind configurationpackages/extension/webpack.config.js- Extension build configuration
Each package has its own AGENTS.md with detailed guidance:
packages/shared/AGENTS.md- Shared components, hooks, design systempackages/webapp/AGENTS.md- Next.js webapp specificspackages/extension/AGENTS.md- Browser extension developmentpackages/storybook/AGENTS.md- Component documentationpackages/playwright/AGENTS.md- E2E testing with Playwright
When adding new content (videos, images, text blocks) to existing pages:
- Study the page structure first - Identify existing sections and their purpose
- Integrate into existing components rather than creating new wrapper components
- Avoid duplicating section headers - If a page has "How it works", don't add "See how it works"
- Extend existing components - Add content to the relevant existing component instead of creating parallel components
Example: Adding a video to the jobs page
- ❌ Wrong: Create new
OpportunityVideocomponent with its own "See how it works" title - ✅ Right: Add the video embed inside the existing
OpportunityHowItWorkscomponent
- Extension uses
webextension-polyfillfor cross-browser compatibility - SVG imports are converted to React components via
@svgr/webpack - Tailwind utilities preferred over CSS-in-JS
- GraphQL schema changes require manual TypeScript type updates
NEVER create index.ts files that re-export from other files. Barrel exports cause dependency cycles and hurt build performance.
// ❌ NEVER do this - no index.ts barrel files
// hooks/index.ts
export * from './useAuth';
export * from './useUser';
// ❌ NEVER import from barrel
import { useAuth } from './hooks';
// ✅ ALWAYS import directly from the file
import { useAuth } from './hooks/useAuth';
import { useUser } from './hooks/useUser';When you see an existing barrel file, delete it and update all imports to use direct paths.
NEVER copy-paste utility functions into multiple files. If a helper is needed in more than one place, add it to a shared utility file and import it. Do not define the same function locally in each file that needs it.
Before implementing new functionality, always check if similar code already exists:
-
Search for existing utilities - Use Grep/Glob to find similar patterns:
# Search for similar function names grep -r "functionName" packages/shared/src/lib # Search for similar logic patterns grep -r "specific_pattern" packages/
-
Check shared libraries first:
packages/shared/src/lib/func.ts- General utility functionspackages/shared/src/lib/strings.ts- String manipulation, text utilitiespackages/shared/src/lib/links.ts- URL and link utilitiespackages/shared/src/lib/[domain].ts- Domain-specific utilities
-
Extract reusable functions:
- If you write similar logic in multiple places, extract it to a helper
- If the logic is used only in one package → package-specific file
- If the logic could be used across packages →
packages/shared/src/lib/ - Don't extract single-use code into separate functions - keep logic inline where it's used
- Only extract functions when the same logic is needed in multiple places
-
Real-world example (from PostSEOSchema refactor):
- ❌ Wrong: Duplicate author schema logic in 3 places
- ✅ Right: Create
getPersonSchema()helper, reuse everywhere - ❌ Wrong: Local
stripHtml()in webapp-only file - ✅ Right: Move to
shared/src/lib/strings.tsasstripHtmlTags()
-
Before submitting a PR:
- Search for similar patterns in the codebase
- Refactor duplications into reusable functions
- Place utilities in appropriate shared locations
- Run lint to ensure code quality:
pnpm --filter <package> lint
When a hook's callback becomes hard to follow, break it into small helper functions:
- Extract repeated selectors to constants
- Create single-purpose helpers - each function does one thing
- Name helpers by what they do -
expandSectionIfCollapsed,focusFirstInput - Keep the main callback simple - it should read like a high-level description
Example (from useMissingFieldNavigation refactor):
// ❌ Wrong: 90-line callback with nested logic
const handleClick = useCallback((key: string) => {
const el = document.querySelector(`[data-key="${key}"]`);
if (el) {
const section = el.closest('[id^="section-"]');
if (section) {
const btn = document.querySelector(`button[aria-controls="${section.id}"]`);
if (btn?.getAttribute('aria-expanded') === 'false') {
btn.click();
}
}
setTimeout(() => {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
// ... 50 more lines of highlighting/focusing logic
}, 100);
}
// ... another 30 lines for fallback
}, []);
// ✅ Right: Small helpers + simple callback
const expandSectionIfCollapsed = (element: HTMLElement): void => { /* ... */ };
const scrollHighlightAndFocus = (element: HTMLElement): void => { /* ... */ };
const navigateToField = (fieldElement: HTMLElement): void => { /* ... */ };
const navigateToSection = (checkKey: string): void => { /* ... */ };
const handleClick = useCallback((key: string) => {
const fieldElement = document.querySelector(`[data-key="${key}"]`);
if (fieldElement) {
navigateToField(fieldElement);
return;
}
navigateToSection(key);
}, []);Keep PR descriptions concise and to the point. Reviewers should not be exhausted by lengthy explanations.
Use conventional commit messages for all commits, for example fix: ..., feat: ..., or chore: ....
Before opening a PR, run git diff --name-only origin/main...HEAD and confirm every changed file belongs to the current task. If unrelated files appear (for example from reverted or merged commits), clean the branch history first.
When reviewing code (or writing code that will be reviewed):
- Always set explicit
typeon<button>elements in forms - Usetype="button"for non-submit actions (close/back/cancel). Never rely on the browser default inside forms. - Delete dead code - Remove unused components, functions, exports, and files. Don't leave code "for later"
- Avoid confusing naming - Don't create multiple components with the same name in different locations (e.g., two
AboutMecomponents) - Remove unused exports - If a function/constant is only used internally, don't export it
- Clean up duplicates - If the same interface/type is defined in multiple places, consolidate to one location and import
- Activity list modals should be metadata-first - For lists like reposts/upvotes/history in modals, prefer compact rows that emphasize source/author and engagement. Avoid large content images that dominate the layout unless image content is the primary purpose.
- Reuse feed/list card primitives first - Before adding modal-specific list item components, check existing card building blocks (
FeedItemContainer,PostCardHeader, list card primitives) and compose with them. - Do not hide accessible data using presentation heuristics - In UI lists, avoid masking content based on flags like
source.public; rely on backend access controls and render the data returned by the query. - Keep scope tight in design iterations - When adjusting UI, avoid unrelated behavioral/SEO changes in the same commit unless explicitly requested.
- Confirm target surface before implementing UI fixes - If a bug report names a specific component or screen, update only that target unless expansion is explicitly requested.
- Keep action spacing consistent in control headers - When adding icon/action buttons near search fields or other controls, match existing horizontal gaps on both sides to avoid controls touching each other.
- Place feed promos in content flow unless explicitly sticky - If a promo belongs between feed navigation and the feed list, render it in the feed/content layout so it pushes content down. Do not attach it to sticky nav with absolute positioning unless the requirement explicitly asks for overlay behavior.
- Use
searchChildrenprop for content above feed results on search pages - InMainFeedLayout, pagechildrenrender AFTER the<Feed>component. To place banners/promos above feed results, pass them via thesearchChildrenprop (throughlayoutPropson the page component). Do NOT render them as page children — they will appear below all posts. Prefer reusing existing props over introducing new ones. - Preserve spacing intent across breakpoints - When fixing missing padding/margins, do not re-disable them at larger breakpoints with classes like
laptop:mx-0unless the requirement explicitly asks for desktop edge-to-edge layout. Verify mobile and desktop behavior before shipping. - Fix spacing consistently across sibling sections - On page layout bugs, audit all adjacent sections (headers, modules, horizontal feeds, list feeds) and keep spacing rules consistent unless explicitly specified otherwise.
- Protect generated HTML from markdown regex passes - In markdown conversion utilities, never run formatting regexes across already-generated HTML tags/attributes (for example, image
srcURLs with_); add regression tests for URL edge cases. - Prefer component-level token swaps for one-off contrast fixes - For isolated UI readability issues, use existing semantic color utilities in the impacted components first; avoid changing global tokens in
base.cssunless explicitly requested. - Gate infinite scroll with separate
canFetchMoreandfetchNextPageprops - When adding infinite scroll to dropdowns or lists, never derivecanFetchMorefrom the existence of a callback (e.g.!!onScrollEnd). Instead, pass three separate props:fetchNextPage(the function),canFetchMore(boolean fromhasNextPage), andisFetchingNextPage(boolean). The scroll hook should usecanFetchMore && !isFetchingNextPageto gate fetches. Follow theInfiniteScrollingcomponent pattern inpackages/shared/src/components/containers/InfiniteScrolling.tsx. - Handle pre-selected values in paginated dropdowns - When a dropdown uses infinite scroll and a value was previously selected (e.g. from a saved integration), that value may not be in the first page of results. Insert an artificial placeholder entry (e.g.
{ id, name: "Channel ${id}" }) at the front of the list so the selection is visible immediately, and deduplicate byidonce the real item is fetched during scrolling. Do not auto-fetch pages in a loop to find the selected item. Never usefindIndex(...) || 0— it silently falls back to the first item when the value isn't found; use?? -1so the dropdown shows the placeholder instead. - Portaled drawers must stop click propagation - The
BaseDraweroverlay stops click propagation so that portaled child drawers (e.g.ListDrawerinside aDropdowninside a modal-as-drawer) don't accidentally close the parent modal viauseOutsideClick. When adding new portaled overlays, alwaysstopPropagationon the overlay and handle outside-click dismissal locally. - Update text-asserting tests when copy changes - If you rename visible labels in menus/buttons/toasts, update the impacted RTL/Jest selectors in the same PR (e.g.
findByText('...')) to keep behavior tests aligned with intentional UX copy updates.
When upgrading Node.js version, update these files:
.nvmrcDockerfile.github/workflows/e2e-tests.yml.circleci/config.yml(multiple occurrences)packages/playwright/package.json(engines field)- This file (
CLAUDE.md- Technology Stack section)
After updating, run pnpm install to check if lock file needs updating and commit any changes.