PageSourceSearch

https://www.grammatip.de/blog

html grammatip.de collected 2026-10-01 23:52:17 UTC 18,715 bytes, 477 lines download raw bytes

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.