MoreRSS

site iconThe Practical DeveloperModify

A constructive and inclusive social network for software developers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of The Practical Developer

How to Generate AI Videos with Seedance API (JavaScript & Python Examples)

2026-02-16 17:05:30

ByteDance's Seedance 2.0 has been getting a lot of attention lately — 2K resolution, up to 15 seconds, native audio generation, and arguably the best motion quality in the current AI video landscape.

But most of the documentation is in Chinese, and figuring out how to actually use it programmatically can be frustrating. I spent a few weeks building tools around Seedance and want to share what I learned about the API integration.

Your Options

There are three ways to access Seedance via API:

Platform Models Available Pricing Auth
fal.ai 1.0 (all variants), 1.5 Pro ~$0.01-0.05/sec API key
Replicate 1.0 Pro, 1.5 Pro ~$0.01-0.05/sec API token
Volcano Engine All versions incl. 2.0 Credits-based Chinese phone required

For most developers outside China, fal.ai or Replicate are the practical choices. Let's start with fal.ai.

Option 1: fal.ai

Setup

npm install @fal-ai/client

Set your API key:

export FAL_KEY="your-api-key-here"

Text-to-Video

import { fal } from "@fal-ai/client";

const result = await fal.subscribe(
  "fal-ai/seedance-v1-pro-t2v",
  {
    input: {
      prompt: "A golden retriever running through a sunlit meadow, slow motion, cinematic lighting, shallow depth of field",
      negative_prompt: "blurry, distorted, low quality",
      num_frames: 120,
      guidance_scale: 7.5,
      seed: 42,
    },
    logs: true,
    onQueueUpdate: (update) => {
      if (update.status === "IN_PROGRESS") {
        console.log(update.logs?.map(l => l.message));
      }
    },
  }
);

console.log(result.data.video.url);

Image-to-Video

This is where Seedance really shines. You provide a reference image and it generates a video that maintains the visual style:

const result = await fal.subscribe(
  "fal-ai/seedance-v1-pro-i2v",
  {
    input: {
      prompt: "The woman turns her head slowly and smiles, wind blowing through her hair",
      image_url: "https://example.com/portrait.jpg",
      num_frames: 120,
      guidance_scale: 7.5,
    },
  }
);

console.log(result.data.video.url);

Option 2: Replicate

Setup

pip install replicate
export REPLICATE_API_TOKEN="your-token-here"

Python Example

import replicate

output = replicate.run(
    "bytedance/seedance-v1-pro-t2v",
    input={
        "prompt": "Aerial drone shot of a coastal city at sunset, golden hour lighting, waves crashing against cliffs, 4K cinematic",
        "negative_prompt": "blurry, distorted",
        "num_frames": 120,
        "guidance_scale": 7.5,
        "seed": 42,
    }
)

print(output)

cURL (for any language)

curl -s -X POST "https://api.replicate.com/v1/predictions" \
  -H "Authorization: Bearer $REPLICATE_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "version": "bytedance/seedance-v1-pro-t2v",
    "input": {
      "prompt": "A cup of coffee with steam rising, macro shot, warm lighting",
      "num_frames": 120,
      "guidance_scale": 7.5
    }
  }'

Prompt Engineering Tips

After generating hundreds of videos, here's what actually matters:

1. Be specific about motion

# Bad
"A cat sitting on a table"

# Good
"A tabby cat slowly stretches on a wooden table, then turns its head toward the camera, soft afternoon light from a window"

2. Specify camera movement

Seedance handles camera instructions well:

"Slow dolly-in on a vintage typewriter, shallow depth of field, the keys begin to press themselves"

3. Use the @reference system for image-to-video

When using i2v mode, reference tags give you precise control:

"@Image1 The person in the photo begins to walk forward, camera follows with a steady tracking shot"

4. Negative prompts matter

Always include: "blurry, distorted, low quality, watermark, text overlay"

Model Comparison: Which Variant to Use?

Model Best For Speed Quality
1.0 Lite Quick prototypes Fast Good
1.0 Pro Production quality Slow Excellent
1.5 Pro Latest features Medium Excellent
1.0 Fast Batch processing Fastest Good

For most use cases, 1.0 Pro gives the best quality-to-cost ratio. Use 1.0 Fast when you're iterating on prompts and need quick feedback.

Cost Estimation

A rough formula:

cost ≈ duration_seconds × $0.03 (fal.ai average)

So a 5-second video costs roughly $0.15, and a 10-second video about $0.30. Significantly cheaper than Sora 2 (which requires a $200/mo ChatGPT Pro subscription).

Common Errors and Fixes

NSFW content detected — Seedance has content filters. Rephrase your prompt to avoid triggering them.

Timeout after 300s — Pro models can take 3-5 minutes for longer videos. Increase your timeout or use the async/webhook pattern.

Invalid image format — For i2v, use JPEG or PNG. WebP sometimes causes issues. Image should be at least 512x512.

Resources

I built a free resource site that covers all of this in more detail:

The site is open source: github.com/hueshadow/seedance

What's your experience with Seedance? Have you found any interesting use cases? Drop a comment below.

Vouch: Mitchell Hashimoto’s Experiment in Restoring Trust to Open Source

2026-02-16 17:00:28

Open source has always operated on an implicit social contract:

If you care enough to contribute, you probably care enough to contribute well.

But that assumption is breaking down.

With AI tools capable of generating pull requests instantly, maintainers are facing a new problem: high volumes of low-context, low-quality contributions that look legitimate but require real human time to evaluate.

Enter Vouch, a new open-source project by Mitchell Hashimoto (co-founder of HashiCorp and creator of Terraform, Vagrant, and more).

Vouch isn't about better linting, smarter CI, or stricter contribution templates.

It's about trust.

What Is Vouch?

Vouch is a trust management system for open source projects.

Instead of allowing anyone to freely submit issues or pull requests, projects can require contributors to be explicitly vouched for by trusted members.

In other words:

  • No vouch = no meaningful contribution access
  • Vouched = full participation
  • Denounced = blocked

It introduces a human approval layer before contribution - not after.

Why Vouch Exists

Historically, open source had natural filters:

  • Writing working code required effort
  • Understanding a codebase required context
  • Submitting a PR required genuine investment

AI has flattened those barriers.

Now:

  • PRs can be generated instantly
  • Surface-level correctness can mask shallow understanding
  • Maintainers must manually filter increasing noise

Vouch proposes a simple shift:

Replace effort-based filtering with trust-based filtering.

