Build a custom Markdoc tag that renders live weather data by calling an API function.
This tutorial explains how to create:
- an API function endpoint at
/api/weather - a React component that fetches from that endpoint
- a Markdoc tag that authors can use in Markdown files
API functions are server-side endpoints defined by adding TypeScript or JavaScript files inside the @api folder. The filename determines the URL path and, optionally, the HTTP method: <name>.<method>.ts (for example, weather.get.ts maps to GET /api/weather). Omit the method segment to handle all HTTP methods with a single file. See File-system and method routing for the full naming reference.
Markdoc tags are custom components registered in the @theme/markdoc folder. You create a React component in @theme/markdoc/components/, export it from @theme/markdoc/components.tsx, and register its tag schema in @theme/markdoc/schema.ts. See Build a Markdoc tag for full details.
In the following solution:
- The API key stays server-side in the API function (
process.env.WEATHER_API_KEY). - The Markdoc component renders live data by calling your own endpoint.
- You can apply role-based access control to the API function. See API functions reference.
Prerequisites
Make sure you have the following:
- familiarity with building Markdoc tags
- understanding of API function basics
- a free API key from weatherapi.com exposed as
WEATHER_API_KEYin your environment variables
Create the file @api/weather.ts. This file defines an endpoint at /api/weather that accepts an optional query parameter location. When location is omitted, the function falls back to the client's IP address for geolocation.
Import types
Import the ApiFunctionsContext type from @redocly/config. This type provides TypeScript definitions for the context object passed to every API function.
Define response types (optional)
Define types for the external weather API responses. Typed responses improve editor support and catch integration errors early.
Read the API key
Access the API key from environment variables using process.env. Return a 500 error early if the key is missing so the caller gets a clear message instead of a cryptic upstream failure.
Resolve the location
Use the location query parameter if the caller provides one. Otherwise, fall back to the client IP address from x-forwarded-for (or auto:ip as a last resort) so the weather API geolocates the visitor automatically.
Fetch weather data
Construct the URL for the external weather API and map your variables to the parameters required by the provider (e.g., mapping your location variable to their q parameter, and setting aqi to no to exclude Air Quality Index data). Call the external weather API with fetch, and handle non-OK responses by returning the upstream error details.
Return the response
Return the relevant subset of the weather data as JSON. The Markdoc component will consume this shape.
Create the file @theme/markdoc/components/CurrentWeather.tsx. This React component fetches from your API function and renders the result.
Import React
Import React so you can use hooks and JSX.
Define component types
Define the expected API response shape and component props. The optional location prop lets authors specify a city; units chooses Celsius or Fahrenheit.
Create the component
Declare the CurrentWeather function component with a state machine that tracks loading, error, and success states.
Fetch from the API function
Use useEffect to call /api/weather when the component mounts. If location is provided, pass it as a query parameter; otherwise omit it and let the API function resolve the location from the client IP. An AbortController cancels the request if the component unmounts before the response arrives.
Render weather data
Render loading and error states first, then display the location, temperature, humidity, and wind speed.
Add the tag schema
Update @theme/markdoc/schema.ts to register a weather tag. The render value must match the exported component name (CurrentWeather), and selfClosing means the tag has no children. Both location and units are optional -- when location is omitted the API function geolocates the visitor by IP address.
Export the component
Export CurrentWeather from @theme/markdoc/components.tsx so the Markdoc runtime can resolve the render value.
Authors can embed the weather tag in any .md file.
Geolocate the visitor by IP address:
{% weather /%}Or specify a city explicitly:
{% weather location="London" units="celsius" /%}- Build a Markdoc tag - Create custom Markdoc tags with React components
- API functions reference - Function signature, routing, context helpers, and access control
import type { ApiFunctionsContext } from '@redocly/config';
type WeatherApiError = {
error?: {
message?: string;
code?: number;
};
};
type WeatherApiResponse = {
location: {
name: string;
region: string;
country: string;
lat: number;
lon: number;
localtime: string;
};
current: {
temp_c: number;
temp_f: number;
feelslike_c: number;
feelslike_f: number;
humidity: number;
wind_kph: number;
wind_mph: number;
condition: {
text: string;
icon: string;
};
};
};
export default async function (request: Request, context: ApiFunctionsContext): Promise<Response> {
const apiKey = process.env.WEATHER_API_KEY;
if (!apiKey) {
return context.status(500).json({
error: 'Server configuration error',
message: 'Weather API key is not configured',
});
}
const queryLocation = context.query.location;
if (queryLocation && typeof queryLocation !== 'string') {
return context.status(400).json({
error: 'Invalid location parameter',
message: 'Please provide a single location',
});
}
const location =
queryLocation || request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'auto:ip';
try {
const url = new URL('https://api.weatherapi.com/v1/current.json');
url.searchParams.set('key', apiKey);
url.searchParams.set('q', location);
url.searchParams.set('aqi', 'no');
const weatherResponse = await fetch(url.toString());
if (!weatherResponse.ok) {
const errorData: WeatherApiError = await weatherResponse.json();
return context.status(weatherResponse.status).json({
error: 'Weather API error',
message: errorData.error?.message || 'Failed to fetch weather data',
code: errorData.error?.code,
});
}
const weatherData: WeatherApiResponse = await weatherResponse.json();
return context.status(200).json({
location: weatherData.location,
current: weatherData.current,
});
} catch (error: unknown) {
console.error('Weather API error:', error);
return context.status(500).json({ error: 'Internal server error' });
}
}