feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories: - anthropics/skills: 16 (docs, design, MCP, testing) - obra/superpowers: 14 (TDD, debugging, agents, planning) - coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth) - better-auth/skills: 5 (auth patterns) - vercel-labs/agent-skills: 5 (React, design, Vercel) - antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo) - Plus 13 individual skills from various repos Mosaic Stack is not limited to coding — the Orchestrator and subagents serve coding, business, design, marketing, writing, logistics, analysis, and more. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
---
|
||||
title: Guard Platform-Specific APIs in Universal SSR Code
|
||||
impact: HIGH
|
||||
impactDescription: Accessing browser-only APIs on server causes crashes; Node.js APIs fail in browser
|
||||
type: gotcha
|
||||
tags: [vue3, ssr, browser-api, nodejs, universal, isomorphic, server-side-rendering]
|
||||
---
|
||||
|
||||
# Guard Platform-Specific APIs in Universal SSR Code
|
||||
|
||||
**Impact: HIGH** - SSR applications run the same code on both server (Node.js) and client (browser). Browser APIs like `window`, `document`, and `localStorage` don't exist in Node.js and will throw `ReferenceError`. Similarly, Node.js APIs like `fs` and `process` aren't available in browsers.
|
||||
|
||||
Universal/isomorphic code must guard platform-specific API access or use libraries that work on both platforms.
|
||||
|
||||
## Task Checklist
|
||||
|
||||
- [ ] Never access `window`, `document`, `navigator` in `setup()` or `created()`
|
||||
- [ ] Move browser API access to `onMounted()` lifecycle hook
|
||||
- [ ] Use `typeof window !== 'undefined'` guard when needed outside lifecycle
|
||||
- [ ] Use cross-platform libraries for common functionality (fetch, storage)
|
||||
- [ ] Use Nuxt's `process.client` / `process.server` guards in Nuxt projects
|
||||
|
||||
## Common Browser APIs That Break SSR
|
||||
|
||||
| API | Node.js Behavior |
|
||||
|-----|-----------------|
|
||||
| `window` | `ReferenceError: window is not defined` |
|
||||
| `document` | `ReferenceError: document is not defined` |
|
||||
| `localStorage` / `sessionStorage` | `ReferenceError` |
|
||||
| `navigator` | `ReferenceError` |
|
||||
| `location` | `ReferenceError` |
|
||||
| `history` | `ReferenceError` |
|
||||
| `alert` / `confirm` / `prompt` | `ReferenceError` |
|
||||
| `requestAnimationFrame` | `ReferenceError` |
|
||||
| `IntersectionObserver` | `ReferenceError` |
|
||||
| `ResizeObserver` | `ReferenceError` |
|
||||
|
||||
**Incorrect - Crashes on Server:**
|
||||
```javascript
|
||||
// WRONG: These run during setup/SSR - crashes in Node.js
|
||||
const width = ref(window.innerWidth)
|
||||
const theme = localStorage.getItem('theme')
|
||||
const userAgent = navigator.userAgent
|
||||
```
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
// WRONG: Runs on server, crashes
|
||||
const scrollY = ref(window.scrollY)
|
||||
|
||||
// WRONG: document doesn't exist on server
|
||||
document.title = 'My Page'
|
||||
</script>
|
||||
```
|
||||
|
||||
**Correct - Use onMounted:**
|
||||
```vue
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
// Safe defaults that work on server
|
||||
const width = ref(0)
|
||||
const theme = ref('light')
|
||||
const scrollY = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
// Browser APIs only accessed after mount (client-only)
|
||||
width.value = window.innerWidth
|
||||
theme.value = localStorage.getItem('theme') || 'light'
|
||||
scrollY.value = window.scrollY
|
||||
|
||||
// Event listeners safe in mounted
|
||||
window.addEventListener('resize', handleResize)
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
|
||||
function handleResize() {
|
||||
width.value = window.innerWidth
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
scrollY.value = window.scrollY
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**Correct - Guard with typeof:**
|
||||
```javascript
|
||||
// When you need to check outside lifecycle hooks
|
||||
function getStoredValue(key, defaultValue) {
|
||||
if (typeof window !== 'undefined' && window.localStorage) {
|
||||
return localStorage.getItem(key) ?? defaultValue
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Composable with SSR awareness
|
||||
export function useMediaQuery(query) {
|
||||
const matches = ref(false)
|
||||
|
||||
// Only run on client
|
||||
if (typeof window !== 'undefined') {
|
||||
const mediaQuery = window.matchMedia(query)
|
||||
matches.value = mediaQuery.matches
|
||||
|
||||
// Setup listener in lifecycle
|
||||
onMounted(() => {
|
||||
const handler = (e) => { matches.value = e.matches }
|
||||
mediaQuery.addEventListener('change', handler)
|
||||
onUnmounted(() => mediaQuery.removeEventListener('change', handler))
|
||||
})
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
```
|
||||
|
||||
## Nuxt.js Guards
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// Nuxt provides process.client and process.server
|
||||
if (process.client) {
|
||||
// Only runs in browser
|
||||
window.analytics.track('page_view')
|
||||
}
|
||||
|
||||
if (process.server) {
|
||||
// Only runs on server
|
||||
console.log('Rendering on server')
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- ClientOnly component for client-only rendering -->
|
||||
<ClientOnly>
|
||||
<BrowserOnlyChart :data="chartData" />
|
||||
<template #fallback>
|
||||
<ChartSkeleton />
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Cross-Platform Libraries
|
||||
|
||||
Use libraries that abstract platform differences:
|
||||
|
||||
```javascript
|
||||
// Fetch - works in both Node.js 18+ and browsers
|
||||
const response = await fetch('/api/data')
|
||||
|
||||
// For older Node.js, use node-fetch or axios
|
||||
import axios from 'axios'
|
||||
const { data } = await axios.get('/api/data')
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Universal cookie handling
|
||||
import Cookies from 'js-cookie' // Client only
|
||||
import { parse } from 'cookie' // Works both
|
||||
|
||||
// In Nuxt, use useCookie()
|
||||
const token = useCookie('auth-token')
|
||||
```
|
||||
|
||||
## Common Node.js APIs That Break in Browser
|
||||
|
||||
| API | Browser Behavior |
|
||||
|-----|-----------------|
|
||||
| `fs` | Module not found |
|
||||
| `path` | Module not found |
|
||||
| `process` (full) | Undefined or limited |
|
||||
| `Buffer` | Undefined (unless polyfilled) |
|
||||
| `__dirname` / `__filename` | Undefined |
|
||||
| `require()` | Undefined in ES modules |
|
||||
|
||||
**Incorrect:**
|
||||
```javascript
|
||||
// WRONG: Node.js APIs in universal code
|
||||
import fs from 'fs'
|
||||
const config = JSON.parse(fs.readFileSync('./config.json'))
|
||||
```
|
||||
|
||||
**Correct - Separate Server Code:**
|
||||
```javascript
|
||||
// server/utils.js - Server-only file
|
||||
import fs from 'fs'
|
||||
export function loadConfig() {
|
||||
return JSON.parse(fs.readFileSync('./config.json'))
|
||||
}
|
||||
|
||||
// app.js - Universal code uses API instead
|
||||
const config = await fetch('/api/config').then(r => r.json())
|
||||
```
|
||||
|
||||
## Environment Detection Utility
|
||||
|
||||
```javascript
|
||||
// utils/environment.js
|
||||
export const isClient = typeof window !== 'undefined'
|
||||
export const isServer = !isClient
|
||||
|
||||
export const isBrowser = isClient && typeof document !== 'undefined'
|
||||
export const isNode = typeof process !== 'undefined' &&
|
||||
process.versions?.node != null
|
||||
|
||||
// Usage
|
||||
import { isClient, isServer } from '@/utils/environment'
|
||||
|
||||
if (isClient) {
|
||||
// Browser-specific code
|
||||
}
|
||||
```
|
||||
|
||||
## Third-Party Library Issues
|
||||
|
||||
Some libraries auto-access browser APIs on import:
|
||||
|
||||
```javascript
|
||||
// WRONG: Library accesses window on import
|
||||
import SomeChartLibrary from 'some-chart-library'
|
||||
// ^ Crashes on server if library does: const x = window.something
|
||||
```
|
||||
|
||||
**Correct - Dynamic Import:**
|
||||
```vue
|
||||
<script setup>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
// Dynamic import only loads on client
|
||||
const Chart = defineAsyncComponent(() =>
|
||||
import('some-chart-library').then(m => m.ChartComponent)
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<Chart :data="data" />
|
||||
</ClientOnly>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Reference
|
||||
- [Vue.js SSR - Platform-Specific APIs](https://vuejs.org/guide/scaling-up/ssr.html#access-to-platform-specific-apis)
|
||||
- [Nuxt ClientOnly Component](https://nuxt.com/docs/api/components/client-only)
|
||||
- [MDN: Web APIs](https://developer.mozilla.org/en-US/docs/Web/API)
|
||||
Reference in New Issue
Block a user