11/* eslint-disable @typescript-eslint/triple-slash-reference */
22/// <reference types="turnstile-types"/>
3- import { ifPresent } from "#elements/utils/attributes" ;
4-
53import { CaptchaController } from "#flow/stages/captcha/controllers/CaptchaController" ;
64
75import { TurnstileObject } from "turnstile-types" ;
@@ -20,7 +18,16 @@ export class TurnstileController extends CaptchaController {
2018 public prepareURL = ( ) : URL | null => {
2119 const input = this . host . challenge ?. jsUrl ;
2220
23- return input && URL . canParse ( input ) ? new URL ( input ) : null ;
21+ if ( ! input || ! URL . canParse ( input ) ) return null ;
22+
23+ const url = new URL ( input ) ;
24+
25+ // Use explicit rendering to prevent Turnstile's 3-hour self-upgrade
26+ // from calling implicitRenderAll() and duplicating widgets.
27+ url . searchParams . set ( "render" , "explicit" ) ;
28+ url . searchParams . set ( "onload" , "onTurnstileReady" ) ;
29+
30+ return url ;
2431 } ;
2532
2633 /**
@@ -33,25 +40,34 @@ export class TurnstileController extends CaptchaController {
3340 /**
3441 * Renders the Turnstile captcha frame.
3542 *
43+ * Uses explicit rendering to avoid Turnstile's self-upgrade mechanism
44+ * (every ~3 hours) from calling `implicitRenderAll()` and duplicating widgets.
45+ *
3646 * @remarks
3747 *
38- * Turnstile will log a warning if the `data- language` attribute
48+ * Turnstile will log a warning if the `language` option
3949 * is not in lower-case format.
4050 *
4151 * @see {@link https://developers.cloudflare.com/turnstile/reference/supported-languages/ Turnstile Supported Languages }
4252 */
4353 public interactive = ( ) => {
44- const languageTag = this . host . activeLanguageTag . toLowerCase ( ) ;
54+ const siteKey = this . host . challenge ?. siteKey ?? "" ;
55+ const theme = this . host . activeTheme ;
56+ const language = this . host . activeLanguageTag . toLowerCase ( ) ;
4557
46- return html `< div
47- id ="ak-container "
48- class ="cf-turnstile "
49- data-sitekey =${ ifPresent ( this . host . challenge ?. siteKey ) }
50- data-theme =${ this . host . activeTheme }
51- data-callback="callback"
52- data-size="flexible"
53- data-language=${ ifPresent ( languageTag ) }
54- > </ div > ` ;
58+ return html `< div id ="ak-container "> </ div >
59+ < script >
60+ function onTurnstileReady ( ) {
61+ turnstile . render ( "#ak-container" , {
62+ sitekey : "${ siteKey } " ,
63+ theme : "${ theme } " ,
64+ language : "${ language } " ,
65+ size : "flexible" ,
66+ callback,
67+ } ) ;
68+ loadListener ( ) ;
69+ }
70+ </ script > ` ;
5571 } ;
5672
5773 public refreshInteractive = async ( ) => {
0 commit comments