Skip to content

Render weather data from API function in a Markdoc tag

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

Key concepts

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.

Before you begin

Prerequisites

Make sure you have the following:

Create the Weather API function

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.

Export the handler

Export a default async function that receives a standard Web API Request and the Redocly context.

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 Markdoc component

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.

Register the Markdoc tag

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.

Use the tag in Markdown

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" /%}

Resources

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' });
  }
}