@@ -23,19 +23,21 @@
23 23 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
def decapify(
24 24 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
site_dir: str,
25 25 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
cms_name: str = None,
26 26 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
cms_logo: str = None,
27 27 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
cms_color: str = None,
28 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ github_repo: str = None,
28 29 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
) -> str:
29 30 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
"""
30 31 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
Add Decap CMS to a Hugo site directory.
31 32 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
32 33 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
Args:
33 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- site_dir: Root of the assembled Hugo site (has hugo.toml, content/, themes/).
34 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- cms_name: Whitelabel name shown in the admin UI (default: 'Content Manager').
35 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- cms_logo: URL to a logo image for the admin UI (optional).
36 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- cms_color: Hex color for the admin top bar (default: '#2e3748').
34 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ site_dir: Root of the assembled Hugo site (has hugo.toml, content/, themes/).
35 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ cms_name: Whitelabel name shown in the admin UI (default: 'Content Manager').
36 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ cms_logo: URL to a logo image for the admin UI (optional).
37 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ cms_color: Hex color for the admin top bar (default: '#2e3748').
38 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ github_repo: GitHub repo slug e.g. 'ConflictHQ/my-site' (optional, for GitHub backend).
37 39 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
38 40 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
Returns:
39 41 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
Status message.
40 42 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
"""
41 43 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
logging.info(f"Adding Decap CMS to {site_dir} ...")
@@ -48,11 +50,13 @@
48 50 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
'logo': cms_logo or DEFAULT_CMS_LOGO,
49 51 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
'color': cms_color or DEFAULT_CMS_COLOR,
50 52 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
51 53 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
52 54 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
_write_admin_index(admin_dir, branding)
53 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- _write_decap_config(site_dir, admin_dir)
55 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _write_decap_config(site_dir, admin_dir, github_repo=github_repo)
56 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _write_oauth_functions(site_dir)
57 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _create_media_dir(site_dir)
54 58 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
55 59 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
logging.info("Decap CMS integration complete.")
56 60 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
return "Decap CMS integration complete"
57 61 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
58 62 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
@@ -102,19 +106,25 @@
102 106 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
103 107 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
# ---------------------------------------------------------------------------
104 108 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
# config.yml
105 109 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
# ---------------------------------------------------------------------------
106 110 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
107 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- def _write_decap_config(site_dir: str, admin_dir: str):
111 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ def _write_decap_config(site_dir: str, admin_dir: str, github_repo: str = None):
108 112 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
content_dir = os.path.join(site_dir, 'content')
109 113 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
collections = _build_collections(content_dir)
110 114 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
115 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ backend = {
116 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'name': 'github',
117 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'branch': 'main',
118 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
119 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if github_repo:
120 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ backend['repo'] = github_repo
121 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ backend['base_url'] = '' # placeholder — set to deployed site URL
122 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ backend['auth_endpoint'] = '/api/auth'
123 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
111 124 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
config = {
112 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- 'backend': {
113 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- 'name': 'git-gateway',
114 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- 'branch': 'main',
115 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- },
125 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'backend': backend,
116 126 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
'media_folder': 'static/images/uploads',
117 127 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
'public_folder': '/images/uploads',
118 128 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
'collections': collections,
119 129 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
120 130 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
@@ -269,5 +279,120 @@
269 279 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
'fields': [
270 280 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
{'label': 'Title', 'name': 'title', 'widget': 'string'},
271 281 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
{'label': 'Body', 'name': 'body', 'widget': 'markdown'},
272 282 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
],
273 283 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
284 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
285 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
286 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # ---------------------------------------------------------------------------
287 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # OAuth functions (Cloudflare Pages Functions)
288 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # ---------------------------------------------------------------------------
289 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
290 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _AUTH_JS = """\
291 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ export async function onRequest(context) {
292 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const { request, env } = context;
293 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const client_id = env.GITHUB_CLIENT_ID;
294 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
295 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
296 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const url = new URL(request.url);
297 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const redirectUrl = new URL('https://github.com/login/oauth/authorize');
298 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ redirectUrl.searchParams.set('client_id', client_id);
299 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ redirectUrl.searchParams.set('redirect_uri', url.origin + '/api/callback');
300 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ redirectUrl.searchParams.set('scope', 'repo user');
301 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ redirectUrl.searchParams.set(
302 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'state',
303 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ crypto.getRandomValues(new Uint8Array(12)).join(''),
304 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ );
305 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return Response.redirect(redirectUrl.href, 301);
306 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch (error) {
307 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ console.error(error);
308 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return new Response(error.message, { status: 500 });
309 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
310 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
311 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ """
312 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
313 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _CALLBACK_JS = """\
314 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function renderBody(status, content) {
315 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const html = `
316 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <script>
317 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const receiveMessage = (message) => {
318 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ window.opener.postMessage(
319 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'authorization:github:${status}:${JSON.stringify(content)}',
320 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ message.origin
321 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ );
322 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ window.removeEventListener("message", receiveMessage, false);
323 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
324 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ window.addEventListener("message", receiveMessage, false);
325 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ window.opener.postMessage("authorizing:github", "*");
326 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </script>
327 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ `;
328 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const blob = new Blob([html]);
329 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return blob;
330 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
331 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
332 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ export async function onRequest(context) {
333 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const { request, env } = context;
334 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const client_id = env.GITHUB_CLIENT_ID;
335 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const client_secret = env.GITHUB_CLIENT_SECRET;
336 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
337 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
338 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const url = new URL(request.url);
339 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const code = url.searchParams.get('code');
340 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const response = await fetch(
341 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'https://github.com/login/oauth/access_token',
342 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ {
343 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ method: 'POST',
344 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ headers: {
345 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'content-type': 'application/json',
346 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'user-agent': 'hugoifier-cms-oauth',
347 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'accept': 'application/json',
348 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ },
349 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ body: JSON.stringify({ client_id, client_secret, code }),
350 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ },
351 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ );
352 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const result = await response.json();
353 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (result.error) {
354 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return new Response(renderBody('error', result), {
355 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ headers: { 'content-type': 'text/html;charset=UTF-8' },
356 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ status: 401
357 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ });
358 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
359 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const token = result.access_token;
360 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const provider = 'github';
361 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const responseBody = renderBody('success', { token, provider });
362 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return new Response(responseBody, {
363 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ headers: { 'content-type': 'text/html;charset=UTF-8' },
364 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ status: 200
365 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ });
366 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch (error) {
367 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ console.error(error);
368 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return new Response(error.message, {
369 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ headers: { 'content-type': 'text/html;charset=UTF-8' },
370 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ status: 500,
371 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ });
372 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
373 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
374 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ """
375 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
376 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
377 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ def _write_oauth_functions(site_dir: str):
378 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ """Write Cloudflare Pages Functions for GitHub OAuth (Decap CMS auth)."""
379 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ functions_dir = os.path.join(site_dir, 'functions', 'api')
380 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ os.makedirs(functions_dir, exist_ok=True)
381 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
382 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ with open(os.path.join(functions_dir, 'auth.js'), 'w') as f:
383 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ f.write(_AUTH_JS)
384 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
385 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ with open(os.path.join(functions_dir, 'callback.js'), 'w') as f:
386 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ f.write(_CALLBACK_JS)
387 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
388 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ logging.info("Wrote OAuth functions to functions/api/")
389 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
390 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
391 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ def _create_media_dir(site_dir: str):
392 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ """Create the media uploads directory so Decap doesn't 404."""
393 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ media_dir = os.path.join(site_dir, 'static', 'images', 'uploads')
394 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ os.makedirs(media_dir, exist_ok=True)
395 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ gitkeep = os.path.join(media_dir, '.gitkeep')
396 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if not os.path.exists(gitkeep):
397 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ with open(gitkeep, 'w') as f:
398 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ pass
274 399 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!