1<!DOCTYPE html> 2<html lang="de"> 3<head> 4 5 <meta charset="utf-8"/> 6 <meta name="viewport" content="width=device-width, initial-scale=1"> 7 <meta name="description" content="Deutschunterricht. Einfach. Digital. - Durch einfache Zuweisung von Grammatikaufgaben und automatische Korrektur den Unterricht digital gestalten. Zeitsparend. Ãbersichtlich."> 8 9 <title>Grammatip - Blog</title> 10 11
11<script src="/javascripts/cookie-helper.js?v=1655670100"></script>
11 12 <link rel="shortcut icon" href="/favicon.ico?v=1782478304" type="image/x-icon" /> 13 14 <!-- PWA Start --> 15<!-- General --> 16<meta data-hid="mobile-web-app-capable" name="mobile-web-app-capable" content="yes"> 17<meta data-hid="og:type" name="og:type" property="og:type" content="website"> 18<meta data-hid="og:title" name="og:title" property="og:title" content="Grammatip"> 19<meta data-hid="og:site_name" name="og:site_name" property="og:site_name" content="Grammatip"> 20<link rel="icon" type="image/x-icon" href="/favicon.ico"> 21<link data-hid="shortcut-icon" rel="shortcut icon" href="/android-chrome-192x192.png"> 22 23<!-- Apple --> 24<meta data-hid="apple-mobile-web-app-title" name="apple-mobile-web-app-title" content="Grammatip"> 25<meta name="theme-color" content="#2D57FC"/> 26<meta name="apple-mobile-web-app-status-bar" content="#2D57FC"/> 27<link data-hid="apple-touch-icon" rel="apple-touch-icon" href="/android-chrome-512x512.png" sizes="512x512"> 28 29<!-- Windows --> 30<meta name="application-name" content="Grammatip"/> 31<meta name="msapplication-TileImage" content="/android-chrome-512x512.png"> 32<meta name="msapplication-TileColor" content="#2D57FC"> 33 34<!-- Manifest --> 35<link rel="manifest" href="/manifest.json" data-hid="manifest" crossorigin="use-credentials"> 36 37<!-- Service worker -->
38<script> 39 40 window.serviceWorkerSupported = true; 41 42 function isIOS16() { 43 return ( 44 navigator.appVersion.includes('Version/16') || 45 navigator.appVersion.includes('OS 16') 46 ) && ( 47 /iPhone/.test(navigator.userAgent) || 48 /iPad/.test(navigator.userAgent) || 49 /iPod/.test(navigator.userAgent) || 50 /Macintosh/.test(navigator.userAgent) 51 ); 52 } 53 54 55 /** 56 * @param tryOnce 57 * @returns {Promise<void|ServiceWorker|*>} 58 * 59 * @see https://stackoverflow.com/a/67612740 60 */ 61 async function registerServiceWorker(tryOnce = false) { 62 if (isIOS16()) { 63 window.serviceWorkerSupported = false; 64 65 try { 66 const reg = await navigator.serviceWorker.getRegistration('/'); 67 await reg.unregister(); 68 } catch (err) { 69 return; 70 } 71 72 return; 73 } 74 75 if (!('serviceWorker' in navigator)) { 76 window.serviceWorkerSupported = false; 77 throw new Error('serviceWorker not supported'); 78 } 79 80 const url = (new URL(`/serviceWorker.js?v=g8534h`, location)).toString(); 81 //console.info('Registering worker'); 82 const registration = await navigator.serviceWorker.register(url, { 83 scope: '/', 84 }); 85 86 const registeredWorker = registration.active || registration.waiting || registration.installing; 87 //console.info('Registered worker:', registeredWorker); 88 if (registeredWorker?.scriptURL != url) { 89 console.log('[ServiceWorker] Old URL:', registeredWorker?.scriptURL || 'none', 'updating to:', url); 90 await registration.update(); 91 //console.info('Updated worker'); 92 } 93 94 //console.info('Waiting for ready worker'); 95 let serviceReg = await navigator.serviceWorker.ready; 96 //console.info('Ready registration:', serviceReg); 97 98 if (!navigator.serviceWorker.controller) { 99 //console.info('Worker isnât controlling, re-register'); 100 try { 101 const reg = await navigator.serviceWorker.getRegistration('/'); 102 //console.info('Unregistering worker'); 103 await reg.unregister(); 104 //console.info('Successfully unregistered, trying registration again'); 105 return registerServiceWorker(); 106 } catch (err) { 107 window.serviceWorkerSupported = false; 108 console.error(`ServiceWorker failed to re-register after hard-refresh, reloading the page!`, err); 109 return location.reload(); 110 } 111 } 112 113 let serviceWorker = serviceReg.active || serviceReg.waiting || serviceReg.installing; 114 if (!serviceWorker) { 115 //console.info('No worker on registration, getting registration again'); 116 serviceReg = await navigator.serviceWorker.getRegistration('/'); 117 serviceWorker = serviceReg.active || serviceReg.waiting || serviceReg.installing; 118 } 119 120 if (!serviceWorker) { 121 //console.info('No worker on registration, waiting 50ms'); 122 await sleep(50); // adjustable or skippable, have a play around 123 } 124 125 serviceWorker = serviceReg.active || serviceReg.waiting || serviceReg.installing; 126 if (!serviceWorker) { 127 window.serviceWorkerSupported = false; 128 throw new Error('after waiting on .ready, still no worker'); 129 } 130 131 if (serviceWorker.state == 'redundant') { 132 //console.info('Worker is redundant, trying again'); 133 return registerServiceWorker(); 134 } 135 136 if (serviceWorker.state != 'activated') { 137 //console.info('Worker IS controlling, but not active yet, waiting on event. state=', serviceWorker.state); 138 try { 139 // timeout is adjustable, but you do want one in case the statechange 140 // doesn't fire / with the wrong state because it gets queued, 141 // see ServiceWorker.onstatechange MDN docs. 142 await timeout(100, new Promise((resolve) => { 143 serviceWorker.addEventListener('statechange', (e) => { 144 if (e.target.state == 'activated') resolve(); 145 }); 146 })); 147 } catch (err) { 148 if (err instanceof TimeoutError) { 149 if (serviceWorker.state != 'activated') { 150 if (tryOnce) { 151 //console.info('Worker is still not active. state=', serviceWorker.state);
152 window.serviceWorkerSupported = false; 153 throw new Error('failed to activate service worker'); 154 } else { 155 //console.info('Worker is still not active, retrying once'); 156 return registerServiceWorker(true); 157 } 158 } 159 } else { 160 // should be unreachable 161 throw err; 162 } 163 } 164 } 165 166 //console.info('Worker is controlling and active, weâre good folks!'); 167 return serviceWorker; 168 } 169 170 class TimeoutError extends Error { 171 } 172 173 /** 174 * Run promise but reject after some timeout. 175 * 176 * @template T 177 * @param {number} ms Milliseconds until timing out 178 * @param {Promise<T>} promise Promise to run until timeout (note that it will keep running after timeout) 179 * @returns {Promise<T, Error>} 180 */ 181 function timeout(ms, promise) { 182 return new Promise((resolve, reject) => { 183 const timer = setTimeout(() => { 184 reject(new TimeoutError); 185 }, ms); 186 187 promise.then((result) => { 188 clearTimeout(timer); 189 resolve(result); 190 }, (error) => { 191 clearTimeout(timer); 192 reject(error); 193 }); 194 }) 195 } 196 197 registerServiceWorker() 198 199</script>
199 200<!-- PWA End --> 201 202 203 <link rel="stylesheet" href="/assets/new/css/web-app.css?id=be3c8f791d151789a2b9627dc861ecbe" type="text/css"> 204 205 206 207
208<script> 209 var analyticsCookies = ['_gcl_au', '_ga', '_gid', '_gat_UA-630585-12']; 210 for (var i in analyticsCookies) { 211 if (!getCookieValue(analyticsCookies[i])) { 212 continue; 213 } 214 215 document.cookie = analyticsCookies[i] + '=; path=/; domain=; expires=Thu, 08 Jan 1970 00:00:01 GMT'; 216 } 217</script>
217 218 219</head> 220<body class=" "> 221 222<div id="vue-app"> 223 <cookies-consent-popup></cookies-consent-popup> 224 225 226<div class="home-container"> 227 <nav class="navbar sticky-top bg-white landing-page-header"> 228 <div class="container-xxl"> 229 <div class="navbar-header"> 230 <a class="navbar-brand" href="/"> 231 <span class="logo"> 232 <span class="d-none d-md-inline logo-text">grammatip.de</span> 233 </span> 234 </a> 235 </div> 236 <div class="nav"> 237 <div class="nav-item d-none d-xl-block"> 238 <a class="nav-link " href="/function">So funktioniert Grammatip</a> 239 </div> 240 <div class="nav-item d-none d-xl-block"> 241 <a class="nav-link active" href="/blog">Blog</a> 242 </div> 243 <div class="nav-item"> 244 <div class="nav-link"> 245 <a class="btn btn-outline-primary btn-lg btn-teardrop d-none d-sm-block" href="/register-school">Nutzerkonto anlegen</a> 246 <a class="btn btn-outline-primary btn-lg btn-teardrop d-sm-none" href="/register-school">Konto anlegen</a> 247 </div> 248 </div> 249 <div class="nav-item"> 250 <div class="nav-link"> 251 <a class="btn btn-primary btn-lg btn-teardrop d-none d-sm-block" href="/login">Einloggen</a> 252 <a class="btn btn-primary btn-lg btn-teardrop d-sm-none" href="/login">Einloggen</a> 253 </div> 254 </div> 255 <div class="nav-item d-xl-none"> 256 <div class="nav-link"> 257 <div class="dropdown lp-header-dropdown"> 258 <a class="btn btn-lg btn-icon dropdown-toggle flex-column menu-button" href="#" id="navbarHeaderDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> 259 <img src="/images/icons/dropdown-icon.svg" alt=""> 260 <span class="menu-text">MENÃ</span> 261 </a> 262 263 <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarHeaderDropdown"> 264 <li><a class="dropdown-item" href="/function">So funktioniert Grammatip</a></li> 265 <li><a class="dropdown-item" href="/blog">Blog</a></li> 266 </ul> 267 </div> 268 </div> 269 </div> 270 </div> 271 </div> 272</nav> 273 274 <main> 275 <div id="vue-app"> 276 <blog-feed ></blog-feed> 277 </div> 278 </main> 279 280 <footer> 281 <div class="top-footer"> 282 <div class="container-xxl"> 283 <div class="row landing-page-content gap-3"> 284 <div class="col-auto d-flex flex-column mb-3"> 285 <a href="/"> 286 <img class="grammatip-logo me-4" src="/assets/images/landing-page/grammatip.de.svg" alt="grammatip.com logo" loading="lazy"> 287 </a> 288 <div class="d-md-flex d-none mt-1 flex-lg-row flex-wrap"> 289 <a class="btn btn-outline-primary btn-lg btn-teardrop text-nowrap me-3 mt-3 mb-lg-0" href="/function">So funktioniert Grammatip</a> 290 <a class="btn btn-outline-primary btn-lg btn-teardrop text-nowrap me-3 mt-3 mb-lg-0" href="/blog">Blog</a> 291 </div> 292 </div> 293 <div class="col flex-column mb-3 d-none d-xxl-flex"> 294 <a class="btn btn-outline-primary btn-lg btn-teardrop text-nowrap me-3 mt-3 mb-lg-0 ms-auto" href="/register-school">Konto anlegen</a> 295 <a class="btn btn-outline-primary btn-lg btn-teardrop text-nowrap me-3 mt-3 mb-lg-0 ms-auto" href="/login">Einloggen</a> 296 </div> 297 <div class="col-auto ms-xl-auto"> 298 <div class="row contact-info"> 299 <div class="col me-3 mb-3 pe-3"> 300 <p class="fw-semibold">Kontakt:</p> 301 <p> 302 <a href="mailto:[email protected]">[email protected]</a> 303 </p> 304 </div> 305 <div class="col"> 306 <ul class="policies"> 307 <li> 308 <a href="/policies/privacy">Datenschutz</a> 309 </li> 310 <li> 311 <a href="/grammatip_de_avv.pdf" target="_blank" download>Datenverarbeitungsvertrag (AVV)</a> 312 </li> 313 <li> 314 <a href="/policies/terms">AGB</a> 315 </li> 316 <li> 317 <a href="/policies/impressum">Impressum</a> 318 </li> 319 <li>
320 <span class="open-cookie-popup cursor-pointer">Cookie Einstellungen</span> 321 </li> 322 </ul> 323 </div> 324 </div> 325 </div> 326 </div> 327 </div> 328 </div> 329 330 <div class="bottom-footer"> 331 <div class="container-xxl"> 332 <div class="row flex-lg-row landing-page-content"> 333 <div class="col-auto d-flex align-items-center mb-2 mb-sm-0"> 334 <p>Entwickelt von Lemma GmbH</p> 335 </div> 336 <div class="col-auto d-flex justify-content-lg-end align-items-center ms-lg-auto"> 337 <p>Lemma GmbH | Ericusspitze 4 | D-20457 Hamburg</p> 338 </div> 339 </div> 340 </div> 341 </div> 342</footer> 343</div> 344 345 346 </div> 347 348 349
349<script> 350 window.COOKIE_DOMAIN = '.grammatip.de'; 351 window.isPWA = ('' === '1'); 352 </script>
352 353
353<script src="/assets/new/js/manifest.js?id=d4bcf00853dad0a3d96b3597ed8ec186"></script>
353 354
354<script src="/assets/new/js/vendor.js?id=a19c435f8194eff597d954c4ab255344"></script>
354 355
355<script src="/assets/new/js/app.js?id=4377efb2389f3ca32b5da6ebbf1fa628"></script>
355 356
356<script> 357 setInterval(function () { 358 var xmlHttp = new XMLHttpRequest(); 359 xmlHttp.open('GET', '/api/v1/keep-alive?time=' + Date.now(), true); 360 xmlHttp.send(null); 361 }, 300 * 1000); 362</script>
362 363
363<script> 364 // Sets the timeout variable to use later 365 let timeoutId; 366 367 // Listen for the window: online event 368 window.addEventListener('online', async (e) => { 369 timeoutId = setTimeout(loop, 2000); 370 }); 371 372 // Loop function to be called every 2 seconds, via setTimeout 373 async function loop() { 374 // "Ping" the server and wait for response. 375 let status = await ping() 376 if (status === 200) { 377 // Make new notification 378 let notification = { 379 detail: {message: "Internetverbindung ist wieder hergestellt.", type: 'success'}, 380 }; 381 382 // Launch the notification 383 const event = new CustomEvent('add-notification', notification); 384 window.dispatchEvent(event); 385 // Stops the looping 386 clearTimeout(timeoutId) 387 } else { 388 // Run the loop if the "ping" failed 389 timeoutId = setTimeout(loop, 2000); 390 } 391 } 392 393 // "ping" the current website 394 function ping() { 395 let URL = window.location.origin; 396 // Defines the configurations for the request 397 let settings = { 398 url: URL, 399 method: "HEAD", 400 timeout: 5000, 401 // Minimizing the data send 402 maxContentLength: 1, 403 maxBodyLength: 1, 404 transformResponse: [function (data, headers) { 405 // Return empty string, because we are not interested in any data 406 return ""; 407 }], 408 headers: { 409 accept: "application/json", 410 }, 411 }; 412 413 // Sends the request and the returned data is set to only be the status code 414 // Also catches and logs any error that might occur 415 return axios(settings) 416 .then((response) => this.status = response.status) 417 .catch((error) => console.log(error.message)); 418 } 419 420 421 // Listens to offline event and sends a notification 422 window.addEventListener('offline', (e) => { 423 clearTimeout(timeoutId) 424 let notification = { 425 detail: {message: "Es besteht momentan keine Internetverbindung.", type: 'error'}, 426 }; 427 428 const event = new CustomEvent('add-notification', notification); 429 430 window.dispatchEvent(event); 431 }); 432 433</script>
433 434 435 436 <!-- Matomo --> 437
437<script type="text/javascript"> 438 var consentCookie = JSON.parse(getCookieValue('COOKIE_CONSENT')); 439 var siteId = '4'; // Prod site id 440 441 var _paq = window._paq = window._paq || []; 442 if (!consentCookie || consentCookie.statistics != true) { 443 _paq.push(['requireCookieConsent']); // Require cookie consent before adding cookies 444 } 445 446 /* tracker methods like "setCustomDimension" should be called before "trackPageView" */ 447 448 449 _paq.push(['trackPageView']); 450 _paq.push(['enableLinkTracking']); 451 _paq.push(['enableHeartBeatTimer', 30]); 452 (function () { 453 var u = "//analytics.grammatip.com/"; 454 _paq.push(['setTrackerUrl', u + 'matomo.php']); 455 _paq.push(['setSiteId', siteId]); 456 var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0]; 457 g.type = 'text/javascript'; 458 g.async = true; 459 g.src = u + 'matomo.js'; 460 s.parentNode.insertBefore(g, s); 461 })(); 462 </script>
462 463 <!-- End Matomo Code --> 464 465 466
466<script> 467 // enable (bootstrap) tooltips 468 if (!window.isTouchDevice) { 469 var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) 470 var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { 471 return new bootstrap.Tooltip(tooltipTriggerEl); 472 }) 473 } 474 </script>
474 475 476</body> 477</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.