diff --git a/docker-compose.yaml b/docker-compose.yaml
index 9b367a2..6921f43 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -9,6 +9,9 @@ services:
volumes:
- ${PWD}/config/nginx/nginx_dev.conf:/etc/nginx/nginx.conf
- ${PWD}/proxy-cache:/var/cache/nginx
+ depends_on:
+ - webapp
+ - api
# The webapp dev server on port 3000
webapp:
diff --git a/package.json b/package.json
index 1b5c8de..e570c81 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
"react-dom": "^16.8.5",
"react-infinite-scroller": "^1.2.4",
"react-router-dom": "^5.0.0",
+ "react-transition-group": "^4.0.0",
"resolve": "^1.10.0",
"style-loader": "^0.23.1",
"sw-precache-webpack-plugin": "^0.11.5",
diff --git a/src/CriteriumField.css b/src/CriteriumField.css
new file mode 100644
index 0000000..1a72f2f
--- /dev/null
+++ b/src/CriteriumField.css
@@ -0,0 +1,24 @@
+.CriteriumField.bad, .CriteriumField.bad .CriteriumField-title > a {
+ color: #ae4b53;
+ font-size: 1rem;
+}
+.CriteriumField.mediocre, .CriteriumField.mediocre .CriteriumField-title > a {
+ color: #c49863;
+ font-size: 1rem;
+}
+.CriteriumField.good {
+ color: #46962b;
+ font-size: 1rem;
+}
+
+.CriteriumField-details {
+ color: rgb(33, 37, 41);
+ font-size: 14px;
+}
+.CriteriumField-details p {
+ margin-bottom: 0.4rem
+}
+
+.CriteriumField-title > a {
+ text-decoration: underline;
+}
diff --git a/src/CriteriumField.js b/src/CriteriumField.js
new file mode 100644
index 0000000..a5ea544
--- /dev/null
+++ b/src/CriteriumField.js
@@ -0,0 +1,99 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { Transition } from 'react-transition-group';
+
+import './CriteriumField.css';
+
+const transitionDuration = 300;
+
+const defaultTransitionStyle = {
+ transition: `max-height ${transitionDuration}ms ease-in-out`,
+ maxHeight: 0,
+ overflowY: 'hidden',
+};
+
+const transitionStyles = {
+ entering: { maxHeight: 500 },
+ entered: { maxHeight: 500, overflowY: 'auto' },
+ exiting: { maxHeight: 0 },
+ exited: { maxHeight: 0 },
+};
+
+
+class CriteriumField extends Component {
+ state = {expanded: false};
+
+ showHide = (evt) => {
+ evt.preventDefault();
+ this.setState({expanded: !this.state.expanded});
+ };
+
+ render() {
+ if (this.props.type === 'positive') {
+ return (
+
+
+ {this.props.title}
+
+ );
+ } else if (this.props.type === 'mediocre') {
+ return (
+
+ { this.props.icon ? this.props.icon : }
+ {this.props.title}
+
+ );
+ } else {
+ if (this.props.children === null || typeof this.props.children === 'undefined') {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ {state => (
+
+ {this.props.children}
+
+ )}
+
+
+ );
+ }
+ }
+}
+
+CriteriumField.propTypes = {
+ type: PropTypes.oneOf(['positive', 'mediocre', 'negative']),
+ title: PropTypes.string.isRequired,
+ keyProp: PropTypes.string.isRequired,
+ icon: PropTypes.instanceOf(Component),
+};
+
+class IconGood extends Component {
+ render() {
+ return ;
+ }
+}
+
+class IconBad extends Component {
+ render() {
+ return ;
+ }
+}
+
+class IconOptimize extends Component {
+ render() {
+ return ;
+ }
+}
+
+export default CriteriumField;
diff --git a/src/SiteDetailsPage.css b/src/SiteDetailsPage.css
index 094f442..4777e1c 100644
--- a/src/SiteDetailsPage.css
+++ b/src/SiteDetailsPage.css
@@ -1,15 +1,3 @@
-.SiteDetailsPage .bad {
- color: #ae4b53;
- font-size: 1rem;
-}
-.SiteDetailsPage .mediocre {
- color: #c49863;
- font-size: 1rem;
-}
-.SiteDetailsPage .good {
- color: #46962b;
- font-size: 1rem;
-}
.SiteDetailsPage h1 {
margin-top: 2rem;
font-family: 'Arvo', sans-serif;
diff --git a/src/SiteDetailsPage.js b/src/SiteDetailsPage.js
index fde4fdf..6d69a77 100644
--- a/src/SiteDetailsPage.js
+++ b/src/SiteDetailsPage.js
@@ -1,5 +1,6 @@
import React, { Component } from 'react';
import { TypeField, StateField } from './LocationLabel';
+import CriteriumField from './CriteriumField';
import FavouriteAddRemove from './FavouriteAddRemove';
import LocationLabel from './LocationLabel';
import ScoreField from './ScoreField';
@@ -9,6 +10,25 @@ import axios from 'axios';
import _ from 'underscore';
+/**
+ * A cheap hash function for hashing strings
+ *
+ * @param String The string to be hashed
+ */
+function hashCode(str) {
+ var hash = 0;
+ if (str.length === 0) {
+ return hash;
+ }
+ for (let i=0; i < str.length; i++) {
+ var char = str.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash = hash & hash; // Convert to 32bit integer
+ }
+ return hash.toString();
+}
+
+
class SiteDetailsPage extends Component {
_isMounted = false;
@@ -71,7 +91,7 @@ class SiteDetailsPage extends Component {
},
{
criterium: 'CANONICAL_URL',
- component: ,
+ component: ,
data: this.state.site.rating.CANONICAL_URL,
},
{
@@ -116,17 +136,17 @@ class SiteDetailsPage extends Component {
},
{
criterium: 'NO_THIRD_PARTY_COOKIES',
- component: ,
+ component: ,
data: this.state.site.rating.NO_THIRD_PARTY_COOKIES,
},
{
criterium: 'NO_SCRIPT_ERRORS',
- component: ,
+ component: ,
data: this.state.site.rating.NO_SCRIPT_ERRORS,
},
{
criterium: 'NO_NETWORK_ERRORS',
- component: ,
+ component: ,
data: this.state.site.rating.NO_NETWORK_ERRORS,
},
{
@@ -257,42 +277,23 @@ class SiteDetailsPage extends Component {
}
}
-class IconGood extends Component {
- render() {
- return ;
- }
-}
-
-class IconBad extends Component {
- render() {
- return ;
- }
-}
-
-class IconOptimize extends Component {
- render() {
- return ;
- }
-}
-
-class CriteriumField extends Component {
- render() {
- if (this.props.type === 'positive') {
- return {this.props.title}
;
- } else if (this.props.type === 'mediocre') {
- return { this.props.icon ? this.props.icon : }{this.props.title}
;
- } else {
- return {this.props.title}
;
- }
- }
-}
-
class CanonicalURLField extends Component {
render() {
if (this.props.data.value) {
return ;
}
- return ;
+
+ return
+ Die Site ist unter den folgenden URLs erreichbar:
+
+ {
+ this.props.details.map((url) => {
+ return - {url}
;
+ })
+ }
+
+ Es sollte eine URL ausgewählt werden, auf die von allen anderen Varianten weiter geleitet wird.
+ ;
}
}
@@ -347,7 +348,12 @@ class DNSResolvableField extends Component {
if (this.props.data.value) {
return
}
- return
+
+ return
+ Das bedeutet in der Regel, dass eine genutzte Domain beim Registrar nicht verlängert wurde. Falls der Hostname
+ der Site nicht identisch mit der Domain ist, und stattdessen beispielsweise mit `www.` beginnt, könnte es sich
+ auch um eine fehlende Konfiguration beim DNS-Provider handeln.
+ ;
}
}
@@ -356,7 +362,12 @@ class FaviconField extends Component {
if (this.props.data.value) {
return ;
}
- return ;
+
+ return
+ Ein Icon hilft Nutzer*innen, ein Browser-Tab oder ein Bookmark der Site besser wieder zu erkennen.
+ Anleitung: How
+ to Add a Favicon to your Site
+ ;
}
}
@@ -365,16 +376,93 @@ class FeedField extends Component {
if (this.props.data.value) {
return ;
}
- return ;
+
+ return
+ Feeds helfen Suchmaschinen dabei, aktuelle Meldungen zeitnah nach Veröffentlichung in ihren Suchindex
+ aufzunehmen, was die Site besser in Suchergebnissen platziert. Außerdem helfen sie auch versierten
+ Nutzer*innen dabei, über neue Inhalte auf dem laufenden zu bleiben.
+ Die meistgenutzten CMSe unterstützen die Veröffentlichung von Feeds ohne zusätzlichen Aufwand.
+ Anleitung: RSS Feeds aktivieren in GCMS
+ ;
}
}
class CookiesField extends Component {
+ state = {thirdPartyCookies: null};
+
+ componentDidMount() {
+ let url = Object.keys(this.props.details)[0];
+ let cookies = this.props.details[url].cookies;
+ if (typeof cookies !== 'undefined') {
+ let parsedURL = new URL(url);
+ let thirdPartyCookies = cookies.filter(cookie => {
+ return parsedURL.hostname.indexOf(cookie.host_key);
+ });
+ if (thirdPartyCookies.length > 0) {
+ this.setState({thirdPartyCookies: thirdPartyCookies});
+ }
+ }
+ }
+
+ expiryString(duration) {
+ if (duration < 60 * 3) {
+ return Math.floor(duration).toString() + " Sekunden";
+ }
+ duration = duration / 60.0;
+ if (duration < 100) {
+ return Math.floor(duration).toString() + " Minuten";
+ }
+ duration = duration / 60.0;
+ if (duration < 48) {
+ return Math.floor(duration).toString() + " Stunden";
+ }
+ duration = duration / 24.0;
+ if (duration < 100) {
+ return Math.floor(duration).toString() + " Tage";
+ }
+ duration = duration / 30.0;
+ if (duration < 15) {
+ return Math.floor(duration).toString() + " Monate";
+ }
+ duration = duration * 30.0 / 365;
+ return Math.floor(duration).toString() + " Jahre";
+ }
+
render() {
if (this.props.data.value) {
return ;
}
- return ;
+
+ return (
+
+ Cookies von Dritten, auch Third Party Cookies genannt, erlauben das Verfolgen von Nutzer*innen über
+ die Grenzen der Seite, auf der die Cookies gesetzt wurden, hinweg. Damit stellen sie einen Eingriff in die
+ Informationelle Selbstbestimmung dar, insbesondere dann, wenn sie ohne Einwilligung gesetzt werden.
+ Da Green Spider keine Einwilligung in das Setzen von Cookies gibt, werden alle nachstehenden Cookies
+ ohne explizite Einwilligung gesetzt.
+
+
+
+ Domain |
+ Name |
+ Lebensdauer |
+
+
+
+ {
+ this.state.thirdPartyCookies !== null ? this.state.thirdPartyCookies.map((cookie) => {
+ return
+ {cookie.host_key} |
+ {cookie.name} |
+ {this.expiryString(Math.abs(cookie.expires_utc - cookie.creation_utc) / 1000000)} |
+
;
+ }) : null
+ }
+
+
+
+ );
}
}
@@ -385,13 +473,23 @@ class FontField extends Component {
font = 'Titillium';
}
- if (typeof this.props.data !== 'undefined') {
- if (this.props.data.value) {
- return ;
- }
- return ;
+ if (typeof this.props.data === 'undefined') {
+ return ;
}
- return ;
+
+ if (this.props.data.value) {
+ return ;
+ }
+
+ return
+ Die Schriftart Arvo bzw. der Variante für Überschriften, Arvo Gruen, ist ein markanter Bestandteil der
+ Corporate-Design-Richtlinien von BÜNDNIS 90/DIE GRÜNEN. Die Verwendung der Schrift hilft dabei, den Absender
+ kenntlich zu machen, so wie es auch der Einsatz der richtigen Farben und die Verwendung des Logos tun.
+ Die empfohlenen Schriften stehen unter{' '}
+ github.com/netzbegruenung/webfonts für die
+ einfache Verwendung auf Webseiten zur Verfügung.
+ ;
}
}
@@ -400,7 +498,17 @@ class HTTPSField extends Component {
if (this.props.data.value) {
return ;
}
- return ;
+
+ return
+ Per TLS verschlüsselte HTTP-Verbindungen schützen Nutzer*innen vor der Preisgabe privater Informationen.
+ Entsprechend gehört HTTPS für immer mehr Nutzer*innen bei einem vertrauenswürdigen Webangebot zu den
+ Pflicht-Kriterien. Auch viele Unternehmen, darunter beispielsweise Google, haben inzwischen die HTTPS-Verbindung
+ zum Standard erklärt. Seiten, die nicht per HTTPS erreichbar sind, werden entsprechend von Google im
+ Suchergebnis schlechter platziert.
+ Inzwischen gibt es TLS-Zertifikate für Verschlüsselte Server-Kommunikation auch kostenlos, z. B. von
+ Let's Encrypt.
+ Lesetipp: HTTPS as a ranking signal
+ ;
}
}
@@ -432,7 +540,16 @@ class ResponsiveField extends Component {
} else if (this.props.data.score > 0) {
return ;
}
- return ;
+
+ return
+ Green Spider testet, wie breit die Startseite der Site auf verschieden breiten Bildschirmen ausfällt.
+ Ist in einer Breite die Seite breiter als der Bildschirm, so gilt der Test als nicht bestanden.
+ Nutzer*innen con Smartphones sehen in diesen Fällen häufig einen horizontalen Scrollbalken oder müssen
+ zum vollständigen Betrachten der Seite die Inhalte horizontal Verschieben.
+
+ Tipp: Zieh den Browserfenster so schmal wie Du kannst, im besten Fall auf 360 Pixel Breite. Damit erhältst
+ Du einen Eindruck, welche Inhalte über den Rand hinausragen.
+ ;
}
}
@@ -441,7 +558,11 @@ class ContactLinkField extends Component {
if (this.props.data.value) {
return ;
}
- return ;
+
+ return
+ Wenn Nutzer*innen mit dem Betreiber einer Site in Kontakt treten wollen, ist ein gut sichtbarer Link mit der
+ Beschriftung "Kontakt" eine der einfachsten Möglichkeit.
+ ;
}
}
@@ -450,7 +571,12 @@ class SocialMediaLinksField extends Component {
if (this.props.data.value) {
return ;
}
- return ;
+
+ return
+ Über Social-Media-Profile ist es möglich, häufiger mit Nutzer*innen in Kontakt zu treten. Sofern es Profile
+ gibt, sollten diese am besten von jeder Seite der Site verlinkt werden. Aktuell werden Links zu Facebook,
+ Twitter und Instagram gewertet.
+ ;
}
}
@@ -600,27 +726,87 @@ class WWWOptionalField extends Component {
}
}
-class ScriptErrorsField extends Component {
+class LoggedErrorsField extends Component {
render() {
- if (typeof this.props.data !== 'undefined') {
- if (this.props.data.value) {
- return ;
- }
- return ;
+ if (this.props.type === 'positive') {
+ return ;
}
- return ;
+
+ return (
+
+ { this.props.logEntries !== null && this.props.logEntries !== [] ?
+
+
+ {
+ this.props.logEntries.map((item) => {
+ return
+ {item.source} |
+ {item.level} |
+ {item.message} |
+
;
+ })
+ }
+
+
+ :
+ Es können leider keine Details zu den gesammelten Fehlern angezeigt werden.
}
+
+ );
}
}
class NetworkErrorsField extends Component {
+ state = {logEntries: null};
+
+ componentDidMount() {
+ let logEntries = Object.values(this.props.details)[0].logs;
+ if (typeof logEntries !== 'undefined') {
+ let filteredEntries = logEntries.filter(item => item.source !== 'javascript');
+ if (filteredEntries.length > 0) {
+ this.setState({logEntries: filteredEntries});
+ }
+ }
+ }
+
render() {
- if (typeof this.props.data !== 'undefined') {
- if (this.props.data.value) {
- return ;
+ if (typeof this.props.data === 'undefined') {
+ return ;
+ }
+
+ if (this.props.data.value) {
+ return ;
+ }
+
+ return (
+
+ );
+ }
+}
+
+
+class ScriptErrorsField extends Component {
+ state = {logEntries: null};
+
+ componentDidMount() {
+ let logEntries = Object.values(this.props.details)[0].logs;
+ if (typeof logEntries !== 'undefined') {
+ let filteredEntries = logEntries.filter(item => item.source === 'javascript');
+ if (filteredEntries.length > 0) {
+ this.setState({logEntries: filteredEntries});
}
- return ;
}
- return ;
+ }
+
+ render() {
+ if (typeof this.props.data === 'undefined') {
+ return ;
+ }
+
+ if (this.props.data.value) {
+ return ;
+ }
+
+ return ;
}
}
diff --git a/yarn.lock b/yarn.lock
index 576726a..0dd20ec 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2516,6 +2516,13 @@ dom-converter@~0.1:
dependencies:
utila "~0.3"
+dom-helpers@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
+ integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
+ dependencies:
+ "@babel/runtime" "^7.1.2"
+
dom-serializer@0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
@@ -6959,6 +6966,15 @@ react-router@5.0.0:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
+react-transition-group@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.0.0.tgz#1d82b20d78aa09eac6268ceef0349307146942c6"
+ integrity sha512-b+uvkr15Pb80mqcsz5WAB+d53zS8/pTp3wDEsOiqpea93G8BqfsMFcPv2XZR0owqU13BJWoJvd17VjOPEY/9aA==
+ dependencies:
+ dom-helpers "^3.4.0"
+ loose-envify "^1.4.0"
+ prop-types "^15.6.2"
+
react@^16.8.5:
version "16.8.5"
resolved "https://registry.yarnpkg.com/react/-/react-16.8.5.tgz#49be3b655489d74504ad994016407e8a0445de66"