Build a Bookmark Manager with Laravel
Build a full-stack bookmark manager with Laravel, React, and Inertia.js.
Introduction
30 Second Summary
You probably have dozens of browser tabs open right now with links you want to save for later. Bookmarks scattered across browsers, note apps, and messaging threads are hard to find when you actually need them.
In this project, you will build a personal bookmark manager app with a Laravel backend and a React frontend. You will create, edit, and delete bookmarks through a clean interface powered by a MySQL database.
What You'll Build
A full-stack bookmark manager application where Inertia.js connects your Laravel controllers directly to React page components, giving you one unified project instead of separate frontend and backend servers.
By the end of this project, you'll have:
- A Laravel 13 project with the React starter kit that includes authentication, layouts, and Tailwind CSS styling.
- A MySQL database with migrations and an Eloquent model for storing bookmarks.
- A complete CRUD resource controller and interactive React page components for listing, creating, editing, and deleting bookmarks.
- Secret Mission: Add a real-time search bar and category filter to find bookmarks instantly.
Are there any prerequisites?
You should have a basic understanding of HTML, CSS, JavaScript, and PHP. You also need XAMPP installed with MySQL running on your machine.
Set Up Your Development Environment
Every web app needs two things: a backend to handle data and logic, and a frontend to display it to users. In this project, you will wire those two halves together using Laravel for the backend and React for the frontend.
Before you start building, you need a fresh Laravel project configured to talk to your local MySQL database. In this step, you will create a new Laravel 13 project with the React starter kit, connect it to a MySQL database, and verify everything runs.
In this step, get ready to:
- Create a new Laravel project with the React starter kit.
- Create a MySQL database and connect it to your project.
- Run database migrations and start the development server.
Create a new Laravel project with the React starter kit
Laravel's installer creates a full project scaffold in one command. You will use the --react flag to skip the interactive starter kit prompt and go straight to the React starter kit.
- Open the terminal in VS Code by pressing Ctrl+` (backtick key).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your Laravel project by running this command:
laravel new bookmark-manager --react --database=mysql
What does this command do?
The laravel new command scaffolds a full Laravel application. The --react flag installs the React starter kit, which includes Inertia.js, React 19, Tailwind CSS, and authentication scaffolding.
The --database=mysql flag tells Laravel to configure .env for MySQL instead of the default SQLite.
The installer will ask you several interactive questions. Answer each one as follows:
- When asked about the authentication provider, select Laravel's built-in authentication.
- When asked about the testing framework, select Pest.
- When asked whether to run the default database migrations, select No. You will run migrations after configuring the database connection.
- When asked whether to run npm install and npm run build, select Yes.
- When asked about Laravel Boost, select No.
Why skip migrations for now?
The installer set DB_DATABASE=bookmark_manager in your .env file, but that database does not exist in MySQL yet. Running migrations now would fail. You will create the database first, then migrate.
- Once the installer finishes, move into your new project folder by running:
cd bookmark-manager
- Confirm the project files exist by running:
ls
You should see folders like app, resources, routes, and files like artisan, composer.json, and .env.
Command not found?
If you see an error like "laravel: command not found", the Laravel installer is not in your system path. Run composer global require laravel/installer and try again.
If you see a PHP version error, confirm your PHP version is 8.3 or higher by running php -v.
Help me fix a Laravel installer error.
Create the MySQL database and configure the connection
Laravel expects a database to already exist before it can run migrations. You will create one in phpMyAdmin, the web-based database management tool that comes with XAMPP.
- Make sure both Apache and MySQL are running in the XAMPP Control Panel.
- Open your web browser and navigate to:
http://localhost/phpmyadmin
- In phpMyAdmin, click New in the left sidebar.
- In the Database name field, type bookmark_manager.
- Click Create.
You should see bookmark_manager appear in the left sidebar under the list of databases.
What is phpMyAdmin?
phpMyAdmin is a web interface for managing MySQL databases. It comes bundled with XAMPP and lets you create databases, browse tables, and run SQL queries without using the command line.
Now you need to make sure your Laravel project's .env file points to this new database.
- Back in VS Code, open the .env file in the root of your bookmark-manager folder.
- Find the database configuration section and confirm it looks like this:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=bookmark_manager
DB_USERNAME=root
DB_PASSWORD=
What do these values mean?
- DB_CONNECTION=mysql tells Laravel to use the MySQL driver.
- DB_HOST=127.0.0.1 points to your local machine where XAMPP's MySQL is running.
- DB_PORT=3306 is the standard MySQL port.
- DB_DATABASE=bookmark_manager is the database you just created.
- DB_USERNAME=root and DB_PASSWORD= are XAMPP's default MySQL credentials (empty password).
Changed your XAMPP password?
If you set a root password in XAMPP, update DB_PASSWORD to match. The value must match whatever credentials phpMyAdmin uses to log in.
- Save the .env file.
Can't access phpMyAdmin?
Make sure Apache is running in the XAMPP Control Panel. phpMyAdmin is served by Apache, so MySQL alone is not enough.
If port 80 is in use by another application, try http://localhost:8080/phpmyadmin instead.
Help me access phpMyAdmin in XAMPP.
Run migrations and start the dev server
Migrations are Laravel's way of creating database tables using PHP code instead of writing raw SQL. The starter kit ships with migrations for users, sessions, and other authentication tables.
- In the VS Code terminal, make sure you are inside the bookmark-manager folder.
- Run the database migrations by running this command:
php artisan migrate
What does this command do?
The php artisan migrate command reads migration files from database/migrations/ and creates the corresponding tables in your MySQL database. The default migrations create tables for users, password reset tokens, sessions, cache, and jobs.
- You should see output confirming each migration ran successfully, similar to:
Running migrations.
0001_01_01_000000_create_users_table .............. DONE
0001_01_01_000001_create_cache_table .............. DONE
0001_01_01_000002_create_jobs_table ............... DONE
Seeing a connection refused error?
This means Laravel cannot connect to MySQL. Double check that MySQL is running in the XAMPP Control Panel (it should show a green status).
Also verify your .env has DB_HOST=127.0.0.1 and DB_PORT=3306. If you changed XAMPP's MySQL port, update DB_PORT to match.
Help me fix my Laravel database connection.
Now start the development server so you can see your app in the browser.
- Start the Laravel development server by running:
composer run dev
What does composer run dev do?
This single command starts three processes at once: the Laravel PHP server, a queue worker, and the Vite development server for your React frontend. It is the recommended way to run your app locally during development.
- Open your browser and navigate to http://localhost:8000.
You should see the Laravel welcome page with links to Log in and Register in the top-right corner. This confirms your Laravel + React app is running correctly with authentication ready to go.
Page not loading?
Make sure composer run dev is still running in your terminal. If you closed the terminal, run the command again from the bookmark-manager folder.
If port 8000 is already in use by another process, stop that process first or check if your app started on a different port in the terminal output.
Help me get my Laravel dev server running.
Your Laravel + React project is fully set up and connected to MySQL. Next up, you will create the bookmarks database table and the Eloquent model that represents it.
Create the Bookmarks Database Table and Model
Your Laravel project is running with a MySQL database connected. But that database only has the default tables that come with Laravel (users, sessions, and so on).
To store bookmarks, you need a dedicated table in your database and a PHP class that represents a bookmark. In this step, you will create both using Laravel's built-in tools.
In this step, get ready to:
- Generate and configure a database migration for the bookmarks table.
- Generate and configure the Bookmark Eloquent model.
- Run the migration to create the bookmarks table in MySQL.
Generate the bookmarks migration file
A migration is a PHP file that describes a change to your database schema. Instead of creating tables manually in phpMyAdmin, you define them in code. This makes your database portable and version-controlled.
- Open a new terminal in VS Code by pressing Ctrl+` (backtick key, usually above Tab).
- Generate the migration file by running this command:
php artisan make:migration create_bookmarks_table
What does this command do?
The make:migration Artisan command generates a new migration file inside database/migrations/. The filename includes a timestamp so Laravel knows the order to run migrations.
Because the name follows the pattern create_bookmarks_table, Laravel automatically pre-fills the migration with a Schema::create('bookmarks', ...) call.
- You should see a confirmation message in your terminal with the path to the new file.
- Open the newly created migration file in database/migrations/. It will have a timestamp prefix and end with _create_bookmarks_table.php.
- Replace the entire contents of the file with the following code:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('bookmarks', function (Blueprint $table) {
// Auto-incrementing primary key
$table->id();
// Foreign key linking each bookmark to a user
$table->foreignId('user_id')->constrained()->onDelete('cascade');
// The bookmark's display name
$table->string('title');
// The web address being saved
$table->string('url');
// Optional notes about the bookmark
$table->text('description')->nullable();
// Optional grouping label
$table->string('category')->nullable();
// created_at and updated_at columns
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('bookmarks');
}
};
What does this code do?
The up() method defines what happens when you run the migration. It creates a bookmarks table with columns for each piece of data a bookmark needs.
The foreignId('user_id')->constrained()->onDelete('cascade') line links each bookmark to a user. If that user is deleted, all their bookmarks are automatically deleted too.
The down() method reverses the migration by dropping the table entirely. This is useful if you ever need to undo the change.
- Save the migration file.
Not sure which file to open?
Look in the database/migrations/ folder in the VS Code Explorer sidebar. The file you just generated will be the most recent one (highest timestamp). It ends with _create_bookmarks_table.php.
Help me find my migration file in the database/migrations folder.
Generate the Bookmark model
An Eloquent model is a PHP class that represents a database table. It lets you interact with your bookmarks table using PHP objects instead of writing raw SQL queries.
- Generate the Bookmark model by running this command:
php artisan make:model Bookmark
What does this command do?
This creates a new file at app/Models/Bookmark.php. Laravel automatically maps the Bookmark model to the bookmarks table (pluralizing the model name).
- Open app/Models/Bookmark.php in the VS Code Explorer sidebar.
- Replace the entire contents of the file with the following code:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Bookmark extends Model
{
// Allow these fields to be set when creating or updating a bookmark
protected $fillable = [
'title',
'url',
'description',
'category',
'user_id',
];
// Each bookmark belongs to one user
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
What does this code do?
The $fillable array lists which columns can be filled when you create or update a bookmark. This is a security feature called mass assignment protection that prevents unexpected fields from being saved.
The user() method defines the relationship between a bookmark and its owner. It tells Laravel that each bookmark belongs to one user. This is the inverse side of the one-to-many relationship.
- Save app/Models/Bookmark.php.
Seeing a red underline on User::class?
This is normal. The User class is in the same App\Models namespace, so PHP resolves it automatically. Your code editor might show a warning, but it will work correctly when you run the app.
Why does my editor show an error for User::class in my Bookmark model?
✔️ Awesome, I've got everything!
Great. Double check you have saved both files.
ⓧ I'd like to double check the full code
Here is the complete migration file:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('bookmarks', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->string('url');
$table->text('description')->nullable();
$table->string('category')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('bookmarks');
}
};
Here is the complete Bookmark model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Bookmark extends Model
{
protected $fillable = [
'title',
'url',
'description',
'category',
'user_id',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
Run the migration
The migration file only describes the table. You still need to execute it against your database so the actual table gets created in MySQL.
- Run the migration by executing this command in your terminal:
php artisan migrate
- You should see output confirming that the bookmarks table was created.
What should I see?
Your terminal will show a line like Running migrations followed by the name of your migration file and a DONE confirmation. This means the bookmarks table now exists in your bookmark_manager database.
Seeing a connection refused error?
Make sure XAMPP is running and MySQL is active. Check the XAMPP Control Panel and confirm the MySQL module shows a green status.
Also verify your .env file has DB_CONNECTION=mysql and DB_DATABASE=bookmark_manager. The database name must match exactly what you created earlier.
Help me fix a database connection error when running php artisan migrate.
Your database is ready. The bookmarks table is waiting for data. Next up, you will build the backend logic that creates, reads, updates, and deletes bookmarks.
Build the Backend (Controller and Routes)
Your database and model are ready. Now you need a way for the app to handle incoming requests from the browser.
In this step, you will create a controller that handles all the bookmark operations (listing, creating, editing, deleting) and register routes that map URLs to those controller methods. Laravel calls this pattern a resource controller because it gives you a complete set of CRUD actions in one class.
In this step, get ready to:
- Generate a resource controller with all CRUD method stubs.
- Register resource routes behind authentication middleware.
- Implement each controller method to handle bookmarks with validation and authorization.
Generate a resource controller
A resource controller is a class that provides methods for each standard action: index (list), create (show form), store (save new), edit (show edit form), update (save changes), and destroy (delete). Laravel's artisan command generates all these method stubs for you.
- Open a new terminal in VS Code by pressing Ctrl+` (macOS and Windows).
- Generate the BookmarkController by running this command:
php artisan make:controller BookmarkController --resource
What does this command do?
The --resource flag tells artisan to scaffold a controller with empty method stubs for index, create, store, show, edit, update, and destroy.
You will fill in these methods with your own logic in a later substep.
- Confirm the file was created by running:
ls app/Http/Controllers/BookmarkController.php
You should see the file path printed back to you, confirming the controller exists.
Command not found?
Make sure your terminal is in the bookmark-manager project folder. Run cd bookmark-manager if needed.
Help me fix my artisan command
Register resource routes in the web routes file
Routes connect URLs to controller methods. A resource route registers all seven standard routes (GET, POST, PUT, DELETE) for your controller in a single line.
- Open routes/web.php in the VS Code Explorer sidebar.
- Replace the entire file contents with this code:
<?php
use App\Http\Controllers\BookmarkController;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::get('/', function () {
return Inertia::render('welcome');
})->name('home');
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('dashboard', function () {
return Inertia::render('dashboard');
})->name('dashboard');
// Register all bookmark CRUD routes
Route::resource('bookmarks', BookmarkController::class);
});
require __DIR__.'/settings.php';
require __DIR__.'/auth.php';
What does this code do?
The Route::resource('bookmarks', BookmarkController::class) line registers routes like /bookmarks (index), /bookmarks/create (create form), /bookmarks/{bookmark} (show/update/delete), and more.
Wrapping it inside Route::middleware(['auth', 'verified']) means only logged-in users can access these URLs. Unauthenticated visitors get redirected to the login page.
- Save routes/web.php.
- Verify the routes are registered by running:
php artisan route:list --path=bookmarks
You should see a table listing routes like GET /bookmarks, POST /bookmarks, GET /bookmarks/create, and others pointing to BookmarkController methods.
Seeing a class not found error?
Check that the use App\Http\Controllers\BookmarkController; import at the top of routes/web.php matches the controller's namespace exactly.
Help me debug my route registration
Implement the controller methods
Your controller currently has empty method stubs. Now you will fill each method with the logic to list, create, edit, and delete bookmarks using Inertia.js to render React pages.
Before implementing the controller, you need to tell the User model how to find its bookmarks. The controller calls $request->user()->bookmarks() which requires a hasMany relationship on the User model.
- Open app/Models/User.php in VS Code.
- Add this import near the top of the file, below the existing use statements:
use Illuminate\Database\Eloquent\Relations\HasMany;
- Scroll down to the bottom of the User class (just before the closing }) and add this method:
public function bookmarks(): HasMany
{
return $this->hasMany(Bookmark::class);
}
- Also add this import near the top of the file with the other use statements:
use App\Models\Bookmark;
What is a hasMany relationship?
This tells Eloquent that one User can own many Bookmarks. Laravel uses the user_id column in the bookmarks table to connect each bookmark to its owner.
Once defined, you can call $user->bookmarks() to query only that user's bookmarks.
- Save app/Models/User.php.
Now open app/Http/Controllers/BookmarkController.php and replace its entire contents. You will build the file up in three parts.
- Replace the file contents with the imports, class declaration, and the first two read methods by pasting this code:
<?php
namespace App\Http\Controllers;
use App\Models\Bookmark;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class BookmarkController extends Controller
{
public function index(Request $request): Response
{
// Fetch all bookmarks for the logged-in user, newest first
$bookmarks = $request->user()->bookmarks()->latest()->get();
return Inertia::render('bookmarks/index', [
'bookmarks' => $bookmarks,
]);
}
public function create(): Response
{
// Render the "add bookmark" form page
return Inertia::render('bookmarks/create');
}
What do index and create do?
The index method fetches the current user's bookmarks (sorted newest first) and passes them to a React page component at bookmarks/index.
The create method simply renders the bookmarks/create React page. No data is needed because the form starts empty.
- Add the store method directly below the create method:
public function store(Request $request): RedirectResponse
{
// Validate the incoming form data
$validated = $request->validate([
'title' => 'required|string|max:255',
'url' => 'required|url|max:255',
'description' => 'nullable|string|max:1000',
'category' => 'nullable|string|max:100',
]);
// Create a new bookmark owned by the current user
$request->user()->bookmarks()->create($validated);
return redirect()->route('bookmarks.index');
}
What does store do?
The store method handles the form submission. It validates that title and url are present and that url is a valid web address.
After validation passes, it creates a new bookmark attached to the logged-in user via the bookmarks() relationship. Then it redirects back to the bookmarks list.
- Add the edit, update, and destroy methods below store, then close the class with a }:
public function edit(Bookmark $bookmark): Response
{
// Only the bookmark owner can edit
$this->authorize('update', $bookmark);
return Inertia::render('bookmarks/edit', [
'bookmark' => $bookmark,
]);
}
public function update(Request $request, Bookmark $bookmark): RedirectResponse
{
// Only the bookmark owner can update
$this->authorize('update', $bookmark);
$validated = $request->validate([
'title' => 'required|string|max:255',
'url' => 'required|url|max:255',
'description' => 'nullable|string|max:1000',
'category' => 'nullable|string|max:100',
]);
$bookmark->update($validated);
return redirect()->route('bookmarks.index');
}
public function destroy(Bookmark $bookmark): RedirectResponse
{
// Only the bookmark owner can delete
$this->authorize('delete', $bookmark);
$bookmark->delete();
return redirect()->route('bookmarks.index');
}
}
What do edit, update, and destroy do?
Each of these methods starts with $this->authorize() to check that the logged-in user owns the bookmark. This prevents one user from editing or deleting another user's bookmarks.
The edit method passes the existing bookmark data to the edit form. The update method validates and saves changes. The destroy method deletes the bookmark and redirects home.
- Save app/Http/Controllers/BookmarkController.php.
The authorize calls require a policy class. Laravel uses policies to define who can perform each action on a model.
- Generate the BookmarkPolicy by running this command:
php artisan make:policy BookmarkPolicy --model=Bookmark
- Open app/Policies/BookmarkPolicy.php and replace its entire contents with:
<?php
namespace App\Policies;
use App\Models\Bookmark;
use App\Models\User;
class BookmarkPolicy
{
public function update(User $user, Bookmark $bookmark): bool
{
// Only the owner can update this bookmark
return $user->id === $bookmark->user_id;
}
public function delete(User $user, Bookmark $bookmark): bool
{
// Only the owner can delete this bookmark
return $user->id === $bookmark->user_id;
}
}
What is a policy?
A policy is a class that groups authorization logic for a specific model. Laravel auto-discovers policies by naming convention. BookmarkPolicy is automatically used for the Bookmark model.
Each method returns true or false. Here, both update and delete check that the user's ID matches the bookmark's user_id. If the check fails, Laravel returns a 403 Forbidden response.
- Save app/Policies/BookmarkPolicy.php.
- Verify the whole setup works by running:
php artisan route:list --path=bookmarks
You should see a full table of bookmark routes. The backend is wired up.
Seeing an authorization error when you test later?
Make sure BookmarkPolicy.php is in the app/Policies/ folder. Laravel auto-discovers policies by naming convention. If the file is named differently or in the wrong folder, the authorize calls will fail.
Help me debug my BookmarkPolicy
✔️ Awesome, I've got everything!
Great. Double check you have saved all three files: routes/web.php, BookmarkController.php, and BookmarkPolicy.php.
ⓧ I'd like to double check the full code
Here is the complete code for each file at this point:
routes/web.php:
<?php
use App\Http\Controllers\BookmarkController;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::get('/', function () {
return Inertia::render('welcome');
})->name('home');
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('dashboard', function () {
return Inertia::render('dashboard');
})->name('dashboard');
Route::resource('bookmarks', BookmarkController::class);
});
require __DIR__.'/settings.php';
require __DIR__.'/auth.php';
app/Http/Controllers/BookmarkController.php:
<?php
namespace App\Http\Controllers;
use App\Models\Bookmark;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class BookmarkController extends Controller
{
public function index(Request $request): Response
{
$bookmarks = $request->user()->bookmarks()->latest()->get();
return Inertia::render('bookmarks/index', [
'bookmarks' => $bookmarks,
]);
}
public function create(): Response
{
return Inertia::render('bookmarks/create');
}
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'url' => 'required|url|max:255',
'description' => 'nullable|string|max:1000',
'category' => 'nullable|string|max:100',
]);
$request->user()->bookmarks()->create($validated);
return redirect()->route('bookmarks.index');
}
public function edit(Bookmark $bookmark): Response
{
$this->authorize('update', $bookmark);
return Inertia::render('bookmarks/edit', [
'bookmark' => $bookmark,
]);
}
public function update(Request $request, Bookmark $bookmark): RedirectResponse
{
$this->authorize('update', $bookmark);
$validated = $request->validate([
'title' => 'required|string|max:255',
'url' => 'required|url|max:255',
'description' => 'nullable|string|max:1000',
'category' => 'nullable|string|max:100',
]);
$bookmark->update($validated);
return redirect()->route('bookmarks.index');
}
public function destroy(Bookmark $bookmark): RedirectResponse
{
$this->authorize('delete', $bookmark);
$bookmark->delete();
return redirect()->route('bookmarks.index');
}
}
app/Policies/BookmarkPolicy.php:
<?php
namespace App\Policies;
use App\Models\Bookmark;
use App\Models\User;
class BookmarkPolicy
{
public function update(User $user, Bookmark $bookmark): bool
{
return $user->id === $bookmark->user_id;
}
public function delete(User $user, Bookmark $bookmark): bool
{
return $user->id === $bookmark->user_id;
}
}
Your backend is complete. The controller can list, create, edit, and delete bookmarks, and the policy ensures users can only touch their own data. Next up, you will build the React pages that display these bookmarks in the browser.
Build the React Frontend (List and Create Pages)
Your backend is fully wired up. The controller knows how to fetch, create, and manage bookmarks, and the routes are ready to handle requests.
Now you need the pages that users actually see and interact with. In this step, you'll create React page components that Inertia.js connects directly to your Laravel controller methods.
In this step, get ready to:
- Create the Index page component to display all bookmarks.
- Create the Create page component with a form for adding bookmarks.
- Verify navigation links between the bookmark pages.
Create the bookmarks Index page
Your BookmarkController index method renders a page called bookmarks/index. You need to create the matching React component at that path.
- Create the bookmarks folder inside resources/js/pages/ by running this command in your VS Code terminal:
mkdir resources/js/pages/bookmarks
Why a new folder?
The starter kit already has a resources/js/pages/ directory with pages like dashboard.tsx. Grouping your bookmark pages in a subfolder keeps things organized as your app grows.
- Create a new file called index.tsx inside resources/js/pages/bookmarks/ and paste the following code:
import { Head, Link } from '@inertiajs/react';
import AppLayout from '@/layouts/app-layout';
// Define the shape of a bookmark object from the database
interface Bookmark {
id: number;
title: string;
url: string;
description: string | null;
category: string | null;
created_at: string;
}
// Props passed from the BookmarkController's index method
interface Props {
bookmarks: Bookmark[];
}
export default function Index({ bookmarks }: Props) {
return (
<AppLayout>
<Head title="My Bookmarks" />
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">My Bookmarks</h1>
<Link
href="/bookmarks/create"
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
Add Bookmark
</Link>
</div>
{bookmarks.length === 0 ? (
<p className="text-gray-500">No bookmarks yet. Add your first one!</p>
) : (
<div className="grid gap-4">
{bookmarks.map((bookmark) => (
<div key={bookmark.id} className="border rounded-lg p-4 bg-white shadow-sm">
<div className="flex justify-between items-start">
<div>
<h2 className="text-lg font-semibold">
<a href={bookmark.url} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
{bookmark.title}
</a>
</h2>
<p className="text-sm text-gray-500 mt-1">{bookmark.url}</p>
{bookmark.description && (
<p className="text-gray-700 mt-2">{bookmark.description}</p>
)}
{bookmark.category && (
<span className="inline-block mt-2 bg-gray-100 text-gray-600 text-xs px-2 py-1 rounded">
{bookmark.category}
</span>
)}
</div>
<div className="flex gap-2">
<Link
href={`/bookmarks/${bookmark.id}/edit`}
className="text-sm text-blue-600 hover:underline"
>
Edit
</Link>
<Link
href={`/bookmarks/${bookmark.id}`}
method="delete"
as="button"
className="text-sm text-red-600 hover:underline"
>
Delete
</Link>
</div>
</div>
</div>
))}
</div>
)}
</div>
</AppLayout>
);
}
What does this code do?
This component receives the bookmarks array as a prop from your BookmarkController@index method via Inertia.
- The Bookmark interface defines the TypeScript types for the data coming from your database.
- The AppLayout wrapper provides the authenticated page shell (sidebar, navigation) from the starter kit.
- Each bookmark displays as a card with its title linked to the external URL, plus Edit and Delete actions.
- The Link component from Inertia handles navigation without full page reloads.
- Save index.tsx.
- In your browser, navigate to http://localhost:8000/bookmarks (log in if prompted). You should see the heading "My Bookmarks" with an "Add Bookmark" button and the message "No bookmarks yet."
Seeing a 404 or blank page?
- Make sure the file is at exactly resources/js/pages/bookmarks/index.tsx (lowercase bookmarks). The path must match what Inertia::render('bookmarks/index') expects.
- Confirm your dev server is still running. If you stopped it, run composer run dev again.
- Make sure you are logged in. The bookmarks routes require authentication.
Why can't I see my bookmarks page?
Create the bookmarks Create page
The "Add Bookmark" button links to /bookmarks/create. Your controller's create method renders bookmarks/create, so you need a matching component with a form.
- Create a new file called create.tsx inside resources/js/pages/bookmarks/ and paste the following code:
import { Head, Link, useForm } from '@inertiajs/react';
import AppLayout from '@/layouts/app-layout';
export default function Create() {
// useForm manages form state, submission, and error handling
const { data, setData, post, processing, errors } = useForm({
title: '',
url: '',
description: '',
category: '',
});
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
// POST to /bookmarks triggers BookmarkController@store
post('/bookmarks');
}
return (
<AppLayout>
<Head title="Add Bookmark" />
<div className="p-6 max-w-2xl">
<h1 className="text-2xl font-bold mb-6">Add Bookmark</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700">
Title
</label>
<input
id="title"
type="text"
value={data.title}
onChange={(e) => setData('title', e.target.value)}
className="mt-1 block w-full border border-gray-300 rounded px-3 py-2"
/>
{errors.title && <p className="text-red-600 text-sm mt-1">{errors.title}</p>}
</div>
<div>
<label htmlFor="url" className="block text-sm font-medium text-gray-700">
URL
</label>
<input
id="url"
type="url"
value={data.url}
onChange={(e) => setData('url', e.target.value)}
className="mt-1 block w-full border border-gray-300 rounded px-3 py-2"
/>
{errors.url && <p className="text-red-600 text-sm mt-1">{errors.url}</p>}
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
Description (optional)
</label>
<textarea
id="description"
value={data.description}
onChange={(e) => setData('description', e.target.value)}
rows={3}
className="mt-1 block w-full border border-gray-300 rounded px-3 py-2"
/>
{errors.description && <p className="text-red-600 text-sm mt-1">{errors.description}</p>}
</div>
<div>
<label htmlFor="category" className="block text-sm font-medium text-gray-700">
Category (optional)
</label>
<input
id="category"
type="text"
value={data.category}
onChange={(e) => setData('category', e.target.value)}
className="mt-1 block w-full border border-gray-300 rounded px-3 py-2"
/>
{errors.category && <p className="text-red-600 text-sm mt-1">{errors.category}</p>}
</div>
<div className="flex gap-4">
<button
type="submit"
disabled={processing}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
Save Bookmark
</button>
<Link href="/bookmarks" className="text-gray-600 px-4 py-2 hover:underline">
Cancel
</Link>
</div>
</form>
</div>
</AppLayout>
);
}
What does this code do?
This component uses Inertia's useForm hook to manage the entire form lifecycle.
- The data object holds the current form values. setData updates them as the user types.
- Calling post('/bookmarks') sends a POST request to your BookmarkController@store method with the form data.
- The errors object automatically populates with Laravel validation errors. Each field displays its error message below the input.
- The processing boolean disables the submit button while the request is in flight, preventing double submissions.
- Save create.tsx.
- In your browser, click the **Add Bookmark** button on the bookmarks page. You should see the "Add Bookmark" form with fields for Title, URL, Description, and Category.
Form not showing up?
- Confirm the file is named create.tsx (not Create.tsx) inside the bookmarks folder.
- Check your browser console for any TypeScript or import errors. A typo in the AppLayout import path will cause a blank page.
Why is my create page not rendering?
Verify the navigation links
Both pages include Inertia Link components that connect them together. Time to test the full flow.
- On the "Add Bookmark" form, fill in a test bookmark:
- Enter NextWork in the **Title** field.
- Enter https://www.nextwork.org in the **URL** field.
- Enter Learn cloud and DevOps in the **Description** field.
- Enter Learning in the **Category** field.
- Click **Save Bookmark**.
After submitting, Inertia redirects you back to the Index page. You should see your new bookmark displayed as a card with the title linked to the URL, the description below it, and a "Learning" category badge.
- Click the **Cancel** link on the create page (navigate back to the form first by clicking **Add Bookmark** again). It should take you back to the bookmarks list without saving.
What is Inertia doing behind the scenes?
When you submit the form, Inertia sends an XHR request (not a full page reload) to your Laravel controller. The controller validates the data, saves the bookmark, and returns a redirect. Inertia intercepts that redirect and fetches the new page data via another XHR, then swaps the React component on screen.
This gives you the speed of a single-page app with the simplicity of server-side controllers.
Bookmark not saving?
- If you see red validation errors under the fields, check that you entered a valid URL (must start with http:// or https://).
- If nothing happens on submit, open your browser's DevTools Network tab and look for a failed POST request to /bookmarks. A 500 error means a server-side issue in your controller.
- Make sure XAMPP is running and your database connection in .env is correct.
Why won't my bookmark save to the database?
✔️ Awesome, I've got everything!
Great. Double check you have saved both files before moving on.
ⓧ I'd like to double check the full code
Here is the complete code for both bookmark page components:
import { Head, Link } from '@inertiajs/react';
import AppLayout from '@/layouts/app-layout';
interface Bookmark {
id: number;
title: string;
url: string;
description: string | null;
category: string | null;
created_at: string;
}
interface Props {
bookmarks: Bookmark[];
}
export default function Index({ bookmarks }: Props) {
return (
<AppLayout>
<Head title="My Bookmarks" />
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">My Bookmarks</h1>
<Link
href="/bookmarks/create"
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
Add Bookmark
</Link>
</div>
{bookmarks.length === 0 ? (
<p className="text-gray-500">No bookmarks yet. Add your first one!</p>
) : (
<div className="grid gap-4">
{bookmarks.map((bookmark) => (
<div key={bookmark.id} className="border rounded-lg p-4 bg-white shadow-sm">
<div className="flex justify-between items-start">
<div>
<h2 className="text-lg font-semibold">
<a href={bookmark.url} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
{bookmark.title}
</a>
</h2>
<p className="text-sm text-gray-500 mt-1">{bookmark.url}</p>
{bookmark.description && (
<p className="text-gray-700 mt-2">{bookmark.description}</p>
)}
{bookmark.category && (
<span className="inline-block mt-2 bg-gray-100 text-gray-600 text-xs px-2 py-1 rounded">
{bookmark.category}
</span>
)}
</div>
<div className="flex gap-2">
<Link
href={`/bookmarks/${bookmark.id}/edit`}
className="text-sm text-blue-600 hover:underline"
>
Edit
</Link>
<Link
href={`/bookmarks/${bookmark.id}`}
method="delete"
as="button"
className="text-sm text-red-600 hover:underline"
>
Delete
</Link>
</div>
</div>
</div>
))}
</div>
)}
</div>
</AppLayout>
);
}
import { Head, Link, useForm } from '@inertiajs/react';
import AppLayout from '@/layouts/app-layout';
export default function Create() {
const { data, setData, post, processing, errors } = useForm({
title: '',
url: '',
description: '',
category: '',
});
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
post('/bookmarks');
}
return (
<AppLayout>
<Head title="Add Bookmark" />
<div className="p-6 max-w-2xl">
<h1 className="text-2xl font-bold mb-6">Add Bookmark</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700">
Title
</label>
<input
id="title"
type="text"
value={data.title}
onChange={(e) => setData('title', e.target.value)}
className="mt-1 block w-full border border-gray-300 rounded px-3 py-2"
/>
{errors.title && <p className="text-red-600 text-sm mt-1">{errors.title}</p>}
</div>
<div>
<label htmlFor="url" className="block text-sm font-medium text-gray-700">
URL
</label>
<input
id="url"
type="url"
value={data.url}
onChange={(e) => setData('url', e.target.value)}
className="mt-1 block w-full border border-gray-300 rounded px-3 py-2"
/>
{errors.url && <p className="text-red-600 text-sm mt-1">{errors.url}</p>}
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
Description (optional)
</label>
<textarea
id="description"
value={data.description}
onChange={(e) => setData('description', e.target.value)}
rows={3}
className="mt-1 block w-full border border-gray-300 rounded px-3 py-2"
/>
{errors.description && <p className="text-red-600 text-sm mt-1">{errors.description}</p>}
</div>
<div>
<label htmlFor="category" className="block text-sm font-medium text-gray-700">
Category (optional)
</label>
<input
id="category"
type="text"
value={data.category}
onChange={(e) => setData('category', e.target.value)}
className="mt-1 block w-full border border-gray-300 rounded px-3 py-2"
/>
{errors.category && <p className="text-red-600 text-sm mt-1">{errors.category}</p>}
</div>
<div className="flex gap-4">
<button
type="submit"
disabled={processing}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
Save Bookmark
</button>
<Link href="/bookmarks" className="text-gray-600 px-4 py-2 hover:underline">
Cancel
</Link>
</div>
</form>
</div>
</AppLayout>
);
}
Your frontend is taking shape. Users can now view their bookmarks and add new ones. Next up, you'll add the ability to edit and delete bookmarks to complete the full CRUD experience.
Add Edit and Delete Functionality
Your bookmark manager can list and create bookmarks. Now it's time to close the loop by adding the ability to edit and delete them.
In this step, you'll create the Edit page component that lets users update their saved bookmarks. The good news is that your index page already has the Edit and Delete buttons wired up from the previous step. You just need the Edit page to land on.
In this step, get ready to:
- Create the Edit page component with pre-filled form data.
- Understand how the existing edit and delete buttons work.
- Test the complete CRUD flow in the browser.
Create the Edit page component
The Edit page is nearly identical to the Create page, with two key differences. It receives the existing bookmark data as a prop and pre-fills the form fields. It also submits a PUT request instead of a POST request, because you're updating an existing resource.
- Create a new file at resources/js/pages/bookmarks/edit.tsx by right-clicking the bookmarks folder in the VS Code Explorer sidebar and selecting New File.
- Paste the following code into edit.tsx:
import { Head, Link, useForm } from '@inertiajs/react';
import AppLayout from '@/layouts/app-layout';
// Define the shape of bookmark data passed from the controller
interface Bookmark {
id: number;
title: string;
url: string;
description: string | null;
category: string | null;
}
// Props received from the BookmarkController edit method
interface Props {
bookmark: Bookmark;
}
export default function Edit({ bookmark }: Props) {
// Initialize form with existing bookmark data
const { data, setData, put, processing, errors } = useForm({
title: bookmark.title,
url: bookmark.url,
description: bookmark.description || '',
category: bookmark.category || '',
});
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
// Send a PUT request to update the specific bookmark
put(`/bookmarks/${bookmark.id}`);
}
return (
<AppLayout>
<Head title="Edit Bookmark" />
<div className="p-6 max-w-2xl">
<h1 className="text-2xl font-bold mb-6">Edit Bookmark</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700">
Title
</label>
<input
id="title"
type="text"
value={data.title}
onChange={(e) => setData('title', e.target.value)}
className="mt-1 block w-full border border-gray-300 rounded px-3 py-2"
/>
{errors.title && <p className="text-red-600 text-sm mt-1">{errors.title}</p>}
</div>
<div>
<label htmlFor="url" className="block text-sm font-medium text-gray-700">
URL
</label>
<input
id="url"
type="url"
value={data.url}
onChange={(e) => setData('url', e.target.value)}
className="mt-1 block w-full border border-gray-300 rounded px-3 py-2"
/>
{errors.url && <p className="text-red-600 text-sm mt-1">{errors.url}</p>}
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
Description (optional)
</label>
<textarea
id="description"
value={data.description}
onChange={(e) => setData('description', e.target.value)}
rows={3}
className="mt-1 block w-full border border-gray-300 rounded px-3 py-2"
/>
{errors.description && <p className="text-red-600 text-sm mt-1">{errors.description}</p>}
</div>
<div>
<label htmlFor="category" className="block text-sm font-medium text-gray-700">
Category (optional)
</label>
<input
id="category"
type="text"
value={data.category}
onChange={(e) => setData('category', e.target.value)}
className="mt-1 block w-full border border-gray-300 rounded px-3 py-2"
/>
{errors.category && <p className="text-red-600 text-sm mt-1">{errors.category}</p>}
</div>
<div className="flex gap-4">
<button
type="submit"
disabled={processing}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
Update Bookmark
</button>
<Link href="/bookmarks" className="text-gray-600 px-4 py-2 hover:underline">
Cancel
</Link>
</div>
</form>
</div>
</AppLayout>
);
}
How is this different from create.tsx?
The structure is almost identical to your Create page, but three things are different:
- The component receives a bookmark prop from the controller, which contains the existing data.
- useForm is initialized with bookmark.title, bookmark.url, etc. instead of empty strings. This pre-fills the form.
- The submit calls put() instead of post(). A PUT request tells Laravel to update the existing bookmark rather than creating a new one.
- Save edit.tsx.
Seeing a blank page when you click Edit?
- Make sure the file is saved at exactly resources/js/pages/bookmarks/edit.tsx. The path must match what the controller passes to Inertia::render('bookmarks/edit').
- Check that the file extension is .tsx and not .ts or .jsx.
- help me debug why my edit page isn't rendering
✔️ Awesome, I've got everything!
Great. Make sure you've saved the file before moving on.
ⓧ I'd like to double check the full code
import { Head, Link, useForm } from '@inertiajs/react';
import AppLayout from '@/layouts/app-layout';
interface Bookmark {
id: number;
title: string;
url: string;
description: string | null;
category: string | null;
}
interface Props {
bookmark: Bookmark;
}
export default function Edit({ bookmark }: Props) {
const { data, setData, put, processing, errors } = useForm({
title: bookmark.title,
url: bookmark.url,
description: bookmark.description || '',
category: bookmark.category || '',
});
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
put(`/bookmarks/${bookmark.id}`);
}
return (
<AppLayout>
<Head title="Edit Bookmark" />
<div className="p-6 max-w-2xl">
<h1 className="text-2xl font-bold mb-6">Edit Bookmark</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700">
Title
</label>
<input
id="title"
type="text"
value={data.title}
onChange={(e) => setData('title', e.target.value)}
className="mt-1 block w-full border border-gray-300 rounded px-3 py-2"
/>
{errors.title && <p className="text-red-600 text-sm mt-1">{errors.title}</p>}
</div>
<div>
<label htmlFor="url" className="block text-sm font-medium text-gray-700">
URL
</label>
<input
id="url"
type="url"
value={data.url}
onChange={(e) => setData('url', e.target.value)}
className="mt-1 block w-full border border-gray-300 rounded px-3 py-2"
/>
{errors.url && <p className="text-red-600 text-sm mt-1">{errors.url}</p>}
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
Description (optional)
</label>
<textarea
id="description"
value={data.description}
onChange={(e) => setData('description', e.target.value)}
rows={3}
className="mt-1 block w-full border border-gray-300 rounded px-3 py-2"
/>
{errors.description && <p className="text-red-600 text-sm mt-1">{errors.description}</p>}
</div>
<div>
<label htmlFor="category" className="block text-sm font-medium text-gray-700">
Category (optional)
</label>
<input
id="category"
type="text"
value={data.category}
onChange={(e) => setData('category', e.target.value)}
className="mt-1 block w-full border border-gray-300 rounded px-3 py-2"
/>
{errors.category && <p className="text-red-600 text-sm mt-1">{errors.category}</p>}
</div>
<div className="flex gap-4">
<button
type="submit"
disabled={processing}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
Update Bookmark
</button>
<Link href="/bookmarks" className="text-gray-600 px-4 py-2 hover:underline">
Cancel
</Link>
</div>
</form>
</div>
</AppLayout>
);
}
Understand the edit and delete buttons
Your index.tsx already has Edit and Delete buttons for each bookmark. Let's look at how they work.
- Switch back to resources/js/pages/bookmarks/index.tsx in VS Code.
- Find the <div className="flex gap-2"> section near the bottom of the component. This is where both buttons live.
How do the Edit and Delete buttons work?
Both buttons use Inertia.js's Link component, but in different ways:
- The Edit link uses a standard navigation: href={`/bookmarks/${bookmark.id}/edit`}. This sends a GET request to the edit method on your controller, which renders the Edit page you just created.
- The Delete button uses method="delete" and as="button". This sends a DELETE request to /bookmarks/{id}, which triggers the destroy method on your controller.
The as="button" prop renders the Link as a <button> element instead of an <a> tag. This prevents accidental navigation from a right-click or middle-click.
Test the full CRUD flow
Your bookmark manager now has all four CRUD operations wired up. Let's verify them all work together.
- Return to your browser at http://localhost:8000/bookmarks.
- Click Add Bookmark and create a test bookmark with any title and URL.
- After it saves, click Edit on your new bookmark.
- Confirm that the form fields are pre-filled with the existing bookmark data.
- Change the title to something different and click Update Bookmark.
- Verify that the bookmark list now shows your updated title.
- Click Delete on the bookmark.
- Confirm the bookmark disappears from the list immediately.
What is CRUD?
CRUD stands for Create, Read, Update, and Delete. These are the four basic operations for managing data in any application. Your bookmark manager now handles all four: creating new bookmarks, reading/listing them, updating existing ones, and deleting them.
Edit form not pre-filling?
- Make sure you saved edit.tsx. The dev server needs the latest version of the file.
- Check that the useForm initialization uses bookmark.title and not empty strings. Empty strings mean you copied the Create page without changing the initial values.
- If you see a 403 error, make sure you're logged in as the same user who created the bookmark. The BookmarkPolicy only allows owners to edit their own bookmarks.
- help me fix my edit page pre-fill issue
Your bookmark manager is fully functional. You can create, view, edit, and delete bookmarks. Next up, you'll take things further with a search and filter feature in the Secret Mission.
Secret mission
Add Search and Filter
Your bookmark list works, but finding a specific bookmark means scrolling through everything. Add a search bar and category filter so you can instantly narrow down your bookmarks in real-time.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
This project runs entirely locally, so there are no ongoing costs. MySQL runs through XAMPP on your machine at no charge.
Resources you used:
- MySQL database named bookmark_manager (local, no ongoing cost).
- Laravel project folder bookmark-manager on your Desktop.
Keep everything running
No action needed. Choose this if you are still actively building or want to keep experimenting with your bookmark manager.
- Your bookmark_manager database stays in MySQL and takes up minimal disk space.
- Your project folder remains on your Desktop, ready to use whenever you start XAMPP and run composer run dev.
- PHP, Composer, and the Laravel installer remain installed on your system.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your files and database stay intact for next time.
- In your terminal, press Ctrl+C to stop the dev server.
- Open the XAMPP Control Panel and click Stop next to MySQL.
- When you come back, start MySQL in XAMPP again and run composer run dev from your bookmark-manager folder.
Delete - I don't want to use this again
Remove all project resources and start fresh. This permanently deletes your bookmarks and project files.
- In your terminal, press Ctrl+C to stop the dev server if it is still running.
- Drop the MySQL database by opening phpMyAdmin at http://localhost/phpmyadmin in your browser.
- In the left sidebar, click bookmark_manager.
- Click the Operations tab at the top.
- Scroll down to the Remove database section and click Drop the database (DROP).
- Confirm when prompted.
- Delete the project folder by running the command for your operating system below.
macOS
rm -rf ~/Desktop/bookmark-manager
Windows
Remove-Item -Recurse -Force ~\Desktop\bookmark-manager
Nice Work!
Nice Work!
Nice work! You just built a complete full-stack bookmark manager application with a Laravel backend and a React frontend, connected seamlessly through Inertia.js.
You've learned how to:
- Built a full Laravel 13 application with a React 19 frontend using Inertia.js to connect your backend controllers directly to page components.
- Created a MySQL database with migrations and an Eloquent model, then implemented a complete CRUD resource controller to handle all bookmark operations.
- Built interactive React page components using Inertia's useForm hook for form handling with server-side validation, plus edit and delete functionality with proper HTTP methods.
- Secret Mission: Added real-time search and category filtering to your bookmark list using router.get with preserveState for a smooth user experience.
Ready to quiz yourself?