diff --git a/packages/api/src/api/rest/demarches.ts b/packages/api/src/api/rest/demarches.ts
index c3de75315aad638db6a9dc1044ddafdade7554e2..1ba20b73e6fef22ccbb4910b2a68242b7dd1fcb7 100644
--- a/packages/api/src/api/rest/demarches.ts
+++ b/packages/api/src/api/rest/demarches.ts
@@ -25,7 +25,7 @@ import { isDemarcheTypeId, isTravaux } from 'camino-common/src/static/demarchesT
 import { titreGet } from '../../database/queries/titres'
 import { CaminoApiError } from '../../types'
 import { EffectDbQueryAndValidateErrors } from '../../pg-database'
-import { callAndExit } from '../../tools/fp-tools'
+import { callAndExit, filterOrFailFromValidWithError } from '../../tools/fp-tools'
 
 const canReadDemarcheError = 'impossible de savoir si on peut lire la démarche' as const
 
@@ -192,14 +192,9 @@ export const getResultatEnConcurrence: RestNewGetCall<'/rest/demarches/:demarche
   return rootPipe.pipe(
     Effect.bind('etapes', ({ pool, params }) => getEtapesByDemarcheId(pool, params.demarcheId)),
     Effect.bind('demarche', ({ pool, params }) => getDemarcheByIdOrSlug(pool, params.demarcheId)),
-    Effect.tap(({ etapes, demarche, user }) => {
-      const result = canPublishResultatMiseEnConcurrence(user, demarche.titre_type_id, demarche.demarche_type_id, etapes, demarche.demarche_id)
-      if (result.valid) {
-        return Effect.succeed(null)
-      } else {
-        return Effect.fail({ message: 'droits insuffisants' as const, detail: result.error })
-      }
-    }),
+    Effect.tap(({ etapes, demarche, user }) =>
+      filterOrFailFromValidWithError(canPublishResultatMiseEnConcurrence(user, demarche.titre_type_id, demarche.demarche_type_id, etapes, demarche.demarche_id), 'droits insuffisants' as const)
+    ),
     Effect.flatMap(({ pool, params, user }) => getDemarchePivotEnConcurrence(pool, params.demarcheId, user)),
     Effect.mapError(caminoError =>
       Match.value(caminoError.message).pipe(
diff --git a/packages/api/src/api/rest/titres.test.integration.ts b/packages/api/src/api/rest/titres.test.integration.ts
index 8701c1a8264e23fe4afb0e27c5ed4a2011801c93..d1193926ad68fca93d6bfb65d163a37aada57b90 100644
--- a/packages/api/src/api/rest/titres.test.integration.ts
+++ b/packages/api/src/api/rest/titres.test.integration.ts
@@ -141,7 +141,7 @@ async function createTitreWithEtapes(
   etapes: (Omit<ITitreEtape, 'id' | 'titreDemarcheId' | 'concurrence' | 'demarcheIdsConsentement' | 'hasTitreFrom'> & { demarcheIdsConsentement?: DemarcheId[] })[],
   entreprises: any
 ) {
-  const titreId = newTitreId()
+  const titreId = newTitreId(`id-${nomTitre}`)
   await insertTitreGraph({
     id: titreId,
     nom: nomTitre,
@@ -212,7 +212,7 @@ describe('titresLiaisons', () => {
     )
     const titreId = getTitres.body[0].id
 
-    const axmId = newTitreId()
+    const axmId = newTitreId('titreIdTitreLie')
     const axmNom = 'mon axm simple'
     await insertTitreGraph({
       id: axmId,
@@ -222,7 +222,7 @@ describe('titresLiaisons', () => {
       propsTitreEtapesIds: {},
     })
 
-    const tested = await restNewPostCall(
+    let tested = await restNewPostCall(
       dbPool,
       '/rest/titres/:id/titreLiaisons',
       { id: axmId },
@@ -233,15 +233,19 @@ describe('titresLiaisons', () => {
       [titreId]
     )
 
-    expect(tested.statusCode).toBe(200)
-    expect(tested.body.amont).toHaveLength(1)
-    expect(tested.body.aval).toHaveLength(0)
-    expect(tested.body.amont[0]).toStrictEqual({
-      id: titreId,
-      nom: getTitres.body[0].nom,
-    })
+    expect(tested.body).toMatchInlineSnapshot(`
+      {
+        "amont": [
+          {
+            "id": "id-titre1",
+            "nom": "titre1",
+          },
+        ],
+        "aval": [],
+      }
+    `)
 
-    const avalTested = await restNewCall(
+    let avalTested = await restNewCall(
       dbPool,
       '/rest/titres/:id/titreLiaisons',
       { id: titreId },
@@ -251,13 +255,52 @@ describe('titresLiaisons', () => {
       }
     )
 
-    expect(avalTested.statusCode).toBe(200)
-    expect(avalTested.body.amont).toHaveLength(0)
-    expect(avalTested.body.aval).toHaveLength(1)
-    expect(avalTested.body.aval[0]).toStrictEqual({
-      id: axmId,
-      nom: axmNom,
-    })
+    expect(avalTested.body).toMatchInlineSnapshot(`
+      {
+        "amont": [],
+        "aval": [
+          {
+            "id": "titreIdTitreLie",
+            "nom": "mon axm simple",
+          },
+        ],
+      }
+    `)
+
+    // On vérifie qu'on peut bien supprimer les liens
+    tested = await restNewPostCall(
+      dbPool,
+      '/rest/titres/:id/titreLiaisons',
+      { id: axmId },
+      {
+        role: 'admin',
+        administrationId: ADMINISTRATION_IDS['DGTM - GUYANE'],
+      },
+      []
+    )
+    expect(tested.body).toMatchInlineSnapshot(`
+      {
+        "amont": [],
+        "aval": [],
+      }
+    `)
+
+    avalTested = await restNewCall(
+      dbPool,
+      '/rest/titres/:id/titreLiaisons',
+      { id: titreId },
+      {
+        role: 'admin',
+        administrationId: ADMINISTRATION_IDS['DGTM - GUYANE'],
+      }
+    )
+
+    expect(avalTested.body).toMatchInlineSnapshot(`
+      {
+        "amont": [],
+        "aval": [],
+      }
+    `)
   })
 })
 
diff --git a/packages/api/src/api/rest/titres.ts b/packages/api/src/api/rest/titres.ts
index 61a0ded5431cae9b964e9771c129dabbcf587996..c44b988e06999763470ce3264f0959f1f12271ba 100644
--- a/packages/api/src/api/rest/titres.ts
+++ b/packages/api/src/api/rest/titres.ts
@@ -5,7 +5,7 @@ import { CaminoRequest, CustomResponse } from './express-type'
 import { userSuper } from '../../database/user-super'
 import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, onlyUnique } from 'camino-common/src/typescript-tools'
 import { CaminoApiError } from '../../types'
-import { NotNullableKeys, isNullOrUndefinedOrEmpty } from 'camino-common/src/typescript-tools'
+import { isNullOrUndefinedOrEmpty } from 'camino-common/src/typescript-tools'
 import TitresTitres from '../../database/models/titres--titres'
 import { titreAdministrationsGet } from '../_format/titres'
 import { canDeleteTitre, canEditTitre, canLinkTitres } from 'camino-common/src/permissions/titres'
@@ -31,6 +31,7 @@ import { CaminoMachines, machineFind } from '../../business/rules-demarches/mach
 import { demarcheEnregistrementDemandeDateFind } from 'camino-common/src/demarche'
 import { DBTitre } from '../../database/models/titres'
 import { AdministrationId } from 'camino-common/src/static/administrations'
+import { filterOrFailFromValid } from '../../tools/fp-tools'
 
 const etapesAMasquer = [ETAPES_TYPES.classementSansSuite, ETAPES_TYPES.desistementDuDemandeur, ETAPES_TYPES.demandeDeComplements_RecevabiliteDeLaDemande_]
 
@@ -232,15 +233,16 @@ export const postTitreLiaisons: RestNewPostCall<'/rest/titres/:id/titreLiaisons'
             user
           ),
         catch: e => ({ message: "Impossible d'exécuter la requête dans la base de données" as const, extra: e }),
-      })
-    ),
-    Effect.filterOrFail(
-      (binded): binded is NotNullableKeys<typeof binded> => isNotNullNorUndefined(binded.titre),
-      () => ({ message: droitInsuffisant })
+      }).pipe(
+        Effect.filterOrFail(
+          titre => isNotNullNorUndefined(titre),
+          () => ({ message: droitInsuffisant })
+        )
+      )
     ),
     Effect.bind('administrations', ({ titre }) =>
-      Effect.tryPromise({
-        try: async () => titreAdministrationsGet(titre),
+      Effect.try({
+        try: () => titreAdministrationsGet(titre),
         catch: e => ({ message: "Impossible d'exécuter la requête dans la base de données" as const, extra: e }),
       })
     ),
@@ -252,23 +254,16 @@ export const postTitreLiaisons: RestNewPostCall<'/rest/titres/:id/titreLiaisons'
       ({ titre }) => isNotNullNorUndefined(titre.demarches),
       () => ({ message: demarcheNonChargeesError })
     ),
-    Effect.bind('titresFrom', ({ body, user }) =>
-      Effect.tryPromise({
-        try: async () => titresGet({ ids: [...body] }, { fields: { id: {} } }, user),
-        catch: e => ({ message: "Impossible d'exécuter la requête dans la base de données" as const, extra: e }),
-      })
-    ),
-    Effect.tap(({ titre, titresFrom, body }) => {
-      const result = checkTitreLinks(titre.typeId, body, titresFrom, titre.demarches ?? [])
-
-      if (result.valid) {
-        return Effect.succeed(null)
-      } else {
-        console.warn(result.errors)
-
-        return Effect.fail({ message: result.errors[0], extra: result.errors })
+    Effect.bind('titresFrom', ({ body, user }) => {
+      if (body.length > 0) {
+        return Effect.tryPromise({
+          try: () => titresGet({ ids: [...body] }, { fields: { id: {} } }, user),
+          catch: e => ({ message: "Impossible d'exécuter la requête dans la base de données" as const, extra: e }),
+        })
       }
+      return Effect.succeed([])
     }),
+    Effect.tap(({ titre, titresFrom, body }) => filterOrFailFromValid(checkTitreLinks(titre.typeId, body, titresFrom, titre.demarches ?? []))),
     Effect.tap(({ pool, params, body }) => linkTitres(pool, { linkTo: params.id, linkFrom: body })),
     Effect.bind('amont', ({ params, user }) => titreLinksGet(params.id, 'titreFromId', user)),
     Effect.bind('aval', ({ params, user }) => titreLinksGet(params.id, 'titreToId', user)),
diff --git a/packages/api/src/business/validations/titre-links-validate.ts b/packages/api/src/business/validations/titre-links-validate.ts
index 867632301e883a44179fe3586246479ac115915d..5dd14fe26e4fd81cc526fdbbeea2f0ffb118fcfb 100644
--- a/packages/api/src/business/validations/titre-links-validate.ts
+++ b/packages/api/src/business/validations/titre-links-validate.ts
@@ -1,19 +1,14 @@
 import { TitreId } from 'camino-common/src/validators/titres'
 import { ITitre, ITitreDemarche } from '../../types'
 import { getLinkConfig } from 'camino-common/src/permissions/titres'
-import { NonEmptyArray, isNonEmptyArray, isNullOrUndefined } from 'camino-common/src/typescript-tools'
+import { CaminoValid, isNonEmptyArray, isNullOrUndefined } from 'camino-common/src/typescript-tools'
 import { TitreTypeId } from 'camino-common/src/static/titresTypes'
 
 const linkImpossible = 'ce titre ne peut pas être lié à d’autres titres' as const
 const oneLinkTitre = 'ce titre peut avoir un seul titre lié' as const
 const droitsInsuffisants = 'droits insuffisants ou titre inexistant' as const
 export type CheckTitreLinksError = typeof linkImpossible | typeof oneLinkTitre | typeof droitsInsuffisants | 'lien incompatible entre ces types de titre'
-export const checkTitreLinks = (
-  titreTypeId: TitreTypeId,
-  titreFromIds: Readonly<TitreId[]>,
-  titresFrom: ITitre[],
-  demarches: ITitreDemarche[]
-): { valid: true } | { valid: false; errors: NonEmptyArray<CheckTitreLinksError> } => {
+export const checkTitreLinks = (titreTypeId: TitreTypeId, titreFromIds: Readonly<TitreId[]>, titresFrom: ITitre[], demarches: ITitreDemarche[]): CaminoValid<CheckTitreLinksError> => {
   const linkConfig = getLinkConfig(
     titreTypeId,
     demarches.map(({ typeId }) => ({ demarche_type_id: typeId }))
@@ -39,5 +34,5 @@ export const checkTitreLinks = (
     return { valid: false, errors }
   }
 
-  return { valid: true }
+  return { valid: true, errors: null }
 }
diff --git a/packages/api/src/tools/fp-tools.ts b/packages/api/src/tools/fp-tools.ts
index 5ced5c46047e0ea070a5d50959a484e4f78f453e..336794612f844ff176c45d4ffd9c0a185c668354 100644
--- a/packages/api/src/tools/fp-tools.ts
+++ b/packages/api/src/tools/fp-tools.ts
@@ -1,6 +1,6 @@
-import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
+import { CaminoValid, isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
 import { CaminoError, CaminoZodErrorReadableMessage, translateIssue } from 'camino-common/src/zod-tools'
-import { Cause, Effect, Exit, pipe } from 'effect'
+import { Cause, Effect, Exit, Option, pipe } from 'effect'
 import { ZodTypeAny } from 'zod'
 import { fromError, isZodErrorLike } from 'zod-validation-error'
 
@@ -73,3 +73,9 @@ export const shortCircuitError = <T extends string>(value: T, ...stuff: unknown[
   console.debug(`shortCircuit ${value}`, ...stuff)
   return { _tag: value }
 }
+
+export const filterOrFailFromValid = <T extends string>(value: CaminoValid<T>): Effect.Effect<void, CaminoError<T>> =>
+  value.valid ? Effect.succeed(Option.none) : Effect.fail({ message: value.errors[0], detail: value.errors.join(', ') })
+
+export const filterOrFailFromValidWithError = <ErrorMessage extends string>(value: CaminoValid<string>, errorMessage: ErrorMessage): Effect.Effect<void, CaminoError<ErrorMessage>> =>
+  value.valid ? Effect.succeed(Option.none) : Effect.fail({ message: errorMessage, detail: value.errors.join(', ') })
diff --git a/packages/common/src/permissions/titres-demarches.test.ts b/packages/common/src/permissions/titres-demarches.test.ts
index af81bbccd8e339f148d0dd8ae07258a96823fc1b..38749f47d24194052d9e4148101070c729fccc0a 100644
--- a/packages/common/src/permissions/titres-demarches.test.ts
+++ b/packages/common/src/permissions/titres-demarches.test.ts
@@ -118,11 +118,13 @@ describe('canPublishResultatMiseEnConcurrence', () => {
   test('machine non procédure spécifique', () => {
     expect(canPublishResultatMiseEnConcurrence({ ...testBlankUser, role: 'super' }, 'arm', 'oct', [{ date: toCaminoDate('2020-01-01'), etape_statut_id: 'fai', etape_type_id: 'mfr' }], demarcheId))
       .toMatchInlineSnapshot(`
-      {
-        "error": "Cette démarche n'est pas une procédure spécifique",
-        "valid": false,
-      }
-    `)
+        {
+          "errors": [
+            "Cette démarche n'est pas une procédure spécifique",
+          ],
+          "valid": false,
+        }
+      `)
   })
   test('mise en concurrence non terminée', () => {
     expect(
@@ -135,7 +137,9 @@ describe('canPublishResultatMiseEnConcurrence', () => {
       )
     ).toMatchInlineSnapshot(`
       {
-        "error": "Cette démarche n'a pas terminé sa mise en concurrence",
+        "errors": [
+          "Cette démarche n'a pas terminé sa mise en concurrence",
+        ],
         "valid": false,
       }
     `)
@@ -158,7 +162,9 @@ describe('canPublishResultatMiseEnConcurrence', () => {
       )
     ).toMatchInlineSnapshot(`
       {
-        "error": "L'utilisateur ne dispose pas des droits suffisants",
+        "errors": [
+          "L'utilisateur ne dispose pas des droits suffisants",
+        ],
         "valid": false,
       }
     `)
@@ -167,7 +173,9 @@ describe('canPublishResultatMiseEnConcurrence', () => {
   test("pas d'étapes", () => {
     expect(canPublishResultatMiseEnConcurrence({ ...testBlankUser, role: 'super' }, 'arm', 'oct', [], demarcheId)).toMatchInlineSnapshot(`
       {
-        "error": "Au moins une étape est nécessaire",
+        "errors": [
+          "Au moins une étape est nécessaire",
+        ],
         "valid": false,
       }
     `)
@@ -191,7 +199,7 @@ describe('canPublishResultatMiseEnConcurrence', () => {
       )
     ).toMatchInlineSnapshot(`
       {
-        "error": null,
+        "errors": null,
         "valid": true,
       }
     `)
@@ -221,7 +229,9 @@ describe('canPublishResultatMiseEnConcurrence', () => {
       )
     ).toMatchInlineSnapshot(`
       {
-        "error": "Cette démarche a déja un résultat final de la mise en concurrence",
+        "errors": [
+          "Cette démarche a déja un résultat final de la mise en concurrence",
+        ],
         "valid": false,
       }
     `)
diff --git a/packages/common/src/permissions/titres-demarches.ts b/packages/common/src/permissions/titres-demarches.ts
index 34ae82575c08d03da4c9a4cb7cf301c8223d0407..9e1c332345e77f5ee967a6730e58d13037d626ec 100644
--- a/packages/common/src/permissions/titres-demarches.ts
+++ b/packages/common/src/permissions/titres-demarches.ts
@@ -8,7 +8,7 @@ import { getEtapesTDE } from '../static/titresTypes_demarchesTypes_etapesTypes/i
 import { DemarcheTypeId } from '../static/demarchesTypes'
 import { canCreateEtape } from './titres-etapes'
 import { TitreGetDemarche } from '../titres'
-import { isNotNullNorUndefinedNorEmpty, isNullOrUndefined } from '../typescript-tools'
+import { CaminoValid, isNotNullNorUndefinedNorEmpty, isNullOrUndefined } from '../typescript-tools'
 import { ETAPE_IS_BROUILLON } from '../etape'
 import { demarcheEnregistrementDemandeDateFind, DemarcheEtape, DemarcheId } from '../demarche'
 import { machineIdFind } from '../machines'
@@ -94,19 +94,19 @@ export const canPublishResultatMiseEnConcurrence = (
   demarche_type_id: DemarcheTypeId,
   etapes: Pick<DemarcheEtape, 'etape_type_id' | 'demarche_id_en_concurrence' | 'date' | 'etape_statut_id'>[],
   id: DemarcheId
-): { valid: true; error: null } | { valid: false; error: string } => {
+): CaminoValid<string> => {
   if (!isSuper(user) && !isAdministrationAdmin(user) && !isAdministrationEditeur(user)) {
-    return { valid: false, error: "L'utilisateur ne dispose pas des droits suffisants" }
+    return { valid: false, errors: ["L'utilisateur ne dispose pas des droits suffisants"] }
   }
 
   const firstEtapeDate = demarcheEnregistrementDemandeDateFind(etapes.map(etape => ({ ...etape, typeId: etape.etape_type_id })))
   if (isNullOrUndefined(firstEtapeDate)) {
-    return { valid: false, error: 'Au moins une étape est nécessaire' }
+    return { valid: false, errors: ['Au moins une étape est nécessaire'] }
   }
   const machineId = machineIdFind(titre_type_id, demarche_type_id, id, firstEtapeDate)
 
   if (machineId !== 'ProcedureSpecifique') {
-    return { valid: false, error: "Cette démarche n'est pas une procédure spécifique" }
+    return { valid: false, errors: ["Cette démarche n'est pas une procédure spécifique"] }
   }
 
   if (
@@ -115,12 +115,12 @@ export const canPublishResultatMiseEnConcurrence = (
         etape_type_id === ETAPES_TYPES.avisDeMiseEnConcurrenceAuJORF && etape_statut_id === EtapesTypesEtapesStatuts.avisDeMiseEnConcurrenceAuJORF.TERMINE.etapeStatutId
     )
   ) {
-    return { valid: false, error: "Cette démarche n'a pas terminé sa mise en concurrence" }
+    return { valid: false, errors: ["Cette démarche n'a pas terminé sa mise en concurrence"] }
   }
 
   if (etapes.some(({ etape_type_id }) => etape_type_id === ETAPES_TYPES.resultatMiseEnConcurrence)) {
-    return { valid: false, error: `Cette démarche a déja un ${EtapesTypes[ETAPES_TYPES.resultatMiseEnConcurrence].nom}` }
+    return { valid: false, errors: [`Cette démarche a déja un ${EtapesTypes[ETAPES_TYPES.resultatMiseEnConcurrence].nom}`] }
   }
 
-  return { valid: true, error: null }
+  return { valid: true, errors: null }
 }
diff --git a/packages/common/src/permissions/titres.ts b/packages/common/src/permissions/titres.ts
index fbf7a9368df304e53161dfd81ffe1dee1bb1e388..12f1348736a7871178c08c141501605cfd50e8e7 100644
--- a/packages/common/src/permissions/titres.ts
+++ b/packages/common/src/permissions/titres.ts
@@ -16,7 +16,8 @@ import { EntrepriseId } from '../entreprise'
 import { TITRES_TYPES_TYPES_IDS } from '../static/titresTypesTypes'
 
 export const canSeeTitreLastModifiedDate = (user: User): boolean => isSuper(user) || isAdministration(user)
-export const getLinkConfig = (typeId: TitreTypeId, demarches: { demarche_type_id: DemarcheTypeId }[]): { count: 'single' | 'multiple'; typeId: TitreTypeId } | null => {
+export type LinkConfig = { count: 'single' | 'multiple'; typeId: TitreTypeId }
+export const getLinkConfig = (typeId: TitreTypeId, demarches: { demarche_type_id: DemarcheTypeId }[]): LinkConfig | null => {
   const titreType = TitresTypes[typeId]
 
   if (titreType.typeId === TITRES_TYPES_TYPES_IDS.CONCESSION && demarches.some(({ demarche_type_id }) => demarche_type_id === DEMARCHES_TYPES_IDS.Fusion)) {
diff --git a/packages/common/src/static/__snapshots__/substancesLegales.test.ts.snap b/packages/common/src/static/__snapshots__/substancesLegales.test.ts.snap
deleted file mode 100644
index e6df61632223177659479515ae8541c946bc2a81..0000000000000000000000000000000000000000
--- a/packages/common/src/static/__snapshots__/substancesLegales.test.ts.snap
+++ /dev/null
@@ -1,77 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[`substancesFiscalesBySubstanceLegale 1`] = `
-[
-  {
-    "calculFiscalite": {
-      "unite": "mkg",
-    },
-    "description": "contenu dans les minerais",
-    "id": "auru",
-    "nom": "or",
-    "substanceLegaleId": "auru",
-    "uniteId": "mgr",
-  },
-]
-`;
-
-exports[`substancesFiscalesBySubstanceLegale 2`] = `
-[
-  {
-    "description": "bauxite nettes livrées",
-    "id": "aloh",
-    "nom": "bauxite",
-    "substanceLegaleId": "aloh",
-    "uniteId": "mtk",
-  },
-  {
-    "description": "contenu dans les minerais",
-    "id": "cuiv",
-    "nom": "cuivre",
-    "substanceLegaleId": "cuiv",
-    "uniteId": "mtt",
-  },
-  {
-    "description": "contenu dans les minerais",
-    "id": "etai",
-    "nom": "étain",
-    "substanceLegaleId": "etai",
-    "uniteId": "mtt",
-  },
-  {
-    "description": "net livré",
-    "id": "fera",
-    "nom": "pyrite de fer",
-    "substanceLegaleId": "ferx",
-    "uniteId": "mtk",
-  },
-  {
-    "description": "net livré",
-    "id": "ferb",
-    "nom": "minerais de fer",
-    "substanceLegaleId": "ferx",
-    "uniteId": "mtk",
-  },
-  {
-    "description": "contenu dans les minerais",
-    "id": "mang",
-    "nom": "manganèse",
-    "substanceLegaleId": "mang",
-    "uniteId": "mtc",
-  },
-  {
-    "description": "contenu dans les minerais",
-    "id": "plom",
-    "nom": "plomb",
-    "substanceLegaleId": "plom",
-    "uniteId": "mtc",
-  },
-  {
-    "description": "contenu dans les minerais",
-    "id": "zinc",
-    "nom": "zinc",
-    "substanceLegaleId": "zinc",
-    "uniteId": "mtc",
-  },
-]
-`;
diff --git a/packages/common/src/typescript-tools.ts b/packages/common/src/typescript-tools.ts
index 25d913ebb6146aa756cd7bb61d9cd838399015c9..94a4099e1e681bda1a05891563c110a15bc37c7b 100644
--- a/packages/common/src/typescript-tools.ts
+++ b/packages/common/src/typescript-tools.ts
@@ -75,6 +75,8 @@ export const exhaustiveCheck = (param: never): never => {
   throw new Error(`Unreachable case: ${JSON.stringify(param)}`)
 }
 
+export type CaminoValid<T extends string> = { valid: true; errors: null } | { valid: false; errors: NonEmptyArray<T> }
+
 export type NonEmptyArray<T> = [T, ...T[]]
 export const isNonEmptyArray = <T>(arr: T[]): arr is NonEmptyArray<T> => {
   return arr.length > 0
diff --git a/packages/ui/src/components/titre.stories_snapshots_AbattisKoticaOctroi.html b/packages/ui/src/components/titre.stories_snapshots_AbattisKoticaOctroi.html
index 554912ee99bf156b9c6c17ece54f534275a191c9..4309bf9ac3ff58f710f43a1b79dbed2c6dc36ae0 100644
--- a/packages/ui/src/components/titre.stories_snapshots_AbattisKoticaOctroi.html
+++ b/packages/ui/src/components/titre.stories_snapshots_AbattisKoticaOctroi.html
@@ -28,6 +28,7 @@
       <div>
         <!---->
         <!---->
+        <!---->
       </div>
     </div>
     <!---->
diff --git a/packages/ui/src/components/titre.stories_snapshots_BasseManaMod.html b/packages/ui/src/components/titre.stories_snapshots_BasseManaMod.html
index 503bd2710e9afd67d46e212a8954e061f9c75a42..2ac6e2f493d0f7188e9a4aae993758aab6adca4a 100644
--- a/packages/ui/src/components/titre.stories_snapshots_BasseManaMod.html
+++ b/packages/ui/src/components/titre.stories_snapshots_BasseManaMod.html
@@ -28,6 +28,7 @@
       <div>
         <!---->
         <!---->
+        <!---->
       </div>
     </div>
     <!---->
diff --git a/packages/ui/src/components/titre.stories_snapshots_BonEspoirOctroi.html b/packages/ui/src/components/titre.stories_snapshots_BonEspoirOctroi.html
index 8e2db12e791c760b42f2e17b8b67f5333cd22848..910a4e7c13e0422297b1f9024e357cbead4d6c53 100644
--- a/packages/ui/src/components/titre.stories_snapshots_BonEspoirOctroi.html
+++ b/packages/ui/src/components/titre.stories_snapshots_BonEspoirOctroi.html
@@ -28,6 +28,7 @@
       <div>
         <!---->
         <!---->
+        <!---->
       </div>
     </div>
     <!---->
diff --git a/packages/ui/src/components/titre.stories_snapshots_BonEspoirProlongation2.html b/packages/ui/src/components/titre.stories_snapshots_BonEspoirProlongation2.html
index 6c3e3960ed8a72f24b6b1a56914e65d48bbec050..fd0a69a2006e51e172fef850e39161937d5aa6fe 100644
--- a/packages/ui/src/components/titre.stories_snapshots_BonEspoirProlongation2.html
+++ b/packages/ui/src/components/titre.stories_snapshots_BonEspoirProlongation2.html
@@ -28,6 +28,7 @@
       <div>
         <!---->
         <!---->
+        <!---->
       </div>
     </div>
     <!---->
diff --git a/packages/ui/src/components/titre.stories_snapshots_BonEspoirTravaux.html b/packages/ui/src/components/titre.stories_snapshots_BonEspoirTravaux.html
index 2f93b97b0b8998f38f158954eeca366951840508..3dbda53e32f3c07709b0266596d9752b79f443f0 100644
--- a/packages/ui/src/components/titre.stories_snapshots_BonEspoirTravaux.html
+++ b/packages/ui/src/components/titre.stories_snapshots_BonEspoirTravaux.html
@@ -28,6 +28,7 @@
       <div>
         <!---->
         <!---->
+        <!---->
       </div>
     </div>
     <!---->
diff --git a/packages/ui/src/components/titre.stories_snapshots_ChantepieMutation.html b/packages/ui/src/components/titre.stories_snapshots_ChantepieMutation.html
index ae95a59ba2722c7cb96824e5d4a2ea4cfa82a7fe..4a19fbb8e345545633d31abc0e2fb15521f7a399 100644
--- a/packages/ui/src/components/titre.stories_snapshots_ChantepieMutation.html
+++ b/packages/ui/src/components/titre.stories_snapshots_ChantepieMutation.html
@@ -27,6 +27,10 @@
         <p>Il manque 2 rapports d'activités. <a href="/mocked-href" title="Remplir les rapports d'activités" class="fr-link" aria-label="Remplir les rapports d'activités">Remplir les rapports d'activités</a></p>
       </div><a href="/mocked-href" title="Consulter les rapports d'activités" class="fr-mt-2w fr-btn fr-btn--secondary" aria-label="Consulter les rapports d'activités">Consulter les rapports d'activités</a>
       <div>
+        <div style="display: flex; gap: 0.5rem; align-items: center;"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Lier un titre <div class="flex flex-center" style="gap: 0.5rem;"><button class="fr-btn fr-btn--tertiary-no-outline fr-btn--md fr-icon-pencil-line" title="modifier les titres liés" aria-label="modifier les titres liés" type="button">
+              <!---->
+            </button></div>
+        </div>
         <!---->
         <!---->
       </div>
diff --git a/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroi.html b/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroi.html
index 44f706b80836a562956795f556e54e2930bb084a..9173dcdef3e269deee296adf772189cd81490935 100644
--- a/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroi.html
+++ b/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroi.html
@@ -28,6 +28,10 @@
         <p>Il manque 2 rapports d'activités. <a href="/mocked-href" title="Remplir les rapports d'activités" class="fr-link" aria-label="Remplir les rapports d'activités">Remplir les rapports d'activités</a></p>
       </div><a href="/mocked-href" title="Consulter les rapports d'activités" class="fr-mt-2w fr-btn fr-btn--secondary" aria-label="Consulter les rapports d'activités">Consulter les rapports d'activités</a>
       <div>
+        <div style="display: flex; gap: 0.5rem; align-items: center;"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Lier un titre <div class="flex flex-center" style="gap: 0.5rem;"><button class="fr-btn fr-btn--tertiary-no-outline fr-btn--md fr-icon-pencil-line" title="modifier les titres liés" aria-label="modifier les titres liés" type="button">
+              <!---->
+            </button></div>
+        </div>
         <!---->
         <!---->
       </div>
diff --git a/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroiAsEntreprise.html b/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroiAsEntreprise.html
index fed6d71f874dfb9519acee1d7b1aab2ddd92a630..92eea09c403b786b662ada7553fa7f7414fa9ea9 100644
--- a/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroiAsEntreprise.html
+++ b/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroiAsEntreprise.html
@@ -30,6 +30,7 @@
       <div>
         <!---->
         <!---->
+        <!---->
       </div>
     </div>
     <!---->
diff --git a/packages/ui/src/components/titre.stories_snapshots_CriqueAdolpheOctroi.html b/packages/ui/src/components/titre.stories_snapshots_CriqueAdolpheOctroi.html
index 98b672ca762576d96d27a8821e9165547fa8ca78..6841a78b953912f98cf9039264284888b61489f9 100644
--- a/packages/ui/src/components/titre.stories_snapshots_CriqueAdolpheOctroi.html
+++ b/packages/ui/src/components/titre.stories_snapshots_CriqueAdolpheOctroi.html
@@ -28,6 +28,7 @@
       <div>
         <!---->
         <!---->
+        <!---->
       </div>
     </div>
     <!---->
diff --git a/packages/ui/src/components/titre.stories_snapshots_Empty.html b/packages/ui/src/components/titre.stories_snapshots_Empty.html
index b57faa16adba1f5e5024fe92a969fdb89fff6478..e0cd3adf42b4326e970f92680d58648177cce253 100644
--- a/packages/ui/src/components/titre.stories_snapshots_Empty.html
+++ b/packages/ui/src/components/titre.stories_snapshots_Empty.html
@@ -26,6 +26,7 @@
       <div>
         <!---->
         <!---->
+        <!---->
       </div>
     </div>
     <!---->
diff --git a/packages/ui/src/components/titre.stories_snapshots_Full.html b/packages/ui/src/components/titre.stories_snapshots_Full.html
index 68081d2f080c9aba0312577c323a1140b40ab0cc..69fb573745dc604f7e240299a6f3276a47037e22 100644
--- a/packages/ui/src/components/titre.stories_snapshots_Full.html
+++ b/packages/ui/src/components/titre.stories_snapshots_Full.html
@@ -28,6 +28,7 @@
       <div>
         <!---->
         <!---->
+        <!---->
       </div>
     </div>
     <div class="fr-alert fr-alert--warning fr-mt-2w">
diff --git a/packages/ui/src/components/titre.stories_snapshots_Lenoncourt.html b/packages/ui/src/components/titre.stories_snapshots_Lenoncourt.html
index 4b73581da07de799253bbeab35c28c91f69cb218..74b0058378d8b3be38dabf6fef32f6b0ac07d9b8 100644
--- a/packages/ui/src/components/titre.stories_snapshots_Lenoncourt.html
+++ b/packages/ui/src/components/titre.stories_snapshots_Lenoncourt.html
@@ -28,6 +28,7 @@
       <div>
         <!---->
         <!---->
+        <!---->
       </div>
     </div>
     <!---->
diff --git a/packages/ui/src/components/titre.stories_snapshots_TitreAvecUnOctroiEnConstructionEtUnTravaux.html b/packages/ui/src/components/titre.stories_snapshots_TitreAvecUnOctroiEnConstructionEtUnTravaux.html
index e56a8c733f1f1eb3c67dfcf3fddd0827e5107813..15ac8c0c2fe5a693e2d85bba09341f1cf6d6d029 100644
--- a/packages/ui/src/components/titre.stories_snapshots_TitreAvecUnOctroiEnConstructionEtUnTravaux.html
+++ b/packages/ui/src/components/titre.stories_snapshots_TitreAvecUnOctroiEnConstructionEtUnTravaux.html
@@ -28,6 +28,7 @@
       <div>
         <!---->
         <!---->
+        <!---->
       </div>
     </div>
     <div class="fr-alert fr-alert--warning fr-mt-2w">
diff --git a/packages/ui/src/components/titre.stories_snapshots_TitreAvecUneSeuleDemarcheEnConstruction.html b/packages/ui/src/components/titre.stories_snapshots_TitreAvecUneSeuleDemarcheEnConstruction.html
index 26405942646315725f325cbceba8534deae239da..e551ae8de184dd4aa636a2ce865905ea3198c4b8 100644
--- a/packages/ui/src/components/titre.stories_snapshots_TitreAvecUneSeuleDemarcheEnConstruction.html
+++ b/packages/ui/src/components/titre.stories_snapshots_TitreAvecUneSeuleDemarcheEnConstruction.html
@@ -28,6 +28,7 @@
       <div>
         <!---->
         <!---->
+        <!---->
       </div>
     </div>
     <div class="fr-alert fr-alert--warning fr-mt-2w">
diff --git a/packages/ui/src/components/titre.stories_snapshots_WithDoublon.html b/packages/ui/src/components/titre.stories_snapshots_WithDoublon.html
index e3e2e1c3362f39b8dc87cc06a8b26e4753ec7340..bb3aded12ba2136912368016417235f8a5e48637 100644
--- a/packages/ui/src/components/titre.stories_snapshots_WithDoublon.html
+++ b/packages/ui/src/components/titre.stories_snapshots_WithDoublon.html
@@ -28,6 +28,7 @@
       <div>
         <!---->
         <!---->
+        <!---->
       </div>
     </div>
     <!---->
diff --git a/packages/ui/src/components/titre.stories_snapshots_WithLinkableTitreAmont.html b/packages/ui/src/components/titre.stories_snapshots_WithLinkableTitreAmont.html
index 069802e97152b6551f0deb9b883e8c5907ce1224..0fa8b039a15a572191936a3ea1d494bd9fe3a833 100644
--- a/packages/ui/src/components/titre.stories_snapshots_WithLinkableTitreAmont.html
+++ b/packages/ui/src/components/titre.stories_snapshots_WithLinkableTitreAmont.html
@@ -29,6 +29,7 @@
             </button></div>
         </div>
         <!---->
+        <!---->
       </div>
     </div>
     <!---->
diff --git a/packages/ui/src/components/titre.stories_snapshots_WithLinkableTitreAval.html b/packages/ui/src/components/titre.stories_snapshots_WithLinkableTitreAval.html
index 5364209fe2fa2da959827ce8aae2f2cf8ba1ae90..d0e79253ef838d1a0fca9df7a51a998e3eb0e1b5 100644
--- a/packages/ui/src/components/titre.stories_snapshots_WithLinkableTitreAval.html
+++ b/packages/ui/src/components/titre.stories_snapshots_WithLinkableTitreAval.html
@@ -24,9 +24,13 @@
       <!---->
       <!---->
       <div>
-        <!---->
+        <div style="display: flex; gap: 0.5rem; align-items: center;"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Lier un titre <div class="flex flex-center" style="gap: 0.5rem;"><button class="fr-btn fr-btn--tertiary-no-outline fr-btn--md fr-icon-pencil-line" title="modifier les titres liés" aria-label="modifier les titres liés" type="button">
+              <!---->
+            </button></div>
+        </div>
         <div style="display: flex; gap: 0.5rem; align-items: center;" class="fr-mt-1w"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Titre issu de ce titre :<div class="flex flex-center" style="gap: 0.5rem;"><a href="/mocked-href" title="nom du titre en aval" class="fr-tag fr-tag--sm" aria-label="nom du titre en aval">nom du titre en aval</a></div>
         </div>
+        <!---->
       </div>
     </div>
     <!---->
diff --git a/packages/ui/src/components/titre/titres-link-form.stories.tsx b/packages/ui/src/components/titre/titres-link-form.stories.tsx
index 2b7e209327795003cdbdb27999b477117cbc08cf..483db4834215746dc16885f27332b2560580b97c 100644
--- a/packages/ui/src/components/titre/titres-link-form.stories.tsx
+++ b/packages/ui/src/components/titre/titres-link-form.stories.tsx
@@ -54,7 +54,7 @@ const titresFrom: TitreLink[] = [linkableTitres[0]]
 
 const apiClient: Props['apiClient'] = {
   loadLinkableTitres: () => () => Promise.resolve(linkableTitres),
-  loadTitreLinks: () => Promise.resolve({ aval: titresTo, amont: titresFrom }),
+  loadTitreLinks: async () => ({ aval: titresTo, amont: titresFrom }),
   linkTitres: () => new Promise<TitreLinks>(resolve => resolve({ aval: titresTo, amont: titresFrom })),
 }
 
@@ -67,6 +67,15 @@ export const AxmWithAlreadySelectedTitre: StoryFn = () => (
   />
 )
 
+export const AxmWithoutSelectedTitre: StoryFn = () => (
+  <TitresLinkForm
+    user={{ role: 'super', ...testBlankUser }}
+    titre={{ typeId: 'axm', administrations: [], id: titreIdValidator.parse('titreId'), demarches: [] }}
+    apiClient={{ ...apiClient, loadTitreLinks: async () => ({ aval: [], amont: [] }) }}
+    onTitresFromLoaded={() => {}}
+  />
+)
+
 export const AxmWithAlreadySelectedTitreNotEditable: StoryFn = () => (
   <TitresLinkForm
     user={{ role: 'defaut', ...testBlankUser }}
@@ -101,6 +110,26 @@ export const FusionWithAlreadySelectedTitre: StoryFn = () => (
   />
 )
 
+export const FusionWithoutSelectedTitre: StoryFn = () => (
+  <TitresLinkForm
+    user={{ role: 'super', ...testBlankUser }}
+    titre={{
+      typeId: 'cxm',
+      administrations: [],
+      id: titreIdValidator.parse('titreId'),
+      demarches: [{ demarche_type_id: 'fus' }],
+    }}
+    apiClient={{
+      ...apiClient,
+      loadTitreLinks: async () => ({
+        aval: [],
+        amont: [],
+      }),
+    }}
+    onTitresFromLoaded={() => {}}
+  />
+)
+
 export const TitreWithTitreLinksLoading: StoryFn = () => (
   <TitresLinkForm
     user={{ role: 'super', ...testBlankUser }}
diff --git a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithAlreadySelectedTitre.html b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithAlreadySelectedTitre.html
index ce240850c42ed0c4c5d84a788874074ab8014f84..ffb573fa5e5ff691ea9e2f431be2b1253e4659a0 100644
--- a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithAlreadySelectedTitre.html
+++ b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithAlreadySelectedTitre.html
@@ -5,4 +5,5 @@
   </div>
   <div style="display: flex; gap: 0.5rem; align-items: center;" class="fr-mt-1w"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Titre issu de ce titre :<div class="flex flex-center" style="gap: 0.5rem;"><a href="/mocked-href" title="Titre fils" class="fr-tag fr-tag--sm" aria-label="Titre fils">Titre fils</a></div>
   </div>
+  <!---->
 </div>
\ No newline at end of file
diff --git a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithAlreadySelectedTitreNotEditable.html b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithAlreadySelectedTitreNotEditable.html
index c94a248e0d5253d5295b9826485a440943c1d7ec..5cc9908db7cae978b6dd22693fec7b055f7e5b4d 100644
--- a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithAlreadySelectedTitreNotEditable.html
+++ b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithAlreadySelectedTitreNotEditable.html
@@ -5,4 +5,5 @@
   </div>
   <div style="display: flex; gap: 0.5rem; align-items: center;" class="fr-mt-1w"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Titre issu de ce titre :<div class="flex flex-center" style="gap: 0.5rem;"><a href="/mocked-href" title="Titre fils" class="fr-tag fr-tag--sm" aria-label="Titre fils">Titre fils</a></div>
   </div>
+  <!---->
 </div>
\ No newline at end of file
diff --git a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithoutSelectedTitre.html b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithoutSelectedTitre.html
new file mode 100644
index 0000000000000000000000000000000000000000..efa67a367250470a3e549de223d61b7a3898e96e
--- /dev/null
+++ b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_AxmWithoutSelectedTitre.html
@@ -0,0 +1,8 @@
+<div>
+  <div style="display: flex; gap: 0.5rem; align-items: center;"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Lier un titre <div class="flex flex-center" style="gap: 0.5rem;"><button class="fr-btn fr-btn--tertiary-no-outline fr-btn--md fr-icon-pencil-line" title="modifier les titres liés" aria-label="modifier les titres liés" type="button">
+        <!---->
+      </button></div>
+  </div>
+  <!---->
+  <!---->
+</div>
\ No newline at end of file
diff --git a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_DefautCantUpdateLinks.html b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_DefautCantUpdateLinks.html
index c94a248e0d5253d5295b9826485a440943c1d7ec..5cc9908db7cae978b6dd22693fec7b055f7e5b4d 100644
--- a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_DefautCantUpdateLinks.html
+++ b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_DefautCantUpdateLinks.html
@@ -5,4 +5,5 @@
   </div>
   <div style="display: flex; gap: 0.5rem; align-items: center;" class="fr-mt-1w"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Titre issu de ce titre :<div class="flex flex-center" style="gap: 0.5rem;"><a href="/mocked-href" title="Titre fils" class="fr-tag fr-tag--sm" aria-label="Titre fils">Titre fils</a></div>
   </div>
+  <!---->
 </div>
\ No newline at end of file
diff --git a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_FusionWithAlreadySelectedTitre.html b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_FusionWithAlreadySelectedTitre.html
index 45fdcc0494ff4887bce8a03e562f1cf8d17cb77b..dce627fb0a4ea837f0f254f77c982f767b53ea0c 100644
--- a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_FusionWithAlreadySelectedTitre.html
+++ b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_FusionWithAlreadySelectedTitre.html
@@ -3,6 +3,7 @@
         <!---->
       </button></div>
   </div>
-  <div style="display: flex; gap: 0.5rem; align-items: center;" class="fr-mt-1w"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Titres issu de ce titre :<div class="flex flex-center" style="gap: 0.5rem;"><a href="/mocked-href" title="Titre fils" class="fr-tag fr-tag--sm" aria-label="Titre fils">Titre fils</a><a href="/mocked-href" title="Titre fils 2" class="fr-tag fr-tag--sm" aria-label="Titre fils 2">Titre fils 2</a></div>
+  <div style="display: flex; gap: 0.5rem; align-items: center;" class="fr-mt-1w"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Titres issus de ce titre :<div class="flex flex-center" style="gap: 0.5rem;"><a href="/mocked-href" title="Titre fils" class="fr-tag fr-tag--sm" aria-label="Titre fils">Titre fils</a><a href="/mocked-href" title="Titre fils 2" class="fr-tag fr-tag--sm" aria-label="Titre fils 2">Titre fils 2</a></div>
   </div>
+  <!---->
 </div>
\ No newline at end of file
diff --git a/packages/ui/src/components/titre/titres-link-form.stories_snapshots_FusionWithoutSelectedTitre.html b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_FusionWithoutSelectedTitre.html
new file mode 100644
index 0000000000000000000000000000000000000000..07ae89d8e3598fbb2cb296791b6e03a0462510ba
--- /dev/null
+++ b/packages/ui/src/components/titre/titres-link-form.stories_snapshots_FusionWithoutSelectedTitre.html
@@ -0,0 +1,8 @@
+<div>
+  <div style="display: flex; gap: 0.5rem; align-items: center;"><span class="fr-icon-link fr-icon--sm" style="color: var(--text-title-blue-france);" aria-hidden="true"></span>Lier plusieurs titres <div class="flex flex-center" style="gap: 0.5rem;"><button class="fr-btn fr-btn--tertiary-no-outline fr-btn--md fr-icon-pencil-line" title="modifier les titres liés" aria-label="modifier les titres liés" type="button">
+        <!---->
+      </button></div>
+  </div>
+  <!---->
+  <!---->
+</div>
\ No newline at end of file
diff --git a/packages/ui/src/components/titre/titres-link-form.tsx b/packages/ui/src/components/titre/titres-link-form.tsx
index 30f8ad1ddfb4bf434c700e78f67264b9c1762e48..642a01b7b541cf78c29b45eaca9f561f0172c810 100644
--- a/packages/ui/src/components/titre/titres-link-form.tsx
+++ b/packages/ui/src/components/titre/titres-link-form.tsx
@@ -1,5 +1,5 @@
-import { canLinkTitres, getLinkConfig } from 'camino-common/src/permissions/titres'
-import { computed, defineComponent, onMounted, ref, watch } from 'vue'
+import { canLinkTitres, getLinkConfig, LinkConfig } from 'camino-common/src/permissions/titres'
+import { computed, defineComponent, onMounted, watch } from 'vue'
 import { TitreTypeId } from 'camino-common/src/static/titresTypes'
 import { User } from 'camino-common/src/roles'
 import { AdministrationId } from 'camino-common/src/static/administrations'
@@ -11,10 +11,12 @@ import { TitreLink, TitreLinks } from 'camino-common/src/titres'
 import { TitreId } from 'camino-common/src/validators/titres'
 import { ApiClient } from '@/api/api-client'
 import { TitresLinkConfig } from '@/components/titre/titres-link-form-api-client'
-import { DsfrButton, DsfrButtonIcon } from '../_ui/dsfr-button'
+import { DsfrButtonIcon } from '../_ui/dsfr-button'
 import { DsfrIcon } from '../_ui/icon'
 import { DsfrTag } from '../_ui/tag'
 import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty } from 'camino-common/src/typescript-tools'
+import { useState } from '@/utils/vue-tsx-utils'
+import { FunctionalPopup } from '../_ui/functional-popup'
 
 export interface Props {
   user: User
@@ -28,9 +30,16 @@ export interface Props {
   onTitresFromLoaded: (hasTitresFrom: boolean) => void
 }
 export const TitresLinkForm = defineComponent<Props>(props => {
-  const mode = ref<'read' | 'edit'>('read')
-  const selectedTitres = ref<TitreLink[]>([])
-  const titresLinks = ref<AsyncData<TitreLinks>>({ status: 'LOADING' })
+  const [popupOpen, setPopupOpen] = useState<boolean>(false)
+  const [titresLinks, setTitresLinks] = useState<AsyncData<TitreLinks>>({ status: 'LOADING' })
+
+  const openEditPopup = () => {
+    setPopupOpen(true)
+  }
+
+  const closeEditPopup = () => {
+    setPopupOpen(false)
+  }
 
   const linkConfig = computed(() => getLinkConfig(props.titre.typeId, props.titre.demarches))
 
@@ -46,22 +55,21 @@ export const TitresLinkForm = defineComponent<Props>(props => {
   )
 
   const init = async () => {
-    titresLinks.value = { status: 'LOADING' }
+    setTitresLinks({ status: 'LOADING' })
     const result = await props.apiClient.loadTitreLinks(props.titre.id)
     if ('message' in result) {
-      titresLinks.value = {
+      setTitresLinks({
         status: 'NEW_ERROR',
         error: result,
-      }
+      })
     } else {
-      titresLinks.value = { status: 'LOADED', value: result }
+      setTitresLinks({ status: 'LOADED', value: result })
       props.onTitresFromLoaded(result.amont.length > 0)
-      selectedTitres.value = titresLinks.value.value.amont
     }
   }
 
   const canEditLink = computed<boolean>(() => {
-    // On ne peut pas lier si ce type de titre n’accepte pas de liaison
+    // On ne peut pas lier si ce type de titre n'accepte pas de liaison
     if (!linkConfig.value) {
       return false
     }
@@ -69,94 +77,45 @@ export const TitresLinkForm = defineComponent<Props>(props => {
     return canLinkTitres(props.user, props.titre.administrations ?? [])
   })
 
-  const titreLinkConfig = computed<TitresLinkConfig | null>(() => {
-    if (titresLinks.value.status !== 'LOADED') {
-      return null
-    }
-
-    const titreFromIds = titresLinks.value.value.amont.map(({ id }) => id)
-    if (linkConfig.value?.count === 'single') {
-      return {
-        type: 'single',
-        selectedTitreId: titreFromIds.length === 1 ? titreFromIds[0] : null,
-      }
-    }
-
-    return {
-      type: 'multiple',
-      selectedTitreIds: titreFromIds,
-    }
-  })
-
-  const onSelectedTitres = (titres: TitreLink[]) => {
-    selectedTitres.value = titres
-  }
-
-  const saveLink = async () => {
-    titresLinks.value = { status: 'LOADING' }
-    try {
-      const links = await props.apiClient.linkTitres(
-        props.titre.id,
-        selectedTitres.value.map(({ id }) => id)
-      )
-      if ('message' in links) {
-        titresLinks.value = {
-          status: 'NEW_ERROR',
-          error: links,
-        }
-      } else {
-        mode.value = 'read'
-        titresLinks.value = {
+  const myApiClient = {
+    ...props.apiClient,
+    linkTitres: async (titreId: TitreId, titreFromIds: TitreId[]) => {
+      const links = await props.apiClient.linkTitres(titreId, titreFromIds)
+      if (!('message' in links)) {
+        setTitresLinks({
           status: 'LOADED',
           value: links,
-        }
+        })
       }
-    } catch (e: any) {
-      titresLinks.value = {
-        status: 'ERROR',
-        message: e.message ?? 'something wrong happened',
-      }
-    }
+      return links
+    },
   }
 
-  const closeForm = () => (mode.value = 'read')
-
   return () => (
     <div>
       <LoadingElement
         data={titresLinks.value}
         renderItem={item => (
           <>
-            {isNotNullNorUndefinedNorEmpty(item.amont) && isNotNullNorUndefined(linkConfig.value) ? (
+            {(isNotNullNorUndefinedNorEmpty(item.amont) || canEditLink.value) && isNotNullNorUndefined(linkConfig.value) ? (
               <div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
                 <DsfrIcon name="fr-icon-link" size="sm" color="text-title-blue-france" aria-hidden="true" />
-                Titre{item.amont.length > 1 ? 's' : ''} à l'origine de ce titre :
-                {mode.value === 'edit' ? (
-                  <>
-                    {titreLinkConfig.value ? (
-                      <TitresLink config={titreLinkConfig.value} loadLinkableTitres={props.apiClient.loadLinkableTitres(props.titre.typeId, props.titre.demarches)} onSelectTitres={onSelectedTitres} />
-                    ) : null}
-                    <>
-                      <DsfrButton buttonType="primary" title="Enregistrer" onClick={saveLink} />
-                      <DsfrButton buttonType="secondary" title="Annuler" onClick={closeForm} />
-                    </>
-                  </>
-                ) : (
-                  <div class="flex flex-center" style={{ gap: '0.5rem' }}>
-                    {item.amont.map(titreFrom => (
-                      <DsfrTag key={titreFrom.id} tagSize="sm" to={{ name: 'titre', params: { id: titreFrom.id } }} ariaLabel={titreFrom.nom} />
-                    ))}
-
-                    {canEditLink.value ? <DsfrButtonIcon buttonType="tertiary-no-outline" title="modifier les titres liés" onClick={() => (mode.value = 'edit')} icon="fr-icon-pencil-line" /> : null}
-                  </div>
-                )}
+                {item.amont.length === 0 ? <>Lier {linkConfig.value.count === 'single' ? 'un titre' : 'plusieurs titres'} </> : <>Titre{item.amont.length > 1 ? 's' : ''} à l'origine de ce titre :</>}
+
+                <div class="flex flex-center" style={{ gap: '0.5rem' }}>
+                  {item.amont.map(titreFrom => (
+                    <DsfrTag key={titreFrom.id} tagSize="sm" to={{ name: 'titre', params: { id: titreFrom.id } }} ariaLabel={titreFrom.nom} />
+                  ))}
+
+                  {canEditLink.value ? <DsfrButtonIcon buttonType="tertiary-no-outline" title="modifier les titres liés" onClick={openEditPopup} icon="fr-icon-pencil-line" /> : null}
+                </div>
               </div>
             ) : null}
 
             {item.aval.length ? (
               <div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }} class="fr-mt-1w">
                 <DsfrIcon name="fr-icon-link" size="sm" color="text-title-blue-france" aria-hidden="true" />
-                Titre{item.aval.length > 1 ? 's' : ''} issu de ce titre :
+                {item.aval.length > 1 ? 'Titres issus ' : 'Titre issu'} de ce titre :
                 <div class="flex flex-center" style={{ gap: '0.5rem' }}>
                   {item.aval.map(titreTo => (
                     <DsfrTag key={titreTo.id} tagSize="sm" to={{ name: 'titre', params: { id: titreTo.id } }} ariaLabel={titreTo.nom} />
@@ -164,6 +123,9 @@ export const TitresLinkForm = defineComponent<Props>(props => {
                 </div>
               </div>
             ) : null}
+            {popupOpen.value && isNotNullNorUndefined(linkConfig.value) ? (
+              <EditPopup titreLinks={item} linkConfig={linkConfig.value} close={closeEditPopup} apiClient={myApiClient} titre={props.titre} />
+            ) : null}
           </>
         )}
       />
@@ -171,5 +133,65 @@ export const TitresLinkForm = defineComponent<Props>(props => {
   )
 })
 
+interface EditPopupProps {
+  titreLinks: TitreLinks
+  linkConfig: LinkConfig
+  titre: {
+    id: TitreId
+    typeId: TitreTypeId
+    administrations: AdministrationId[]
+    demarches: { demarche_type_id: DemarcheTypeId }[]
+  }
+  close: () => void
+  apiClient: Pick<ApiClient, 'loadTitreLinks' | 'loadLinkableTitres' | 'linkTitres'>
+}
+
+const EditPopup = defineComponent<EditPopupProps>(props => {
+  const [selectedTitres, setSelectedTitres] = useState<TitreLink[]>([])
+  const onSelectedTitres = (titres: TitreLink[]) => {
+    setSelectedTitres(titres)
+  }
+  const titreLinkConfig = computed<TitresLinkConfig>(() => {
+    const titreFromIds = props.titreLinks.amont.map(({ id }) => id)
+    if (props.linkConfig.count === 'single') {
+      return {
+        type: 'single',
+        selectedTitreId: titreFromIds.length === 1 ? titreFromIds[0] : null,
+      }
+    }
+
+    return {
+      type: 'multiple',
+      selectedTitreIds: titreFromIds,
+    }
+  })
+
+  const content = () => (
+    <form>
+      <TitresLink config={titreLinkConfig.value} loadLinkableTitres={props.apiClient.loadLinkableTitres(props.titre.typeId, props.titre.demarches)} onSelectTitres={onSelectedTitres} />
+    </form>
+  )
+
+  return () => (
+    <FunctionalPopup
+      title="Modification des liens entre les titres"
+      content={content}
+      close={props.close}
+      validate={{
+        action: () =>
+          props.apiClient.linkTitres(
+            props.titre.id,
+            selectedTitres.value.map(({ id }) => id)
+          ),
+        text: 'Enregistrer',
+      }}
+      canValidate={true}
+    />
+  )
+})
+
 // @ts-ignore waiting for https://github.com/vuejs/core/issues/7833
 TitresLinkForm.props = ['apiClient', 'titre', 'user', 'onTitresFromLoaded']
+
+// @ts-ignore waiting for https://github.com/vuejs/core/issues/7833
+EditPopup.props = ['apiClient', 'titre', 'titreLinks', 'linkConfig', 'close']