How Projects Reference External Plugin State for URL Parameter Handling in GeoLibre

GeoLibre enables projects to automatically activate external plugins and pass them URL query parameters through a combination of urlParameterNames declarations, PluginManager orchestration, and ProjectPluginState persistence.

Modern web mapping applications frequently rely on deep-link URLs to share specific views and configurations. In GeoLibre, this requirement extends beyond core functionality to external plugins—third-party extensions that may not exist when a project is first created. The opengeos/GeoLibre repository implements a robust system that allows saved projects to reference external plugin state and seamlessly handle URL parameters even for plugins loaded from remote manifests.

This article examines the key mechanisms that make this possible: plugin metadata declarations, centralized parameter dispatch, deduplication logic, and project-state persistence across save and restore operations.

Plugin Metadata: Declaring URL Parameter Interest

Plugins opt into URL parameter handling by declaring the query-string keys they care about. This metadata lives in the GeoLibrePlugin interface defined in packages/plugins/src/types.ts:

export interface GeoLibrePlugin {
  id: string;
  // Optional: names of query parameters this plugin wants to handle
  urlParameterNames?: string[];
  // Optional: handler called when matching parameters are present
  handleUrlParameters?: (
    app: PluginAppAPI,
    params: URLSearchParams
  ) => Promise<void> | void;
  // ... other lifecycle methods
}

The urlParameterNames array serves as a registration hook. When any declared parameter appears in the URL, GeoLibre knows to activate that plugin and invoke its handler. This declaration-based approach keeps the system lazy: plugins are only loaded and initialized when actually needed.

The handleUrlParameters method receives a scoped PluginAppAPI and the parsed URLSearchParams, allowing the plugin to read values and update its internal state or trigger UI effects.

Example: Elevation Profile Plugin

The built-in elevation profile plugin demonstrates this pattern in packages/plugins/src/plugins/elevation-profile/index.ts:

export const elevationProfilePlugin: GeoLibrePlugin = {
  id: "elevationProfile",
  urlParameterNames: ["line"],

  async handleUrlParameters(app, params) {
    const line = params.get("line");
    if (line) {
      // Parse polyline and dispatch to UI state
      app.dispatch({ type: "elevationProfile/setLine", payload: line });
    }
  },
};

When a user visits https://app.geolibre.io/?line=encodedPolyline123, the PluginManager detects the line parameter, activates the elevation profile plugin, and passes it the parsed value.

PluginManager: Centralized Registration and Dispatch

The PluginManager class in packages/plugins/src/plugin-manager.ts orchestrates the entire URL parameter flow. It maintains three critical data structures:

  • urlParameterNamesById: A Map<string, string[]> storing normalized parameter names per plugin ID, built at registration time
  • active: A Set<string> tracking currently activated plugins
  • handledUrlParametersByContext: A Map<string, Set<string>> preventing duplicate invocations for the same URL context

Registration-Time Indexing

When PluginManager.register is called, the manager indexes the plugin's parameter interests:

this.urlParameterNamesById.set(
  plugin.id,
  normalizeUrlParameterNames(plugin.urlParameterNames),
);

This normalization ensures consistent matching regardless of how parameter names are declared (case, whitespace, duplicates).

Context-Aware Deduplication

The handleUrlParameters method implements sophisticated deduplication to handle async re-entrancy and race conditions. It creates a context key from the serialized query string:

const contextKey = params.toString();
let handledPluginIds = this.handledUrlParametersByContext.get(contextKey);

if (!handledPluginIds) {
  handledPluginIds = new Set<string>();
  this.handledUrlParametersByContext.set(contextKey, handledPluginIds);
}

Before processing each plugin, the manager checks if it has already handled this context. This prevents duplicate work when the URL changes rapidly or when the method is called multiple times during startup.

Activation and Dispatch Flow

For plugins whose declared parameters intersect the incoming URL, the manager handles activation and dispatch:

for (const [id, paramNames] of this.urlParameterNamesById) {
  // Skip if no parameter overlap
  const hasMatchingParam = paramNames.some(p => params.has(p));
  if (!hasMatchingParam) continue;

  // Skip if already handled this context
  if (handledPluginIds.has(id)) continue;

  // Mark as handled early to prevent races
  handledPluginIds.add(id);

  // Activate if not already active
  if (!this.active.has(id)) {
    const activated = await this.activate(id, app);
    if (!activated) {
      handledPluginIds.delete(id); // Allow retry on failure
      continue;
    }
  }

  // Dispatch to plugin handler
  const plugin = this.plugins.get(id)!;
  try {
    await plugin.handleUrlParameters!(
      scopeAppToPlugin(app, id),
      new URLSearchParams(params),
    );
  } catch (err) {
    handledPluginIds.delete(id); // Allow retry on error
    console.warn(`Plugin ${id} failed to handle URL parameters`, err);
  }
}

This implementation guarantees that each plugin processes each unique URL context exactly once, even in the face of activation failures or async timing issues.

