diff --git a/package-lock.json b/package-lock.json
index 2206dbf7b0ec938717b97926f1cc4af5512f08a3..1b18042d48472e1ec4f4d72e7bbb8ef684a920c7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,6 +23,8 @@
         "@sveltejs/adapter-node": "^1.0.0-next.85",
         "@sveltejs/kit": "^1.0.0-next.428",
         "@tailwindcss/typography": "^0.5.4",
+        "@tricoteuses/explorer-tools": "^0.1.12",
+        "@tricoteuses/legal-explorer": "^0.0.1",
         "@types/cookie": "^0.5.0",
         "@types/fs-extra": "^9.0.11",
         "@typescript-eslint/eslint-plugin": "^5.0.0",
@@ -1960,6 +1962,15 @@
       "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
       "dev": true
     },
+    "node_modules/@iconify-icons/codicon": {
+      "version": "1.2.16",
+      "resolved": "https://registry.npmjs.org/@iconify-icons/codicon/-/codicon-1.2.16.tgz",
+      "integrity": "sha512-85rBsFEhhq2qSBfIEF0hzUk31i4GjeRzNyd0DZGFWo5v+PgAeTBiv8ftsTDf8d2fxy9F5kesT/R7bOtRy1xKmw==",
+      "dev": true,
+      "dependencies": {
+        "@iconify/types": "*"
+      }
+    },
     "node_modules/@iconify/svelte": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/@iconify/svelte/-/svelte-2.2.1.tgz",
@@ -1969,6 +1980,12 @@
         "url": "http://github.com/sponsors/cyberalien"
       }
     },
+    "node_modules/@iconify/types": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@iconify/types/-/types-1.1.0.tgz",
+      "integrity": "sha512-Jh0llaK2LRXQoYsorIH8maClebsnzTcve+7U3rQUSnC11X4jtPnFuyatqFLvMxZ8MLG8dB4zfHsbPfuvxluONw==",
+      "dev": true
+    },
     "node_modules/@jridgewell/gen-mapping": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
@@ -2195,6 +2212,23 @@
         "tailwindcss": ">=3.0.0 || insiders"
       }
     },
+    "node_modules/@tricoteuses/explorer-tools": {
+      "version": "0.1.12",
+      "resolved": "https://registry.npmjs.org/@tricoteuses/explorer-tools/-/explorer-tools-0.1.12.tgz",
+      "integrity": "sha512-Aachs9YOz1R624H3gl1WuMkdbqhBPmjKIogfO3iukjVH4MBZ7g1jN2a8q4mRj0c9v82fwiKKBdcCz0FLi4S8Cw==",
+      "dev": true,
+      "dependencies": {
+        "@iconify-icons/codicon": "^1.2.15",
+        "@iconify/svelte": "^2.2.1",
+        "augmented-data-viewer": "^0.1.5"
+      }
+    },
+    "node_modules/@tricoteuses/legal-explorer": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/@tricoteuses/legal-explorer/-/legal-explorer-0.0.1.tgz",
+      "integrity": "sha512-EILVjFAlQBff4ysltQqwSUlwUKtVzomfyClITNMMYRGEdrJBthL9zqsGLJexK5D4WEbFv+ZLpRKgq5GEpdKz6g==",
+      "dev": true
+    },
     "node_modules/@types/cookie": {
       "version": "0.5.1",
       "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz",
@@ -2563,6 +2597,16 @@
         "node": ">=8"
       }
     },
+    "node_modules/augmented-data-viewer": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/augmented-data-viewer/-/augmented-data-viewer-0.1.5.tgz",
+      "integrity": "sha512-LuzvtNsO3rxfl9PbASCJiptr7svRyUhaoHi2PPcD+OqGqgrMd7kuEbeNDAtl7PC/cU2eIQPvjPnN9QO3lLlU5w==",
+      "dev": true,
+      "dependencies": {
+        "@iconify-icons/codicon": "^1.2.15",
+        "@iconify/svelte": "^2.2.1"
+      }
+    },
     "node_modules/autoprefixer": {
       "version": "10.4.8",
       "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.8.tgz",
@@ -7935,12 +7979,27 @@
       "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
       "dev": true
     },