Instead of evaluating every submission, maintainers evaluate people first.

How Vouch Works

Vouching

A trusted contributor can vouch for someone else.

Once vouched, that contributor can:

  • Open PRs
  • Open issues
  • Participate normally

Important: A vouched contributor cannot vouch others. This prevents uncontrolled trust expansion.

Denouncing

Trusted members can also denounce a contributor.

This immediately revokes trust and blocks participation.

The Trust List

The trust system is stored in a simple, version-controlled text file inside the repository.

That means:

  • Transparent history
  • Code-reviewed changes
  • No hidden database
  • Works across GitHub and other forges

Web of Trust

One of Vouch’s most interesting ideas is shared trust.

Projects can reference trust lists from other projects.

So if someone is trusted in:

  • Project A

They may automatically be trusted in:

  • Project B (if B chooses to accept A's trust graph)

This creates a decentralized web of trust, similar to PGP-style identity systems - but applied to contributor reputation.

Example Workflow

Here’s what onboarding might look like:

  1. A new contributor opens an issue introducing themselves
  2. A maintainer evaluates them (conversation, past work, references)
  3. The maintainer adds them to the trust list
  4. From then on, they can contribute normally

No vouch? The PR may automatically close via CI.

Benefits

  • Reduces AI-generated noise
  • Saves maintainer time
  • Makes trust explicit instead of assumed
  • Portable across platforms
  • Transparent and version-controlled

It moves from "review every submission" to "review the person once."

Concerns & Criticism

Of course, this isn't without trade-offs.

Potential downsides:

  • Adds friction for first-time contributors
  • Could feel like gatekeeping
  • Might discourage casual drive-by fixes
  • Social bias could creep into trust decisions

Vouch doesn't attempt to solve fairness universally. It simply gives maintainers more control over how open their project should be.

Philosophical Shift

Vouch raises an important question:

Is open source about unlimited access — or sustainable collaboration?

In an era where AI can flood repositories with plausible contributions, unlimited access may no longer scale.

Vouch doesn't close open source.

It makes openness intentional.

Real-World Testing

Mitchell is already using Vouch in his own project, Ghostty, as a proving ground.

  • It's early.
  • It's experimental.
  • It's controversial.

But it's also one of the first serious attempts to rethink open source trust in the AI era.

Final Thoughts

Vouch is less about tooling and more about sociology.

It acknowledges that:

  • Open source is fundamentally human
  • Trust is the real scaling bottleneck
  • AI changes contribution dynamics

Whether Vouch becomes widely adopted or remains niche, it forces an important conversation:

How do we protect maintainer time without sacrificing openness?

And that's a question every modern open source project will eventually have to answer.

Written by a Human logo

Next.js JWT Authentication: Complete Guide to Secure Your App in 2026

2026-02-16 16:44:54

Building secure authentication in Next.js applications can be challenging, especially with the framework's unique server-side and client-side rendering capabilities. In this comprehensive guide, I'll show you how to implement JWT (JSON Web Token) authentication in Next.js 14+ with App Router, covering everything from setup to production-ready security practices.

Why JWT Authentication for Next.js?

Next.js's hybrid rendering model (SSR, SSG, CSR) requires an authentication solution that works seamlessly across server and client components. JWT authentication provides:

Stateless Authentication: Perfect for serverless deployments and edge functions

API Route Protection: Secure your Next.js API routes effortlessly

SSR Compatibility: Works with server-side rendering out of the box

Scalability: No session storage needed, ideal for distributed systems

Mobile-Friendly: Easy integration with React Native or mobile apps

Edge-Ready: Compatible with Next.js Edge Runtime

What We'll Build

By the end of this tutorial, you'll have a fully functional Next.js app with:

  • User registration and login
  • Protected pages and API routes
  • JWT token management with refresh tokens
  • Middleware for authentication
  • Secure cookie storage
  • Logout functionality
  • Client and server-side authentication checks

Prerequisites

Before we start, ensure you have:

  • Node.js 18+ installed
  • Basic understanding of React and Next.js
  • Familiarity with TypeScript (optional but recommended)
  • A backend API or we'll use Next.js API routes

Project Setup

Step 1: Create a Next.js Project

npx create-next-app@latest nextjs-jwt-auth
cd nextjs-jwt-auth

When prompted, select:

  • ✅ TypeScript
  • ✅ ESLint
  • ✅ Tailwind CSS
  • ✅ App Router
  • ❌ Turbopack

Step 2: Install Required Dependencies

npm install jsonwebtoken bcryptjs jose
npm install --save-dev @types/jsonwebtoken @types/bcryptjs

Package Breakdown:

  • jsonwebtoken: JWT creation and verification
  • bcryptjs: Password hashing
  • jose: JWT handling for Edge Runtime (Next.js compatible)

Step 3: Project Structure

Create the following folder structure:

nextjs-jwt-auth/
├── app/
│   ├── api/
│   │   ├── auth/
│   │   │   ├── login/route.ts
│   │   │   ├── register/route.ts
│   │   │   ├── logout/route.ts
│   │   │   └── refresh/route.ts
│   │   └── protected/route.ts
│   ├── login/page.tsx
│   ├── register/page.tsx
│   ├── dashboard/page.tsx
│   ├── layout.tsx
│   └── page.tsx
├── lib/
│   ├── auth.ts
│   ├── db.ts
│   └── types.ts
├── middleware.ts
└── .env.local

Setting Up Environment Variables

Create a .env.local file in your project root:

JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
JWT_REFRESH_SECRET=your-super-secret-refresh-key-change-this
ACCESS_TOKEN_EXPIRY=15m
REFRESH_TOKEN_EXPIRY=7d
NODE_ENV=development

Important: Generate strong secrets using:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Creating Type Definitions

Create lib/types.ts:

export interface User {
  id: string;
  email: string;
  name: string;
  password?: string;
  createdAt: Date;
}

export interface JWTPayload {
  userId: string;
  email: string;
  name: string;
  iat?: number;
  exp?: number;
}

export interface LoginCredentials {
  email: string;
  password: string;
}

export interface RegisterData {
  name: string;
  email: string;
  password: string;
}

export interface AuthResponse {
  success: boolean;
  message?: string;
  user?: Omit<User, 'password'>;
  accessToken?: string;
}

Building Authentication Utilities

Create lib/auth.ts:

import { SignJWT, jwtVerify } from 'jose';
import { cookies } from 'next/headers';
import { NextRequest } from 'next/server';
import { JWTPayload } from './types';

const secretKey = process.env.JWT_SECRET!;
const refreshSecretKey = process.env.JWT_REFRESH_SECRET!;
const key = new TextEncoder().encode(secretKey);
const refreshKey = new TextEncoder().encode(refreshSecretKey);

// Generate Access Token
export async function generateAccessToken(payload: JWTPayload): Promise<string> {
  return await new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('15m')
    .sign(key);
}

// Generate Refresh Token
export async function generateRefreshToken(payload: JWTPayload): Promise<string> {
  return await new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('7d')
    .sign(refreshKey);
}

// Verify Access Token
export async function verifyAccessToken(token: string): Promise<JWTPayload | null> {
  try {
    const verified = await jwtVerify(token, key);
    return verified.payload as JWTPayload;
  } catch (error) {
    return null;
  }
}

// Verify Refresh Token
export async function verifyRefreshToken(token: string): Promise<JWTPayload | null> {
  try {
    const verified = await jwtVerify(token, refreshKey);
    return verified.payload as JWTPayload;
  } catch (error) {
    return null;
  }
}

// Get token from cookies
export async function getTokenFromCookies(): Promise<string | null> {
  const cookieStore = await cookies();
  const token = cookieStore.get('accessToken');
  return token?.value || null;
}

// Get token from request headers
export function getTokenFromHeaders(request: NextRequest): string | null {
  const authHeader = request.headers.get('authorization');
  if (authHeader && authHeader.startsWith('Bearer ')) {
    return authHeader.substring(7);
  }
  return null;
}

// Get current user from token
export async function getCurrentUser(request: NextRequest): Promise<JWTPayload | null> {
  const cookieStore = await cookies();
  const token = cookieStore.get('accessToken')?.value || getTokenFromHeaders(request);

  if (!token) return null;

  return await verifyAccessToken(token);
}

// Set auth cookies
export async function setAuthCookies(accessToken: string, refreshToken: string) {
  const cookieStore = await cookies();

  cookieStore.set('accessToken', accessToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 60 * 15, // 15 minutes
    path: '/',
  });

  cookieStore.set('refreshToken', refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 7, // 7 days
    path: '/',
  });
}

