Maintaining Frontend Codebases Long-Term: A Freelancer's Playbook
What happens after launch — dependency hygiene, documentation, handoffs, and the maintenance habits that keep client projects healthy for years.
The email arrived fourteen months after I shipped a Next.js dashboard for a logistics startup in Manchester. Subject line: “Site won’t build.” Their new hire had run npm update, committed the lockfile, and CI had been red for three days. Nobody knew which dependency broke the build. The founder was preparing for a board meeting and needed a demo environment working by morning.
I fixed it in four hours. React 19 had landed in their range, a charting library wasn’t compatible, and their ESLint config referenced a deprecated flat config format. None of this was catastrophic. But it was entirely preventable.
This is the part of frontend work clients don’t budget for and developers don’t advertise: maintenance. Launch day is the beginning of a codebase’s life, not the end of your relationship with it. After maintaining dozens of client projects — some I built, some I inherited — I’ve developed a playbook that keeps codebases healthy long after the freelance contract ends.
Why Frontend Maintenance Is Underrated
Business owners understand hosting renewals and domain fees. They less often understand that npm packages change, browsers update, frameworks ship majors, and security vulnerabilities get disclosed in dependencies you didn’t know you had.
A frontend codebase degrades through:
- Dependency drift — outdated packages with accumulating security and compatibility risk
- Framework evolution — Next.js, Astro, and React ship breaking changes on predictable cycles
- Design debt — one-off components that bypassed the design system and now multiply
- Knowledge loss — the developer who made key decisions leaves, documentation doesn’t exist
- Environment rot — Node version mismatches, deprecated CI images, expired API keys in configs
- Performance regression — new features add bundle weight nobody measures
Ignoring maintenance doesn’t freeze the codebase in a working state. It increases the cost of the next change exponentially.
The Maintenance Tiers I Offer Clients
I structure maintenance as explicit tiers so clients choose their risk level consciously.
Tier 1: Security and Stability ($500–$1,500/month)
- Monthly dependency audit and patch updates
- Security advisory monitoring (Dependabot, Snyk, or npm audit)
- CI pipeline health checks
- Hosting and SSL monitoring
- Emergency fix SLA (24–48 hours)
Appropriate for: marketing sites, portfolios, low-traffic content sites.
Tier 2: Active Maintenance ($2,000–$5,000/month)
Everything in Tier 1, plus:
- Minor feature requests (under 8 hours/month)
- Performance monitoring with quarterly Lighthouse audits
- Framework minor version upgrades
- CMS and integration health checks
- Monthly status report
Appropriate for: SaaS marketing sites, e-commerce storefronts, client portals with moderate traffic.
Tier 3: Ongoing Development ($5,000+/month)
Dedicated capacity. Tier 2 plus feature development, A/B test implementation, design system evolution.
Appropriate for: products where the website is the primary revenue channel.
Clients who decline all tiers get a handoff doc and my hourly rate for emergencies. That’s honest. Maintenance is optional until something breaks at the worst possible time.
Dependency Hygiene: The Foundation
Most “site won’t build” emergencies trace back to dependencies.
Lock Your Lockfile
package-lock.json or pnpm-lock.yaml must be committed. CI and production must install from the lockfile, not resolve fresh ranges.
# GitHub Actions — correct install
- run: npm ci
# NOT: npm install
Pin Node Version
// package.json
{
"engines": {
"node": ">=20.11.0 <21"
}
}
Add .nvmrc or .node-version:
20.11.0
CI, local dev, and deployment should use the same Node version. The Manchester project’s CI ran Node 18 while the developer used Node 22 locally. Works until it doesn’t.
Scheduled Dependency Updates
I configure Dependabot or Renovate with rules:
- Patch and minor updates: auto-merge if CI passes
- Major updates: manual review, dedicated PR, test checklist
- Grouped updates: one PR per ecosystem (e.g., all
@astrojs/*together)
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
groups:
astro:
patterns: ["astro", "@astrojs/*"]
react:
patterns: ["react", "react-dom", "@types/react*"]
Weekly small updates beat annual “update everything” nightmares.
Audit Before You Upgrade Majors
Major framework upgrades (Next.js 14 → 15, Astro 4 → 5) get their own branch and checklist:
- Read the migration guide completely
- Run codemods if available
- Upgrade in a branch, fix type errors
- Run full test suite and manual smoke test
- Deploy to staging, run Lighthouse
- Monitor error tracking for 48 hours post-deploy
I block a day per major upgrade on client retainers. Surprises are fewer when upgrades are planned, not reactive.
Documentation That Actually Gets Used
Documentation fails when it’s a 40-page Google Doc nobody opens. Documentation succeeds when it’s embedded where developers already work.
Minimum Viable Docs
Every project I hand off includes:
README.md — how to run locally, environment variables, deploy commands, Node version.
## Quick Start
npm ci
cp .env.example .env.local
npm run dev
## Deploy
Merges to `main` auto-deploy to production via Netlify.
Staging: push to `staging` branch.
## Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `SANITY_PROJECT_ID` | Yes | Sanity CMS project |
| `PUBLIC_SITE_URL` | Yes | Canonical site URL |
ARCHITECTURE.md — one page explaining folder structure, data flow, key decisions.
src/
├── components/ui/ # Design system (Layer 2)
├── components/ # Feature components (Layer 3)
├── lib/sanity.ts # CMS client — all GROQ queries in lib/queries.ts
├── pages/ # Astro routes
└── content/ # Local markdown (docs only; blog is in Sanity)
CHANGELOG.md — maintained during my contract, handed off with last entry noting handoff date.
Decision log — short ADR-style notes for non-obvious choices:
## ADR-003: Use Astro islands instead of full React layout
**Date:** 2025-03-12
**Context:** Marketing site with 3 interactive widgets
**Decision:** Astro default, React only for form, nav, calculator
**Consequence:** JS bundle < 40KB; team must learn client: directives
In-App Documentation
For design systems, I add a /design-system route or Storybook. Developers see components rendered, not just described. For CMS integrations, I add field descriptions in Sanity schema (visible in Studio) explaining what each field controls on the frontend.
Code Quality Gates
Maintenance is easier when bad patterns can’t merge.
CI Pipeline Minimum
# .github/workflows/ci.yml
name: CI
on: [pull_request, push]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "npm"
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm run build
- run: npm audit --audit-level=high
Optional but valuable: Lighthouse CI on PRs affecting frontend routes, Playwright smoke tests on critical paths.
Linting Rules That Prevent Debt
I enable rules that catch maintenance killers:
- No
console.login production code - No unused exports (knip or ts-prune)
- Import order consistency
- Accessibility rules (jsx-a11y) on interactive components
// eslint.config.js (excerpt)
{
rules: {
"no-console": ["warn", { allow: ["warn", "error"] }],
"import/no-cycle": "error",
},
}
Bundle Size Monitoring
// next.config.js or build script
import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";
// Run on CI weekly or on PRs with label "check-bundle"
When a PR adds 80 KB to the main chunk, someone should know before merge.
Handoff: The Most Important Deliverable
Freelance projects end. The codebase shouldn’t become unmaintainable the day I leave.
Handoff Checklist
- README with working local setup verified on a clean machine
- All environment variables documented in
.env.example - Admin access transferred (hosting, CMS, DNS, analytics, error tracking)
- Loom video walkthrough (15–20 minutes): architecture, deploy, common tasks
- List of known technical debt with priority ratings
- Contact info for third-party services (Sanity, Stripe, etc.)
- Maintenance tier recommendation with rationale
The Known Debt List
Honesty builds trust and protects the next developer:
## Technical Debt Register
| Item | Priority | Effort | Notes |
|------|----------|--------|-------|
| Migrate `<img>` to Astro Image component | Medium | 4h | 23 pages affected |
| Extract duplicated filter logic in /products | Low | 2h | Works, just DRY |
| Upgrade chart library when v5 stable | High | 8h | v4 has open CVE |
| Replace placeholder OG images on /services/* | Low | 1h | Using stock photos |
Clients appreciate knowing what’s wrong. Surprises cost more.
Performance Monitoring Over Time
Launch-day Lighthouse scores aren’t permanent. I set up ongoing monitoring:
Synthetic: Lighthouse CI on schedule (weekly) or on deploy. Track LCP, CLS, INP trends.
Real user monitoring: Cloudflare Web Analytics (free), Vercel Analytics, or Speed Insights. Field data beats lab data.
Alerts: Notify if LCP p75 exceeds 2.5s for three consecutive days. Something shipped that shouldn’t have.
For a US e-commerce client, RUM caught a third-party script regression two days after a marketing team added a chat widget. We lazy-loaded it behind interaction and LCP recovered. Without monitoring, they’d have blamed “SEO” for three months of soft conversions.
Managing Third-Party Scripts
Third-party scripts are the leading cause of performance regression on maintained sites. Marketing teams add analytics, chat, heatmaps, and A/B testing tools without engineering review.
My policy for client projects:
- Inventory all third-party scripts in a
THIRD_PARTY.mddoc - Require async/defer loading and performance review before adding new ones
- Use a tag manager only with a governance process
- Review quarterly — remove tools nobody looks at
---
// Pattern: lazy-load non-critical scripts
---
<script>
function loadChat() {
const s = document.createElement("script");
s.src = "https://widget.intercom.io/...";
document.body.appendChild(s);
}
document.getElementById("chat-btn")?.addEventListener("click", loadChat, { once: true });
</script>
When to Refactor vs. When to Rebuild
Clients ask: “Should we fix this or start over?”
Refactor when:
- Core architecture is sound
- Debt is localized (a few components, one integration)
- Team knows the codebase
- Business can’t afford downtime
Rebuild when:
- Framework is end-of-life (old Create React App with no migration path)
- Security posture is unsalvageable
- Performance ceiling is structural (SPA for a content site)
- Cost of each feature exceeds rebuild amortized over 18 months
I rebuilt rather than refactored twice in five years. Both times the original was a WordPress page builder site that had been “customized” for three years. Refactoring would have preserved the wrong foundation.
Building for the Next Developer
I write code assuming someone else maintains it in twelve months. That someone might be me, might not.
Practices:
- Obvious file names over clever abstractions
- Colocate related code; don’t create deep folder hierarchies
- TypeScript on every project — types are documentation that doesn’t go stale
- Comments only for non-obvious business logic, not narrating what code does
- Consistent patterns — if three components fetch data the same way, they should look identical
// Good: clear, typed, boring
export async function getPublishedPosts(): Promise<Post[]> {
return sanity.fetch(allPostsQuery);
}
// Bad: clever abstraction used once
export const withSanity = <T,>(q: string) => (...args: unknown[]) => ...
Boring code maintains well. Clever code confuses the next person.
Client Education: Setting Expectations
Before handoff, I explain to non-technical stakeholders:
- Websites are not furniture. They don’t stay perfect without upkeep.
- Browser updates and dependency changes are external forces, not developer negligence.
- Small monthly maintenance costs less than emergency fixes.
- Content editors can break layouts — that’s a training issue, not a bug.
This conversation prevents the “you built this six months ago, why is it broken?” email. It’s not broken. The ecosystem moved.
Conclusion
Maintaining frontend codebases long-term is disciplined, unglamorous work. Lockfiles, Dependabot, CI pipelines, honest debt registers, and clear handoff docs aren’t exciting. They’re what stand between a fourteen-month-old Next.js app that deploys confidently and one that panics the week before a board meeting.
The Manchester logistics startup signed a Tier 2 retainer after that emergency. We’ve had zero build failures in the eight months since. Updates happen weekly in small PRs. Their new hire knows where the docs live.
If you own a frontend codebase — or hire freelancers to build one — budget for maintenance like you budget for hosting. The code outlives the launch party.
Key Takeaways
- Launch day starts the codebase lifecycle — plan maintenance budget and ownership before handoff
- Commit lockfiles, pin Node versions, and run
npm ciin CI to prevent dependency drift emergencies - Use Dependabot or Renovate for weekly small updates instead of annual upgrade crises
- Minimum docs: README, architecture overview, env var table, and a known technical debt register
- CI should run lint, typecheck, build, and security audit on every PR
- Set up Lighthouse and real-user monitoring to catch performance regressions from new features or third-party scripts
- Handoff includes access transfers, a Loom walkthrough, and honest documentation of what’s imperfect
- Write boring, typed, consistent code — the next maintainer will thank you