+    "@iconify-icons/codicon": {
+      "version": "1.2.16",
+      "resolved": "https://registry.npmjs.org/@iconify-icons/codicon/-/codicon-1.2.16.tgz",
+      "integrity": "sha512-85rBsFEhhq2qSBfIEF0hzUk31i4GjeRzNyd0DZGFWo5v+PgAeTBiv8ftsTDf8d2fxy9F5kesT/R7bOtRy1xKmw==",
+      "dev": true,
+      "requires": {
+        "@iconify/types": "*"
+      }
+    },
     "@iconify/svelte": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/@iconify/svelte/-/svelte-2.2.1.tgz",
       "integrity": "sha512-eWZq8CRrr3WfnKAj8SWknfE3S/d+j/AzEcypeJaHurS1s4zTdFnkjATcFa8lerGtcX0PAtXiVL94tbIEd69N+w==",
       "dev": true
     },
+    "@iconify/types": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@iconify/types/-/types-1.1.0.tgz",
+      "integrity": "sha512-Jh0llaK2LRXQoYsorIH8maClebsnzTcve+7U3rQUSnC11X4jtPnFuyatqFLvMxZ8MLG8dB4zfHsbPfuvxluONw==",
+      "dev": true
+    },
     "@jridgewell/gen-mapping": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
@@ -8101,6 +8160,23 @@
         "lodash.merge": "^4.6.2"
       }
     },
+    "@tricoteuses/explorer-tools": {
+      "version": "0.1.12",
+      "resolved": "https://registry.npmjs.org/@tricoteuses/explorer-tools/-/explorer-tools-0.1.12.tgz",
+      "integrity": "sha512-Aachs9YOz1R624H3gl1WuMkdbqhBPmjKIogfO3iukjVH4MBZ7g1jN2a8q4mRj0c9v82fwiKKBdcCz0FLi4S8Cw==",
+      "dev": true,
+      "requires": {
+        "@iconify-icons/codicon": "^1.2.15",
+        "@iconify/svelte": "^2.2.1",
+        "augmented-data-viewer": "^0.1.5"
+      }
+    },
+    "@tricoteuses/legal-explorer": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/@tricoteuses/legal-explorer/-/legal-explorer-0.0.1.tgz",
+      "integrity": "sha512-EILVjFAlQBff4ysltQqwSUlwUKtVzomfyClITNMMYRGEdrJBthL9zqsGLJexK5D4WEbFv+ZLpRKgq5GEpdKz6g==",
+      "dev": true
+    },
     "@types/cookie": {
       "version": "0.5.1",
       "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz",
@@ -8349,6 +8425,16 @@
       "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
       "dev": true
     },