// Clear auth cookies
export async function clearAuthCookies() {
  const cookieStore = await cookies();
  cookieStore.delete('accessToken');
  cookieStore.delete('refreshToken');
}

Simple User Database (In-Memory)

Create lib/db.ts:

import bcrypt from 'bcryptjs';
import { User } from './types';

// In production, use a real database (PostgreSQL, MongoDB, etc.)
const users: Map<string, User> = new Map();

export const db = {
  // Find user by email
  findUserByEmail: async (email: string): Promise<User | null> => {
    for (const user of users.values()) {
      if (user.email === email) {
        return user;
      }
    }
    return null;
  },

  // Find user by ID
  findUserById: async (id: string): Promise<User | null> => {
    return users.get(id) || null;
  },

  // Create new user
  createUser: async (name: string, email: string, password: string): Promise<User> => {
    const existingUser = await db.findUserByEmail(email);
    if (existingUser) {
      throw new Error('User already exists');
    }

    const hashedPassword = await bcrypt.hash(password, 10);
    const userId = Date.now().toString();

    const user: User = {
      id: userId,
      name,
      email,
      password: hashedPassword,
      createdAt: new Date(),
    };

    users.set(userId, user);
    return user;
  },

  // Verify password
  verifyPassword: async (plainPassword: string, hashedPassword: string): Promise<boolean> => {
    return await bcrypt.compare(plainPassword, hashedPassword);
  },

  // Remove password from user object
  sanitizeUser: (user: User): Omit<User, 'password'> => {
    const { password, ...userWithoutPassword } = user;
    return userWithoutPassword;
  },
};

Creating API Routes

Registration Route

Create app/api/auth/register/route.ts:

import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { RegisterData } from '@/lib/types';

export async function POST(request: NextRequest) {
  try {
    const body: RegisterData = await request.json();
    const { name, email, password } = body;

    // Validation
    if (!name || !email || !password) {
      return NextResponse.json(
        { success: false, message: 'All fields are required' },
        { status: 400 }
      );
    }

    if (password.length < 6) {
      return NextResponse.json(
        { success: false, message: 'Password must be at least 6 characters' },
        { status: 400 }
      );
    }

    // Create user
    const user = await db.createUser(name, email, password);
    const sanitizedUser = db.sanitizeUser(user);

    return NextResponse.json(
      {
        success: true,
        message: 'User registered successfully',
        user: sanitizedUser,
      },
      { status: 201 }
    );
  } catch (error: any) {
    return NextResponse.json(
      { success: false, message: error.message || 'Registration failed' },
      { status: 400 }
    );
  }
}

Login Route

Create app/api/auth/login/route.ts:

import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { generateAccessToken, generateRefreshToken, setAuthCookies } from '@/lib/auth';
import { LoginCredentials } from '@/lib/types';

export async function POST(request: NextRequest) {
  try {
    const body: LoginCredentials = await request.json();
    const { email, password } = body;

    // Validation
    if (!email || !password) {
      return NextResponse.json(
        { success: false, message: 'Email and password are required' },
        { status: 400 }
      );
    }

    // Find user
    const user = await db.findUserByEmail(email);
    if (!user) {
      return NextResponse.json(
        { success: false, message: 'Invalid credentials' },
        { status: 401 }
      );
    }

    // Verify password
    const isValidPassword = await db.verifyPassword(password, user.password!);
    if (!isValidPassword) {
      return NextResponse.json(
        { success: false, message: 'Invalid credentials' },
        { status: 401 }
      );
    }

    // Generate tokens
    const payload = {
      userId: user.id,
      email: user.email,
      name: user.name,
    };

    const accessToken = await generateAccessToken(payload);
    const refreshToken = await generateRefreshToken(payload);

    // Set cookies
    await setAuthCookies(accessToken, refreshToken);

    const sanitizedUser = db.sanitizeUser(user);

    return NextResponse.json(
      {
        success: true,
        message: 'Login successful',
        user: sanitizedUser,
        accessToken,
      },
      { status: 200 }
    );
  } catch (error: any) {
    return NextResponse.json(
      { success: false, message: error.message || 'Login failed' },
      { status: 500 }
    );
  }
}

Refresh Token Route

Create app/api/auth/refresh/route.ts:

import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { verifyRefreshToken, generateAccessToken, setAuthCookies, generateRefreshToken } from '@/lib/auth';

export async function POST(request: NextRequest) {
  try {
    const cookieStore = await cookies();
    const refreshToken = cookieStore.get('refreshToken')?.value;

    if (!refreshToken) {
      return NextResponse.json(
        { success: false, message: 'No refresh token provided' },
        { status: 401 }
      );
    }

    const payload = await verifyRefreshToken(refreshToken);
    if (!payload) {
      return NextResponse.json(
        { success: false, message: 'Invalid refresh token' },
        { status: 401 }
      );
    }

    // Generate new tokens
    const newAccessToken = await generateAccessToken({
      userId: payload.userId,
      email: payload.email,
      name: payload.name,
    });

    const newRefreshToken = await generateRefreshToken({
      userId: payload.userId,
      email: payload.email,
      name: payload.name,
    });

    // Set new cookies
    await setAuthCookies(newAccessToken, newRefreshToken);

    return NextResponse.json(
      {
        success: true,
        message: 'Token refreshed successfully',
        accessToken: newAccessToken,
      },
      { status: 200 }
    );
  } catch (error: any) {
    return NextResponse.json(
      { success: false, message: error.message || 'Token refresh failed' },
      { status: 500 }
    );
  }
}

Logout Route

Create app/api/auth/logout/route.ts:

import { NextRequest, NextResponse } from 'next/server';
import { clearAuthCookies } from '@/lib/auth';

export async function POST(request: NextRequest) {
  try {
    await clearAuthCookies();

    return NextResponse.json(
      { success: true, message: 'Logged out successfully' },
      { status: 200 }
    );
  } catch (error: any) {
    return NextResponse.json(
      { success: false, message: error.message || 'Logout failed' },
      { status: 500 }
    );
  }
}

Protected API Route Example

Create app/api/protected/route.ts:

import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth';

export async function GET(request: NextRequest) {
  try {
    const user = await getCurrentUser(request);

    if (!user) {
      return NextResponse.json(
        { success: false, message: 'Unauthorized' },
        { status: 401 }
      );
    }

    return NextResponse.json(
      {
        success: true,
        message: 'This is protected data',
        user,
        data: {
          secretInfo: 'Only authenticated users can see this',
          timestamp: new Date().toISOString(),
        },
      },
      { status: 200 }
    );
  } catch (error: any) {
    return NextResponse.json(
      { success: false, message: error.message || 'Server error' },
      { status: 500 }
    );
  }
}

Creating Middleware for Route Protection

Create middleware.ts in the root directory:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifyAccessToken } from './lib/auth';

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('accessToken')?.value;

  // Protected routes
  const protectedPaths = ['/dashboard', '/profile', '/settings'];
  const isProtectedPath = protectedPaths.some(path => 
    request.nextUrl.pathname.startsWith(path)
  );

  // Public routes that should redirect if authenticated
  const authPaths = ['/login', '/register'];
  const isAuthPath = authPaths.some(path => 
    request.nextUrl.pathname.startsWith(path)
  );

  if (isProtectedPath) {
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url));
    }

    const user = await verifyAccessToken(token);
    if (!user) {
      const response = NextResponse.redirect(new URL('/login', request.url));
      response.cookies.delete('accessToken');
      response.cookies.delete('refreshToken');
      return response;
    }
  }

  if (isAuthPath && token) {
    const user = await verifyAccessToken(token);
    if (user) {
      return NextResponse.redirect(new URL('/dashboard', request.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: [
    '/dashboard/:path*',
    '/profile/:path*',
    '/settings/:path*',
    '/login',
    '/register',
  ],
};

Building Frontend Components

Login Page

Create app/login/page.tsx:

'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';

export default function LoginPage() {
  const router = useRouter();
  const [formData, setFormData] = useState({
    email: '',
    password: '',
  });
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');
    setLoading(true);

    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData),
      });

      const data = await response.json();

      if (data.success) {
        router.push('/dashboard');
        router.refresh();
      } else {
        setError(data.message || 'Login failed');
      }
    } catch (error) {
      setError('An error occurred. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
      <div className="max-w-md w-full space-y-8">
        <div>
          <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
            Sign in to your account
          </h2>
        </div>
        <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
          {error && (
            <div className="rounded-md bg-red-50 p-4">
              <p className="text-sm text-red-800">{error}</p>
            </div>
          )}
          <div className="rounded-md shadow-sm -space-y-px">
            <div>
              <label htmlFor="email" className="sr-only">Email address</label>
              <input
                id="email"
                name="email"
                type="email"
                required
                className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                placeholder="Email address"
                value={formData.email}
                onChange={(e) => setFormData({ ...formData, email: e.target.value })}
              />
            </div>
            <div>
              <label htmlFor="password" className="sr-only">Password</label>
              <input
                id="password"
                name="password"
                type="password"
                required
                className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                placeholder="Password"
                value={formData.password}
                onChange={(e) => setFormData({ ...formData, password: e.target.value })}
              />
            </div>
          </div>

          <div>
            <button
              type="submit"
              disabled={loading}
              className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
            >
              {loading ? 'Signing in...' : 'Sign in'}
            </button>
          </div>

          <div className="text-sm text-center">
            <Link href="/register" className="font-medium text-indigo-600 hover:text-indigo-500">
              Don't have an account? Register
            </Link>
          </div>
        </form>
      </div>
    </div>
  );
}

Register Page

Create app/register/page.tsx:

'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';

export default function RegisterPage() {
  const router = useRouter();
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: '',
    confirmPassword: '',
  });
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');

    if (formData.password !== formData.confirmPassword) {
      setError('Passwords do not match');
      return;
    }

    setLoading(true);

    try {
      const response = await fetch('/api/auth/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          name: formData.name,
          email: formData.email,
          password: formData.password,
        }),
      });

      const data = await response.json();

      if (data.success) {
        router.push('/login?registered=true');
      } else {
        setError(data.message || 'Registration failed');
      }
    } catch (error) {
      setError('An error occurred. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
      <div className="max-w-md w-full space-y-8">
        <div>
          <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
            Create your account
          </h2>
        </div>
        <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
          {error && (
            <div className="rounded-md bg-red-50 p-4">
              <p className="text-sm text-red-800">{error}</p>
            </div>
          )}
          <div className="rounded-md shadow-sm space-y-4">
            <input
              type="text"
              required
              className="appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
              placeholder="Full Name"
              value={formData.name}
              onChange={(e) => setFormData({ ...formData, name: e.target.value })}
            />
            <input
              type="email"
              required
              className="appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
              placeholder="Email address"
              value={formData.email}
              onChange={(e) => setFormData({ ...formData, email: e.target.value })}
            />
            <input
              type="password"
              required
              className="appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
              placeholder="Password"
              value={formData.password}
              onChange={(e) => setFormData({ ...formData, password: e.target.value })}
            />
            <input
              type="password"
              required
              className="appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
              placeholder="Confirm Password"
              value={formData.confirmPassword}
              onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
            />
          </div>

          <button
            type="submit"
            disabled={loading}
            className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
          >
            {loading ? 'Creating account...' : 'Register'}
          </button>

          <div className="text-sm text-center">
            <Link href="/login" className="font-medium text-indigo-600 hover:text-indigo-500">
              Already have an account? Sign in
            </Link>
          </div>
        </form>
      </div>
    </div>
  );
}