ProjectPluginState: Persisting External Plugin References

External plugins pose a unique challenge: they are not bundled with the application and must be loaded from remote URLs. GeoLibre solves this through ProjectPluginState, defined in packages/core/src/types.ts:

export interface ProjectPluginState {
  // URLs of external plugin manifests to load
  manifestUrls: string[];
  // IDs of currently active plugins
  activePluginIds: string[];
  // Plugin-specific serialized state
  settings: Record<string, unknown>;
}

The manifestUrls array is the critical link for referencing external plugin state. When a project is saved, this list captures all external plugins that were loaded. When restored, these URLs are fetched and registered before URL parameter handling begins.

Persist and Restore Cycle

The PluginManager provides methods for state serialization and restoration:

// Saving: gather current state
getProjectState(): ProjectPluginState {
  return {
    manifestUrls: this.externalManifestUrls,
    activePluginIds: Array.from(this.active),
    settings: Object.fromEntries(
      Array.from(this.plugins.entries()).map(([id, plugin]) => [
        id,
        plugin.getProjectState?.() ?? {},
      ])
    ),
  };
}

// Loading: restore previous state
async restoreProjectState(state: ProjectPluginState, app: AppAPI): Promise<void> {
  // Load external manifests first
  for (const url of state.manifestUrls) {
    await this.loadExternalManifest(url);
  }
  
  // Restore activation status and settings
  for (const id of state.activePluginIds) {
    await this.activate(id, app);
    const plugin = this.plugins.get(id);
    const pluginState = state.settings[id];
    plugin?.setProjectState?.(pluginState);
  }
}

This round-trip ensures that URL-derived settings are preserved. If a plugin's state was originally populated from URL parameters, that state is captured in getProjectState and reapplied via restoreProjectState, making the saved project a faithful reproduction of the original view.

Startup Sequence: External Plugins Before URL Handling

The correct load order is essential to avoid race conditions. The host application follows this sequence:

async function startApp(savedProject?: ProjectPluginState) {
  const pluginMgr = new PluginManager();
  
  // 1. Restore external plugins FIRST
  if (savedProject) {
    await pluginMgr.restoreProjectState(savedProject, appAPI);
  }
  
  // 2. Now handle URL parameters—external plugins are available
  await pluginMgr.handleUrlParameters(
    new URLSearchParams(window.location.search),
    appAPI,
  );
  
  // 3. Continue with UI initialization
  renderUI();
}

This ordering guarantees that external plugins are trusted and registered before the URL parameter dispatcher runs. Without this step, a deep-link referencing an external plugin's parameters would fail to find a matching handler.

Error Handling and Resilience

The PluginManager implements defensive patterns for production reliability:

  • Activation failures: If activate() returns false or throws, the plugin ID is removed from handledPluginIds, allowing retry on subsequent URL changes
  • Handler exceptions: Errors in handleUrlParameters are caught, logged, and the plugin is marked unhandled for that context
  • Missing plugins: If a saved project references an external manifest that is no longer available, the load fails gracefully with optional user notification

These protections ensure that one misbehaving plugin cannot break URL parameter handling for others.

Summary

  • urlParameterNames and handleUrlParameters in the GeoLibrePlugin interface let plugins declare and process URL query parameters, implemented in packages/plugins/src/types.ts
  • PluginManager.urlParameterNamesById indexes plugin interests at registration time for efficient lookup, located in packages/plugins/src/plugin-manager.ts
  • Context-based deduplication via handledUrlParametersByContext prevents duplicate invocations and handles async race conditions
  • ProjectPluginState.manifestUrls persists references to external plugins, ensuring they are loaded and trusted before URL parameter dispatch
  • getProjectState and restoreProjectState enable round-trip persistence of plugin settings, including those derived from URL parameters

Frequently Asked Questions

How does GeoLibre know which plugins to activate for a given URL?

The PluginManager compares incoming query parameter names against the urlParameterNamesById map built at plugin registration. Any plugin whose declared parameters intersect the URL is activated automatically, as implemented in packages/plugins/src/plugin-manager.ts.

What happens if an external plugin fails to load from its manifest URL?

During restoreProjectState, manifest loading errors are caught and logged. The plugin is skipped, and URL parameter processing continues for other plugins. The error does not block the entire project load, though the user may see a notification about missing functionality.

Can a plugin handle URL parameters without declaring urlParameterNames?

Yes, but with limitations. Plugins without urlParameterNames will not be automatically activated based on URL content. They can still access window.location.search directly, but they miss the activation orchestration and deduplication benefits provided by the PluginManager.

How does GeoLibre prevent a plugin from processing the same URL parameters multiple times?

The manager creates a context key from the serialized query string and tracks handled plugins per context in handledUrlParametersByContext. Each plugin ID is added to this set before activation begins, blocking re-entrancy even if the method is called asynchronously during an existing run.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →