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

> Discover how GeoLibre projects reference external plugin state for URL parameter handling using urlParameterNames PluginManager and ProjectPluginState.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: deep-dive
- Published: 2026-08-15

---

**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`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/types.ts):

```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`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugins/elevation-profile/index.ts):

```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`](https://github.com/opengeos/GeoLibre/blob/main/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:

```ts
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:

```ts
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:

```ts
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`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/types.ts):

```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:

```ts
// 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:

```ts
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`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/types.ts)
- **`PluginManager.urlParameterNamesById`** indexes plugin interests at registration time for efficient lookup, located in [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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.