Protected Dashboard Page

Create app/dashboard/page.tsx:

'use client';

import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';

interface UserData {
  userId: string;
  email: string;
  name: string;
}

export default function DashboardPage() {
  const router = useRouter();
  const [userData, setUserData] = useState<UserData | null>(null);
  const [protectedData, setProtectedData] = useState<any>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchProtectedData();
  }, []);

  const fetchProtectedData = async () => {
    try {
      const response = await fetch('/api/protected');
      const data = await response.json();

      if (data.success) {
        setUserData(data.user);
        setProtectedData(data.data);
      }
    } catch (error) {
      console.error('Error fetching protected data:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleLogout = async () => {
    try {
      await fetch('/api/auth/logout', { method: 'POST' });
      router.push('/login');
      router.refresh();
    } catch (error) {
      console.error('Logout error:', error);
    }
  };

  if (loading) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <div className="text-xl">Loading...</div>
      </div>
    );
  }

  return (
    <div className="min-h-screen bg-gray-50">
      <nav className="bg-white shadow-sm">
        <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
          <div className="flex justify-between h-16">
            <div className="flex items-center">
              <h1 className="text-xl font-bold">Dashboard</h1>
            </div>
            <div className="flex items-center">
              <button
                onClick={handleLogout}
                className="ml-4 px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700"
              >
                Logout
              </button>
            </div>
          </div>
        </div>
      </nav>

      <main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
        <div className="px-4 py-6 sm:px-0">
          <div className="bg-white shadow rounded-lg p-6 mb-6">
            <h2 className="text-2xl font-bold mb-4">Welcome, {userData?.name}!</h2>
            <div className="space-y-2">
              <p className="text-gray-600">
                <span className="font-semibold">Email:</span> {userData?.email}
              </p>
              <p className="text-gray-600">
                <span className="font-semibold">User ID:</span> {userData?.userId}
              </p>
            </div>
          </div>

          <div className="bg-white shadow rounded-lg p-6">
            <h3 className="text-xl font-bold mb-4">Protected Data</h3>
            {protectedData && (
              <div className="space-y-2">
                <p className="text-gray-600">{protectedData.secretInfo}</p>
                <p className="text-sm text-gray-500">
                  Last accessed: {new Date(protectedData.timestamp).toLocaleString()}
                </p>
              </div>
            )}
          </div>
        </div>
      </main>
    </div>
  );
}

Creating a Custom Auth Hook

Create lib/useAuth.ts:

'use client';

import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';

interface User {
  userId: string;
  email: string;
  name: string;
}

export function useAuth() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const router = useRouter();

  useEffect(() => {
    checkAuth();
  }, []);

  const checkAuth = async () => {
    try {
      const response = await fetch('/api/protected');
      const data = await response.json();

      if (data.success) {
        setUser(data.user);
      } else {
        setUser(null);
      }
    } catch (error) {
      setUser(null);
    } finally {
      setLoading(false);
    }
  };

  const login = async (email: string, password: string) => {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });

    const data = await response.json();

    if (data.success) {
      setUser(data.user);
      return { success: true };
    }

    return { success: false, message: data.message };
  };

  const logout = async () => {
    await fetch('/api/auth/logout', { method: 'POST' });
    setUser(null);
    router.push('/login');
  };

  const refreshToken = async () => {
    try {
      const response = await fetch('/api/auth/refresh', {
        method: 'POST',
      });

      const data = await response.json();
      return data.success;
    } catch (error) {
      return false;
    }
  };

  return { user, loading, login, logout, refreshToken };
}

Testing Your Authentication

Start the Development Server

npm run dev

Test Flow:

  1. Register: Navigate to http://localhost:3000/register

    • Create a new account
    • Should redirect to login
  2. Login: Go to http://localhost:3000/login

    • Use your credentials
    • Should redirect to dashboard
  3. Dashboard: Access http://localhost:3000/dashboard

    • See your user information
    • View protected data
  4. Logout: Click logout button

    • Should redirect to login
    • Cannot access dashboard

Security Best Practices

1. Token Storage

✅ DO:

  • Store tokens in httpOnly cookies (server-side)
  • Use secure flag in production
  • Set appropriate sameSite attribute

❌ DON'T:

  • Store tokens in localStorage
  • Store tokens in regular cookies accessible via JavaScript
  • Expose tokens in URLs

2. Token Expiration

// Short-lived access tokens
ACCESS_TOKEN_EXPIRY=15m

// Long-lived refresh tokens
REFRESH_TOKEN_EXPIRY=7d

3. HTTPS in Production

Always use HTTPS in production to prevent token interception:

cookieStore.set('accessToken', accessToken, {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production', // HTTPS only in prod
  sameSite: 'lax',
  maxAge: 60 * 15,
  path: '/',
});

4. CSRF Protection

Next.js provides built-in CSRF protection for API routes. Ensure you're using:

export const config = {
  api: {
    bodyParser: true,
  },
};

5. Rate Limiting

Install and configure rate limiting:

npm install @upstash/ratelimit @upstash/redis

6. Environment Variables

Never commit .env.local to version control:

# .gitignore
.env.local
.env.*.local

Production Deployment Checklist

  • [ ] Generate strong JWT secrets
  • [ ] Enable HTTPS/SSL
  • [ ] Set secure cookie flags
  • [ ] Configure CORS properly
  • [ ] Implement rate limiting
  • [ ] Set up monitoring and logging
  • [ ] Use environment variables
  • [ ] Enable token refresh mechanism
  • [ ] Implement token blacklisting (optional)
  • [ ] Add CSP headers
  • [ ] Configure proper error handling
  • [ ] Set up database (replace in-memory storage)

