<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Web Development - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Web Development - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 12 Jul 2026 08:54:46 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/web-development/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Implement Role-Based Access Control in a Node.js REST API with JWT ]]>
                </title>
                <description>
                    <![CDATA[ The first time I built an API without thinking about roles, I gave every logged-in user the same access. It worked fine until a regular user accidentally hit a delete endpoint and wiped test data. Tha ]]>
                </description>
                <link>https://www.freecodecamp.org/news/role-based-access-control-nodejs-rest-api-jwt/</link>
                <guid isPermaLink="false">6a4fb4570140649a4367b476</guid>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ api ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Zia Ullah ]]>
                </dc:creator>
                <pubDate>Thu, 09 Jul 2026 14:46:47 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/d742efbd-8170-4fb6-8851-1f7c6ef9125e.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>The first time I built an API without thinking about roles, I gave every logged-in user the same access. It worked fine until a regular user accidentally hit a delete endpoint and wiped test data. That was the day I actually sat down and learned RBAC properly.</p>
<p>Role-Based Access Control sounds fancy, but the idea is simple: what you can do depends on <em>who you are</em>, not just <em>that you're logged in</em>. An admin deletes users. An editor creates posts. A regular user just reads. Same app, completely different experience depending on who's asking.</p>
<p>That's what we're building here. A REST API with three roles: JWT to carry those roles on every request, and a pair of middleware functions that check permissions before your route handlers even run. There's no database hit per request, and no if/else soup in your business logic.</p>
<p>By the end, you'll have three working roles (<code>admin</code>, <code>editor</code>, <code>user</code>) each locked to their own endpoints. More importantly, the pattern is transferable: once it clicks, you'll wire it into your next project without needing a tutorial.</p>
<p><strong>Full source code on GitHub:</strong> <a href="https://github.com/ziaongit/nodejs-rbac-jwt-api">github.com/ziaongit/nodejs-rbac-jwt-api</a></p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-youll-learn">What You'll Learn</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-well-build">What We'll Build</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-setting-up-the-in-memory-data-store">Setting Up the In-Memory Data Store</a></p>
</li>
<li><p><a href="#heading-building-the-auth-routes">Building the Auth Routes</a></p>
</li>
<li><p><a href="#heading-building-the-rbac-middleware">Building the RBAC Middleware</a></p>
</li>
<li><p><a href="#heading-building-the-protected-routes">Building the Protected Routes</a></p>
</li>
<li><p><a href="#heading-putting-it-all-together">Putting It All Together</a></p>
</li>
<li><p><a href="#heading-testing-the-api">Testing the API</a></p>
</li>
<li><p><a href="#heading-key-takeaways">Key Takeaways</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-youll-learn">What You'll Learn</h2>
<ul>
<li><p>What RBAC is and how it differs from basic authentication</p>
</li>
<li><p>How to embed roles in JWT payloads</p>
</li>
<li><p>How to write reusable Express middleware for token verification and role checking</p>
</li>
<li><p>How to protect API routes based on user roles</p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>Node.js (v18+) installed</p>
</li>
<li><p>Basic knowledge of Express.js</p>
</li>
<li><p>Familiarity with how JWTs work (we'll cover the relevant parts)</p>
</li>
<li><p>npm installed</p>
</li>
</ul>
<h2 id="heading-what-well-build">What We'll Build</h2>
<p>We'll build a REST API for a simple content management system with three user roles:</p>
<table>
<thead>
<tr>
<th>Role</th>
<th>Permissions</th>
</tr>
</thead>
<tbody><tr>
<td><code>user</code></td>
<td>Read content</td>
</tr>
<tr>
<td><code>editor</code></td>
<td>Read + create content</td>
</tr>
<tr>
<td><code>admin</code></td>
<td>Full access — read, create, delete content, manage users</td>
</tr>
</tbody></table>
<p>The API will expose these endpoints:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Endpoint</th>
<th>Access</th>
</tr>
</thead>
<tbody><tr>
<td>POST</td>
<td>/api/auth/register</td>
<td>Public</td>
</tr>
<tr>
<td>POST</td>
<td>/api/auth/login</td>
<td>Public</td>
</tr>
<tr>
<td>GET</td>
<td>/api/content</td>
<td>user, editor, admin</td>
</tr>
<tr>
<td>POST</td>
<td>/api/content</td>
<td>editor, admin</td>
</tr>
<tr>
<td>DELETE</td>
<td>/api/content/:id</td>
<td>admin only</td>
</tr>
<tr>
<td>GET</td>
<td>/api/admin/users</td>
<td>admin only</td>
</tr>
</tbody></table>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Create a new folder and initialize the project:</p>
<pre><code class="language-bash">mkdir nodejs-rbac-jwt-api
cd nodejs-rbac-jwt-api
npm init -y
</code></pre>
<p>Install the dependencies:</p>
<pre><code class="language-bash">npm install express jsonwebtoken bcryptjs dotenv
npm install --save-dev nodemon
</code></pre>
<p>Here's what each package does:</p>
<ul>
<li><p><strong>express</strong>: web framework for building the API</p>
</li>
<li><p><strong>jsonwebtoken</strong>: creates and verifies JWTs</p>
</li>
<li><p><strong>bcryptjs</strong>: securely hashes passwords</p>
</li>
<li><p><strong>dotenv</strong>: reads your <code>.env</code> file so you're not hardcoding secrets in your source code</p>
</li>
</ul>
<p>Update <code>package.json</code> to add start scripts:</p>
<pre><code class="language-json">"scripts": {
  "start": "node src/app.js",
  "dev": "nodemon src/app.js"
}
</code></pre>
<p>Create the project structure:</p>
<pre><code class="language-plaintext">nodejs-rbac-jwt-api/
├── src/
│   ├── middleware/
│   │   └── auth.js
│   ├── routes/
│   │   ├── auth.js
│   │   ├── content.js
│   │   └── admin.js
│   ├── data/
│   │   └── users.js
│   └── app.js
├── .env
├── .env.example
└── package.json
</code></pre>
<p>Create your <code>.env</code> file:</p>
<pre><code class="language-plaintext">JWT_SECRET=your_super_secret_key_change_this_in_production
PORT=3000
</code></pre>
<p><strong>Important:</strong> Never commit your <code>.env</code> file to version control. Add it to <code>.gitignore</code>.</p>
<h2 id="heading-setting-up-the-in-memory-data-store">Setting Up the In-Memory Data Store</h2>
<p>We don't have a database here, just an array in memory. The point was to keep the focus on RBAC, not spend half the tutorial on database config. In a real project, swap the array for whatever database you're already using.</p>
<p>Create <code>src/data/users.js</code>:</p>
<pre><code class="language-javascript">// In-memory users store
// In production, replace this with a real database (MongoDB, PostgreSQL, etc.)
const users = [];

const findUserByEmail = (email) =&gt; users.find((u) =&gt; u.email === email);
const findUserById = (id) =&gt; users.find((u) =&gt; u.id === id);
const createUser = (user) =&gt; {
  users.push(user);
  return user;
};
const getAllUsers = () =&gt; users.map(({ password, ...user }) =&gt; user);

module.exports = { findUserByEmail, findUserById, createUser, getAllUsers };
</code></pre>
<p>One thing worth noting: <code>getAllUsers</code> uses destructuring to drop the password before returning anything. Never send password fields in API responses, even hashed ones.</p>
<h2 id="heading-building-the-auth-routes">Building the Auth Routes</h2>
<p>The auth routes handle registration and login. Login is where roles first enter the picture — we embed the user's role directly into the JWT payload.</p>
<p>Create <code>src/routes/auth.js</code>:</p>
<pre><code class="language-javascript">const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { findUserByEmail, createUser } = require('../data/users');

const router = express.Router();

// POST /api/auth/register
router.post('/register', async (req, res) =&gt; {
  const { name, email, password, role } = req.body;

  if (!name || !email || !password) {
    return res.status(400).json({ message: 'Name, email, and password are required' });
  }

  if (findUserByEmail(email)) {
    return res.status(409).json({ message: 'Email already registered' });
  }

  // Only allow valid roles — default to 'user' if none provided
  const validRoles = ['user', 'editor', 'admin'];
  const assignedRole = validRoles.includes(role) ? role : 'user';

  const hashedPassword = await bcrypt.hash(password, 10);

  const newUser = {
    id: Date.now().toString(),
    name,
    email,
    password: hashedPassword,
    role: assignedRole,
  };

  createUser(newUser);

  res.status(201).json({
    message: 'User registered successfully',
    user: {
      id: newUser.id,
      name: newUser.name,
      email: newUser.email,
      role: newUser.role,
    },
  });
});

// POST /api/auth/login
router.post('/login', async (req, res) =&gt; {
  const { email, password } = req.body;

  if (!email || !password) {
    return res.status(400).json({ message: 'Email and password are required' });
  }

  const user = findUserByEmail(email);
  if (!user) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }

  const isMatch = await bcrypt.compare(password, user.password);
  if (!isMatch) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }

  // Issue JWT — embed role in the payload
  const token = jwt.sign(
    {
      id: user.id,
      email: user.email,
      role: user.role,   // ← This is the key part for RBAC
    },
    process.env.JWT_SECRET,
    { expiresIn: '24h' }
  );

  res.json({
    message: 'Login successful',
    token,
  });
});

module.exports = router;
</code></pre>
<p>The most important line is the JWT payload:</p>
<pre><code class="language-javascript">jwt.sign({ id, email, role }, process.env.JWT_SECRET, { expiresIn: '24h' })
</code></pre>
<p>By embedding <code>role</code> in the token, every subsequent request carries the user's permissions without requiring a database lookup. The server just verifies the token and reads the role from the payload.</p>
<h2 id="heading-building-the-rbac-middleware">Building the RBAC Middleware</h2>
<p>This is the core of the system. We need two separate middleware functions:</p>
<ol>
<li><p><code>verifyToken</code> confirms the JWT is valid and attaches the decoded payload to <code>req.user</code></p>
</li>
<li><p><code>checkRole</code> confirms the user has the required role for a specific route</p>
</li>
</ol>
<p>Keeping them separate gives you flexibility. Some routes only need authentication. Others need both authentication and a specific role.</p>
<p>Create <code>src/middleware/auth.js</code>:</p>
<pre><code class="language-javascript">const jwt = require('jsonwebtoken');

// Middleware 1: Verify the JWT token
const verifyToken = (req, res, next) =&gt; {
  const authHeader = req.headers['authorization'];
  const token = authHeader &amp;&amp; authHeader.split(' ')[1]; // Expects: Bearer &lt;token&gt;

  if (!token) {
    return res.status(401).json({ message: 'Access denied. No token provided.' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded; // Attach decoded payload (including role) to request
    next();
  } catch (err) {
    return res.status(403).json({ message: 'Invalid or expired token.' });
  }
};

// Middleware 2: Check if user has one of the required roles
const checkRole = (...allowedRoles) =&gt; {
  return (req, res, next) =&gt; {
    if (!req.user) {
      return res.status(401).json({ message: 'Not authenticated.' });
    }

    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({
        message: `Access denied. Required role: ${allowedRoles.join(' or ')}. Your role: ${req.user.role}`,
      });
    }

    next();
  };
};

module.exports = { verifyToken, checkRole };
</code></pre>
<p><code>checkRole</code> uses a rest parameter (<code>...allowedRoles</code>) so you can pass in one or multiple roles:</p>
<pre><code class="language-javascript">checkRole('admin')                  // only admin
checkRole('editor', 'admin')        // editor or admin
checkRole('user', 'editor', 'admin') // all roles
</code></pre>
<p>This makes route definitions clean and readable — the permissions are visible right at the route level.</p>
<h2 id="heading-building-the-protected-routes">Building the Protected Routes</h2>
<p>Now let's wire up routes that use the middleware.</p>
<p>Create <code>src/routes/content.js</code>:</p>
<pre><code class="language-javascript">const express = require('express');
const { verifyToken, checkRole } = require('../middleware/auth');

const router = express.Router();

// In-memory content store
const content = [
  { id: '1', title: 'Getting Started with Node.js', author: 'admin' },
  { id: '2', title: 'Express Middleware Explained', author: 'editor' },
];

// GET /api/content — all authenticated users
router.get('/', verifyToken, checkRole('user', 'editor', 'admin'), (req, res) =&gt; {
  res.json({ content });
});

// POST /api/content — editors and admins only
router.post('/', verifyToken, checkRole('editor', 'admin'), (req, res) =&gt; {
  const { title } = req.body;

  if (!title) {
    return res.status(400).json({ message: 'Title is required' });
  }

  const newItem = {
    id: Date.now().toString(),
    title,
    author: req.user.email,
  };

  content.push(newItem);
  res.status(201).json({ message: 'Content created', item: newItem });
});

// DELETE /api/content/:id — admin only
router.delete('/:id', verifyToken, checkRole('admin'), (req, res) =&gt; {
  const index = content.findIndex((c) =&gt; c.id === req.params.id);

  if (index === -1) {
    return res.status(404).json({ message: 'Content not found' });
  }

  content.splice(index, 1);
  res.json({ message: 'Content deleted successfully' });
});

module.exports = router;
</code></pre>
<p>Notice how readable each route is:</p>
<pre><code class="language-javascript">router.delete('/:id', verifyToken, checkRole('admin'), handler)
</code></pre>
<p>You can understand the access control without reading the handler body. This is one of the key advantages of middleware-based RBAC: permissions live at the routing layer, not buried in business logic.</p>
<p>Create <code>src/routes/admin.js</code>:</p>
<pre><code class="language-javascript">const express = require('express');
const { verifyToken, checkRole } = require('../middleware/auth');
const { getAllUsers } = require('../data/users');

const router = express.Router();

// GET /api/admin/users — admin only
router.get('/users', verifyToken, checkRole('admin'), (req, res) =&gt; {
  res.json({ users: getAllUsers() });
});

module.exports = router;
</code></pre>
<h2 id="heading-putting-it-all-together">Putting It All Together</h2>
<p>Create <code>src/app.js</code>:</p>
<pre><code class="language-javascript">require('dotenv').config();
const express = require('express');

const authRoutes = require('./routes/auth');
const contentRoutes = require('./routes/content');
const adminRoutes = require('./routes/admin');

const app = express();

app.use(express.json());

// Routes
app.use('/api/auth', authRoutes);
app.use('/api/content', contentRoutes);
app.use('/api/admin', adminRoutes);

// Health check
app.get('/', (req, res) =&gt; {
  res.json({ message: 'RBAC API is running' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () =&gt; {
  console.log(`Server running on port ${PORT}`);
});
</code></pre>
<h2 id="heading-testing-the-api">Testing the API</h2>
<p>Start the server:</p>
<pre><code class="language-bash">npm run dev
</code></pre>
<h3 id="heading-step-1-register-users-with-different-roles">Step 1: Register Users with Different Roles</h3>
<p>Register an admin:</p>
<pre><code class="language-bash">curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"name": "Admin User", "email": "admin@example.com", "password": "password123", "role": "admin"}'
</code></pre>
<p>Register an editor:</p>
<pre><code class="language-bash">curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"name": "Editor User", "email": "editor@example.com", "password": "password123", "role": "editor"}'
</code></pre>
<p>Register a regular user (no role specified — defaults to <code>user</code>):</p>
<pre><code class="language-bash">curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"name": "Regular User", "email": "user@example.com", "password": "password123"}'
</code></pre>
<h3 id="heading-step-2-log-in-and-get-a-token">Step 2: Log in and Get a Token</h3>
<pre><code class="language-bash">curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "password": "password123"}'
</code></pre>
<p>You'll get a response like:</p>
<pre><code class="language-json">{
  "message": "Login successful",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
</code></pre>
<p>Copy the token.</p>
<h3 id="heading-step-3-test-role-based-access">Step 3: Test Role-based Access</h3>
<p><strong>Read content as a regular user (should succeed):</strong></p>
<pre><code class="language-bash">curl http://localhost:3000/api/content \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"
</code></pre>
<p><strong>Try creating content as a regular user (should fail — 403):</strong></p>
<pre><code class="language-bash">curl -X POST http://localhost:3000/api/content \
  -H "Authorization: Bearer YOUR_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{"title": "New Article"}'
</code></pre>
<p>Response:</p>
<pre><code class="language-json">{
  "message": "Access denied. Required role: editor or admin. Your role: user"
}
</code></pre>
<p>Now log in as an editor and try the same POST request. It succeeds. Log in as admin and try the DELETE route. Only the admin token will work.</p>
<h3 id="heading-step-4-decode-the-jwt-to-see-the-role">Step 4: Decode the JWT to See the Role</h3>
<p>You can paste any token into <a href="https://jwt.io">jwt.io</a> to inspect the payload. You'll see something like:</p>
<pre><code class="language-json">{
  "id": "1720300000000",
  "email": "admin@example.com",
  "role": "admin",
  "iat": 1720300000,
  "exp": 1720386400
}
</code></pre>
<p>The <code>role</code> field is exactly what <code>checkRole</code> reads on every protected request.</p>
<h2 id="heading-key-takeaways">Key Takeaways</h2>
<p>Roles live in the JWT payload. The role travels with the token — no extra DB call needed every time someone hits a protected route. It gets embedded at login and verified cryptographically on each request.</p>
<p>Middleware is composable. <code>verifyToken</code> and <code>checkRole</code> are separate, reusable functions. You can chain them on any route in any combination.</p>
<p>Permissions are visible at the route level. <code>router.delete('/:id', verifyToken, checkRole('admin'), handler)</code> tells you everything about access control before you even read the handler.</p>
<p><strong>Before you ship this to production:</strong></p>
<ul>
<li><p>The in-memory array was just to keep this tutorial focused — replace it with a real database before anything goes near production. A server restart wipes all your users right now.</p>
</li>
<li><p>That 24h token expiry is too long. Cut it to 15 minutes and add refresh token rotation. A stolen token becomes useless fast.</p>
</li>
<li><p>Re-validate roles from the DB on sensitive operations. A role change won't reflect in an existing token until it expires</p>
</li>
<li><p>HTTPS, always</p>
</li>
<li><p>If your permission logic grows beyond "check a role", look at <a href="https://casl.js.org/">casl</a>. It handles attribute-level rules cleanly</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The core of it fits in two middleware functions and a JWT payload. I've used this same pattern across several projects. And once you've built it yourself, you'll start spotting it everywhere, because almost every multi-user app needs some version of it.</p>
<p><strong>Full source code on GitHub:</strong> <a href="https://github.com/ziaongit/nodejs-rbac-jwt-api">github.com/ziaongit/nodejs-rbac-jwt-api</a></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Browser-Based PDF OCR to Text Converter Using JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ Not every PDF contains searchable or editable text. Many PDFs are simply scanned images of documents such as invoices, contracts, books, receipts, government forms, and handwritten notes. While these  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-pdf-ocr-to-text-converter-javascript/</link>
                <guid isPermaLink="false">6a4d27fa9720ba8235700935</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf ]]>
                    </category>
                
                    <category>
                        <![CDATA[ OCR  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf to text ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Bhavin Sheth ]]>
                </dc:creator>
                <pubDate>Tue, 07 Jul 2026 16:23:22 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/ba3a97e6-1829-4062-acef-9d05eaa14c34.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Not every PDF contains searchable or editable text. Many PDFs are simply scanned images of documents such as invoices, contracts, books, receipts, government forms, and handwritten notes.</p>
<p>While these documents are easy to read, copying, searching, or editing their content isn't possible without additional processing.</p>
<p>This is where <strong>Optical Character Recognition (OCR)</strong> comes in. OCR recognizes text inside scanned images and converts it into editable, searchable digital text.</p>
<p>In this tutorial, you'll build a browser-based <strong>PDF OCR to Text Converter</strong> using JavaScript. Users will be able to upload PDF files, preview pages, configure OCR settings, extract text, monitor processing progress, review OCR confidence scores, and export the results – all directly inside the browser.</p>
<p>Since everything runs locally, uploaded documents never leave the user's device, making the tool both fast and privacy-friendly.</p>
<p>By the end of this tutorial, you'll understand how browser-based OCR works and how to build your own PDF-to-text converter using JavaScript.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-why-pdf-ocr-is-useful">Why PDF OCR Is Useful</a></p>
</li>
<li><p><a href="#heading-how-pdf-ocr-works">How PDF OCR Works</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-what-libraries-are-we-using">What Libraries Are We Using?</a></p>
</li>
<li><p><a href="#heading-creating-the-upload-interface">Creating the Upload Interface</a></p>
</li>
<li><p><a href="#heading-previewing-uploaded-pdf-pages">Previewing Uploaded PDF Pages</a></p>
</li>
<li><p><a href="#heading-configuring-ocr-settings">Configuring OCR Settings</a></p>
</li>
<li><p><a href="#heading-extracting-text-from-the-pdf">Extracting Text from the PDF</a></p>
</li>
<li><p><a href="#heading-tracking-ocr-progress">Tracking OCR Progress</a></p>
</li>
<li><p><a href="#heading-understanding-ocr-confidence-scores">Understanding OCR Confidence Scores</a></p>
</li>
<li><p><a href="#heading-reviewing-the-extracted-text">Reviewing the Extracted Text</a></p>
</li>
<li><p><a href="#heading-exporting-the-ocr-results">Exporting the OCR Results</a></p>
</li>
<li><p><a href="#heading-demo-how-the-pdf-ocr-tool-works">Demo: How the PDF OCR Tool Works</a></p>
</li>
<li><p><a href="#heading-performance-optimization-tips">Performance Optimization Tips</a></p>
</li>
<li><p><a href="#heading-important-notes-from-real-world-use">Important Notes from Real-World Use</a></p>
</li>
<li><p><a href="#heading-common-mistakes-to-avoid">Common Mistakes to Avoid</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-why-pdf-ocr-is-useful">Why PDF OCR Is Useful</h2>
<p>Many PDF files are scanned documents rather than digital text. Although they look readable, the text is actually stored as images, making it impossible to search, copy, edit, or analyze the content.</p>
<p>OCR (Optical Character Recognition) solves this problem by recognizing characters from scanned pages and converting them into editable, searchable text. Once the text is extracted, it can be copied, translated, indexed, summarized, or imported into other applications.</p>
<p>OCR is widely used across many industries. Businesses use it to process invoices, purchase orders, receipts, contracts, bank statements, and tax documents without manually entering data. Legal professionals use OCR to search agreements, affidavits, and court documents for names, dates, or specific clauses. Government agencies digitize historical records, application forms, passports, and official documents to build searchable digital archives.</p>
<p>Educational institutions convert scanned books, research papers, lecture notes, and examination materials into searchable text, making learning resources easier to access. Healthcare organizations use OCR to digitize prescriptions, laboratory reports, insurance claims, and patient records, reducing paperwork and improving record management.</p>
<p>OCR is also valuable for e-commerce businesses. Sellers handling hundreds of invoices, shipping labels, and purchase orders from platforms such as Amazon, Flipkart, Meesho, or Shopify can quickly extract order numbers, customer details, addresses, and product information instead of typing everything manually.</p>
<p>Developers use OCR when building document management systems, enterprise search tools, AI assistants, and workflow automation platforms where scanned documents need to become searchable digital content.</p>
<p>Since this application performs OCR entirely inside the browser, users can process confidential documents without uploading them to external servers. This keeps document processing fast, private, and secure while making scanned PDFs much more useful.</p>
<h2 id="heading-how-pdf-ocr-works">How PDF OCR Works</h2>
<p>A PDF OCR application converts scanned pages into editable text by combining PDF rendering with Optical Character Recognition.</p>
<p>When a user uploads a PDF, the browser first validates the document and loads it into memory. Each page is then rendered as an image using PDF.js. These rendered page images become the input for the OCR engine.</p>
<p>The OCR engine examines every image pixel by pixel. It identifies printed characters, recognizes words and sentences, and reconstructs the document as digital text. Depending on the selected language, the recognition engine applies language-specific dictionaries and character models to improve accuracy.</p>
<p>If the user enables image enhancement, the application can improve the scanned page before recognition. Converting the page to grayscale, increasing contrast, or sharpening the image often helps OCR detect characters more accurately, especially when working with old scans or low-quality photocopies.</p>
<p>As each page is processed, the application updates a progress indicator so users can monitor the extraction process in real time. The OCR engine also returns a confidence score for every page, allowing users to estimate how reliable the recognized text is.</p>
<p>After all selected pages have been processed, the application combines the extracted text into a single document. Users can review the output, copy it directly from the browser, or export it as a TXT or JSON file for further use.</p>
<p>Since every stage of the workflow runs locally, the uploaded PDF never leaves the user's device. This makes browser-based OCR an excellent solution for sensitive business documents, legal records, healthcare files, financial reports, and government paperwork.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>We'll build the PDF OCR application using standard web technologies.</p>
<p>Create the following project structure.</p>
<pre><code class="language-text">pdf-ocr-tool/

│── index.html

│── style.css

│── script.js
</code></pre>
<p>Next, include the required JavaScript libraries inside <strong>index.html</strong>.</p>
<pre><code class="language-html">&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.min.js"&gt;&lt;/script&gt;

&lt;script src="https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js"&gt;&lt;/script&gt;

&lt;script src="https://unpkg.com/pdf-lib"&gt;&lt;/script&gt;
</code></pre>
<p>These libraries provide everything needed to render PDF pages, recognize text, and manage PDF-related operations directly inside the browser.</p>
<h2 id="heading-what-libraries-are-we-using">What Libraries Are We Using?</h2>
<p>This project combines several JavaScript libraries because OCR involves multiple processing stages.</p>
<p>The primary library is <strong>PDF.js</strong>, which loads the uploaded PDF document and renders every page as an image inside the browser. Since OCR engines work with images rather than PDF files directly, rendering each page is the first step of the workflow.</p>
<p>The application uses <strong>Tesseract.js</strong> to perform Optical Character Recognition. Tesseract is one of the most popular open-source OCR engines and supports dozens of languages, making it possible to recognize printed text from scanned documents without relying on any external API or cloud service.</p>
<p>We also include <strong>PDF-lib</strong>, which helps manage PDF-related operations and provides additional flexibility if future features such as annotations, metadata editing, or document modifications are added.</p>
<p>Together, these libraries create a complete browser-based OCR solution capable of rendering PDF pages, recognizing printed text, tracking recognition progress, reporting confidence scores, and exporting the extracted text while keeping every document private on the user's device.</p>
<h2 id="heading-creating-the-upload-interface">Creating the Upload Interface</h2>
<p>Every OCR workflow begins with selecting a PDF document. Before the application can recognize any text, it must first load the PDF into the browser and verify that it's a supported file type.</p>
<p>A good upload interface should be simple, intuitive, and accessible for both desktop and mobile users. Supporting drag-and-drop uploads alongside the traditional file picker gives users multiple ways to import their documents.</p>
<p>In this project, the upload section serves as the starting point for the entire OCR workflow. After a PDF is selected, the browser validates the file, reads it into memory, and prepares it for page rendering. Since the application runs completely inside the browser, no document is uploaded to an external server. This ensures confidential PDFs remain private throughout the OCR process.</p>
<p>The upload interface also provides clear instructions so users immediately understand how to begin using the tool.</p>
<p>Here's the HTML for the upload area:</p>
<pre><code class="language-html">&lt;div class="upload-container"&gt;

    &lt;div id="dropZone" class="drop-zone"&gt;

        &lt;div class="upload-icon"&gt;
            ☁
        &lt;/div&gt;

        &lt;h2&gt;Drag &amp; Drop PDF Here&lt;/h2&gt;

        &lt;p&gt;Or click to browse file&lt;/p&gt;

        &lt;button id="selectPDF"&gt;

            Select PDF

        &lt;/button&gt;

        &lt;input

            type="file"

            id="pdfInput"

            accept="application/pdf"

            hidden&gt;

    &lt;/div&gt;

&lt;/div&gt;
</code></pre>
<p>Next, validate the uploaded file before loading it.</p>
<pre><code class="language-javascript">const pdfInput = document.getElementById("pdfInput");

pdfInput.addEventListener("change", async (event)=&gt;{

    const file = event.target.files[0];

    if(!file) return;

    if(file.type !== "application/pdf"){

        alert("Please upload a valid PDF file.");

        return;

    }

    loadPDF(file);

});
</code></pre>
<p>Once the validation succeeds, the PDF is loaded into memory and the application proceeds to generate preview thumbnails for each page.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/c47b97f2-6e5f-421d-90c6-0dd34e3440ed.png" alt="PDF upload interface allowing users to drag and drop or browse for a PDF document before OCR processing." style="display:block;margin:0 auto" width="570" height="636" loading="lazy">

<h2 id="heading-previewing-uploaded-pdf-pages">Previewing Uploaded PDF Pages</h2>
<p>After the PDF has been loaded successfully, the application generates page previews.</p>
<p>Instead of immediately starting OCR, users first see thumbnail images for every page in the uploaded document. This allows them to confirm that the correct file has been selected and inspect the document before extraction begins.</p>
<p>The preview stage is especially useful for large PDFs because users can quickly identify scanned pages, blank pages, rotated pages, or incorrect uploads without wasting time running OCR on the wrong document.</p>
<p>PDF.js renders every page as a canvas before displaying it inside the preview grid.</p>
<p>First, load the PDF document.</p>
<pre><code class="language-javascript">const pdf = await pdfjsLib.getDocument({

    data: await file.arrayBuffer()

}).promise;
</code></pre>
<p>Next, render every page.</p>
<pre><code class="language-javascript">for(let pageNumber = 1; pageNumber &lt;= pdf.numPages; pageNumber++){

    const page = await pdf.getPage(pageNumber);

    const viewport = page.getViewport({

        scale:0.35

    });

    const canvas = document.createElement("canvas");

    const context = canvas.getContext("2d");

    canvas.width = viewport.width;

    canvas.height = viewport.height;

    await page.render({

        canvasContext:context,

        viewport

    }).promise;

    previewContainer.appendChild(canvas);

}
</code></pre>
<p>Each rendered page becomes a thumbnail, allowing users to scroll through the document before choosing the OCR settings.</p>
<p>This visual confirmation greatly reduces mistakes when processing long reports, contracts, invoices, books, or multi-page scanned documents.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/84da8fa8-372f-47b9-ad16-fef208211722.png" alt="Uploaded PDF preview displaying thumbnail images of every page before OCR processing begins." style="display:block;margin:0 auto" width="564" height="505" loading="lazy">

<h2 id="heading-configuring-ocr-settings">Configuring OCR Settings</h2>
<p>Different PDF documents require different OCR configurations. A clean digital scan usually processes very quickly, while old photocopies or low-quality scans often require additional image enhancement to improve recognition accuracy.</p>
<p>Before starting OCR, the application allows users to customize several options that affect how text is extracted.</p>
<p>Users can choose whether OCR should process every page or only a specific page range. This is particularly useful when working with large documents where only a few pages contain important information.</p>
<p>The OCR engine also supports multiple recognition languages. Selecting the correct language helps improve accuracy because Tesseract uses language-specific dictionaries and character models during recognition.</p>
<p>For users who prioritize speed, the Fast mode completes OCR quickly while still producing good results. When working with low-quality scans or official documents, High Accuracy mode performs additional processing to improve recognition quality.</p>
<p>The application also includes optional image enhancement settings. Converting pages to grayscale, increasing contrast, or sharpening the scanned image often improves OCR accuracy by making printed characters easier to recognize.</p>
<p>These configurable options allow the OCR engine to adapt to many different document types without overwhelming users with unnecessary complexity.</p>
<p>The page selection section allows users to process either the entire document or only selected pages.</p>
<pre><code class="language-html">&lt;input

type="radio"

name="pages"

value="all"

checked&gt;

All Pages

&lt;input

type="radio"

name="pages"

value="custom"&gt;

Specific Pages
</code></pre>
<p>Users can also choose the OCR language.</p>
<pre><code class="language-html">&lt;select id="language"&gt;

    &lt;option&gt;English&lt;/option&gt;

    &lt;option&gt;Hindi&lt;/option&gt;

    &lt;option&gt;Gujarati&lt;/option&gt;

    &lt;option&gt;Spanish&lt;/option&gt;

    &lt;option&gt;French&lt;/option&gt;

    &lt;option&gt;German&lt;/option&gt;

    &lt;option&gt;Chinese (Simplified)&lt;/option&gt;

&lt;/select&gt;
</code></pre>
<p>Next, configure the OCR accuracy mode.</p>
<pre><code class="language-javascript">const mode = document.querySelector(

'input[name="accuracy"]:checked'

).value;

console.log(mode);
</code></pre>
<p>Finally, enable optional image enhancement features before OCR begins.</p>
<pre><code class="language-javascript">const grayscale = grayscaleCheckbox.checked;

const contrast = contrastCheckbox.checked;

const sharpen = sharpenCheckbox.checked;

console.log(

grayscale,

contrast,

sharpen

);
</code></pre>
<p>These settings allow the application to balance processing speed and recognition quality depending on the type of PDF being analyzed.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/9e4287b5-bb0c-4ac3-a042-5e99ec37be07.png" alt="OCR settings showing page selection, language selection, accuracy mode, and image enhancement options." style="display:block;margin:0 auto" width="565" height="563" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/0335cedb-8dcd-4b8a-b26a-58e71b7f056f.png" alt="Language selection dropdown displaying supported OCR languages including English, Hindi, Gujarati, Spanish, French, German, and Chinese." style="display:block;margin:0 auto" width="526" height="292" loading="lazy">

<h3 id="heading-improving-ocr-accuracy-before-processing">Improving OCR Accuracy Before Processing</h3>
<p>One advantage of browser-based OCR is that the document can be optimized before recognition begins. Small image enhancements often have a significant impact on the quality of the extracted text.</p>
<p>For example, grayscale conversion removes unnecessary color information, allowing the OCR engine to focus only on character shapes. Increasing contrast helps distinguish text from the page background, while sharpening makes blurred letters easier to recognize.</p>
<p>These enhancements are especially valuable when processing old books, photocopies, historical records, receipts, handwritten forms, government documents, engineering drawings, and low-resolution scans.</p>
<p>Choosing the correct OCR language is equally important. A scanned Gujarati document processed using the English language model will usually produce poor recognition results. Selecting the matching language significantly improves OCR accuracy.</p>
<p>Taking a few moments to configure these settings before processing often produces cleaner extracted text, fewer recognition errors, and higher confidence scores, particularly when working with challenging documents.</p>
<h2 id="heading-extracting-text-from-the-pdf">Extracting Text from the PDF</h2>
<p>Once the document has been uploaded, previewed, and the OCR settings have been configured, the application is ready to extract text from the selected pages.</p>
<p>Unlike searchable PDFs that already contain digital text, scanned PDF documents consist entirely of images. OCR works by examining each rendered page image, recognizing every visible character, and converting those characters into editable text.</p>
<p>The extraction process begins by rendering each selected PDF page as an image using PDF.js. Each rendered page is then passed to Tesseract.js, which analyzes the image pixel by pixel and reconstructs words, sentences, paragraphs, and punctuation.</p>
<p>If the user selected a specific page range, only those pages are processed. Otherwise, every page in the document is analyzed.</p>
<p>Because OCR can be computationally intensive, especially for high-resolution scans, the application processes one page at a time. This approach keeps memory usage lower while providing continuous progress updates to the user.</p>
<p>The recognized text from each page is appended to a single output document that can later be reviewed, copied, or exported.</p>
<p>First, create the OCR worker.</p>
<pre><code class="language-javascript">const worker = await Tesseract.createWorker(

    selectedLanguage

);
</code></pre>
<p>Next, loop through the selected pages.</p>
<pre><code class="language-javascript">for(let page = startPage; page &lt;= endPage; page++){

    await processPage(page);

}
</code></pre>
<p>Now perform OCR on the rendered page.</p>
<pre><code class="language-javascript">const result = await worker.recognize(

    canvas

);

const extractedText = result.data.text;
</code></pre>
<p>Append the extracted text to the final output.</p>
<pre><code class="language-javascript">finalText +=

`----- Page ${page} -----\n\n`;

finalText += extractedText;

finalText += "\n\n";
</code></pre>
<p>Once every page has been processed, terminate the OCR worker.</p>
<pre><code class="language-javascript">await worker.terminate();
</code></pre>
<p>Processing one page at a time allows users to monitor OCR progress while ensuring stable performance, even for large documents.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f0d63a41-e872-40c3-9dd1-6bec86a8a545.png" alt="Extract Text button used to begin OCR processing for the uploaded PDF." style="display:block;margin:0 auto" width="575" height="171" loading="lazy">

<h2 id="heading-tracking-ocr-progress">Tracking OCR Progress</h2>
<p>OCR processing can take anywhere from a few seconds to several minutes depending on the size of the document, image quality, language, and selected accuracy mode.</p>
<p>Providing a progress indicator is important because users can immediately see that the application is actively processing the document instead of appearing frozen.</p>
<p>As each page finishes recognition, the progress bar updates automatically, displaying both the current page number and the overall completion percentage.</p>
<p>For example, a 42-page document may display messages such as "Processing Page 2 of 42" before eventually reaching the final page.</p>
<p>Showing real-time progress improves the overall user experience and makes it easier to estimate the remaining processing time.</p>
<p>The OCR engine reports its progress while recognizing each page.</p>
<pre><code class="language-javascript">logger: info =&gt; {

    console.log(info);

}
</code></pre>
<p>Update the progress bar.</p>
<pre><code class="language-javascript">progressBar.style.width =

`${percentage}%`;

progressLabel.innerText =

`${percentage}%`;
</code></pre>
<p>Display the currently processed page.</p>
<pre><code class="language-javascript">status.innerText =

`Processing Page ${currentPage}

of ${totalPages}`;
</code></pre>
<p>Once the final page has been processed, the progress bar reaches one hundred percent and the extracted text becomes available for review.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/bc5e38e8-9fb8-44c8-8a8e-1dcd0c44cc5c.png" alt="OCR progress indicator showing the current page being processed and the overall completion percentage." style="display:block;margin:0 auto" width="567" height="100" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/b00a2478-9079-49db-b8e7-909985109016.png" alt="OCR progress reaching the final page before completing text extraction." style="display:block;margin:0 auto" width="559" height="94" loading="lazy">

<h2 id="heading-understanding-ocr-confidence-scores">Understanding OCR Confidence Scores</h2>
<p>One useful feature of Tesseract.js is that it reports a confidence score for every page that it processes.</p>
<p>The confidence score estimates how accurately the OCR engine recognized the characters contained on a page. Higher confidence generally indicates cleaner scans, sharper text, and fewer recognition errors.</p>
<p>For example, a professionally scanned document with clear printed text may produce confidence scores above ninety-five percent, while older photocopies or blurry mobile phone images may produce lower values.</p>
<p>Displaying confidence scores helps users quickly identify pages that may require manual review or reprocessing.</p>
<p>In this application, every processed page displays its individual OCR confidence score after recognition finishes.</p>
<p>The OCR engine returns the confidence value together with the extracted text.</p>
<pre><code class="language-javascript">const confidence =

result.data.confidence;
</code></pre>
<p>Store each page's score.</p>
<pre><code class="language-javascript">confidenceScores.push({

    page: currentPage,

    confidence

});
</code></pre>
<p>Display the results.</p>
<pre><code class="language-javascript">confidenceScores.forEach(score=&gt;{

    console.log(

        score.page,

        score.confidence

    );

});
</code></pre>
<p>Pages with lower confidence scores may contain faded text, handwritten notes, poor lighting, skewed scans, or low image resolution. Reviewing these pages helps improve the overall quality of the extracted document.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/b08a9ace-3e1b-474c-9f13-939ed55522b5.png" alt="OCR confidence scores displayed for every processed PDF page." style="display:block;margin:0 auto" width="160" height="697" loading="lazy">

<h2 id="heading-optimizing-ocr-accuracy">Optimizing OCR Accuracy</h2>
<p>Even with a powerful OCR engine, the quality of the original document has a significant impact on the extracted text.</p>
<p>Scanned PDFs with sharp, high-resolution pages usually produce excellent results without additional processing. But documents containing faded printing, uneven lighting, shadows, handwritten annotations, or compression artifacts may require image enhancement before OCR begins.</p>
<p>The application includes several preprocessing options that improve recognition quality.</p>
<p>Grayscale conversion removes unnecessary color information and simplifies the image for the OCR engine. Increasing contrast helps separate text from the background, while sharpening improves character edges that may appear blurry in low-quality scans.</p>
<p>Selecting the correct recognition language is equally important. OCR models are trained for specific languages, so choosing the matching language greatly improves character recognition and reduces spelling mistakes.</p>
<p>Users should also select the appropriate accuracy mode. Fast Mode works well for clean digital scans, while High Accuracy Mode performs additional analysis that produces better results for difficult documents, although it requires more processing time.</p>
<p>Taking a few extra seconds to configure these settings often produces significantly cleaner text, higher confidence scores, and fewer manual corrections after extraction.</p>
<h2 id="heading-reviewing-the-extracted-text">Reviewing the Extracted Text</h2>
<p>Once the OCR process finishes, the application combines the recognized text from every processed page into a single output area.</p>
<p>Instead of immediately downloading the results, users can first review the extracted text directly inside the browser. This provides an opportunity to verify the OCR output, check formatting, identify recognition errors, and ensure that the correct pages were processed.</p>
<p>The extracted text preserves the page sequence by separating the content from each page with a clear page heading. This makes it much easier to navigate large documents such as books, contracts, technical manuals, invoices, government records, and research papers.</p>
<p>For searchable PDFs, the extracted text is usually very accurate. For scanned documents, users can quickly compare the OCR output with the original page preview and decide whether additional image enhancement or a different OCR language would improve the results.</p>
<p>The application also includes a <strong>Copy</strong> button so users can instantly copy all extracted text to the clipboard without downloading a file.</p>
<p>First, display the extracted text.</p>
<pre><code class="language-javascript">document.getElementById(

"output"

).value = finalText;
</code></pre>
<p>Next, implement the copy feature.</p>
<pre><code class="language-javascript">async function copyText(){

    await navigator.clipboard.writeText(

        finalText

    );

    alert("Text copied successfully.");

}
</code></pre>
<p>Attach the event listener.</p>
<pre><code class="language-javascript">document.getElementById(

"copyButton"

).addEventListener(

"click",

copyText

);
</code></pre>
<p>Providing an in-browser preview allows users to verify OCR quality before exporting the results.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/97a5b7c6-834f-4fd9-baba-c5f50b35708a.png" alt="Extracted OCR text displayed inside the browser with a copy button for quickly copying the recognized text." style="display:block;margin:0 auto" width="551" height="266" loading="lazy">

<h2 id="heading-exporting-the-ocr-results">Exporting the OCR Results</h2>
<p>After reviewing the extracted content, users can export it in different formats depending on how they intend to use the information.</p>
<p>Plain text files are ideal for editing inside any text editor, importing into word processors, or searching with desktop applications.</p>
<p>JSON exports are useful for developers building document management systems, AI applications, search engines, automation workflows, or APIs that consume structured OCR results.</p>
<p>Providing multiple export formats makes the OCR tool suitable for both everyday users and software developers.</p>
<p>Creating a downloadable TXT file is straightforward.</p>
<pre><code class="language-javascript">const blob = new Blob(

    [finalText],

    {

        type:"text/plain"

    }

);
</code></pre>
<p>Generate the download link.</p>
<pre><code class="language-javascript">const url = URL.createObjectURL(

blob

);

const link = document.createElement(

"a"

);

link.href = url;

link.download = "ocr-output.txt";

link.click();
</code></pre>
<p>JSON exports include additional information such as page numbers and confidence scores.</p>
<pre><code class="language-javascript">const report = {

    text: finalText,

    confidence: confidenceScores

};

downloadJSON(report);
</code></pre>
<p>These export options allow users to continue working with the extracted text in virtually any application.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/2dc04fb4-b1ff-498b-876d-6eeb85e59de9.png" alt="Export options allowing users to download OCR results as TXT or JSON files." style="display:block;margin:0 auto" width="575" height="123" loading="lazy">

<h2 id="heading-demo-how-the-pdf-ocr-tool-works">Demo: How the PDF OCR Tool Works</h2>
<h3 id="heading-step-1-upload-your-pdf">Step 1: Upload Your PDF</h3>
<p>The OCR workflow begins by uploading a PDF document using either the drag-and-drop area or the file picker.</p>
<p>Once a document has been selected, the browser validates the file format, loads the PDF into memory, and prepares it for page rendering. Since all processing occurs locally, the uploaded file never leaves the user's computer.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/eb91d9de-30de-480b-93cd-70f2ae0ca5e7.png" alt="PDF upload interface allowing users to select a PDF document for OCR text extraction." style="display:block;margin:0 auto" width="570" height="636" loading="lazy">

<h3 id="heading-step-2-preview-the-uploaded-pdf">Step 2: Preview the Uploaded PDF</h3>
<p>After the upload is complete, the application renders thumbnail previews of every page.</p>
<p>This allows users to verify that the correct document has been selected and inspect the page order before running OCR.</p>
<p>Previewing the document is particularly useful when processing large books, reports, legal documents, or scanned archives containing dozens of pages.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/622cfe19-938c-46af-aa40-0cee319ee477.png" alt="Uploaded PDF preview displaying page thumbnails before OCR processing begins." style="display:block;margin:0 auto" width="564" height="505" loading="lazy">

<h3 id="heading-step-3-configure-ocr-settings">Step 3: Configure OCR Settings</h3>
<p>Before text extraction begins, users configure the OCR options.</p>
<p>The application allows users to choose all pages or a specific page range, select the OCR language, switch between Fast and High Accuracy modes, and enable optional image enhancement features such as grayscale conversion, contrast improvement, and sharpening.</p>
<p>These settings help improve recognition quality depending on the condition of the scanned document.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/6bedc5cd-cc83-4bc6-be6f-2e7da57e408d.png" alt="OCR configuration panel showing page selection, language selection, accuracy mode, and image enhancement options.language selection menu displaying multiple supported recognition languages." style="display:block;margin:0 auto" width="565" height="563" loading="lazy">

<h3 id="heading-step-4-start-ocr-processing">Step 4: Start OCR Processing</h3>
<p>After reviewing the settings, users click the <strong>Extract Text</strong> button.</p>
<p>The browser begins processing every selected page one by one. During this stage, each rendered page image is analyzed by the OCR engine, which recognizes printed characters and converts them into editable text.</p>
<p>Because OCR runs directly inside the browser, even confidential documents remain completely private throughout the process.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/9f9cf7cd-6cf8-4686-ab7c-d4b5ab96dc47.png" alt="Extract Text button used to begin OCR processing for the uploaded PDF." style="display:block;margin:0 auto" width="575" height="171" loading="lazy">

<h3 id="heading-step-5-monitor-processing-progress">Step 5: Monitor Processing Progress</h3>
<p>As OCR runs, the application displays a live progress indicator.</p>
<p>Users can monitor the current page being processed, overall completion percentage, and recognition progress in real time. For large documents, this provides useful feedback and reassures users that the application is actively processing the file.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/51a2fcc1-f97f-4e9f-b53d-4a133b0ec4fc.png" alt="OCR progress indicator displaying the current page and completion percentage" style="display:block;margin:0 auto" width="567" height="100" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/1db2c8f7-84f7-441b-9cbe-2a691e98c40f.png" alt="OCR processing nearing completion on the final page of the document." style="display:block;margin:0 auto" width="575" height="133" loading="lazy">

<h3 id="heading-step-6-review-ocr-confidence-scores">Step 6: Review OCR Confidence Scores</h3>
<p>Once recognition is complete, the application displays confidence scores for every processed page.</p>
<p>These values indicate how accurately the OCR engine recognized each page. Pages with lower confidence scores may contain faded text, skewed scans, or poor image quality and can be reviewed manually if necessary.</p>
<p>Confidence scores provide an additional layer of quality assurance before exporting the extracted text.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/1a3fec10-8c8c-4104-b9f9-9a555029999c.png" alt="OCR confidence scores displayed for each processed PDF page." style="display:block;margin:0 auto" width="160" height="697" loading="lazy">

<h3 id="heading-step-7-review-the-extracted-text">Step 7: Review the Extracted Text</h3>
<p>After OCR finishes, the complete extracted text appears inside the browser.</p>
<p>Users can scroll through the recognized content, compare it with the original document, and copy the text directly to the clipboard using the built-in Copy button.</p>
<p>This makes it easy to reuse the extracted information immediately without downloading a separate file.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/52b22a16-9fa3-4b34-9687-7923be187f4f.png" alt="Browser-based OCR text output with a built-in copy button." style="display:block;margin:0 auto" width="551" height="266" loading="lazy">

<h3 id="heading-step-8-export-the-results">Step 8: Export the Results</h3>
<p>Finally, users can export the OCR results.</p>
<p>The application supports downloading the extracted text as a TXT file for general editing or as a JSON file for software development and automation workflows.</p>
<p>After selecting the preferred format, the browser generates the file instantly without uploading any data to external servers.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/e60a2073-a432-4b97-a879-83430abb9cdb.png" alt="Export section allowing users to download OCR results in TXT or JSON format." style="display:block;margin:0 auto" width="575" height="123" loading="lazy">

<h2 id="heading-performance-optimization-tips">Performance Optimization Tips</h2>
<p>OCR is one of the most computationally intensive operations performed inside a browser. Although modern JavaScript engines and OCR libraries are highly optimized, a few simple techniques can significantly improve performance.</p>
<p>Before processing begins, render PDF pages at an appropriate resolution. Extremely high-resolution images increase processing time without always improving recognition accuracy.</p>
<pre><code class="language-javascript">const viewport = page.getViewport({

    scale:1.5

});
</code></pre>
<p>Processing pages sequentially instead of loading every page simultaneously reduces memory consumption for large documents.</p>
<pre><code class="language-javascript">for(let page = 1; page &lt;= totalPages; page++){

    await processPage(page);

}
</code></pre>
<p>Users should enable OCR only when working with scanned PDFs. Searchable PDFs already contain digital text, so OCR simply increases processing time without improving the results.</p>
<p>If the document contains hundreds of pages, allowing users to analyze only a selected page range can significantly reduce processing time.</p>
<p>Using grayscale images instead of full-color pages also improves recognition speed while reducing memory usage.</p>
<p>Whenever possible, choose the OCR language that matches the document. Smaller language models generally process faster and produce more accurate results than attempting recognition with an incorrect language.</p>
<p>Finally, remember to terminate the OCR worker after processing completes to release browser resources.</p>
<pre><code class="language-javascript">await worker.terminate();
</code></pre>
<p>These small optimizations produce a smoother user experience while making browser-based OCR practical even for large documents.</p>
<h2 id="heading-important-notes-from-real-world-use">Important Notes from Real-World Use</h2>
<p>OCR accuracy depends heavily on the quality of the original document.</p>
<p>Clean scans with high resolution, good lighting, and sharp printed text usually produce excellent recognition results. Older photocopies, faded documents, handwritten notes, or skewed scans may require image enhancement before OCR begins.</p>
<p>Before processing, always verify that the uploaded file is a valid PDF.</p>
<pre><code class="language-javascript">if(file.type !== "application/pdf"){

    alert("Please upload a valid PDF.");

    return;

}
</code></pre>
<p>Selecting the correct OCR language is equally important. Processing a Gujarati document with the English language model will significantly reduce recognition accuracy.</p>
<pre><code class="language-javascript">console.log(

"Selected Language:",

selectedLanguage

);
</code></pre>
<p>Users should also review OCR confidence scores after processing. Pages with lower confidence values often benefit from rescanning or using image enhancement options.</p>
<p>Because the entire workflow runs locally, browser-based OCR is well suited for confidential business reports, contracts, financial documents, legal records, healthcare files, and government paperwork that should never be uploaded to third-party services.</p>
<h2 id="heading-common-mistakes-to-avoid">Common Mistakes to Avoid</h2>
<p>One common mistake is enabling OCR for documents that already contain selectable text.</p>
<p>Searchable PDFs can usually be processed much faster by extracting the embedded text directly.</p>
<pre><code class="language-javascript">if(pdfHasText){

    skipOCR();

}
</code></pre>
<p>Another mistake is choosing the wrong recognition language.</p>
<p>Always select the language that matches the document before starting OCR.</p>
<pre><code class="language-javascript">worker = await Tesseract.createWorker(

selectedLanguage

);
</code></pre>
<p>Some users also attempt OCR on extremely low-quality scans without enabling image enhancement.</p>
<p>Using grayscale conversion, contrast adjustment, or sharpening often improves recognition quality considerably.</p>
<p>Finally, always review the extracted text before exporting it.</p>
<p>Checking the OCR output and confidence scores helps identify pages that may require rescanning or additional processing before the results are used in business workflows.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a browser-based PDF OCR to Text Converter using JavaScript.</p>
<p>You learned how to upload PDF documents, preview scanned pages, configure OCR settings, select recognition languages, improve image quality, extract text, monitor processing progress, review OCR confidence scores, and export the recognized text directly from the browser.</p>
<p>More importantly, you discovered how modern browsers can perform Optical Character Recognition locally without requiring a backend server or cloud-based OCR service.</p>
<p>This approach keeps document processing fast, private, and secure while giving users complete control over how scanned PDFs are converted into editable text.</p>
<p>You can try the complete implementation here:</p>
<p><a href="https://allinonetools.net/pdf-to-text/"><strong>PDF OCR to Text Converter</strong></a></p>
<p>Once you understand this workflow, you can extend the project further by adding handwriting recognition, AI-powered document summarization, automatic translation, named entity extraction, keyword detection, document classification, searchable PDF generation, or intelligent document automation.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build Production-Ready Card Components with shadcn/ui ]]>
                </title>
                <description>
                    <![CDATA[ Card components are one of the most common UI patterns in web development. You see them in property listing apps, SaaS analytics dashboards, e-commerce product pages, and admin panels. But building a  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-production-ready-card-components-with-shadcn-ui/</link>
                <guid isPermaLink="false">6a4d22a80e40282fd1e657a8</guid>
                
                    <category>
                        <![CDATA[ shadcn ]]>
                    </category>
                
                    <category>
                        <![CDATA[ UI Design ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Vaibhav Gupta ]]>
                </dc:creator>
                <pubDate>Tue, 07 Jul 2026 16:00:40 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/1efc2e2c-00a5-4891-9845-18de2a38c5f1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Card components are one of the most common UI patterns in web development. You see them in property listing apps, SaaS analytics dashboards, e-commerce product pages, and admin panels.</p>
<p>But building a card that handles hover states cleanly, supports dark mode, stays accessible, and works across screen sizes takes more than wrapping content in a <code>&lt;div&gt;</code>. You need a consistent component structure, a reliable design system, and well-thought-out Tailwind patterns.</p>
<p>In this tutorial, you'll build four types of production-ready card components using shadcn/ui and Base UI primitives via Shadcn Space. Each card targets a specific, real-world UI pattern that developers run into regularly.</p>
<p>By the end, you'll have:</p>
<ol>
<li><p>A Preview Card with a group hover image effect, an overlay arrow icon, and a property details layout</p>
</li>
<li><p>An Analytics Card with typed metric props, conditional badge colors, and a decorative background image</p>
</li>
<li><p>A Statistics Card with a responsive four-column e-commerce stats grid and icon badges</p>
</li>
<li><p>An Ecommerce Product Variant Card with size selection, a wishlist toggle, a bag button, and a ripple animation on the buy button</p>
</li>
</ol>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-why-shadcnui">Why shadcn/ui?</a></p>
</li>
<li><p><a href="#heading-what-is-shadcn-space">What is Shadcn Space?</a></p>
</li>
<li><p><a href="#heading-what-youll-build">What You'll Build</a></p>
</li>
<li><p><a href="#heading-how-to-set-up-the-cli-registry">How to Set Up the CLI Registry</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-preview-card-card-02">How to Build the Preview Card (card-02)</a></p>
</li>
<li><p><a href="#heading-live-preview">Live Preview</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-analytics-card-card-05">How to Build the Analytics Card (card-05)</a></p>
</li>
<li><p><a href="#heading-live-preview">Live Preview</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-statistics-card-card-06">How to Build the Statistics Card (card-06)</a></p>
</li>
<li><p><a href="#heading-live-preview">Live Preview</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-ecommerce-product-variant-card-card-17">How to Build the Ecommerce Product Variant Card (card-17)</a></p>
</li>
<li><p><a href="#heading-live-preview">Live Preview</a></p>
</li>
<li><p><a href="#heading-quick-reference-table">Quick Reference Table</a></p>
</li>
<li><p><a href="#heading-key-concepts-recap">Key Concepts Recap</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-resources">Resources</a></p>
</li>
</ul>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>Before you start, make sure you have the following in place:</p>
<ul>
<li><p>Node.js 18 or higher installed</p>
</li>
<li><p>A Next.js or React project set up</p>
</li>
<li><p>shadcn/ui initialized in your project (<code>npx shadcn@latest init</code>)</p>
</li>
<li><p>Tailwind CSS configured</p>
</li>
<li><p>Basic knowledge of React and TypeScript</p>
</li>
</ul>
<p>If you haven't initialized shadcn/ui yet, run <code>npx shadcn@latest init</code> in your project root and follow the prompts before continuing.</p>
<h2 id="heading-why-shadcnui"><strong>Why shadcn/ui?</strong></h2>
<p><a href="https://ui.shadcn.com/"><strong>shadcn/ui</strong></a> is a collection of accessible, open-source React components built on top of Radix UI, Base UI, and styled with Tailwind CSS.</p>
<p>The way it works is different from a traditional component library. Instead of installing a package, you use a CLI to copy the component source files directly into your project. This means you own every line of the code. You can read it, edit it, and the component will never break because of a library update you didn't control.</p>
<p>Some key benefits:</p>
<ul>
<li><p><strong>Accessible by default</strong>: built on Radix UI and Base UI primitives</p>
</li>
<li><p><strong>Fully Tailwind-based</strong>: no external CSS files, no specificity conflicts</p>
</li>
<li><p><strong>Zero lock-in</strong>: components live in your <code>components/</code> folder, not inside <code>node_modules</code></p>
</li>
<li><p><strong>Works everywhere</strong>: Next.js, Vite, Astro, Remix, and other React frameworks</p>
</li>
</ul>
<p>The <code>Card</code>, <code>Badge</code>, <code>Button</code>, and <code>Separator</code> components you'll use in this tutorial all come from the shadcn/ui base install.</p>
<h2 id="heading-what-is-shadcn-space"><strong>What is Shadcn Space?</strong></h2>
<p><a href="https://shadcnspace.com/"><strong>Shadcn Space</strong></a> is an open-source registry of production-ready components and UI blocks built on top of shadcn/ui. It extends the default shadcn/ui component set with additional variants from common patterns to highly appealing layouts.</p>
<p>The key difference from the default shadcn/ui <code>Card</code> component is that Shadcn Space cards are designed for specific layout patterns. You get more structure out of the box.</p>
<p>Each component in Shadcn Space supports both Radix UI and Base UI primitives. You also get the functionality of <strong>Copy Prompt</strong>. This tutorial uses the Base UI versions. You install them the same way as any shadcn/ui component, through a single CLI command, and the source files land in your project.</p>
<p>You can browse the full card collection in the <a href="https://shadcnspace.com/components/card"><strong>Shadcn card component library</strong></a>.</p>
<h2 id="heading-what-youll-build"><strong>What You'll Build</strong></h2>
<p>Here's an overview of the four cards you'll build, along with their specific features:</p>
<p><strong>Preview Card (card-02)</strong></p>
<ul>
<li><p>Large image with hover brightness and scale animation</p>
</li>
<li><p>An arrow icon that appears only on hover</p>
</li>
<li><p>Property title and location</p>
</li>
<li><p>Price badge with a teal color scheme</p>
</li>
<li><p>Amenity row with bed, bath, and area icons</p>
</li>
</ul>
<p><strong>Analytics Card (card-05)</strong></p>
<ul>
<li><p>Typed TypeScript props with a built-in default dataset</p>
</li>
<li><p>Two metric columns separated by a vertical divider</p>
</li>
<li><p>Conditional badge colors based on positive or negative trend</p>
</li>
<li><p>Decorative background image pinned to the bottom-right corner</p>
</li>
</ul>
<p><strong>Statistics Card (card-06)</strong></p>
<ul>
<li><p>Four-column responsive grid that stacks on mobile</p>
</li>
<li><p>Iconify Solar icon set for each metric</p>
</li>
<li><p>Badge with trend direction icon</p>
</li>
<li><p>Border dividers are removed from the last column automatically</p>
</li>
</ul>
<p><strong>Ecommerce Product Variant Card (card-17)</strong></p>
<ul>
<li><p>Product image with 3D drop shadow and hover zoom</p>
</li>
<li><p>Wishlist heart toggle with dark mode support</p>
</li>
<li><p>Size selector with active state highlighting</p>
</li>
<li><p>Bag icon toggle that fills on click</p>
</li>
<li><p>"Buy Now" button with a CSS ripple animation</p>
</li>
<li><p>Dynamic delivery date with ordinal suffix formatting</p>
</li>
</ul>
<h2 id="heading-how-to-set-up-the-cli-registry"><strong>How to Set Up the CLI Registry</strong></h2>
<p>Before you run any install commands, you need to register the Shadcn Space registry in your <code>components.json</code> file.</p>
<p>Open <code>components.json</code> in your project root and add the <code>registries</code> field:</p>
<pre><code class="language-javascript">{
  "registries": {
    "@shadcn-space": {
      "url": "https://shadcnspace.com/r/{name}.json"
    }
  }
}
</code></pre>
<p>This tells the shadcn CLI where to find components prefixed with <code>@shadcn-space/</code>. Without this step, all the install commands in this tutorial will fail.</p>
<p>Your full <code>components.json</code> should look something like this after adding the registry:</p>
<pre><code class="language-javascript">{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "default",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "app/globals.css",
    "baseColor": "neutral",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils"
  },
  "registries": {
    "@shadcn-space": {
      "url": "https://shadcnspace.com/r/{name}.json"
    }
  }
}
</code></pre>
<p>For a full walkthrough of how the CLI works with third-party registries, visit the <a href="https://shadcnspace.com/docs/getting-started/how-to-use-shadcn-cli"><strong>getting started guide</strong></a>. You can also watch the video walkthrough if you prefer to follow along visually.</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/n6dvjVxy02U" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>

<h2 id="heading-how-to-build-the-preview-card-card-02"><strong>How to Build the Preview Card (card-02)</strong></h2>
<h3 id="heading-what-the-preview-card-does">What the Preview Card Does</h3>
<p>The Preview Card is designed for property listings, hotel pages, or any content that benefits from a large image with supporting details below it.</p>
<p>When a user hovers the card, the image darkens and scales up. An arrow icon appears in the corner. Below the image, a title, location, price badge, and amenity row are displayed.</p>
<h3 id="heading-how-to-install-the-preview-card">How to Install the Preview Card</h3>
<p>Run one of the following commands based on your package manager:</p>
<p><strong>npm:</strong></p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/card-02
</code></pre>
<p><strong>pnpm:</strong></p>
<pre><code class="language-javascript">pnpm dlx shadcn@latest add @shadcn-space/card-02
</code></pre>
<p><strong>Yarn:</strong></p>
<pre><code class="language-javascript">yarn dlx shadcn@latest add @shadcn-space/card-02
</code></pre>
<p><strong>Bun:</strong></p>
<pre><code class="language-javascript">bunx --bun shadcn@latest add @shadcn-space/card-02
</code></pre>
<p>The CLI copies the component into your project at:</p>
<pre><code class="language-javascript">components/
  shadcn-space/
    card/
      Card-02.tsx
</code></pre>
<h3 id="heading-the-component-code">The Component Code</h3>
<pre><code class="language-javascript">import { Badge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
import { ArrowRight, Bath, BedDouble, Expand } from "lucide-react";

const PreviewCard = () =&gt; (
  &lt;Card className="relative gap-0 py-0 rounded-2xl group hover:shadow-3xl duration-300"&gt;
    &lt;div className="overflow-hidden rounded-t-2xl"&gt;
      &lt;a href="#"&gt;
        &lt;div className="w-full h-72"&gt;
          &lt;img
            src="https://images.shadcnspace.com/assets/card/property-cover-1.jpg"
            alt="Serenity Residential Home"
            width={440}
            height={300}
            className="w-full h-full object-cover rounded-t-2xl group-hover:brightness-50 group-hover:scale-125 transition duration-300 delay-75"
          /&gt;
        &lt;/div&gt;
      &lt;/a&gt;

      &lt;div className="absolute top-6 right-6 hidden p-4 bg-white rounded-full group-hover:block"&gt;
        &lt;ArrowRight className="text-card-foreground" /&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div className="p-6"&gt;
      &lt;div className="flex justify-between gap-5 mb-6"&gt;
        &lt;div&gt;
          &lt;a href="#"&gt;
            &lt;h3 className="text-xl font-medium duration-300 group-hover:text-primary"&gt;
              Serenity Residential Home
            &lt;/h3&gt;
          &lt;/a&gt;
          &lt;p className="text-base font-normal text-muted-foreground"&gt;
            15 S Aurora Ave, Miami
          &lt;/p&gt;
        &lt;/div&gt;

        &lt;Badge className="px-5 py-4 text-base font-normal rounded-full bg-teal-500/10 text-teal-500"&gt;
          $570,000
        &lt;/Badge&gt;
      &lt;/div&gt;

      &lt;div className="flex"&gt;
        &lt;div className="flex flex-col gap-2 xs:pr-4 pr-8 border-e border-border"&gt;
          &lt;BedDouble size={20} /&gt;
          &lt;p className="text-sm sm:text-base"&gt;5 Bedrooms&lt;/p&gt;
        &lt;/div&gt;

        &lt;div className="flex flex-col gap-2 xs:px-4 px-8 border-e border-border"&gt;
          &lt;Bath size={20} /&gt;
          &lt;p className="text-sm sm:text-base"&gt;3 Bathrooms&lt;/p&gt;
        &lt;/div&gt;

        &lt;div className="flex flex-col gap-2 xs:pl-4 pl-8"&gt;
          &lt;Expand size={20} /&gt;
          &lt;p className="text-sm sm:text-base"&gt;
            120m&lt;sup&gt;2&lt;/sup&gt;
          &lt;/p&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/Card&gt;
);

export default PreviewCard;
</code></pre>
<p>Let's now go through how this code works.</p>
<h4 id="heading-1-group-hover-behavior">1. Group hover behavior</h4>
<p>The <code>group</code> class on the outer <code>Card</code> element is the core of this component. Any child element with a <code>group-hover:</code> class will respond when the card is hovered, not just that individual element.</p>
<p>This is how the image darkens (<code>group-hover:brightness-50</code>), scales up (<code>group-hover:scale-125</code>), and the arrow icon appears (<code>group-hover:block</code>).</p>
<h4 id="heading-2-overflow-clipping-on-image-zoom">2. Overflow clipping on image zoom</h4>
<p>Without <code>overflow-hidden</code> on the image wrapper, the <code>scale-125</code> transform would bleed past the card's rounded corners on hover. The wrapper clips the image so it stays inside the card boundary.</p>
<p>Notice that <code>rounded-t-2xl</code> appears on both the wrapper and the image itself to maintain a consistent corner radius during the transition.</p>
<h4 id="heading-3-logical-border-properties-in-the-amenity-row">3. Logical border properties in the amenity row</h4>
<p>The amenity row uses <code>border-e</code> instead of <code>border-r</code>. This is a CSS logical property meaning "border at the inline end." In left-to-right layouts, that's the right side. In right-to-left layouts, it flips automatically. Using logical properties is a good production habit for any component that may need to support multiple locales.</p>
<h3 id="heading-live-preview">Live Preview:</h3>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/21233f5e-2d39-4430-8013-a780bd24419c.gif" alt="21233f5e-2d39-4430-8013-a780bd24419c" style="display:block;margin:0 auto" width="960" height="720" loading="lazy">

<hr>
<h2 id="heading-how-to-build-the-analytics-card-card-05"><strong>How to Build the Analytics Card (card-05)</strong></h2>
<h3 id="heading-what-the-analytics-card-does">What the Analytics Card Does</h3>
<p>The Analytics Card is a compact dashboard widget. It shows two metrics side by side with values and percentage-change badges. A decorative chart image sits in the bottom-right corner.</p>
<p>The component is typed with TypeScript interfaces, making it easy to swap in real data from an API.</p>
<h3 id="heading-how-to-install-the-analytics-card">How to Install the Analytics Card</h3>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/card-05
</code></pre>
<p>The CLI copies the component into:</p>
<pre><code class="language-javascript">components/
  shadcn-space/
    card/
      card-05.tsx
</code></pre>
<h3 id="heading-the-component-code">The Component Code</h3>
<pre><code class="language-javascript">import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";

type DashboardMetric = {
  label: string;
  value: string;
  percentage: string;
  isPositive?: boolean;
};

type MainDashboardData = {
  title: string;
  description: string;
  metrics: DashboardMetric[];
};

type WidgetProps = {
  mainDashboard?: MainDashboardData;
};

const mainDashboardData: MainDashboardData = {
  title: "Analytics Dashboard",
  description: "Check all the statistics",
  metrics: [
    {
      label: "Earnings",
      value: "$27,850",
      percentage: "+18%",
      isPositive: true,
    },
    {
      label: "Expense",
      value: "$18,453",
      percentage: "-5%",
      isPositive: false,
    },
  ],
};

const AnalyticsCard = ({ mainDashboard = mainDashboardData }: WidgetProps) =&gt; {
  return (
    &lt;div className="flex items-center justify-center w-full"&gt;
      &lt;div className="max-w-7xl mx-auto px-4 lg:px-8 xl:px-16 py-10 w-full"&gt;
        &lt;Card className="p-0 ring-0 border rounded-2xl relative h-full max-w-xl w-full mx-auto"&gt;
          &lt;CardContent className="p-0"&gt;
            &lt;div className="ps-6 py-4 flex flex-col gap-9 justify-between"&gt;
              &lt;div&gt;
                &lt;p className="text-lg font-medium text-card-foreground"&gt;
                  {mainDashboard.title}
                &lt;/p&gt;
                &lt;p className="text-xs font-normal text-muted-foreground"&gt;
                  {mainDashboard.description}
                &lt;/p&gt;
              &lt;/div&gt;
              &lt;div className="flex items-center gap-6"&gt;
                {mainDashboard.metrics.map((metric, index) =&gt; (
                  &lt;div key={index} className="flex items-center gap-6"&gt;
                    &lt;div&gt;
                      &lt;p className="text-xs font-normal text-muted-foreground"&gt;
                        {metric.label}
                      &lt;/p&gt;
                      &lt;div className="flex items-center gap-1"&gt;
                        &lt;p className="text-2xl font-medium text-card-foreground"&gt;
                          {metric.value}
                        &lt;/p&gt;
                        &lt;Badge
                          className={cn(
                            "font-normal text-muted-foreground",
                            metric.isPositive
                              ? "bg-teal-400/10"
                              : "bg-red-500/10"
                          )}
                        &gt;
                          {metric.percentage}
                        &lt;/Badge&gt;
                      &lt;/div&gt;
                    &lt;/div&gt;
                    {index &lt; mainDashboard.metrics.length - 1 &amp;&amp; (
                      &lt;Separator orientation="vertical" className="h-12" /&gt;
                    )}
                  &lt;/div&gt;
                ))}
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;img
              src="https://images.shadcnspace.com/assets/backgrounds/stats-01.webp"
              alt="stats chart"
              width={211}
              height={168}
              className="absolute bottom-0 right-0 hidden sm:block"
            /&gt;
          &lt;/CardContent&gt;
        &lt;/Card&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default AnalyticsCard;
</code></pre>
<p>How the Analytics Card works:</p>
<h4 id="heading-1-optional-props-with-a-default-dataset">1. Optional props with a default dataset</h4>
<p>The component accepts an optional <code>mainDashboard</code> prop. If you don't pass anything, it falls back to <code>mainDashboardData</code>, the constant is defined in the same file:</p>
<pre><code class="language-javascript">const AnalyticsCard = ({ mainDashboard = mainDashboardData }: WidgetProps) =&gt; {
</code></pre>
<p>This pattern lets the component work out of the box in demos or Storybook, while still being fully driven by real API data in production. To connect it to live data, you just pass a prop that matches the <code>MainDashboardData</code> shape.</p>
<h4 id="heading-2-conditional-badge-colors-with-cn">2. Conditional badge colors with <code>cn()</code></h4>
<p>The <code>cn()</code> utility (from <code>@/lib/utils</code>) merges Tailwind class names and handles conditional logic cleanly. It also de-duplicates conflicting Tailwind classes automatically, which plain template literals don't do:</p>
<pre><code class="language-javascript">className={cn(
  "font-normal text-muted-foreground",
  metric.isPositive ? "bg-teal-400/10" : "bg-red-500/10"
)}
</code></pre>
<h4 id="heading-3-separators-only-between-metrics-not-after-the-last-one">3. Separators only between metrics, not after the last one</h4>
<p>The <code>Separator</code> component renders only between metrics, never after the last one. The index check handles this:</p>
<pre><code class="language-javascript">{index &lt; mainDashboard.metrics.length - 1 &amp;&amp; (
  &lt;Separator orientation="vertical" className="h-12" /&gt;
)}
</code></pre>
<h4 id="heading-4-absolute-positioned-decorative-image">4. Absolute-positioned decorative image</h4>
<p>The chart image is used <code>absolute bottom-0 right-0</code> to pin it to the card's bottom-right corner. It hides on small screens with <code>hidden sm:block</code> to avoid layout issues on mobile. The parent <code>Card</code> has <code>relative</code> positioning to contain it.</p>
<h3 id="heading-live-preview">Live Preview:</h3>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/de09194f-ff75-49c1-95e5-21d758e499e8.png" alt="de09194f-ff75-49c1-95e5-21d758e499e8" style="display:block;margin:0 auto" width="1266" height="356" loading="lazy">

<hr>
<h2 id="heading-how-to-build-the-statistics-card-card-06"><strong>How to Build the Statistics Card (card-06)</strong></h2>
<h3 id="heading-what-the-statistics-card-does">What the Statistics Card Does</h3>
<p>The Statistics Card displays four e-commerce metrics in a horizontal grid: Orders, Sales, Profit, and Expense. Each column has an icon, a large value, a time period label, and a badge showing the percentage trend.</p>
<p>The layout is fully responsive, collapsing from four columns to two on medium screens and stacking on mobile.</p>
<p>This card uses <code>@iconify/react</code> for icons instead of <code>lucide-react</code>, giving you access to thousands of icon sets using string-based icon names.</p>
<h3 id="heading-how-to-install-the-statistics-card">How to Install the Statistics Card</h3>
<p>First, install <code>@iconify/react</code> if you don't have it:</p>
<pre><code class="language-javascript">npm install @iconify/react
</code></pre>
<p>Then add the card component:</p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/card-06
</code></pre>
<p>The CLI copies the component into:</p>
<pre><code class="language-javascript">components/
  shadcn-space/
    card/
      card-06.tsx
</code></pre>
<h3 id="heading-the-component-code">The Component Code</h3>
<pre><code class="language-javascript">"use client";
import { Icon } from "@iconify/react";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";

const StatisticsCard = () =&gt; {
  const EcommerceActions = [
    {
      title: "Orders",
      subtitle: "5868",
      cardIcon: "solar:bag-4-line-duotone",
      badgeColor: "bg-teal-400/10",
      statusValue: "+18%",
      statusIcon: "solar:course-up-line-duotone",
    },
    {
      title: "Sales",
      subtitle: "$96,850",
      cardIcon: "solar:box-line-duotone",
      badgeColor: "bg-orange-400/10",
      statusValue: "-5%",
      statusIcon: "solar:course-down-line-duotone",
    },
    {
      title: "Profit",
      subtitle: "$82,906",
      cardIcon: "solar:chart-square-line-duotone",
      badgeColor: "bg-teal-400/10",
      statusValue: "+18%",
      statusIcon: "solar:course-up-line-duotone",
    },
    {
      title: "Expense",
      subtitle: "$14,653",
      cardIcon: "solar:star-line-duotone",
      badgeColor: "bg-teal-400/10",
      statusValue: "+18%",
      statusIcon: "solar:course-up-line-duotone",
    },
  ];

  return (
    &lt;div className="max-w-7xl mx-auto px-4 w-full"&gt;
      &lt;Card className="p-0"&gt;
        &lt;CardContent className="flex items-center w-full lg:flex-nowrap flex-wrap px-0"&gt;
          {EcommerceActions.map((item, index) =&gt; (
            &lt;div
              className="lg:w-3/12 md:w-6/12 w-full border-e border-border last:border-e-0"
              key={index}
            &gt;
              &lt;div className="p-6"&gt;
                &lt;div className="flex flex-col gap-1"&gt;
                  &lt;div className="flex justify-between items-start"&gt;
                    &lt;h5 className="text-base font-medium"&gt;{item.title}&lt;/h5&gt;
                    &lt;div className="p-3 rounded-full outline outline-border text-primary"&gt;
                      &lt;Icon icon={item.cardIcon} width={16} height={16} /&gt;
                    &lt;/div&gt;
                  &lt;/div&gt;
                  &lt;div className="flex flex-col gap-1"&gt;
                    &lt;h5 className="text-2xl font-semibold"&gt;{item.subtitle}&lt;/h5&gt;
                    &lt;div className="flex items-center gap-2"&gt;
                      &lt;p className="text-xs text-muted-foreground"&gt;Last 7 days&lt;/p&gt;
                      &lt;Badge className={`${item.badgeColor} text-muted-foreground`}&gt;
                        &lt;div className="flex items-center gap-1"&gt;
                          {item.statusValue}
                          &lt;Icon icon={item.statusIcon} width={14} height={14} /&gt;
                        &lt;/div&gt;
                      &lt;/Badge&gt;
                    &lt;/div&gt;
                  &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          ))}
        &lt;/CardContent&gt;
      &lt;/Card&gt;
    &lt;/div&gt;
  );
};

export default StatisticsCard;
</code></pre>
<p>How the Statistics Card works:</p>
<h4 id="heading-1-data-driven-layout-with-an-array">1. Data-driven layout with an array</h4>
<p>All four metrics live in the <code>EcommerceActions</code> array. Adding or removing a metric only requires updating the array. The JSX stays the same. This is the right approach for any component with a repeating structure: keep data and markup separate.</p>
<h4 id="heading-2-responsive-column-widths">2. Responsive column widths</h4>
<p>Each column uses three width classes to handle every breakpoint:</p>
<ul>
<li><p><code>w-full</code> on mobile (single column, stacked vertically)</p>
</li>
<li><p><code>md:w-6/12</code> on medium screens (two columns)</p>
</li>
<li><p><code>lg:w-3/12</code> on large screens (four equal columns)</p>
</li>
</ul>
<p>The <code>flex-wrap</code> on the <code>CardContent</code> lets columns wrap naturally on smaller screens. <code>lg:flex-nowrap</code> forces them into a single row on large screens.</p>
<h4 id="heading-3-removing-the-last-border-with-last">3. Removing the last border with <code>last:</code></h4>
<p>The <code>last:border-e-0</code> class removes the right border from the final column. Without it, there'd be a stray border on the right edge of the card.</p>
<p>The <code>last:</code> variant is a Tailwind pseudo-class that targets the last child element in a group, which is cleaner than tracking the index manually.</p>
<h4 id="heading-4-why-use-client-is-needed-here">4. Why <code>"use client"</code> is needed here</h4>
<p>The <code>@iconify/react</code> package requires a browser environment. In Next.js with the App Router, any component that imports a client-only package needs the <code>"use client"</code> directive at the top of the file. Without it, the server will throw an error during rendering.</p>
<h3 id="heading-live-preview">Live Preview:</h3>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/dd7600b4-753c-4e67-abff-2d8955a22653.png" alt="dd7600b4-753c-4e67-abff-2d8955a22653" style="display:block;margin:0 auto" width="1272" height="257" loading="lazy">

<hr>
<h2 id="heading-how-to-build-the-ecommerce-product-variant-card-card-17"><strong>How to Build the Ecommerce Product Variant Card (card-17)</strong></h2>
<h3 id="heading-what-the-ecommerce-product-variant-card-does">What the Ecommerce Product Variant Card Does</h3>
<p>This is the most interactive card in this tutorial. It's a product card for a shoe listing with a hover zoom, a wishlist toggle, size buttons, a bag toggle, and a ripple-animation buy button. All interactions are handled with React's <code>useState</code>, so no external state management library is required.</p>
<h3 id="heading-how-to-install-the-product-variant-card">How to Install the Product Variant Card</h3>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/card-17
</code></pre>
<p>The CLI copies the component into:</p>
<pre><code class="language-javascript">components/
  shadcn-space/
    card/
      card-17.tsx
</code></pre>
<h3 id="heading-the-component-code">The Component Code</h3>
<pre><code class="language-javascript">"use client";

import { useState } from "react";
import { Card, CardContent, CardFooter } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Heart, ShoppingBag } from "lucide-react";
import { cn } from "@/lib/utils";

const sizes = ["7", "8", "9", "10"];

const getDeliveryDate = () =&gt; {
  const date = new Date();
  date.setDate(date.getDate() + 3);
  const day = date.getDate();
  const month = [
    "Jan", "Feb", "Mar", "Apr", "May", "Jun",
    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
  ][date.getMonth()];
  const suffix = ["th", "st", "nd", "rd"][
    day % 10 &gt; 3 ? 0 : (day % 100 - day % 10 !== 10 ? day % 10 : 0)
  ];
  return `${day}${suffix} ${month}`;
};

export default function EcommerceProductCard() {
  const [activeSize, setActiveSize] = useState(1);
  const [isWishlisted, setIsWishlisted] = useState(false);
  const [inBag, setInBag] = useState(false);

  return (
    &lt;div className="flex items-center justify-center p-8 w-full bg-background"&gt;
      &lt;Card className="w-80 rounded-2xl overflow-hidden p-0 gap-0 group/card"&gt;

        {/* Image zone */}
        &lt;div className="relative overflow-hidden h-80"&gt;
          &lt;img
            src="https://images.shadcnspace.com/assets/card/running-shoe-3d.png"
            className="object-contain drop-shadow-2xl px-8 py-6 transition-transform duration-500 ease-out group-hover/card:scale-105"
            alt="Nike Air Max Pulse"
          /&gt;

          {/* Discount badge */}
          &lt;span className="absolute top-3 left-3 text-xs tracking-widest font-bold uppercase bg-foreground text-background px-2.5 py-1 rounded-sm select-none"&gt;
            -21%
          &lt;/span&gt;

          {/* Wishlist button */}
          &lt;button
            onClick={() =&gt; setIsWishlisted(!isWishlisted)}
            title="Wishlist"
            className={cn(
              "absolute top-3 right-3 h-8 w-8 rounded-full border shadow-sm flex items-center justify-center transition-all duration-200 hover:scale-110 active:scale-95",
              isWishlisted
                ? "bg-rose-50 border-rose-200 dark:bg-rose-950 dark:border-rose-800"
                : "bg-background"
            )}
          &gt;
            &lt;Heart
              className={cn(
                "w-3.5 h-3.5 transition-colors",
                isWishlisted ? "fill-rose-500 text-rose-500" : "text-muted-foreground"
              )}
            /&gt;
          &lt;/button&gt;
        &lt;/div&gt;

        {/* Info zone */}
        &lt;CardContent className="px-4 pt-4 pb-4 space-y-1.5"&gt;
          &lt;div className="min-w-0"&gt;
            &lt;h3 className="text-base font-bold text-foreground truncate"&gt;Nike&lt;/h3&gt;
            &lt;p className="text-sm text-muted-foreground truncate"&gt;
              Air Max Pulse Running Shoes
            &lt;/p&gt;
          &lt;/div&gt;

          &lt;div className="flex items-center gap-2 pt-1"&gt;
            &lt;span className="text-green-600 dark:text-green-500 font-semibold text-sm"&gt;
              Down 21%
            &lt;/span&gt;
            &lt;span className="text-muted-foreground line-through text-sm"&gt;$150&lt;/span&gt;
            &lt;span className="text-foreground font-bold text-base"&gt;$119&lt;/span&gt;
          &lt;/div&gt;

          &lt;div className="text-xs text-muted-foreground font-medium"&gt;
            Delivery by{" "}
            &lt;span suppressHydrationWarning className="text-foreground font-bold"&gt;
              {getDeliveryDate()}
            &lt;/span&gt;
          &lt;/div&gt;

          {/* Size selector */}
          &lt;div className="flex gap-1.5 pt-2"&gt;
            {sizes.map((s, i) =&gt; (
              &lt;button
                key={s}
                onClick={() =&gt; setActiveSize(i)}
                className={cn(
                  "flex-1 h-7 rounded-lg text-sm font-medium border transition-all duration-150",
                  activeSize === i
                    ? "bg-foreground text-background border-foreground"
                    : "text-muted-foreground hover:border-foreground/50 hover:text-foreground"
                )}
              &gt;
                US {s}
              &lt;/button&gt;
            ))}
          &lt;/div&gt;
        &lt;/CardContent&gt;

        {/* Action zone */}
        &lt;CardFooter className="px-4 pb-6 gap-2 bg-transparent border-t-0"&gt;
          &lt;button
            onClick={() =&gt; setInBag(!inBag)}
            title={inBag ? "Remove from bag" : "Add to bag"}
            className={cn(
              "h-12 w-12 shrink-0 rounded-xl border flex items-center justify-center transition-all duration-200 hover:scale-105 active:scale-95",
              inBag
                ? "bg-foreground text-background border-foreground"
                : "bg-background text-muted-foreground hover:border-foreground/50 hover:text-foreground"
            )}
          &gt;
            &lt;ShoppingBag className="w-5 h-5" /&gt;
          &lt;/button&gt;

          &lt;Button className="relative overflow-hidden group/btn flex-1 h-12 rounded-xl font-semibold text-base cursor-pointer border border-primary transition-all flex items-center justify-center gap-2"&gt;
            &lt;span className="absolute left-1/2 -translate-x-1/2 top-full -translate-y-1/2 w-8 h-8 bg-white dark:bg-gray-950 rounded-full scale-0 transition-transform duration-700 ease-in-out group-hover/btn:scale-[20]" /&gt;
            &lt;span className="relative z-10 transition-colors duration-500 group-hover/btn:text-gray-950 dark:group-hover/btn:text-white"&gt;
              Buy Now
            &lt;/span&gt;
          &lt;/Button&gt;
        &lt;/CardFooter&gt;

      &lt;/Card&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>How the Ecommerce Product Variant Card works:</p>
<h4 id="heading-1-named-group-hover-scopes">1. Named group hover scopes</h4>
<p>This card uses two independent hover group scopes: <code>group/card</code> on the outer card and <code>group/btn</code> on the Buy Now button. Tailwind's named group feature uses the <code>/name</code> suffix to keep them separate:</p>
<pre><code class="language-javascript">// Card-level hover: zooms the product image
&lt;Card className="... group/card"&gt;
  &lt;img className="... group-hover/card:scale-105" /&gt;

// Button-level hover: triggers the ripple animation
  &lt;Button className="... group/btn"&gt;
    &lt;span className="... group-hover/btn:scale-[20]" /&gt;
</code></pre>
<p>Without named groups, hovering the button would also trigger the card's hover styles. The <code>/card</code> and <code>/btn</code> suffixes prevent this.</p>
<h4 id="heading-2-css-ripple-animation-on-the-buy-now-button">2. CSS ripple animation on the Buy Now button</h4>
<p>The ripple effect uses a pure CSS scale animation. A white circle (<code>w-8 h-8 rounded-full</code>) starts at <code>scale-0</code> and transitions to <code>scale-[20]</code> when the button is hovered. The <code>overflow-hidden</code> on the <code>Button</code> clips it to the button's boundary. The <code>z-10</code> on the label keeps the text visible above the expanding circle.</p>
<h4 id="heading-3-ordinal-suffix-logic-for-the-delivery-date">3. Ordinal suffix logic for the delivery date</h4>
<p>The <code>getDeliveryDate()</code> function calculates a date three days from now and attaches the correct ordinal suffix (st, nd, rd, th):</p>
<pre><code class="language-javascript">const suffix = ["th", "st", "nd", "rd"][
  day % 10 &gt; 3 ? 0 : (day % 100 - day % 10 !== 10 ? day % 10 : 0)
];
</code></pre>
<p>The logic handles the edge cases for 11th, 12th, and 13th, which always use "th" regardless of their last digit. This is a common gotcha in ordinal formatting.</p>
<h4 id="heading-4-suppresshydrationwarning-on-the-delivery-date-span">4. <code>suppressHydrationWarning</code> on the delivery date span</h4>
<p>The delivery date is calculated at render time using <code>new Date()</code>. The server calculates it at request time, and the client recalculates it at hydration time.</p>
<p>If there's a timezone difference, React throws a hydration mismatch warning. <code>suppressHydrationWarning</code> silences this warning for that specific node without affecting the rest of the tree.</p>
<h3 id="heading-live-preview">Live Preview:</h3>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/f81584b2-a2ba-4233-a628-5daca018a1d9.gif" alt="f81584b2-a2ba-4233-a628-5daca018a1d9" style="display:block;margin:0 auto" width="960" height="720" loading="lazy">

<h2 id="heading-quick-reference-table"><strong>Quick Reference Table</strong></h2>
<table>
<thead>
<tr>
<th>Card</th>
<th>Identifier</th>
<th>Use Case</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Preview Card</strong></td>
<td><code>card-02</code></td>
<td>Property listings, hotel cards, product previews</td>
</tr>
<tr>
<td><strong>Analytics Card</strong></td>
<td><code>card-05</code></td>
<td>Dashboard widgets with metric data</td>
</tr>
<tr>
<td><strong>Statistics Card</strong></td>
<td><code>card-06</code></td>
<td>E-commerce stats grids</td>
</tr>
<tr>
<td><strong>Product Variant Card</strong></td>
<td><code>card-17</code></td>
<td>Product pages with size selection and cart</td>
</tr>
</tbody></table>
<p>To install any of these, replace the identifier in the CLI command:</p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/&lt;identifier&gt;
</code></pre>
<h2 id="heading-key-concepts-recap"><strong>Key Concepts Recap</strong></h2>
<p>Here's a summary of the key Tailwind, React, and TypeScript patterns used across the four cards in this tutorial.</p>
<h3 id="heading-tailwind-group-hover">Tailwind Group Hover</h3>
<p>The <code>group</code> class on a parent element lets any child respond to the parent's hover state using <code>group-hover:</code> classes.</p>
<p>For nested hover scopes, use named groups like <code>group/card</code> and <code>group/btn</code> with <code>group-hover/card:</code> and <code>group-hover/btn:</code>. This prevents hover styles from bleeding across component boundaries.</p>
<h3 id="heading-the-cn-utility">The <code>cn()</code> Utility</h3>
<p><code>cn()</code> from <code>@/lib/utils</code> merges Tailwind class strings, handles conditional class logic, and de-duplicates conflicting Tailwind utilities. Use it instead of template literals whenever you have conditional classes.</p>
<h3 id="heading-last-tailwind-variant"><code>last:</code> Tailwind Variant</h3>
<p>The <code>last:</code> pseudo-class variant targets the last child element in a group. In the Statistics Card, <code>last:border-e-0</code> remove the trailing border from the final column without any index tracking in JavaScript.</p>
<h3 id="heading-css-logical-properties">CSS Logical Properties</h3>
<p><code>border-e</code> means "border at the inline end," which is the right side in LTR layouts and the left side in RTL layouts. Using logical properties like <code>border-e</code>, <code>ps-</code>, and <code>pe-</code> instead of <code>border-r</code>, <code>pl-</code>, and <code>pr-</code> makes your components locale-aware by default.</p>
<h3 id="heading-typescript-optional-props-with-defaults">TypeScript Optional Props with Defaults</h3>
<p>Assigning a default value directly in the function signature, like <code>({ mainDashboard = mainDashboardData }: WidgetProps)</code>, is a clean pattern for components that need sensible fallback data while still being configurable. It works for demos, Storybook, and production use with real API data.</p>
<h3 id="heading-use-client-in-nextjs-app-router"><code>"use client"</code> in Next.js App Router</h3>
<p>Any component that uses <code>useState</code>, browser APIs, or client-only packages like <code>@iconify/react</code> needs the <code>"use client"</code> directive at the top of the file. Without it, the Next.js App Router will try to render the component on the server and throw an error.</p>
<h3 id="heading-suppresshydrationwarning"><code>suppressHydrationWarning</code></h3>
<p>When a value rendered on the server (for example, the current date or time) differs from the value rendered on the client due to timezone differences, React throws a hydration mismatch warning. Adding <code>suppressHydrationWarning</code> to the specific element silences the warning without affecting the rest of the component tree.</p>
<h3 id="heading-css-ripple-animation-pattern">CSS Ripple Animation Pattern</h3>
<p>A CSS ripple effect can be built without JavaScript by using a <code>scale-0</code> to <code>scale-[N]</code> transition on a <code>rounded-full</code> element placed inside an <code>overflow-hidden</code> container. On hover, the circle expands and gets clipped by the container boundary. The label text sits above it with <code>relative z-10</code>.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>In this tutorial, you built four production-ready card components using shadcn/ui and Base UI primitives:</p>
<ol>
<li><p><strong>Preview Card</strong>: group hover image animation, overflow clipping, and a logical border amenity row</p>
</li>
<li><p><strong>Analytics Card</strong>: typed props with default data, conditional badge colors with <code>cn()</code>, and an absolutely-positioned decorative image</p>
</li>
<li><p><strong>Statistics Card</strong>: data-driven repeating layout, responsive flex columns, and automatic last-border removal with <code>last:</code></p>
</li>
<li><p><strong>Ecommerce Product Variant Card</strong>: named hover groups, a CSS ripple button, ordinal date formatting, and hydration warning suppression</p>
</li>
</ol>
<p>Each card is installed with one CLI command and lives in your project's source tree. You own the code and can modify anything to fit your design system.</p>
<p>The patterns covered here, from named group hover scopes to typed props with defaults to logical CSS properties, apply well beyond card components. You'll find them useful across most UI components you build with shadcn/ui and Tailwind CSS.</p>
<h2 id="heading-resources"><strong>Resources</strong></h2>
<ul>
<li><p><a href="https://shadcnspace.com/"><strong>Shadcn UI</strong></a>: Component library and documentation home</p>
</li>
<li><p><a href="https://shadcnspace.com/components/card"><strong>Shadcn Card Component Collection</strong></a>: All card variants available in Radix and Base UI</p>
</li>
<li><p><a href="https://shadcnspace.com/docs/getting-started/how-to-use-shadcn-cli"><strong>How to Use the Shadcn CLI</strong></a>: Getting started guide for the CLI and registry setup</p>
</li>
<li><p><a href="https://shadcnspace.com/cli"><strong>Shadcn CLI Reference</strong></a>: Full CLI command reference</p>
</li>
<li><p><a href="https://shadcnspace.com/docs/getting-started/component"><strong>Component Getting Started Guide</strong></a>: How to install and use individual components</p>
</li>
<li><p><a href="https://shadcnspace.com/components"><strong>Shadcn Components</strong></a>: Full component library with all available categories</p>
</li>
<li><p><a href="https://shadcnspace.com/blocks/dashboard-ui"><strong>Shadcn Dashboard UI Blocks</strong></a>: Ready-to-use dashboard layout blocks built from the same system</p>
</li>
<li><p><a href="https://ui.shadcn.com/docs"><strong>Official Shadcn/ui Documentation</strong></a></p>
</li>
<li><p><a href="https://shadcnspace.com/figma"><strong>Shadcn Figma Kit</strong></a>: Figma UI kit that mirrors the Shadcn Space component system</p>
</li>
<li><p><a href="https://youtu.be/n6dvjVxy02U?si=EXfClzSyI8D97VaI"><strong>Video Walkthrough: How to Use Shadcn Space with CLI</strong></a>: YouTube tutorial for CLI setup and component installation</p>
</li>
<li><p><a href="https://www.npmjs.com/package/@iconify/react"><strong>@iconify/react on npm</strong></a>: Icon library used in the Statistics Card</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Dark Mode Toggle Without JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ Over the years, I've worked on many Static Site Generated (SSG) websites that work without JavaScript. And during that time, I've created a few solutions. One of them is a dark mode toggle that doesn' ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-dark-mode-toggle-without-javascript/</link>
                <guid isPermaLink="false">6a4bb3bf6aa89de94c683aa8</guid>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ dark mode ]]>
                    </category>
                
                    <category>
                        <![CDATA[ HTML ]]>
                    </category>
                
                    <category>
                        <![CDATA[ CSS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jakub T. Jankiewicz ]]>
                </dc:creator>
                <pubDate>Mon, 06 Jul 2026 13:55:11 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/166e89af-ab96-48c8-bf0b-cbc5972aad2c.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Over the years, I've worked on many Static Site Generated (SSG) websites that work without JavaScript. And during that time, I've created a few solutions. One of them is a dark mode toggle that doesn't require JavaScript.</p>
<p>I created this solution for my <a href="https://jakub.jankiewicz.org/blog/">own blog</a>, and then I enhanced it for the <a href="https://wikizeit.edu.pl/">WikiZEIT project</a>. I also included the improved version in my <a href="https://complite.jcubic.pl/">Eleventy starter "Complite"</a>.</p>
<p>In this article, I'll explain how to create a dark mode toggle with just HTML and CSS with help from the new <code>:has()</code> selector. I will also use CSS variables.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-why-should-websites-work-without-javascript">Why Should Websites Work Without JavaScript?</a></p>
</li>
<li><p><a href="#heading-html-structure">HTML Structure</a></p>
</li>
<li><p><a href="#heading-css-code">CSS Code</a></p>
<ul>
<li><p><a href="#heading-styling-the-toggle">Styling the Toggle</a></p>
</li>
<li><p><a href="#heading-css-variables-and-the-website-style">CSS Variables and the Website Style</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-why-should-websites-work-without-javascript">Why Should Websites Work Without JavaScript?</h2>
<p>If you're wondering why a website may benefit from using no JavaScript, there are several reasons, like accessibility (a11y), SEO, and AI visibility.</p>
<p>Modern screen readers can handle JavaScript websites, but this isn't the only thing you should care about. People might be using old computers or phones. Others might have poor internet connections and may disable JavaScript to use less bandwidth.</p>
<p>As for SEO, Google is one of the few major crawlers that can reliably render JavaScript-heavy pages, but rendering can still take extra time compared with indexing server-rendered HTML. Many AI crawlers and answer engines don't appear to execute JavaScript the way a browser does. They often work from the raw HTML response.</p>
<p>So if, for example, you have a React app that's purely client-side rendered, a bot that only reads the initial HTML may see little more than <code>&lt;div id="root"&gt;&lt;/div&gt;</code> until JavaScript runs.</p>
<p>To be clear, the issue isn't React itself, but client-side rendering. Server-side rendering, static generation, or prerendering can make your content available in the initial HTML so search engines and AI crawlers can read it more reliably.</p>
<p>But the point remains: if you want your website to be reliably more accessible to all people and machines, you should make it work even without JavaScript.</p>
<p>Note that your website doesn't need to be written in pure/only HTML and CSS. As mentioned above, if you use solutions like React, consider also using a framework like <a href="https://nextjs.org/">Next.js</a> with Static Site Rendering (SSR) or a Static Site Generator (SSG) like <a href="https://gohugo.io/">Hugo</a>. You can also consider modern SSG solutions like <a href="https://www.11ty.dev/">Eleventy</a> that I'm using.</p>
<p>To learn more about Elventy and Hugo, you can read those two articles:</p>
<ul>
<li><p><a href="https://www.freecodecamp.org/news/your-first-hugo-blog-a-practical-guide/">How to Create Your First Hugo Blog: a Practical Guide</a></p>
</li>
<li><p><a href="https://www.freecodecamp.org/news/learn-eleventy/">Learn the Eleventy Static Site Generator by Building and Deploying a Portfolio Website</a></p>
</li>
</ul>
<p>To learn more about Next.js, you can <a href="https://www.youtube.com/results?search_query=Next.js+tutorial">search videos on YouTube</a> (but remember to check if they explain the modern App Router, not the old Page Router).</p>
<h2 id="heading-html-structure">HTML Structure</h2>
<p>Alright, now that you understand why this approach can be useful, let's dive in and build our dark mode toggle. The HTML of the toggle uses HTML radio buttons:</p>
<pre><code class="language-html">&lt;div class="theme-toggle"&gt;
    &lt;input type="radio" id="mode_dark"
           name="mode" value="dark"&gt;
    &lt;label for="mode_dark"
           aria-label="Switch to dark mode"&gt;
      🌙
    &lt;/label&gt;
    &lt;input type="radio" id="mode_light"
            name="mode" value="light"&gt;
    &lt;label for="mode_light"
           aria-label="Switch to light mode"&gt;
      ☀️
     &lt;/label&gt;
&lt;/div&gt;
</code></pre>
<p>The above code uses moon and sun emojis.</p>
<h2 id="heading-css-code">CSS Code</h2>
<p>Now let's see the CSS part of the solution. The trick to making this work without JavaScript involves using this CSS <code>:has</code> pseudo-class:</p>
<pre><code class="language-css">:has(#mode_dark:checked)
</code></pre>
<p>The way this works is a bit like a parent selector that was always missing in CSS. If you have some code like this <code>p:has(img)</code>, it will match all <code>&lt;p&gt;</code> tags that have images inside.</p>
<p>In our case, <code>:has</code> will match when there's a radio button or checkbox selected anywhere on the page. This is the <code>:checked</code> part and and <code>#mode_dark</code> is the <code>id</code> of the input for the dark mode that we have in the <a href="#heading-html-structure">HTML Structure</a> section.</p>
<p>So to summarize, you can add <code>:has</code> to any element that can be styled when dark mode is selected. Here's an example:</p>
<pre><code class="language-css">html:has(#mode_dark:checked) p {
  color: white;
  background: black;
}
</code></pre>
<p>This CSS will style all <code>&lt;p&gt;</code> tags when dark mode is enabled.</p>
<p>The above example (plus the HTML) is everything you need to create a CSS-only dark mode switch.</p>
<h3 id="heading-styling-the-toggle">Styling the Toggle</h3>
<p>Here's the CSS that styles the toggle so only one emoji (sun or moon) is displayed at a time. This is only to make the toggle look nice.</p>
<p>The code also makes sure that the initially selected value is always the system-preferred mode.</p>
<pre><code class="language-css">/* style of the toggle */
.theme-toggle {
  display: flex;
  align-items: center;
}

/* hide the input */
.theme-toggle input[type="radio"] {
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.theme-toggle label {
    width: 40px; height: 40px;
    display: grid;
    place-items: center;
    cursor: pointer;
}

/* icon visibility
 *
 * default light with system and radio button overwrite
 */
label[for="mode_light"] { display: none; }
label[for="mode_dark"] { display: grid; }

@media (prefers-color-scheme: dark) {
  label[for="mode_light"] { display: grid; }
  label[for="mode_dark"] { display: none; }
}
:root:has(#mode_dark:checked) label[for="mode_light"] {
   display: grid;
}
:root:has(#mode_dark:checked) label[for="mode_dark"] {
   display: none;
}
:root:has(#mode_light:checked) label[for="mode_light"] {
   display: none;
}
:root:has(#mode_light:checked) label[for="mode_dark"] {
   display: grid;
}
</code></pre>
<p>The <code>:root</code> selector above means the root of the HTML tree. It's often used instead of <code>html</code>.</p>
<p>The CSS code <code>@media (prefers-color-scheme: dark)</code> is a media query, a way to add CSS code when special conditions are met. Here the media query is checking whether the user has system settings set to dark mode.</p>
<p>The code hides the inputs, and the labels control the toggle of the radio button. This is a common way to style radio buttons and checkboxes.</p>
<div>
<div>💡</div>
<div>The toggle always displays the mode that it's switching into. That's why in dark mode the sun is showing, not the moon.</div>
</div>

<h3 id="heading-css-variables-and-the-website-style">CSS Variables and the Website Style</h3>
<p>The last part is to style the website. Here we have CSS variables with only two colors. But in a real website, you might have all colors and settings for the dark/light mode that's applied to the whole page:</p>
<pre><code class="language-css">@media (prefers-color-scheme: dark) {
  :root {
    --bg: #252525; /* dark gray */
    --fg: #fff;    /* white */
  }
}
:root:has(#mode_dark:checked) {
  --bg: #252525; /* dark gray */
  --fg: #fff;    /* white */
}
:root:has(#mode_light:checked) {
  --bg: #fff;    /* white */
  --fg: #252525; /* dark gray */
}
</code></pre>
<p>It's useful to use CSS variables because you can put styles that change between dark and light mode in one place. And then you can use one variable instead of hardcoding each style all over your CSS file.</p>
<p>So instead of using code like this:</p>
<pre><code class="language-css">html:has(#mode_dark:checked) p {
  color: #fff;
  background: #252525;
}
</code></pre>
<p>You can use variables:</p>
<pre><code class="language-css">html:has(#mode_dark:checked) p {
    background: var(--bg);
    color: var(--fg);
}
</code></pre>
<p>In case of background and foreground colors, you only need this code:</p>
<pre><code class="language-css">body {
  background: var(--bg);
  color: var(--fg);
}
</code></pre>
<p>You can read more about CSS variables with the <code>:root</code> selector in this article:</p>
<ul>
<li><a href="https://www.freecodecamp.org/news/what-are-css-variables-and-how-to-use-them/">CSS Variables Definition – What are CSS Vars and How to Use Them?</a></li>
</ul>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>When creating a website, it's always worth making it work without JavaScript. It's good for accessibility and SEO.</p>
<p>Now with modern CSS, most of the things a website needs can be done without JS. You should incorporate <a href="https://en.wikipedia.org/wiki/Progressive_enhancement">progressive enhancement</a> and add JavaScript on top of the existing HTML/CSS foundation.</p>
<p>To read more about progressive enhancement, check this article: <a href="https://www.freecodecamp.org/news/what-is-progressive-enhancement-and-why-it-matters-e80c7aaf834a/">What is Progressive Enhancement, and why it matters</a>.</p>
<p>And here is a <a href="https://codepen.io/jcubic/pen/myRqrmj">CodePen demo of the whole solution</a>.</p>
<p>If you have any questions, you can contact me on <a href="https://x.com/jcubic">Twitter/X</a>. My DMs are open. You can also check out my <a href="https://jakub.jankiewicz.org/blog/">personal blog</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Browser-Based PDF Analyzer Using JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ PDF files are one of the most widely used document formats for sharing reports, invoices, contracts, books, research papers, manuals, forms, and business documents. Although viewing a PDF is simple, u ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-pdf-analyzer-javascript/</link>
                <guid isPermaLink="false">6a47d7568dc454430aaca51a</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                    <category>
                        <![CDATA[ freeCodeCamp.org ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Bhavin Sheth ]]>
                </dc:creator>
                <pubDate>Fri, 03 Jul 2026 15:37:58 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/a009f906-4ae4-4808-99fa-a31b419f66d5.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>PDF files are one of the most widely used document formats for sharing reports, invoices, contracts, books, research papers, manuals, forms, and business documents. Although viewing a PDF is simple, understanding what's inside the document is often much more difficult.</p>
<p>For example, you may need to know how many pages a PDF contains, whether it's password protected, who created it, what metadata it includes, how much text it contains, which fonts are used, or whether the document contains embedded images.</p>
<p>Manually inspecting all of this information can be time-consuming, especially when working with large collections of PDF files.</p>
<p>A PDF Analyzer solves this problem by automatically extracting detailed information from a document. Instead of opening the file in multiple applications, users can upload a PDF once and instantly view metadata, security settings, text statistics, image information, page details, fonts, and much more.</p>
<p>In this tutorial, you'll build a browser-based PDF Analyzer using JavaScript. The application allows users to upload a PDF, preview its pages, configure analysis options, perform different levels of document analysis, inspect the extracted information, and export a complete analysis report in multiple formats.</p>
<p>Everything runs directly inside the browser without requiring a backend server, making document analysis fast, private, and secure.</p>
<p>By the end of this tutorial, you'll have a fully functional PDF Analyzer capable of examining both simple and complex PDF documents.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/ba3b5025-7320-422d-a0ca-c96858c3ea73.png" alt="allinonetools pdf tools pdf analyzer tool" style="display:block;margin:0 auto" width="574" height="277" loading="lazy">

<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-why-pdf-analysis-is-useful">Why PDF Analysis Is Useful</a></p>
</li>
<li><p><a href="#heading-how-pdf-analysis-works">How PDF Analysis Works</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-what-library-are-we-using">What Library Are We Using?</a></p>
</li>
<li><p><a href="#heading-creating-the-upload-interface">Creating the Upload Interface</a></p>
</li>
<li><p><a href="#heading-previewing-uploaded-pdf-pages">Previewing Uploaded PDF Pages</a></p>
</li>
<li><p><a href="#heading-configuring-analysis-settings">Configuring Analysis Settings</a></p>
</li>
<li><p><a href="#heading-analyzing-the-pdf">Analyzing the PDF</a></p>
</li>
<li><p><a href="#heading-displaying-the-analysis-report">Displaying the Analysis Report</a></p>
</li>
<li><p><a href="#heading-exporting-the-analysis-report">Exporting the Analysis Report</a></p>
</li>
<li><p><a href="#heading-demo-how-the-pdf-analyzer-works">Demo: How the PDF Analyzer Works</a></p>
</li>
<li><p><a href="#heading-important-notes-from-real-world-use">Important Notes from Real-World Use</a></p>
</li>
<li><p><a href="#heading-common-mistakes-to-avoid">Common Mistakes to Avoid</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-why-pdf-analysis-is-useful">Why PDF Analysis Is Useful</h2>
<p>Most people think of a PDF as simply a document that can be viewed or printed, but every PDF contains much more information than what appears on the screen.</p>
<p>Behind every document is a collection of properties such as metadata, security settings, page information, fonts, embedded images, and document statistics. Accessing this information can help users better understand the document before editing, sharing, printing, or archiving it.</p>
<p>Businesses often receive hundreds of PDF files every day from clients, suppliers, government departments, and employees. Before these files are stored or distributed, they frequently need to be inspected to verify their contents. A PDF Analyzer makes this process much faster by automatically extracting important document information.</p>
<p>Legal professionals regularly review contracts and agreements where document properties such as creation dates, authorship, and security restrictions may be important. Instead of manually checking each document, an analyzer provides these details in seconds.</p>
<p>Educational institutions use PDF analysis when reviewing assignments, research papers, and digital course materials. Teachers and administrators can quickly inspect page counts, metadata, extracted text, and document properties before storing or distributing files.</p>
<p>Publishing companies analyze PDF files before printing books, manuals, catalogs, and magazines. Reviewing page sizes, fonts, metadata, and embedded resources helps identify formatting problems before production begins.</p>
<p>Government agencies and healthcare organizations also benefit from document analysis when processing applications, medical records, permits, forms, and official reports. Verifying document integrity before long-term storage helps reduce errors and maintain consistent records.</p>
<p>A PDF Analyzer is equally useful for developers. Before building editing tools such as watermarking, page rotation, cropping, metadata editing, or page extraction, developers often need to inspect the document structure to determine how it should be processed.</p>
<p>Because this application performs all analysis directly inside the browser, users can inspect sensitive documents without uploading them to external servers. This provides an additional layer of privacy while delivering instant results.</p>
<h2 id="heading-how-pdf-analysis-works">How PDF Analysis Works</h2>
<p>A PDF Analyzer reads the uploaded document and extracts useful information from its internal structure.</p>
<p>Once the user selects a PDF file, the browser loads the document into memory. Instead of modifying the PDF, the application examines its contents and collects various types of information that can later be displayed in a structured report.</p>
<p>The analysis begins by reading the document itself. Basic properties such as the filename, total number of pages, and file size are identified immediately.</p>
<p>Next, the application extracts metadata including the document title, author, subject, keywords, creator, producer, creation date, modification date, and PDF version.</p>
<p>The analyzer can also inspect security-related properties to determine whether the document is password protected or contains restrictions on printing, copying, or editing.</p>
<p>After processing the document structure, the application examines each page individually. It can count words, characters, images, fonts, estimate reading time, calculate speaking time, and even perform sentiment analysis on extracted text when OCR is enabled.</p>
<p>If the uploaded document consists of scanned pages instead of selectable text, OCR can be used to recognize text before analysis begins.</p>
<p>Once all information has been collected, the application generates a complete report that can be viewed inside the browser or exported as a PDF, JSON, CSV, or text file.</p>
<p>Since the entire workflow runs locally, the original document remains on the user's device throughout the process.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>We'll build this project using standard web technologies.</p>
<p>Create the following files:</p>
<pre><code class="language-text">pdf-analyzer/

│── index.html

│── style.css

│── script.js
</code></pre>
<p>Next, include the required libraries inside <strong>index.html</strong>.</p>
<pre><code class="language-html">&lt;script src="https://unpkg.com/pdf-lib"&gt;&lt;/script&gt;

&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.min.js"&gt;&lt;/script&gt;

&lt;script src="https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js"&gt;&lt;/script&gt;

&lt;script src="https://cdn.jsdelivr.net/npm/chart.js"&gt;&lt;/script&gt;
</code></pre>
<p>These libraries provide everything needed for PDF loading, rendering, OCR processing, and report visualization.</p>
<h2 id="heading-what-library-are-we-using">What Library Are We Using?</h2>
<p>This project combines several JavaScript libraries because no single library can perform every type of PDF analysis.</p>
<p>The primary library is <strong>PDF-lib</strong>, which allows the application to load PDF documents and access important document properties such as metadata and page information. It's lightweight, fast, and runs entirely inside modern browsers.</p>
<p>The project also uses <strong>PDF.js</strong> to render document pages for previews. This enables users to visually inspect uploaded PDFs before running the analysis.</p>
<p>For scanned documents that don't contain selectable text, <strong>Tesseract.js</strong> provides Optical Character Recognition (OCR). It recognizes text directly inside the browser, making it possible to analyze scanned PDFs without requiring any server-side processing.</p>
<p>To visualize analysis results, we'll use <strong>Chart.js</strong> for generating simple graphs and statistics such as word counts, sentiment distribution, and other document metrics.</p>
<p>Together, these libraries create a powerful browser-based PDF Analyzer capable of extracting metadata, rendering previews, recognizing scanned text, generating statistics, and exporting detailed analysis reports while keeping every document completely private.</p>
<h2 id="heading-creating-the-upload-interface">Creating the Upload Interface</h2>
<p>Every PDF workflow begins with selecting a document. Before any analysis can take place, users need a simple and reliable way to upload one or more PDF files into the browser.</p>
<p>A good upload interface should clearly indicate that only PDF documents are accepted while supporting both drag-and-drop uploads and the traditional file picker. This makes the tool easy to use regardless of whether users are working on a desktop or a mobile device.</p>
<p>In this project, the upload area acts as the entry point for the entire analysis process. When a user selects a PDF, the browser validates the file type, loads the document into memory, and prepares it for previewing and analysis. Since everything happens locally, the original PDF never leaves the user's device.</p>
<p>Our upload component displays a drag-and-drop area, a browse button, and helpful instructions that guide users through the first step of the workflow.</p>
<p>Here's the HTML for the upload area:</p>
<pre><code class="language-html">&lt;div class="upload-container"&gt;

    &lt;div id="dropZone" class="drop-zone"&gt;

        &lt;div class="upload-icon"&gt;
            ☁
        &lt;/div&gt;

        &lt;h2&gt;Drag &amp; Drop PDF Here&lt;/h2&gt;

        &lt;p&gt;Or click to browse file&lt;/p&gt;

        &lt;button id="selectPDF"&gt;
            Select PDF
        &lt;/button&gt;

        &lt;input
            type="file"
            id="pdfInput"
            accept="application/pdf"
            hidden&gt;

    &lt;/div&gt;

&lt;/div&gt;
</code></pre>
<p>Next, register the file input and handle PDF selection.</p>
<pre><code class="language-javascript">const pdfInput = document.getElementById("pdfInput");

pdfInput.addEventListener("change", async (event) =&gt; {

    const file = event.target.files[0];

    if (!file) return;

    if (file.type !== "application/pdf") {

        alert("Please select a valid PDF file.");

        return;

    }

    loadPDF(file);

});
</code></pre>
<p>This validation prevents unsupported file types from being processed while ensuring the application only loads valid PDF documents.</p>
<p>After the upload interface is complete, users can immediately select a document and move to the preview stage.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/5a12d42f-494d-434f-8717-66b7563c6c52.png" alt="PDF upload interface allowing users to drag and drop or browse for a PDF document before analysis." style="display:block;margin:0 auto" width="740" height="548" loading="lazy">

<h2 id="heading-previewing-uploaded-pdf-pages">Previewing Uploaded PDF Pages</h2>
<p>Once a PDF has been uploaded, it's helpful to display a visual preview before starting the analysis. This allows users to verify that they selected the correct document and quickly inspect its pages.</p>
<p>Instead of showing only the file name, our application renders thumbnail previews of every page in the PDF. Users can scroll through the thumbnails to inspect the document and confirm that all pages loaded successfully.</p>
<p>Displaying previews also improves the user experience because it gives immediate visual feedback while the document is being prepared for analysis.</p>
<p>The browser uses PDF.js to render each page as a canvas before converting it into an image that can be displayed inside the page preview grid.</p>
<p>The following code loads the PDF document:</p>
<pre><code class="language-javascript">const pdf = await pdfjsLib.getDocument({

    data: await file.arrayBuffer()

}).promise;
</code></pre>
<p>Next, render each page:</p>
<pre><code class="language-javascript">for (let pageNumber = 1; pageNumber &lt;= pdf.numPages; pageNumber++) {

    const page = await pdf.getPage(pageNumber);

    const viewport = page.getViewport({

        scale: 0.35

    });

    const canvas = document.createElement("canvas");

    const context = canvas.getContext("2d");

    canvas.width = viewport.width;

    canvas.height = viewport.height;

    await page.render({

        canvasContext: context,

        viewport

    }).promise;

    previewContainer.appendChild(canvas);

}
</code></pre>
<p>Each page is rendered independently, making it possible to preview documents containing dozens or even hundreds of pages.</p>
<p>The preview shown in this project displays all page thumbnails together, making it easy to verify page order before continuing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f74f0c85-6910-445f-b005-710c312a6281.png" alt="Uploaded PDF preview displaying page thumbnails before document analysis begins." style="display:block;margin:0 auto" width="745" height="705" loading="lazy">

<h2 id="heading-configuring-analysis-settings">Configuring Analysis Settings</h2>
<p>Before analyzing the document, users can customize how the application should examine the PDF.</p>
<p>Different documents require different levels of analysis. Some users may only need basic information such as the page count and metadata, while others may want detailed statistics about extracted text, embedded images, fonts, security permissions, and OCR results.</p>
<p>To support these different scenarios, the PDF Analyzer provides several configurable options before processing begins.</p>
<p>The first option allows users to choose which pages should be analyzed. They can analyze every page in the document or specify a custom page range when only certain pages are relevant.</p>
<p>For scanned PDFs, OCR can be enabled to recognize text that's stored as images rather than selectable characters. Users can also select the OCR language before processing starts.</p>
<p>Finally, the application offers multiple analysis levels. Basic mode extracts essential document information such as metadata and security properties. Standard mode additionally collects text and image statistics. Advanced mode performs the most detailed inspection available, including fonts, page-level statistics, OCR processing, and sentiment analysis.</p>
<p>The analysis settings panel gives users complete control over how the document should be processed while keeping the interface simple and easy to understand.</p>
<p>Here's the HTML used for the settings panel:</p>
<pre><code class="language-html">&lt;select id="analysisLevel"&gt;

    &lt;option value="basic"&gt;
        Basic (Info, Metadata, Security)
    &lt;/option&gt;

    &lt;option value="standard"&gt;
        Standard (Basic + Text &amp; Image Stats)
    &lt;/option&gt;

    &lt;option value="advanced"&gt;
        Advanced (All Features)
    &lt;/option&gt;

&lt;/select&gt;
</code></pre>
<p>Users can also enable OCR when analyzing scanned PDF documents:</p>
<pre><code class="language-javascript">const enableOCR = document.getElementById("enableOCR").checked;

const language = document.getElementById("ocrLanguage").value;

if (enableOCR) {

    console.log("OCR Enabled");

    console.log(language);

}
</code></pre>
<p>Finally, capture the selected analysis level:</p>
<pre><code class="language-javascript">const level = document.getElementById("analysisLevel").value;

switch (level) {

    case "basic":

        runBasicAnalysis();

        break;

    case "standard":

        runStandardAnalysis();

        break;

    case "advanced":

        runAdvancedAnalysis();

        break;

}
</code></pre>
<p>These settings allow the application to adapt to many different types of PDF documents, from simple text files to complex scanned reports containing images, metadata, and security restrictions.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/62bf34b8-e706-44b3-a2b9-7f6481ed98ff.png" alt="PDF analysis settings showing page selection, OCR configuration, language selection, and available analysis levels." style="display:block;margin:0 auto" width="741" height="703" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/886c865b-fa4a-42aa-95d2-9b507cfbe43b.png" alt="OCR language selection dropdown with multiple supported languages." style="display:block;margin:0 auto" width="731" height="254" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/39370da7-2caf-4536-9603-03562b6206ce.png" alt="Analysis level selector showing Basic, Standard, and Advanced PDF analysis modes." style="display:block;margin:0 auto" width="670" height="174" loading="lazy">

<h2 id="heading-analyzing-the-pdf">Analyzing the PDF</h2>
<p>Once the PDF has been uploaded, previewed, and the analysis options have been configured, the application is ready to examine the document.</p>
<p>Unlike editing tools that modify pages, a PDF Analyzer inspects the document and extracts useful information without changing the original file. The analyzer reads the PDF structure, examines each page, and collects information that can later be displayed in a detailed report.</p>
<p>The analysis begins by loading the uploaded document into memory. From there, the application extracts basic information such as the filename, file size, total number of pages, and document validity. It then reads metadata including the title, author, subject, creator, producer, creation date, modification date, and PDF version.</p>
<p>Depending on the selected analysis level, the application can also inspect security permissions, count words and characters, estimate reading time, identify embedded images, list fonts used throughout the document, and perform OCR on scanned PDFs. When OCR is enabled, the analyzer converts scanned images into searchable text before calculating document statistics.</p>
<p>Because the application processes everything inside the browser, users receive instant results while maintaining complete privacy.</p>
<p>The first step is loading the uploaded PDF:</p>
<pre><code class="language-javascript">async function analyzePDF(file){

    const bytes = await file.arrayBuffer();

    const pdf = await PDFLib.PDFDocument.load(bytes);

    return pdf;

}
</code></pre>
<p>Next, extract the document metadata:</p>
<pre><code class="language-javascript">const metadata = {

    title: pdf.getTitle(),

    author: pdf.getAuthor(),

    subject: pdf.getSubject(),

    creator: pdf.getCreator(),

    producer: pdf.getProducer(),

    keywords: pdf.getKeywords(),

    creationDate: pdf.getCreationDate(),

    modificationDate: pdf.getModificationDate()

};
</code></pre>
<p>Basic document information is also collected:</p>
<pre><code class="language-javascript">const fileInfo = {

    fileName: file.name,

    fileSize: file.size,

    totalPages: pdf.getPageCount(),

    valid: true

};
</code></pre>
<p>If the user selects Advanced Analysis, additional routines extract page statistics, fonts, images, OCR results, and text analysis:</p>
<pre><code class="language-javascript">if(selectedLevel === "advanced"){

    analyzeFonts();

    analyzeImages();

    analyzeText();

    performOCR();

}
</code></pre>
<p>Once every analysis step has finished, the application combines the collected information into a single report object that will be displayed in the next stage.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f5fbb355-dabb-4536-ae09-d2c3e79c2c4c.png" alt="Analyze PDF button used to generate a complete PDF analysis report." style="display:block;margin:0 auto" width="399" height="70" loading="lazy">

<h2 id="heading-displaying-the-analysis-report">Displaying the Analysis Report</h2>
<p>After processing is complete, the application presents the collected information inside a structured report.</p>
<p>Instead of showing raw JSON or technical output, the report organizes related information into separate cards. This layout makes it much easier for users to understand large amounts of document information.</p>
<p>The first section displays basic document information, including the filename, file size, total number of pages, and validation status.</p>
<p>The metadata section contains properties such as the document title, author, subject, keywords, creator, producer, PDF version, creation date, and modification date.</p>
<p>Security information indicates whether the document is password protected and whether printing, copying, or modification restrictions are present.</p>
<p>When text analysis is enabled, the report includes the total word count, character count, average words per page, estimated reading time, and estimated speaking time. If OCR has been performed, the extracted text is also analyzed to calculate sentiment statistics.</p>
<p>Additional cards display image information, embedded fonts, and page-by-page extracted text for users who need a deeper inspection of the document.</p>
<p>The following example creates a simple report section:</p>
<pre><code class="language-javascript">function renderBasicInfo(info){

    document.getElementById("fileName").textContent = info.fileName;

    document.getElementById("pageCount").textContent = info.totalPages;

    document.getElementById("fileSize").textContent = info.fileSize;

}
</code></pre>
<p>Rendering the metadata is straightforward:</p>
<pre><code class="language-javascript">function renderMetadata(metadata){

    title.innerText = metadata.title;

    author.innerText = metadata.author;

    creator.innerText = metadata.creator;

    producer.innerText = metadata.producer;

}
</code></pre>
<p>Page-wise extracted content can also be displayed:</p>
<pre><code class="language-javascript">pages.forEach((page,index)=&gt;{

    createPageCard(

        index + 1,

        page.text

    );

});
</code></pre>
<p>Organizing the results into individual sections allows users to quickly locate the information they need without scrolling through large blocks of text.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/08b89c29-48ee-4fd2-a5c1-059c2b61e732.png" alt="PDF analysis report displaying metadata, security information, text statistics, image information, fonts, and document insights." style="display:block;margin:0 auto" width="674" height="715" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/b84c18f4-5cfc-4ea5-82c6-c4bf1b45495d.png" alt="Page-wise extracted text generated during PDF document analysis." style="display:block;margin:0 auto" width="660" height="880" loading="lazy">

<h2 id="heading-exporting-the-analysis-report">Exporting the Analysis Report</h2>
<p>After reviewing the analysis results, users often need to save the report for future reference or share it with colleagues.</p>
<p>To support different workflows, the PDF Analyzer allows the report to be exported in several formats. Depending on the user's needs, the report can be downloaded as a PDF document, JSON file, CSV spreadsheet, or plain text file.</p>
<p>PDF reports are useful for documentation and sharing with clients or team members. JSON exports are ideal for developers who want to process the analysis programmatically. CSV files can be opened in spreadsheet applications for further analysis, while text files provide a simple human-readable version of the report.</p>
<p>Providing multiple export formats makes the analyzer suitable for business users, developers, researchers, and system administrators alike.</p>
<p>The following example creates a JSON export:</p>
<pre><code class="language-javascript">const report = JSON.stringify(

    analysisResult,

    null,

    2

);
</code></pre>
<p>Create a downloadable file:</p>
<pre><code class="language-javascript">const blob = new Blob(

    [report],

    {

        type:"application/json"

    }

);
</code></pre>
<p>Generate the download link:</p>
<pre><code class="language-javascript">const url = URL.createObjectURL(blob);

const link = document.createElement("a");

link.href = url;

link.download = "analysis-report.json";

link.click();
</code></pre>
<p>The export menu allows users to choose the most appropriate output format before downloading the completed report.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/bed0c18f-4e25-492c-88c8-24423ce0f6b1.png" alt="bed0c18f-4e25-492c-88c8-24423ce0f6b1" style="display:block;margin:0 auto" width="901" height="373" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/39ce70f7-d2dd-4c5a-9fe1-95eeb0c0b0ba.png" alt="Export format dropdown allowing users to select PDF, JSON, CSV, or text output before downloading." style="display:block;margin:0 auto" width="631" height="201" loading="lazy">

<h2 id="heading-demo-how-the-pdf-analyzer-works">Demo: How the PDF Analyzer Works</h2>
<h3 id="heading-step-1-upload-your-pdf-file">Step 1: Upload Your PDF File</h3>
<p>The process begins by uploading a PDF document using either the drag-and-drop area or the file selection button.</p>
<p>Once a file is selected, the browser validates that it's a PDF before loading it into memory. Because the application runs entirely inside the browser, the uploaded document never leaves the user's device, making the tool suitable for confidential business reports, contracts, invoices, research papers, legal documents, and other sensitive files.</p>
<p>After the PDF is loaded successfully, the application prepares it for page preview generation and document analysis.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/cce8e552-7d7d-4ec0-ac98-0312ae9b2395.png" alt="PDF upload interface allowing users to drag and drop or browse for a PDF document before analysis." style="display:block;margin:0 auto" width="740" height="548" loading="lazy">

<h3 id="heading-step-2-preview-uploaded-pdf-pages">Step 2: Preview Uploaded PDF Pages</h3>
<p>After the document has been loaded, the application generates page previews for the uploaded PDF.</p>
<p>Displaying page thumbnails allows users to confirm that the correct file has been selected before analysis begins. Users can quickly browse through the document, inspect page order, and verify that every page has loaded successfully.</p>
<p>This visual preview also helps identify scanned pages, blank pages, or unexpected formatting issues before processing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/80d61017-4222-44a4-8c0b-71a17e9fa3aa.png" alt="Uploaded PDF page thumbnails displayed before document analysis." style="display:block;margin:0 auto" width="745" height="705" loading="lazy">

<h3 id="heading-step-3-configure-analysis-settings">Step 3: Configure Analysis Settings</h3>
<p>Next, users configure how the PDF should be analyzed.</p>
<p>The tool allows users to choose whether every page or only a specific page range should be processed. For scanned PDFs, OCR can be enabled to recognize text stored as images, and users can select the appropriate recognition language.</p>
<p>The application also offers multiple analysis levels. Basic mode extracts essential document properties, Standard mode adds text and image statistics, and Advanced mode performs a more detailed inspection that includes fonts, OCR, page-level information, sentiment analysis, and additional document insights.</p>
<p>These settings allow users to customize the analysis based on the type of PDF they are working with.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/c6b089c7-006e-4661-8890-f093bea294e8.png" alt="PDF analysis settings showing page selection, OCR configuration, language selection, and analysis level options." style="display:block;margin:0 auto" width="741" height="703" loading="lazy">

<h3 id="heading-step-4-analyze-the-pdf">Step 4: Analyze the PDF</h3>
<p>Once the settings have been reviewed, users simply click the <strong>Analyze PDF</strong> button.</p>
<p>The browser reads the uploaded document and extracts the selected information. Depending on the chosen analysis level, the application examines metadata, security settings, page information, extracted text, fonts, embedded images, and OCR results.</p>
<p>Although large documents may require a few additional seconds, the entire analysis is completed locally without uploading the PDF to a remote server.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/8eda0352-95ba-4735-914d-3b95ff975f30.png" alt="Analyze PDF button used to generate the document analysis report." style="display:block;margin:0 auto" width="399" height="70" loading="lazy">

<h3 id="heading-step-5-review-the-analysis-report">Step 5: Review the Analysis Report</h3>
<p>After processing is complete, the application displays a comprehensive analysis report.</p>
<p>The report is divided into multiple sections that make it easy to inspect different aspects of the document. Users can review basic document information, metadata, security settings, extracted text statistics, page information, fonts, embedded images, OCR results, estimated reading time, speaking time, and sentiment analysis.</p>
<p>Each section is organized into individual cards so that important information can be located quickly.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/850b54a3-74a5-4617-bdf3-faac236a0eee.png" alt="PDF analysis report displaying metadata, security settings, text statistics, fonts, images, and document insights." style="display:block;margin:0 auto" width="674" height="715" loading="lazy">

<h3 id="heading-step-6-review-page-level-analysis">Step 6: Review Page-Level Analysis</h3>
<p>For users who need more detailed information, the application also displays page-by-page analysis.</p>
<p>Each page can include extracted text, OCR output, word count, image statistics, page dimensions, and additional information collected during processing.</p>
<p>This level of detail is especially useful when analyzing large reports, scanned books, research papers, contracts, technical documentation, and multi-page business documents.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/dea81704-27d8-44a5-a0bd-8756659fcef2.png" alt="Page-by-page PDF analysis showing extracted content and document statistics." style="display:block;margin:0 auto" width="660" height="880" loading="lazy">

<h3 id="heading-step-7-export-the-analysis-report">Step 7: Export the Analysis Report</h3>
<p>After reviewing the analysis, users can export the report for future reference.</p>
<p>The tool supports multiple export formats, including PDF, JSON, CSV, and plain text. This allows developers, researchers, businesses, and system administrators to choose the format that best fits their workflow.</p>
<p>Exported reports can be archived, shared with team members, imported into other systems, or used for additional processing.</p>
<p>Once the desired format is selected, the browser generates the report and downloads it instantly.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/3b799aaa-8c47-4670-8d5e-77bac599c550.png" alt="Export analysis report section showing download options for PDF, JSON, CSV, and text files." style="display:block;margin:0 auto" width="901" height="373" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/2f33c51b-d52b-43e6-979b-00eaeafe3162.png" alt="Export format selector allowing users to choose PDF, JSON, CSV, or text output before downloading." style="display:block;margin:0 auto" width="631" height="201" loading="lazy">

<h2 id="heading-important-notes-from-real-world-use">Important Notes from Real-World Use</h2>
<p>A PDF Analyzer can process everything from a single-page document to large reports containing hundreds of pages. While modern browsers handle most documents efficiently, larger files containing high-resolution images or scanned pages may require additional processing time, especially when OCR is enabled.</p>
<p>Before starting the analysis, it's good practice to validate the uploaded file.</p>
<pre><code class="language-javascript">if (file.type !== "application/pdf") {

    alert("Please upload a valid PDF document.");

    return;

}
</code></pre>
<p>If OCR is enabled, remember that recognizing text from scanned pages takes longer than extracting text from a standard searchable PDF. Users should only enable OCR when it's actually needed.</p>
<pre><code class="language-javascript">if(enableOCR){

    console.log("Running OCR Analysis...");

}
</code></pre>
<p>When analyzing very large documents, processing pages individually helps reduce memory usage and keeps the browser responsive.</p>
<pre><code class="language-javascript">for(let page = 1; page &lt;= pdf.numPages; page++){

    analyzePage(page);

}
</code></pre>
<p>Before exporting the report, review the extracted information to ensure metadata, text statistics, page information, and OCR results are accurate.</p>
<h2 id="heading-common-mistakes-to-avoid">Common Mistakes to Avoid</h2>
<p>One common mistake is running OCR on documents that already contain selectable text.</p>
<p>OCR is designed for scanned PDFs where text exists only as images. Running OCR on searchable PDFs increases processing time without improving the analysis.</p>
<pre><code class="language-javascript">if(pdfContainsText){

    enableOCR = false;

}
</code></pre>
<p>Another mistake is selecting the wrong analysis level.</p>
<p>For example, users who only need metadata and document properties can choose <strong>Basic Analysis</strong> instead of <strong>Advanced Analysis</strong>, which performs additional processing such as OCR, font inspection, sentiment analysis, and image detection.</p>
<pre><code class="language-javascript">const analysisLevel = "basic";

console.log(analysisLevel);
</code></pre>
<p>Some users also forget to verify the page selection before starting the analysis.</p>
<p>When working with large reports, analyzing only the required pages can significantly reduce processing time.</p>
<pre><code class="language-javascript">const pageRange = "1-20";

console.log(pageRange);
</code></pre>
<p>Finally, always review the generated report before exporting it.</p>
<p>A quick inspection helps verify that metadata, page statistics, OCR output, document properties, and extracted text are accurate before downloading the final report.</p>
<p>Taking a few extra moments to validate the results can save considerable time when working with large document collections.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a browser-based PDF Analyzer using JavaScript.</p>
<p>You learned how to upload PDF files, preview document pages, configure analysis options, inspect metadata, analyze document structure, extract text, perform OCR, generate detailed reports, and export the analysis in multiple formats directly from the browser.</p>
<p>More importantly, you saw how modern browsers can inspect complex PDF documents without requiring a backend server or uploading files to third-party services.</p>
<p>This approach keeps document analysis fast, private, and secure while giving users valuable insights into the contents and structure of their PDF files.</p>
<p>You can try the complete implementation here:</p>
<p><strong>PDF Analyzer:</strong> <a href="https://allinonetools.net/pdf-analyzer/">https://allinonetools.net/pdf-analyzer/</a></p>
<p>Once you understand this workflow, you can extend the project further by adding AI-powered document summarization, keyword extraction, duplicate document detection, document comparison, accessibility analysis, compliance checking, digital signature validation, or advanced reporting dashboards.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build an AJAX Cart Drawer in Shopify (the 2026 Way) ]]>
                </title>
                <description>
                    <![CDATA[ Add a product to a Shopify store the default way and the whole page reloads. The shopper is looking at your product, they click Add to cart, and the browser throws the page away and rebuilds it. On a  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/shopify-ajax-cart-drawer/</link>
                <guid isPermaLink="false">6a47d58ad3e40bd062df32d5</guid>
                
                    <category>
                        <![CDATA[ shopify ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Shopify Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ecommerce ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ baslefeber ]]>
                </dc:creator>
                <pubDate>Fri, 03 Jul 2026 15:30:18 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/16d974f7-5940-4fef-a053-6e1790ed6107.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Add a product to a Shopify store the default way and the whole page reloads. The shopper is looking at your product, they click <strong>Add to cart</strong>, and the browser throws the page away and rebuilds it.</p>
<p>On a slow connection, that's two or three seconds of blank screen. Sometimes they even land on <code>/cart</code>, a full page away from the thing they were about to buy, and the momentum is gone.</p>
<p>A cart drawer fixes that. It's the slide-out panel that appears when someone adds an item: the cart updates, a Checkout button sits right there, and the shopper never leaves the page they were on.</p>
<p>Nearly every high-converting Shopify store has one, and you don't need an app to build it. You need two Ajax endpoints and less than a hundred lines of JavaScript.</p>
<p>The catch is that most tutorials, and most AI coding tools, build it the fragile way. They keep a running count in a JavaScript variable and update the DOM by hand. That looks fine in a demo and falls apart the first time two lines merge into one, a discount lands, or a variant sells out between the click and the checkout.</p>
<p>This guide builds it the way a senior developer does, where the server is always the source of truth. Then it shows the 2026 addition that lets your drawer speak the same language as apps and AI shopping agents.</p>
<p>If you want to code along, open a development theme you can edit and build each piece as we go. Everything here runs on a stock Online Store 2.0 theme (Horizon or Dawn) with no app installed.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a424d9ed27378c517882227/a5246d30-ff15-4759-b6e5-9f38741fef3f.gif" alt="The finished cart drawer: a shopper clicks Add to cart and a panel slides in over the storefront with no page reload" style="display:block;margin:0 auto" width="560" height="339" loading="lazy">

<p><em>The finished drawer: add to cart, no page reload, the panel slides in showing the real cart.</em></p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-youll-build">What You'll Build</a></p>
</li>
<li><p><a href="#heading-step-1-the-drawer-markup">Step 1: The Drawer Markup</a></p>
</li>
<li><p><a href="#heading-step-2-add-to-cart-without-a-reload">Step 2: Add to Cart Without a Reload</a></p>
</li>
<li><p><a href="#heading-step-3-render-the-drawer-from-the-servers-truth">Step 3: Render the Drawer from the Server's Truth</a></p>
</li>
<li><p><a href="#heading-step-4-quantity-plus-and-minus-with-event-delegation">Step 4: Quantity Plus and Minus, with Event Delegation</a></p>
</li>
<li><p><a href="#heading-step-5-remove-a-line-and-clear-the-cart">Step 5: Remove a Line, and Clear the Cart</a></p>
</li>
<li><p><a href="#heading-step-6-re-rendering-with-the-section-rendering-api-the-version-youd-ship">Step 6: Re-Rendering with the Section Rendering API (the Version You'd Ship)</a></p>
</li>
<li><p><a href="#heading-the-2026-upgrade-standard-storefront-events-and-actions">The 2026 Upgrade: Standard Storefront Events and Actions</a></p>
</li>
<li><p><a href="#heading-why-this-matters">Why This Matters</a></p>
</li>
<li><p><a href="#heading-the-complete-files">The Complete Files</a></p>
</li>
<li><p><a href="#heading-wrap-up">Wrap Up</a></p>
</li>
</ul>
<h2 id="heading-what-youll-build">What You'll Build</h2>
<p>By the end, you'll have a drawer that:</p>
<ul>
<li><p>adds to cart over Ajax with no page reload</p>
</li>
<li><p>re-reads the cart after every change and treats that response as the truth</p>
</li>
<li><p>renders its own contents and slides open</p>
</li>
<li><p>handles quantity plus/minus with a single delegated listener</p>
</li>
<li><p>removes a line and clears the cart</p>
</li>
<li><p>and, as the final upgrade, exposes itself through Shopify's new standard storefront actions so apps and AI agents can drive it</p>
</li>
</ul>
<h3 id="heading-prerequisites">Prerequisites</h3>
<ul>
<li><p>A Shopify theme you can edit (examples assume Online Store 2.0, such as Horizon or Dawn)</p>
</li>
<li><p>Comfort with <code>fetch</code> and promises</p>
</li>
<li><p>Basic Liquid</p>
</li>
<li><p>No app, no framework, no build step</p>
</li>
</ul>
<h2 id="heading-step-1-the-drawer-markup">Step 1: The Drawer Markup</h2>
<p>You'll build the drawer as a section so it renders on every page, and then render the current cart <strong>server-side with Liquid</strong> before any JavaScript runs.</p>
<p>This matters: if a shopper arrives with items already in their cart, the drawer is correct on first paint, and your JavaScript only has to update it after changes. That's progressive enhancement, and it is the first thing a homemade build skips.</p>
<p>The <code>data-*</code> attributes below are the contract between the markup and the script. Everything the JavaScript touches, it finds through one of these attributes, never through a class name or a tag position.</p>
<pre><code class="language-liquid">{%- comment -%} sections/cart-drawer.liquid {%- endcomment -%}
&lt;button type="button" class="cart-toggle" data-cart-toggle&gt;
  Cart &lt;span class="cart-count" data-cart-count&gt;{{ cart.item_count }}&lt;/span&gt;
&lt;/button&gt;

&lt;aside class="drawer" data-drawer aria-label="Cart" aria-hidden="true"&gt;
  &lt;div class="drawer__head"&gt;
    &lt;h2&gt;Your cart&lt;/h2&gt;
    &lt;button type="button" class="drawer__close" data-drawer-close aria-label="Close cart"&gt;&amp;times;&lt;/button&gt;
  &lt;/div&gt;

  &lt;div class="drawer__body"&gt;
    &lt;p class="drawer__empty" data-drawer-empty {% if cart.item_count &gt; 0 %}style="display:none"{% endif %}&gt;
      Your cart is empty.
    &lt;/p&gt;
    &lt;ul class="drawer__items" data-drawer-items&gt;
      {%- for item in cart.items -%}
        &lt;li class="drawer__item" data-line data-line-key="{{ item.key }}" data-quantity="{{ item.quantity }}"&gt;
          {{ item.image | image_url: width: 96 | image_tag: class: 'drawer__item-img', loading: 'lazy', alt: item.product.title }}
          &lt;span class="drawer__item-title"&gt;{{ item.product.title }}&lt;/span&gt;
          &lt;span class="drawer__item-price"&gt;{{ item.final_line_price | money }}&lt;/span&gt;
        &lt;/li&gt;
      {%- endfor -%}
    &lt;/ul&gt;
  &lt;/div&gt;

  &lt;div class="drawer__foot"&gt;
    &lt;div class="drawer__subtotal"&gt;
      &lt;span&gt;Subtotal&lt;/span&gt;
      &lt;span data-cart-subtotal&gt;{{ cart.total_price | money }}&lt;/span&gt;
    &lt;/div&gt;
    &lt;p class="drawer__ship"&gt;Shipping &amp;amp; taxes calculated at checkout.&lt;/p&gt;
    &lt;a href="{{ routes.cart_url }}" class="drawer__checkout"&gt;Checkout&lt;/a&gt;
  &lt;/div&gt;
&lt;/aside&gt;
&lt;div class="drawer__scrim" data-drawer-scrim&gt;&lt;/div&gt;
</code></pre>
<p>Two details worth pausing on here: the line item uses <code>item.final_line_price</code>, not <code>item.line_price</code>. Both exist on the cart, but <code>final_line_price</code> reflects line-level discounts, so it's the number the shopper will actually be charged. And each <code>&lt;li&gt;</code> carries <code>data-line-key</code> and <code>data-quantity</code>, which the quantity and remove controls will read later.</p>
<p>The add button lives on your product card or product page and carries the variant id straight from Liquid, so the right variant is added every time:</p>
<pre><code class="language-liquid">{%- comment -%} in the product card / PDP {%- endcomment -%}
&lt;button
  type="button"
  class="pdp__add"
  data-add
  data-variant-id="{{ product.selected_or_first_available_variant.id }}"
&gt;
  Add to cart
&lt;/button&gt;
</code></pre>
<h2 id="heading-step-2-add-to-cart-without-a-reload">Step 2: Add to Cart Without a Reload</h2>
<p>Here is the whole pattern in one sentence: mutate the cart, then re-read it, then render and open. Written out, that is <code>POST /cart/add.js</code> to add the variant, <code>GET /cart.js</code> to read the entire cart back, and then paint the drawer from what came back.</p>
<pre><code class="language-javascript">// assets/cart-drawer.js
document.querySelectorAll("[data-add]").forEach(function (btn) {
  btn.addEventListener("click", function () {
    var id = Number(btn.getAttribute("data-variant-id"));
    fetch("/cart/add.js", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ id: id, quantity: 1 }),
    })
      .then(function (r) { return r.json(); })
      .then(function () { return refresh(); })  // re-read /cart.js
      .then(openDrawer);                         // then slide it in
  });
});
</code></pre>
<p>Why re-read the cart when <code>/cart/add.js</code> already returned something? Because the response to <code>add.js</code> describes the line you just added, not the whole cart, and the whole cart is what the drawer shows.</p>
<p>More importantly, Shopify is the one that decides the final cart. It might merge your new line into an existing one, apply an automatic discount, or reject a sold-out variant. The only way to be sure the drawer matches reality is to ask for reality. That's what <code>refresh()</code> does, and it's the single function in this whole file that's allowed to touch the cart UI:</p>
<pre><code class="language-javascript">function refresh() {
  return fetch("/cart.js").then(function (r) { return r.json(); }).then(render);
}
</code></pre>
<p>Both endpoints here (<code>/cart/add.js</code> and <code>/cart.js</code>) are part of Shopify's <a href="https://shopify.dev/docs/api/ajax">Ajax API</a>, which is available on every storefront with no setup.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a424d9ed27378c517882227/c58705c9-daa0-4650-980b-0db4e3facaa7.png" alt="The three-step loop: the shopper clicks Add to cart, the script posts to /cart/add.js, then re-reads /cart.js, then renders and opens the drawer" style="display:block;margin:0 auto" width="3666" height="2062" loading="lazy">

<p><em>Mutate, then re-read, then render. The same loop runs for add, for quantity changes, and for remove.</em></p>
<h2 id="heading-step-3-render-the-drawer-from-the-servers-truth">Step 3: Render the Drawer from the Server's Truth</h2>
<p><code>render(cart)</code> takes a <code>/cart.js</code> response and paints the drawer from it. Notice what it doesn't do: it never adds up a total or increments a counter. It reads <code>item_count</code> and <code>total_price</code> straight off the object Shopify handed back.</p>
<pre><code class="language-javascript">function money(cents) {
  return "$" + (cents / 100).toFixed(2);
}

function render(cart) {
  document.querySelector("[data-cart-count]").textContent = cart.item_count;
  document.querySelector("[data-cart-subtotal]").textContent = money(cart.total_price);

  var itemsEl = document.querySelector("[data-drawer-items]");
  var emptyEl = document.querySelector("[data-drawer-empty]");
  itemsEl.innerHTML = "";
  if (!cart.items.length) { emptyEl.style.display = "block"; return; }
  emptyEl.style.display = "none";

  cart.items.forEach(function (line) {
    var li = document.createElement("li");
    li.className = "drawer__item";
    li.setAttribute("data-line", "");
    li.setAttribute("data-line-key", line.key);
    li.setAttribute("data-quantity", line.quantity);
    li.innerHTML =
      '&lt;img class="drawer__item-img" src="' + (line.image || "") + '" alt=""&gt;' +
      '&lt;span class="drawer__item-title"&gt;' + line.title + "&lt;/span&gt;" +
      '&lt;span class="drawer__item-price"&gt;' + money(line.final_line_price) + "&lt;/span&gt;";
    itemsEl.appendChild(li);
  });
}
</code></pre>
<p>The one thing that trips people up is money. The Ajax API returns prices in <strong>cents</strong>. A line that costs <code>$18.99</code> comes back as <code>1899</code>. Forget to divide by 100 and you ship a <code>$1,899</code> coffee. The <code>money()</code> helper does that conversion in one place.</p>
<p>One production note: this reads <code>final_line_price</code>, which reflects line-level discounts, so it's the number the shopper is actually charged (<code>line_price</code> is the pre-discount amount). On a multi-currency store, swap the hand-rolled <code>money()</code> for Shopify's own money formatting so the symbol and decimals follow the active market.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a424d9ed27378c517882227/47f48e61-0d93-434c-914d-bf3a143214b8.gif" alt="The cart drawer open on a coffee storefront, showing one line item with its image, title and price, a subtotal, and a Checkout button" style="display:block;margin:0 auto" width="460" height="278" loading="lazy">

<p><em>The drawer rendered entirely from a</em> <code>/cart.js</code> <em>response. The count, the line, and the subtotal all came from the server.</em></p>
<h2 id="heading-step-4-quantity-plus-and-minus-with-event-delegation">Step 4: Quantity Plus and Minus, with Event Delegation</h2>
<p>Every drawer lets the shopper nudge quantities. The obvious way to wire the plus and minus buttons is to loop over them and attach a click handler to each. Do that and the controls go dead after the first change.</p>
<p>Here's why: every time the cart changes you re-render the list. This replaces those <code>&lt;li&gt;</code> elements, and the handlers you attached went with the old elements. The new buttons have no listeners.</p>
<p>The fix is <strong>event delegation</strong>. Attach one listener to the parent that never gets replaced (the <code>&lt;ul&gt;</code>), and when a click bubbles up, check what was actually clicked. One handler, and it keeps working through every re-render because the element it's attached to never moves.</p>
<pre><code class="language-javascript">var itemsEl = document.querySelector("[data-drawer-items]");

itemsEl.addEventListener("click", function (e) {
  var inc = e.target.closest("[data-qty-inc]");
  var dec = e.target.closest("[data-qty-dec]");
  if (!inc &amp;&amp; !dec) return;

  var line = e.target.closest("[data-line]");
  var key = line.getAttribute("data-line-key");
  var qty = Number(line.getAttribute("data-quantity"));
  var nextQty = inc ? qty + 1 : qty - 1;

  fetch("/cart/change.js", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ id: key, quantity: nextQty }),
  })
    .then(function (r) { return r.json(); })
    .then(render);
});
</code></pre>
<p>For this version, <code>render()</code> emits the stepper controls inside each line:</p>
<pre><code class="language-javascript">li.innerHTML =
  '&lt;img class="drawer__item-img" src="' + (line.image || "") + '" alt=""&gt;' +
  '&lt;span class="drawer__item-title"&gt;' + line.title + "&lt;/span&gt;" +
  '&lt;span class="drawer__item-qty"&gt;' +
    '&lt;button class="qty-btn" data-qty-dec aria-label="Decrease"&gt;-&lt;/button&gt;' +
    '&lt;span class="qty-value"&gt;' + line.quantity + "&lt;/span&gt;" +
    '&lt;button class="qty-btn" data-qty-inc aria-label="Increase"&gt;+&lt;/button&gt;' +
  "&lt;/span&gt;" +
  '&lt;span class="drawer__item-price"&gt;' + money(line.final_line_price) + "&lt;/span&gt;";
</code></pre>
<p>One detail that's easy to get wrong: the change request sends <code>id: key</code>, the line <strong>key</strong>, not the variant id. A cart can hold the same variant on two separate lines when they carry different line-item properties (an engraving, a gift note). The key is what uniquely identifies a single line, so that's what <code>/cart/change.js</code> wants.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a424d9ed27378c517882227/f8d4e99b-a446-4a7c-b3de-70fbfdcbf184.gif" alt="The quantity stepper mid-interaction: a line item with minus and plus buttons and the quantity between them, and the subtotal updating" style="display:block;margin:0 auto" width="460" height="278" loading="lazy">

<p><em>One delegated listener drives every line's stepper, now and after every re-render.</em></p>
<h2 id="heading-step-5-remove-a-line-and-clear-the-cart">Step 5: Remove a Line, and Clear the Cart</h2>
<p>New developers might go looking for <code>/cart/remove.js</code>, but it doesn't exist. In Shopify's cart API, removing a line <strong>is</strong> changing its quantity to zero on the same <code>/cart/change.js</code> route you just used for the stepper. Clearing the whole cart has its own endpoint, <code>/cart/clear.js</code>, which takes no body.</p>
<pre><code class="language-javascript">// Remove: one delegated listener, quantity 0 deletes the line.
itemsEl.addEventListener("click", function (e) {
  var remove = e.target.closest("[data-remove]");
  if (!remove) return;
  var key = e.target.closest("[data-line]").getAttribute("data-line-key");
  fetch("/cart/change.js", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ id: key, quantity: 0 }),
  })
    .then(function (r) { return r.json(); })
    .then(render);
});

// Clear: empty the whole cart, no body.
document.querySelector("[data-cart-clear]").addEventListener("click", function () {
  fetch("/cart/clear.js", { method: "POST" })
    .then(function (r) { return r.json(); })
    .then(render);
});
</code></pre>
<p>Both re-render from the server response, same as everything else. That's the discipline that keeps a removed item from lingering in the count: you don't splice the <code>&lt;li&gt;</code> out of the DOM and hope. You tell the server, then draw what the server says.</p>
<h2 id="heading-step-6-re-rendering-with-the-section-rendering-api-the-version-youd-ship">Step 6: Re-Rendering with the Section Rendering API (the Version You'd Ship)</h2>
<p>Everything so far rebuilds the drawer's HTML in JavaScript. It works, but look at what it costs: your drawer markup now lives in two places, once in the Liquid from Step 1 and once in the template strings inside <code>render()</code>. Change the design and you have to change both, in sync, forever.</p>
<p>Shopify's answer is <strong>bundled section rendering</strong>. You ask the cart endpoint to return the re-rendered section HTML in the same request that changes the cart. The markup lives only in Liquid, and the server hands you the finished HTML to drop in.</p>
<p>You opt in by adding a <code>sections</code> parameter to the cart request:</p>
<pre><code class="language-javascript">fetch("/cart/add.js", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    id: variantId,
    quantity: 1,
    sections: "cart-drawer",              // section id(s), comma-separated
    sections_url: window.location.pathname // optional render context
  }),
})
  .then(function (r) { return r.json(); })
  .then(function (cart) {
    // Shopify returns rendered HTML under `sections`, keyed by id.
    var html = cart.sections["cart-drawer"];
    if (html) {
      document.querySelector("[data-drawer]").innerHTML =
        new DOMParser().parseFromString(html, "text/html")
          .querySelector("[data-drawer]").innerHTML;
    }
    openDrawer();
  });
</code></pre>
<p>What's worth knowing about it, all from the <a href="https://shopify.dev/docs/api/ajax/reference/cart">Ajax Cart API reference</a> and the <a href="https://shopify.dev/docs/api/ajax/section-rendering">Section Rendering API docs</a>:</p>
<ul>
<li><p>Bundled section rendering is available on <code>/cart/add</code>, <code>/cart/change</code>, <code>/cart/clear</code>, and <code>/cart/update</code>.</p>
</li>
<li><p><code>sections</code> is a comma-separated list (or array) of section IDs, up to five.</p>
</li>
<li><p><code>sections_url</code> must begin with <code>/</code>. If you omit it, sections render in the context of the current page (based on the <code>Referer</code> header).</p>
</li>
<li><p>The rendered HTML comes back under the <code>sections</code> key of the JSON response, keyed by section ID.</p>
</li>
<li><p>A section that fails to render, including one that doesn't exist, comes back as <code>null</code> with an HTTP 200. Always guard for null.</p>
</li>
<li><p>The section ID is <code>section.id</code> in Liquid, or the <code>id="shopify-section-[id]"</code> on the wrapper. For a section rendered by filename (for example <code>{% section 'cart-drawer' %}</code> in <code>theme.liquid</code>), the ID is just <code>cart-drawer</code>. Inside a JSON template it gets a dynamic ID like <code>template--123__cart-drawer</code>, so check the wrapper before you hardcode the key.</p>
</li>
</ul>
<p>The plain Section Rendering API (a <code>GET</code> with <code>?sections=</code> or <code>?section_id=</code> appended to any page URL) is the same idea for non-cart updates like paginated search or infinite scroll. Shopify's own guidance is: for anything driven by a cart change, prefer bundled section rendering over a separate call, because it saves a round trip.</p>
<p>This isn't a toy pattern. Shopify's own Horizon theme drives its cart exactly this way: its cart components send a <code>sections</code> list on their <code>/cart/change.js</code> calls and re-render from <code>response.sections</code>, reading each section's id from a data attribute rather than hardcoding it. That's the production version of the caveat above: never assume the id is a bare <code>cart-drawer</code>. Read it off the rendered wrapper instead.</p>
<h2 id="heading-the-2026-upgrade-standard-storefront-events-and-actions">The 2026 Upgrade: Standard Storefront Events and Actions</h2>
<p>Everything above is timeless Shopify. Now the new part.</p>
<p>On June 17, 2026, as part of the Spring '26 Edition, Shopify shipped <a href="https://shopify.dev/changelog/standard-storefront-events-and-actions">standard storefront events and actions</a>, and they're generally available.</p>
<p>The idea is that themes now <strong>emit</strong> a standardized set of DOM events (all namespaced <code>shopify:</code>) and <strong>expose</strong> a standardized set of actions on <code>Shopify.actions</code>. An app or an AI shopping agent can now interact with any storefront through one contract, instead of reverse-engineering each theme's private JavaScript.</p>
<p>To feel why that matters, think about how an upsell app used to detect an add-to-cart on a theme it had never seen.</p>
<p>It had four bad options: monkey-patch <code>fetch</code> to sniff calls to <code>/cart/add</code>, listen for a theme-specific custom event whose name changed from theme to theme, poll <code>/cart.js</code> on a timer and diff it, or scrape the DOM for a cart-count node by selector. Every one of them breaks when the merchant reskins or switches themes.</p>
<p>This is also the exact code an AI assistant tends to generate when you ask it to "run something after add to cart," because that brittle pattern dominates its training data.</p>
<p>The problem was never that the developer was careless. There was simply no stable interface to target.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a424d9ed27378c517882227/59634003-a020-4b57-8d12-7ddcfde97f7f.png" alt="On the left, four brittle ways an app used to detect a cart change: patching fetch, listening for a theme-specific event, polling cart.js, and scraping the DOM. On the right, one standard contract: subscribe to shopify:cart:lines-update and call Shopify.actions" style="display:block;margin:0 auto" width="3666" height="2062" loading="lazy">

<p><em>Four fragile per-theme tactics collapse into one integration against a contract that survives a reskin.</em></p>
<p>Crucially, this layer sits <strong>on top of</strong> the drawer you just built. It doesn't replace the Ajax API. The docs even show the theme-side event wrapping the same <code>/cart/add.js</code> and <code>/cart.js</code> calls, and Shopify shipped a helper whose only job is to convert a <code>/cart.js</code> response into the event's payload shape. So none of your work is wasted. You're about to give it a public door.</p>
<h3 id="heading-the-events-the-theme-tells-the-world-what-happened">The Events: the Theme Tells the World What Happened</h3>
<p>Each event uses the <code>shopify:</code> namespace, follows a <code>category:action</code> naming pattern, dispatches from the most specific element (a product card, the cart, a collection container), and bubbles to <code>document</code>. The payloads follow the Storefront GraphQL API shape, with camelCase fields.</p>
<table>
<thead>
<tr>
<th>Event</th>
<th>Fires when</th>
</tr>
</thead>
<tbody><tr>
<td><code>shopify:page:view</code></td>
<td>Every page load</td>
</tr>
<tr>
<td><code>shopify:product:view</code></td>
<td>A product becomes visible</td>
</tr>
<tr>
<td><code>shopify:product:select</code></td>
<td>Buyer changes variant selection</td>
</tr>
<tr>
<td><code>shopify:cart:view</code></td>
<td>Cart becomes visible</td>
</tr>
<tr>
<td><code>shopify:cart:lines-update</code></td>
<td>Cart lines added, updated, or removed</td>
</tr>
<tr>
<td><code>shopify:cart:note-update</code></td>
<td>Cart note changes</td>
</tr>
<tr>
<td><code>shopify:cart:discount-update</code></td>
<td>Discount codes applied or removed</td>
</tr>
<tr>
<td><code>shopify:cart:error</code></td>
<td>A cart mutation fails</td>
</tr>
<tr>
<td><code>shopify:collection:view</code></td>
<td>Collection page loads</td>
</tr>
<tr>
<td><code>shopify:collection:update</code></td>
<td>Collection filters or sort change</td>
</tr>
<tr>
<td><code>shopify:search:update</code></td>
<td>Search filters or sort change</td>
</tr>
</tbody></table>
<p>An app subscribes with the ordinary DOM API. No SDK:</p>
<pre><code class="language-javascript">document.addEventListener('shopify:cart:lines-update', (event) =&gt; {
  console.log(event.action, event.lines);
  event.promise?.then(({ cart }) =&gt; {
    console.log(cart.cost.totalAmount.amount);
  });
});
</code></pre>
<p>The cart, note, discount, product-select, collection-update, and search-update events carry a <code>promise</code> field for their async result. That lets a listener show a loading or optimistic state immediately, then read the settled <code>{ cart }</code> when the operation resolves.</p>
<h3 id="heading-emitting-an-event-from-your-theme">Emitting an Event From Your Theme</h3>
<p>The events library is hosted on the Shopify CDN. You load it through an import map (for module themes like Horizon) or assign it to a global (for Dawn-style themes).</p>
<p>A theme fires an event by constructing the event class and calling <code>dispatchEvent()</code>. Read this carefully and you'll see that it wraps the exact same <code>/cart/add.js</code> and <code>/cart.js</code> calls from Step 2:</p>
<pre><code class="language-javascript">import { CartLinesUpdateEvent, CartErrorEvent } from '@theme/standard-events';

const deferred = CartLinesUpdateEvent.createPromise();
element.dispatchEvent(new CartLinesUpdateEvent({
  action: 'add',
  context: 'product',
  lines: [{ merchandiseId: variantId, quantity: 1 }],
  promise: deferred.promise,
}));

try {
  const response = await fetch(window.Shopify.routes.root + 'cart/add.js', { method: 'POST', body, headers });
  if (!response.ok) throw new Error('Add to cart failed');
  const ajaxCart = await fetch(window.Shopify.routes.root + 'cart.js').then(r =&gt; r.json());
  deferred.resolve({
    cart: CartLinesUpdateEvent.createCartFromAjaxResponse(ajaxCart),
  });
} catch (e) {
  element.dispatchEvent(new CartErrorEvent({ error: e.message, code: 'SERVICE_UNAVAILABLE' }));
  deferred.reject(e);
}
</code></pre>
<p>That static <code>createCartFromAjaxResponse(ajaxCart)</code> is the bridge between the classic Ajax drawer and the new event contract. It converts your <code>/cart.js</code> response into the Storefront-API-shaped payload the events expect, so the drawer you already have plugs straight in.</p>
<h3 id="heading-the-actions-the-world-asks-the-theme-to-do-something">The Actions: the World Asks the Theme to Do Something</h3>
<p>Actions are async functions on <code>Shopify.actions</code>, injected on every Liquid storefront with no script tag of your own:</p>
<pre><code class="language-javascript">// Add, update, or remove lines. Also handles note + discountCodes.
const { cart, userErrors, warnings } = await Shopify.actions.updateCart({
  lines: [
    { merchandiseId: "gid://shopify/ProductVariant/123", quantity: 1 }, // add
    { id: "gid://shopify/CartLine/456", quantity: 5 },                  // update
    { id: "gid://shopify/CartLine/789", quantity: 0 },                  // remove
  ],
});
// Returns: Promise&lt;{ cart, userErrors?, warnings? }&gt;

await Shopify.actions.openCart();                 // Promise&lt;void&gt;
const { cart } = await Shopify.actions.getCart(); // reads current cart
</code></pre>
<p>The defaults work on a stock theme with no changes: <code>updateCart</code> writes to the Storefront API and refreshes in place, falling back to a full page reload. <code>openCart</code> opens a <code>&lt;cart-drawer-component&gt;</code> or <code>&lt;cart-drawer&gt;</code> element if one exists, otherwise it redirects to <code>/cart</code>. <code>getCart</code> reads the current cart. And when a configured action succeeds, the runtime <strong>auto-emits</strong> the matching event, so an app that calls <code>updateCart</code> never has to also dispatch <code>shopify:cart:lines-update</code>.</p>
<h3 id="heading-overriding-an-action-to-drive-your-drawer">Overriding an Action to Drive Your Drawer</h3>
<p>This is the payoff. Because a theme can override an action's default, you can intercept <code>openCart</code> and <code>updateCart</code> so that any app's call routes through the drawer you already built, instead of triggering a page reload. The app doesn't need to know your markup. It calls the standard action, and your override decides what the UI does.</p>
<p>Register the override inside a <code>DOMContentLoaded</code> listener placed <strong>above</strong> <code>{{ content_for_header }}</code> in your layout, so it runs before any app code:</p>
<pre><code class="language-javascript">document.addEventListener('DOMContentLoaded', () =&gt; {
  Shopify.actions.updateCart.configure({
    eventTarget: (meta) =&gt; {
      if (meta.type === 'shopify:cart:note-update') return document.querySelector('cart-note');
      if (meta.type === 'shopify:cart:discount-update') return document.querySelector('cart-discount');
      if (meta.type === 'shopify:cart:lines-update' &amp;&amp; meta.action === 'add') {
        return document.querySelector('product-form');
      }
      return document.querySelector('cart-items');
    },
    async handler(defaultHandler, payload, options) {
      const result = await defaultHandler();
      customUpdateUI(result); // your render() + openDrawer() from earlier
      return result;
    },
  });
});
</code></pre>
<p><code>openCart</code> has a simpler override:</p>
<pre><code class="language-javascript">Shopify.actions.openCart.configure({
  handler() { document.querySelector('cart-drawer')?.open(); },
});
</code></pre>
<p>A few rules that will save you time:</p>
<ul>
<li><p><code>eventTarget</code> is required for <code>updateCart</code>. It decides which element the auto-emitted events dispatch from.</p>
</li>
<li><p><code>getCart</code> is intentionally <strong>not</strong> configurable. Calling <code>configure()</code> on it is a TypeScript error and a runtime <code>TypeError</code>.</p>
</li>
<li><p><code>isDefault()</code> tells you whether the theme has overridden an action yet.</p>
</li>
<li><p><code>updateCart</code> resolves with <code>{ cart, userErrors?, warnings?, detail? }</code> and rejects only when it couldn't run at all (a network failure or a malformed payload). A <code>userErrors</code> array means the mutation was rejected (codes like <code>INVALID</code>, <code>MAXIMUM_EXCEEDED</code>). A <code>warnings</code> array means it succeeded with caveats (<code>MERCHANDISE_OUT_OF_STOCK</code>, <code>DISCOUNT_NOT_FOUND</code>). Check both before you trust <code>cart</code>.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6a424d9ed27378c517882227/1401861d-5800-40b7-ad04-7887625986ec.png" alt="An app or AI agent calls a standard action, a theme override routes it through the existing cart-drawer logic, and the drawer opens in place with no reload" style="display:block;margin:0 auto" width="3666" height="2062" loading="lazy">

<p><em>The app calls the standard action and knows nothing about your markup. Your override decides the UI.</em></p>
<h3 id="heading-verifying-it">Verifying it</h3>
<p>Run <code>shopify theme dev</code> and the CLI loads a development build of the events runtime that validates payloads and logs a warning when a field is malformed or missing.</p>
<p>Those checks are stripped in production. Add the <code>--standard-events-inspector</code> flag and it injects a floating debug panel into your local pages with two tabs: <strong>Events</strong>, which shows every emitted standard event live with its full payload, and <strong>Actions</strong>, which lets you dispatch actions by hand and inspect the result. When you're wiring up payloads, trust the inspector over any tutorial, including this one.</p>
<h2 id="heading-why-this-matters">Why This Matters</h2>
<p>Two ideas in this build outlast the specific code, and they're the difference between a drawer that works and one you can maintain.</p>
<h3 id="heading-event-delegation">Event Delegation</h3>
<p>One listener on a parent that never gets replaced, reading <code>e.target.closest(...)</code>, handles every child element: the ones on the page now and the ones you have not rendered yet. Bind a handler per button instead and it dies the instant the list re-renders, which in a cart drawer is constantly.</p>
<p>Delegation is also, not by coincidence, the pattern AI tools most reliably get wrong, because per-element binding is what shows up most in their training data. Knowing to reach for delegation is exactly the kind of judgment that doesn't come from the syntax.</p>
<h3 id="heading-the-section-rendering-api">The Section Rendering API</h3>
<p>Instead of keeping your drawer's markup in two places and praying they stay in sync, you let the server render the section and hand you the HTML. Your markup lives in one file, and it stays correct when a merchant edits the section in the theme editor.</p>
<p>The trade is a slightly larger response and a parse step, in exchange for never maintaining the same markup twice.</p>
<p>And under all of it, one rule: after every mutation, re-read the cart (or the rendered section) and paint from the server's response. The local counter is the bug. Everything else in this article is a variation on trusting the server.</p>
<h2 id="heading-the-complete-files">The Complete Files</h2>
<p>For anyone who scrolled straight here, this is the whole thing: the section markup, the JavaScript, and the CSS. The JavaScript combines add, quantity, remove, and clear, all re-rendering from the server response.</p>
<p><code>sections/cart-drawer.liquid</code>:</p>
<pre><code class="language-liquid">&lt;button type="button" class="cart-toggle" data-cart-toggle&gt;
  Cart &lt;span class="cart-count" data-cart-count&gt;{{ cart.item_count }}&lt;/span&gt;
&lt;/button&gt;

&lt;aside class="drawer" data-drawer aria-label="Cart" aria-hidden="true"&gt;
  &lt;div class="drawer__head"&gt;
    &lt;h2&gt;Your cart&lt;/h2&gt;
    &lt;button type="button" class="drawer__close" data-drawer-close aria-label="Close cart"&gt;&amp;times;&lt;/button&gt;
  &lt;/div&gt;

  &lt;div class="drawer__body"&gt;
    &lt;p class="drawer__empty" data-drawer-empty {% if cart.item_count &gt; 0 %}style="display:none"{% endif %}&gt;
      Your cart is empty.
    &lt;/p&gt;
    &lt;ul class="drawer__items" data-drawer-items&gt;
      {%- for item in cart.items -%}
        &lt;li class="drawer__item" data-line data-line-key="{{ item.key }}" data-quantity="{{ item.quantity }}"&gt;
          {{ item.image | image_url: width: 96 | image_tag: class: 'drawer__item-img', loading: 'lazy', alt: item.product.title }}
          &lt;span class="drawer__item-title"&gt;{{ item.product.title }}&lt;/span&gt;
          &lt;span class="drawer__item-qty"&gt;
            &lt;button type="button" class="qty-btn" data-qty-dec aria-label="Decrease"&gt;-&lt;/button&gt;
            &lt;span class="qty-value"&gt;{{ item.quantity }}&lt;/span&gt;
            &lt;button type="button" class="qty-btn" data-qty-inc aria-label="Increase"&gt;+&lt;/button&gt;
          &lt;/span&gt;
          &lt;span class="drawer__item-price"&gt;{{ item.final_line_price | money }}&lt;/span&gt;
          &lt;button type="button" class="drawer__remove" data-remove aria-label="Remove"&gt;Remove&lt;/button&gt;
        &lt;/li&gt;
      {%- endfor -%}
    &lt;/ul&gt;
  &lt;/div&gt;

  &lt;div class="drawer__foot"&gt;
    &lt;div class="drawer__subtotal"&gt;
      &lt;span&gt;Subtotal&lt;/span&gt;
      &lt;span data-cart-subtotal&gt;{{ cart.total_price | money }}&lt;/span&gt;
    &lt;/div&gt;
    &lt;button type="button" class="drawer__clear" data-cart-clear&gt;Clear cart&lt;/button&gt;
    &lt;a href="{{ routes.cart_url }}" class="drawer__checkout"&gt;Checkout&lt;/a&gt;
  &lt;/div&gt;
&lt;/aside&gt;
&lt;div class="drawer__scrim" data-drawer-scrim&gt;&lt;/div&gt;

{% schema %}
{ "name": "Cart drawer" }
{% endschema %}
</code></pre>
<p><code>assets/cart-drawer.js</code>:</p>
<pre><code class="language-javascript">(function () {
  var countEl = document.querySelector("[data-cart-count]");
  var itemsEl = document.querySelector("[data-drawer-items]");
  var emptyEl = document.querySelector("[data-drawer-empty]");
  var subtotalEl = document.querySelector("[data-cart-subtotal]");
  var drawer = document.querySelector("[data-drawer]");
  var scrim = document.querySelector("[data-drawer-scrim]");
  var clearBtn = document.querySelector("[data-cart-clear]");

  // --- Add to cart: mutate, re-read, render, open ---
  document.querySelectorAll("[data-add]").forEach(function (btn) {
    btn.addEventListener("click", function () {
      var id = Number(btn.getAttribute("data-variant-id"));
      fetch("/cart/add.js", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ id: id, quantity: 1 }),
      })
        .then(function (r) { return r.json(); })
        .then(function () { return refresh(); })
        .then(openDrawer);
    });
  });

  // --- Quantity +/- : one delegated listener on the stable list ---
  itemsEl.addEventListener("click", function (e) {
    var inc = e.target.closest("[data-qty-inc]");
    var dec = e.target.closest("[data-qty-dec]");
    if (!inc &amp;&amp; !dec) return;
    var line = e.target.closest("[data-line]");
    var key = line.getAttribute("data-line-key");
    var qty = Number(line.getAttribute("data-quantity"));
    var nextQty = inc ? qty + 1 : qty - 1;
    fetch("/cart/change.js", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ id: key, quantity: nextQty }),
    }).then(function (r) { return r.json(); }).then(render);
  });

  // --- Remove a line: quantity 0 (there is no /cart/remove.js) ---
  itemsEl.addEventListener("click", function (e) {
    var remove = e.target.closest("[data-remove]");
    if (!remove) return;
    var key = e.target.closest("[data-line]").getAttribute("data-line-key");
    fetch("/cart/change.js", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ id: key, quantity: 0 }),
    }).then(function (r) { return r.json(); }).then(render);
  });

  // --- Clear the whole cart ---
  clearBtn.addEventListener("click", function () {
    fetch("/cart/clear.js", { method: "POST" })
      .then(function (r) { return r.json(); })
      .then(render);
  });

  // --- The one function that paints the drawer from the cart ---
  function money(cents) { return "$" + (cents / 100).toFixed(2); }

  function render(cart) {
    countEl.textContent = cart.item_count;
    subtotalEl.textContent = money(cart.total_price);
    itemsEl.innerHTML = "";
    if (!cart.items.length) { emptyEl.style.display = "block"; return; }
    emptyEl.style.display = "none";
    cart.items.forEach(function (line) {
      var li = document.createElement("li");
      li.className = "drawer__item";
      li.setAttribute("data-line", "");
      li.setAttribute("data-line-key", line.key);
      li.setAttribute("data-quantity", line.quantity);
      li.innerHTML =
        '&lt;img class="drawer__item-img" src="' + (line.image || "") + '" alt=""&gt;' +
        '&lt;span class="drawer__item-title"&gt;' + line.title + "&lt;/span&gt;" +
        '&lt;span class="drawer__item-qty"&gt;' +
          '&lt;button type="button" class="qty-btn" data-qty-dec aria-label="Decrease"&gt;-&lt;/button&gt;' +
          '&lt;span class="qty-value"&gt;' + line.quantity + "&lt;/span&gt;" +
          '&lt;button type="button" class="qty-btn" data-qty-inc aria-label="Increase"&gt;+&lt;/button&gt;' +
        "&lt;/span&gt;" +
        '&lt;span class="drawer__item-price"&gt;' + money(line.final_line_price) + "&lt;/span&gt;" +
        '&lt;button type="button" class="drawer__remove" data-remove aria-label="Remove"&gt;Remove&lt;/button&gt;';
      itemsEl.appendChild(li);
    });
  }

  function refresh() {
    return fetch("/cart.js").then(function (r) { return r.json(); }).then(render);
  }
  function openDrawer() { drawer.classList.add("is-open"); scrim.classList.add("is-open"); }
  function closeDrawer() { drawer.classList.remove("is-open"); scrim.classList.remove("is-open"); }

  document.querySelector("[data-cart-toggle]").addEventListener("click", function () { refresh().then(openDrawer); });
  document.querySelector("[data-drawer-close]").addEventListener("click", closeDrawer);
  scrim.addEventListener("click", closeDrawer);

  refresh(); // paint on load
})();
</code></pre>
<p><code>assets/cart-drawer.css</code>:</p>
<pre><code class="language-css">.drawer {
  position: fixed;
  inset: 0 0 0 auto;
  width: min(420px, 100%);
  background: #fff;
  transform: translateX(100%);
  transition: transform 0.3s ease;
  display: flex;
  flex-direction: column;
}
.drawer.is-open { transform: translateX(0); }
.drawer__scrim {
  position: fixed; inset: 0;
  background: rgba(30, 18, 6, 0.45);
  opacity: 0; pointer-events: none;
  transition: opacity 0.3s ease;
}
.drawer__scrim.is-open { opacity: 1; pointer-events: auto; }
</code></pre>
<h2 id="heading-wrap-up">Wrap Up</h2>
<p>You now have a cart drawer that adds, updates, removes, and clears without a reload, that never lets its UI drift from the real cart, and that, with an action override, presents a clean public interface to the entire app ecosystem, humans and AI agents alike.</p>
<p>The Ajax foundation is the same it has been for years. The 2026 layer sits on top of it, so the drawer you built today is ready for whatever calls it tomorrow.</p>
<p><em>If you want to build this exact drawer interactively, writing the JavaScript and watching the real storefront react, you can do it for free at</em> <a href="https://learnshopify.dev/learn/cart-drawer-component?utm_source=freecodecamp&amp;utm_medium=referral&amp;utm_campaign=ajax-cart-drawer-2026"><em>learnshopify.dev</em></a><em>.</em></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a424d9ed27378c517882227/7bd7cf3b-389d-413c-b2e6-c287ffcafbc3.png" alt="learnshopify.dev landings page, interactive platform to learn shopify development" style="display:block;margin:0 auto" width="2041" height="1269" loading="lazy"> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Browser-Based PDF Margin Tool Using JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ Adding margins to a PDF is a common task when preparing documents for printing, binding, archiving, or sharing professionally. While many PDF editors include this feature, they often require installin ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-pdf-margin-tool-javascript/</link>
                <guid isPermaLink="false">6a4540361d2a0d9b0b18d5ff</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Online PDF Tools ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Bhavin Sheth ]]>
                </dc:creator>
                <pubDate>Wed, 01 Jul 2026 16:28:38 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/617f84e9-a33b-494f-b036-7583a3c22585.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Adding margins to a PDF is a common task when preparing documents for printing, binding, archiving, or sharing professionally. While many PDF editors include this feature, they often require installing desktop software or uploading files to an online service.</p>
<p>In this tutorial, you'll learn how to build a browser-based PDF Add Margins Tool using JavaScript. The application allows users to upload a PDF, preview its pages, configure custom margin values, choose measurement units, apply preset margin sizes, select specific pages, and generate an updated PDF directly inside the browser.</p>
<p>Everything runs locally on the user's device using JavaScript, which means documents remain private and no backend server is required. This approach provides fast processing while giving users complete control over how margins are applied.</p>
<p>By the end of this guide, you'll understand how to work with PDF pages, create new page dimensions, reposition existing content, and export a new PDF with the desired margins.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/35dfed35-7a7a-4f65-8e1b-837b9dd842f4.png" alt="allinonetools ppdf toolkit add margin to pdf file or any pages" style="display:block;margin:0 auto" width="571" height="219" loading="lazy">

<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-why-pdf-margins-are-useful">Why PDF Margins Are Useful</a></p>
</li>
<li><p><a href="#heading-how-pdf-margin-editing-works">How PDF Margin Editing Works</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-what-library-are-we-using">What Library Are We Using?</a></p>
</li>
<li><p><a href="#heading-creating-the-upload-interface">Creating the Upload Interface</a></p>
</li>
<li><p><a href="#heading-previewing-uploaded-pdf-pages">Previewing Uploaded PDF Pages</a></p>
</li>
<li><p><a href="#heading-configuring-margin-settings">Configuring Margin Settings</a></p>
</li>
<li><p><a href="#heading-applying-the-margins">Applying the Margins</a></p>
</li>
<li><p><a href="#heading-generating-the-updated-pdf">Generating the Updated PDF</a></p>
</li>
<li><p><a href="#heading-demo-how-the-add-margins-tool-works">Demo: How the Add Margins Tool Works</a></p>
</li>
<li><p><a href="#heading-important-notes-from-real-world-use">Important Notes from Real-World Use</a></p>
</li>
<li><p><a href="#heading-common-mistakes-to-avoid">Common Mistakes to Avoid</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-why-pdf-margins-are-useful">Why PDF Margins Are Useful</h2>
<p>PDF documents are designed to preserve their appearance across different devices and printers, but that doesn't always mean they're ready for every use case. Many PDFs are created with very little white space around the content, making them difficult to print, bind, annotate, or archive.</p>
<p>Adding margins creates extra space around the page without changing the document's content. This additional white space improves readability, prevents content from being clipped during printing, and provides room for notes, signatures, stamps, or hole punching.</p>
<p>One of the most common uses for PDF margins is printing. Most home and office printers can't print all the way to the edge of the paper, so documents with little or no margin may lose important text or images. Adding margins ensures the entire page fits safely within the printer's printable area.</p>
<p>Margins are also essential when preparing books, manuals, reports, and training materials for binding. Without enough inner spacing, text can disappear into the binding, making the document difficult to read. Publishers often use larger inner margins or mirror margins to create professional-looking printed books.</p>
<p>Businesses regularly add margins before printing invoices, quotations, purchase orders, financial reports, contracts, and presentations. The extra space makes documents easier to file and leaves room for handwritten notes, approval stamps, signatures, or comments.</p>
<p>Students, teachers, and researchers also benefit from margin editing. Universities and educational institutions often require assignments, dissertations, and research papers to follow specific formatting guidelines, including minimum page margins. Instead of recreating the document, users can simply add the required spacing before submission.</p>
<p>Government offices, legal firms, and healthcare organizations frequently work with PDFs that must meet strict printing or filing standards. Adding consistent margins helps ensure forms, applications, agreements, medical records, and official documents are easier to print, review, and archive.</p>
<p>Another practical example comes from e-commerce businesses. Sellers who process hundreds of orders from platforms such as Amazon, Flipkart, or Meesho often print invoices, packing slips, shipping labels, and courier documents in bulk. If the content is positioned too close to the paper edges, some printers may crop important information. Adding consistent margins before printing helps prevent this issue and ensures every document prints correctly.</p>
<p>Because this tool works entirely inside the browser, users can add margins to sensitive PDF documents without uploading them to external servers. This keeps document processing fast, private, and secure while producing professional-looking PDFs that are ready for printing, sharing, binding, or long-term storage.</p>
<h2 id="heading-how-pdf-margin-editing-works">How PDF Margin Editing Works</h2>
<p>Unlike editing text inside a PDF, adding margins doesn't modify the original content. Instead, the application creates a larger page and places the existing page content inside it with the specified spacing around each edge.</p>
<p>When a user uploads a document, the browser first reads every page in the PDF. The application calculates the current page dimensions and determines how much additional space should be added to the top, bottom, left, and right sides.</p>
<p>Depending on the selected settings, the tool can either expand the overall page size while preserving the original content dimensions or keep the existing page size and reposition the content within the available area.</p>
<p>Users can also choose whether the margin changes should be applied to every page or only selected page ranges. Mirror margins are available for printed books where the inner margin alternates between left and right pages to leave room for binding.</p>
<p>After processing every selected page, the browser generates a brand-new PDF containing the updated page dimensions and revised content positioning. Because everything happens locally, no files leave the user's computer during the process.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Before writing any JavaScript, create a simple project structure for the application.</p>
<p>Create a new project folder and add the following files:</p>
<pre><code class="language-text">pdf-add-margins/
│
├── index.html
├── style.css
├── script.js
└── assets/
</code></pre>
<p>The HTML file contains the upload area, preview section, margin settings, and download interface.</p>
<p>The CSS file styles the application and creates the responsive layout used throughout the project.</p>
<p>The JavaScript file handles file uploads, PDF processing, page rendering, margin calculations, and generation of the updated document.</p>
<p>Because everything runs inside the browser, there's no need to configure a backend server or install server-side frameworks.</p>
<h2 id="heading-what-library-are-we-using">What Library Are We Using?</h2>
<p>This project uses <strong>PDF-lib</strong>, one of the most popular JavaScript libraries for creating and editing PDF files directly in the browser.</p>
<p>PDF-lib allows developers to load existing PDF documents, create new pages, copy pages between documents, edit document metadata, rotate pages, resize pages, crop pages, add page numbers, insert images, draw text, and export completely new PDF files without relying on external software.</p>
<p>Install PDF-lib using npm:</p>
<pre><code class="language-bash">npm install pdf-lib
</code></pre>
<p>Or include it directly from a CDN inside your HTML page:</p>
<pre><code class="language-html">&lt;script src="https://unpkg.com/pdf-lib/dist/pdf-lib.min.js"&gt;&lt;/script&gt;
</code></pre>
<p>Once the library is loaded, you can import the required objects:</p>
<pre><code class="language-javascript">const {
  PDFDocument
} = PDFLib;
</code></pre>
<p>Throughout this tutorial, PDF-lib will be responsible for loading uploaded documents, creating new page dimensions, repositioning page content according to the selected margins, and exporting the finished PDF.</p>
<h2 id="heading-creating-the-upload-interface">Creating the Upload Interface</h2>
<p>The first feature users interact with is the upload interface. A simple and intuitive upload area makes it easy to select a PDF file using either drag-and-drop or the traditional file picker.</p>
<p>In this project, the upload section accepts only PDF documents. Once a valid file is selected, the browser immediately begins loading the document and prepares it for previewing and margin editing.</p>
<p>The upload component also acts as the starting point for the entire workflow. Every action that follows (page preview, margin configuration, page selection, and PDF generation) depends on the uploaded file.</p>
<p>Because the application runs entirely inside the browser, the uploaded PDF never leaves the user's computer. This improves privacy while reducing processing time.</p>
<p>Here's a simple upload field:</p>
<pre><code class="language-html">&lt;div class="upload-box"&gt;
    &lt;input
        type="file"
        id="pdfFile"
        accept=".pdf,application/pdf"
        hidden
    &gt;

    &lt;button id="selectPDF"&gt;
        Select PDF
    &lt;/button&gt;
&lt;/div&gt;
</code></pre>
<p>Connect the upload button with JavaScript:</p>
<pre><code class="language-javascript">const input = document.getElementById("pdfFile");
const button = document.getElementById("selectPDF");

button.addEventListener("click", () =&gt; {
    input.click();
});

input.addEventListener("change", async (event) =&gt; {

    const file = event.target.files[0];

    if (!file) return;

    const bytes = await file.arrayBuffer();

    console.log("PDF Loaded", bytes);

});
</code></pre>
<p>The uploaded PDF is now available for rendering page previews and applying margin settings.</p>
<h3 id="heading-upload-interface-demo">Upload Interface Demo</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/1f11396b-ba30-48c5-a0e9-b2109af7f81a.png" alt="PDF upload interface allowing users to drag and drop or select a PDF file for adding margins." style="display:block;margin:0 auto" width="628" height="665" loading="lazy">

<h2 id="heading-previewing-uploaded-pdf-pages">Previewing Uploaded PDF Pages</h2>
<p>Once the PDF has been uploaded successfully, the next step is displaying its pages.</p>
<p>Showing page previews gives users confidence that the correct document has been selected before any changes are made. It also allows them to inspect each page and decide whether margins should be applied to the entire document or only to specific pages.</p>
<p>In this project, every page is rendered as a thumbnail. Users can quickly scroll through the document and verify page order before adjusting any settings.</p>
<p>For large documents, thumbnail previews make navigation much easier than displaying one full-size page at a time.</p>
<p>The browser renders each page directly from the uploaded PDF without sending the document to a server.</p>
<p>After loading the document, each page can be rendered individually.</p>
<pre><code class="language-javascript">const pdfDoc = await PDFDocument.load(pdfBytes);

const pages = pdfDoc.getPages();

console.log("Total Pages:", pages.length);
</code></pre>
<p>Each page is then displayed inside the preview gallery.</p>
<pre><code class="language-javascript">pages.forEach((page, index) =&gt; {

    console.log(`Rendering page ${index + 1}`);

});
</code></pre>
<p>After the previews are generated, users can move on to configuring the margin settings.</p>
<h3 id="heading-preview-demo">Preview Demo</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/609ff23a-a621-4c03-babd-cdea1d330bb2.png" alt="Uploaded PDF showing page thumbnail previews before margin settings are applied." style="display:block;margin:0 auto" width="750" height="567" loading="lazy">

<h2 id="heading-configuring-margin-settings">Configuring Margin Settings</h2>
<p>After verifying the uploaded document, users can configure exactly how the margins should be added.</p>
<p>Rather than applying one fixed margin to every document, the tool provides several options that make it suitable for different printing, publishing, and business workflows.</p>
<p>Users can enter custom values for the top, bottom, left, and right margins. These values can be measured in millimeters, pixels, or inches depending on the intended use.</p>
<p>For users who don't want to calculate measurements manually, the application also includes preset margin sizes such as None, Narrow, Normal, and Wide.</p>
<p>The tool supports applying margins to every page or only to a specific page range. This is especially useful when only certain pages require additional spacing.</p>
<p>For printed books and manuals, mirror margins can be enabled so that left and right pages automatically receive opposite inner margins for binding.</p>
<p>Users can also decide how the margin should be applied. Expanding the page size preserves the original content dimensions while increasing the overall page size. Alternatively, the existing page size can be maintained and the content repositioned within the available space.</p>
<p>All of these settings are configured before any processing begins, allowing users to preview and adjust everything in advance.</p>
<p>Example margin configuration:</p>
<pre><code class="language-javascript">const marginSettings = {

    top: 25.4,

    bottom: 25.4,

    left: 25.4,

    right: 25.4,

    unit: "mm",

    applyTo: "all",

    mirrorMargins: false,

    preset: "Normal",

    resizeMode: "Expand Page Size"

};
</code></pre>
<p>The selected values are then used while generating the updated PDF pages.</p>
<h3 id="heading-margin-settings-demo">Margin Settings Demo</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/2435c48a-d66f-4976-a368-44f1ee3a85f6.png" alt="Margin settings panel showing custom margin values, units, presets, mirror margins, page selection mode, and page resize options." style="display:block;margin:0 auto" width="690" height="771" loading="lazy">

<h2 id="heading-applying-the-margins">Applying the Margins</h2>
<p>Once the margin settings have been configured, the application can begin processing the PDF.</p>
<p>Instead of modifying the original document directly, the tool creates a new page layout based on the selected margin values. The existing page content is then repositioned inside the newly calculated page dimensions.</p>
<p>This approach preserves the original document while generating a new PDF with additional white space around the content.</p>
<p>Depending on the selected resize mode, the application can either expand the page size to accommodate the new margins or keep the existing page size and reposition the content within the available printable area.</p>
<p>For documents containing multiple pages, the same settings can be applied to every page or only to a selected page range.</p>
<p>A simplified example looks like this:</p>
<pre><code class="language-javascript">const pages = pdfDoc.getPages();

pages.forEach((page) =&gt; {

    const { width, height } = page.getSize();

    const newWidth = width + leftMargin + rightMargin;

    const newHeight = height + topMargin + bottomMargin;

    page.setSize(newWidth, newHeight);

});
</code></pre>
<p>The application then adjusts the page content so it remains correctly positioned inside the new page dimensions.</p>
<pre><code class="language-javascript">page.translateContent(
    leftMargin,
    bottomMargin
);
</code></pre>
<p>This ensures the document content shifts into the correct position while leaving the requested space around the page edges.</p>
<h3 id="heading-applying-margins-demo">Applying Margins Demo</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/a22dac1d-55e3-4cc5-aa00-1ae844b6a4de.png" alt="Add Margins button used to process the PDF and generate the updated document." style="display:block;margin:0 auto" width="694" height="144" loading="lazy">

<h2 id="heading-generating-the-updated-pdf">Generating the Updated PDF</h2>
<p>After every selected page has been processed, the browser creates a brand-new PDF containing the updated page sizes and margin layout.</p>
<p>The original PDF remains unchanged while the modified document is prepared for download.</p>
<p>Because everything happens locally, the generation process is usually very fast, even for multi-page documents.</p>
<p>Once processing is complete, the updated PDF is converted into downloadable bytes.</p>
<pre><code class="language-javascript">const pdfBytes = await pdfDoc.save();
</code></pre>
<p>A Blob object can then be created for downloading.</p>
<pre><code class="language-javascript">const blob = new Blob(
    [pdfBytes],
    {
        type: "application/pdf"
    }
);

const url = URL.createObjectURL(blob);
</code></pre>
<p>Finally, the browser starts the download.</p>
<pre><code class="language-javascript">const link = document.createElement("a");

link.href = url;

link.download = "updated-document.pdf";

link.click();
</code></pre>
<p>The generated PDF can also be previewed before downloading.</p>
<p>Users are able to review the updated document, rename the output file, view the total number of pages, check the final file size, and download the completed PDF when they are satisfied with the results.</p>
<h3 id="heading-updated-pdf-preview">Updated PDF Preview</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/91c81790-f17b-41e9-b7df-bb52677daf46.png" alt="Updated PDF preview showing added margins, filename, page count, file size, page navigation controls, and download button." style="display:block;margin:0 auto" width="681" height="781" loading="lazy">

<h2 id="heading-demo-how-the-add-margins-tool-works">Demo: How the Add Margins Tool Works</h2>
<h3 id="heading-step-1-upload-your-pdf-file">Step 1: Upload Your PDF File</h3>
<p>The process begins by uploading a PDF document using either the drag-and-drop area or the file picker.</p>
<p>Once a file is selected, the browser validates that it's a PDF and loads it locally. Since all processing happens inside the browser, the document never leaves the user's device, making the tool suitable for confidential reports, contracts, invoices, and other sensitive documents.</p>
<p>After the file is loaded successfully, the application prepares the document for preview generation and margin editing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/e7c41384-a5e3-4ccb-abec-c8e17056177d.png" alt=" PDF upload interface allowing users to select a PDF before adding margins." style="display:block;margin:0 auto" width="628" height="665" loading="lazy">

<h3 id="heading-step-2-preview-uploaded-pdf-pages">Step 2: Preview Uploaded PDF Pages</h3>
<p>After uploading the document, the application generates page previews directly inside the browser.</p>
<p>Displaying page thumbnails allows users to verify that the correct document has been selected before making any changes. For larger PDFs, the preview section also makes it easier to navigate through the document and inspect individual pages.</p>
<p>This step helps prevent mistakes before the margin settings are applied.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/7a1c681f-8b4f-4ae2-8d2e-e87ea96a07a8.png" alt="Uploaded PDF page previews displayed before margin editing." style="display:block;margin:0 auto" width="750" height="567" loading="lazy">

<h3 id="heading-step-3-configure-margin-settings">Step 3: Configure Margin Settings</h3>
<p>Next, users configure how margins should be added to the document.</p>
<p>The tool allows custom values for the top, bottom, left, and right margins while also supporting predefined presets such as None, Narrow, Normal, and Wide.</p>
<p>Users can choose whether the margins should be applied to every page or only to selected page ranges. Mirror margins are available for printed books and documents that require binding.</p>
<p>Another useful option lets users decide whether the application should expand the overall page size or reposition the existing content while keeping the original page dimensions.</p>
<p>These settings provide complete control over how the final document will appear.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/ef4ca256-537e-4523-9213-8ebf98cfd923.png" alt="Margin settings panel showing custom margins, presets, mirror margins, measurement units, page selection, and resize options." style="display:block;margin:0 auto" width="690" height="771" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/1a21a94d-6e9b-4a59-8df4-4022ece9dcc5.png" alt="marign units setiins " style="display:block;margin:0 auto" width="306" height="151" loading="lazy">

<h3 id="heading-step-4-apply-the-margins">Step 4: Apply the Margins</h3>
<p>After reviewing the selected settings, users simply click the <strong>Add Margins</strong> button.</p>
<p>The browser processes every selected page, calculates the new page dimensions, repositions the original content, and generates an updated PDF with the requested spacing around each page.</p>
<p>If users want to work with another document, the <strong>Start Over</strong> button clears the current session without requiring a page refresh.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f85e1e17-2466-43fc-9462-c14a356d47bb.png" alt="Add Margins button used to generate the updated PDF." style="display:block;margin:0 auto" width="694" height="144" loading="lazy">

<h3 id="heading-step-5-preview-the-updated-pdf">Step 5: Preview the Updated PDF</h3>
<p>Once processing has finished, the updated document is displayed inside the browser.</p>
<p>Users can review every page before downloading to ensure the margins have been applied correctly.</p>
<p>The preview section also includes page navigation controls, making it easy to browse through multi-page documents and confirm that every selected page has been processed successfully.</p>
<p>Reviewing the document before downloading helps catch formatting issues early and reduces the need for additional edits later.</p>
<h3 id="heading-step-6-download-the-final-pdf">Step 6: Download the Final PDF</h3>
<p>After verifying the updated document, users can download the finished PDF.</p>
<p>The final output section displays useful information including the output filename, total number of pages, and file size. Users can rename the generated document before downloading it, making file organization much easier.</p>
<p>Once everything looks correct, the updated PDF can be downloaded and immediately used for printing, sharing, binding, archiving, or submitting to organizations that require specific page margins.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/11e369ac-480d-47b6-96d1-589ead6b0365.png" alt=" Final PDF ready for download showing filename, page count, file size, download button, and Start Over option." style="display:block;margin:0 auto" width="681" height="781" loading="lazy">

<h2 id="heading-important-notes-from-real-world-use">Important Notes from Real-World Use</h2>
<p>Adding margins is generally a lightweight operation, but large PDF files containing hundreds of pages or high-resolution images may require additional processing time.</p>
<p>Before processing begins, it's a good idea to validate the uploaded file.</p>
<pre><code class="language-javascript">if (file.type !== "application/pdf") {
    alert("Please upload a valid PDF file.");
    return;
}
</code></pre>
<p>When working with large documents, verify the selected margin values before generating the final PDF.</p>
<pre><code class="language-javascript">console.log(`Top: ${topMargin}`);
console.log(`Bottom: ${bottomMargin}`);
console.log(`Left: ${leftMargin}`);
console.log(`Right: ${rightMargin}`);
</code></pre>
<p>If the document is intended for printing, preview the generated PDF to ensure that text, tables, images, and page numbers remain correctly positioned.</p>
<p>Because all processing happens locally, documents remain on the user's device throughout the entire workflow, making browser-based margin editing suitable for contracts, invoices, financial reports, legal documents, educational records, healthcare forms, and other confidential PDFs.</p>
<h2 id="heading-common-mistakes-to-avoid">Common Mistakes to Avoid</h2>
<p>One common mistake is using excessively large margin values that reduce the printable area more than necessary.</p>
<p>Always verify the selected measurements before generating the updated document.</p>
<pre><code class="language-javascript">if (leftMargin &lt; 0 || rightMargin &lt; 0) {
    alert("Margin values cannot be negative.");
}
</code></pre>
<p>Another mistake is forgetting to choose the correct page selection mode.</p>
<p>Sometimes only the first page or a specific page range requires additional margins, while the rest of the document should remain unchanged.</p>
<pre><code class="language-javascript">const applyTo = "all";

console.log(`Apply margins to: ${applyTo}`);
</code></pre>
<p>Users should also verify whether <strong>Expand Page Size</strong> or <strong>Keep Original Page Size</strong> is the correct option for their workflow. Choosing the wrong mode can affect the final layout when printing or sharing the document.</p>
<p>Finally, always review the generated PDF before downloading it.</p>
<p>Taking a few moments to inspect the updated pages helps confirm that the spacing is correct, page content remains properly aligned, and the document is ready for printing, binding, archiving, or distribution.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a browser-based PDF Add Margins Tool using JavaScript.</p>
<p>You learned how to upload PDF files, preview document pages, configure custom margin settings, apply margins to selected pages, and generate updated PDF files directly inside the browser.</p>
<p>More importantly, you saw how modern browsers can perform PDF page layout modifications without requiring a backend server.</p>
<p>This approach keeps document processing fast, private, and easy to use while giving users complete control over page spacing.</p>
<p>You can try the live implementation here: <a href="https://allinonetools.net/add-margins-to-pdf/">AllInOneTools - Add Margin to PDF</a>.</p>
<p>Once you understand this workflow, you can extend it further by adding features such as page cropping, resizing, page numbering, watermarking, document organization, metadata editing, and other advanced PDF editing capabilities.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Zero-Cost Personal Project with PHP, Wasmer, and Cloudflare ]]>
                </title>
                <description>
                    <![CDATA[ Recently, I wanted to reinvigorate my open-source project Clarity, an icon theme for Linux (GTK+). The icons allow users to create custom colors by adding SVG templates. And I wanted to have a platfor ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-zero-cost-personal-project-with-php-wasmer-and-cloudflare/</link>
                <guid isPermaLink="false">6a4535bbc75b935d2260d3c0</guid>
                
                    <category>
                        <![CDATA[ PHP ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Hosting ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cloudflare ]]>
                    </category>
                
                    <category>
                        <![CDATA[ dns ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jakub T. Jankiewicz ]]>
                </dc:creator>
                <pubDate>Wed, 01 Jul 2026 15:43:55 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/ac7d0b26-c611-4454-847e-cf69d28a04f8.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Recently, I wanted to reinvigorate my open-source project Clarity, an icon theme for Linux (GTK+).</p>
<p>The icons allow users to create custom colors by adding SVG templates. And I wanted to have a platform where users would be able to submit their own custom templates for the icons.</p>
<p>The problem is that if I create a new website for my project that requires recurring payment, and I'm no longer alive, the website will disappear. So to keep it going in perpetuity, I needed a <strong>free domain</strong> and <strong>free hosting</strong>. I decided to use PHP for this new project.</p>
<p>In this article, I'll show you step by step how to:</p>
<ul>
<li><p>Get a free .eu.org domain.</p>
</li>
<li><p>Set up name servers on Cloudflare.</p>
</li>
<li><p>Set up hosting on <a href="https://wasmer.io/?utm_source=freecodecamp&amp;utm_medium=article&amp;utm_campaign=jcubic">Wasmer</a>.</p>
</li>
<li><p>Glue everything together.</p>
</li>
</ul>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-requirements">Requirements</a></p>
</li>
<li><p><a href="#heading-about-wasmer">About Wasmer</a></p>
</li>
<li><p><a href="#heading-what-is-dns">What is DNS?</a></p>
</li>
<li><p><a href="#heading-how-to-create-a-wasmer-account">How to Create a Wasmer Account?</a></p>
</li>
<li><p><a href="#heading-how-to-create-a-wasmer-php-project">How to Create a Wasmer PHP Project?</a></p>
</li>
<li><p><a href="#heading-how-to-set-up-cloudflare">How to Set Up Cloudflare?</a></p>
</li>
<li><p><a href="#heading-how-to-register-an-euorg-domain">How to Register an eu.org Domain?</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-requirements"><strong>Requirements</strong></h2>
<p>For this article, I assume that you already have a GitHub account and know how to create Git repositories. You should also have a Cloudflare account. The code uses PHP, but you don't need to know it to go through this tutorial. Wasmer supports other languages and frameworks that you can use instead if you like.</p>
<h2 id="heading-about-wasmer">About Wasmer</h2>
<p>First, let me explain how hosting works with Wasmer. You may have had problems with hosting tools that take too long to wake up your project when it hasn't been used for a while. Well, you'll be happy to learn that in Wasmer, a cold start takes less than 90 milliseconds.</p>
<p>Applications on Wasmer are stateless, so if you want to keep user data, you'll need a persistent store. It natively supports cloud MySQL instances and network protocols for external PostgreSQL and MySQL/MariaDB, as well as file-based SQLite through persistent volumes.</p>
<p>Wasmer also supports Python, Rust, PHP, and Node.js, and it plans to support GoLang soon, too.</p>
<p>You can host applications in Django, Flask, FastAPI, WordPress, Symfony, Laravel, Next.js, Nuxt, Hugo, Astro, Vite, and others as well, so you have lots of options.</p>
<p>The platform also supports <a href="https://en.wikipedia.org/wiki/Common_Gateway_Interface">CGI</a>, which allows you to use any language that compiles to a binary executable like Rust, C, or C++.</p>
<h2 id="heading-what-is-dns">What is DNS?</h2>
<p>Now is a good time to explain what DNS is. DNS stands for Domain Name System, and it's a way to translate human-readable names like freecodecamp.org to the IP address of a server where the website or web app is located.</p>
<p>In short, the system works like a tree of servers. At the root there's the Root Server, which directs your request to the TLD (Top-Level Domain) Server responsible for <code>.org</code> extensions.</p>
<p>This TLD server points to the specific Authoritative Name Server of the domain. A Name Server is a specialized server that acts as a storage folder for a domain's official DNS records, telling the internet exactly where to find the website's assets.</p>
<p>Inside these records, you'll find different types of pointers. One common type is the CNAME record (Canonical Name). Instead of pointing a domain directly to an IP address, a CNAME record acts as an alias that maps one domain name to another domain name. For example, it can forward <code>www.freecodecamp.org</code> to the root <code>freecodecamp.org</code>.</p>
<h2 id="heading-how-to-create-a-wasmer-account">How to Create a Wasmer Account</h2>
<p>Let's jump in and create a Wasmer account so we can get started. To do this, you need to go to <a href="https://wasmer.io/">wasmer.io</a> and pick the Hobby plan:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/6a00593d-5e5a-41d2-bc82-c3b061f22ff8.png" alt="Wasmer sign up page that allows you to pick Hobby or Pro plan" style="display:block;margin:0 auto" width="1453" height="873" loading="lazy">

<p>Then create an account:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/8299cdf3-57be-4424-a68d-a07c08cbde68.png" alt="A Screenshot of a Wasmer page were you create your account. It has username, email, password form and allows you to use Google/GitHub to create your account." style="display:block;margin:0 auto" width="1329" height="1063" loading="lazy">

<p>The simplest way forward is to connect to your GitHub:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/26a3a344-011b-4a37-8d22-275ac6c02f6f.png" alt="The screenshot that shows a popup to authorise Wasmer page when you use GitHub as you authentication" style="display:block;margin:0 auto" width="1302" height="1050" loading="lazy">

<p>After you authorize via GitHub, you should be logged in and see your avatar in the top right corner:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/67abeb45-ca0b-44d7-a5e3-1eefecdcae5b.png" alt="Wasmer screenshot showing an avatar and a menu that shows signed in as jcubic with links to your profile, settings, join discord, and sign out." style="display:block;margin:0 auto" width="295" height="371" loading="lazy">

<h2 id="heading-how-to-create-a-wasmer-php-project">How to Create a Wasmer PHP Project</h2>
<p>First, you need to create a new GitHub repository for your PHP project. For this tutorial, I created one with a simple index.php file that shows my new domain name (I'll show you how to register the domain name in a bit).</p>
<pre><code class="language-php">&lt;?php

header("Content-Type: text/plain");
echo "clarity-icons.eu.org";

?&gt;
</code></pre>
<p>After you commit the file, this is how your repo should look on GitHub:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/e50b9e14-5f2f-427c-b868-217155ec8871.png" alt="A screenshot of the GitHub repo with a single index.php file and no README" style="display:block;margin:0 auto" width="1165" height="675" loading="lazy">

<p>This was my repository: <a href="https://github.com/jcubic/Clarity-icons">https://github.com/jcubic/Clarity-icons</a>. But then I decided to add the website as part of my <a href="https://github.com/jcubic/Clarity">main repo</a>.</p>
<p>After you create a GitHub account, you have to tell Wasmer about it. To do this, you need to add a Wasmer GitHub account. Create a new project and click "Add GitHub Account":</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/49e2e192-2b20-4a5f-baba-c68aa61acdc2.png" alt="A screenshot of a page in Wasmer that shows at the top two cards one to import from GitHub and one to drag &amp; drop your site. At the bottom are list of Templates: Wordpress Starter, Flask Starter, Hugo Starter, Gatsby Starter" style="display:block;margin:0 auto" width="1381" height="1150" loading="lazy">

<p>You should see a popup where you can select the account where you want to install the Wasmer app. If you're not part of any organization on GitHub, you should see only your own GitHub profile. Select that one.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/18a96d88-d473-4aae-b9cc-0e39c4c81737.png" alt="A screenshot of a modal that allows you to install Wasmer app into your account or organization" style="display:block;margin:0 auto" width="1406" height="1180" loading="lazy">

<p>It should redirect you to GitHub, where you can either provide access to all repositories or pick the one you want. I usually just give access to specific repositories. So I've selected my new repo here:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/330e4072-3b9b-42d5-96b6-fac681f1074f.png" alt="A screenshot of a page that allows Wasmer app to give projects. The option to select all repositories is unselected and instead a single repo jcubic/clarity-icons is. There is a button labeled Install and Authorize." style="display:block;margin:0 auto" width="686" height="969" loading="lazy">

<p>After you set up permissions, you'll need to import the app into Wasmer:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/93267bbb-0321-4335-a9aa-2f4e491d2724.png" alt="A screnshot with a card that says Import from GitHub that has a dropdown with GitHub icon and jcubic name. Below there is a repository called clarity-icons and a button labeled Import" style="display:block;margin:0 auto" width="679" height="453" loading="lazy">

<p>After you click import, you can set up some details about your project like the owner, project link, and so on:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/3d5be9b9-8fbd-4f7f-8b4a-88044b2b064d.png" alt="A screesnhot of the page where you can configure the project.  It shows the project link (clarity-icons.wasmer.app), project preset with selected PHP and version 8.3" style="display:block;margin:0 auto" width="939" height="1080" loading="lazy">

<p>After you click "Deploy", you'll need to wait awhile for deployment to happen:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/961a7494-e05a-4691-bae8-e5bef94ec206.png" alt="A screenshot of page that says Configure Project completed and shows a deployment progress indicator" style="display:block;margin:0 auto" width="938" height="590" loading="lazy">

<p>When it finishes, you should see this celebration page with confettii:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/0bd084d2-66e4-4dc2-9789-473efbd58383.png" alt="Deployment congratulations page with confetti" style="display:block;margin:0 auto" width="1509" height="1012" loading="lazy">

<p>When you go to the dashboard, you should see that the app is running:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/2b2ffdfb-7239-49ee-8166-ea228ff6c680.png" alt="Main Wasmer project dashboard for clarity-icons project with status ready and screenshot of the website" style="display:block;margin:0 auto" width="1470" height="973" loading="lazy">

<p>This is how the website looks live:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/1987b13c-cd8f-4ccb-936d-89c5fc284084.png" alt="A screenshot of the browser windows that shows URL clarity-icons.wasmer.app. The page shows text: clarity-icons.eu.org" style="display:block;margin:0 auto" width="473" height="134" loading="lazy">

<p>In the settings, you can see how to add a custom domain (that we'll set up in a minute).</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/5a7f5775-c9b9-49be-987b-276916104cee.png" alt="A screenshot of DNS configuration for the clarity-icons project. IT shows CNAME record that you need to add to enable a custom domain." style="display:block;margin:0 auto" width="1407" height="1228" loading="lazy">

<p>It shows the CNAME DNS record that you need to include in order for the custom domain to work properly.</p>
<h2 id="heading-how-to-set-up-cloudflare">How to Set Up Cloudflare</h2>
<p>Next, we'll set up Cloudflare, which allows you to manage DNS. We'll keep our new domain there.</p>
<p>After you create a Cloudflare account, you'll need to go into the domain overview section:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/b9962ff0-9d7d-4b46-be40-67c2eec85797.png" alt="A screenshot of the page where you can add a new domain" style="display:block;margin:0 auto" width="1750" height="670" loading="lazy">

<p>Click "Add domain" and pick "Connect a domain":</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/0feaf033-a185-41ae-998a-5bdf4c5f77f4.png" alt="A screenshot of a page with a list of options: Connect a domain, Transfer a domain, and Buy a domain" style="display:block;margin:0 auto" width="815" height="455" loading="lazy">

<p>Then you need to name your domain:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/176d8c85-b85d-4969-b7cf-ab1cd7220a90.png" alt="a form to connect your domain with one input box that has domain name &quot;clarity-icons.eu.org&quot;" style="display:block;margin:0 auto" width="807" height="409" loading="lazy">

<p>Then pick the Free plan:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/aad724cf-f59f-48c7-8071-cc3106aaf77f.png" alt="Option to pick one of the 4 plans, the first on the left is Free $0" style="display:block;margin:0 auto" width="1898" height="1334" loading="lazy">

<p>After you create your domain (you'll need to wait a few seconds), you can set up DNS records (we already have the CNAME from Wasmer).</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/82d6fcda-ab5e-477e-b09b-e8a379a7050f.png" alt="A screenshot of page to configure DNS without any record" style="display:block;margin:0 auto" width="1128" height="1311" loading="lazy">

<p>Add a new CNAME record from Wasmer:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/f4c9c419-cf3e-4667-adf9-14c22917978e.png" alt="A screenshot of a page for domain clarity-icons.eu.org with one DNS CNAME record that has the name clarity-icons and ID from Wasmer" style="display:block;margin:0 auto" width="1125" height="1294" loading="lazy">

<p>After you click "Continue activation" you should see this page with two nameservers:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/18874ad2-3136-46ef-8a26-b763ab3b2ff0.png" alt="A screenshot of the page that shows how to add Cloudflare name servers" style="display:block;margin:0 auto" width="924" height="917" loading="lazy">

<p>We'll use those two domain names when we register the eu.org domain.</p>
<h2 id="heading-how-to-register-an-euorg-domain">How to Register an eu.org Domain</h2>
<p>To create a .eu.org domain, you first have to create an account. For this, you'll need to visit <a href="https://nic.eu.org/arf/en/contact/create/">https://nic.eu.org/arf/en/contact/create/</a> and fill in the details, like your address and a phone number. That type of information is required for any domain you register.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/dc97078f-fa42-4c9f-893b-db08a5f9504b.png" alt="A screenshot of a simple form that has user details: username, email, password, address, phone number and a button labeled create" style="display:block;margin:0 auto" width="1631" height="680" loading="lazy">

<p>In the above screenshot, I've added "Fax" instead of "Phone". I corrected that later. You can always edit the information if you make a mistake.</p>
<p>After you click create, you should see this page showing that you successfully created your contact page:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/99b74695-4902-488c-9d0c-4078cd654deb.png" alt="A pages that says: Contact sucessfully create as JTJ18-FREE. Please check instructions sent t o jcubic@jcubic.pl to validate it." style="display:block;margin:0 auto" width="989" height="128" loading="lazy">

<p>After you validate your email (click the activation link), you should see this page stating that your contact handle is now valid:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/d4c25b8b-af6c-4d41-b8eb-ae4012303766.png" alt="A page with text &quot;Your contact handle is now valid&quot;" style="display:block;margin:0 auto" width="989" height="134" loading="lazy">

<p>After you log in, you'll see a form where you can add domains:</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/2a413835-2d7c-4657-8e0e-10f0a456a7ea.png" alt="Authenticated page for JTJ18-FREE user account, with a button to add a new domain." style="display:block;margin:0 auto" width="990" height="231" loading="lazy">

<p>Then just click and select a domain name.</p>
<p>I initially picked the clarity-icons.eu.org domain, but on the page <a href="https://nic.eu.org/opendomains.html">https://nic.eu.org/opendomains.html</a>, they don't recommend using a .eu.org directly. Instead, they recommend picking one of the subdomains (an additional prefix name with a dot). I've picked .pl.eu.org since I'm from Poland.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/88b2f036-5374-47cb-a408-521ccd42253e.png" alt="A screenshot of a form to add a new domain with user information" style="display:block;margin:0 auto" width="992" height="731" loading="lazy">

<p>The process of creating a .eu.org can take a few days. I registered an account on the 1st of June and got the below email on the 6th of June. So a week is a safe bet.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60fdacd518fbbb77c946882e/f89b6cc1-34ea-4d91-b8ea-6f600cb194a4.png" alt="Email confirmation that says that the clarity.pl.eu.org was created with details about domain and text: &quot;Please allow about half a day for propagation&quot;" style="display:block;margin:0 auto" width="744" height="734" loading="lazy">

<p>The domain appeared after about 24 hours.</p>
<p>I've checked the next day, and my domain, <a href="https://clarity.pl.eu.org/">clarity.pl.eu.org</a> was up.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Creating a sustainable website for your open-source project that will remain long after you're gone is possible. This type of setup is also great for small personal projects.</p>
<p>If you have any questions, you can contact me on <a href="https://x.com/jcubic">Twitter/X</a>, my DMs are open. You can also check out my <a href="https://jakub.jankiewicz.org/blog/">personal blog</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Text Compare Tool with HTML, CSS, and JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ Have you ever tried to spot the differences between two long paragraphs of text? Reading line-by-line to find a missing word or a new sentence is a massive headache. In this tutorial, you'll build you ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-text-compare-tool-html-css-javascript/</link>
                <guid isPermaLink="false">6a4533f11247307c0491a76f</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ HTML5 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ CSS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Bansidhar Kadiya ]]>
                </dc:creator>
                <pubDate>Wed, 01 Jul 2026 15:36:17 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/632b58c6-8930-421d-b17d-847b24bb0e9e.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Have you ever tried to spot the differences between two long paragraphs of text? Reading line-by-line to find a missing word or a new sentence is a massive headache.</p>
<p>In this tutorial, you'll build your very own browser-based Text Compare Tool. It will take an original piece of text, compare it against a changed version, and instantly highlight exactly what was added or removed.</p>
<p>Building this project will help you level up your JavaScript skills. You'll also create a tool that's highly secure, because everything happens locally in the user's browser. No sensitive data is ever sent to a server.</p>
<p>Let’s get started.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along easily, you should know:</p>
<ul>
<li><p><strong>Basic HTML and CSS knowledge:</strong> How to structure a page and use Flexbox to put items side-by-side.</p>
</li>
<li><p><strong>Basic JavaScript knowledge:</strong> How to write functions, use arrays, and listen for button clicks.</p>
</li>
<li><p><strong>Your Setup:</strong> A code editor (like VS Code) and a web browser to view your work.</p>
</li>
</ul>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-step-1-set-up-your-project-files">Step 1: Set Up Your Project Files</a></p>
</li>
<li><p><a href="#heading-step-2-build-the-html-structure">Step 2: Build the HTML Structure</a></p>
</li>
<li><p><a href="#heading-step-3-style-the-tool-with-css">Step 3: Style the Tool with CSS</a></p>
</li>
<li><p><a href="#heading-step-4-write-the-javascript-engine">Step 4: Write the JavaScript Engine</a></p>
</li>
<li><p><a href="#heading-step-5-test-your-application">Step 5: Test Your Application</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-step-1-set-up-your-project-files">Step 1: Set Up Your Project Files</h2>
<p>First, you need a place to store your code. Create a new folder on your computer and name it <code>text-compare-tool</code>.</p>
<p>Inside that folder, create three empty files:</p>
<ul>
<li><p><code>index.html</code> (This holds the structure of your app)</p>
</li>
<li><p><code>style.css</code> (This makes your app look good)</p>
</li>
<li><p><code>script.js</code> (This makes your app actually work)</p>
</li>
</ul>
<h2 id="heading-step-2-build-the-html-structure">Step 2: Build the HTML Structure</h2>
<p>Open your <code>index.html</code> file. You need to create a simple layout with two large text boxes: one for the original text, and one for the updated text.</p>
<p>Copy and paste this code into your HTML file:</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;Text Compare Tool&lt;/title&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;h1&gt;Text Compare Tool&lt;/h1&gt;
    &lt;p class="description"&gt;
        Quickly find every addition and deletion between two versions of your text. Just paste them into our tool, and we’ll show you exactly what’s been changed.
    &lt;/p&gt;

    &lt;div class="container"&gt;
        
        &lt;div class="panels-wrapper"&gt;
            &lt;!-- Left Side: Original Text --&gt;
            &lt;div class="panel"&gt;
                &lt;textarea id="text1" placeholder="Paste your original text here..."&gt;&lt;/textarea&gt;
                &lt;div id="result1" class="result-box"&gt;&lt;/div&gt;
            &lt;/div&gt;
            
            &lt;!-- Right Side: Changed Text --&gt;
            &lt;div class="panel"&gt;
                &lt;textarea id="text2" placeholder="Paste your changed text here..."&gt;&lt;/textarea&gt;
                &lt;div id="result2" class="result-box"&gt;&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;!-- Action Buttons --&gt;
        &lt;div class="controls"&gt;
            &lt;button class="btn-compare" onclick="compareText()"&gt;Compare&lt;/button&gt;
            &lt;button class="btn-clear" onclick="clearText()"&gt;Clear&lt;/button&gt;
        &lt;/div&gt;

    &lt;/div&gt;

    &lt;script src="script.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>Understanding the HTML:</p>
<ul>
<li><p><strong>The two panels:</strong> Inside the <code>.panels-wrapper</code>, you have a left side and a right side.</p>
</li>
<li><p><strong>Textareas vs results:</strong> Each side has a <code>&lt;textarea&gt;</code> where the user can type. Right below the text area is a <code>&lt;div&gt;</code> with the class <code>.result-box</code>. Right now, those result boxes are invisible. Later, JavaScript will hide the text areas and show the result boxes instead.</p>
</li>
<li><p><strong>The buttons:</strong> The "Compare" and "Clear" buttons are hooked up to JavaScript functions using <code>onclick</code>.</p>
</li>
</ul>
<h2 id="heading-step-3-style-the-tool-with-css">Step 3: Style the Tool with CSS</h2>
<p>A good utility tool should be easy on the eyes. You'll use a clean white and blue design, and apply soft red and green colors to highlight the text changes.</p>
<p>Open your <code>style.css</code> file and add this code:</p>
<pre><code class="language-css">:root {
    --primary-blue: #007bff;
    --background-color: #f8f9fa;
    --text-color: #202124;
    --border-color: #dadce0;
    
    /* Highlight Colors */
    --red-bg: #fce8e6;
    --red-text: #c5221f;
    --green-bg: #e6f4ea;
    --green-text: #137333;
}

body {
    font-family: Arial, sans-serif;
    background-color: var(--background-color);
    color: var(--text-color);
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 40px 20px;
    margin: 0;
}

h1 {
    margin-bottom: 10px;
}

.description {
    text-align: center;
    max-width: 600px;
    color: #5f6368;
    margin-bottom: 30px;
    line-height: 1.5;
}

.container {
    background: white;
    padding: 20px;
    border-radius: 8px;
    border: 1px solid var(--border-color);
    width: 100%;
    max-width: 1000px;
    box-shadow: 0 4px 10px rgba(0,0,0,0.05);
}

.panels-wrapper {
    display: flex;
    gap: 20px;
    margin-bottom: 20px;
}

.panel {
    flex: 1;
    display: flex;
    flex-direction: column;
}

textarea, .result-box {
    width: 100%;
    height: 300px;
    padding: 15px;
    border: 1px solid var(--border-color);
    border-radius: 6px;
    font-size: 16px;
    line-height: 1.5;
    box-sizing: border-box;
    resize: vertical;
}

textarea:focus {
    outline: none;
    border-color: var(--primary-blue);
}

/* Hidden by default */
.result-box {
    display: none; 
    background-color: #fafafa;
    overflow-y: auto;
    white-space: pre-wrap; 
}

.controls {
    display: flex;
    justify-content: center;
    gap: 15px;
}

button {
    padding: 10px 25px;
    font-size: 16px;
    font-weight: bold;
    border: none;
    border-radius: 5px;
    cursor: pointer;
}

.btn-compare {
    background-color: var(--primary-blue);
    color: white;
}

.btn-clear {
    background-color: white;
    color: var(--primary-blue);
    border: 1px solid var(--border-color);
}

/* How the differences will look */
.deleted {
    background-color: var(--red-bg);
    color: var(--red-text);
    padding: 2px 4px;
    border-radius: 3px;
}

.added {
    background-color: var(--green-bg);
    color: var(--green-text);
    padding: 2px 4px;
    border-radius: 3px;
}
</code></pre>
<p>Understanding the CSS:</p>
<ul>
<li><p><strong>Flexbox layout:</strong> <code>display: flex;</code> inside <code>.panels-wrapper</code> is what places your two text boxes neatly side-by-side.</p>
</li>
<li><p><strong>The highlighters:</strong> The <code>.deleted</code> and <code>.added</code> classes are the most important part of the visual design. When a user deletes a word, we give it a soft red background. When they add a word, it gets a soft green background.</p>
</li>
</ul>
<p>This is what your tool will look like once it's finished:</p>
<img src="https://cdn.hashnode.com/uploads/covers/699c7b22cf5def0f6aaf982b/6676b86c-c0ea-4dec-b5b7-489e8e06f58b.png" alt="Text Compare Tool Preview" style="display:block;margin:0 auto" width="1874" height="872" loading="lazy">

<h2 id="heading-step-4-write-the-javascript-engine">Step 4: Write the JavaScript Engine</h2>
<p>Now you need to make the tool actually work. How does your computer know if a word has changed?</p>
<p>We have to write logic that breaks paragraphs down into individual words. The code will look at the original list of words and compare it to the new list. If a word from the original text is missing, it gets marked as "deleted." If a brand new word appears, it gets marked as "added."</p>
<p>Open your <code>script.js</code> file and paste in this complete, working code:</p>
<pre><code class="language-javascript">function compareText() {
    // 1. Grab the text from the text boxes
    const text1 = document.getElementById('text1').value;
    const text2 = document.getElementById('text2').value;

    // 2. Chop the text up into an array of words (and keep the spaces)
    const words1 = text1.split(/(\s+)/);
    const words2 = text2.split(/(\s+)/);

    // 3. Find the differences
    const { diff1, diff2 } = calculateDifferences(words1, words2);

    const resultBox1 = document.getElementById('result1');
    const resultBox2 = document.getElementById('result2');
    
    // 4. Turn those differences into HTML with colors
    resultBox1.innerHTML = createColoredHTML(diff1, 'deleted');
    resultBox2.innerHTML = createColoredHTML(diff2, 'added');

    // 5. Hide the text boxes and show the final results
    document.getElementById('text1').style.display = 'none';
    document.getElementById('text2').style.display = 'none';
    resultBox1.style.display = 'block';
    resultBox2.style.display = 'block';
}

// The engine that compares the two lists of words
function calculateDifferences(arr1, arr2) {
    const n = arr1.length;
    const m = arr2.length;
    
    // Create a grid to keep track of matching words
    const grid = Array.from({ length: n + 1 }, () =&gt; Array(m + 1).fill(0));

    for (let i = 1; i &lt;= n; i++) {
        for (let j = 1; j &lt;= m; j++) {
            if (arr1[i - 1] === arr2[j - 1]) {
                grid[i][j] = grid[i - 1][j - 1] + 1;
            } else {
                grid[i][j] = Math.max(grid[i - 1][j], grid[i][j - 1]);
            }
        }
    }

    let i = n, j = m;
    const diff1 = [];
    const diff2 = [];

    // Walk backwards through the grid to mark what changed
    while (i &gt; 0 || j &gt; 0) {
        if (i &gt; 0 &amp;&amp; j &gt; 0 &amp;&amp; arr1[i - 1] === arr2[j - 1]) {
            diff1.unshift({ value: arr1[i - 1], type: 'equal' });
            diff2.unshift({ value: arr2[j - 1], type: 'equal' });
            i--;
            j--;
        } else if (j &gt; 0 &amp;&amp; (i === 0 || grid[i][j - 1] &gt;= grid[i - 1][j])) {
            diff2.unshift({ value: arr2[j - 1], type: 'changed' });
            j--;
        } else if (i &gt; 0 &amp;&amp; (j === 0 || grid[i][j - 1] &lt; grid[i - 1][j])) {
            diff1.unshift({ value: arr1[i - 1], type: 'changed' });
            i--;
        }
    }

    return { diff1, diff2 };
}

// Packages the text safely into HTML span elements
function createColoredHTML(diffArray, colorClass) {
    return diffArray.map(wordItem =&gt; {
        // Replace dangerous characters so the browser doesn't crash
        const safeText = wordItem.value.replace(/&lt;/g, "&amp;lt;").replace(/&gt;/g, "&amp;gt;");
        
        // If the word was changed (and isn't just a blank space), wrap it in color
        if (wordItem.type === 'changed' &amp;&amp; !/^\s+$/.test(wordItem.value)) {
            return `&lt;span class="${colorClass}"&gt;${safeText}&lt;/span&gt;`;
        }
        return safeText;
    }).join('');
}

// Puts the tool back to its default state
function clearText() {
    document.getElementById('text1').value = '';
    document.getElementById('text2').value = '';
    
    document.getElementById('text1').style.display = 'block';
    document.getElementById('text2').style.display = 'block';
    
    document.getElementById('result1').style.display = 'none';
    document.getElementById('result2').style.display = 'none';
}
</code></pre>
<p>Understanding the JavaScript:</p>
<ol>
<li><p><strong>Keeping the formatting:</strong> In the first function, you see <code>.split(/(\s+)/)</code>. This splits the text up by spaces, but <em>keeps</em> the spaces and line-breaks. If you don't do this, all of the user's paragraphs will mash into one giant block of text!</p>
</li>
<li><p><strong>The grid system:</strong> The <code>calculateDifferences</code> function creates an invisible grid. It compares every word in the first box with every word in the second box. If it sees the same word in the same order, it leaves it alone. If it hits a snag, it marks the word as a change.</p>
</li>
<li><p><strong>Safety first:</strong> The <code>createColoredHTML</code> function wraps our changed words in <code>&lt;span class="added"&gt;</code> or <code>&lt;span class="deleted"&gt;</code> so CSS can color them. But before it does that, it removes any <code>&lt;</code> or <code>&gt;</code> symbols using <code>.replace()</code>. This stops hackers from pasting malicious code into your app.</p>
</li>
</ol>
<h2 id="heading-step-5-test-your-application">Step 5: Test Your Application</h2>
<p>You're completely done coding! Now it’s time to see it in action.</p>
<ol>
<li><p>Open your <code>text-compare-tool</code> folder.</p>
</li>
<li><p>Double-click the <code>index.html</code> file. It will open in your default web browser.</p>
</li>
<li><p>Type a sentence into the left box: <em>"The quick brown fox jumps over the lazy dog."</em></p>
</li>
<li><p>Type a slightly different sentence into the right box: <em>"The fast brown fox jumps over the sleepy dog."</em></p>
</li>
<li><p>Click <strong>Compare</strong>.</p>
</li>
</ol>
<p>You will instantly see the word "quick" highlight in red on the left, and the word "fast" highlight in green on the right. If you want to start over, just click <strong>Clear</strong>.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Great job! You just built a highly practical, browser-based text comparison utility using nothing but pure HTML, CSS, and JavaScript.</p>
<p>You learned how to break text into arrays, compare them using a grid-based algorithm, and manipulate the DOM to show those differences to the user safely. Because this tool relies on local browser processing, it's incredibly fast and 100% private.</p>
<p>If you want to see this exact logic running in a live production environment, or if you need to bookmark a fast tool for your own writing tasks, check out the live <a href="https://99tools.net/text-compare-tool/">Text Compare Tool</a>. Keep experimenting with the code, and happy building!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use the Screen Reader That's Built into Your iPhone ]]>
                </title>
                <description>
                    <![CDATA[ Every iPhone and iPad includes a built-in screen reader called VoiceOver. VoiceOver speaks aloud the text on the screen, app names, icons, buttons, menus, links, and notifications and alerts. These ac ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-the-screen-reader-that-s-built-into-your-iphone/</link>
                <guid isPermaLink="false">6a442b57dd852a5d76691859</guid>
                
                    <category>
                        <![CDATA[ Accessibility ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ iphone ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Screen Reader ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ilknur Eren ]]>
                </dc:creator>
                <pubDate>Tue, 30 Jun 2026 20:47:19 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/b5b3867f-7404-4557-85e6-bc508c88a961.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Every iPhone and iPad includes a built-in screen reader called VoiceOver.</p>
<p>VoiceOver speaks aloud the text on the screen, app names, icons, buttons, menus, links, and notifications and alerts.</p>
<p>These accessibility features are crucial for users who may be blind, have low vision, or have reading differences.</p>
<p>As a developer, it's always important to manually test your website for accessibility. A screen reader is one of the most important tools to add to your testing process. Even small issues, like a button with no label or an image with no alt text, can make a page completely unusable for someone relying on VoiceOver.</p>
<p>As you test on an actual device with VoiceOver, you may find accessibility issues you didn’t know you had. In this tutorial, we'll cover how to turn VoiceOver on, the basic gestures to know, and how to adjust its settings to fit your needs.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-how-to-turn-on-voiceover">How to turn on VoiceOver</a></p>
<ul>
<li><p><a href="#heading-option-1-use-settings">Option 1: Use Settings</a></p>
</li>
<li><p><a href="#heading-option-2-use-siri">Option 2: Use Siri</a></p>
</li>
<li><p><a href="#heading-option-3-set-up-the-accessibility-shortcut">Option 3: Set up the Accessibility Shortcut</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-basic-gestures-to-know">Basic Gestures to Know</a></p>
</li>
<li><p><a href="#heading-how-to-adjust-voiceover-settings">How to Adjust VoiceOver Settings</a></p>
<ul>
<li><p><a href="#heading-change-the-speaking-rate">Change the Speaking Rate</a></p>
</li>
<li><p><a href="#heading-change-the-voice">Change the Voice</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-how-to-turn-on-voiceover"><strong>How to Turn On VoiceOver</strong></h2>
<p>There are a few ways to turn VoiceOver on or off. As you practice, you might lean toward one option over the others.</p>
<h3 id="heading-option-1-use-settings">Option 1: Use Settings</h3>
<ol>
<li><p>Open the <strong>Settings</strong> app.</p>
</li>
<li><p>Tap <strong>Accessibility</strong>.</p>
</li>
<li><p>Tap <strong>VoiceOver</strong>.</p>
</li>
<li><p>Toggle it on or off.</p>
</li>
</ol>
<p>In the Accessibility Settings section, you can also find other accessible settings to explore — including Display &amp; Text Size, Motion, and Spoken Content. It's worth browsing through to understand what tools are available to users.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68379e7b1fd0b956f4ad9839/d6b8dc8e-cb17-481d-a0cd-a8966448f7c7.png" alt="iOS Accessibility setting page" style="display:block;margin:0 auto" width="1125" height="2436" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/68379e7b1fd0b956f4ad9839/416f54e2-5c86-436c-a870-1ec65ed9128f.png" alt="iOS Accessibility Setting VoiceOver Section" style="display:block;margin:0 auto" width="1125" height="2436" loading="lazy">

<h3 id="heading-option-2-use-siri">Option 2: Use Siri</h3>
<p>To turn on VoiceOver using Siri, say: <strong>"Hey Siri, turn on VoiceOver."</strong></p>
<p>To turn it off, say: <strong>"Hey Siri, turn off VoiceOver."</strong></p>
<p>Using Siri might be the easiest way to turn VoiceOver on or off for beginners. If you accidentally turn VoiceOver on and don't yet know any shortcuts or gestures, just ask Siri. Siri works independently of VoiceOver gestures, so it's a reliable fallback when you feel stuck.</p>
<h3 id="heading-option-3-set-up-the-accessibility-shortcut">Option 3: Set Up the Accessibility Shortcut</h3>
<p>If you find yourself turning VoiceOver on and off frequently, setting up the Accessibility Shortcut makes sense. This is the quickest method for developers who are regularly switching VoiceOver on to test and off to work. It lets you toggle VoiceOver by pressing the side button three times.</p>
<ol>
<li><p>Go to <strong>Settings &gt; Accessibility</strong>.</p>
</li>
<li><p>Scroll down and tap <strong>Accessibility Shortcut</strong>.</p>
</li>
<li><p>Select <strong>VoiceOver</strong>.</p>
</li>
</ol>
<p>After that, press the side button (or Home button on older iPhones) <strong>three times</strong> to toggle VoiceOver on or off. If you have more than one accessibility feature enabled in the shortcut, your iPhone will show a menu to pick from instead of toggling automatically.</p>
<h2 id="heading-basic-gestures-to-know"><strong>Basic Gestures to Know</strong></h2>
<p>When VoiceOver is on, the way you touch the screen changes what the phone interprets. The same swipe or tap that normally opens an app does something different with VoiceOver active.</p>
<p>Here are the five core gestures every developer should learn first:</p>
<ul>
<li><p><strong>Swipe right</strong>: Move to the next item on screen</p>
</li>
<li><p><strong>Swipe left</strong>: Move to the previous item on screen</p>
</li>
<li><p><strong>Swipe up or down with three fingers</strong>: Scroll up or down the page</p>
</li>
<li><p><strong>One tap</strong>: Hear VoiceOver read the item aloud</p>
</li>
<li><p><strong>Double-tap</strong>: Open an app or activate a button</p>
</li>
</ul>
<p><strong>Tip:</strong> When you first tap an item, VoiceOver reads it to you. Then you can double-tap to actually open or activate it. This two-step process helps you confirm you're on the right element before you act. This is especially useful when testing unfamiliar interfaces.</p>
<p>These five gestures are the foundation. As you use VoiceOver more frequently, they'll become second nature. Once you're comfortable, you can explore more advanced gestures like the VoiceOver rotor, which lets you navigate by headings, links, form fields, and more.</p>
<p>For the time being, if you're comfortable with these five gestures, you’ll be able to test mobile accessibility issues for your products.</p>
<h2 id="heading-how-to-adjust-voiceover-settings"><strong>How to Adjust VoiceOver Settings</strong></h2>
<p>You can change how VoiceOver sounds and behaves to better suit your testing workflow or personal preferences.</p>
<h3 id="heading-change-the-speaking-rate">Change the Speaking Rate</h3>
<ol>
<li><p>Go to <strong>Settings &gt; Accessibility &gt; VoiceOver</strong>.</p>
</li>
<li><p>Use the <strong>Speaking Rate</strong> slider to make it faster or slower.</p>
</li>
</ol>
<p>You can also adjust the speaking rate with the VoiceOver rotor. Rotate two fingers on the screen until you hear “Speaking Rate,” then swipe up or down with one finger to make VoiceOver faster or slower.</p>
<p>This changes the rate on the fly without going into Settings. It's handy when you want to slow down while exploring a complex page or speed up when navigating familiar content.</p>
<p>Experienced VoiceOver users often run the speaking rate very fast. Don't be surprised if the default speed feels quick. You can always slow it down while you're learning.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68379e7b1fd0b956f4ad9839/4247bbbb-44e7-43c6-a341-07f0c7b3fc2e.png" alt="iOS Accessibility Speaking Rate Section" style="display:block;margin:0 auto" width="1125" height="2436" loading="lazy">

<h3 id="heading-change-the-voice">Change the Voice</h3>
<p>If you want to change the voice or language, Go to Settings &gt; Accessibility &gt; VoiceOver &gt; Speech. From there, you can choose a different VoiceOver voice, add rotor voices for other languages, or enable language detection.</p>
<p>Apple offers multiple voice options across many languages. This is useful when testing multilingual content, but make sure your site also uses correct <code>lang</code> attributes so screen readers can switch pronunciation appropriately.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>As a developer, it's always important to manually test your website for accessibility. Testing it with the accessibility features your users use is crucial to access the product through their lens and fix accessibility bugs the product might have.</p>
<p>Simply turning the VoiceOver on and learning about these five simple gestures will give you the tools to audit and test your website for accessibility issues.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Stop Your AI Coding Agent from Writing Outdated Code with Modern Web Guidance ]]>
                </title>
                <description>
                    <![CDATA[ AI coding agents can save developers a lot of time – that is, until you open the output and realize they've written code like it's 2019. Ask an agent to build a tooltip, for example. The HTML looks po ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-stop-your-ai-coding-agent-from-writing-outdated-code-with-modern-web-guidance/</link>
                <guid isPermaLink="false">6a3c65fd59937171080147f8</guid>
                
                    <category>
                        <![CDATA[ ai agents ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ophy Boamah ]]>
                </dc:creator>
                <pubDate>Wed, 24 Jun 2026 23:19:25 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/c231d1d5-6f7b-40c2-b255-81012edb97e0.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>AI coding agents can save developers a lot of time – that is, until you open the output and realize they've written code like it's 2019.</p>
<p>Ask an agent to build a tooltip, for example. The HTML looks polished, the CSS transitions are smooth, the <code>aria-describedby</code> wiring is correct. Then you get to the JavaScript: a <code>js-hidden</code> class toggle system, a <code>dismissAllTooltips()</code> function, touch event handlers, click-outside detection, and an entire interaction management layer to compensate for what CSS alone can't do.</p>
<p>The agent isn't broken. It's just reaching for patterns that dominate its training data, even though the browser has had better answers for years.</p>
<p>Modern Web Guidance (MWG) is Google Chrome's open-source fix. It injects expert-vetted, platform-aware guidance directly into your AI agent's context, steering it toward current, accessible, and performant web standards.</p>
<p>In this article, you'll learn why Modern Web Guidance solves the "legacy code" problem, and how to integrate it into your workflow for consistently up-to-date results.</p>
<h3 id="heading-table-of-contents">Table of Contents:</h3>
<ul>
<li><p><a href="#heading-why-do-ai-agents-default-to-legacy-patterns">Why Do AI Agents Default to Legacy Patterns?</a></p>
</li>
<li><p><a href="#heading-what-is-modern-web-guidance-mwg">What Is Modern Web Guidance (MWG)?</a></p>
</li>
<li><p><a href="#heading-how-to-install-modern-web-guidance">How To Install Modern Web Guidance</a></p>
</li>
<li><p><a href="#heading-after-installing-modern-web-guidance-what-actually-changes">After Installing Modern Web Guidance: What Actually Changes</a></p>
</li>
<li><p><a href="#heading-what-modern-web-guidance-does-not-handle-for-you">What Modern Web Guidance Does Not Handle for You</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-why-do-ai-agents-default-to-legacy-patterns">Why Do AI Agents Default to Legacy Patterns?</h2>
<p>Every large language model (LLM) learns from the web, which is evolving at a truly rapid pace. New browser APIs ship years before they have enough tutorials, Stack Overflow answers, and real-world codebases to meaningfully appear in training data.</p>
<p>The practical result: even when a model has been trained to know that a modern API exists, it has seen the old approach thousands of times and the new approach a handful of times. As a result, when it generates code, the legacy pattern wins, not because the model is ignorant, but because the training signal for the outdated approach is stronger.</p>
<p>Prompting doesn't fully solve this. Telling your agent to "use modern APIs" nudges things slightly, but it doesn't provide the dense, expert-vetted implementation patterns the model needs to write production-ready modern code confidently. You'd have to paste in documentation for every feature, in every session, indefinitely.</p>
<p>Here's what the problem looks like in practice. To have real outputs to test, I prompted Antigravity IDE to build two separate components without Modern Web Guidance installed.</p>
<h3 id="heading-before-tooltip-component">Before: Tooltip Component</h3>
<p><strong>Prompt:</strong> "Build a tooltip component that appears above a button when hovered."</p>
<p>The HTML is reasonable. The CSS handles positioning with <code>position: absolute</code>, animates opacity, and even wires up <code>role="tooltip"</code> and <code>aria-describedby</code> correctly. Then you get to the JavaScript:</p>
<pre><code class="language-javascript">// ❌ Before MWG — a full interaction management layer built in JS
document.addEventListener('DOMContentLoaded', () =&gt; {
  const containers = document.querySelectorAll('.tooltip-container');

  containers.forEach(container =&gt; {
    const trigger = container.querySelector('.tooltip-trigger');
    const tooltip = container.querySelector('.tooltip-content');

    const forceHide = () =&gt; tooltip.classList.add('js-hidden');
    const resetVisibility = () =&gt; tooltip.classList.remove('js-hidden');

    // Escape key to dismiss
    trigger.addEventListener('keydown', (e) =&gt; {
      if (e.key === 'Escape') { forceHide(); e.preventDefault(); }
    });

    trigger.addEventListener('blur', resetVisibility);
    container.addEventListener('mouseleave', resetVisibility);
    container.addEventListener('mouseenter', resetVisibility);

    // Touch handling
    trigger.addEventListener('touchstart', (e) =&gt; {
      const isVisible = !tooltip.classList.contains('js-hidden') &amp;&amp;
        getComputedStyle(tooltip).visibility === 'visible';
      if (isVisible) { forceHide(); } else { dismissAllTooltips(); resetVisibility(); }
    }, { passive: true });
  });

  function dismissAllTooltips() {
    document.querySelectorAll('.tooltip-content').forEach(t =&gt; t.classList.add('js-hidden'));
  }

  document.addEventListener('click', (e) =&gt; {
    if (!e.target.closest('.tooltip-container')) {
      document.querySelectorAll('.tooltip-content').forEach(t =&gt; t.classList.remove('js-hidden'));
    }
  });
});
</code></pre>
<p>The problem isn't that the above code is wrong – not at all, it works. The problem is what it reveals: because the CSS <code>:hover</code> and <code>:focus-within</code> selectors can't handle Escape-to-dismiss, touch toggle, or click-outside detection, the agent has to build a parallel JavaScript system to manage tooltip state. Visibility is now split across two systems that have to stay in sync. A <code>js-hidden</code> class exists specifically to let JavaScript override CSS.</p>
<p>You can move ahead to <a href="#heading-after-tooltip-component">see the updated Tooltip component code after Modern Web Guidance was installed</a> if you're curious right now.</p>
<p>Next, let's look at how the agent builds a toast notification without Modern Web Guidance.</p>
<h3 id="heading-before-toast-notification-with-exit-animation">Before: Toast Notification with Exit Animation</h3>
<p><strong>Prompt:</strong> "Build a toast notification system where notifications fade out before being removed."</p>
<pre><code class="language-javascript">// ❌ Before MWG — JavaScript owns the entire animation lifecycle
const dismissToast = (toast) =&gt; {
  if (toast.classList.contains('toast-fade-out')) return;

  // 1. Apply fade-out class to trigger CSS transition
  toast.classList.add('toast-fade-out');

  // 2. Wait for transition, then remove from DOM
  const handleUnmount = (e) =&gt; {
    if (e.propertyName === 'opacity' || e.propertyName === 'transform') {
      toast.removeEventListener('transitionend', handleUnmount);
      toast.remove();
    }
  };
  toast.addEventListener('transitionend', handleUnmount);

  // 3. Fallback in case transitionend doesn't fire
  setTimeout(() =&gt; {
    if (toast.parentNode) toast.remove();
  }, 400);
};

// Auto-dismiss after 4 seconds
autoDismissTimer = setTimeout(() =&gt; {
  dismissToast(toast);
}, 4000);
</code></pre>
<p>Reviewing the code above: this pattern is extremely common, and again it does work. But notice how much JavaScript is dedicated to a problem that's fundamentally about animation timing.</p>
<p>The agent adds a CSS class to start a transition, then uses <code>transitionend</code> to know when to remove the element, then adds a <code>setTimeout</code> fallback in case <code>transitionend</code> doesn't fire, then another <code>setTimeout</code> for auto-dismissal.</p>
<p>The JavaScript and CSS are deeply entangled. Change the transition duration in CSS and you have to update the JavaScript timeout to match.</p>
<p>You can move ahead to <a href="#heading-after-toast-notification-with-exit-animation">see the updated Toast notification code after Modern Web Guidance was installed</a> if you're curious now.</p>
<p>Both examples share the same shape: the agent writes JavaScript to compensate for what it doesn't know the browser can handle natively.</p>
<h2 id="heading-what-is-modern-web-guidance-mwg">What Is Modern Web Guidance (MWG)?</h2>
<p><a href="https://developer.chrome.com/docs/modern-web-guidance">Modern Web Guidance</a> is an open-source project backed by the Google Chrome team and the Microsoft Edge team. Instead of hoping the model knows what the modern platform offers, you give it a structured, expert-vetted reference file that maps common development scenarios to the right solutions.</p>
<p>It ships as an <strong>agent skill</strong>, a <code>SKILL.md</code> file that lives in your project and gets read by your coding agent before it generates code. Think of it as a project-specific instruction manual that teaches the agent which modern APIs exist and when to use them. The skill shifts the probability distribution toward modern platform solutions in a way that a one-line prompt instruction can't.</p>
<p>Under the hood, the mechanism works in three steps:</p>
<ol>
<li><p>Your agent activates the skill because the task is web-related.</p>
</li>
<li><p>The agent runs <code>modern-web-guidance search "&lt;query&gt;"</code>, a local semantic search using an offline TensorFlow.js model. No API key, and no network call.</p>
</li>
<li><p>The agent retrieves the matched guide via <code>modern-web-guidance retrieve &lt;guide-id&gt;</code>, injecting targeted patterns, gotchas, and fallback strategies directly into its context window.</p>
</li>
</ol>
<p>Two skill packs are available. <code>modern-web-guidance</code> covers modern browser APIs, CSS layout systems, performance, accessibility, and built-in AI APIs. This is what most developers want.</p>
<p><code>chrome-extensions</code> covers Manifest V3, background workers, and Chrome Web Store publishing. <a href="https://developer.chrome.com/docs/modern-web-guidance/get-started#how_is_accuracy_ensured">Early evals show a 37 percentage point improvement</a> in adherence to modern best practices when agents run with it installed.</p>
<h2 id="heading-how-to-install-modern-web-guidance">How to Install Modern Web Guidance</h2>
<p>The universal path (works with any agent):</p>
<pre><code class="language-shell">npx modern-web-guidance@latest install
</code></pre>
<p>This runs an interactive wizard that detects your coding agent, asks which skill packs you want, and drops the <code>SKILL.md</code> file in the correct location automatically. The CLI is fully offline and self-contained: no external dependencies and no API keys.</p>
<p>Claude Code:</p>
<pre><code class="language-shell">#1. Add the marketplace /plugin marketplace add GoogleChrome/modern-web-guidance

#2. Install the plugin
/plugin install modern-web-guidance@googlechrome

#3. Reload plugins
/reload-plugins
</code></pre>
<p>After installation, verify that .claude/skills/ exists in your project root and contains the skill file. That's where Claude Code reads skills from.</p>
<p>Cursor:</p>
<p>Modern Web Guidance is listed in the Skill Marketplace.</p>
<p><code>Search for modern-web-guidance and click Install, no CLI step required.</code></p>
<p>GitHub Copilot CLI:</p>
<pre><code class="language-shell"># 1. Add the marketplace /plugin marketplace add GoogleChrome/modern-web-guidance

# 2. Install the plugin
/plugin install modern-web-guidance@googlechrome
</code></pre>
<p>Vercel Agent Skills:</p>
<pre><code class="language-shell">npx skills add GoogleChrome/modern-web-guidance
</code></pre>
<p>Google Antigravity:</p>
<p>One-click install available directly inside the app.</p>
<h2 id="heading-after-installing-modern-web-guidance-what-actually-changes">After Installing Modern Web Guidance: What Actually Changes</h2>
<p><a href="#heading-why-do-ai-agents-default-to-legacy-patterns">Earlier</a>, we saw the outputs for the prompts on both the Tooltip and Toast Notification components when Modern Web Guidance was not installed. Run the same prompts with Modern Web Guidance installed and the agent reaches for entirely different tools.</p>
<h3 id="heading-after-tooltip-component">After: Tooltip Component</h3>
<p>With Modern Web Guidance, the same tooltip prompt produces no JavaScript at all. Instead, the agent reaches for two APIs working together: <code>popover="hint"</code> for native hover/focus-triggered visibility, and <code>interestfor</code> (the Interest Invokers API) to wire the trigger to its target declaratively in HTML.</p>
<pre><code class="language-html">&lt;!-- ✅ After MWG — declarative HTML, zero JavaScript --&gt;
&lt;div class="tooltip-wrapper"&gt;
  &lt;button
    id="btn-deploy"
    class="btn-trigger"
    interestfor="tooltip-deploy"
  &gt;
    Deploy App
  &lt;/button&gt;
  &lt;div popover="hint" id="tooltip-deploy" class="tooltip-content"&gt;
    Instantly push code changes live
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<pre><code class="language-css">/* Anchor positioning wires layout to the trigger */
#btn-deploy {
  anchor-name: --tooltip-deploy;
}

#tooltip-deploy {
  position-anchor: --tooltip-deploy;
}

.tooltip-content[popover] {
  position: absolute;
  bottom: anchor(top);
  left: anchor(center);
  transform: translateX(-50%) translateY(8px);

  opacity: 0;
  transition: opacity 0.2s ease,
              display 0.2s allow-discrete,
              overlay 0.2s allow-discrete;
}

.tooltip-content[popover]:popover-open {
  opacity: 1;
  transform: translateX(-50%) translateY(-12px);
}

@starting-style {
  .tooltip-content[popover]:popover-open {
    opacity: 0;
    transform: translateX(-50%) translateY(8px);
  }
}
</code></pre>
<p>The <code>js-hidden</code> class is gone. The <code>dismissAllTooltips()</code> function is gone. The <code>touchstart</code> handler is gone. The click-outside detection is gone.</p>
<p><code>popover="hint"</code> provides light-dismiss behavior natively, the browser handles hover intent, focus management, Escape-to-dismiss, and touch semantics without a line of JavaScript. <code>@starting-style</code> defines the entry animation state, and <code>allow-discrete</code> handles the exit, so both directions of the transition are owned entirely by CSS.</p>
<p><strong>Browser compatibility note:</strong> The Interest Invokers API (<code>interestfor</code>) is currently available in Chrome with a flag and has a polyfill at <code>unpkg.com/interestfor</code>. CSS Anchor Positioning is Baseline 2025. The agent also included polyfill loading in the output. Check <a href="https://caniuse.com/css-anchor-positioning">caniuse.com/css-anchor-positioning</a> and assess against your browser support requirements before shipping.</p>
<p>One thing worth knowing: of the two APIs here, CSS Anchor Positioning is already shipping in stable browsers, while <code>interestfor</code> is the more experimental one. The polyfill covers it, but think of it as a preview of where the platform is heading rather than something you would ship to production today without testing.</p>
<h3 id="heading-after-toast-notification-with-exit-animation">After: Toast Notification with Exit Animation</h3>
<p>The same toast prompt with Modern Web Guidance produces a <code>popover="manual"</code> element instead of a class-toggled <code>&lt;div&gt;</code>. The browser's Top Layer handles rendering and stacking context natively.</p>
<pre><code class="language-javascript">// ✅ After MWG — the browser handles show/hide; JS handles auto-dismiss timing only
const createToast = (type) =&gt; {
  const toast = document.createElement('div');
  toast.setAttribute('popover', 'manual');
  toast.className = `toast toast-${type}`;

  toast.innerHTML = `
    &lt;div class="toast-icon"&gt;...&lt;/div&gt;
    &lt;div class="toast-content"&gt;...&lt;/div&gt;
    &lt;button
      popovertarget="${toastId}"
      popovertargetaction="hide"
      class="toast-close"
      aria-label="Dismiss notification"
    &gt;&amp;times;&lt;/button&gt;
  `;

  container.appendChild(toast);
  toast.showPopover(); // triggers @starting-style entry animation natively

  // Auto-dismiss
  const autoDismissTimer = setTimeout(() =&gt; {
    if (toast.matches(':popover-open')) toast.hidePopover();
  }, 4000);

  // Remove from DOM after exit transition completes
  toast.addEventListener('beforetoggle', (event) =&gt; {
    if (event.newState === 'closed') {
      clearTimeout(autoDismissTimer);
      toast.addEventListener('transitionend', () =&gt; toast.remove(), { once: true });
      setTimeout(() =&gt; { if (toast.parentNode) toast.remove(); }, 500); // fallback
    }
  });
};
</code></pre>
<pre><code class="language-css">/* ✅ CSS owns both entry and exit animation */
.toast[popover] {
  opacity: 0;
  transform: translateX(60px) scale(0.95);
  transition: opacity 0.3s ease,
              transform 0.3s ease,
              display 0.3s allow-discrete,
              overlay 0.3s allow-discrete;
}

.toast[popover]:popover-open {
  opacity: 1;
  transform: translateX(0) scale(1);
}

@starting-style {
  .toast[popover]:popover-open {
    opacity: 0;
    transform: translateX(60px) scale(0.95);
  }
}
</code></pre>
<p>The manual close button now uses <code>popovertarget</code> and <code>popovertargetaction="hide"</code>, a declarative HTML binding that requires no click handler. <code>showPopover()</code> triggers the <code>@starting-style</code> entry animation natively. <code>hidePopover()</code> triggers the CSS exit transition via <code>allow-discrete</code>.</p>
<p>JavaScript is now responsible for only two things: scheduling the auto-dismiss timeout and removing the element from the DOM after the exit transition completes. The animation coordination that previously required <code>transitionend</code> listeners, CSS class toggling, and synchronized timing is gone, as the browser owns it.</p>
<h2 id="heading-what-modern-web-guidance-does-not-handle-for-you">What Modern Web Guidance Does Not Handle for You</h2>
<p>Modern Web Guidance shifts what the agent writes on a first attempt. It doesn't eliminate the need for code review, and in practice two friction points come up consistently.</p>
<h3 id="heading-1-the-bleeding-edge-cliff">1. The Bleeding-edge Cliff</h3>
<p>Modern Web Guidance defaults to the newest Baseline features. <code>@starting-style</code>, <code>transition-behavior: allow-discrete</code>, CSS Anchor Positioning, and the Interest Invokers API are all correct, but some are new enough that they require polyfills for production use today. The agent will include those polyfill imports in its output.</p>
<p>You still need to verify the features used against your actual browser support requirements. A junior developer reading <code>interestfor</code> or <code>position-anchor</code> for the first time will need to look these up, because Modern Web Guidance assumes you want the most modern correct answer, not the most familiar one.</p>
<h3 id="heading-2-the-css-encapsulation-trade-off">2. The CSS Encapsulation Trade-off</h3>
<p>When Modern Web Guidance guides the agent toward moving inline styles or <code>dangerouslySetInnerHTML</code> keyframes into a global stylesheet, which it does for security and hydration reasons, it breaks component-level encapsulation. Delete the component later and you'll have orphaned CSS in your global file. The call is architecturally correct, but you still need to namespace those classes and track the dependency manually.</p>
<p>The 37-point improvement in best-practice adherence is real, but Modern Web Guidance is better understood as raising the default ceiling and not removing the need for human judgment. Think of it as giving your agent the habits of a developer who stays updated by actually reading current web docs.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The problem was never that AI coding agents were bad at web development. The problem is that they were working from an outdated picture of the platform, one shaped by training data that reflects the early 2020s web more than the browser capabilities available today.</p>
<p>Modern Web Guidance updates that picture. The tooltip before/after alone tells the whole story: the agent went from a <code>js-hidden</code> state machine with touch handlers and click-outside detection to two HTML attributes and a block of CSS. The JavaScript interaction layer didn't get refactored, it became unnecessary.</p>
<p>The code your agent writes is only as current as what it was trained on. Modern Web Guidance closes that gap.</p>
<p>I ran this exact experiment on my own project. You can read the full case study with raw diffs at <a href="https://www.ophyboamah.com/blog/i-installed-modern-web-guidance-in-my-projects-heres-what-actually-changed">ophyboamah.com/blog</a>.</p>
<p>Here are some helpful resources:</p>
<ul>
<li><p><a href="https://developer.chrome.com/docs/modern-web-guidance">Modern Web Guidance</a></p>
</li>
<li><p><a href="https://www.youtube.com/watch?v=bo3i0FzDUYo">Modern Web Guidance video - Chrome for Developers</a></p>
</li>
<li><p><a href="https://github.com/GoogleChrome/modern-web-guidance">Modern Web Guidance open-source</a> (open to contributions)</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Browser-Based PDF Reverse Tool Using JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ PDF files are often created by combining scans, exporting documents from different systems, or processing large batches of pages. In many cases, the final PDF ends up with pages arranged in the wrong  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-browser-based-pdf-reverse-tool-javascript/</link>
                <guid isPermaLink="false">6a3c3ffeeed203a44f97b779</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Online PDF Tools ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Frontend Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ browser tools ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Bhavin Sheth ]]>
                </dc:creator>
                <pubDate>Wed, 24 Jun 2026 20:37:18 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/49e3966a-f387-409f-b181-ac8feffcab13.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>PDF files are often created by combining scans, exporting documents from different systems, or processing large batches of pages. In many cases, the final PDF ends up with pages arranged in the wrong order.</p>
<p>A PDF Reverse Tool solves this problem by flipping the page sequence automatically. Instead of manually rearranging pages one by one, users can reverse an entire document in seconds.</p>
<p>In this tutorial, you'll learn how to build a browser-based PDF Reverse Tool using JavaScript and PDF-lib. The tool allows users to upload PDFs, preview pages, choose different reverse modes, generate a reversed document, and download the updated PDF directly from the browser.</p>
<p>You can try the live tool here:</p>
<p><strong>Reverse PDF Tool:</strong> <a href="https://allinonetools.net/reverse-pdf/">https://allinonetools.net/reverse-pdf/</a></p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f98498f9-4ec8-459e-83dc-705cde7557e7.png" alt="allinonetools pdf tools pdf reverse tool" style="display:block;margin:0 auto" width="617" height="257" loading="lazy">

<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-why-reversing-pdf-pages-is-useful">Why Reversing PDF Pages Is Useful</a></p>
</li>
<li><p><a href="#heading-how-pdf-page-reversal-works">How PDF Page Reversal Works</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-what-library-are-we-using">What Library Are We Using?</a></p>
</li>
<li><p><a href="#heading-creating-the-upload-interface">Creating the Upload Interface</a></p>
</li>
<li><p><a href="#heading-previewing-uploaded-pdf-pages">Previewing Uploaded PDF Pages</a></p>
</li>
<li><p><a href="#heading-configuring-reverse-options">Configuring Reverse Options</a></p>
</li>
<li><p><a href="#heading-applying-the-reverse-operation">Applying the Reverse Operation</a></p>
</li>
<li><p><a href="#heading-generating-the-reversed-pdf">Generating the Reversed PDF</a></p>
</li>
<li><p><a href="#heading-why-pdf-reversal-is-useful-in-real-world-documents">Why PDF Reversal Is Useful in Real-World Documents</a></p>
</li>
<li><p><a href="#heading-demo-how-the-reverse-pdf-tool-works">Demo: How the Reverse PDF Tool Works</a></p>
</li>
<li><p><a href="#heading-important-notes-from-real-world-use">Important Notes from Real-World Use</a></p>
</li>
<li><p><a href="#heading-common-mistakes-to-avoid">Common Mistakes to Avoid</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-why-reversing-pdf-pages-is-useful">Why Reversing PDF Pages Is Useful</h2>
<p>PDF page reversal changes the order of pages inside a document.</p>
<p>For example, a 10-page PDF normally follows this sequence: Page 1 → Page 2 → Page 3 → Page 4, and so on.</p>
<p>After reversal, the order becomes Page 10 → Page 9 → Page 8 → Page 7, and on down.</p>
<p>This process is useful when scanned documents are imported in the wrong sequence, when merged files need to be reordered, or when printing workflows require reverse page order.</p>
<p>Instead of rearranging pages manually, users can reverse the entire document instantly.</p>
<h2 id="heading-how-pdf-page-reversal-works">How PDF Page Reversal Works</h2>
<p>A PDF Reverse Tool reads the uploaded PDF file, extracts its pages, rearranges the page order according to the selected reverse mode, and creates a new downloadable PDF.</p>
<p>The browser loads the document, processes page indexes, copies pages into a new PDF document, and exports the updated file.</p>
<p>Everything happens directly inside the browser. No files are uploaded to external servers, helping maintain privacy and improving processing speed.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Create a simple project structure:</p>
<pre><code class="language-text">pdf-reverse-tool/
│
├── index.html
├── style.css
├── app.js
│
└── libs/
    └── pdf-lib.min.js
</code></pre>
<p>Load PDF-lib:</p>
<pre><code class="language-html">&lt;script src="libs/pdf-lib.min.js"&gt;&lt;/script&gt;
&lt;script src="app.js"&gt;&lt;/script&gt;
</code></pre>
<h2 id="heading-what-library-are-we-using">What Library Are We Using?</h2>
<p>This project uses PDF-lib.</p>
<p>PDF-lib is a powerful JavaScript library that allows developers to create, modify, merge, split, organize, and export PDF documents directly inside the browser.</p>
<p>For a page reversal tool, PDF-lib provides everything needed to read page indexes, copy pages, rearrange document structure, and generate updated PDFs.</p>
<p>Example:</p>
<pre><code class="language-javascript">const pdfDoc = await PDFLib.PDFDocument.load(pdfBytes);

const totalPages = pdfDoc.getPageCount();

console.log(totalPages);
</code></pre>
<h2 id="heading-creating-the-upload-interface">Creating the Upload Interface</h2>
<p>The first step is allowing users to upload a PDF document.</p>
<p>A drag-and-drop upload area provides a simple and user-friendly experience while supporting traditional file selection.</p>
<p>Example HTML:</p>
<pre><code class="language-html">&lt;input type="file" id="pdfFile" accept=".pdf" /&gt;
</code></pre>
<p>Example JavaScript:</p>
<pre><code class="language-javascript">document
  .getElementById("pdfFile")
  .addEventListener("change", loadPDF);
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/88f77a3a-0ed0-4d19-8359-9b5952b1b3ea.png" alt="Reverse PDF upload area with drag-and-drop PDF uploader and file selection button" style="display:block;margin:0 auto" width="618" height="689" loading="lazy">

<h2 id="heading-previewing-uploaded-pdf-pages">Previewing Uploaded PDF Pages</h2>
<p>After a PDF is uploaded, users should be able to preview document pages before performing any operations.</p>
<p>Page previews help users verify that the correct file was selected and make it easier to understand how the reversal process will affect the document.</p>
<p>The preview section displays page thumbnails and allows users to navigate between pages before processing.</p>
<p>Example:</p>
<pre><code class="language-javascript">const totalPages = pdfDoc.getPageCount();

for(let i = 0; i &lt; totalPages; i++) {
   console.log(`Rendering page ${i + 1}`);
}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/05490d45-cf3b-4acb-9fae-1385e8239d4b.png" alt="PDF page preview showing uploaded document pages with page navigation controls" style="display:block;margin:0 auto" width="1315" height="863" loading="lazy">

<h2 id="heading-configuring-reverse-options">Configuring Reverse Options</h2>
<p>The Reverse PDF Tool supports multiple reversal modes.</p>
<p>Users can reverse an entire PDF document or reverse only a specific range of pages.</p>
<p>For example, a user may want to reverse pages 10 through 20 while leaving the rest of the document unchanged.</p>
<p>The tool also includes additional document-editing features such as rotating pages, adding blank pages, and importing another PDF before generating the final output.</p>
<p>This flexibility makes the tool useful for both simple and advanced document workflows.</p>
<p>Example configuration:</p>
<pre><code class="language-javascript">const reverseMode = "full";

const startPage = 5;
const endPage = 15;
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/87400d29-7ef5-44e6-a7ac-9dbb54c13a91.png" alt="Reverse PDF settings panel showing reverse mode selection page range controls and PDF editing options" style="display:block;margin:0 auto" width="482" height="296" loading="lazy">

<h2 id="heading-applying-the-reverse-operation">Applying the Reverse Operation</h2>
<p>Once the user selects the desired reverse mode, the tool generates a new page order.</p>
<p>For a full document reversal, the last page becomes the first page and the first page becomes the last page.</p>
<p>Example:</p>
<pre><code class="language-javascript">const reversedIndices = [];

for(let i = totalPages - 1; i &gt;= 0; i--) {
    reversedIndices.push(i);
}
</code></pre>
<p>The generated page order is then used when creating the final PDF.</p>
<h2 id="heading-generating-the-reversed-pdf">Generating the Reversed PDF</h2>
<p>PDF-lib allows pages to be copied into a new PDF document in any order.</p>
<p>The reversal process creates a new PDF and inserts pages according to the generated sequence.</p>
<p>Example:</p>
<pre><code class="language-javascript">const reversedIndices = [];

for (let i = totalPages - 1; i &gt;= 0; i--) {
  reversedIndices.push(i);
}

const copiedPages = await pdfDoc.copyPages(
  sourcePdf,
  reversedIndices
);

copiedPages.forEach(page =&gt; {
  pdfDoc.addPage(page);
});
</code></pre>
<p>Once processing is complete, the updated PDF is exported directly inside the browser.</p>
<h2 id="heading-why-pdf-reversal-is-useful-in-real-world-documents">Why PDF Reversal Is Useful in Real-World Documents</h2>
<p>Many documents are accidentally created in reverse order during scanning, merging, exporting, or printing workflows.</p>
<p>A common example occurs when users scan large stacks of paper using automatic document feeders. Depending on how pages are loaded into the scanner, the resulting PDF may place the final page first and the first page last.</p>
<p>Educational institutions frequently encounter this issue when scanning answer sheets, student records, assignments, admission documents, and examination papers.</p>
<p>Businesses often receive contracts, invoices, purchase orders, reports, and legal documents that arrive in reverse sequence after scanning. A PDF reversal tool restores the intended reading order instantly.</p>
<p>The feature is particularly useful for e-commerce businesses.</p>
<p>For example, a seller may receive hundreds of shipping labels, invoices, packing slips, or courier documents from marketplaces such as Flipkart, Amazon, Meesho, or other platforms. Sometimes these documents are generated in reverse order compared to the packing workflow.</p>
<p>Instead of manually rearranging pages, the seller can reverse the entire PDF and immediately print documents in the correct sequence. This saves significant time when processing large batches of orders.</p>
<p>Accounting teams, warehouse staff, administrative departments, legal offices, publishers, and document management teams regularly use page reversal to streamline document preparation.</p>
<p>The result is a cleaner workflow, reduced manual effort, and a document that is easier to read, print, archive, and distribute.</p>
<h2 id="heading-demo-how-the-reverse-pdf-tool-works">Demo: How the Reverse PDF Tool Works</h2>
<h3 id="heading-step-1-upload-your-pdf-file">Step 1: Upload Your PDF File</h3>
<p>Users start by uploading a PDF document using either the drag-and-drop area or the file selection button.</p>
<p>Once the file is selected, the browser reads the PDF locally and prepares it for processing. No files are uploaded to external servers, helping maintain privacy and security.</p>
<p>The tool automatically loads the document structure and extracts page information required for preview generation and page reversal.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f69a57a1-2b8e-4273-945c-6ff0fd3aff40.png" alt="PDF upload interface for selecting a PDF file to reverse" style="display:block;margin:0 auto" width="698" height="701" loading="lazy">

<h3 id="heading-step-2-preview-uploaded-pages">Step 2: Preview Uploaded Pages</h3>
<p>After the PDF is loaded, the tool generates page previews directly inside the browser.</p>
<p>Users can browse through the document pages before making any changes. This helps verify that the correct file has been uploaded and allows users to understand the current page sequence.</p>
<p>The preview section also provides page navigation controls so users can move between pages and inspect the document before processing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/526f08cf-7afa-497e-a584-a1d71ba04bab.png" alt="Uploaded PDF page preview with page navigation controls" style="display:block;margin:0 auto" width="1301" height="887" loading="lazy">

<h3 id="heading-step-3-select-reverse-mode">Step 3: Select Reverse Mode</h3>
<p>Next, users choose how the page reversal should be applied.</p>
<p>The tool supports two reversal modes.</p>
<p>The first option reverses the entire document, changing the page order from first-to-last into last-to-first.</p>
<p>The second option allows users to specify a page range and reverse only that section while keeping the rest of the document unchanged.</p>
<p>Additional document editing options such as rotating pages, adding blank pages, importing another PDF, and resetting the document are also available before processing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/159d25f3-d001-4e94-a6d7-7e5be287e3b1.png" alt="Reverse mode settings showing full reverse and custom range options" style="display:block;margin:0 auto" width="564" height="232" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/6291e9fc-e9dd-4062-afba-72b227dc820e.png" alt="Reverse mode settings showing full reverse and custom range options" style="display:block;margin:0 auto" width="376" height="137" loading="lazy">

<h3 id="heading-step-4-review-pages-before-processing">Step 4: Review Pages Before Processing</h3>
<p>Before generating the final PDF, users can review all page thumbnails and verify the selected reversal settings.</p>
<p>This step is especially useful when working with large reports, scanned documents, contracts, books, manuals, invoices, and merged PDFs where page order is important.</p>
<p>Taking a few moments to verify the document can prevent mistakes and reduce the need for reprocessing later.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/a86e26b0-727f-466c-bc2d-0731b0e99ab4.png" alt="Alt Text: PDF page preview before applying page reversal" style="display:block;margin:0 auto" width="652" height="859" loading="lazy">

<h3 id="heading-step-5-reverse-the-document">Step 5: Reverse the Document</h3>
<p>After confirming the settings, users click the <strong>Reverse PDF</strong> button.</p>
<p>The browser processes the selected pages and generates a new page sequence based on the chosen reversal mode.</p>
<p>Since everything happens locally inside the browser, processing is usually very fast and no document data leaves the user's device.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/6ff36eba-7ab2-4cfe-a8e4-2b8950e2b248.png" alt="Reverse PDF button used to generate reversed page order" style="display:block;margin:0 auto" width="518" height="119" loading="lazy">

<h3 id="heading-step-6-preview-the-reversed-pdf">Step 6: Preview the Reversed PDF</h3>
<p>Once processing is complete, the tool displays the newly generated PDF.</p>
<p>Users can browse through the updated document using the page navigation controls and verify that the page order has been reversed correctly.</p>
<p>This preview stage provides a final opportunity to inspect the output before downloading.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/4bf42481-fd7c-4677-ba5c-f8eab5ad8181.png" alt="Alt Text: Preview of reversed PDF document with page navigation controls" style="display:block;margin:0 auto" width="524" height="585" loading="lazy">

<h3 id="heading-step-7-download-the-final-pdf">Step 7: Download the Final PDF</h3>
<p>After confirming the results, users can download the updated document.</p>
<p>The final output section displays useful file information including the generated filename, total number of pages, and file size as well as file rename option before download.</p>
<p>This information helps users quickly verify that the output matches expectations before saving the file.</p>
<p>The document can then be downloaded and used immediately for printing, sharing, archiving, business workflows, educational records, legal documents, or other PDF-related tasks.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/45ece9e1-7e7e-4848-9165-03a708085a8c.png" alt="Final reversed PDF ready for download showing filename file size page count and download button" style="display:block;margin:0 auto" width="530" height="684" loading="lazy">

<h2 id="heading-important-notes-from-real-world-use">Important Notes from Real-World Use</h2>
<p>Large PDF files may require additional processing time, especially when reversing documents containing hundreds or thousands of pages.</p>
<p>When processing large PDFs, it's a good practice to validate the uploaded file before loading it into memory.</p>
<p>Example:</p>
<pre><code class="language-javascript">if (file.size &gt; 50 * 1024 * 1024) {
    alert("Large PDF detected. Processing may take longer.");
}
</code></pre>
<p>When working with very large documents, developers should avoid unnecessary page rendering operations to reduce memory usage and improve performance.</p>
<p>It's also recommended to verify the page count before starting the reversal process.</p>
<p>Example:</p>
<pre><code class="language-javascript">const totalPages = pdfDoc.getPageCount();

console.log(`Pages: ${totalPages}`);
</code></pre>
<p>Previewing the final output before download helps users catch mistakes early and confirm that the page order has been reversed correctly.</p>
<p>Since processing happens entirely inside the browser, documents never leave the user's device, providing better privacy and security for sensitive files.</p>
<p>This approach is especially useful when working with business reports, legal documents, invoices, contracts, educational records, and confidential PDFs that shouldn't be uploaded to third-party servers.</p>
<h2 id="heading-common-mistakes-to-avoid">Common Mistakes to Avoid</h2>
<p>One common mistake is reversing a document without first checking the existing page order.</p>
<p>Many users assume the pages are arranged incorrectly and reverse the entire document, only to discover that the original file was already in the correct sequence.</p>
<p>Before processing, it's a good idea to verify the first and last pages.</p>
<p>Example:</p>
<pre><code class="language-javascript">const totalPages = pdfDoc.getPageCount();

console.log(`First Page: 1`);
console.log(`Last Page: ${totalPages}`);
</code></pre>
<p>Another common mistake is reversing an entire PDF when only a specific section needs to be reordered.</p>
<p>Large reports, books, manuals, and scanned documents sometimes require only a subset of pages to be reversed.</p>
<p>Always confirm whether a full-document reversal or a custom range reversal is required.</p>
<p>Example:</p>
<pre><code class="language-javascript">const startPage = 10;
const endPage = 25;

console.log(`Reverse pages \({startPage} to \){endPage}`);
</code></pre>
<p>Users also frequently assume scanned pages are already arranged correctly. But automatic document feeders and batch scanners can sometimes create PDFs with pages in unexpected sequences.</p>
<p>Previewing uploaded pages before processing helps identify these issues early.</p>
<p>Another mistake is skipping the final preview after generating the reversed PDF. A quick review allows users to confirm that page order, page count, and document structure are correct before downloading.</p>
<p>Example:</p>
<pre><code class="language-javascript">const finalPages = reversedPdf.getPageCount();

console.log(`Output Pages: ${finalPages}`);
</code></pre>
<p>Taking a few seconds to verify the output can prevent unnecessary reprocessing, save time, and ensure the final PDF is ready for sharing, printing, archiving, or business use.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a browser-based PDF Reverse Tool using JavaScript.</p>
<p>You learned how to upload PDF files, preview document pages, configure reverse modes, generate reversed page orders, and create downloadable PDF documents directly inside the browser.</p>
<p>More importantly, you saw how modern browsers can perform document organization tasks locally without relying on backend servers.</p>
<p>This approach keeps document processing fast, private, and easy to use.</p>
<p>You can try the live implementation here:</p>
<p><a href="https://allinonetools.net/reverse-pdf/">Reverse PDF Tool</a></p>
<p>Once you understand this workflow, you can extend it further with features such as PDF splitting, merging, page rotation, page numbering, metadata editing, watermarking, document encryption, and advanced PDF organization tools.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build an Animated Badge Component with shadcn/ui ]]>
                </title>
                <description>
                    <![CDATA[ Badges are everywhere in modern web apps. You see them on notification counters, status labels, and feature tags. Most of them are static, though. They sit there doing nothing, blending into the page. ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-an-animated-badge-component-with-shadcn-ui/</link>
                <guid isPermaLink="false">6a3c0d40b101451dd3ba52e8</guid>
                
                    <category>
                        <![CDATA[ shadcn ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Vaibhav Gupta ]]>
                </dc:creator>
                <pubDate>Wed, 24 Jun 2026 17:00:48 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/90ffff22-4ea2-47c2-8b8c-011e8e566301.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Badges are everywhere in modern web apps. You see them on notification counters, status labels, and feature tags.</p>
<p>Most of them are static, though. They sit there doing nothing, blending into the page. But a well-animated badge can tell the user something happened without them having to read a single word.</p>
<p>In this tutorial, you'll build an animated “success” badge using shadcn/ui, Tailwind CSS, and Framer Motion. The badge will have a glowing top light, an animated check icon that bounces into view, and letters that drop in one at a time with a stagger effect.</p>
<p>The component comes from the <a href="https://shadcnspace.com/components/badge"><strong>Shadcn Space badge collection</strong></a> and uses the Base UI primitive version of Badge. You'll install it with a single CLI command, then walk through every piece of code.</p>
<p>By the end, you'll build an animated "Success" badge by:</p>
<ol>
<li><p>Installing the <code>badge-07</code> component from Shadcn Space using the Shadcn CLI</p>
</li>
<li><p>Using <code>motion.create()</code> to wrap the shadcn/ui <code>Badge</code> into an animatable component</p>
</li>
<li><p>Adding layered radial-gradient glow effects as absolutely positioned spans</p>
</li>
<li><p>Animating the check icon with a scale and rotate entrance</p>
</li>
<li><p>Animating each letter of the label individually using staggered <code>variants</code></p>
</li>
</ol>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-youll-build">What You'll Build</a></p>
</li>
<li><p><a href="#heading-how-to-install-the-component">How to Install the Component</a></p>
</li>
<li><p><a href="#heading-component-structure">Component Structure</a></p>
</li>
<li><p><a href="#heading-step-1-set-up-the-imports">Step 1: Set Up the Imports</a></p>
</li>
<li><p><a href="#heading-step-2-define-letter-animation-variants">Step 2: Define Letter Animation Variants</a></p>
</li>
<li><p><a href="#heading-step-3-wrap-the-badge-with-motion">Step 3: Wrap the Badge with Motion</a></p>
</li>
<li><p><a href="#heading-step-4-build-the-glow-layers">Step 4: Build the Glow Layers</a></p>
</li>
<li><p><a href="#heading-step-5-animate-the-icon">Step 5: Animate the Icon</a></p>
</li>
<li><p><a href="#heading-step-6-animate-each-letter">Step 6: Animate Each Letter</a></p>
</li>
<li><p><a href="#heading-how-to-use-it-in-your-app">How to Use It in Your App</a></p>
</li>
<li><p><a href="#heading-how-to-customize-the-component">How to Customize the Component</a></p>
</li>
<li><p><a href="#heading-live-preview">Live Preview</a></p>
</li>
<li><p><a href="#heading-key-concepts-recap">Key Concepts Recap</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-resources">Resources</a></p>
</li>
</ul>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>You'll need:</p>
<ul>
<li><p>A Next.js project with shadcn/ui initialized</p>
</li>
<li><p>Tailwind CSS set up</p>
</li>
<li><p><code>motion</code> installed: <code>npm install motion</code></p>
</li>
<li><p><code>lucide-react</code> installed: <code>npm install lucide-react</code></p>
</li>
<li><p>Basic TypeScript and React knowledge</p>
</li>
</ul>
<h2 id="heading-what-youll-build"><strong>What You'll Build</strong></h2>
<p>In this tutorial, we'll build a self-contained animated badge with three moving parts:</p>
<pre><code class="language-plaintext">├── MotionBadge (outline, rounded-full, teal border)
│   ├── Glow layers  → 3 radial gradient spans above the top border
│   ├── CheckCircle  → scale + rotate entrance, easeOutBack
│   └── Letter spans → staggered drop-in, easeOutCubic
</code></pre>
<p>After installation, the component file lands here:</p>
<pre><code class="language-plaintext">components/
└── shadcn-space/
    └── badge/
        └── badge-07.tsx
</code></pre>
<h2 id="heading-how-to-install-the-component"><strong>How to Install the Component</strong></h2>
<p><a href="https://shadcnspace.com/"><strong>Shadcn UI</strong></a> provides a registry of production-ready components. You pull them into your project with the Shadcn CLI, just like you'd add any standard shadcn/ui component.</p>
<p>Before running any command, check the <a href="https://shadcnspace.com/docs/getting-started/how-to-use-shadcn-cli"><strong>Getting Started guide</strong></a> or the <a href="https://shadcnspace.com/cli"><strong>CLI page</strong></a> for setup details.</p>
<p>You can also follow along with this video walkthrough:</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/n6dvjVxy02U" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>

<p>Run the command for your package manager:</p>
<p><strong>pnpm</strong></p>
<pre><code class="language-javascript">pnpm dlx shadcn@latest add @shadcn-space/badge-07
</code></pre>
<p><strong>npm</strong></p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/badge-07
</code></pre>
<p><strong>Yarn</strong></p>
<pre><code class="language-javascript">yarn dlx shadcn@latest add @shadcn-space/badge-07
</code></pre>
<p><strong>Bun</strong></p>
<pre><code class="language-javascript">bunx --bun shadcn@latest add @shadcn-space/badge-07
</code></pre>
<p><strong>Note:</strong> <code>badge-07</code> uses the <strong>Base UI</strong> primitive version of Badge. Both Radix and Base UI versions are available in the registry. This tutorial covers the Base UI version.</p>
<h2 id="heading-component-structure"><strong>Component Structure</strong></h2>
<p>Here's the complete component. Read through it once, then each step below breaks down a specific part.</p>
<pre><code class="language-javascript">'use client'
import { motion, type Variants } from "motion/react";
import { CheckCircle } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";

const LETTER_VARIANTS: Variants = {
  hidden: { y: -14, opacity: 0 },
  visible: (i: number) =&gt; ({
    y: 0,
    opacity: 1,
    transition: {
      delay: i * 0.038,
      duration: 0.35,
      ease: [0.215, 0.61, 0.355, 1],
    },
  }),
};

const MotionBadge = motion.create(Badge);

const SuccessBadgeDemo = () =&gt; {
  const label = "Success";

  return (
    &lt;MotionBadge
      variant="outline"
      className={cn(
        "relative h-auto cursor-default overflow-visible rounded-full",
        "gap-2 px-3 py-2",
        "bg-background backdrop-blur-md",
        "text-foreground text-sm font-medium leading-none",
        "border-teal-400/25",
      )}
    &gt;
      {/* Top glow */}
      &lt;motion.span
        aria-hidden
        animate={{ opacity: 0.55 }}
        transition={{ duration: 0.45 }}
        className="pointer-events-none absolute -top-2 left-[10%] right-[10%] h-4 blur bg-[radial-gradient(ellipse_80%_100%_at_50%_100%,rgba(45,212,191,0.95)_0%,transparent_70%)]"
      /&gt;
      &lt;motion.span
        aria-hidden
        animate={{ opacity: 0.75 }}
        transition={{ duration: 0.45 }}
        className="pointer-events-none absolute -top-1 left-[22%] right-[22%] h-2 blur-sm bg-[radial-gradient(ellipse_70%_100%_at_50%_100%,rgba(45,212,191,0.85)_0%,transparent_70%)]"
      /&gt;
      &lt;motion.span
        aria-hidden
        animate={{ opacity: 0.9 }}
        transition={{ duration: 0.45 }}
        className="pointer-events-none absolute top-0 left-[28%] right-[28%] h-px bg-[radial-gradient(ellipse_40%_50%_at_50%_50%,rgba(45,212,191,0.95)_0%,transparent_100%)]"
      /&gt;

      {/* Icon */}
      &lt;motion.span
        initial={{ scale: 0.35, opacity: 0, rotate: -25 }}
        animate={{ scale: 1, opacity: 1, rotate: 0 }}
        transition={{ duration: 0.32, ease: [0.175, 0.885, 0.32, 1.275] }}
        className="flex h-4 w-4 shrink-0 items-center justify-center"
      &gt;
        &lt;CheckCircle size={16} strokeWidth={2} className="text-teal-400" /&gt;
      &lt;/motion.span&gt;

      {/* Animated label */}
      &lt;span className="inline-flex overflow-hidden leading-none"&gt;
        {label.split("").map((char, i) =&gt; (
          &lt;motion.span
            key={i}
            custom={i}
            variants={LETTER_VARIANTS}
            initial="hidden"
            animate="visible"
            className="inline-block whitespace-pre"
          &gt;
            {char}
          &lt;/motion.span&gt;
        ))}
      &lt;/span&gt;
    &lt;/MotionBadge&gt;
  );
};

export default SuccessBadgeDemo;
</code></pre>
<p>Now let's break it down piece by piece.</p>
<h2 id="heading-step-1-set-up-the-imports"><strong>Step 1: Set Up the Imports</strong></h2>
<pre><code class="language-javascript">'use client'
import { motion, type Variants } from "motion/react";
import { CheckCircle } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
</code></pre>
<p><code>'use client'</code> marks this as a Client Component in Next.js App Router. Motion animations run in the browser, not on the server, so this directive is required.</p>
<p><code>motion/react</code> is the import path for Motion v11 and above. If your project uses an older version, the import is <code>framer-motion</code>. The <code>Variants</code> type is a TypeScript helper for typing named animation state objects.</p>
<p><code>cn()</code> is the class name utility that ships with every shadcn/ui project. It merges Tailwind classes and handles conditional logic cleanly.</p>
<h2 id="heading-step-2-define-letter-animation-variants"><strong>Step 2: Define Letter Animation Variants</strong></h2>
<pre><code class="language-javascript">const LETTER_VARIANTS: Variants = {
  hidden: { y: -14, opacity: 0 },
  visible: (i: number) =&gt; ({
    y: 0,
    opacity: 1,
    transition: {
      delay: i * 0.038,
      duration: 0.35,
      ease: [0.215, 0.61, 0.355, 1],
    },
  }),
};
</code></pre>
<p>Each letter starts 14px above its final position and is fully transparent. When the component mounts, it moves to <code>y: 0</code> at full opacity.</p>
<p>The <code>delay: i * 0.038</code> formula is the stagger. Letter 0 has no delay, letter 1 waits 38ms, letter 2 waits 76ms, and so on. This makes the letters appear to cascade in from left to right.</p>
<p>The <code>ease</code> value <code>[0.215, 0.61, 0.355, 1]</code> is <code>easeOutCubic</code>. It starts fast and decelerates at the end, giving each letter a natural landing rather than a hard stop.</p>
<p>The <code>visible</code> function accepts a <code>custom</code> value. When you pass <code>custom={i}</code> on the <code>motion.span</code>, Motion calls this function with that index. Each letter calculates its own delay independently.</p>
<p><strong>Accessibility tip:</strong> To respect users with reduced motion preferences, import <code>useReducedMotion</code> from <code>motion/react</code> and skip the stagger when it returns <code>true</code>.</p>
<h2 id="heading-step-3-wrap-the-badge-with-motion"><strong>Step 3: Wrap the Badge with Motion</strong></h2>
<pre><code class="language-javascript">const MotionBadge = motion.create(Badge);
</code></pre>
<p>The <code>Badge</code> Component from shadcn/ui is a standard React component. You can't apply Motion props like <code>animate</code> or <code>initial</code> to it directly.</p>
<p><code>motion.create()</code> wraps any React component and returns a new version that accepts all Motion animation props. The result, <code>MotionBadge</code>, behaves exactly like <code>Badge</code> But it's now fully animatable.</p>
<p>Use this pattern any time you want to animate a custom or third-party library component with Motion.</p>
<h2 id="heading-step-4-build-the-glow-layers"><strong>Step 4: Build the Glow Layers</strong></h2>
<pre><code class="language-javascript">&lt;motion.span
  aria-hidden
  animate={{ opacity: 0.55 }}
  transition={{ duration: 0.45 }}
  className="pointer-events-none absolute -top-2 left-[10%] right-[10%] h-4 blur bg-[radial-gradient(ellipse_80%_100%_at_50%_100%,rgba(45,212,191,0.95)_0%,transparent_70%)]"
/&gt;
&lt;motion.span
  aria-hidden
  animate={{ opacity: 0.75 }}
  transition={{ duration: 0.45 }}
  className="pointer-events-none absolute -top-1 left-[22%] right-[22%] h-2 blur-sm bg-[radial-gradient(ellipse_70%_100%_at_50%_100%,rgba(45,212,191,0.85)_0%,transparent_70%)]"
/&gt;
&lt;motion.span
  aria-hidden
  animate={{ opacity: 0.9 }}
  transition={{ duration: 0.45 }}
  className="pointer-events-none absolute top-0 left-[28%] right-[28%] h-px bg-[radial-gradient(ellipse_40%_50%_at_50%_50%,rgba(45,212,191,0.95)_0%,transparent_100%)]"
/&gt;
</code></pre>
<p>Three spans stack on top of each other above the badge border. Each is narrower and more opaque than the one behind it:</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Position</th>
<th>Width</th>
<th>Blur</th>
<th>Final Opacity</th>
</tr>
</thead>
<tbody><tr>
<td>Outer</td>
<td><code>-top-2</code></td>
<td>80%</td>
<td><code>blur</code></td>
<td>0.55</td>
</tr>
<tr>
<td>Middle</td>
<td><code>-top-1</code></td>
<td>56%</td>
<td><code>blur-sm</code></td>
<td>0.75</td>
</tr>
<tr>
<td>Inner line</td>
<td><code>top-0</code></td>
<td>44%</td>
<td>none</td>
<td>0.90</td>
</tr>
</tbody></table>
<p>The innermost layer is only 1px tall (<code>h-px</code>) with no blur. This gives the glow a crisp, bright edge right at the badge border. The two outer layers create the soft falloff around it.</p>
<p>All three carry <code>aria-hidden</code> because they're purely decorative. Screen readers skip them. The <code>overflow-visible</code> class on <code>MotionBadge</code> is what allows these spans to render outside the component's boundary without clipping.</p>
<h2 id="heading-step-5-animate-the-icon"><strong>Step 5: Animate the Icon</strong></h2>
<pre><code class="language-javascript">&lt;motion.span
  initial={{ scale: 0.35, opacity: 0, rotate: -25 }}
  animate={{ scale: 1, opacity: 1, rotate: 0 }}
  transition={{ duration: 0.32, ease: [0.175, 0.885, 0.32, 1.275] }}
  className="flex h-4 w-4 shrink-0 items-center justify-center"
&gt;
  &lt;CheckCircle size={16} strokeWidth={2} className="text-teal-400" /&gt;
&lt;/motion.span&gt;
</code></pre>
<p>The icon starts at 35% scale, invisible, and rotated 25 degrees counter-clockwise. It animates to full size and zero rotation on mount.</p>
<p>The <code>ease</code> value <code>[0.175, 0.885, 0.32, 1.275]</code> is <code>easeOutBack</code>. Unlike <code>easeOutCubic</code>, this curve overshoots its target slightly before snapping back. The icon appears to spring into place. It is a subtle effect, but it makes the icon feel physical.</p>
<p><code>shrink-0</code> on the wrapper prevents the icon from compressing inside the flex container.</p>
<h2 id="heading-step-6-animate-each-letter"><strong>Step 6: Animate Each Letter</strong></h2>
<pre><code class="language-javascript">&lt;span className="inline-flex overflow-hidden leading-none"&gt;
  {label.split("").map((char, i) =&gt; (
    &lt;motion.span
      key={i}
      custom={i}
      variants={LETTER_VARIANTS}
      initial="hidden"
      animate="visible"
      className="inline-block whitespace-pre"
    &gt;
      {char}
    &lt;/motion.span&gt;
  ))}
&lt;/span&gt;
</code></pre>
<p><code>label.split("")</code> turns <code>"Success"</code> into <code>["S", "u", "c", "c", "e", "s", "s"]</code>. Each character gets its own <code>motion.span</code>.</p>
<p><code>variants={LETTER_VARIANTS}</code> connects each span to the animation states from Step 2. <code>custom={i}</code> passes the character's index into the <code>visible</code> resolver so each letter knows its own delay.</p>
<p>Two Tailwind classes matter here:</p>
<ul>
<li><p><code>overflow-hidden</code> on the wrapper clips, each letter as it slides in from above. Without it, letters would be visible outside the badge before they land.</p>
</li>
<li><p><code>inline-block</code> on each <code>motion.span</code> is required for <code>translateY</code> to work. CSS transforms do not apply to inline elements by default.</p>
</li>
</ul>
<h2 id="heading-how-to-use-it-in-your-app"><strong>How to Use It in Your App</strong></h2>
<p>Import and render <code>SuccessBadgeDemo</code> anywhere in your project:</p>
<pre><code class="language-javascript">// app/page.tsx
import SuccessBadgeDemo from "@/components/shadcn-space/badge/badge-07";

export default function Page() {
  return (
    &lt;div className="flex items-center justify-center min-h-screen"&gt;
      &lt;SuccessBadgeDemo /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>The component is self-contained. It carries its own animation state, theme tokens, and glow layers. No props are required.</p>
<h2 id="heading-how-to-customize-the-component"><strong>How to Customize the Component</strong></h2>
<p>You can change the label by replacing <code>"Success"</code> it with any string. The letter animation applies automatically since it splits whatever string you pass.</p>
<p>To build a complete blue "Verified" variant, you just need to change three things: the border color class, the glow gradient color values, and the icon. Here's the full updated component:</p>
<pre><code class="language-javascript">'use client'
import { motion, type Variants } from "motion/react";
import { ShieldCheck } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";

const LETTER_VARIANTS: Variants = {
  hidden: { y: -14, opacity: 0 },
  visible: (i: number) =&gt; ({
    y: 0,
    opacity: 1,
    transition: {
      delay: i * 0.038,
      duration: 0.35,
      ease: [0.215, 0.61, 0.355, 1],
    },
  }),
};

const MotionBadge = motion.create(Badge);

const VerifiedBadgeDemo = () =&gt; {
  const label = "Verified";

  return (
    &lt;MotionBadge
      variant="outline"
      className={cn(
        "relative h-auto cursor-default overflow-visible rounded-full",
        "gap-2 px-3 py-2",
        "bg-background backdrop-blur-md",
        "text-foreground text-sm font-medium leading-none",
        "border-blue-400/25",
      )}
    &gt;
      &lt;motion.span aria-hidden animate={{ opacity: 0.55 }} transition={{ duration: 0.45 }}
        className="pointer-events-none absolute -top-2 left-[10%] right-[10%] h-4 blur bg-[radial-gradient(ellipse_80%_100%_at_50%_100%,rgba(96,165,250,0.95)_0%,transparent_70%)]"
      /&gt;
      &lt;motion.span aria-hidden animate={{ opacity: 0.75 }} transition={{ duration: 0.45 }}
        className="pointer-events-none absolute -top-1 left-[22%] right-[22%] h-2 blur-sm bg-[radial-gradient(ellipse_70%_100%_at_50%_100%,rgba(96,165,250,0.85)_0%,transparent_70%)]"
      /&gt;
      &lt;motion.span aria-hidden animate={{ opacity: 0.9 }} transition={{ duration: 0.45 }}
        className="pointer-events-none absolute top-0 left-[28%] right-[28%] h-px bg-[radial-gradient(ellipse_40%_50%_at_50%_50%,rgba(96,165,250,0.95)_0%,transparent_100%)]"
      /&gt;

      &lt;motion.span
        initial={{ scale: 0.35, opacity: 0, rotate: -25 }}
        animate={{ scale: 1, opacity: 1, rotate: 0 }}
        transition={{ duration: 0.32, ease: [0.175, 0.885, 0.32, 1.275] }}
        className="flex h-4 w-4 shrink-0 items-center justify-center"
      &gt;
        &lt;ShieldCheck size={16} strokeWidth={2} className="text-blue-400" /&gt;
      &lt;/motion.span&gt;

      &lt;span className="inline-flex overflow-hidden leading-none"&gt;
        {label.split("").map((char, i) =&gt; (
          &lt;motion.span key={i} custom={i} variants={LETTER_VARIANTS}
            initial="hidden" animate="visible" className="inline-block whitespace-pre"
          &gt;
            {char}
          &lt;/motion.span&gt;
        ))}
      &lt;/span&gt;
    &lt;/MotionBadge&gt;
  );
};

export default VerifiedBadgeDemo;
</code></pre>
<p>The only changes from the original: <code>border-blue-400/25</code> on the badge, <code>rgba(96, 165, 250, ...)</code> in the glow gradients (<code>blue-400</code> in Tailwind), <code>ShieldCheck</code> for the icon, and <code>text-blue-400</code> on the icon class.</p>
<p>To adjust stagger speed, just change the delay multiplier in <code>LETTER_VARIANTS</code>:</p>
<pre><code class="language-javascript">delay: i * 0.06, // slower stagger
delay: i * 0.02, // faster stagger
</code></pre>
<p>You can also explore the <a href="https://shadcnspace.com/blocks"><strong>Shadcn Blocks</strong></a> collection to see how animated badges fit into full dashboard and card layouts.</p>
<hr>
<h2 id="heading-live-preview"><strong>Live Preview</strong></h2>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/08db3820-9f72-4ddb-a507-e33cdcda5fb8.gif" alt="08db3820-9f72-4ddb-a507-e33cdcda5fb8" style="display:block;margin:0 auto" width="1152" height="648" loading="lazy">

<h2 id="heading-key-concepts-recap"><strong>Key Concepts Recap</strong></h2>
<table>
<thead>
<tr>
<th>Concept</th>
<th>What It Does</th>
</tr>
</thead>
<tbody><tr>
<td><code>motion.create(Component)</code></td>
<td>Wraps any React component to accept Motion animation props</td>
</tr>
<tr>
<td><code>Variants</code></td>
<td>Named animation states (<code>hidden</code>, <code>visible</code>) defined outside JSX for reuse</td>
</tr>
<tr>
<td><code>custom={i}</code> + variant function</td>
<td>Passes a per-element value into the variant resolver for dynamic transitions</td>
</tr>
<tr>
<td><code>delay: i * 0.038</code></td>
<td>Stagger formula: each element's delay grows by its index</td>
</tr>
<tr>
<td><code>easeOutCubic</code> <code>[0.215, 0.61, 0.355, 1]</code></td>
<td>Fast start, smooth deceleration. Letter drop-in.</td>
</tr>
<tr>
<td><code>easeOutBack</code> <code>[0.175, 0.885, 0.32, 1.275]</code></td>
<td>Overshoots slightly, snaps back. Icon pop.</td>
</tr>
<tr>
<td>Three stacked radial gradients</td>
<td>Wide + soft outer glow, narrow + sharp inner line</td>
</tr>
<tr>
<td><code>overflow-visible</code> on the badge</td>
<td>Allows glow spans to extend outside the component's own bounds</td>
</tr>
</tbody></table>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>In this tutorial, you built a complete animated badge from scratch with a layered glow, bouncing icon, and staggered letter animation. Every part of it uses your existing Shadcn theme tokens, so it drops into any project without extra configuration.</p>
<p>You can browse more <a href="https://shadcnspace.com/components"><strong>Shadcn Components</strong></a> on Shadcn Space to apply the same animation patterns to other UI elements. If you work with external services and tooling in your stack, the <a href="https://shadcnspace.com/mcp"><strong>Shadcn MCP</strong></a> integration is worth looking at as a next step.</p>
<h2 id="heading-resources"><strong>Resources</strong></h2>
<ul>
<li><p><a href="https://shadcnspace.com/components/badge"><strong>Shadcn Space Badge Components</strong></a>: with all badge variants, including Pending, Failed, and more</p>
</li>
<li><p><a href="https://shadcnspace.com/docs/getting-started/how-to-use-shadcn-cli"><strong>Shadcn Space Getting Started Guide</strong></a>: how to use the Shadcn CLI with third-party registries</p>
</li>
<li><p><a href="https://motion.dev/"><strong>Motion Docs</strong></a>: official documentation for <code>motion/react</code></p>
</li>
<li><p><a href="https://lucide.dev/"><strong>Lucide React</strong></a>: icon library used in this tutorial</p>
</li>
<li><p><a href="https://ui.shadcn.com/docs"><strong>Shadcn/ui Documentation</strong></a></p>
</li>
<li><p><a href="https://youtu.be/n6dvjVxy02U?si=EXfClzSyI8D97VaI"><strong>YouTube: Shadcn Space CLI Walkthrough</strong></a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ From LLMs to LangChain: Understanding How Modern AI Applications Actually Work ]]>
                </title>
                <description>
                    <![CDATA[ Typically, when we start experimenting with AI, many of us begin similarly. We try a single LLM call as the core of an app, like this: const response = await llm.chat("Explain Kubernetes"); For a lit ]]>
                </description>
                <link>https://www.freecodecamp.org/news/from-llms-to-langchain-understanding-how-modern-ai-applications-actually-work/</link>
                <guid isPermaLink="false">6a3aab13b5ad15098db82372</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ llm ]]>
                    </category>
                
                    <category>
                        <![CDATA[ langchain ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Open Source ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Sudheesh Shetty ]]>
                </dc:creator>
                <pubDate>Tue, 23 Jun 2026 15:49:39 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/38787e16-7e86-44da-9a6a-620cc1a99fce.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Typically, when we start experimenting with AI, many of us begin similarly. We try a single LLM call as the core of an app, like this:</p>
<pre><code class="language-plaintext">const response = await llm.chat("Explain Kubernetes");
</code></pre>
<p>For a little while it feels like the whole flow is: the user asks something, and the model returns an answer. That early success often creates a false impression that building AI is just about sending prompts and getting responses.</p>
<p>That simplicity is seductive, but it doesn't hold up. Over time, users want the assistant to find answers in their documents and knowledge bases, call APIs, fetch live data, or trigger services or schedule meetings.</p>
<p>Users also expect the agent to access internal systems and interact with ERPs, CRMs, or other tools holding critical business data. They'll want agents to combine multiple steps, as workflows often require chaining queries, computations, and side effects into reliable processes.</p>
<p>This is where concepts like MCP (the Model Context Protocol) and tools like LangChain come in. Initially, they may seem like buzzwords, but they address different aspects of LLM production.</p>
<p>After experimenting with AI tools, I found that these concepts help solve different problems related to interfaces, orchestration, and system integration.</p>
<p>This article is a practical guide to understanding how LLMs connect with tools, orchestrate workflows, and power real AI applications.</p>
<h3 id="heading-heres-what-well-cover">Here’s what we’ll cover:</h3>
<ol>
<li><p><a href="#heading-what-is-an-llm">What Is an LLM?</a></p>
</li>
<li><p><a href="#heading-why-llms-need-tools">Why LLMs Need Tools</a></p>
</li>
<li><p><a href="#heading-where-mcp-comes-in">Where MCP Comes In</a></p>
</li>
<li><p><a href="#heading-so-what-does-langchain-actually-do">So What Does LangChain Actually Do?</a></p>
</li>
<li><p><a href="#heading-putting-it-together">Putting It Together</a></p>
</li>
<li><p><a href="#heading-what-i-built-while-learning-this">What I Built While Learning This</a></p>
</li>
</ol>
<p>Throughout the article we'll discuss what LLMs are and how they work, what tool-calling looks like in practice, what MCP is and how it works, how LangChain fits into the whole process, and how to put all these tools together.</p>
<p>To follow along, you'll need a basic understanding of Node.js, API operations, and basic JavaScript concepts.</p>
<h2 id="heading-what-is-an-llm"><strong>What Is an LLM?</strong></h2>
<p>LLM stands for <strong>Large Language Model</strong>. It's a class of deep neural networks trained on massive amounts of text to model and generate human-like language. Popular examples you might have heard of include GPT, Claude, Gemini, and Llama.</p>
<h3 id="heading-how-to-call-an-llm-from-a-nodejs-application">How to Call an LLM From a Node.js Application</h3>
<p>Before writing code, let’s understand what it means to call an LLM from a Node.js application.</p>
<p>Calling an LLM means sending input from your application to an AI provider’s API and receiving generated output in return. It's similar to calling any other external service.</p>
<p>In most real-world applications, the model isn't hosted or trained by your application. Instead, providers such as OpenAI and Groq host and maintain the models, while your application communicates with them over HTTP APIs.</p>
<p>In this example, we’ll build a minimal API using Node.js and Express. We’ll create a simple <code>POST /chat</code> endpoint that accepts a user message, sends it to the OpenAI API, receives the generated response, and returns it to the client.</p>
<p>Here, our Node.js server acts as the bridge between the user and the LLM provider.</p>
<p>For this example, create an API key from the <a href="https://console.groq.com/keys">Groq</a> console. Since it offers a free tier, it’s a simple way to experiment and understand the concepts.</p>
<p>First, install the dependencies:</p>
<pre><code class="language-plaintext">npm install express
</code></pre>
<pre><code class="language-javascript">import express from "express";

const app = express();
app.use(express.json());

app.post("/chat", async (req, res) =&gt; {
  const { message } = req.body;
  const response = await fetch("https://api.groq.com/openai/v1/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: GROQ_API_KEY,
    },
    body: JSON.stringify({
      model: "llama-3.3-70b-versatile",
      messages: [{ role: "user", content: message }],
    }),
  });

  const data = await response.json();

  if (!response.ok) {
    return res.status(response.status).json({ error: data });
  }

  const reply = data.choices[0].message.content;

  res.json({ reply });
});

const PORT = process.env.PORT || 8888;
app.listen(PORT, () =&gt; {
  console.log(`Server running on http://localhost:${PORT}`);
});
</code></pre>
<p>Start the server and make a request. Use Postman and do a POST request to <code>/chat</code> using the below body:</p>
<pre><code class="language-plaintext">POST /chat

{
  "message": "Explain Kubernetes"
}
</code></pre>
<p>Example response:</p>
<pre><code class="language-plaintext">{
  "reply": "Kubernetes is a container orchestration platform..."
}
</code></pre>
<p>The backend receives the message, forwards it to the model provider, receives generated text, and returns it to the client.</p>
<p>LLMs are excellent at language-centric tasks: they understand phrasing and intent, generate coherent text, extract structured information from unstructured input, and perform basic reasoning over provided context. These capabilities make them powerful for things like summarization, drafting, and conversational QA.</p>
<p>But there’s an important limitation: LLMs don't automatically know about and can't access your private or live data. They don’t have implicit access to your company database, internal APIs, or the current state of your systems unless you provide that information at runtime.</p>
<p>Because of that limitation, you need secure mechanisms to connect models to live systems and data — which brings us to the idea of tools.</p>
<h2 id="heading-why-llms-need-tools"><strong>Why LLMs Need Tools</strong></h2>
<p>Imagine asking:</p>
<blockquote>
<p>Check my order and raise support if delivery is delayed.</p>
</blockquote>
<p>The model alone can't inspect your order database or create a support ticket in your system. To do that, it must call external functions — for example, a <code>getOrderStatus(orderId)</code> API and a <code>createSupportTicket(orderId, issue)</code> action.</p>
<p>Those callable functions are what we call tools: programmatic interfaces the AI can use to interact with systems and take concrete actions on behalf of users.</p>
<p>A tool is simply a function that an AI model can call to interact with external systems or perform actions.</p>
<p>For example, imagine we have a getOrderStatus(id) function that returns an order’s delivery status.</p>
<p>To expose this to the LLM, we define a tools array. Each tool includes:</p>
<ul>
<li><p>type – currently "function"</p>
</li>
<li><p>function name – the function identifier</p>
</li>
<li><p>function description – helps the LLM decide when to call the tool</p>
</li>
<li><p>function parameters – a JSON Schema describing the arguments the tool expects</p>
</li>
</ul>
<p>Here's an example:</p>
<pre><code class="language-typescript">function getOrderStatus(id) {
  const statuses = ["pending", "success", "cancelled"];
  const status = statuses[Math.floor(Math.random() * statuses.length)];
  return `Your order status is ${status}.`;
}

const tools = [
  {
    type: "function",
    function: {
      name: "getOrderStatus",
      description: "Get the status of an order by its ID",
      parameters: {
        type: "object",
        properties: {
          id: { type: "string", description: "The order ID" },
        },
        required: ["id"],
      },
    },
  },
];
</code></pre>
<p>The above tool format is for Grok. Different LLM providers may use different formats for defining tools, but the overall idea remains the same.</p>
<p>When making the API call, we pass both the user messages and the list of available tools.</p>
<pre><code class="language-typescript">body: JSON.stringify({
    model: "llama-3.3-70b-versatile",
    messages: [{ role: "user", content: message }],
    tools,
}),
</code></pre>
<p>After the API call, the LLM decides whether a tool is needed. If a tool call is requested, our application executes the corresponding function and sends the result back to the model.</p>
<p>For this example, we'll only handle the <code>getOrderStatus</code> tool. We can check whether the model requested a tool call like this:</p>
<pre><code class="language-typescript">const toolCall = data.choices[0].message.tool_calls[0];
const { id } = JSON.parse(toolCall.function.arguments);
const toolResult = getOrderStatus(id)
</code></pre>
<p>and later we can pass the message context with tool result</p>
<pre><code class="language-typescript">body: JSON.stringify({
    model: "llama-3.3-70b-versatile",
    messages: [
        { role: "user", content: message },
        assistantMessage,
        { role: "tool", tool_call_id: toolCall.id, content: toolResult },
    ],
    tools,
}),
</code></pre>
<p>Finally, return the response:</p>
<pre><code class="language-typescript">return res.json({ reply: followUpData.choices[0].message.content });
</code></pre>
<p>Here's a diagram of the flow:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a1fa5fdc5c3ae375fb38ab2/22d6dc4d-ad5e-4fbb-84f6-71c367565282.png" alt="User -> LLM -> Tool Execution -> Tool Result -> Final Response" style="display:block;margin:0 auto" width="1774" height="887" loading="lazy">

<p>The LLM decides whether a tool is needed and generates the required inputs, while your application executes the function.</p>
<h2 id="heading-where-mcp-comes-in"><strong>Where MCP Comes In</strong></h2>
<p>Tools are simple. You define functions and tell the AI what it can use.</p>
<p>For example, <code>getOrderStatus()</code> works well when all tools are built inside your application. But as applications grow, tools may come from many places, like Slack, GitHub, databases, internal systems, or third-party services. Each one may expose tools differently.</p>
<p>This is where <a href="https://www.freecodecamp.org/news/how-does-an-mcp-work-under-the-hood/">MCP (Model Context Protocol) helps</a>. Think of MCP as a common language that lets AI systems connect to external tools in a consistent way.</p>
<p>Tools define what the AI can do. MCP standardizes how the AI connects to and uses those tools.</p>
<p>Now let’s extend the previous /chat API example so the LLM can use tools exposed through MCP. There are multiple ways to do this:</p>
<ul>
<li><p>build and host your own MCP server and expose your application functions</p>
</li>
<li><p>connect to existing third-party MCP servers such as Slack</p>
</li>
</ul>
<p>For this tutorial, we'll keep things simple and use a remote MCP server approach because it's easier to understand.</p>
<pre><code class="language-plaintext">npm install express @modelcontextprotocol/sdk zod
</code></pre>
<p>Now let’s create our own MCP server and expose the same <code>getOrderStatus</code> function as an MCP tool:</p>
<pre><code class="language-typescript">import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";

function getOrderStatus(id) {
  const statuses = ["pending", "success", "cancelled"];
  const status = statuses[Math.floor(Math.random() * statuses.length)];
  return `Your order status is ${status}.`;
}

function createOrderServer() {
  const server = new McpServer({ name: "order-server", version: "1.0.0" });

  server.registerTool(
    "getOrderStatus",
    {
      description: "Get the status of an order by its ID",
      inputSchema: { id: z.string() },
    },
    async ({ id }) =&gt; ({
      content: [{ type: "text", text: getOrderStatus(id) }],
    })
  );

  return server;
}

const app = createMcpExpressApp({ host: "0.0.0.0" });

app.post("/mcp", async (req, res) =&gt; {
  const server = createOrderServer();
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
  });

  res.on("close", () =&gt; {
    transport.close();
    server.close();
  });

  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

const PORT = process.env.PORT || 3001;
app.listen(PORT, "0.0.0.0", () =&gt; {
  console.log(`Order MCP server running on http://0.0.0.0:${PORT}/mcp`);
});
</code></pre>
<p>This is useful when you want to expose your own application functions through MCP. Typically, the MCP server runs separately and is accessed by MCP clients. Now any MCP client can connect to this server and discover the available tools automatically.</p>
<p>The same idea applies to third-party MCP servers.</p>
<p>For example, if a Slack MCP server is available, we can connect to it instead of writing Slack integration code ourselves.</p>
<p>In that case, our application isn't directly calling Slack APIs. It connects to the Slack MCP server, which exposes Slack-related tools using the MCP standard.</p>
<p>So the difference is:</p>
<ul>
<li><p>For our own features, we can build our own MCP server</p>
</li>
<li><p>For external systems, we can use existing MCP servers when available</p>
</li>
</ul>
<p>Now we can pass MCP servers to the LLM request:</p>
<pre><code class="language-typescript">body: JSON.stringify({
  model: "llama-3.3-70b-versatile",
  messages: [{ role: "user", content: message }],
  tools: [
    {
      type: "mcp",
      server_label: "OrderServer",
      server_url: `http://0.0.0.0:${PORT}/mcp`,
      server_description: "Get the status of an order by its ID",
    },
    {
      type: "mcp",
      server_label: "Slack",
      server_url: "https://mcp.slack.com/mcp",
      server_description: "Send and read Slack messages",
      headers: {
        Authorization: `Bearer ${process.env.SLACK_BOT_TOKEN}`,
      },
    },
  ],
})
</code></pre>
<p>We can also use local MCP servers instead of remote URLs by connecting through transports such as <code>StdioClientTransport</code>. In that case, we connect locally, discover the available tools, and expose them to the LLM.</p>
<p>Now if the user sends:</p>
<pre><code class="language-json">{
  "message": "What is status of order 123"
}
</code></pre>
<p>The LLM decides whether a tool is needed, MCP exposes and executes the tool, and the final response is returned to the user.</p>
<p>The flow becomes:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a1fa5fdc5c3ae375fb38ab2/2db75d86-db9a-477e-b578-92221a490a2a.png" alt="User -> /chat api -> LLM -> MCP Tool -> Tool Result -> Tool Response" style="display:block;margin:0 auto" width="1774" height="887" loading="lazy">

<p>This standardization makes integrations far more reusable: instead of rewriting glue logic for each new connector, teams can register MCP-compliant tools and let the orchestrator and model handle discovery and invocation.</p>
<h2 id="heading-so-what-does-langchain-actually-do"><strong>So What Does LangChain Actually Do?</strong></h2>
<p>I initially thought LangChain was simply another wrapper around LLM APIs, but it is better understood as an orchestration framework for AI workflows. Tools let an LLM perform actions. MCP standardizes how tools are exposed. LangChain helps coordinate models, tools, and application logic to build multi-step workflows.</p>
<p>For example:</p>
<blockquote>
<p>User: Find flights, compare prices, book hotel, send confirmation.</p>
</blockquote>
<p>Now the system may need to:</p>
<ul>
<li><p>Check order status</p>
</li>
<li><p>Decide whether support is needed</p>
</li>
<li><p>Create a support ticket</p>
</li>
<li><p>Generate the final response</p>
</li>
</ul>
<p>Without orchestration, you would manually control each step. LangChain helps manage this flow.</p>
<p>To use LangChain, Install the required packages:</p>
<pre><code class="language-json">npm install express langchain @langchain/groq
</code></pre>
<p>We'll reuse the same tool functions from earlier:</p>
<pre><code class="language-typescript">import express from "express";
import { createAgent } from "langchain";
import { ChatGroq } from "@langchain/groq";

const app = express();
app.use(express.json());

const agent = createAgent({
  model: new ChatGroq({
    model: "llama-3.3-70b-versatile",
    apiKey: GROQ_API_KEY,
  }),
  tools: [
    {
      name: "getOrderStatus",
      description:
        "Get order status",
      execute: ({ id }) =&gt;
        getOrderStatus(id), // we have this function above
    },
    {
      name: "createSupportTicket",
      description:
        "Create support ticket",
      execute: ({ id }) =&gt;
        createSupportTicket(id), //imagine a function that creates a support ticket
    },
  ],
});

app.post(
  "/chat",
  async (req, res) =&gt; {
    const { message } = req.body;

    const response =
      await agent.invoke({
        messages: [
          {
            role: "user",
            content: message,
          },
        ],
      });

    res.json({
      reply:
        response.messages
          ?.at(-1)
          ?.text,
    });
  }
);

app.listen(3000);
</code></pre>
<p>Now the flow becomes:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a1fa5fdc5c3ae375fb38ab2/bd2a266c-39eb-4f3e-9909-ad81360bccb7.png" alt="Horizontal architecture diagram showing User → /chat API → LangChain Agent → OpenAI → Tool → Tool Result → Final Response." style="display:block;margin:0 auto" width="1930" height="815" loading="lazy">

<p>LangChain doesn't replace tools or MCP. It sits above them and coordinates how everything works together.</p>
<h2 id="heading-putting-it-together"><strong>Putting It Together</strong></h2>
<p>A modern AI application usually has multiple layers working together. The LLM handles reasoning and language generation. Tools perform real operations such as reading data, calling APIs, or executing actions. MCP helps standardize how those tools are exposed and accessed. LangChain helps orchestrate the interaction between models, tools, and workflows.</p>
<p>By separating these responsibilities, applications become easier to extend, maintain, and scale.</p>
<p>The goal is more than just generating text. You want to be able to build systems that can reason, retrieve information, take actions, and reliably solve real user problems.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a1fa5fdc5c3ae375fb38ab2/bfc88660-3145-4b89-a626-158c4ec52bcc.png" alt="User ->LLM -> LangChain -> MCP -> Tools -> Systems &amp; Data" style="display:block;margin:0 auto" width="1536" height="1024" loading="lazy">

<h2 id="heading-what-i-built-while-learning-this"><strong>What I Built While Learning This</strong></h2>
<p>After understanding the concepts above, I wanted to reduce some of this setup for my own projects. As I experimented, I noticed most applications recreate the same plumbing over and over: connecting an LLM, wiring up tools, managing execution, and exposing orchestration patterns.</p>
<p>So I built a small open-source toolkit to reduce that setup. The goal was simple: you should be able to focus on business logic instead of wiring AI infrastructure.</p>
<p>Current capabilities:</p>
<ul>
<li><p>LLM integration</p>
</li>
<li><p>Tool registration</p>
</li>
<li><p>Tool execution</p>
</li>
<li><p>Chat orchestration</p>
</li>
<li><p>LangChain support</p>
</li>
<li><p>Extensible architecture</p>
</li>
</ul>
<h3 id="heading-packages">Packages:</h3>
<p>AI Chat Widget: <a href="https://www.npmjs.com/package/ai-chat-toolkit-widget">https://www.npmjs.com/package/ai-chat-toolkit-widget</a></p>
<p>AI Chat Server: <a href="https://www.npmjs.com/package/ai-chat-toolkit-server">https://www.npmjs.com/package/ai-chat-toolkit-server</a></p>
<p>GitHub Repository: <a href="https://github.com/sudheeshshetty/ai-chat-toolkit">https://github.com/sudheeshshetty/ai-chat-toolkit</a></p>
<p>To build a server using the toolkit:</p>
<pre><code class="language-typescript">npm install express ai-chat-toolkit-server
</code></pre>
<p>Create the chat server:</p>
<pre><code class="language-typescript">const aiChat = new AiChatServer({
  path: "/my-chat",
  provider: "groq",
  apiKey: process.env.API_KEY,
  model: process.env.MODEL || "llama-3.3-70b-versatile",
  cors: {
    origin: "http://localhost:5174",
  },
  orchestration: "langchain",
  maxToolRounds: 6,
  systemPrompt:
    "You are a helpful operations assistant for a demo store. Keep answers concise.",
});
</code></pre>
<p>Add your tools:</p>
<pre><code class="language-typescript">aiChat.addTools([
  {
    name: "...",
    description: "...",
    inputSchema: { ... },
    handler: async (input) =&gt; { /* runs in Node */ },
  },
]);
</code></pre>
<p>Attach it to your Express app:</p>
<pre><code class="language-typescript">aiChat.attach(app);
</code></pre>
<p>Now <code>/my-chat</code> is exposed in your Express server and can be used directly.</p>
<p>You can also use <code>ai-chat-toolkit-widget</code> if you want to skip building the chat UI.</p>
<p>Examples are available in the repository, so you can try it out quickly.</p>
<p>A quick glance of one of the examples:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a1fa5fdc5c3ae375fb38ab2/a9079710-be65-472b-881f-350daeeb0f3b.gif" alt="a9079710-be65-472b-881f-350daeeb0f3b" style="display:block;margin:0 auto" width="3456" height="2234" loading="lazy">

<p>If you find it useful, I’d appreciate a star, feedback, or contributions on GitHub as I continue improving the developer experience and exploring new ideas.<br>Thanks for reading — I hope this helped make LLMs, tools, MCP, and LangChain feel a little less magical and a lot more practical.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Building a Website in 2026: What Matters More Than Your Tech Stack ]]>
                </title>
                <description>
                    <![CDATA[ For years, developers have debated which technology stack was best for building websites. Some preferred React. Others chose Vue, Angular, Svelte, or server-side frameworks such as Laravel and Django. ]]>
                </description>
                <link>https://www.freecodecamp.org/news/building-a-website-what-matters-more-than-your-tech-stack/</link>
                <guid isPermaLink="false">6a2e0f54136fd4eb2c9cc925</guid>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ web performance ]]>
                    </category>
                
                    <category>
                        <![CDATA[ infrastructure ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Sun, 14 Jun 2026 02:17:56 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/10d14873-8414-410e-8325-17e7df039608.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>For years, developers have debated which technology stack was best for building websites.</p>
<p>Some preferred React. Others chose Vue, Angular, Svelte, or server-side frameworks such as Laravel and Django.</p>
<p>Entire conferences, blogs, and social media discussions have been dedicated to comparing frameworks and programming languages.</p>
<p>In 2026, those debates matter less than many developers think.</p>
<p>A modern website can be built with almost any mature framework and still perform well. The bigger challenge is making sure people can actually find, trust, and use that website.</p>
<p>Discoverability, performance, infrastructure, structured data, and AI search visibility now have a greater impact on success than the choice between competing frontend libraries.</p>
<p>The websites that win today aren't necessarily built with the most fashionable technologies. They're built with a strong foundation that helps users and search systems understand, access, and trust their content.</p>
<p>In this article, we'll look at what really matters when building a website these days. We'll explore why performance, hosting, domain management, structured data, and content quality often have a bigger impact than the technology stack itself.</p>
<p>We'll also examine how AI-powered search is changing the way people find information online and what developers can do to improve their website's visibility.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-the-tech-stack-has-become-a-commodity">The Tech Stack Has Become a Commodity</a></p>
</li>
<li><p><a href="#heading-performance-is-still-a-competitive-advantage">Performance Is Still a Competitive Advantage</a></p>
</li>
<li><p><a href="#heading-domains-and-infrastructure-still-matter">Domains and Infrastructure Still Matter</a></p>
</li>
<li><p><a href="#heading-hosting-is-no-longer-just-about-servers">Hosting Is No Longer Just About Servers</a></p>
</li>
<li><p><a href="#heading-structured-data-has-become-essential">Structured Data Has Become Essential</a></p>
</li>
<li><p><a href="#heading-the-rise-of-ai-search-and-answer-engines">The Rise of AI Search and Answer Engines</a></p>
</li>
<li><p><a href="#heading-content-quality-is-more-important-than-ever">Content Quality Is More Important Than Ever</a></p>
</li>
<li><p><a href="#heading-user-experience-is-the-new-differentiator">User Experience Is the New Differentiator</a></p>
</li>
<li><p><a href="#heading-the-future-is-about-outcomes-not-frameworks">The Future Is About Outcomes, Not Frameworks</a></p>
</li>
</ul>
<h2 id="heading-the-tech-stack-has-become-a-commodity"><strong>The Tech Stack Has Become a Commodity</strong></h2>
<p>The web development ecosystem has matured significantly over the past decade. Most modern frameworks provide similar capabilities. They support <a href="https://www.freecodecamp.org/news/a-brief-introduction-to-web-components/">component-based development</a>, <a href="https://www.freecodecamp.org/news/rendering-patterns/">server-side rendering</a>, API integrations, authentication systems, and performance optimization.</p>
<p>As a result, the gap between frameworks has narrowed.</p>
<p>A poorly optimized website built with the latest framework will often perform worse than a well-optimized website built with older technology. Users rarely care whether a page was built with React, Vue, or another framework. They care whether it loads quickly, works on mobile devices, and provides useful information.</p>
<p>Businesses care even more about outcomes. They want traffic, conversions, customer engagement, and revenue growth. None of those metrics improve simply because a team adopted a trendy technology stack.</p>
<p>This shift has forced development teams to focus on factors that have a direct impact on visibility and user experience.</p>
<h2 id="heading-performance-is-still-a-competitive-advantage"><strong>Performance Is Still a Competitive Advantage</strong></h2>
<p>Despite advances in hosting and frontend tooling, <a href="https://www.freecodecamp.org/news/performance-testing-for-web-applications/">website performance</a> remains one of the strongest predictors of user satisfaction.</p>
<p>Research consistently shows that slower websites lead to higher <a href="https://www.semrush.com/blog/bounce-rate/">bounce rates</a> and lower conversion rates. Users expect pages to load almost instantly. Even a delay of a few seconds can cause visitors to abandon a website before interacting with its content.</p>
<p>Modern performance optimisation goes beyond minimising JavaScript bundles. Teams must consider image optimisation, edge caching, content delivery networks, lazy loading, and server response times.</p>
<p>For example, an e-commerce website might reduce page load times by serving product images in modern formats such as WebP, implementing lazy loading for below-the-fold content, and using a CDN to deliver assets from locations closer to shoppers. These improvements often produce a more noticeable impact than migrating to a new frontend framework.</p>
<p>Many websites spend months migrating between frameworks while ignoring performance bottlenecks that would have a much larger impact on user experience. In practice, improving page speed often delivers greater business value than rebuilding an application using a different frontend stack.</p>
<p>Performance has also become increasingly important for search visibility. Search engines reward websites that provide a fast and reliable user experience. A technically impressive website that loads slowly is unlikely to achieve its full potential.</p>
<h2 id="heading-domains-and-infrastructure-still-matter"><strong>Domains and Infrastructure Still Matter</strong></h2>
<p>Developers often focus on application code while overlooking the infrastructure that supports it.</p>
<p>A website's domain remains one of its most important digital assets. Domain management affects security, reliability, and long-term brand ownership. Choosing a reputable registrar and maintaining proper DNS configuration are critical responsibilities.</p>
<p>A simple example is setting up DNS failover and enabling registrar-level security features such as domain lock and two-factor authentication. These measures help prevent outages and unauthorised domain transfers that could take a website offline.</p>
<p>For many teams, services such as <a href="https://www.namecheap.com/">Namecheap</a> and GoDaddy provide a straightforward way to manage domain registration, DNS records, SSL certificates, and related infrastructure. While these tasks may seem mundane compared to application development, they directly influence website availability and security.</p>
<p><a href="https://www.freecodecamp.org/news/how-dns-works-the-internets-address-book/">DNS performance</a> has become particularly important as websites adopt distributed architectures. Modern applications frequently rely on multiple services, APIs, content delivery networks, and edge platforms. A poorly configured DNS setup can introduce unnecessary latency and create reliability issues.</p>
<p>Infrastructure decisions also influence scalability. As traffic grows, websites must continue delivering fast and consistent experiences without requiring major architectural changes.</p>
<p>The most successful development teams treat infrastructure as a strategic asset rather than an afterthought.</p>
<h2 id="heading-hosting-is-no-longer-just-about-servers"><strong>Hosting Is No Longer Just About Servers</strong></h2>
<p>In the past, hosting primarily involved renting a server and deploying application code.</p>
<p>Today, hosting platforms offer far more than compute resources. They provide global content delivery networks, automatic scaling, integrated security features, <a href="https://www.hostinger.com/in/tutorials/best-observability-tools?utm_source=google&amp;utm_medium=cpc&amp;utm_id=11181890096&amp;utm_campaign=Generic-Tutorials-DSA-t1%7CNT:Se%7CLang:EN%7CLO:IN&amp;utm_term=&amp;utm_content=798975275269&amp;gad_source=1&amp;gad_campaignid=11181890096&amp;gbraid=0AAAAADMy-hZNKr2zB2PoiZCDVXWmMXbaA&amp;gclid=Cj0KCQjwof_QBhCgARIsADaMzOdeTB4LogkEU5Tg4r1U90UwKS3_-I-_yR5rTyGUdjeBDBoOwXaiIVgaAh2zEALw_wcB">observability tools</a>, and deployment automation.</p>
<p>The rise of edge computing has changed how websites are delivered. Content can now be served from locations close to users, reducing latency and improving responsiveness.</p>
<p>A media website experiencing a sudden traffic spike after a story goes viral can benefit from automatic scaling and edge caching, maintaining fast load times without requiring engineers to provision additional infrastructure manually.</p>
<p>Modern hosting decisions affect everything from performance and reliability to search rankings and customer satisfaction.</p>
<p>This means developers should evaluate hosting providers based on outcomes rather than specifications. Raw server resources matter less than factors such as uptime, deployment speed, geographic distribution, and operational simplicity.</p>
<p>A website that remains available during traffic spikes creates a better user experience than one that struggles under load, regardless of the underlying technology stack.</p>
<h2 id="heading-structured-data-has-become-essential"><strong>Structured Data Has Become Essential</strong></h2>
<p>One of the most overlooked aspects of modern website development is structured data.</p>
<p>Search engines and AI systems increasingly rely on structured information to understand website content. Schema markup helps machines identify products, articles, organisations, events, reviews, and many other types of information.</p>
<p>For instance, an online store can use a Product schema to display pricing and availability information in search results. At the same time, a recipe website can implement a Recipe schema to surface cooking times, ratings, and ingredients directly within search experiences.</p>
<p>Without structured data, websites force search systems to infer meaning from unstructured text. This increases the likelihood of misinterpretation.</p>
<p>Structured data improves the chances that content will appear in rich search results, featured snippets, knowledge panels, and other enhanced search experiences.</p>
<p>More importantly, structured data provides context that helps emerging AI systems understand content accurately.</p>
<p>As search evolves beyond traditional blue links, machine-readable information becomes increasingly valuable.</p>
<p>Developers who ignore structured data risk making their websites less visible, even if the content itself is excellent.</p>
<h2 id="heading-the-rise-of-ai-search-and-answer-engines"><strong>The Rise of AI Search and Answer Engines</strong></h2>
<p>Perhaps the biggest shift in website visibility is the growth of AI-powered search experiences.</p>
<p>Users increasingly ask questions directly to AI assistants rather than typing keywords into traditional search engines. These systems generate answers by combining information from multiple sources and presenting results in a conversational format.</p>
<p>This change creates new challenges for website owners.</p>
<p>Ranking on Google is no longer the only goal. Websites must also be structured in ways that help AI systems understand, retrieve, and reference their content.</p>
<p>A software company publishing detailed comparison guides, implementation tutorials, and clearly structured FAQs is more likely to be cited in AI-generated responses than a competitor relying solely on promotional landing pages.</p>
<p>This is where <a href="https://www.semrush.com/blog/answer-engine-optimization">Answer Engine Optimisation (AEO)</a> is becoming important. Unlike traditional SEO, which focuses on improving rankings in search results, AEO focuses on increasing the likelihood that content will be selected, cited, or referenced within AI-generated responses.</p>
<p>AI-powered search systems evaluate content differently from traditional search engines. Rather than simply matching keywords, they attempt to identify sources that provide clear explanations, authoritative information, and direct answers to user questions. Content that is well structured, factually accurate, and easy to interpret tends to perform better in these environments.</p>
<p>Platforms such as <a href="https://www.dirjournal.com/">DirJournal</a>, an answer engine optimisation platform, help businesses understand how their content appears across AI-driven search environments. As teams adapt to changing search behaviour, they're increasingly monitoring not only search rankings but also the frequency with which AI systems reference their brands, products, and expertise.</p>
<p>The websites that succeed in this environment are often those that publish clear, authoritative content supported by strong technical foundations.</p>
<p>In many cases, the same practices that improve traditional SEO also support AI discoverability. Fast websites, structured data, authoritative content, and clear information architecture all contribute to better visibility.</p>
<h2 id="heading-content-quality-is-more-important-than-ever"><strong>Content Quality Is More Important Than Ever</strong></h2>
<p>Technology can improve delivery, but content remains the primary reason users visit a website.</p>
<p>AI systems are becoming increasingly effective at identifying expertise, authority, and relevance. Thin content designed solely for search rankings is becoming less effective.</p>
<p>Modern websites must provide genuine value. They need original insights, practical examples, clear explanations, and trustworthy information.</p>
<p>For example, a cybersecurity vendor might publish original research on emerging threats, while a healthcare provider could create evidence-based patient guides reviewed by medical professionals. Content grounded in expertise tends to earn greater trust and visibility.</p>
<p>Developers building content-driven websites should think beyond page views and rankings. The goal is to create resources that answer real questions and solve real problems.</p>
<p>Content that demonstrates expertise is more likely to earn links, generate engagement, and be referenced by both search engines and AI systems.</p>
<p>The websites that stand out now are those that prioritize usefulness over optimization tricks.</p>
<h2 id="heading-user-experience-is-the-new-differentiator"><strong>User Experience Is the New Differentiator</strong></h2>
<p>As technology becomes more accessible, user experience becomes a larger competitive advantage.</p>
<p>Visitors expect intuitive navigation, accessible interfaces, responsive layouts, and consistent performance across devices.</p>
<p>Simple improvements such as reducing the number of checkout steps, increasing button sizes on mobile devices, or ensuring keyboard navigation works correctly can significantly improve usability and conversion rates.</p>
<p>Poor user experiences create friction that drives users away regardless of how advanced the underlying technology may be.</p>
<p><a href="https://www.freecodecamp.org/news/the-web-accessibility-handbook/">Accessibility deserves particular attention</a>. Websites should be usable by people with diverse abilities and assistive technologies. Accessibility improvements often enhance usability for all visitors while supporting compliance requirements.</p>
<p>The best websites combine technical excellence with thoughtful design. They remove obstacles and help users accomplish their goals quickly and efficiently.</p>
<h2 id="heading-the-future-is-about-outcomes-not-frameworks"><strong>The Future Is About Outcomes, Not Frameworks</strong></h2>
<p>The web development industry has reached a point where most modern frameworks are capable of delivering excellent results.</p>
<p>The real challenge is no longer choosing the perfect technology stack.</p>
<p>Success depends on building websites that are fast, discoverable, reliable, secure, and understandable to both humans and machines. Performance optimization, domain management, hosting strategy, structured data, content quality, and AI search visibility now play a larger role in determining outcomes.</p>
<p>These days, the websites that succeed aren't necessarily built with the newest technologies. They're built with the strongest foundations.</p>
<p>Developers who focus on those foundations will create websites that continue to perform well regardless of how search engines, AI systems, or frontend frameworks evolve in the years ahead.</p>
<p>Hope you enjoyed this article. You can <a href="https://linkedin.com/in/manishmshiva">connect with me on LinkedIn</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