+    "augmented-data-viewer": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/augmented-data-viewer/-/augmented-data-viewer-0.1.5.tgz",
+      "integrity": "sha512-LuzvtNsO3rxfl9PbASCJiptr7svRyUhaoHi2PPcD+OqGqgrMd7kuEbeNDAtl7PC/cU2eIQPvjPnN9QO3lLlU5w==",
+      "dev": true,
+      "requires": {
+        "@iconify-icons/codicon": "^1.2.15",
+        "@iconify/svelte": "^2.2.1"
+      }
+    },
     "autoprefixer": {
       "version": "10.4.8",
       "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.8.tgz",
diff --git a/package.json b/package.json
index e82791a0a5a34d0403147de4d533f1ff82891d1c..f47f7ac3aa8b5207816a730d691c753dd676e28c 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,8 @@
     "@sveltejs/adapter-node": "^1.0.0-next.85",
     "@sveltejs/kit": "^1.0.0-next.428",
     "@tailwindcss/typography": "^0.5.4",
+    "@tricoteuses/explorer-tools": "^0.1.12",
+    "@tricoteuses/legal-explorer": "^0.0.1",
     "@types/cookie": "^0.5.0",
     "@types/fs-extra": "^9.0.11",
     "@typescript-eslint/eslint-plugin": "^5.0.0",
diff --git a/src/lib/components/legifrance/ArticleView.svelte b/src/lib/components/legifrance/ArticleView.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..dd69f0edd246b4fea890b4db1075ff3188c67794
--- /dev/null
+++ b/src/lib/components/legifrance/ArticleView.svelte
@@ -0,0 +1,92 @@
+<script lang="ts">
+  import Icon from "@iconify/svelte"
+  import {
+    firstValueOfArrayOrSingleton,
+    iterArrayOrSingleton,
+  } from "@tricoteuses/explorer-tools"
+  import type {
+    Article,
+    LegalObject,
+    LegalObjectType,
+  } from "@tricoteuses/legal-explorer"
+
+  import LienView from "$lib/components/legifrance/LienView.svelte"
+
+  export let article: Article
+  export let level = 1
+
+  const dateFormatter = new Intl.DateTimeFormat("fr-FR", {
+    dateStyle: "medium",
+  })
+
+  $: metaArticle = article.META.META_SPEC.META_ARTICLE
+
+  $: legifranceUrl = legifranceUrlFromLegalObject("article", article)
+
+  $: liens = [...iterArrayOrSingleton(article.LIENS?.LIEN)]
+  $: ciblesCreation = liens.filter(
+    (lien) => lien["@sens"] === "cible" && lien["@typelien"] === "CREATION",
+  )
+  $: titreTexte = firstValueOfArrayOrSingleton(
+    article.CONTEXTE.TEXTE.TITRE_TXT,
+  )?.["#text"]
+
+  export function legifranceUrlFromLegalObject(
+    type: LegalObjectType,
+    object: LegalObject,
+  ): string | undefined {
+    switch (type) {
+      case "article":
+        return `https://www.legifrance.gouv.fr/codes/article_lc/${
+          (object as Article).META.META_COMMUN.ID
+        }`
+      default:
+        console.warn(
+          `ArticleView.legifranceUrlFromLegalObject: TODO Handle legal object type: ${type}`,
+        )
+        return undefined
+    }
+  }
+</script>
+
+<h4 class="mb-4 font-serif text-2xl italic text-gray-700 md:text-3xl">
+  <Icon
+    class="mr-1 inline-flex h-7 w-7 place-self-center text-le-gris-dispositif-light"
+    icon="ri-map-pin-2-fill"
+  />
+  <span class="font-bold">Article {metaArticle.NUM}</span>
+  - {titreTexte}
+</h4>
+
+<div class="prose">
+  {@html article.BLOC_TEXTUEL.CONTENU}
+</div>
+
+{#if ciblesCreation.length > 0}
+  <ul>
+    {#each liens as lien}
+      <li>
+        <LienView level={level + 1} {lien} />
+      </li>
+    {/each}
+  </ul>
+{/if}
+
+<div class="mt-4 text-right text-sm text-gray-500">
+  {#if metaArticle.DATE_DEBUT !== "2999-01-01"}
+    <p>
+      Article en vigueur {#if metaArticle.DATE_FIN === "2999-01-01"}depuis le {dateFormatter.format(
+          new Date(metaArticle.DATE_DEBUT),
+        )}{:else}du {dateFormatter.format(new Date(metaArticle.DATE_DEBUT))} au {dateFormatter.format(
+          new Date(metaArticle.DATE_FIN),
+        )}{/if}
+    </p>
+  {/if}
+  {#if legifranceUrl !== undefined}
+    <span class="text-sm text-gray-500 md:text-base">
+      <a class="link" href={legifranceUrl} target="_blank"
+        >Voir l'article sur Légifrance.fr</a
+      >
+    </span>
+  {/if}
+</div>
diff --git a/src/lib/components/legifrance/LienView.svelte b/src/lib/components/legifrance/LienView.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..b287f65c6a3438623b11de0cfaf989c805195e6a
--- /dev/null
+++ b/src/lib/components/legifrance/LienView.svelte
@@ -0,0 +1,101 @@
+<script lang="ts">
+  import Icon from "@iconify/svelte"
+  import {
+    type Aggregate,
+    type Article,
+    type LegalObject,
+    type LegalObjectType,
+    type Lien,
+    pathnameFromLegalId,
+    rootTypeFromLegalId,
+  } from "@tricoteuses/legal-explorer"
+
+  import { page } from "$app/stores"
+  import ArticleView from "$lib/components/legifrance/ArticleView.svelte"
+
+  export let level = 1
+  export let lien: Lien
+
+  let open = false
+
+  $: id = lien["@id"]
+  $: rootType = rootTypeFromLegalId(id)
+  $: componentAndPropertiesPromise =
+    componentAndPropertiesFromTypeAndTarget(rootType)
+
+  async function componentAndPropertiesFromTypeAndTarget(
+    type: LegalObjectType | undefined,
+  ) {
+    if (type === undefined) {
+      return undefined
+    }
+    switch (type) {
+      case "article":
+        const article = await retrieveLegifranceArticle(id)
+        if (article === undefined) {
+          return undefined
+        }
+        return {
+          component: ArticleView,
+          properties: { article, level },
+        }
+      default:
+        console.log(
+          `LienView.componentAndPropertiesFromTypeAndTarget: Document type ${type} not handled yet`,
+        )
+        return undefined
+    }
+  }
+
+  async function retrieveLegifranceArticle(
+    id: string,
+  ): Promise<Article | undefined> {
+    const response = await fetch(
+      new URL(`api/articles/${encodeURIComponent(id)}`, $page.data.legalUrl),
+      { headers: { Accept: "application/json" } },
+    )
+    if (!response.ok) {
+      console.error(
+        `LienView: Error retrieving article ${id}:\n${response.status} ${response.statusText}`,
+      )
+      console.error(await response.text())
+      return undefined
+    }
+    const data = (await response.json()) as Aggregate
+    return data.id === undefined ? undefined : data.article?.[data.id]
+  }
+
+  function toggle() {
+    open = !open
+  }
+</script>
+
+{#if componentAndPropertiesPromise === undefined}
+  <div class="inline-flex align-top" on:click|stopPropagation={toggle}>
+    <Icon class="mt-1 inline-block shrink-0" icon="codicon:dash" inline />
+    <a class="link link-hover link-primary" href={pathnameFromLegalId(id)}>
+      {lien["#text"]}
+    </a>
+  </div>
+{:else}
+  <div
+    class="inline-flex cursor-pointer align-top"
+    on:click|stopPropagation={toggle}
+  >
+    <Icon
+      class="mt-1 inline-block shrink-0"
+      icon={open ? "codicon:triangle-down" : "codicon:triangle-right"}
+      inline
+    />
+    {lien["#text"]}
+  </div>
+  {#if open}
+    {#await componentAndPropertiesPromise}
+      <p>Récupération du lien Légifrance en cours…</p>
+    {:then { component, properties }}
+      <div class="ml-2 border-l-4 pl-2">
+        <svelte:component this={component} {...properties} />
+      </div>
+    {/await}
+  {/if}
+{/if}
diff --git a/src/lib/components/parameters/ArticleModal.svelte b/src/lib/components/parameters/ArticleModal.svelte
index cbc067687c171069f85bb37c57f5713e38e81864..554425a0f034bd398f6e7e89f1caa4aa5cdb84fc 100644
--- a/src/lib/components/parameters/ArticleModal.svelte
+++ b/src/lib/components/parameters/ArticleModal.svelte
@@ -15,30 +15,27 @@
   } from "@rgossiaux/svelte-headlessui"
 
   import { page } from "$app/stores"
+  import ArticleView from "$lib/components/legifrance/ArticleView.svelte"
 
   export let billLegalReferences: Reference[] | undefined
   export let billParameter: ValueParameter | ScaleParameter
   export let isOpen = false
 
-  const dateFormatter = new Intl.DateTimeFormat("fr-FR", {
-    dateStyle: "medium",
-  })
-
   $: firstLegalReference = billLegalReferences?.[0]
 
   $: articlePromise =
     !isOpen || firstLegalReference === undefined
       ? undefined
-      : retrieveLegalArticle(firstLegalReference.href)
+      : retrieveLegifranceArticle(firstLegalReference.href)
 
   function closeModal() {
     isOpen = false
   }
 
-  async function retrieveLegalArticle(url: string) {
+  async function retrieveLegifranceArticle(url: string) {
     const response = await fetch(
       new URL(
-        `api/recherche?q=${decodeURIComponent(url)}`,
+        `api/recherche?q=${encodeURIComponent(url)}`,
         $page.data.legalUrl,
       ),
       { headers: { Accept: "application/json" } },
@@ -921,47 +918,7 @@
                   Récupération de l'article légal ou règlementaire en cours…
                 </p>
               {:then article}
-                {@const metaArticle = article.META.META_SPEC.META_ARTICLE}
-                {@const titreTxt = article.CONTEXTE.TEXTE.TITRE_TXT}
-                {@const titreTexte = (
-                  Array.isArray(titreTxt) ? titreTxt[0] : titreTxt
-                )["#text"]}
-                <h4
-                  class="mb-4 font-serif text-2xl italic text-gray-700 md:text-3xl"
-                >
-                  <Icon
-                    class="mr-1 inline-flex h-7 w-7 place-self-center  text-le-gris-dispositif-light"
-                    icon="ri-map-pin-2-fill"
-                  />
-                  <span class="font-bold">Article {metaArticle.NUM}</span>
-                  - {titreTexte}
-                </h4>
-
-                <div class="prose">
-                  {@html article.BLOC_TEXTUEL.CONTENU}
-                </div>
-
-                <div class="mt-4 text-right text-sm text-gray-500">
-                  {#if metaArticle.DATE_DEBUT !== "2999-01-01"}
-                    <p>
-                      Article en vigueur {#if metaArticle.DATE_FIN === "2999-01-01"}depuis
-                        le {dateFormatter.format(
-                          new Date(metaArticle.DATE_DEBUT),
-                        )}{:else}du {dateFormatter.format(
-                          new Date(metaArticle.DATE_DEBUT),
-                        )} au {dateFormatter.format(
-                          new Date(metaArticle.DATE_FIN),
-                        )}{/if}
-                    </p>
-                  {/if}
-                  <span class="text-sm text-gray-500 md:text-base">
-                    <a
-                      class="link"
-                      href={firstLegalReference.href}
-                      target="_blank">Voir l'article sur Légifrance.fr</a
-                    >
-                  </span>
-                </div>
+                <ArticleView {article} />
               {/await}
             {:else}
               <p>Aucune référence légale ou règlementaire trouvée</p>
diff --git a/src/lib/users.ts b/src/lib/users.ts
new file mode 100644
index 0000000000000000000000000000000000000000..19b723dc090dc704a879a7164dbce2c061d9638f
--- /dev/null
+++ b/src/lib/users.ts
@@ -0,0 +1,12 @@
+export interface User {
+  email: string // "john@doe.com"
+  email_verified: boolean
+  family_name: string // "Doe"
+  given_name: string // "John"
+  last_name: string // "Doe"
+  locale: string // "fr"
+  name: string // "John Doe"
+  preferred_username: string // "jdoe",
+  roles?: string[] // [ 'offline_access', 'default-roles-leximpact', 'uma_authorization' ],
+  sub: string // "12345678-9abc-def0-1234-56789abcdef0"
+}