Connecting to a Real Database

Replace the in-memory storage with a real database. Here's an example with Prisma:

npm install prisma @prisma/client
npx prisma init

Update prisma/schema.prisma:

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String
  password  String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Run migrations:

npx prisma migrate dev --name init
npx prisma generate

Advanced Features to Implement

1. Email Verification

Add email verification during registration to ensure valid users.

2. Password Reset

Implement forgot password functionality with time-limited reset tokens.

3. Two-Factor Authentication

Add an extra security layer with 2FA using libraries like speakeasy.

4. Social Authentication

Integrate OAuth providers using NextAuth.js:

npm install next-auth

5. Role-Based Access Control

Extend JWT payload with user roles:

export interface JWTPayload {
  userId: string;
  email: string;
  name: string;
  role: 'admin' | 'user' | 'moderator';
}

Common Issues and Solutions

Issue 1: "Invalid Token" After Refresh

Solution: Implement automatic token refresh in your API calls.

Issue 2: Cookies Not Being Set

Solution: Check cookie settings and ensure domain matches.

Issue 3: Middleware Not Working

Solution: Verify middleware.ts is in the root directory and config matcher is correct.

Issue 4: CORS Errors

Solution: Configure CORS headers in next.config.js:

module.exports = {
  async headers() {
    return [
      {
        source: '/api/:path*',
        headers: [
          { key: 'Access-Control-Allow-Credentials', value: 'true' },
          { key: 'Access-Control-Allow-Origin', value: '*' },
          { key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE,OPTIONS' },
          { key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' },
        ],
      },
    ];
  },
};

Performance Optimization

1. Token Caching

Cache verified tokens to reduce computation:

const tokenCache = new Map<string, { payload: JWTPayload; exp: number }>();

2. Edge Runtime

Deploy middleware to Edge for faster authentication checks:

export const config = {
  matcher: ['/dashboard/:path*'],
  runtime: 'edge',
};

3. Lazy Loading

Lazy load authentication check on client:

const { user } = useAuth();

Monitoring and Logging

Implement logging for security events:

// lib/logger.ts
export const logger = {
  login: (email: string, success: boolean) => {
    console.log(`Login attempt: ${email} - ${success ? 'SUCCESS' : 'FAILED'}`);
  },
  tokenRefresh: (userId: string) => {
    console.log(`Token refreshed for user: ${userId}`);
  },
  logout: (userId: string) => {
    console.log(`User logged out: ${userId}`);
  },
};

Conclusion

You've successfully implemented a complete JWT authentication system in Next.js with the App Router. This implementation includes user registration, login, logout, token refresh, protected routes, and middleware protection.

The stateless nature of JWT makes it perfect for Next.js applications, especially when deploying to serverless or edge environments. Remember to follow security best practices, use HTTPS in production, and keep your tokens short-lived.

Have questions about Next.js JWT authentication? Drop them in the comments below!

Found this helpful? Give it a like and follow for more Next.js tutorials!

Why I Stopped Writing Markdown in VS Code (and What I Built Instead) 🚀

2026-02-16 16:40:49

Markdown is the backbone of the web, but let's be honest—traditional editors are boring. They are basically text boxes with a side-by-side preview. Boring, static, and often a distraction.

I wanted something more. I wanted a Writing Studio, not just an editor.

That’s why I built Markdown Weaver, and today, I'm thrilled to announce v1.3.0—the most ambitious update yet. We’ve turned "just another editor" into a visual brainstorming and presentation powerhouse.

Hey everyone! 👋

A while back, I teased a project I was building while procrastinating on my developer analytics tool. Well, that "side project" just grew up.

Markdown Weaver v1.3.0 is finally here, and it changes the game for anyone who lives in markdown. This isn't just about bold text and headers; it's about visualizing your thoughts and presenting your ideas instantly.

Here is why you might want to ditch your current markdown workflow.

1. 🧠 Visualizing Your Thoughts (Mind Map Mode)

Most markdown editors let you see your text. Markdown Weaver lets you understand its structure.

With one click, your document transforms into an Interactive Mind Map. Powered by Markmap, it takes your headers and turns them into a node-based visual hierarchy.

  • Brainstorming: See the gaps in your logic visually.
  • Navigation: Click a node to jump to that section in the editor.
  • Export: Grab it as an SVG and put it in your presentations or documentation.

2. 📺 From Notes to Slides in 0.5 Seconds

Writing slides is a chore. Formatting them is even worse.

In v1.3.0, we’ve introduced Presentation Mode. Just write your content, use --- to separate your ideas, and hit play.

  • Instant Slides: Beautiful, responsive, and clean.
  • AI-Slide Logic: Let the integrated AI suggest slide breaks and summaries for you.
  • Productivity: Zero time spent on PPT or Google Slides. Just write and present.

3. ⌨️ The Command Palette (Power User Heaven)

If you love VS Code or Obsidian, you'll feel right at home. Press Ctrl+K (or Cmd+K) to open the Command Palette.

  • Instant Search: Find any document by title or content instantly.
  • Fast Actions: Toggle dark mode, export to PDF, or switch views without ever touching your mouse.
  • Universal Search: It’s the "Magic Bar" for your entire writing workspace.

4. ✨ The "Magic Menu" (AI Assistant That Actually Works)

We didn't just slap a chatbot in the sidebar. We integrated AI into the Magic Menu.

  1. Highlight some text.
  2. Watch the floating menu appear.
  3. Choose: Fix Grammar, Improve Clarity, Change Tone, or Continue Writing.

It’s like having an editor sitting next to you, helping you polish your work as you type.

5. 🏠 A Workspace That Grows With You

The new Project Dashboard and Welcome Experience make managing 50+ documents a breeze.

  • Trash System: Deleted a masterpiece? Restore it from the trash.
  • Statistics: Track your writing streaks and Wordcounts.
  • Templates: Start with battle-tested templates for READMEs, Blog Posts, and Meeting Notes.

🛠️ Technical Polish (v1.3.0 Highlights)

Under the hood, we’ve moved to TypeScript 5.8 and a more robust Project-based Storage System. This means:

  • Real-time Cross-tab Sync: Open five tabs, edit in one, see it in all.
  • Drag & Drop: Drop any .md file to import it instantly.
  • Offline First: Your data stays local, private, and fast.

Ready to Level Up Your Writing?

Markdown Weaver is open-source, free, and built for people who love the simplicity of markdown but crave the power of a modern studio.

Check it out and let's make writing fun again!

🔗 Live Demo: https://markdown-weaver.netlify.app/

📦 Source Code: GitHub Repo

Happy writing! 📝

Transformers - Encoder Deep Dive - Part 2

2026-02-16 16:33:53

In our journey so far, we have explored the high-level intuition of why Transformers exist and mapped out the blueprint and notations in Part 1.

Wait... What exactly is the Encoder?

Before we prep the ingredients, let's look at the "Chef."

In the Transformer diagram, the Encoder is the tower on the left.

Visualisation explain about transformers architecture

If you think of a Transformer as a translation system:

  • The Encoder is the "Scholar" who reads the English sentence and understands every hidden nuance, relationship, and bit of context.
  • The Decoder is the "Writer" who takes that scholar's notes and writes the sentence out in French.

Where is it used?

While the original paper used both towers for translation, the tech world realized the Encoder is a powerhouse on its own. Below are few samples,

  • Search Engines: To understand the intent behind your query, not just the keywords.
  • Sentiment Analysis: To "feel" if a product review is happy or angry.
  • Text Classification: To automatically sort your emails into "Spam" or "Work."

In short: The Encoder's sole job is to turn a human sentence into a "Context-Rich" mathematical map.

Let's learn how to build this map

1. Encoder Input: Why does the Encoder need (Seq, d_model)?

As we discussed in Part 1, the hardware (GPU) is built for speed. It doesn't want to read a sentence word-by-word. It wants a Matrix.

Specifically, the Encoder requires a matrix of shape (Seq, d_model).

  • Seq (Sequence Length): The number of words (e.g., 5 for "The dog bit the man").
  • d_model (Model Dimension): The "width" of our mathematical understanding (usually 512).

The Encoder's job is to understand and refine this matrix.

let's learn how to feed input to encoder in this structure, Meaning (Embedding) and Order (Positional Encoding).

2. Input Embedding: Giving Words a Digital Identity

A computer doesn't know what a "dog" is. To a machine, "dog" is just a string of bits. Input Embedding is the process of giving that word a "Semantic Passport."

How is this vector actually generated?

To understand how we get our (Seq, d_model) matrix, let's follow the word "dog" through the three-step mechanical process we teased in Part 1:

Step 1: The ID Lookup One-Hot Vector

First, the model looks up "dog" in its dictionary (Vocabulary) and finds its unique ID (e.g., ID 432). It creates a One-Hot Vector: a massive list of zeros with a single 1 at position 432.

Recall from Part 1: This is a "Sparse" representation. It's huge, mostly empty, and contains zero meaning.

Step 2: The Embedding Matrix

This is where the magic happens. The model maintains a giant Embedding Matrix of size (Vocab_Size, d_model).

  • Each row in this matrix is a 512-dimension vector.
  • Initially, these numbers are random nonsense.

Step 3: The Row Selection

The model uses the ID from Step 1 to select exactly one row from this matrix. This row is our d_model vector. We have now successfully converted a "Sparse" ID into a "Dense" list of numbers.

Visualisation explaining embedding vector

The Analogy: The Semantic Passport

Imagine every word in our sentence, "The dog bit the man," gets a passport with 512 pages (our d_model). Each page describes a feature that the model has learned:

  • Page 1 (Noun-ness): High value for "dog", low for "bit".
  • Page 2 (Living): High value for "dog" and "man".
  • Page 3 (Animal): High value for "dog", low for "man".

💡 Important: These Values Change!

Unlike a fixed dictionary, these embedding values are learnable weights.

  • Why? At the start of training, the model's "understanding" is random. As it reads millions of sentences, it uses backpropagation to adjust these numbers.
  • The Goal: It learns that "dog" and "wolf" often appear in similar contexts (near words like "bark", "forest", or "pack"). To satisfy the math, it moves their 512-dimension coordinates closer together in space.
  • The Result: The embedding vector evolves as the model gets "smarter."

3. The Parallelism Paradox

In Part 1, we praised Transformers for their Parallelism. Unlike the "Drunken Narrator" (RNN), the Transformer looks at the entire input matrix at the same time.

The Paradox: If you look at every word simultaneously, you lose the sense of time.
To a Transformer, these two sentences look identical because the words are the same:

  1. "The dog bit the man."
  2. "The man bit the dog."

We have Meaning from our Embeddings, but we are missing Order.

4. Positional Encoding: The "Home Address"

To solve this, we need to "stamp" a position onto our embeddings. This tells the model where each word is standing in line.

The Formulas (How to calculate)

For an index i in our 512-dimensional vector:

  • For Even Steps (2i): PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
  • For Odd Steps (2i+1): PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

Visulaisation about positonal encoding calculation

Why Sine and Cosine?

The researchers used Sine and Cosine waves because they allow the model to understand relative positions. Because these functions oscillate, the model can easily learn that "word A is specific distance away from word B."

> Using waves to create a unique mathematical "signature" for every seat in the sentence.

Important: These Values are CONSTANT

Unlike Embeddings, Positional Encodings are fixed.

  • Why? The "meaning" of a word like "dog" might change as the model learns, but the "meaning" of Position #2 should never change. Position #2 is always Position #2. By keeping this constant, the model has a stable "grid" to map its learnable meanings onto.

5. Why do we ADD them? (Meaning + Order)

The researchers chose to ADD the Positional Encoding vector directly to the Input Embedding vector.

> Final Vector = Learnable Meaning + Constant Position

Why add instead of just attaching it to the end (concatenation)?

It keeps the matrix size at (Seq, d_model). We don't make the matrix "fatter," which keeps the hardware processing fast.

Visualisation explains why do we need to add input embedding with positional encoding

6. Summary: The Prepared Matrix

We have successfully prepared our ingredients. We started with raw text and ended with a refined (Seq, d_model) matrix where every word knows who it is and where it sits.

This matrix is finally ready to enter the first actual "box" of the Encoder.

Visualisation about the input matrix fed to encoder

What's Next?

Now that the input is prepared, it’s time to feed the Encoder.

In Part 3, we will explore Multi-Head Self-Attention. This is where the words actually start "talking" to each other using the Matrix Multiplication logic we teased in Part 1. We’ll see how the model decides which words are the most important in the sentence.

References

Stop Wrestling with JSON-LD: Type-Safe Structured Data for Next.js

2026-02-16 16:31:48

Structured data is one of those things every developer knows they should implement, but few actually do. Why? Because writing JSON-LD by hand is error-prone, tedious, and easy to get wrong.
What if you could generate type-safe structured data with autocomplete, validation, and zero runtime overhead?

The Problem with JSON-LD

Here's what typical JSON-LD looks like:

{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "My Blog Post",
  "author": {
    "@type": "Person",
    "name": "Jane Doe"
  },
  "datePublished": "2026-02-10"
}

Looks simple, right? But try maintaining this across dozens of pages:

  • Did you forget the @context?
  • Is datePublished in the right format?
  • Did you typo Article as Aticle?
  • Is this even valid according to schema.org? These mistakes cost you search rankings and AI visibility. ## Enter Schema Sentry Schema Sentry is a type-safe library for generating JSON-LD structured data. It gives you: ✅ Full TypeScript autocomplete - No more guessing field names ✅ Compile-time validation - Catch errors before they reach production ✅ Zero runtime overhead - Everything happens at build time ✅ Works with both Pages Router and App Router - Your choice of Next.js patterns ✅ CI/CD validation - Automated checks in your pipeline ## Quick Example Instead of hand-writing JSON:
// ❌ Error-prone manual JSON
<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
    __html: JSON.stringify({
      "@context": "https://schema.org",
      "@type": "Article",
      "headline": "My Post",
      // Did I get all required fields? Who knows!
    })
  }}
