Step-by-Step Guide to building a custom multi-currency switcher block for Gutenberg using Next.js headless configurations
Setting Up the Headless WordPress Environment with Next.js
To build a custom Gutenberg block that interacts with a headless WordPress backend, we’ll leverage Next.js for our frontend application. This approach offers significant advantages in terms of performance, developer experience, and SEO. First, ensure you have a WordPress installation configured for headless operation. This typically involves disabling the theme and relying solely on the REST API for content retrieval. For the Next.js setup, we’ll use the `create-next-app` utility.
Initialize your Next.js project:
npx create-next-app@latest my-headless-ecommerce cd my-headless-ecommerce
Next, we need to configure our WordPress API endpoint. Create a new file, .env.local, in the root of your Next.js project and add your WordPress REST API URL.
WORDPRESS_API_URL=https://your-wordpress-site.com/wp-json/
We’ll also set up a utility function to fetch data from the WordPress API. Create a directory lib and inside it, a file named api.js.
import Cookies from 'js-cookie';
const WORDPRESS_API_URL = process.env.WORDPRESS_API_URL;
export async function fetchAPI(path) {
const res = await fetch(`${WORDPRESS_API_URL}${path}`);
if (!res.ok) {
console.error(`API Error: ${res.status} ${res.statusText}`);
throw new Error('Failed to fetch API');
}
const json = await res.json();
return json;
}
export async function getCurrencies() {
const data = await fetchAPI('wp-json/myplugin/v1/currencies'); // Assuming a custom endpoint for currencies
return data;
}
export async function setCurrency(currencyCode) {
Cookies.set('user_currency', currencyCode, { expires: 365 });
// Potentially trigger a re-render or data refetch here
return true;
}
export function getUserCurrency() {
return Cookies.get('user_currency') || 'USD'; // Default to USD
}
Developing the Custom Gutenberg Block
For Gutenberg block development, we’ll use the official WordPress `@wordpress/scripts` package. This provides a build process for JavaScript, CSS, and other assets. First, navigate to your WordPress plugin directory (or create a new one) and install the necessary packages.
# Inside your WordPress plugin directory (e.g., wp-content/plugins/my-currency-block) npm init -y npm install @wordpress/scripts --save-dev
In your plugin’s package.json, add the following scripts:
{
"name": "my-currency-block",
"version": "1.0.0",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start"
},
"devDependencies": {
"@wordpress/scripts": "^26.0.0"
}
}
Now, create the main plugin file (e.g., my-currency-block.php) and register your block. We’ll also define a custom REST API endpoint to fetch available currencies from your WordPress site. This endpoint will be managed by a separate plugin or theme functions.
<?php
/**
* Plugin Name: My Custom Currency Block
* Description: A Gutenberg block for multi-currency switching.
* Version: 1.0.0
* Author: Your Name
*/
function my_currency_block_register_block() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'my_currency_block_register_block' );
// Custom REST API endpoint for currencies
function my_get_currencies_rest_api() {
register_rest_route( 'myplugin/v1', '/currencies', array(
'methods' => 'GET',
'callback' => 'my_get_currencies_callback',
'permission_callback' => '__return_true', // Adjust permissions as needed
) );
}
add_action( 'rest_api_init', 'my_get_currencies_rest_api' );
function my_get_currencies_callback( $request ) {
// In a real-world scenario, fetch this from your e-commerce plugin settings
// or a custom database table. For this example, we'll hardcode.
$currencies = array(
'USD' => array( 'name' => 'US Dollar', 'symbol' => '$' ),
'EUR' => array( 'name' => 'Euro', 'symbol' => '€' ),
'GBP' => array( 'name' => 'British Pound', 'symbol' => '£' ),
);
return new WP_REST_Response( $currencies, 200 );
}
?>
Create the block’s source files in a src directory. Specifically, src/index.js for the block registration and src/edit.js and src/save.js for the editor and frontend rendering.
// src/index.js
import { registerBlockType } from '@wordpress/blocks';
import './style.scss'; // For frontend styles
import './editor.scss'; // For editor styles
import Edit from './edit';
import save from './save';
registerBlockType('myplugin/currency-switcher', {
apiVersion: 2,
title: 'Currency Switcher',
icon: 'money',
category: 'ecommerce',
attributes: {
selectedCurrency: {
type: 'string',
default: 'USD',
},
},
edit: Edit,
save: save,
});
// src/edit.js
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, SelectControl } from '@wordpress/components';
import { useState, useEffect } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
export default function Edit({ attributes, setAttributes }) {
const blockProps = useBlockProps();
const { selectedCurrency } = attributes;
const [currencies, setCurrencies] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchCurrencies = async () => {
try {
const response = await apiFetch({ path: '/myplugin/v1/currencies' });
const currencyOptions = Object.keys(response).map(key => ({
label: `${response[key].name} (${response[key].symbol})`,
value: key,
}));
setCurrencies(currencyOptions);
setLoading(false);
} catch (error) {
console.error('Error fetching currencies:', error);
setLoading(false);
}
};
fetchCurrencies();
}, []);
const onCurrencyChange = (newCurrency) => {
setAttributes({ selectedCurrency: newCurrency });
};
return (
<>
<InspectorControls>
<PanelBody title={__('Currency Settings', 'myplugin')} initialOpen={true}>
{loading ? (
<p>{__('Loading currencies...', 'myplugin')}</p>
) : (
<SelectControl
label={__('Default Currency', 'myplugin')}
value={selectedCurrency}
options={currencies}
onChange={onCurrencyChange}
/>
)}
</PanelBody>
</InspectorControls>
<div {...blockProps}>
<p>{__('Currency Switcher (Editor View)', 'myplugin')}</p>
<p>{__('Selected: ', 'myplugin')}{selectedCurrency}</p>
{/* In a real editor view, you might show actual currency options */}
</div>
</>
);
}
// src/save.js
import { useBlockProps } from '@wordpress/block-editor';
export default function save({ attributes }) {
const blockProps = useBlockProps.save();
const { selectedCurrency } = attributes;
return (
<div {...blockProps}>
<p>Current Currency: {selectedCurrency}</p>
{/* This will be the frontend display. The actual switching logic will be in Next.js */}
</div>
);
}
After creating these files, run the build command in your plugin’s directory:
cd wp-content/plugins/my-currency-block npm run build
This will generate the necessary JavaScript and CSS files in the build directory. Activate your plugin in WordPress. You can now add the “Currency Switcher” block to your pages or posts.
Integrating the Block with Next.js for Dynamic Switching
The Gutenberg block itself primarily handles the *display* of the selected currency and its configuration within the WordPress editor. The actual *switching* logic and its impact on product prices or content will be managed by your Next.js application. We’ll create a component in Next.js that fetches currencies and allows the user to select one, updating a cookie that Next.js reads.
Create a new component, components/CurrencySwitcher.js, in your Next.js project.
// components/CurrencySwitcher.js
import React, { useState, useEffect } from 'react';
import { getCurrencies, setCurrency, getUserCurrency } from '../lib/api';
function CurrencySwitcher() {
const [currencies, setCurrencies] = useState([]);
const [currentCurrency, setCurrentCurrency] = useState('USD');
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchAndSetCurrency = async () => {
try {
const fetchedCurrencies = await getCurrencies();
const currencyList = Object.entries(fetchedCurrencies).map(([code, data]) => ({
code,
name: data.name,
symbol: data.symbol,
}));
setCurrencies(currencyList);
const savedCurrency = getUserCurrency();
setCurrentCurrency(savedCurrency);
setLoading(false);
} catch (error) {
console.error('Failed to load currencies:', error);
setLoading(false);
}
};
fetchAndSetCurrency();
}, []);
const handleCurrencyChange = async (event) => {
const newCurrency = event.target.value;
await setCurrency(newCurrency);
setCurrentCurrency(newCurrency);
// Optionally, trigger a global state update or page reload here
// For example, using a context API or a state management library
window.location.reload(); // Simple reload for demonstration
};
if (loading) {
return <div>Loading currencies...</div>;
}
return (
<div>
<label htmlFor="currency-select">Select Currency: </label>
<select id="currency-select" value={currentCurrency} onChange={handleCurrencyChange}>
{currencies.map((currency) => (
<option key={currency.code} value={currency.code}>
{currency.name} ({currency.symbol})
</option>
))}
</select>
</div>
);
}
export default CurrencySwitcher;
Now, integrate this component into your Next.js pages. For example, in pages/index.js:
// pages/index.js
import Head from 'next/head';
import CurrencySwitcher from '../components/CurrencySwitcher';
import { getUserCurrency } from '../lib/api'; // Import to get initial currency
export default function Home({ initialCurrency }) {
return (
<div>
<Head>
<title>My Headless E-commerce</title>
<meta name="description" content="Welcome to our store" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<h1>Welcome to Our Store!</h1>
<CurrencySwitcher />
<p>Your current currency is: {initialCurrency}</p>
{/* Product listings and pricing would go here, dynamically adjusted */}
</main>
</div>
);
}
// This function runs on the server-side to get the initial currency
export async function getServerSideProps(context) {
const initialCurrency = getUserCurrency(); // This needs to be adapted for SSR context if using cookies directly
// For SSR, you'd typically read cookies from the request headers.
// For simplicity here, we'll assume a default or client-side read.
// A more robust solution would involve a custom server or middleware.
// Placeholder for actual SSR cookie reading
const serverSideCurrency = context.req.cookies?.user_currency || 'USD';
return {
props: {
initialCurrency: serverSideCurrency,
},
};
}
To make the currency selection persist across page loads and sessions, the js-cookie library is used. Ensure it’s installed:
npm install js-cookie
The core idea is that the Gutenberg block, when rendered on the frontend via the REST API or GraphQL, will output static HTML (e.g., <p>Current Currency: USD</p>). Your Next.js application then *overrides* this display by fetching the actual currencies from WordPress and rendering the interactive CurrencySwitcher component. The component updates a cookie, and on subsequent loads or reloads, Next.js reads this cookie to determine the user’s preferred currency. Product prices and other e-commerce data would then be fetched and displayed according to this selected currency.
Handling Currency Conversion and Data Fetching
The actual currency conversion logic is outside the scope of the Gutenberg block and the basic Next.js component. This typically involves:
- Fetching Exchange Rates: Integrate with a third-party API (e.g., Open Exchange Rates, Fixer.io) to get real-time exchange rates.
- Storing Base Prices: In your WordPress e-commerce setup (e.g., WooCommerce), store product prices in a single base currency.
- Dynamic Price Calculation: When a user selects a currency in Next.js, fetch the relevant exchange rate and calculate the displayed price. This calculation should happen on the Next.js server (e.g., in
getServerSidePropsor API routes) or on the client-side if performance is not critical.
For example, a product listing page in Next.js might look like this:
// pages/products/[slug].js (Example)
import React from 'react';
import { getProductBySlug } from '../../lib/products'; // Assume this fetches product data
import { getUserCurrency } from '../../lib/api';
import CurrencySwitcher from '../../components/CurrencySwitcher';
// Assume a function to convert prices
import { convertPrice } from '../../lib/currencyConverter';
export default function ProductPage({ product, currentCurrency }) {
const displayPrice = convertPrice(product.price, 'USD', currentCurrency); // Convert base price
return (
<div>
<h1>{product.name}</h1>
<CurrencySwitcher />
<p>Price: {displayPrice}</p>
<div dangerouslySetInnerHTML={{ __html: product.content }} />
</div>
);
}
export async function getServerSideProps(context) {
const { slug } = context.params;
const product = await getProductBySlug(slug);
const currentCurrency = context.req.cookies?.user_currency || 'USD';
if (!product) {
return { notFound: true };
}
return {
props: {
product,
currentCurrency,
},
};
}
This setup provides a robust foundation for a headless e-commerce site with a custom multi-currency switcher, combining the flexibility of Gutenberg for content management with the performance and modern development practices of Next.js.