/>

Use type-safe builders:

import { Schema, Article } from "@schemasentry/next";
// ✅ Type-safe with autocomplete
const article = Article({
  headline: "My Post",
  authorName: "Jane Doe",
  datePublished: "2026-02-10",
  url: "https://example.com/blog/post",
});
export default function Page() {
  return <Schema data={article} />;
}

Same output, zero guesswork.

Pages Router vs App Router

Good news: Schema Sentry works identically with both Next.js routing patterns.

App Router (Next.js 13+)

// app/blog/[slug]/page.tsx
import { Schema, Article, BreadcrumbList } from "@schemasentry/next";
export default function BlogPost({ params }: { params: { slug: string } }) {
  const article = Article({
    headline: "Getting Started",
    authorName: "Jane Doe",
    datePublished: "2026-02-10",
    url: `https://example.com/blog/${params.slug}`,
  });
  const breadcrumbs = BreadcrumbList({
    items: [
      { name: "Home", url: "https://example.com" },
      { name: "Blog", url: "https://example.com/blog" },
    ],
  });
  return (
    <>
      <Schema data={[article, breadcrumbs]} />
      <article>{/* content */}</article>
    </>
  );
}

Pages Router (Classic Next.js)

// pages/blog/[slug].tsx
import Head from "next/head";
import { Schema, Article, BreadcrumbList } from "@schemasentry/next";
export default function BlogPost() {
  const article = Article({
    headline: "Getting Started",
    authorName: "Jane Doe",
    datePublished: "2026-02-10",
    url: "https://example.com/blog/post",
  });
  const breadcrumbs = BreadcrumbList({
    items: [
      { name: "Home", url: "https://example.com" },
      { name: "Blog", url: "https://example.com/blog" },
    ],
  });
  return (
    <>
      <Head>
        <title>Getting Started - My Blog</title>
      </Head>
      <Schema data={[article, breadcrumbs]} />
      <article>{/* content */}</article>
    </>
  );
}

The only difference? App Router doesn't need next/head because it supports native metadata.

Supported Schema Types

Schema Sentry includes builders for the most common types:

  • Organization - Company/publisher info
  • WebSite - Site metadata
  • Article / BlogPosting - Blog posts and articles
  • Product - E-commerce products with offers
  • FAQPage - FAQ sections with expandable questions
  • HowTo - Step-by-step guides
  • BreadcrumbList - Navigation breadcrumbs
  • Person - Author/contributor info ## CI/CD Validation Here's where it gets powerful. Schema Sentry includes a CLI for automated validation:
// schema-sentry.manifest.json
{
  "routes": {
    "/": ["Organization", "WebSite"],
    "/blog": ["WebSite"],
    "/blog/getting-started": ["Article"],
    "/products/widget": ["Organization", "Product"],
    "/faq": ["FAQPage", "Organization"]
  }
}

Add to your GitHub Actions:

# .github/workflows/schema-check.yml
name: Schema Validation
on: [push, pull_request]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v2
      - run: pnpm install
      - run: pnpm build
      - run: pnpm schemasentry validate

Now your CI will fail if:

  • A page is missing expected schema
  • Schema types don't match the manifest
  • Invalid JSON-LD is generated Never ship broken structured data again. ## Why Structured Data Matters ### For Traditional SEO Google uses JSON-LD to create rich snippets in search results:
  • Article headlines and publication dates
  • Product prices and ratings
  • FAQ dropdowns
  • Breadcrumb navigation These rich results get higher click-through rates than plain blue links. ### For AI Discovery This is the big one that most developers are missing. AI systems like:
  • ChatGPT
  • Claude
  • Perplexity
  • Google Bard ...all use structured data to understand and cite content. Without proper schema, your content is invisible to the AI discovery layer. ## Getting Started
# Install packages
npm install @schemasentry/next
npm install -D @schemasentry/cli
# Or with pnpm
pnpm add @schemasentry/next
pnpm add -D @schemasentry/cli

Create your first schema:

import { Schema, Organization } from "@schemasentry/next";
const org = Organization({
  name: "My Company",
  url: "https://example.com",
  logo: "https://example.com/logo.png",
  description: "We build amazing things",
});
export default function Home() {
  return (
    <>
      <Schema data={org} />
      <h1>Welcome</h1>
    </>
  );
}

That's it! The <Schema /> component injects the JSON-LD into your page's <head> automatically.

Live Examples

Check out the complete working examples:

  • App Router Example - Modern Next.js 13+ patterns
  • Pages Router Example - Classic Next.js patterns Both demonstrate the same UI with all 11+ schema types. ## Summary Adding structured data doesn't have to be painful:
  • Type safety catches errors at compile time
  • Autocomplete guides you to the right fields
  • CI validation prevents broken schema from shipping
  • Zero runtime cost - everything is static
  • Works with any Next.js routing pattern Your future self (and your search rankings) will thank you. --- Have you implemented structured data on your Next.js site? What challenges did you face? Schema Sentry is open source under MIT license. Star us on GitHub if you found this helpful!