Compare commits
3 Commits
2b37907c27
...
2c43fe7784
Author | SHA1 | Date |
---|---|---|
|
2c43fe7784 | |
|
f0d296f108 | |
|
869b08ec0e |
|
@ -8,4 +8,5 @@
|
|||
/README.md
|
||||
/stack.production.yml
|
||||
/stack.staging.yml
|
||||
/tests/
|
||||
# /tests/
|
||||
Dockerfile.production
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "docker",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "docker",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"xmldom": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmldom": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz",
|
||||
"integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,7 +29,7 @@ colinknapp.com {
|
|||
Cache-Control "public, max-age=31536000, immutable"
|
||||
|
||||
# CSP with hashes for scripts and styles
|
||||
Content-Security-Policy "default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"
|
||||
Content-Security-Policy "default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"
|
||||
}
|
||||
|
||||
# Handle 404s
|
||||
|
@ -79,7 +79,7 @@ colinknapp.com {
|
|||
Cache-Control "public, max-age=31536000, immutable"
|
||||
|
||||
# CSP with hashes for scripts and styles
|
||||
Content-Security-Policy "default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"
|
||||
Content-Security-Policy "default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"
|
||||
}
|
||||
|
||||
# Handle 404s
|
||||
|
|
|
@ -39,6 +39,6 @@
|
|||
Cache-Control "public, max-age=31536000, immutable"
|
||||
|
||||
# CSP with hashes for scripts and styles
|
||||
Content-Security-Policy "default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"
|
||||
Content-Security-Policy "default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,43 +1,21 @@
|
|||
FROM caddy:2-alpine
|
||||
FROM caddy:2.7-alpine
|
||||
|
||||
# Install required tools for hash calculation and CSP updates
|
||||
RUN apk add --no-cache bash coreutils findutils grep sed xxd perl gawk
|
||||
|
||||
# Copy update scripts first
|
||||
COPY update-csp-hashes.sh /srv/update-csp-hashes.sh
|
||||
COPY caddy.sh /srv/caddy.sh
|
||||
|
||||
# Copy Caddyfile and static content
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
# Also copy to /srv for compatibility with the script
|
||||
COPY Caddyfile /srv/Caddyfile
|
||||
COPY index.html /srv/
|
||||
COPY theme.js /srv/
|
||||
COPY utils.js /srv/
|
||||
COPY styles.css /srv/
|
||||
COPY favicon.ico /srv/
|
||||
COPY includes.js /srv/
|
||||
COPY papaparse.min.js /srv/
|
||||
|
||||
# Copy one-pager-tools directory
|
||||
COPY one-pager-tools /srv/one-pager-tools/
|
||||
|
||||
# Copy includes directory
|
||||
COPY includes/ /srv/includes/
|
||||
|
||||
# Copy stories directory
|
||||
COPY stories/ /srv/stories/
|
||||
# Install dependencies
|
||||
RUN apk add --no-cache nodejs bash
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /srv
|
||||
|
||||
# Run the update-csp-hashes.sh script to update CSP hashes
|
||||
RUN chmod +x /srv/caddy.sh /srv/update-csp-hashes.sh && \
|
||||
cd /srv && \
|
||||
./update-csp-hashes.sh
|
||||
# Copy website files
|
||||
COPY . /srv
|
||||
|
||||
# Expose port 8080
|
||||
# Run all update scripts (sitemap, navigation, stories, CSP hashes, accessibility fixes)
|
||||
RUN cd /srv && \
|
||||
chmod +x update-all.sh && \
|
||||
./update-all.sh
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Run Caddy
|
||||
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile"]
|
||||
# Start Caddy with the local Caddyfile
|
||||
CMD ["caddy", "run", "--config", "/srv/Caddyfile.local"]
|
||||
|
|
|
@ -1 +1 @@
|
|||
FROM git.nixc.us/colin/resume:staging
|
||||
FROM git.nixc.us/nixc/resume:staging
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
}
|
||||
</style>
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# generate-sitemap.sh - Generate sitemap.xml for the website
|
||||
# =====================================================================
|
||||
# This script generates a sitemap.xml file for the website
|
||||
# It should be run after any content updates
|
||||
# =====================================================================
|
||||
|
||||
set -e
|
||||
|
||||
echo "Generating sitemap.xml..."
|
||||
|
||||
# Directory containing the files
|
||||
BASE_DIR="$(pwd)"
|
||||
DOMAIN="https://resume.example.com" # Replace with your actual domain in production
|
||||
|
||||
# Get current date in ISO 8601 format
|
||||
CURRENT_DATE=$(date -u +"%Y-%m-%dT%H:%M:%S+00:00")
|
||||
|
||||
# Create sitemap header
|
||||
cat > "$BASE_DIR/sitemap.xml" << EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
EOF
|
||||
|
||||
# Find all HTML files and add them to sitemap
|
||||
find "$BASE_DIR" -name "*.html" -type f | sort | while read -r html_file; do
|
||||
# Skip files in includes directory and template files
|
||||
if [[ "$html_file" == *"/includes/"* ]] || [[ "$html_file" == *"-with-includes.html" ]] || [[ "$html_file" == *"template"* ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Get relative path from base directory
|
||||
rel_path="${html_file#$BASE_DIR/}"
|
||||
|
||||
# Skip csv-tool-output.html as it's dynamically generated
|
||||
if [[ "$rel_path" == "csv-tool-output.html" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Create URL
|
||||
url="$DOMAIN/$rel_path"
|
||||
|
||||
# Add URL to sitemap
|
||||
cat >> "$BASE_DIR/sitemap.xml" << EOF
|
||||
<url>
|
||||
<loc>$url</loc>
|
||||
<lastmod>$CURRENT_DATE</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
EOF
|
||||
done
|
||||
|
||||
# Close sitemap
|
||||
cat >> "$BASE_DIR/sitemap.xml" << EOF
|
||||
</urlset>
|
||||
EOF
|
||||
|
||||
echo "Sitemap generated at $BASE_DIR/sitemap.xml"
|
||||
echo "Total URLs: $(grep -c "<url>" "$BASE_DIR/sitemap.xml")"
|
|
@ -15,13 +15,19 @@
|
|||
<li class="dropdown">
|
||||
<a href="/stories/" id="nav-stories">Stories</a>
|
||||
<div class="dropdown-content">
|
||||
<a href="/stories/viperwire.html" id="nav-viperwire">ViperWire</a>
|
||||
<a href="/stories/fawe-plotsquared.html" id="nav-fawe">FastAsyncWorldEdit</a>
|
||||
<a href="/stories/healthcare-platform.html" id="nav-healthcare">Healthcare Platform</a>
|
||||
<a href="/stories/wordpress-security.html" id="nav-wordpress">WordPress Security</a>
|
||||
<a href="/stories/airport-dns.html" id="nav-airport">Airport DNS</a>
|
||||
<a href="/stories/nitric-leadership.html" id="nav-nitric">NitricConcepts</a>
|
||||
<a href="/stories/open-source-success.html" id="nav-opensource">Open Source Success</a>
|
||||
<a href="/stories/airport-dns.html" id="nav-airportdns">Airport Dns</a>
|
||||
<a href="/stories/app-development.html" id="nav-appdevelopment">App Development</a>
|
||||
<a href="/stories/athion-turnaround.html" id="nav-athionturnaround">Athion Turnaround</a>
|
||||
<a href="/stories/fawe-plotsquared.html" id="nav-faweplotsquared">Fawe Plotsquared</a>
|
||||
<a href="/stories/healthcare-platform.html" id="nav-healthcareplatform">Healthcare Platform</a>
|
||||
<a href="/stories/motherboard-repair.html" id="nav-motherboardrepair">Motherboard Repair</a>
|
||||
<a href="/stories/nitric-leadership.html" id="nav-nitricleadership">Nitric Leadership</a>
|
||||
<a href="/stories/open-source-success.html" id="nav-opensourcesuccess">Open Source Success</a>
|
||||
<a href="/stories/showerloop.html" id="nav-showerloop">Showerloop</a>
|
||||
<a href="/stories/viperwire.html" id="nav-viperwire">Viperwire</a>
|
||||
<a href="/stories/web-design-java.html" id="nav-webdesignjava">Web Design Java</a>
|
||||
<a href="/stories/wordpress-security.html" id="nav-wordpresssecurity">Wordpress Security</a>
|
||||
<a href="/stories/youtube-game-dev.html" id="nav-youtubegamedev">Youtube Game Dev</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<script src="theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
|
||||
<script src="includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<script src="theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
|
||||
<script src="includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
@ -198,10 +198,6 @@
|
|||
<p><a href="stories/open-source-success.html" class="read-more">Read more about my open source success →</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<p class="accessibility-notice"><strong>Accessibility:</strong> This website is designed and developed to meet WCAG 2.1 Level AAA standards, ensuring the highest level of accessibility for all users. Features include high contrast ratios, keyboard navigation, screen reader compatibility, and responsive design. The site supports both light and dark modes with automatic system preference detection.</p>
|
||||
</div>
|
||||
|
||||
<!-- Footer Include -->
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
}
|
||||
</style>
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<script src="../utils.js" integrity="sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=" crossorigin="anonymous"></script>
|
||||
<!-- Add tool-specific scripts here -->
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
|
||||
<script src="tool-example.js" defer></script>
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://resume.example.com/index.html</loc>
|
||||
<lastmod>2025-07-06T22:28:44+00:00</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://resume.example.com/one-pager-tools/csv-tool.html</loc>
|
||||
<lastmod>2025-07-06T22:28:44+00:00</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://resume.example.com/stories/airport-dns.html</loc>
|
||||
<lastmod>2025-07-06T22:28:44+00:00</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://resume.example.com/stories/app-development.html</loc>
|
||||
<lastmod>2025-07-06T22:28:44+00:00</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://resume.example.com/stories/athion-turnaround.html</loc>
|
||||
<lastmod>2025-07-06T22:28:44+00:00</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://resume.example.com/stories/fawe-plotsquared.html</loc>
|
||||
<lastmod>2025-07-06T22:28:44+00:00</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://resume.example.com/stories/healthcare-platform.html</loc>
|
||||
<lastmod>2025-07-06T22:28:44+00:00</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://resume.example.com/stories/index.html</loc>
|
||||
<lastmod>2025-07-06T22:28:44+00:00</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://resume.example.com/stories/motherboard-repair.html</loc>
|
||||
<lastmod>2025-07-06T22:28:44+00:00</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://resume.example.com/stories/nitric-leadership.html</loc>
|
||||
<lastmod>2025-07-06T22:28:44+00:00</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://resume.example.com/stories/open-source-success.html</loc>
|
||||
<lastmod>2025-07-06T22:28:44+00:00</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://resume.example.com/stories/showerloop.html</loc>
|
||||
<lastmod>2025-07-06T22:28:44+00:00</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://resume.example.com/stories/viperwire.html</loc>
|
||||
<lastmod>2025-07-06T22:28:44+00:00</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://resume.example.com/stories/web-design-java.html</loc>
|
||||
<lastmod>2025-07-06T22:28:44+00:00</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://resume.example.com/stories/wordpress-security.html</loc>
|
||||
<lastmod>2025-07-06T22:28:44+00:00</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://resume.example.com/stories/youtube-game-dev.html</loc>
|
||||
<lastmod>2025-07-06T22:28:44+00:00</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
</urlset>
|
|
@ -9,9 +9,9 @@
|
|||
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous">
|
||||
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=">
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=">
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous">
|
||||
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=">
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=">
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous">
|
||||
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=">
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=">
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous">
|
||||
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=">
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=">
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous">
|
||||
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=">
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=">
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous">
|
||||
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=">
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=">
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
@ -23,101 +23,100 @@
|
|||
<hr>
|
||||
|
||||
<div class="stories-grid">
|
||||
<div class="story-card">
|
||||
<h2>Open Source Community Success</h2>
|
||||
<p class="story-excerpt">How I revitalized an abandoned open source project, built a thriving community of 32,000+ members, and established sustainable funding.</p>
|
||||
<p class="story-meta">Category: Open Source | Date: 2019-Present</p>
|
||||
<a href="open-source-success.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>ViperWire Cybersecurity</h2>
|
||||
<p class="story-excerpt">How I built an AI-powered cybersecurity consultancy from the ground up, focusing on cutting-edge protection for digital assets.</p>
|
||||
<p class="story-meta">Category: Cybersecurity | Date: 2023</p>
|
||||
<a href="viperwire.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>FastAsyncWorldEdit & PlotSquared</h2>
|
||||
<p class="story-excerpt">The technical challenges overcome in scaling Minecraft world editing from crashing at 50,000 edits to seamlessly handling billions.</p>
|
||||
<p class="story-meta">Category: Open Source | Date: 2014-Present</p>
|
||||
<a href="fawe-plotsquared.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>YouTube Game Development</h2>
|
||||
<p class="story-excerpt">Designing custom video games for prominent online creators while integrating advanced cybersecurity measures.</p>
|
||||
<p class="story-meta">Category: Game Development, Cybersecurity | Date: 2009-2022</p>
|
||||
<a href="youtube-game-dev.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>Web Design & Java Plugin Development</h2>
|
||||
<p class="story-excerpt">Developing web solutions and Java plugins with a focus on CI/CD efficiency and accessibility standards.</p>
|
||||
<p class="story-meta">Category: Web Development, Java | Date: 2009-2023</p>
|
||||
<a href="web-design-java.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>App Development for Influencers</h2>
|
||||
<p class="story-excerpt">Creating an ad revenue tracking app to help content creators optimize their earnings and content strategies.</p>
|
||||
<p class="story-meta">Category: Mobile Development, Analytics | Date: 2013-2018</p>
|
||||
<a href="app-development.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>Healthcare Platform Infrastructure</h2>
|
||||
<p class="story-excerpt">An in-depth look at the infrastructure design and security implementation for the Improving MI Practices healthcare platform.</p>
|
||||
<p class="story-meta">Category: Infrastructure | Date: 2019-Present</p>
|
||||
<a href="healthcare-platform.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>WordPress Security Automation</h2>
|
||||
<p class="story-excerpt">How I developed a Docker-based solution that eliminated persistent malware attacks on a high-profile website.</p>
|
||||
<p class="story-meta">Category: Security | Date: 2023</p>
|
||||
<a href="wordpress-security.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>Airport DNS Infrastructure</h2>
|
||||
<p class="story-excerpt">Building a geographically redundant DNS cluster for Flint Bishop International Airport that achieves A+ reliability standards.</p>
|
||||
<p class="story-meta">Category: Infrastructure | Date: 2019-Present</p>
|
||||
<p class="story-excerpt">Category: Infrastructure & Resilience | Date: 2019-Present...</p>
|
||||
<p class="story-meta">Category: Project | Date: Recent</p>
|
||||
<a href="airport-dns.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>NitricConcepts Leadership</h2>
|
||||
<p class="story-excerpt">Managing a distributed team of 45 contractors and implementing DevSecOps practices across multiple timezones.</p>
|
||||
<p class="story-meta">Category: Leadership | Date: 2018-2021</p>
|
||||
<a href="nitric-leadership.html" class="story-link">Read Full Story</a>
|
||||
<h2>App Development for Influencers</h2>
|
||||
<p class="story-excerpt">Category: Mobile Development, Analytics | Date: 2013-2018...</p>
|
||||
<p class="story-meta">Category: Project | Date: Recent</p>
|
||||
<a href="app-development.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>Athion.net Turnaround</h2>
|
||||
<p class="story-excerpt">Transforming a struggling business into a self-sustaining operation in just two weeks through strategic optimization.</p>
|
||||
<p class="story-meta">Category: Business Turnaround | Date: 2013-2017</p>
|
||||
<p class="story-excerpt">Category: Business Turnaround, System Optimization | Date: 2013-2017...</p>
|
||||
<p class="story-meta">Category: Project | Date: Recent</p>
|
||||
<a href="athion-turnaround.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>FastAsyncWorldEdit & PlotSquared</h2>
|
||||
<p class="story-excerpt">Category: Open Source Development | Date: 2014-Present...</p>
|
||||
<p class="story-meta">Category: Project | Date: Recent</p>
|
||||
<a href="fawe-plotsquared.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>Healthcare Platform Infrastructure</h2>
|
||||
<p class="story-excerpt">Category: Infrastructure & Security | Date: 2019-Present...</p>
|
||||
<p class="story-meta">Category: Project | Date: Recent</p>
|
||||
<a href="healthcare-platform.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>MotherboardRepair.ca</h2>
|
||||
<p class="story-excerpt">Co-founding a company dedicated to reducing electronic waste through specialized circuit board repair services.</p>
|
||||
<p class="story-meta">Category: Entrepreneurship, Sustainability | Date: 2019-Present</p>
|
||||
<p class="story-excerpt">Category: Entrepreneurship, Sustainable Technology | Date: 2019-Present...</p>
|
||||
<p class="story-meta">Category: Project | Date: Recent</p>
|
||||
<a href="motherboard-repair.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>DevOps & Leadership at NitricConcepts</h2>
|
||||
<p class="story-excerpt">Category: Team Leadership & DevOps | Date: 2018-2021...</p>
|
||||
<p class="story-meta">Category: Project | Date: Recent</p>
|
||||
<a href="nitric-leadership.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>Building a Thriving Open Source Community</h2>
|
||||
<p class="story-excerpt">Category: Open Source | Date: 2019-Present...</p>
|
||||
<p class="story-meta">Category: Project | Date: Recent</p>
|
||||
<a href="open-source-success.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>ShowerLoop Project</h2>
|
||||
<p class="story-excerpt">Revamping the website for an eco-friendly recirculating shower system with a focus on accessibility and modern design.</p>
|
||||
<p class="story-meta">Category: Web Development, Sustainability | Date: 2016</p>
|
||||
<p class="story-excerpt">Category: Web Development, Sustainability | Date: 2016...</p>
|
||||
<p class="story-meta">Category: Project | Date: Recent</p>
|
||||
<a href="showerloop.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>Building ViperWire: An AI-Powered Cybersecurity Consultancy</h2>
|
||||
<p class="story-excerpt">Category: Cybersecurity | Date: 2023-Present...</p>
|
||||
<p class="story-meta">Category: Project | Date: Recent</p>
|
||||
<a href="viperwire.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>Web Design & Java Plugin Development</h2>
|
||||
<p class="story-excerpt">Category: Web Development, Java | Date: 2009-2023...</p>
|
||||
<p class="story-meta">Category: Project | Date: Recent</p>
|
||||
<a href="web-design-java.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>WordPress Security Automation</h2>
|
||||
<p class="story-excerpt">Category: Security & Automation | Date: 2023...</p>
|
||||
<p class="story-meta">Category: Project | Date: Recent</p>
|
||||
<a href="wordpress-security.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
|
||||
<div class="story-card">
|
||||
<h2>YouTube Game Development & Cybersecurity</h2>
|
||||
<p class="story-excerpt">Category: Game Development, Cybersecurity | Date: 2009-2022...</p>
|
||||
<p class="story-meta">Category: Project | Date: Recent</p>
|
||||
<a href="youtube-game-dev.html" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<p class="accessibility-notice"><strong>Accessibility:</strong> This website is designed and developed to meet WCAG 2.1 Level AAA standards, ensuring the highest level of accessibility for all users. Features include high contrast ratios, keyboard navigation, screen reader compatibility, and responsive design. The site supports both light and dark modes with automatic system preference detection.</p>
|
||||
</div>
|
||||
|
||||
<!-- Footer Include -->
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous">
|
||||
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=">
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=">
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous">
|
||||
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=">
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=">
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=">
|
||||
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
|
||||
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=">
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=">
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous">
|
||||
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=">
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=">
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -156,11 +156,13 @@
|
|||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
color: #004494;
|
||||
}
|
||||
|
||||
.story-nav-link:hover {
|
||||
background-color: var(--theme-hover);
|
||||
text-decoration: none;
|
||||
color: #003366;
|
||||
}
|
||||
|
||||
.story-nav-link.prev::before {
|
||||
|
@ -240,6 +242,10 @@
|
|||
|
||||
.placeholder-notice a {
|
||||
font-weight: bold;
|
||||
color: var(--accent-color);
|
||||
color: #004494;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.placeholder-notice a:hover {
|
||||
color: #003366;
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
/* Story-specific styles */
|
||||
.story-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.story-header {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.story-header h1 {
|
||||
font-size: 2em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.story-meta {
|
||||
font-style: italic;
|
||||
color: #555;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.story-content {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.story-content p {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.story-content h2 {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.story-content h3 {
|
||||
margin-top: 25px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.story-content ul, .story-content ol {
|
||||
margin-bottom: 15px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.story-content li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.story-content blockquote {
|
||||
margin: 20px 0;
|
||||
padding: 10px 20px;
|
||||
border-left: 4px solid #ccc;
|
||||
background-color: #f9f9f9;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.story-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 20px 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.story-content .image-caption {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
margin-top: -15px;
|
||||
margin-bottom: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.story-content code {
|
||||
font-family: monospace;
|
||||
background-color: #f0f0f0;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.story-content pre {
|
||||
background-color: #f0f0f0;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.story-content pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.story-navigation {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.story-nav-link {
|
||||
padding: 8px 15px;
|
||||
background-color: #f0f0f0;
|
||||
color: #004494; /* Darker blue for 7:1+ contrast ratio */
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.story-nav-link:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.story-nav-link.prev::before {
|
||||
content: "← ";
|
||||
}
|
||||
|
||||
.story-nav-link.next::after {
|
||||
content: " →";
|
||||
}
|
||||
|
||||
.story-tags {
|
||||
margin-top: 20px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.story-tag {
|
||||
display: inline-block;
|
||||
background-color: #e0e0e0;
|
||||
color: #333;
|
||||
padding: 3px 8px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Story index page styles */
|
||||
.stories-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.story-card {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.story-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.story-card-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.story-card h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.story-card p {
|
||||
margin-bottom: 15px;
|
||||
color: #555;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.story-card-link {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
background-color: #f0f0f0;
|
||||
color: #004494; /* Darker blue for 7:1+ contrast ratio */
|
||||
text-decoration: none;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.story-card-link:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.stories-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.story-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.story-header h1 {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.story-content h2 {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.story-content h3 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
|
@ -7,11 +7,11 @@
|
|||
<title>Story Example - Colin Knapp</title>
|
||||
<link rel="icon" type="image/x-icon" href="../favicon.ico">
|
||||
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=">
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=">
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=">
|
||||
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
|
||||
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous">
|
||||
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=">
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=">
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous">
|
||||
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous">
|
||||
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=">
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=">
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous">
|
||||
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=">
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=">
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous">
|
||||
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=">
|
||||
<link rel="stylesheet" href="stories.css" integrity="sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=">
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<script src="theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
|
||||
<script src="includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
|
||||
</head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-JR8sYN1/jgctBktEsjejl175usnuJQ+LimW18BWyL8I=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-5Lrk4RP6+4oP0Dbe2qVepxbZ0tYjXoWQHz55YlbGXFk=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=' 'sha256-r0ECPtfllGARVL3R4rbe8FsQgyNZPyqJ6vkvvwXQpqM=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';">
|
||||
<body>
|
||||
<!-- Header Include -->
|
||||
<div id="header-include"></div>
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
{"level":"info","ts":1751831831.5810199,"msg":"maxprocs: Leaving GOMAXPROCS=8: CPU quota undefined"}
|
||||
{"level":"info","ts":1751831831.5824819,"msg":"GOMEMLIMIT is updated","package":"github.com/KimMachineGun/automemlimit/memlimit","GOMEMLIMIT":7730941132,"previous":9223372036854775807}
|
||||
{"level":"info","ts":1751831831.582569,"msg":"using config from file","file":"Caddyfile.local"}
|
||||
{"level":"info","ts":1751831831.585237,"msg":"adapted config to JSON","adapter":"caddyfile"}
|
||||
{"level":"warn","ts":1751831831.585273,"msg":"Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies","adapter":"caddyfile","file":"Caddyfile.local","line":15}
|
||||
{"level":"info","ts":1751831831.597684,"logger":"admin","msg":"admin endpoint started","address":"localhost:2019","enforce_origin":false,"origins":["//localhost:2019","//[::1]:2019","//127.0.0.1:2019"]}
|
||||
{"level":"info","ts":1751831831.5983381,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0x140004cb480"}
|
||||
{"level":"warn","ts":1751831831.600636,"logger":"http","msg":"HTTP/2 skipped because it requires TLS","network":"tcp","addr":":8080"}
|
||||
{"level":"warn","ts":1751831831.600668,"logger":"http","msg":"HTTP/3 skipped because it requires TLS","network":"tcp","addr":":8080"}
|
||||
{"level":"info","ts":1751831831.600673,"logger":"http.log","msg":"server running","name":"srv0","protocols":["h1","h2","h3"]}
|
||||
{"level":"info","ts":1751831831.6013281,"msg":"autosaved config (load with --resume flag)","file":"/Users/computerpro/Library/Application Support/Caddy/autosave.json"}
|
||||
{"level":"info","ts":1751831831.601336,"msg":"serving initial configuration"}
|
||||
{"level":"info","ts":1751831831.607745,"logger":"tls","msg":"storage cleaning happened too recently; skipping for now","storage":"FileStorage:/Users/computerpro/Library/Application Support/Caddy","instance":"bb1987a6-f2f6-4230-a2aa-e3b16b9f988e","try_again":1751918231.607744,"try_again_in":86399.99999975}
|
||||
{"level":"info","ts":1751831831.6082098,"logger":"tls","msg":"finished cleaning storage units"}
|
||||
{"level":"info","ts":1751833051.9393191,"msg":"shutting down apps, then terminating","signal":"SIGTERM"}
|
||||
{"level":"warn","ts":1751833051.944179,"msg":"exiting; byeee!! 👋","signal":"SIGTERM"}
|
||||
{"level":"info","ts":1751833051.9507608,"logger":"http","msg":"servers shutting down with eternal grace period"}
|
||||
{"level":"info","ts":1751833051.9561532,"logger":"admin","msg":"stopped previous server","address":"localhost:2019"}
|
||||
{"level":"info","ts":1751833051.956237,"msg":"shutdown complete","signal":"SIGTERM","exit_code":0}
|
|
@ -0,0 +1,80 @@
|
|||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# update-all.sh - Run all update scripts
|
||||
# =====================================================================
|
||||
# This script runs all update scripts in the correct order:
|
||||
# 1. Generate sitemap.xml
|
||||
# 2. Update navigation menu from sitemap
|
||||
# 3. Update stories page from sitemap
|
||||
# 4. Update CSP hashes
|
||||
# 5. Apply accessibility fixes
|
||||
# =====================================================================
|
||||
|
||||
set -e
|
||||
|
||||
echo "=== Running all update scripts ==="
|
||||
|
||||
# Get the directory of this script
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
|
||||
# Generate sitemap.xml
|
||||
echo "=== Generating sitemap.xml ==="
|
||||
if [ -f "$SCRIPT_DIR/generate-sitemap.sh" ]; then
|
||||
"$SCRIPT_DIR/generate-sitemap.sh"
|
||||
else
|
||||
echo "⚠️ generate-sitemap.sh not found, skipping"
|
||||
fi
|
||||
|
||||
# Update navigation from sitemap
|
||||
echo "=== Updating navigation from sitemap ==="
|
||||
if [ -f "$SCRIPT_DIR/update-nav-from-sitemap.js" ]; then
|
||||
node "$SCRIPT_DIR/update-nav-from-sitemap.js"
|
||||
else
|
||||
echo "⚠️ update-nav-from-sitemap.js not found, skipping"
|
||||
fi
|
||||
|
||||
# Update stories page from sitemap
|
||||
echo "=== Updating stories page from sitemap ==="
|
||||
if [ -f "$SCRIPT_DIR/update-stories-page.js" ]; then
|
||||
node "$SCRIPT_DIR/update-stories-page.js"
|
||||
else
|
||||
echo "⚠️ update-stories-page.js not found, skipping"
|
||||
fi
|
||||
|
||||
# Update CSP hashes
|
||||
echo "=== Updating CSP hashes ==="
|
||||
if [ -f "$SCRIPT_DIR/update-csp-hashes.sh" ]; then
|
||||
"$SCRIPT_DIR/update-csp-hashes.sh"
|
||||
else
|
||||
echo "⚠️ update-csp-hashes.sh not found, skipping"
|
||||
fi
|
||||
|
||||
# Ensure accessibility fixes are applied
|
||||
echo "=== Ensuring accessibility fixes are applied ==="
|
||||
# Check if stories.css exists
|
||||
if [ -f "$SCRIPT_DIR/stories/stories.css" ]; then
|
||||
# Check if the file already has the accessibility fixes
|
||||
if ! grep -q "color: #004494;" "$SCRIPT_DIR/stories/stories.css"; then
|
||||
echo "Applying accessibility fixes to stories.css..."
|
||||
# Use sed to add the color property to story-nav-link class
|
||||
sed -i '' 's/\.story-nav-link {/\.story-nav-link {\n color: #004494; \/* Darker blue for 7:1+ contrast ratio *\//g' "$SCRIPT_DIR/stories/stories.css"
|
||||
sed -i '' 's/\.story-nav-link:hover {/\.story-nav-link:hover {\n color: #003366; \/* Even darker on hover for better visibility *\//g' "$SCRIPT_DIR/stories/stories.css"
|
||||
|
||||
# Update placeholder-notice links
|
||||
sed -i '' 's/\.placeholder-notice a {/\.placeholder-notice a {\n color: #004494; \/* Darker blue for 7:1+ contrast ratio *\//g' "$SCRIPT_DIR/stories/stories.css"
|
||||
|
||||
# Add hover state for placeholder-notice links if it doesn't exist
|
||||
if ! grep -q "\.placeholder-notice a:hover" "$SCRIPT_DIR/stories/stories.css"; then
|
||||
echo -e "\n.placeholder-notice a:hover {\n color: #003366; /* Even darker on hover */\n}" >> "$SCRIPT_DIR/stories/stories.css"
|
||||
fi
|
||||
|
||||
echo "Accessibility fixes applied to stories.css"
|
||||
else
|
||||
echo "Accessibility fixes already present in stories.css"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ stories/stories.css not found, skipping accessibility fixes"
|
||||
fi
|
||||
|
||||
echo "=== All updates completed successfully ==="
|
||||
echo "To apply changes, restart the server using: ./caddy.sh"
|
|
@ -0,0 +1,133 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* update-nav-from-sitemap.js
|
||||
*
|
||||
* This script reads the sitemap.xml file and updates the navigation menu in header.html
|
||||
* to include all story pages found in the sitemap.
|
||||
*
|
||||
* No external dependencies required.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Configuration
|
||||
const SITEMAP_PATH = path.join(__dirname, 'sitemap.xml');
|
||||
const HEADER_PATH = path.join(__dirname, 'includes', 'header.html');
|
||||
const STORIES_PATH_PREFIX = '/stories/';
|
||||
const STORIES_PATH_EXCLUDE = ['index.html', 'story-with-includes.html', 'template-story.html'];
|
||||
|
||||
// Helper function to get a friendly name from a filename
|
||||
function getFriendlyName(filename) {
|
||||
// Remove .html extension
|
||||
let name = filename.replace('.html', '');
|
||||
|
||||
// Replace hyphens with spaces
|
||||
name = name.replace(/-/g, ' ');
|
||||
|
||||
// Capitalize first letter of each word
|
||||
return name.split(' ')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
// Simple XML parser for sitemap (no external dependencies)
|
||||
function extractUrlsFromSitemap(sitemapContent) {
|
||||
const urls = [];
|
||||
const matches = sitemapContent.match(/<loc>([^<]+)<\/loc>/g) || [];
|
||||
|
||||
for (const match of matches) {
|
||||
const url = match.replace('<loc>', '').replace('</loc>', '');
|
||||
urls.push(url);
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
// Main function
|
||||
async function updateNavFromSitemap() {
|
||||
console.log('Updating navigation menu from sitemap.xml...');
|
||||
|
||||
try {
|
||||
// Read sitemap.xml
|
||||
const sitemapContent = fs.readFileSync(SITEMAP_PATH, 'utf8');
|
||||
const urls = extractUrlsFromSitemap(sitemapContent);
|
||||
|
||||
// Extract story URLs from sitemap
|
||||
const storyPages = [];
|
||||
|
||||
for (const url of urls) {
|
||||
// Check if this is a story page
|
||||
if (url.includes(STORIES_PATH_PREFIX)) {
|
||||
const filename = url.split('/').pop();
|
||||
|
||||
// Skip excluded files
|
||||
if (!STORIES_PATH_EXCLUDE.includes(filename)) {
|
||||
storyPages.push({
|
||||
filename,
|
||||
url: STORIES_PATH_PREFIX + filename,
|
||||
name: getFriendlyName(filename)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort story pages alphabetically by name
|
||||
storyPages.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Read header.html
|
||||
let headerContent = fs.readFileSync(HEADER_PATH, 'utf8');
|
||||
|
||||
// Find the stories dropdown content
|
||||
const dropdownStartMarker = '<a href="/stories/" id="nav-stories">Stories</a>';
|
||||
const dropdownEndMarker = '</div>';
|
||||
|
||||
const dropdownStartIndex = headerContent.indexOf(dropdownStartMarker);
|
||||
if (dropdownStartIndex === -1) {
|
||||
throw new Error('Could not find stories dropdown in header.html');
|
||||
}
|
||||
|
||||
// Find the start of the dropdown content
|
||||
const contentStartIndex = headerContent.indexOf('<div class="dropdown-content">', dropdownStartIndex);
|
||||
if (contentStartIndex === -1) {
|
||||
throw new Error('Could not find dropdown content in header.html');
|
||||
}
|
||||
|
||||
// Find the end of the dropdown content
|
||||
const contentEndIndex = headerContent.indexOf(dropdownEndMarker, contentStartIndex);
|
||||
if (contentEndIndex === -1) {
|
||||
throw new Error('Could not find end of dropdown content in header.html');
|
||||
}
|
||||
|
||||
// Generate new dropdown content
|
||||
let newDropdownContent = '<div class="dropdown-content">\n';
|
||||
storyPages.forEach(page => {
|
||||
const navId = 'nav-' + page.filename.replace('.html', '').replace(/-/g, '');
|
||||
newDropdownContent += ` <a href="${page.url}" id="${navId}">${page.name}</a>\n`;
|
||||
});
|
||||
|
||||
// Replace dropdown content in header.html
|
||||
const newHeaderContent =
|
||||
headerContent.substring(0, contentStartIndex) +
|
||||
newDropdownContent +
|
||||
' ' + dropdownEndMarker +
|
||||
headerContent.substring(contentEndIndex + dropdownEndMarker.length);
|
||||
|
||||
// Write updated header.html
|
||||
fs.writeFileSync(HEADER_PATH, newHeaderContent, 'utf8');
|
||||
|
||||
console.log(`Updated navigation menu with ${storyPages.length} story pages`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error updating navigation menu:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute if run directly
|
||||
if (require.main === module) {
|
||||
updateNavFromSitemap();
|
||||
}
|
||||
|
||||
module.exports = { updateNavFromSitemap };
|
|
@ -0,0 +1,185 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* update-stories-page.js
|
||||
*
|
||||
* This script reads the sitemap.xml file and updates the stories/index.html page
|
||||
* to include all story pages found in the sitemap.
|
||||
*
|
||||
* No external dependencies required.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Configuration
|
||||
const SITEMAP_PATH = path.join(__dirname, 'sitemap.xml');
|
||||
const STORIES_INDEX_PATH = path.join(__dirname, 'stories', 'index.html');
|
||||
const STORIES_PATH_PREFIX = '/stories/';
|
||||
const STORIES_PATH_EXCLUDE = ['index.html', 'story-with-includes.html', 'template-story.html'];
|
||||
|
||||
// Helper function to get a friendly name from a filename
|
||||
function getFriendlyName(filename) {
|
||||
// Remove .html extension
|
||||
let name = filename.replace('.html', '');
|
||||
|
||||
// Split by hyphens and capitalize each word
|
||||
name = name.split('-').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join(' ');
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
// Simple XML parser for sitemap (no external dependencies)
|
||||
function extractUrlsFromSitemap(sitemapContent) {
|
||||
const urls = [];
|
||||
const matches = sitemapContent.match(/<loc>([^<]+)<\/loc>/g) || [];
|
||||
|
||||
for (const match of matches) {
|
||||
const url = match.replace('<loc>', '').replace('</loc>', '');
|
||||
urls.push(url);
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
// Helper function to extract metadata from a story file
|
||||
async function extractStoryMetadata(storyPath) {
|
||||
try {
|
||||
const content = fs.readFileSync(storyPath, 'utf8');
|
||||
|
||||
// Default metadata
|
||||
let metadata = {
|
||||
title: getFriendlyName(path.basename(storyPath)),
|
||||
excerpt: 'Detailed case study and project information.',
|
||||
category: 'Project',
|
||||
date: 'Recent'
|
||||
};
|
||||
|
||||
// Try to extract title from h1
|
||||
const h1Match = content.match(/<h1[^>]*>(.*?)<\/h1>/i);
|
||||
if (h1Match && h1Match[1]) {
|
||||
metadata.title = h1Match[1].trim();
|
||||
}
|
||||
|
||||
// Try to extract excerpt from first paragraph after h1
|
||||
const excerptMatch = content.match(/<h1[^>]*>.*?<\/h1>\s*<p[^>]*>(.*?)<\/p>/is);
|
||||
if (excerptMatch && excerptMatch[1]) {
|
||||
metadata.excerpt = excerptMatch[1].trim().substring(0, 150) + '...';
|
||||
}
|
||||
|
||||
// Try to extract category and date
|
||||
const categoryMatch = content.match(/<strong>Category:<\/strong>\s*(.*?)(?:<|$)/i);
|
||||
if (categoryMatch && categoryMatch[1]) {
|
||||
metadata.category = categoryMatch[1].trim();
|
||||
}
|
||||
|
||||
const dateMatch = content.match(/<strong>(?:Date|Timeframe):<\/strong>\s*(.*?)(?:<|$)/i);
|
||||
if (dateMatch && dateMatch[1]) {
|
||||
metadata.date = dateMatch[1].trim();
|
||||
}
|
||||
|
||||
return metadata;
|
||||
} catch (error) {
|
||||
console.error(`Error extracting metadata from ${storyPath}:`, error);
|
||||
return {
|
||||
title: getFriendlyName(path.basename(storyPath)),
|
||||
excerpt: 'Detailed case study and project information.',
|
||||
category: 'Project',
|
||||
date: 'Recent'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Main function
|
||||
async function updateStoriesPage() {
|
||||
try {
|
||||
console.log('Updating stories page from sitemap.xml...');
|
||||
|
||||
// Check if sitemap.xml exists
|
||||
if (!fs.existsSync(SITEMAP_PATH)) {
|
||||
console.error('Sitemap.xml not found at:', SITEMAP_PATH);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if stories/index.html exists
|
||||
if (!fs.existsSync(STORIES_INDEX_PATH)) {
|
||||
console.error('Stories index page not found at:', STORIES_INDEX_PATH);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read sitemap.xml
|
||||
const sitemapContent = fs.readFileSync(SITEMAP_PATH, 'utf8');
|
||||
const allUrls = extractUrlsFromSitemap(sitemapContent);
|
||||
|
||||
// Find all story URLs in sitemap
|
||||
const storyUrls = [];
|
||||
|
||||
for (const url of allUrls) {
|
||||
if (url.includes(STORIES_PATH_PREFIX)) {
|
||||
const filename = url.split('/').pop();
|
||||
if (!STORIES_PATH_EXCLUDE.includes(filename)) {
|
||||
storyUrls.push({
|
||||
url: url,
|
||||
filename: filename
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Found ${storyUrls.length} story pages in sitemap.`);
|
||||
|
||||
// Read stories/index.html
|
||||
const storiesIndexContent = fs.readFileSync(STORIES_INDEX_PATH, 'utf8');
|
||||
|
||||
// Extract metadata for each story
|
||||
const storyCards = [];
|
||||
|
||||
for (const storyUrl of storyUrls) {
|
||||
const storyPath = path.join(__dirname, 'stories', storyUrl.filename);
|
||||
if (fs.existsSync(storyPath)) {
|
||||
const metadata = await extractStoryMetadata(storyPath);
|
||||
storyCards.push({
|
||||
filename: storyUrl.filename,
|
||||
title: metadata.title,
|
||||
excerpt: metadata.excerpt,
|
||||
category: metadata.category,
|
||||
date: metadata.date
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Generate HTML for story cards
|
||||
let storiesGridHtml = '<div class="stories-grid">\n';
|
||||
|
||||
for (const card of storyCards) {
|
||||
storiesGridHtml += `
|
||||
<div class="story-card">
|
||||
<h2>${card.title}</h2>
|
||||
<p class="story-excerpt">${card.excerpt}</p>
|
||||
<p class="story-meta">Category: ${card.category} | Date: ${card.date}</p>
|
||||
<a href="${card.filename}" class="story-link">Read Full Story</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
storiesGridHtml += ' </div>';
|
||||
|
||||
// Replace the stories grid in the index.html
|
||||
const updatedContent = storiesIndexContent.replace(
|
||||
/<div class="stories-grid">[\s\S]*?<\/div>\s*<hr>/,
|
||||
`${storiesGridHtml}\n\n <hr>`
|
||||
);
|
||||
|
||||
// Write the updated content back to the file
|
||||
fs.writeFileSync(STORIES_INDEX_PATH, updatedContent, 'utf8');
|
||||
|
||||
console.log(`Updated stories page with ${storyCards.length} story cards.`);
|
||||
} catch (error) {
|
||||
console.error('Error updating stories page:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the main function
|
||||
updateStoriesPage();
|
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
|
@ -1,37 +1,20 @@
|
|||
{
|
||||
"name": "resume",
|
||||
"name": "resume-site",
|
||||
"version": "1.0.0",
|
||||
"description": "Colin Knapp's professional resume website",
|
||||
"description": "Resume website with accessibility testing",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"serve": "node tests/serve.js",
|
||||
"test": "npm run test:lighthouse && npm run test:playwright",
|
||||
"test:playwright": "npx playwright test",
|
||||
"test:lighthouse": "node tests/lighthouse.js",
|
||||
"test:headers": "playwright test tests/headers.spec.js",
|
||||
"setup": "npx playwright install"
|
||||
"test": "tests/run-all-tests.sh",
|
||||
"test:accessibility": "tests/accessibility/run-accessibility-tests.sh",
|
||||
"test:a11y:axe": "node tests/accessibility/axe-test.js",
|
||||
"test:a11y:pa11y": "tests/accessibility/pa11y-test.sh",
|
||||
"test:a11y:manual": "echo 'Please complete the manual checklist at tests/accessibility/manual-checklist.md'",
|
||||
"start": "cd docker/resume && ./caddy.sh"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@git.nixc.us:colin/resume.git"
|
||||
},
|
||||
"keywords": [
|
||||
"resume",
|
||||
"portfolio",
|
||||
"accessibility"
|
||||
],
|
||||
"author": "Colin Knapp",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.10.1",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"chrome-launcher": "^1.1.2",
|
||||
"lighthouse": "^11.4.0",
|
||||
"puppeteer": "^22.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"lighthouse": "^11.6.0",
|
||||
"playwright": "^1.42.1"
|
||||
"axe-core": "^4.10.3",
|
||||
"lighthouse": "^10.0.0",
|
||||
"pa11y": "^9.0.0",
|
||||
"playwright": "^1.53.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,23 +6,35 @@ const { defineConfig, devices } = require('@playwright/test');
|
|||
*/
|
||||
module.exports = defineConfig({
|
||||
testDir: './tests',
|
||||
timeout: 30 * 1000,
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 60 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
*/
|
||||
timeout: 5000
|
||||
},
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
trace: 'on-first-retry',
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:8080',
|
||||
screenshot: 'only-on-failure',
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
|
@ -36,11 +48,21 @@ module.exports = defineConfig({
|
|||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
/* Test against mobile viewports. */
|
||||
{
|
||||
name: 'Mobile Chrome',
|
||||
use: { ...devices['Pixel 5'] },
|
||||
},
|
||||
{
|
||||
name: 'Mobile Safari',
|
||||
use: { ...devices['iPhone 12'] },
|
||||
},
|
||||
],
|
||||
|
||||
/* Run local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'node tests/serve.js',
|
||||
port: 8080,
|
||||
reuseExistingServer: true,
|
||||
url: 'http://localhost:8080',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
# Resume Site Tests
|
||||
|
||||
This directory contains tests for the resume site.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
- `unit/`: Unit tests for JavaScript files and other components
|
||||
- `integration/`: Integration tests that test components working together
|
||||
- `e2e/`: End-to-end tests that simulate user interactions
|
||||
- `*.test.js`, `*.spec.js`: Playwright test files
|
||||
- `lighthouse.js`: Lighthouse performance and accessibility tests
|
||||
- `serve.js`: Simple Node.js server for testing
|
||||
- `server.js`: Alternative server implementation
|
||||
- `pre-test-setup.sh`: Script to set up the test environment, including updating CSP hashes
|
||||
|
||||
## Running Tests
|
||||
|
||||
To run all tests:
|
||||
|
||||
```bash
|
||||
./run-all-tests.sh
|
||||
```
|
||||
|
||||
Or using npm:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Run the pre-test setup script to update CSP hashes
|
||||
2. Check if the server is running
|
||||
3. Run all shell script tests
|
||||
4. Run Playwright tests if available
|
||||
5. Run Lighthouse tests
|
||||
|
||||
## Content Security Policy (CSP) Testing
|
||||
|
||||
The CSP hash update process is an important part of the testing framework. It ensures that:
|
||||
|
||||
1. All JavaScript and CSS files have integrity hashes
|
||||
2. All inline styles have proper CSP hashes
|
||||
3. The Caddyfile and HTML files have the correct CSP headers/meta tags
|
||||
|
||||
The `pre-test-setup.sh` script runs the `update-csp-hashes.sh` script to update all CSP hashes before running the tests. This ensures that any changes to the website are properly reflected in the CSP hashes.
|
||||
|
||||
The `csp-hash-test.sh` integration test checks if the CSP hash update process is working properly by verifying that:
|
||||
|
||||
- CSP headers are present in the response
|
||||
- CSP headers contain the required directives
|
||||
- JavaScript and CSS files have integrity attributes
|
||||
- HTML files have CSP meta tags
|
||||
|
||||
## Running Specific Tests
|
||||
|
||||
### JavaScript Tests
|
||||
|
||||
```bash
|
||||
npm run test:js
|
||||
```
|
||||
|
||||
### Lighthouse Tests
|
||||
|
||||
```bash
|
||||
npm run test:lighthouse
|
||||
```
|
||||
|
||||
### Starting the Test Server
|
||||
|
||||
```bash
|
||||
npm run serve
|
||||
```
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
### Shell Script Tests
|
||||
|
||||
Add new test scripts to the appropriate directory:
|
||||
- `unit/`: For unit tests
|
||||
- `integration/`: For integration tests
|
||||
- `e2e/`: For end-to-end tests
|
||||
|
||||
Shell script tests should:
|
||||
- Be executable bash scripts
|
||||
- Return exit code 0 for success, non-zero for failure
|
||||
- For integration and e2e tests, accept a base URL as the first argument
|
||||
|
||||
### Playwright Tests
|
||||
|
||||
Add new Playwright tests with the `.test.js` or `.spec.js` extension in the tests directory.
|
||||
|
||||
## Test Requirements
|
||||
|
||||
As per the project guidelines, all tests must:
|
||||
- Pass for both mobile and desktop viewports
|
||||
- Maintain Lighthouse scores: 100/100 for accessibility and SEO
|
||||
- Include meaningful assertions, not placeholders
|
|
@ -1,222 +1,22 @@
|
|||
const { test, expect } = require('@playwright/test');
|
||||
const { AxeBuilder } = require('@axe-core/playwright');
|
||||
/**
|
||||
* Main accessibility test runner
|
||||
*
|
||||
* This file is used by Jest to run the accessibility tests
|
||||
*/
|
||||
|
||||
const PRODUCTION_URL = 'https://colinknapp.com';
|
||||
const LOCAL_URL = 'http://localhost:8080';
|
||||
const { execSync } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
const viewports = [
|
||||
{ width: 375, height: 667, name: 'mobile' },
|
||||
{ width: 1024, height: 768, name: 'desktop' }
|
||||
];
|
||||
|
||||
async function getPageUrl(page) {
|
||||
describe('Accessibility Tests', () => {
|
||||
test('Run accessibility tests', () => {
|
||||
try {
|
||||
// Try production first
|
||||
await page.goto(PRODUCTION_URL, { timeout: 60000 });
|
||||
return PRODUCTION_URL;
|
||||
// Run the accessibility tests
|
||||
const scriptPath = path.join(__dirname, 'accessibility', 'run-accessibility-tests.sh');
|
||||
execSync(`bash ${scriptPath}`, { stdio: 'inherit' });
|
||||
} catch (error) {
|
||||
console.log('Production site not available, falling back to local');
|
||||
await page.goto(LOCAL_URL, { timeout: 60000 });
|
||||
return LOCAL_URL;
|
||||
// If the tests fail, the script will exit with a non-zero code
|
||||
// Jest will catch this and mark the test as failed
|
||||
throw new Error('Accessibility tests failed');
|
||||
}
|
||||
}
|
||||
|
||||
viewports.forEach(viewport => {
|
||||
test.describe(`Accessibility Tests (${viewport.name})`, () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
});
|
||||
|
||||
test('should pass WCAG 2.1 Level AAA standards', async ({ page }) => {
|
||||
const url = await getPageUrl(page);
|
||||
console.log(`Running accessibility tests against ${url} on ${viewport.name}`);
|
||||
|
||||
// Run axe accessibility tests
|
||||
const results = await new AxeBuilder({ page })
|
||||
.withTags(['wcag2a', 'wcag2aa', 'wcag2aaa'])
|
||||
.analyze();
|
||||
|
||||
// Check for any violations
|
||||
expect(results.violations).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should have proper ARIA attributes for theme toggle', async ({ page }) => {
|
||||
const url = await getPageUrl(page);
|
||||
console.log(`Running ARIA tests against ${url} on ${viewport.name}`);
|
||||
|
||||
// Check theme toggle button
|
||||
const themeToggle = await page.locator('#themeToggle');
|
||||
expect(await themeToggle.getAttribute('aria-label')).toBe('Theme mode: Auto');
|
||||
expect(await themeToggle.getAttribute('role')).toBe('switch');
|
||||
expect(await themeToggle.getAttribute('aria-checked')).toBe('false');
|
||||
expect(await themeToggle.getAttribute('title')).toBe('Toggle between light, dark, and auto theme modes');
|
||||
expect(await themeToggle.getAttribute('tabindex')).toBe('0');
|
||||
});
|
||||
|
||||
test('should have proper heading structure', async ({ page }) => {
|
||||
const url = await getPageUrl(page);
|
||||
console.log(`Running heading structure tests against ${url} on ${viewport.name}`);
|
||||
|
||||
// Check main content area
|
||||
const mainContent = await page.locator('.container-fluid');
|
||||
expect(await mainContent.getAttribute('role')).toBe('main');
|
||||
|
||||
// Check heading hierarchy
|
||||
const h1 = await page.locator('h1');
|
||||
expect(await h1.count()).toBe(1);
|
||||
|
||||
const h2s = await page.locator('h2');
|
||||
expect(await h2s.count()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should have working external links', async ({ page, request }) => {
|
||||
const url = await getPageUrl(page);
|
||||
console.log(`Running link validation tests against ${url} on ${viewport.name}`);
|
||||
await page.goto(url, { timeout: 60000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Get all external links
|
||||
const externalLinks = await page.$$('a[href^="http"]:not([href^="http://localhost"])');
|
||||
|
||||
// Skip test if no external links found
|
||||
if (externalLinks.length === 0) {
|
||||
console.log('No external links found, skipping test');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set a longer timeout for external link checks
|
||||
test.setTimeout(120000);
|
||||
|
||||
const brokenLinks = [];
|
||||
for (const link of externalLinks) {
|
||||
const href = await link.getAttribute('href');
|
||||
if (!href) continue;
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = 3;
|
||||
let success = false;
|
||||
while (attempts < maxAttempts && !success) {
|
||||
attempts++;
|
||||
try {
|
||||
const response = await request.head(href, { timeout: 15000 });
|
||||
if (response.status() >= 400) {
|
||||
brokenLinks.push({
|
||||
href,
|
||||
status: response.status(),
|
||||
attempt: attempts
|
||||
});
|
||||
} else {
|
||||
success = true;
|
||||
}
|
||||
} catch (error) {
|
||||
if (attempts === maxAttempts) {
|
||||
brokenLinks.push({
|
||||
href,
|
||||
error: error.message,
|
||||
attempt: attempts
|
||||
});
|
||||
}
|
||||
// Wait before retrying
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (brokenLinks.length > 0) {
|
||||
console.log('\nBroken or inaccessible links:');
|
||||
brokenLinks.forEach(link => {
|
||||
if (link.error) {
|
||||
console.log(`- ${link.href}: ${link.error} (Attempt ${link.attempt}/${maxAttempts})`);
|
||||
} else {
|
||||
console.log(`- ${link.href}: HTTP ${link.status} (Attempt ${link.attempt}/${maxAttempts})`);
|
||||
}
|
||||
});
|
||||
throw new Error('Some external links are broken or inaccessible');
|
||||
}
|
||||
});
|
||||
|
||||
test('should have proper color contrast', async ({ page }) => {
|
||||
const url = await getPageUrl(page);
|
||||
console.log(`Running color contrast tests against ${url} on ${viewport.name}`);
|
||||
await page.goto(url, { timeout: 60000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check text color contrast in both light and dark modes
|
||||
const contrastInfo = await page.evaluate(() => {
|
||||
const getContrastRatio = (color1, color2) => {
|
||||
const getLuminance = (r, g, b) => {
|
||||
const [rs, gs, bs] = [r, g, b].map(c => {
|
||||
c = c / 255;
|
||||
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
||||
};
|
||||
|
||||
const parseColor = (color) => {
|
||||
const rgb = color.match(/\d+/g).map(Number);
|
||||
return rgb.length === 3 ? rgb : [0, 0, 0];
|
||||
};
|
||||
|
||||
const l1 = getLuminance(...parseColor(color1));
|
||||
const l2 = getLuminance(...parseColor(color2));
|
||||
const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
||||
return ratio.toFixed(2);
|
||||
};
|
||||
|
||||
const style = getComputedStyle(document.body);
|
||||
const textColor = style.color;
|
||||
const backgroundColor = style.backgroundColor;
|
||||
const contrastRatio = getContrastRatio(textColor, backgroundColor);
|
||||
|
||||
return {
|
||||
textColor,
|
||||
backgroundColor,
|
||||
contrastRatio: parseFloat(contrastRatio)
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Color contrast information:', contrastInfo);
|
||||
|
||||
// WCAG 2.1 Level AAA requires a contrast ratio of at least 7:1 for normal text
|
||||
expect(contrastInfo.contrastRatio).toBeGreaterThanOrEqual(7);
|
||||
});
|
||||
|
||||
test('should have alt text for all images', async ({ page }) => {
|
||||
const url = await getPageUrl(page);
|
||||
console.log(`Running image alt text tests against ${url} on ${viewport.name}`);
|
||||
await page.goto(url, { timeout: 60000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Get all images
|
||||
const images = await page.$$('img');
|
||||
|
||||
// Skip test if no images found
|
||||
if (images.length === 0) {
|
||||
console.log('No images found, skipping test');
|
||||
return;
|
||||
}
|
||||
|
||||
const missingAlt = [];
|
||||
for (const img of images) {
|
||||
const alt = await img.getAttribute('alt');
|
||||
const src = await img.getAttribute('src');
|
||||
|
||||
// Skip decorative images (empty alt is fine)
|
||||
const role = await img.getAttribute('role');
|
||||
if (role === 'presentation' || role === 'none') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!alt) {
|
||||
missingAlt.push(src);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingAlt.length > 0) {
|
||||
console.log('\nImages missing alt text:');
|
||||
missingAlt.forEach(src => console.log(`- ${src}`));
|
||||
throw new Error('Some images are missing alt text');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
# Accessibility Testing Framework
|
||||
|
||||
This directory contains tools and scripts for testing the website's accessibility compliance with WCAG 2.1 AAA standards.
|
||||
|
||||
## Overview
|
||||
|
||||
The accessibility testing framework uses multiple approaches:
|
||||
|
||||
1. **Automated Testing with Pa11y**: Command-line accessibility testing using Pa11y
|
||||
2. **Automated Testing with axe-core**: JavaScript-based accessibility testing using axe-core
|
||||
3. **Manual Testing Checklist**: A comprehensive checklist for aspects that can't be automated
|
||||
|
||||
## Running Tests
|
||||
|
||||
To run all accessibility tests:
|
||||
|
||||
```bash
|
||||
npm run test:accessibility
|
||||
```
|
||||
|
||||
Or run directly:
|
||||
|
||||
```bash
|
||||
./tests/accessibility/run-accessibility-tests.sh [BASE_URL]
|
||||
```
|
||||
|
||||
The default BASE_URL is `http://localhost:8080` if not specified.
|
||||
|
||||
## Test Components
|
||||
|
||||
### Pa11y Tests
|
||||
|
||||
Pa11y tests are run using the `pa11y-test.sh` script, which:
|
||||
|
||||
- Tests multiple pages against WCAG 2.1 AAA standards
|
||||
- Generates JSON reports for each page
|
||||
- Creates a summary report
|
||||
|
||||
### axe-core Tests
|
||||
|
||||
axe-core tests are run using the `playwright-axe.js` script, which:
|
||||
|
||||
- Uses Playwright to load pages
|
||||
- Injects axe-core into each page
|
||||
- Runs accessibility tests against WCAG 2.1 AAA standards
|
||||
- Generates detailed reports
|
||||
|
||||
### Manual Testing
|
||||
|
||||
Some aspects of accessibility can't be automatically tested. The `manual-checklist.md` file provides a comprehensive checklist for manual testing, focusing on AAA-level criteria.
|
||||
|
||||
## Reports
|
||||
|
||||
Test reports are saved in the `tests/reports/` directory:
|
||||
|
||||
- Individual page reports (e.g., `pa11y-index.json`, `axe-index.json`)
|
||||
- Summary reports (`pa11y-summary.json`, `axe-summary.json`)
|
||||
- Combined accessibility report (`accessibility-summary.json`)
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js
|
||||
- NPM packages: pa11y, axe-core, playwright
|
||||
- For Pa11y: `npm install -g pa11y` or use npx
|
|
@ -0,0 +1,167 @@
|
|||
/**
|
||||
* WCAG 2.1 AAA compliance test using axe-core
|
||||
*
|
||||
* This test runs axe-core against all pages of the website to check for WCAG 2.1 AAA compliance.
|
||||
* It tests for issues related to:
|
||||
* - Color contrast (7:1 ratio for AAA)
|
||||
* - Text spacing
|
||||
* - Heading structure
|
||||
* - ARIA attributes
|
||||
* - And many other WCAG 2.1 AAA criteria
|
||||
*/
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
const axeCore = require('axe-core');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// URLs to test
|
||||
const BASE_URL = process.argv[2] || 'http://localhost:8080'\;
|
||||
const PAGES = [
|
||||
'/',
|
||||
'/stories/',
|
||||
'/stories/open-source-success.html',
|
||||
'/stories/viperwire.html',
|
||||
'/one-pager-tools/csv-tool.html'
|
||||
];
|
||||
|
||||
// Create reports directory if it doesn't exist
|
||||
const reportsDir = path.join(__dirname, '../reports');
|
||||
if (!fs.existsSync(reportsDir)) {
|
||||
fs.mkdirSync(reportsDir, { recursive: true });
|
||||
}
|
||||
|
||||
async function runAxe(page, pagePath) {
|
||||
// Inject axe-core into the page
|
||||
await page.evaluate(() => {
|
||||
if (!window.axe) {
|
||||
// This would normally be done by injecting the axe-core script
|
||||
// but for this example, we'll assume axe-core is already available
|
||||
console.log('Warning: axe-core not available in the page');
|
||||
}
|
||||
});
|
||||
|
||||
// Run axe with WCAG 2.1 AAA rules
|
||||
const results = await page.evaluate(() => {
|
||||
return new Promise(resolve => {
|
||||
if (!window.axe) {
|
||||
resolve({ error: 'axe-core not available' });
|
||||
return;
|
||||
}
|
||||
|
||||
axe.run(document, {
|
||||
runOnly: {
|
||||
type: 'tag',
|
||||
values: ['wcag2aaa']
|
||||
},
|
||||
resultTypes: ['violations', 'incomplete', 'inapplicable'],
|
||||
rules: {
|
||||
'color-contrast': { enabled: true, options: { noScroll: true } }
|
||||
}
|
||||
})
|
||||
.then(results => resolve(results))
|
||||
.catch(err => resolve({ error: err.toString() }));
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function testPage(browser, pageUrl) {
|
||||
const page = await browser.newPage();
|
||||
console.log(`Testing ${pageUrl}...`);
|
||||
|
||||
try {
|
||||
// Navigate to the page
|
||||
await page.goto(pageUrl, { waitUntil: 'networkidle' });
|
||||
|
||||
// Run axe-core tests
|
||||
const results = await runAxe(page, pageUrl);
|
||||
|
||||
// Save results to file
|
||||
const fileName = pageUrl === BASE_URL ? 'index' : pageUrl.replace(BASE_URL, '').replace(/\//g, '-').replace(/^-/, '');
|
||||
const reportPath = path.join(reportsDir, `axe-${fileName || 'index'}.json`);
|
||||
fs.writeFileSync(reportPath, JSON.stringify(results, null, 2));
|
||||
|
||||
// Log results summary
|
||||
if (results.error) {
|
||||
console.error(`Error running axe-core on ${pageUrl}:`, results.error);
|
||||
return { url: pageUrl, success: false, error: results.error };
|
||||
}
|
||||
|
||||
const { violations, incomplete, passes, inapplicable } = results;
|
||||
console.log(`Results for ${pageUrl}:`);
|
||||
console.log(`- Violations: ${violations.length}`);
|
||||
console.log(`- Incomplete: ${incomplete.length}`);
|
||||
console.log(`- Passes: ${passes.length}`);
|
||||
console.log(`- Inapplicable: ${inapplicable.length}`);
|
||||
|
||||
// Print violations
|
||||
if (violations.length > 0) {
|
||||
console.log('\nViolations:');
|
||||
violations.forEach((violation, i) => {
|
||||
console.log(`${i + 1}. ${violation.id} - ${violation.help} (Impact: ${violation.impact})`);
|
||||
console.log(` ${violation.description}`);
|
||||
console.log(` WCAG: ${violation.tags.filter(t => t.startsWith('wcag')).join(', ')}`);
|
||||
console.log(` Elements: ${violation.nodes.length}`);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
url: pageUrl,
|
||||
success: violations.length === 0,
|
||||
violations: violations.length,
|
||||
incomplete: incomplete.length,
|
||||
passes: passes.length
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error testing ${pageUrl}:`, error);
|
||||
return { url: pageUrl, success: false, error: error.toString() };
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
const browser = await chromium.launch();
|
||||
const results = [];
|
||||
|
||||
try {
|
||||
// Test each page
|
||||
for (const pagePath of PAGES) {
|
||||
const pageUrl = `${BASE_URL}${pagePath}`;
|
||||
const result = await testPage(browser, pageUrl);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
// Save overall results
|
||||
const overallReport = {
|
||||
timestamp: new Date().toISOString(),
|
||||
baseUrl: BASE_URL,
|
||||
pages: results,
|
||||
summary: {
|
||||
total: results.length,
|
||||
passed: results.filter(r => r.success).length,
|
||||
failed: results.filter(r => !r.success).length
|
||||
}
|
||||
};
|
||||
|
||||
fs.writeFileSync(path.join(reportsDir, 'axe-summary.json'), JSON.stringify(overallReport, null, 2));
|
||||
|
||||
// Print overall summary
|
||||
console.log('\n=== Overall Summary ===');
|
||||
console.log(`Total pages tested: ${overallReport.summary.total}`);
|
||||
console.log(`Pages passed: ${overallReport.summary.passed}`);
|
||||
console.log(`Pages failed: ${overallReport.summary.failed}`);
|
||||
|
||||
// Exit with appropriate code
|
||||
process.exit(overallReport.summary.failed > 0 ? 1 : 0);
|
||||
} catch (error) {
|
||||
console.error('Error running tests:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
runTests();
|
|
@ -0,0 +1,537 @@
|
|||
/**
|
||||
* Generate a detailed accessibility report for WCAG 2.1 AAA compliance
|
||||
*
|
||||
* This script analyzes all pages and generates a comprehensive report
|
||||
* of accessibility issues that need to be fixed for AAA compliance
|
||||
*/
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const axeCore = require('axe-core');
|
||||
|
||||
// URLs to test - expand this list to cover all pages
|
||||
const BASE_URL = process.argv[2] || 'http://localhost:8080'\;
|
||||
const PAGES = [
|
||||
'/',
|
||||
'/stories/',
|
||||
'/stories/open-source-success.html',
|
||||
'/stories/viperwire.html',
|
||||
'/stories/airport-dns.html',
|
||||
'/stories/wordpress-security.html',
|
||||
'/stories/nitric-leadership.html',
|
||||
'/stories/healthcare-platform.html',
|
||||
'/stories/web-design-java.html',
|
||||
'/stories/motherboard-repair.html',
|
||||
'/stories/fawe-plotsquared.html',
|
||||
'/stories/showerloop.html',
|
||||
'/stories/athion-turnaround.html',
|
||||
'/stories/youtube-game-dev.html',
|
||||
'/stories/app-development.html',
|
||||
'/one-pager-tools/csv-tool.html'
|
||||
];
|
||||
|
||||
// Create reports directory if it doesn't exist
|
||||
const reportsDir = path.join(__dirname, '../reports');
|
||||
if (!fs.existsSync(reportsDir)) {
|
||||
fs.mkdirSync(reportsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Issues storage
|
||||
const allIssues = {
|
||||
byPage: {},
|
||||
byRule: {},
|
||||
summary: {
|
||||
totalPages: 0,
|
||||
pagesWithIssues: 0,
|
||||
totalIssues: 0,
|
||||
issuesByImpact: {
|
||||
critical: 0,
|
||||
serious: 0,
|
||||
moderate: 0,
|
||||
minor: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function runAxe(page, pageUrl) {
|
||||
try {
|
||||
// Use a file URL to load axe-core from a local file to avoid CSP issues
|
||||
const axeScriptPath = path.join(__dirname, 'axe-core.js');
|
||||
fs.writeFileSync(axeScriptPath, axeCore.source);
|
||||
|
||||
// Add the script as a file
|
||||
await page.addScriptTag({ path: axeScriptPath });
|
||||
|
||||
// Run axe with WCAG 2.1 AAA rules
|
||||
const results = await page.evaluate(() => {
|
||||
return new Promise(resolve => {
|
||||
axe.run(document, {
|
||||
runOnly: {
|
||||
type: 'tag',
|
||||
values: ['wcag2aaa']
|
||||
},
|
||||
resultTypes: ['violations', 'incomplete'],
|
||||
rules: {
|
||||
'color-contrast': { enabled: true, options: { noScroll: true } }
|
||||
}
|
||||
})
|
||||
.then(results => resolve(results))
|
||||
.catch(err => resolve({ error: err.toString() }));
|
||||
});
|
||||
});
|
||||
|
||||
// Clean up temporary file
|
||||
fs.unlinkSync(axeScriptPath);
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Error running axe:', error);
|
||||
return { error: error.toString() };
|
||||
}
|
||||
}
|
||||
|
||||
async function testPage(browser, pageUrl) {
|
||||
const context = await browser.newContext({
|
||||
bypassCSP: true
|
||||
});
|
||||
const page = await context.newPage();
|
||||
console.log(`Testing ${pageUrl}...`);
|
||||
|
||||
try {
|
||||
// Navigate to the page
|
||||
await page.goto(pageUrl, { waitUntil: 'networkidle' });
|
||||
|
||||
// Run axe-core tests
|
||||
const results = await runAxe(page, pageUrl);
|
||||
|
||||
// Save results to file
|
||||
const fileName = pageUrl === BASE_URL ? 'index' : pageUrl.replace(BASE_URL, '').replace(/\//g, '-').replace(/^-/, '');
|
||||
const reportPath = path.join(reportsDir, `axe-${fileName || 'index'}.json`);
|
||||
fs.writeFileSync(reportPath, JSON.stringify(results, null, 2));
|
||||
|
||||
// Log results summary
|
||||
if (results.error) {
|
||||
console.error(`Error running axe-core on ${pageUrl}:`, results.error);
|
||||
return { url: pageUrl, success: false, error: results.error };
|
||||
}
|
||||
|
||||
const { violations, incomplete } = results;
|
||||
console.log(`Results for ${pageUrl}:`);
|
||||
console.log(`- Violations: ${violations.length}`);
|
||||
console.log(`- Incomplete: ${incomplete.length}`);
|
||||
|
||||
// Track issues for detailed report
|
||||
const pageIssues = [];
|
||||
|
||||
// Process violations
|
||||
if (violations.length > 0) {
|
||||
console.log('\nViolations:');
|
||||
violations.forEach((violation, i) => {
|
||||
console.log(`${i + 1}. ${violation.id} - ${violation.help} (Impact: ${violation.impact})`);
|
||||
console.log(` ${violation.description}`);
|
||||
console.log(` WCAG: ${violation.tags.filter(t => t.startsWith('wcag')).join(', ')}`);
|
||||
console.log(` Elements: ${violation.nodes.length}`);
|
||||
|
||||
// Add to issues tracking
|
||||
violation.nodes.forEach(node => {
|
||||
const issue = {
|
||||
rule: violation.id,
|
||||
impact: violation.impact,
|
||||
description: violation.description,
|
||||
help: violation.help,
|
||||
helpUrl: violation.helpUrl,
|
||||
wcagTags: violation.tags.filter(t => t.startsWith('wcag')),
|
||||
element: node.html,
|
||||
target: node.target,
|
||||
failureSummary: node.failureSummary,
|
||||
elementData: node.any.map(d => d.data || {})
|
||||
};
|
||||
pageIssues.push(issue);
|
||||
|
||||
// Update rule-based tracking
|
||||
if (!allIssues.byRule[violation.id]) {
|
||||
allIssues.byRule[violation.id] = {
|
||||
id: violation.id,
|
||||
description: violation.description,
|
||||
help: violation.help,
|
||||
helpUrl: violation.helpUrl,
|
||||
impact: violation.impact,
|
||||
wcagTags: violation.tags.filter(t => t.startsWith('wcag')),
|
||||
occurrences: []
|
||||
};
|
||||
}
|
||||
|
||||
allIssues.byRule[violation.id].occurrences.push({
|
||||
page: pageUrl,
|
||||
element: node.html,
|
||||
target: node.target,
|
||||
failureSummary: node.failureSummary,
|
||||
elementData: node.any.map(d => d.data || {})
|
||||
});
|
||||
|
||||
// Update summary stats
|
||||
allIssues.summary.totalIssues++;
|
||||
if (violation.impact) {
|
||||
allIssues.summary.issuesByImpact[violation.impact]++;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Store page issues
|
||||
allIssues.byPage[pageUrl] = {
|
||||
url: pageUrl,
|
||||
issueCount: pageIssues.length,
|
||||
issues: pageIssues
|
||||
};
|
||||
|
||||
// Update summary
|
||||
allIssues.summary.totalPages++;
|
||||
if (pageIssues.length > 0) {
|
||||
allIssues.summary.pagesWithIssues++;
|
||||
}
|
||||
|
||||
return {
|
||||
url: pageUrl,
|
||||
success: violations.length === 0,
|
||||
violations: violations.length,
|
||||
incomplete: incomplete.length
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error testing ${pageUrl}:`, error);
|
||||
return { url: pageUrl, success: false, error: error.toString() };
|
||||
} finally {
|
||||
await page.close();
|
||||
await context.close();
|
||||
}
|
||||
}
|
||||
|
||||
function generateFixRecommendations() {
|
||||
const recommendations = {};
|
||||
|
||||
// Process color contrast issues
|
||||
if (allIssues.byRule['color-contrast-enhanced']) {
|
||||
const colorIssues = allIssues.byRule['color-contrast-enhanced'];
|
||||
recommendations.colorContrast = {
|
||||
title: 'Color Contrast Issues',
|
||||
description: 'The following color combinations need to be updated to meet WCAG 2.1 AAA requirements (7:1 contrast ratio):',
|
||||
colorPairs: []
|
||||
};
|
||||
|
||||
// Group by color pairs
|
||||
const colorPairMap = {};
|
||||
colorIssues.occurrences.forEach(occurrence => {
|
||||
occurrence.elementData.forEach(data => {
|
||||
if (data.fgColor && data.bgColor) {
|
||||
const key = `${data.fgColor}/${data.bgColor}`;
|
||||
if (!colorPairMap[key]) {
|
||||
colorPairMap[key] = {
|
||||
fgColor: data.fgColor,
|
||||
bgColor: data.bgColor,
|
||||
contrastRatio: data.contrastRatio,
|
||||
requiredRatio: data.expectedContrastRatio,
|
||||
pages: new Set(),
|
||||
elements: []
|
||||
};
|
||||
}
|
||||
colorPairMap[key].pages.add(occurrence.page);
|
||||
colorPairMap[key].elements.push({
|
||||
html: occurrence.element,
|
||||
page: occurrence.page
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Convert to array and add suggestions
|
||||
Object.values(colorPairMap).forEach(pair => {
|
||||
const suggestion = {
|
||||
foreground: pair.fgColor,
|
||||
background: pair.bgColor,
|
||||
currentRatio: pair.contrastRatio,
|
||||
requiredRatio: pair.requiredRatio,
|
||||
pagesAffected: Array.from(pair.pages),
|
||||
elementCount: pair.elements.length,
|
||||
suggestedFixes: []
|
||||
};
|
||||
|
||||
// Generate suggested fixes
|
||||
// Make foreground darker
|
||||
const darkerFg = darkenColor(pair.fgColor, 0.2);
|
||||
suggestion.suggestedFixes.push({
|
||||
type: 'Darken foreground',
|
||||
color: darkerFg,
|
||||
description: `Change foreground color from ${pair.fgColor} to ${darkerFg}`
|
||||
});
|
||||
|
||||
// Make background lighter
|
||||
const lighterBg = lightenColor(pair.bgColor, 0.2);
|
||||
suggestion.suggestedFixes.push({
|
||||
type: 'Lighten background',
|
||||
color: lighterBg,
|
||||
description: `Change background color from ${pair.bgColor} to ${lighterBg}`
|
||||
});
|
||||
|
||||
recommendations.colorContrast.colorPairs.push(suggestion);
|
||||
});
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
// Helper function to darken a hex color
|
||||
function darkenColor(hex, amount) {
|
||||
return adjustColor(hex, -amount);
|
||||
}
|
||||
|
||||
// Helper function to lighten a hex color
|
||||
function lightenColor(hex, amount) {
|
||||
return adjustColor(hex, amount);
|
||||
}
|
||||
|
||||
// Helper function to adjust a hex color
|
||||
function adjustColor(hex, amount) {
|
||||
let r = parseInt(hex.substring(1, 3), 16);
|
||||
let g = parseInt(hex.substring(3, 5), 16);
|
||||
let b = parseInt(hex.substring(5, 7), 16);
|
||||
|
||||
if (amount > 0) {
|
||||
// Lighten
|
||||
r = Math.min(255, Math.round(r + (255 - r) * amount));
|
||||
g = Math.min(255, Math.round(g + (255 - g) * amount));
|
||||
b = Math.min(255, Math.round(b + (255 - b) * amount));
|
||||
} else {
|
||||
// Darken
|
||||
amount = -amount;
|
||||
r = Math.max(0, Math.round(r * (1 - amount)));
|
||||
g = Math.max(0, Math.round(g * (1 - amount)));
|
||||
b = Math.max(0, Math.round(b * (1 - amount)));
|
||||
}
|
||||
|
||||
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
const browser = await chromium.launch();
|
||||
const results = [];
|
||||
|
||||
try {
|
||||
// Test each page
|
||||
for (const pagePath of PAGES) {
|
||||
const pageUrl = `${BASE_URL}${pagePath}`;
|
||||
const result = await testPage(browser, pageUrl);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
// Generate detailed report
|
||||
const recommendations = generateFixRecommendations();
|
||||
|
||||
const detailedReport = {
|
||||
timestamp: new Date().toISOString(),
|
||||
baseUrl: BASE_URL,
|
||||
summary: allIssues.summary,
|
||||
recommendations: recommendations,
|
||||
issuesByRule: allIssues.byRule
|
||||
};
|
||||
|
||||
// Save detailed report
|
||||
fs.writeFileSync(
|
||||
path.join(reportsDir, 'accessibility-detailed-report.json'),
|
||||
JSON.stringify(detailedReport, null, 2)
|
||||
);
|
||||
|
||||
// Generate HTML report
|
||||
const htmlReport = generateHtmlReport(detailedReport);
|
||||
fs.writeFileSync(
|
||||
path.join(reportsDir, 'accessibility-report.html'),
|
||||
htmlReport
|
||||
);
|
||||
|
||||
console.log('\n=== Detailed Report Generated ===');
|
||||
console.log(`Total pages tested: ${detailedReport.summary.totalPages}`);
|
||||
console.log(`Pages with issues: ${detailedReport.summary.pagesWithIssues}`);
|
||||
console.log(`Total issues found: ${detailedReport.summary.totalIssues}`);
|
||||
console.log(`Issues by impact:`);
|
||||
Object.entries(detailedReport.summary.issuesByImpact).forEach(([impact, count]) => {
|
||||
if (count > 0) {
|
||||
console.log(` - ${impact}: ${count}`);
|
||||
}
|
||||
});
|
||||
console.log(`\nDetailed report saved to: ${path.join(reportsDir, 'accessibility-detailed-report.json')}`);
|
||||
console.log(`HTML report saved to: ${path.join(reportsDir, 'accessibility-report.html')}`);
|
||||
|
||||
// Exit with appropriate code
|
||||
process.exit(detailedReport.summary.totalIssues > 0 ? 1 : 0);
|
||||
} catch (error) {
|
||||
console.error('Error running tests:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
function generateHtmlReport(report) {
|
||||
const colorPairs = report.recommendations.colorContrast ? report.recommendations.colorContrast.colorPairs : [];
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Accessibility Report - WCAG 2.1 AAA Compliance</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
h1, h2, h3, h4 {
|
||||
color: #2c3e50;
|
||||
}
|
||||
.summary {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.issue-count {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.color-pair {
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #ddd;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.color-sample {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
margin-right: 10px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
.color-info {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
.suggestion {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background-color: #f0f7ff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.pages-list {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.impact-critical { color: #d9534f; }
|
||||
.impact-serious { color: #f0ad4e; }
|
||||
.impact-moderate { color: #5bc0de; }
|
||||
.impact-minor { color: #5cb85c; }
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Accessibility Report - WCAG 2.1 AAA Compliance</h1>
|
||||
<p>Generated on: ${new Date(report.timestamp).toLocaleString()}</p>
|
||||
|
||||
<div class="summary">
|
||||
<h2>Summary</h2>
|
||||
<p><span class="issue-count">${report.summary.totalIssues}</span> issues found across ${report.summary.pagesWithIssues} pages (${report.summary.totalPages} pages tested)</p>
|
||||
<p>Issues by impact:</p>
|
||||
<ul>
|
||||
${report.summary.issuesByImpact.critical > 0 ? `<li><span class="impact-critical">Critical: ${report.summary.issuesByImpact.critical}</span></li>` : ''}
|
||||
${report.summary.issuesByImpact.serious > 0 ? `<li><span class="impact-serious">Serious: ${report.summary.issuesByImpact.serious}</span></li>` : ''}
|
||||
${report.summary.issuesByImpact.moderate > 0 ? `<li><span class="impact-moderate">Moderate: ${report.summary.issuesByImpact.moderate}</span></li>` : ''}
|
||||
${report.summary.issuesByImpact.minor > 0 ? `<li><span class="impact-minor">Minor: ${report.summary.issuesByImpact.minor}</span></li>` : ''}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Color Contrast Issues</h2>
|
||||
${colorPairs.length === 0 ? '<p>No color contrast issues found.</p>' : ''}
|
||||
${colorPairs.map(pair => `
|
||||
<div class="color-pair">
|
||||
<h3>Color Combination</h3>
|
||||
<div>
|
||||
<div class="color-sample" style="background-color: ${pair.foreground}"></div>
|
||||
<div class="color-info">
|
||||
<p>Foreground: ${pair.foreground}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="color-sample" style="background-color: ${pair.background}"></div>
|
||||
<div class="color-info">
|
||||
<p>Background: ${pair.background}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>Current contrast ratio: ${pair.currentRatio} (Required: ${pair.requiredRatio})</p>
|
||||
<p>Found on ${pair.elementCount} element${pair.elementCount !== 1 ? 's' : ''} across ${pair.pagesAffected.length} page${pair.pagesAffected.length !== 1 ? 's' : ''}</p>
|
||||
|
||||
<div class="pages-list">
|
||||
<p>Pages affected:</p>
|
||||
<ul>
|
||||
${pair.pagesAffected.map(page => `<li>${page}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h4>Suggested Fixes</h4>
|
||||
${pair.suggestedFixes.map(fix => `
|
||||
<div class="suggestion">
|
||||
<h5>${fix.type}</h5>
|
||||
<div class="color-sample" style="background-color: ${fix.color}"></div>
|
||||
<div class="color-info">
|
||||
<p>${fix.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`).join('')}
|
||||
|
||||
<h2>Issues by Rule</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rule</th>
|
||||
<th>Impact</th>
|
||||
<th>Description</th>
|
||||
<th>Occurrences</th>
|
||||
<th>WCAG Criteria</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${Object.values(report.issuesByRule).map(rule => `
|
||||
<tr>
|
||||
<td><a href="${rule.helpUrl}" target="_blank">${rule.id}</a></td>
|
||||
<td class="impact-${rule.impact}">${rule.impact}</td>
|
||||
<td>${rule.description}</td>
|
||||
<td>${rule.occurrences.length}</td>
|
||||
<td>${rule.wcagTags.join(', ')}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
runTests();
|
|
@ -0,0 +1,212 @@
|
|||
/**
|
||||
* Generate a comprehensive accessibility report
|
||||
* This script analyzes the axe-core and Pa11y test results and generates a report
|
||||
* of all accessibility issues that need to be fixed
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Directory containing test reports
|
||||
const reportsDir = path.join(__dirname, '../reports');
|
||||
|
||||
// Output report file
|
||||
const outputFile = path.join(reportsDir, 'accessibility-issues.md');
|
||||
|
||||
// Get all axe-core report files
|
||||
const axeReports = fs.readdirSync(reportsDir)
|
||||
.filter(file => file.startsWith('axe-') && file.endsWith('.json'))
|
||||
.map(file => {
|
||||
try {
|
||||
const content = fs.readFileSync(path.join(reportsDir, file), 'utf8');
|
||||
return { file, content: JSON.parse(content) };
|
||||
} catch (error) {
|
||||
console.error(`Error reading ${file}:`, error.message);
|
||||
return { file, content: null };
|
||||
}
|
||||
})
|
||||
.filter(report => report.content !== null);
|
||||
|
||||
// Get all Pa11y report files
|
||||
const pa11yReports = fs.readdirSync(reportsDir)
|
||||
.filter(file => file.startsWith('pa11y') && file.endsWith('.json') && !file.includes('summary'))
|
||||
.map(file => {
|
||||
try {
|
||||
const content = fs.readFileSync(path.join(reportsDir, file), 'utf8');
|
||||
return { file, content: JSON.parse(content) };
|
||||
} catch (error) {
|
||||
console.error(`Error reading ${file}:`, error.message);
|
||||
return { file, content: null };
|
||||
}
|
||||
})
|
||||
.filter(report => report.content !== null);
|
||||
|
||||
// Generate report
|
||||
let report = `# Accessibility Issues Report
|
||||
Generated on: ${new Date().toISOString()}
|
||||
|
||||
This report lists all accessibility issues found by automated testing tools.
|
||||
Fixing these issues will help achieve WCAG 2.1 AAA compliance.
|
||||
|
||||
## Summary
|
||||
|
||||
`;
|
||||
|
||||
// Process axe-core reports
|
||||
const axeIssues = [];
|
||||
axeReports.forEach(report => {
|
||||
if (report.content && report.content.violations) {
|
||||
report.content.violations.forEach(violation => {
|
||||
violation.nodes.forEach(node => {
|
||||
axeIssues.push({
|
||||
page: report.file.replace('axe-', '').replace('.json', ''),
|
||||
id: violation.id,
|
||||
impact: violation.impact,
|
||||
description: violation.description,
|
||||
help: violation.help,
|
||||
helpUrl: violation.helpUrl,
|
||||
element: node.html || node.target?.join(', ') || 'Unknown element',
|
||||
wcag: violation.tags.filter(t => t.startsWith('wcag')).join(', ')
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Process Pa11y reports
|
||||
const pa11yIssues = [];
|
||||
pa11yReports.forEach(report => {
|
||||
if (report.content && report.content.issues) {
|
||||
report.content.issues.forEach(issue => {
|
||||
pa11yIssues.push({
|
||||
page: report.file.replace('pa11y', '').replace('.json', ''),
|
||||
code: issue.code,
|
||||
type: issue.type,
|
||||
message: issue.message,
|
||||
context: issue.context,
|
||||
selector: issue.selector
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add summary counts to report
|
||||
report += `- Total pages tested: ${axeReports.length}
|
||||
- Pages with axe-core issues: ${axeReports.filter(r => r.content?.violations?.length > 0).length}
|
||||
- Total axe-core issues: ${axeIssues.length}
|
||||
- Pages with Pa11y issues: ${pa11yReports.filter(r => r.content?.issues?.length > 0).length}
|
||||
- Total Pa11y issues: ${pa11yIssues.length}
|
||||
|
||||
## Common Issues
|
||||
|
||||
`;
|
||||
|
||||
// Group issues by type
|
||||
const issuesByType = {};
|
||||
axeIssues.forEach(issue => {
|
||||
if (!issuesByType[issue.id]) {
|
||||
issuesByType[issue.id] = {
|
||||
count: 0,
|
||||
description: issue.description,
|
||||
help: issue.help,
|
||||
helpUrl: issue.helpUrl,
|
||||
wcag: issue.wcag,
|
||||
pages: new Set()
|
||||
};
|
||||
}
|
||||
issuesByType[issue.id].count++;
|
||||
issuesByType[issue.id].pages.add(issue.page);
|
||||
});
|
||||
|
||||
// Sort issues by count
|
||||
const sortedIssues = Object.entries(issuesByType)
|
||||
.sort((a, b) => b[1].count - a[1].count);
|
||||
|
||||
// Add common issues to report
|
||||
sortedIssues.forEach(([id, info]) => {
|
||||
report += `### ${id} (${info.count} occurrences)
|
||||
|
||||
- **Description**: ${info.description}
|
||||
- **Help**: ${info.help}
|
||||
- **WCAG**: ${info.wcag}
|
||||
- **Pages affected**: ${Array.from(info.pages).join(', ')}
|
||||
- **More info**: ${info.helpUrl}
|
||||
|
||||
`;
|
||||
});
|
||||
|
||||
// Add detailed issues by page
|
||||
report += `## Issues by Page
|
||||
|
||||
`;
|
||||
|
||||
// Group issues by page
|
||||
const issuesByPage = {};
|
||||
axeIssues.forEach(issue => {
|
||||
if (!issuesByPage[issue.page]) {
|
||||
issuesByPage[issue.page] = [];
|
||||
}
|
||||
issuesByPage[issue.page].push(issue);
|
||||
});
|
||||
|
||||
// Add page-specific issues to report
|
||||
Object.entries(issuesByPage).forEach(([page, issues]) => {
|
||||
report += `### ${page}
|
||||
|
||||
`;
|
||||
issues.forEach(issue => {
|
||||
report += `- **${issue.id}** (${issue.impact}): ${issue.help}
|
||||
- Element: \`${issue.element.substring(0, 100)}${issue.element.length > 100 ? '...' : ''}\`
|
||||
- WCAG: ${issue.wcag}
|
||||
|
||||
`;
|
||||
});
|
||||
});
|
||||
|
||||
// Add recommended fixes
|
||||
report += `## Recommended Fixes
|
||||
|
||||
Based on the issues found, here are the recommended fixes:
|
||||
|
||||
`;
|
||||
|
||||
// Add specific recommendations for common issues
|
||||
if (issuesByType['color-contrast-enhanced']) {
|
||||
report += `### Color Contrast Issues
|
||||
|
||||
1. Update link colors to meet 7:1 contrast ratio:
|
||||
- Current blue link color (#0056b3) on light background (#f5f5f5) has 6.45:1 ratio
|
||||
- Recommended: Change to darker blue (#004494) for 7:1+ ratio
|
||||
|
||||
2. Update CSS in the following files:
|
||||
- \`docker/resume/styles.css\`: Update link colors
|
||||
- \`docker/resume/stories/stories.css\`: Update story-specific link colors
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
// Add implementation plan
|
||||
report += `## Implementation Plan
|
||||
|
||||
1. Fix color contrast issues first (highest impact)
|
||||
2. Address any document structure issues
|
||||
3. Fix remaining issues by priority (serious > moderate > minor)
|
||||
4. Re-run tests to verify fixes
|
||||
|
||||
## Testing Instructions
|
||||
|
||||
To test accessibility compliance:
|
||||
|
||||
\`\`\`bash
|
||||
# Run all accessibility tests
|
||||
npm run test:accessibility
|
||||
|
||||
# Generate updated report
|
||||
node tests/accessibility/generate-report.js
|
||||
\`\`\`
|
||||
|
||||
`;
|
||||
|
||||
// Write report to file
|
||||
fs.writeFileSync(outputFile, report);
|
||||
console.log(`Accessibility issues report generated at: ${outputFile}`);
|
|
@ -0,0 +1,119 @@
|
|||
# Manual Accessibility Testing Checklist
|
||||
|
||||
This checklist covers aspects of WCAG 2.1 AAA compliance that cannot be fully automated and require manual verification.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. For each page in the site, go through this checklist
|
||||
2. Mark each item as:
|
||||
- ✅ Pass
|
||||
- ❌ Fail
|
||||
- N/A Not applicable
|
||||
3. For any failures, note the specific issue and location
|
||||
|
||||
## Pages to Test
|
||||
|
||||
- Home page (/)
|
||||
- Stories index (/stories/)
|
||||
- Individual story pages (e.g., /stories/open-source-success.html)
|
||||
- Tools pages (e.g., /one-pager-tools/csv-tool.html)
|
||||
|
||||
## Checklist
|
||||
|
||||
### Perceivable
|
||||
|
||||
#### 1.2.6 Sign Language (AAA)
|
||||
- [ ] Sign language interpretation is provided for all prerecorded audio content in synchronized media
|
||||
|
||||
#### 1.2.7 Extended Audio Description (AAA)
|
||||
- [ ] Extended audio description is provided for all prerecorded video content
|
||||
|
||||
#### 1.2.8 Media Alternative (AAA)
|
||||
- [ ] Alternative for time-based media is provided for all prerecorded synchronized media and for all prerecorded video-only media
|
||||
|
||||
#### 1.2.9 Audio-only (Live) (AAA)
|
||||
- [ ] Alternative for time-based media that presents equivalent information for live audio-only content is provided
|
||||
|
||||
#### 1.3.6 Identify Purpose (AAA)
|
||||
- [ ] The purpose of UI components, icons, and regions can be programmatically determined
|
||||
|
||||
#### 1.4.6 Contrast (Enhanced) (AAA)
|
||||
- [ ] Visual presentation of text and images of text has a contrast ratio of at least 7:1
|
||||
- [ ] Large text (over 18pt or 14pt bold) has a contrast ratio of at least 4.5:1
|
||||
|
||||
#### 1.4.9 Images of Text (No Exception) (AAA)
|
||||
- [ ] Images of text are only used for pure decoration or where a particular presentation of text is essential to the information being conveyed
|
||||
|
||||
### Operable
|
||||
|
||||
#### 2.1.3 Keyboard (No Exception) (AAA)
|
||||
- [ ] All functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes
|
||||
|
||||
#### 2.2.3 No Timing (AAA)
|
||||
- [ ] Timing is not an essential part of the event or activity presented by the content
|
||||
|
||||
#### 2.2.4 Interruptions (AAA)
|
||||
- [ ] Interruptions can be postponed or suppressed by the user
|
||||
|
||||
#### 2.2.5 Re-authenticating (AAA)
|
||||
- [ ] When an authenticated session expires, the user can continue the activity without loss of data after re-authenticating
|
||||
|
||||
#### 2.2.6 Timeouts (AAA)
|
||||
- [ ] Users are warned of the duration of any user inactivity that could cause data loss
|
||||
|
||||
#### 2.3.2 Three Flashes (AAA)
|
||||
- [ ] Web pages do not contain anything that flashes more than three times in any one second period
|
||||
|
||||
#### 2.3.3 Animation from Interactions (AAA)
|
||||
- [ ] Motion animation triggered by interaction can be disabled
|
||||
|
||||
#### 2.4.8 Location (AAA)
|
||||
- [ ] Information about the user's location within a set of web pages is available
|
||||
|
||||
#### 2.4.9 Link Purpose (Link Only) (AAA)
|
||||
- [ ] A mechanism is available to allow the purpose of each link to be identified from link text alone
|
||||
|
||||
#### 2.4.10 Section Headings (AAA)
|
||||
- [ ] Section headings are used to organize the content
|
||||
|
||||
#### 2.5.5 Target Size (AAA)
|
||||
- [ ] The size of the target for pointer inputs is at least 44 by 44 CSS pixels
|
||||
|
||||
#### 2.5.6 Concurrent Input Mechanisms (AAA)
|
||||
- [ ] Web content does not restrict use of input modalities available on a platform
|
||||
|
||||
### Understandable
|
||||
|
||||
#### 3.1.3 Unusual Words (AAA)
|
||||
- [ ] A mechanism is available for identifying specific definitions of words or phrases used in an unusual or restricted way
|
||||
|
||||
#### 3.1.4 Abbreviations (AAA)
|
||||
- [ ] A mechanism for identifying the expanded form or meaning of abbreviations is available
|
||||
|
||||
#### 3.1.5 Reading Level (AAA)
|
||||
- [ ] When text requires reading ability more advanced than the lower secondary education level, supplemental content is available
|
||||
|
||||
#### 3.1.6 Pronunciation (AAA)
|
||||
- [ ] A mechanism is available for identifying specific pronunciation of words where meaning is ambiguous without knowing the pronunciation
|
||||
|
||||
#### 3.2.5 Change on Request (AAA)
|
||||
- [ ] Changes of context are initiated only by user request or a mechanism is available to turn off such changes
|
||||
|
||||
#### 3.3.5 Help (AAA)
|
||||
- [ ] Context-sensitive help is available
|
||||
|
||||
#### 3.3.6 Error Prevention (All) (AAA)
|
||||
- [ ] For web pages that require the user to submit information, at least one of the following is true:
|
||||
- Submissions are reversible
|
||||
- Data entered by the user is checked for input errors and the user is provided an opportunity to correct them
|
||||
- A mechanism is available for reviewing, confirming, and correcting information before finalizing the submission
|
||||
|
||||
## Notes and Issues
|
||||
|
||||
<!-- Document any issues found during manual testing here -->
|
||||
|
||||
## Tester Information
|
||||
|
||||
- Name:
|
||||
- Date:
|
||||
- Browser/assistive technology used:
|
|
@ -0,0 +1,100 @@
|
|||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# pa11y-test.sh - Run Pa11y accessibility tests
|
||||
# =====================================================================
|
||||
# This script runs Pa11y accessibility tests for WCAG 2.1 AAA compliance
|
||||
# =====================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Check if base URL is provided
|
||||
if [ -z "$1" ]; then
|
||||
BASE_URL="http://localhost:8080"
|
||||
else
|
||||
BASE_URL="$1"
|
||||
fi
|
||||
|
||||
TESTS_DIR="$(dirname "$0")"
|
||||
REPORTS_DIR="$TESTS_DIR/../reports"
|
||||
mkdir -p "$REPORTS_DIR"
|
||||
|
||||
echo "=== Testing WCAG 2.1 AAA Compliance with Pa11y ==="
|
||||
echo "Using base URL: $BASE_URL"
|
||||
|
||||
# Get URLs from sitemap
|
||||
SITEMAP_URL="$BASE_URL/sitemap.xml"
|
||||
echo "Fetching pages from sitemap: $SITEMAP_URL"
|
||||
URLS=($(curl -s "$SITEMAP_URL" | grep -o '<loc>.*</loc>' | sed 's/<loc>//;s/<\/loc>//'))
|
||||
|
||||
if [ ${#URLS[@]} -eq 0 ]; then
|
||||
echo "No URLs found in sitemap, or sitemap could not be fetched."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found ${#URLS[@]} pages to test."
|
||||
|
||||
# Track failures
|
||||
FAILURES=0
|
||||
|
||||
# Run tests for each URL
|
||||
for url in "${URLS[@]}"; do
|
||||
echo "Testing ${url}..."
|
||||
|
||||
# Create a report file name from the URL
|
||||
report_file_name="pa11y-$(echo "$url" | sed -e 's|https://.*\.com/||' -e 's|/|-|g' -e 's/\.html//').json"
|
||||
report_file="$REPORTS_DIR/$report_file_name"
|
||||
|
||||
# Run Pa11y and save results
|
||||
if npx pa11y --standard WCAG2AAA --reporter json --ignore "content-security-policy" "${url}" > "${report_file}" 2>/dev/null; then
|
||||
issues=$(jq '.issues | length' "${report_file}" 2>/dev/null || echo 0)
|
||||
echo "Found ${issues} issues on ${url}"
|
||||
|
||||
# Check if there are issues
|
||||
if [ "${issues}" -gt 0 ]; then
|
||||
FAILURES=$((FAILURES + 1))
|
||||
|
||||
# Print summary of issues
|
||||
echo "Issues summary:"
|
||||
jq -r '.issues[] | "- \(.type): \(.message)\n Selector: \(.selector)"' "${report_file}" 2>/dev/null | head -n 10
|
||||
|
||||
# If there are more than 5 issues, indicate that
|
||||
if [ "${issues}" -gt 5 ]; then
|
||||
echo "... and $((issues - 5)) more issues."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Error running Pa11y on ${url}"
|
||||
# Check if a report file was created with error info
|
||||
if [ -f "${report_file}" ]; then
|
||||
error_message=$(jq -r '.error' "${report_file}" 2>/dev/null)
|
||||
if [ -n "$error_message" ]; then
|
||||
echo " Pa11y error: $error_message"
|
||||
fi
|
||||
fi
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
|
||||
echo "---"
|
||||
done
|
||||
|
||||
# Create summary report
|
||||
jq -n --arg timestamp "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
||||
--arg baseUrl "${BASE_URL}" \
|
||||
--arg failures "${FAILURES}" \
|
||||
--arg total "${#URLS[@]}" \
|
||||
'{
|
||||
"timestamp": $timestamp,
|
||||
"baseUrl": $baseUrl,
|
||||
"summary": {
|
||||
"total": ($total | tonumber),
|
||||
"failures": ($failures | tonumber),
|
||||
"passed": (($total | tonumber) - ($failures | tonumber))
|
||||
}
|
||||
}' > "${REPORTS_DIR}/pa11y-summary.json"
|
||||
|
||||
# Exit with appropriate status
|
||||
if [ "${FAILURES}" -gt 0 ]; then
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
|
@ -0,0 +1,199 @@
|
|||
/**
|
||||
* WCAG 2.1 AAA compliance test using Playwright and axe-core
|
||||
*
|
||||
* This script uses Playwright to load pages and axe-core to test them for accessibility issues.
|
||||
* It's an alternative to the axe-test.js script that properly injects axe-core into the page.
|
||||
*/
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const axeCore = require('axe-core');
|
||||
|
||||
// Base URL to test
|
||||
const BASE_URL = process.argv[2] || 'http://localhost:8080';
|
||||
|
||||
// Create reports directory if it doesn't exist
|
||||
const reportsDir = path.join(__dirname, '../reports');
|
||||
if (!fs.existsSync(reportsDir)) {
|
||||
fs.mkdirSync(reportsDir, { recursive: true });
|
||||
}
|
||||
|
||||
async function getPagesFromSitemap(sitemapUrl) {
|
||||
try {
|
||||
const response = await fetch(sitemapUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch sitemap: ${response.statusText}`);
|
||||
}
|
||||
const sitemapText = await response.text();
|
||||
const urls = sitemapText.match(/<loc>(.*?)<\/loc>/g) || [];
|
||||
|
||||
return urls.map(url => {
|
||||
const urlContent = url.replace(/<\/?loc>/g, '');
|
||||
const urlObject = new URL(urlContent);
|
||||
return urlObject.pathname;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error reading sitemap: ${error}`);
|
||||
// Fallback to a default list if sitemap fails
|
||||
return [
|
||||
'/',
|
||||
'/stories/',
|
||||
'/stories/open-source-success.html',
|
||||
'/stories/viperwire.html',
|
||||
'/one-pager-tools/csv-tool.html'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
async function runAxe(page, pageUrl) {
|
||||
try {
|
||||
// Use a file URL to load axe-core from a local file to avoid CSP issues
|
||||
const axeScriptPath = path.join(__dirname, 'axe-core.js');
|
||||
fs.writeFileSync(axeScriptPath, axeCore.source);
|
||||
|
||||
// Add the script as a file
|
||||
await page.addScriptTag({ path: axeScriptPath, type: 'text/javascript' });
|
||||
|
||||
// Run axe with WCAG 2.1 AAA rules
|
||||
const results = await page.evaluate(() => {
|
||||
if (typeof axe === 'undefined') {
|
||||
return { error: 'axe-core not loaded' };
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
axe.run(document, {
|
||||
runOnly: {
|
||||
type: 'tag',
|
||||
values: ['wcag2aaa']
|
||||
},
|
||||
resultTypes: ['violations', 'incomplete', 'inapplicable'],
|
||||
rules: {
|
||||
'color-contrast': { enabled: true, options: { noScroll: true } }
|
||||
}
|
||||
})
|
||||
.then(results => resolve(results))
|
||||
.catch(err => resolve({ error: err.toString() }));
|
||||
});
|
||||
});
|
||||
|
||||
// Clean up temporary file
|
||||
fs.unlinkSync(axeScriptPath);
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Error running axe:', error);
|
||||
return { error: error.toString() };
|
||||
}
|
||||
}
|
||||
|
||||
async function testPage(browser, pageUrl) {
|
||||
const context = await browser.newContext({
|
||||
bypassCSP: true
|
||||
});
|
||||
const page = await context.newPage();
|
||||
console.log(`Testing ${pageUrl}...`);
|
||||
|
||||
try {
|
||||
// Navigate to the page
|
||||
await page.goto(pageUrl, { waitUntil: 'networkidle' });
|
||||
|
||||
// Run axe-core tests
|
||||
const results = await runAxe(page, pageUrl);
|
||||
|
||||
// Save results to file
|
||||
const fileName = pageUrl === BASE_URL ? 'index' : pageUrl.replace(BASE_URL, '').replace(/\//g, '-').replace(/^-/, '');
|
||||
const reportPath = path.join(reportsDir, `axe-${fileName || 'index'}.json`);
|
||||
fs.writeFileSync(reportPath, JSON.stringify(results, null, 2));
|
||||
|
||||
// Log results summary
|
||||
if (results.error) {
|
||||
console.error(`Error running axe-core on ${pageUrl}:`, results.error);
|
||||
return { url: pageUrl, success: false, error: results.error };
|
||||
}
|
||||
|
||||
const { violations, incomplete, passes, inapplicable } = results;
|
||||
console.log(`Results for ${pageUrl}:`);
|
||||
console.log(`- Violations: ${violations.length}`);
|
||||
console.log(`- Incomplete: ${incomplete.length}`);
|
||||
console.log(`- Passes: ${passes.length}`);
|
||||
console.log(`- Inapplicable: ${inapplicable.length}`);
|
||||
|
||||
// Print violations
|
||||
if (violations.length > 0) {
|
||||
console.log('\nViolations:');
|
||||
violations.forEach((violation, i) => {
|
||||
console.log(`${i + 1}. ${violation.id} - ${violation.help} (Impact: ${violation.impact})`);
|
||||
console.log(` Description: ${violation.description}`);
|
||||
console.log(` Help: ${violation.helpUrl}`);
|
||||
violation.nodes.forEach(node => {
|
||||
console.log(` - Element: ${node.html}`);
|
||||
console.log(` Selector: ${node.target.join(', ')}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
url: pageUrl,
|
||||
success: violations.length === 0,
|
||||
violations: violations.length,
|
||||
incomplete: incomplete.length,
|
||||
passes: passes.length
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error testing ${pageUrl}:`, error);
|
||||
return { url: pageUrl, success: false, error: error.toString() };
|
||||
} finally {
|
||||
await page.close();
|
||||
await context.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
const browser = await chromium.launch();
|
||||
const results = [];
|
||||
|
||||
const sitemapUrl = `${BASE_URL}/sitemap.xml`;
|
||||
console.log(`Fetching pages from sitemap: ${sitemapUrl}`);
|
||||
const pagesToTest = await getPagesFromSitemap(sitemapUrl);
|
||||
console.log(`Found ${pagesToTest.length} pages to test.`);
|
||||
|
||||
try {
|
||||
// Test each page
|
||||
for (const pagePath of pagesToTest) {
|
||||
const pageUrl = `${BASE_URL}${pagePath.startsWith('/') ? '' : '/'}${pagePath}`;
|
||||
const result = await testPage(browser, pageUrl);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
// Save overall results
|
||||
const overallReport = {
|
||||
timestamp: new Date().toISOString(),
|
||||
baseUrl: BASE_URL,
|
||||
pages: results,
|
||||
summary: {
|
||||
total: results.length,
|
||||
passed: results.filter(r => r.success).length,
|
||||
failed: results.filter(r => !r.success).length
|
||||
}
|
||||
};
|
||||
|
||||
fs.writeFileSync(path.join(reportsDir, 'axe-summary.json'), JSON.stringify(overallReport, null, 2));
|
||||
|
||||
// Print overall summary
|
||||
console.log('\n=== Overall Summary ===');
|
||||
console.log(`Total pages tested: ${overallReport.summary.total}`);
|
||||
console.log(`Pages passed: ${overallReport.summary.passed}`);
|
||||
console.log(`Pages failed: ${overallReport.summary.failed}`);
|
||||
|
||||
// Exit with appropriate code
|
||||
process.exit(overallReport.summary.failed > 0 ? 1 : 0);
|
||||
} catch (error) {
|
||||
console.error('Error running tests:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
runTests();
|
|
@ -0,0 +1,103 @@
|
|||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# run-accessibility-tests.sh - Run all accessibility tests
|
||||
# =====================================================================
|
||||
# This script runs all accessibility tests for WCAG 2.1 AAA compliance
|
||||
# =====================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Check if base URL is provided
|
||||
if [ -z "$1" ]; then
|
||||
BASE_URL="http://localhost:8080"
|
||||
else
|
||||
BASE_URL="$1"
|
||||
fi
|
||||
|
||||
TESTS_DIR="$(dirname "$0")"
|
||||
REPORTS_DIR="$TESTS_DIR/../reports"
|
||||
mkdir -p "$REPORTS_DIR"
|
||||
|
||||
echo "=== Running Accessibility Tests ==="
|
||||
echo "Using base URL: $BASE_URL"
|
||||
|
||||
# Track test results
|
||||
FAILED_TESTS=0
|
||||
|
||||
# Run Pa11y tests
|
||||
echo "Running Pa11y tests..."
|
||||
if command -v npx &> /dev/null && npx pa11y --version &> /dev/null; then
|
||||
if "$TESTS_DIR/pa11y-test.sh" "$BASE_URL"; then
|
||||
echo "✅ Pa11y tests passed"
|
||||
else
|
||||
echo "❌ Pa11y tests failed"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
fi
|
||||
else
|
||||
echo "❌ Pa11y not installed, skipping Pa11y tests"
|
||||
echo " Install with: npm install -g pa11y"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
fi
|
||||
|
||||
# Run axe-core tests with Playwright
|
||||
echo "Running axe-core tests with Playwright..."
|
||||
if command -v node &> /dev/null; then
|
||||
if [ -f "$TESTS_DIR/playwright-axe.js" ]; then
|
||||
if node "$TESTS_DIR/playwright-axe.js" "$BASE_URL"; then
|
||||
echo "✅ axe-core tests passed"
|
||||
else
|
||||
echo "❌ axe-core tests failed"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
fi
|
||||
else
|
||||
echo "❌ playwright-axe.js not found"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
fi
|
||||
else
|
||||
echo "❌ Node.js not installed, skipping axe-core tests"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
fi
|
||||
|
||||
# Remind about manual testing
|
||||
echo "Don't forget to complete the manual testing checklist:"
|
||||
echo "$TESTS_DIR/manual-checklist.md"
|
||||
|
||||
# Generate combined report
|
||||
echo "Generating combined accessibility report..."
|
||||
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
cat > "$REPORTS_DIR/accessibility-summary.json" << EOJSON
|
||||
{
|
||||
"timestamp": "$TIMESTAMP",
|
||||
"baseUrl": "$BASE_URL",
|
||||
"summary": {
|
||||
"automated": {
|
||||
"pa11y": {
|
||||
"status": "$([ -f "$REPORTS_DIR/pa11y-summary.json" ] && echo "completed" || echo "failed")",
|
||||
"report": "pa11y-summary.json"
|
||||
},
|
||||
"axe": {
|
||||
"status": "$([ -f "$REPORTS_DIR/axe-summary.json" ] && echo "completed" || echo "failed")",
|
||||
"report": "axe-summary.json"
|
||||
}
|
||||
},
|
||||
"manual": {
|
||||
"status": "pending",
|
||||
"checklist": "../accessibility/manual-checklist.md"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOJSON
|
||||
|
||||
# Print summary
|
||||
echo "=== Accessibility Test Summary ==="
|
||||
echo "Tests completed at: $TIMESTAMP"
|
||||
echo "Reports saved to: $REPORTS_DIR"
|
||||
|
||||
if [ "$FAILED_TESTS" -gt 0 ]; then
|
||||
echo "❌ $FAILED_TESTS test suites failed"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ All automated test suites passed"
|
||||
echo "⚠️ Manual testing still required - see checklist"
|
||||
exit 0
|
||||
fi
|
|
@ -0,0 +1,92 @@
|
|||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# csp-hash-test.sh - Test the CSP hash update process
|
||||
# =====================================================================
|
||||
# This script checks if the CSP hash update process is working properly
|
||||
# =====================================================================
|
||||
|
||||
# Check if base URL is provided
|
||||
if [ -z "$1" ]; then
|
||||
BASE_URL="http://localhost:8080"
|
||||
else
|
||||
BASE_URL="$1"
|
||||
fi
|
||||
|
||||
echo "=== Testing CSP Hash Update Process ==="
|
||||
echo "Using base URL: $BASE_URL"
|
||||
|
||||
# Array to track failures
|
||||
FAILURES=0
|
||||
|
||||
# Check if the CSP headers are present
|
||||
echo "Checking if CSP headers are present..."
|
||||
RESPONSE=$(curl -s -I "$BASE_URL/")
|
||||
if echo "$RESPONSE" | grep -q "Content-Security-Policy"; then
|
||||
echo "✅ CSP header found in response"
|
||||
else
|
||||
echo "❌ CSP header not found in response"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
|
||||
# Check if the CSP header contains the required directives
|
||||
echo "Checking if CSP header contains required directives..."
|
||||
CSP_HEADER=$(curl -s -I "$BASE_URL/" | grep -i "Content-Security-Policy" | sed 's/.*: //')
|
||||
|
||||
for directive in "default-src" "script-src" "style-src" "img-src" "font-src" "connect-src" "object-src" "frame-ancestors" "base-uri" "form-action"; do
|
||||
if echo "$CSP_HEADER" | grep -q "$directive"; then
|
||||
echo "✅ CSP header contains $directive directive"
|
||||
else
|
||||
echo "❌ CSP header does not contain $directive directive"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if JavaScript files have integrity attributes
|
||||
echo "Checking if JavaScript files have integrity attributes..."
|
||||
for js_file in "theme.js" "includes.js"; do
|
||||
HTML=$(curl -s "$BASE_URL/")
|
||||
if echo "$HTML" | grep -q "$js_file.*integrity"; then
|
||||
echo "✅ $js_file has integrity attribute"
|
||||
else
|
||||
echo "❌ $js_file does not have integrity attribute"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if CSS files have integrity attributes
|
||||
echo "Checking if CSS files have integrity attributes..."
|
||||
HTML=$(curl -s "$BASE_URL/")
|
||||
if echo "$HTML" | grep -q "styles.css.*integrity"; then
|
||||
echo "✅ styles.css has integrity attribute"
|
||||
else
|
||||
echo "❌ styles.css does not have integrity attribute"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
|
||||
# Check if HTML files have CSP meta tags
|
||||
echo "Checking if HTML files have CSP meta tags..."
|
||||
HTML=$(curl -s "$BASE_URL/")
|
||||
if echo "$HTML" | grep -q '<meta http-equiv="Content-Security-Policy"'; then
|
||||
echo "✅ HTML file has CSP meta tag"
|
||||
else
|
||||
echo "❌ HTML file does not have CSP meta tag"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
|
||||
# Check if the update-csp-hashes.sh script exists
|
||||
echo "Checking if update-csp-hashes.sh script exists..."
|
||||
if [ -f "$(pwd)/docker/resume/update-csp-hashes.sh" ]; then
|
||||
echo "✅ update-csp-hashes.sh script exists"
|
||||
else
|
||||
echo "❌ update-csp-hashes.sh script does not exist"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
|
||||
# Check if any failures occurred
|
||||
if [ "$FAILURES" -eq 0 ]; then
|
||||
echo "=== All CSP Hash Tests Passed ==="
|
||||
exit 0
|
||||
else
|
||||
echo "=== CSP Hash Tests Failed: $FAILURES failures ==="
|
||||
exit 1
|
||||
fi
|
|
@ -0,0 +1,52 @@
|
|||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# test-csv-tool.sh - Test the CSV tool functionality
|
||||
# =====================================================================
|
||||
# This script checks if the CSV tool page loads without CSP errors
|
||||
# =====================================================================
|
||||
|
||||
# Check if base URL is provided
|
||||
if [ -z "$1" ]; then
|
||||
BASE_URL="http://localhost:8080"
|
||||
else
|
||||
BASE_URL="$1"
|
||||
fi
|
||||
|
||||
echo "=== Testing CSV Tool ==="
|
||||
echo "Using base URL: $BASE_URL"
|
||||
|
||||
# Create a test CSV file
|
||||
echo "Name,Age,City
|
||||
John,30,New York
|
||||
Jane,25,San Francisco
|
||||
Bob,40,Chicago" > /tmp/test.csv
|
||||
|
||||
# Check if the page loads properly
|
||||
echo "Checking if the CSV tool page loads properly..."
|
||||
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/one-pager-tools/csv-tool.html")
|
||||
|
||||
if [ "$RESPONSE" -eq 200 ]; then
|
||||
echo "✅ CSV tool page loads successfully (HTTP $RESPONSE)"
|
||||
else
|
||||
echo "❌ CSV tool page failed to load (HTTP $RESPONSE)"
|
||||
rm -f /tmp/test.csv
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for CSP errors in the response headers
|
||||
echo "Checking for CSP errors in response headers..."
|
||||
CSP_HEADER=$(curl -s -I "$BASE_URL/one-pager-tools/csv-tool.html" | grep -i "Content-Security-Policy")
|
||||
|
||||
if [ -n "$CSP_HEADER" ]; then
|
||||
echo "✅ CSP header found in response"
|
||||
else
|
||||
echo "❌ CSP header not found in response"
|
||||
rm -f /tmp/test.csv
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm -f /tmp/test.csv
|
||||
|
||||
echo "=== CSV Tool Test Completed Successfully ==="
|
||||
exit 0
|
|
@ -0,0 +1,122 @@
|
|||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# functional-test.sh - Test the main functionality of the website
|
||||
# =====================================================================
|
||||
# This script checks if the main features of the website are working
|
||||
# =====================================================================
|
||||
|
||||
# Check if base URL is provided
|
||||
if [ -z "$1" ]; then
|
||||
BASE_URL="http://localhost:8080"
|
||||
else
|
||||
BASE_URL="$1"
|
||||
fi
|
||||
|
||||
echo "=== Testing Website Functionality ==="
|
||||
echo "Using base URL: $BASE_URL"
|
||||
|
||||
# Array to track failures
|
||||
FAILURES=0
|
||||
|
||||
# Function to test a page and check for expected content
|
||||
test_page() {
|
||||
local url="$1"
|
||||
local name="$2"
|
||||
local expected_title="$3"
|
||||
local expected_content="$4"
|
||||
|
||||
echo "Testing $name page at $url"
|
||||
|
||||
# Check if the page loads
|
||||
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$url")
|
||||
if [ "$RESPONSE" -eq 200 ]; then
|
||||
echo "✅ $name page loads successfully (HTTP $RESPONSE)"
|
||||
else
|
||||
echo "❌ $name page failed to load (HTTP $RESPONSE)"
|
||||
FAILURES=$((FAILURES+1))
|
||||
return
|
||||
fi
|
||||
|
||||
# Check page title
|
||||
TITLE=$(curl -s "$url" | grep -o "<title>.*</title>" | sed 's/<title>\(.*\)<\/title>/\1/')
|
||||
if [[ "$TITLE" == *"$expected_title"* ]]; then
|
||||
echo "✅ Page title matches: $TITLE"
|
||||
else
|
||||
echo "❌ Page title doesn't match. Expected: $expected_title, Got: $TITLE"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
|
||||
# Check for expected content
|
||||
if [ -n "$expected_content" ]; then
|
||||
CONTENT=$(curl -s "$url")
|
||||
if echo "$CONTENT" | grep -q "$expected_content"; then
|
||||
echo "✅ Page contains expected content: $expected_content"
|
||||
else
|
||||
echo "❌ Page doesn't contain expected content: $expected_content"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "---"
|
||||
}
|
||||
|
||||
# Test main page
|
||||
test_page "$BASE_URL/" "Main" "Colin Knapp - Portfolio" "Colin Knapp"
|
||||
|
||||
# Test stories page
|
||||
test_page "$BASE_URL/stories/" "Stories" "Stories" "Case Studies"
|
||||
|
||||
# Test CSV tool
|
||||
test_page "$BASE_URL/one-pager-tools/csv-tool.html" "CSV Tool" "CSV Viewer" "Paste your CSV data here"
|
||||
|
||||
# Check for JavaScript files
|
||||
echo "Checking for required JavaScript files..."
|
||||
JS_FILES=("theme.js" "includes.js" "utils.js")
|
||||
for js_file in "${JS_FILES[@]}"; do
|
||||
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/$js_file")
|
||||
if [ "$RESPONSE" -eq 200 ]; then
|
||||
echo "✅ $js_file loads successfully (HTTP $RESPONSE)"
|
||||
else
|
||||
echo "❌ $js_file failed to load (HTTP $RESPONSE)"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
done
|
||||
|
||||
# Check for CSS files
|
||||
echo "Checking for required CSS files..."
|
||||
CSS_FILES=("styles.css")
|
||||
for css_file in "${CSS_FILES[@]}"; do
|
||||
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/$css_file")
|
||||
if [ "$RESPONSE" -eq 200 ]; then
|
||||
echo "✅ $css_file loads successfully (HTTP $RESPONSE)"
|
||||
else
|
||||
echo "❌ $css_file failed to load (HTTP $RESPONSE)"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
done
|
||||
|
||||
# Check for security headers
|
||||
echo "Checking for security headers..."
|
||||
HEADERS=$(curl -s -I "$BASE_URL/")
|
||||
if echo "$HEADERS" | grep -q "Content-Security-Policy"; then
|
||||
echo "✅ Content-Security-Policy header found"
|
||||
else
|
||||
echo "❌ Content-Security-Policy header not found"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
|
||||
if echo "$HEADERS" | grep -q "X-Frame-Options"; then
|
||||
echo "✅ X-Frame-Options header found"
|
||||
else
|
||||
echo "❌ X-Frame-Options header not found"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
|
||||
# Check if any failures occurred
|
||||
if [ "$FAILURES" -eq 0 ]; then
|
||||
echo "=== All Functionality Tests Passed ==="
|
||||
exit 0
|
||||
else
|
||||
echo "=== Functionality Tests Failed: $FAILURES failures ==="
|
||||
exit 1
|
||||
fi
|
|
@ -0,0 +1,108 @@
|
|||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# includes-test.sh - Test the includes functionality
|
||||
# =====================================================================
|
||||
# This script checks if the includes system is working properly
|
||||
# =====================================================================
|
||||
|
||||
# Check if base URL is provided
|
||||
if [ -z "$1" ]; then
|
||||
BASE_URL="http://localhost:8080"
|
||||
else
|
||||
BASE_URL="$1"
|
||||
fi
|
||||
|
||||
echo "=== Testing Includes Functionality ==="
|
||||
echo "Using base URL: $BASE_URL"
|
||||
|
||||
# Array to track failures
|
||||
FAILURES=0
|
||||
|
||||
# Test if includes.js exists and loads properly
|
||||
echo "Checking if includes.js exists and loads properly..."
|
||||
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/includes.js")
|
||||
if [ "$RESPONSE" -eq 200 ]; then
|
||||
echo "✅ includes.js loads successfully (HTTP $RESPONSE)"
|
||||
|
||||
# Check includes.js content
|
||||
INCLUDES_JS=$(curl -s "$BASE_URL/includes.js")
|
||||
if echo "$INCLUDES_JS" | grep -q "includeHTML"; then
|
||||
echo "✅ includes.js contains includeHTML function"
|
||||
else
|
||||
echo "❌ includes.js doesn't contain includeHTML function"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
|
||||
if echo "$INCLUDES_JS" | grep -q "DOMContentLoaded"; then
|
||||
echo "✅ includes.js contains DOMContentLoaded event listener"
|
||||
else
|
||||
echo "❌ includes.js doesn't contain DOMContentLoaded event listener"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
else
|
||||
echo "❌ includes.js failed to load (HTTP $RESPONSE)"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
|
||||
# Check if the includes/header.html file exists
|
||||
echo "Checking includes/header.html file..."
|
||||
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/includes/header.html")
|
||||
if [ "$RESPONSE" -eq 200 ] || [ "$RESPONSE" -eq 403 ]; then
|
||||
echo "✅ includes/header.html file exists (HTTP $RESPONSE)"
|
||||
else
|
||||
echo "❌ includes/header.html file doesn't exist (HTTP $RESPONSE)"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
|
||||
# Check if pages with includes load correctly
|
||||
echo "Checking pages that use includes..."
|
||||
INCLUDE_PAGES=(
|
||||
"template-with-includes.html"
|
||||
"stories/story-with-includes.html"
|
||||
"one-pager-tools/tool-with-includes.html"
|
||||
)
|
||||
|
||||
for page in "${INCLUDE_PAGES[@]}"; do
|
||||
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/$page")
|
||||
if [ "$RESPONSE" -eq 200 ]; then
|
||||
echo "✅ $page loads successfully (HTTP $RESPONSE)"
|
||||
|
||||
# Check if the page has include placeholders
|
||||
CONTENT=$(curl -s "$BASE_URL/$page")
|
||||
if echo "$CONTENT" | grep -q "id=\"header-include\""; then
|
||||
echo "✅ $page has header include placeholder"
|
||||
else
|
||||
echo "❌ $page doesn't have header include placeholder"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
|
||||
if echo "$CONTENT" | grep -q "id=\"footer-include\""; then
|
||||
echo "✅ $page has footer include placeholder"
|
||||
else
|
||||
echo "❌ $page doesn't have footer include placeholder"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
|
||||
# Check if includes.js is included in the page
|
||||
if echo "$CONTENT" | grep -q "includes.js"; then
|
||||
echo "✅ $page includes the includes.js script"
|
||||
else
|
||||
echo "❌ $page doesn't include the includes.js script"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
else
|
||||
echo "❌ $page failed to load (HTTP $RESPONSE)"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
|
||||
echo "---"
|
||||
done
|
||||
|
||||
# Check if any failures occurred
|
||||
if [ "$FAILURES" -eq 0 ]; then
|
||||
echo "=== All Includes Tests Passed ==="
|
||||
exit 0
|
||||
else
|
||||
echo "=== Includes Tests Failed: $FAILURES failures ==="
|
||||
exit 1
|
||||
fi
|
|
@ -0,0 +1,63 @@
|
|||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# main-page-test.sh - Test the main page functionality
|
||||
# =====================================================================
|
||||
# This script checks if the main page loads correctly
|
||||
# =====================================================================
|
||||
|
||||
# Check if base URL is provided
|
||||
if [ -z "$1" ]; then
|
||||
BASE_URL="http://localhost:8080"
|
||||
else
|
||||
BASE_URL="$1"
|
||||
fi
|
||||
|
||||
echo "=== Testing Main Page ==="
|
||||
echo "Using base URL: $BASE_URL"
|
||||
|
||||
# Check if the main page loads properly
|
||||
echo "Checking if the main page loads properly..."
|
||||
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/index.html")
|
||||
|
||||
if [ "$RESPONSE" -eq 200 ]; then
|
||||
echo "✅ Main page loads successfully (HTTP $RESPONSE)"
|
||||
else
|
||||
echo "❌ Main page failed to load (HTTP $RESPONSE)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for page title
|
||||
echo "Checking page title..."
|
||||
TITLE=$(curl -s "$BASE_URL/index.html" | grep -o "<title>.*</title>")
|
||||
|
||||
if [ -n "$TITLE" ]; then
|
||||
echo "✅ Page title found: $TITLE"
|
||||
else
|
||||
echo "❌ Page title not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for CSS loading
|
||||
echo "Checking if CSS loads properly..."
|
||||
CSS_LINK=$(curl -s "$BASE_URL/index.html" | grep -o '<link[^>]*href="[^"]*styles.css[^"]*"[^>]*>')
|
||||
|
||||
if [ -n "$CSS_LINK" ]; then
|
||||
echo "✅ CSS link found: $CSS_LINK"
|
||||
|
||||
# Check if the CSS file itself loads
|
||||
CSS_URL=$(echo "$CSS_LINK" | grep -o 'href="[^"]*"' | sed 's/href="//;s/"$//')
|
||||
CSS_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/$CSS_URL")
|
||||
|
||||
if [ "$CSS_RESPONSE" -eq 200 ]; then
|
||||
echo "✅ CSS file loads successfully (HTTP $CSS_RESPONSE)"
|
||||
else
|
||||
echo "❌ CSS file failed to load (HTTP $CSS_RESPONSE)"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "❌ CSS link not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Main Page Test Completed Successfully ==="
|
||||
exit 0
|
|
@ -0,0 +1,67 @@
|
|||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# sitemap-test.sh - Test sitemap.xml
|
||||
# =====================================================================
|
||||
# This script tests that sitemap.xml is properly generated and contains
|
||||
# all expected URLs
|
||||
# =====================================================================
|
||||
|
||||
set -e
|
||||
|
||||
echo "=== Testing sitemap.xml ==="
|
||||
|
||||
# Get the base URL from the command line or use the default
|
||||
if [ -z "$1" ]; then
|
||||
BASE_URL="http://localhost:8080"
|
||||
else
|
||||
BASE_URL="$1"
|
||||
fi
|
||||
|
||||
# Check if sitemap.xml exists
|
||||
if ! curl -s "$BASE_URL/sitemap.xml" > /dev/null; then
|
||||
echo "❌ sitemap.xml not found at $BASE_URL/sitemap.xml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Download sitemap.xml
|
||||
echo "Downloading sitemap.xml from $BASE_URL/sitemap.xml"
|
||||
SITEMAP=$(curl -s "$BASE_URL/sitemap.xml")
|
||||
|
||||
# Count URLs in sitemap
|
||||
URL_COUNT=$(echo "$SITEMAP" | grep -c "<url>")
|
||||
echo "Found $URL_COUNT URLs in sitemap.xml"
|
||||
|
||||
# Check if sitemap contains at least 10 URLs
|
||||
if [ "$URL_COUNT" -lt 10 ]; then
|
||||
echo "❌ sitemap.xml contains fewer than 10 URLs ($URL_COUNT)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if important pages are included
|
||||
IMPORTANT_PAGES=(
|
||||
"$BASE_URL/index.html"
|
||||
"$BASE_URL/stories/index.html"
|
||||
"$BASE_URL/stories/open-source-success.html"
|
||||
"$BASE_URL/stories/viperwire.html"
|
||||
"$BASE_URL/one-pager-tools/csv-tool.html"
|
||||
)
|
||||
|
||||
MISSING_PAGES=0
|
||||
for page in "${IMPORTANT_PAGES[@]}"; do
|
||||
# Extract domain-relative path
|
||||
rel_path="${page#$BASE_URL/}"
|
||||
|
||||
# Check if page is in sitemap
|
||||
if ! echo "$SITEMAP" | grep -q "$rel_path"; then
|
||||
echo "❌ Important page missing from sitemap: $rel_path"
|
||||
MISSING_PAGES=$((MISSING_PAGES + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$MISSING_PAGES" -gt 0 ]; then
|
||||
echo "❌ $MISSING_PAGES important pages missing from sitemap.xml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ sitemap.xml test passed"
|
||||
exit 0
|
|
@ -0,0 +1,76 @@
|
|||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# stories-test.sh - Test all story pages
|
||||
# =====================================================================
|
||||
# This script checks if all story pages load correctly
|
||||
# =====================================================================
|
||||
|
||||
# Check if base URL is provided
|
||||
if [ -z "$1" ]; then
|
||||
BASE_URL="http://localhost:8080"
|
||||
else
|
||||
BASE_URL="$1"
|
||||
fi
|
||||
|
||||
echo "=== Testing Story Pages ==="
|
||||
echo "Using base URL: $BASE_URL"
|
||||
|
||||
# Get the list of story pages from the stories index
|
||||
echo "Getting list of story pages..."
|
||||
STORY_LINKS=$(curl -s "$BASE_URL/stories/index.html" | grep -o 'href="[^"]*\.html"' | grep -v 'index.html' | sed 's/href="//;s/"$//')
|
||||
|
||||
if [ -z "$STORY_LINKS" ]; then
|
||||
echo "❌ No story links found in stories/index.html"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found story links: $STORY_LINKS"
|
||||
|
||||
# Test each story page
|
||||
FAILED=0
|
||||
for link in $STORY_LINKS; do
|
||||
# Make sure the link is properly formed
|
||||
if [[ "$link" != /* && "$link" != http* ]]; then
|
||||
# Relative link, add stories/ prefix if needed
|
||||
if [[ "$link" != stories/* ]]; then
|
||||
STORY_URL="$BASE_URL/stories/$link"
|
||||
else
|
||||
STORY_URL="$BASE_URL/$link"
|
||||
fi
|
||||
elif [[ "$link" == /* ]]; then
|
||||
# Absolute path
|
||||
STORY_URL="$BASE_URL$link"
|
||||
else
|
||||
# Full URL
|
||||
STORY_URL="$link"
|
||||
fi
|
||||
|
||||
echo "Testing story page: $STORY_URL"
|
||||
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$STORY_URL")
|
||||
|
||||
if [ "$RESPONSE" -eq 200 ]; then
|
||||
echo "✅ Story page loads successfully (HTTP $RESPONSE)"
|
||||
|
||||
# Check for required elements
|
||||
CONTENT=$(curl -s "$STORY_URL")
|
||||
if echo "$CONTENT" | grep -q "<h1>" && echo "$CONTENT" | grep -q "<p>"; then
|
||||
echo "✅ Story page has required elements (h1 and p tags)"
|
||||
else
|
||||
echo "❌ Story page is missing required elements"
|
||||
FAILED=1
|
||||
fi
|
||||
else
|
||||
echo "❌ Story page failed to load (HTTP $RESPONSE)"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
echo "---"
|
||||
done
|
||||
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
echo "=== All Story Pages Test Completed Successfully ==="
|
||||
exit 0
|
||||
else
|
||||
echo "=== Story Pages Test Failed ==="
|
||||
exit 1
|
||||
fi
|
|
@ -0,0 +1,88 @@
|
|||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# theme-test.sh - Test the theme functionality
|
||||
# =====================================================================
|
||||
# This script checks if the theme system is working properly
|
||||
# =====================================================================
|
||||
|
||||
# Check if base URL is provided
|
||||
if [ -z "$1" ]; then
|
||||
BASE_URL="http://localhost:8080"
|
||||
else
|
||||
BASE_URL="$1"
|
||||
fi
|
||||
|
||||
echo "=== Testing Theme Functionality ==="
|
||||
echo "Using base URL: $BASE_URL"
|
||||
|
||||
# Array to track failures
|
||||
FAILURES=0
|
||||
|
||||
# Test if theme.js exists and loads properly
|
||||
echo "Checking if theme.js exists and loads properly..."
|
||||
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/theme.js")
|
||||
if [ "$RESPONSE" -eq 200 ]; then
|
||||
echo "✅ theme.js loads successfully (HTTP $RESPONSE)"
|
||||
|
||||
# Check theme.js content
|
||||
THEME_JS=$(curl -s "$BASE_URL/theme.js")
|
||||
if echo "$THEME_JS" | grep -q "DOMContentLoaded"; then
|
||||
echo "✅ theme.js contains DOMContentLoaded event listener"
|
||||
else
|
||||
echo "❌ theme.js doesn't contain DOMContentLoaded event listener"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
|
||||
if echo "$THEME_JS" | grep -q "themeToggle"; then
|
||||
echo "✅ theme.js contains themeToggle functionality"
|
||||
else
|
||||
echo "❌ theme.js doesn't contain themeToggle functionality"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
|
||||
if echo "$THEME_JS" | grep -q "localStorage"; then
|
||||
echo "✅ theme.js uses localStorage for theme persistence"
|
||||
else
|
||||
echo "❌ theme.js doesn't use localStorage for theme persistence"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
else
|
||||
echo "❌ theme.js failed to load (HTTP $RESPONSE)"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
|
||||
# Check if CSS has theme-related styles
|
||||
echo "Checking if styles.css has theme-related styles..."
|
||||
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/styles.css")
|
||||
if [ "$RESPONSE" -eq 200 ]; then
|
||||
echo "✅ styles.css loads successfully (HTTP $RESPONSE)"
|
||||
|
||||
# Check styles.css content for theme-related styles
|
||||
STYLES_CSS=$(curl -s "$BASE_URL/styles.css")
|
||||
if echo "$STYLES_CSS" | grep -q "data-theme"; then
|
||||
echo "✅ styles.css contains data-theme attribute selectors"
|
||||
else
|
||||
echo "❌ styles.css doesn't contain data-theme attribute selectors"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
|
||||
# Check for dark mode styles
|
||||
if echo "$STYLES_CSS" | grep -q "dark"; then
|
||||
echo "✅ styles.css contains dark mode styles"
|
||||
else
|
||||
echo "❌ styles.css doesn't contain dark mode styles"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
else
|
||||
echo "❌ styles.css failed to load (HTTP $RESPONSE)"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
|
||||
# Check if any failures occurred
|
||||
if [ "$FAILURES" -eq 0 ]; then
|
||||
echo "=== All Theme Tests Passed ==="
|
||||
exit 0
|
||||
else
|
||||
echo "=== Theme Tests Failed: $FAILURES failures ==="
|
||||
exit 1
|
||||
fi
|
|
@ -0,0 +1,50 @@
|
|||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# pre-test-setup.sh - Setup environment before running tests
|
||||
# =====================================================================
|
||||
# This script sets up the environment before running tests:
|
||||
# 1. Generates sitemap.xml
|
||||
# 2. Updates CSP hashes
|
||||
# =====================================================================
|
||||
|
||||
set -e
|
||||
|
||||
echo "=== Setting up test environment ==="
|
||||
|
||||
# Get the directory of this script
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
RESUME_DIR="$PROJECT_ROOT/docker/resume"
|
||||
|
||||
# Check if we're in the right directory
|
||||
if [ ! -d "$RESUME_DIR" ]; then
|
||||
echo "❌ Could not find resume directory at $RESUME_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Change to the resume directory
|
||||
cd "$RESUME_DIR"
|
||||
|
||||
# Run update scripts
|
||||
if [ -f "./update-all.sh" ]; then
|
||||
echo "=== Running update-all.sh ==="
|
||||
./update-all.sh
|
||||
else
|
||||
# Run individual scripts if update-all.sh doesn't exist
|
||||
if [ -f "./generate-sitemap.sh" ]; then
|
||||
echo "=== Generating sitemap.xml ==="
|
||||
./generate-sitemap.sh
|
||||
else
|
||||
echo "⚠️ generate-sitemap.sh not found, skipping sitemap generation"
|
||||
fi
|
||||
|
||||
if [ -f "./update-csp-hashes.sh" ]; then
|
||||
echo "=== Updating CSP hashes ==="
|
||||
./update-csp-hashes.sh
|
||||
else
|
||||
echo "⚠️ update-csp-hashes.sh not found, skipping CSP hash updates"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=== Test environment setup complete ==="
|
||||
exit 0
|
|
@ -0,0 +1,64 @@
|
|||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# run-all-tests.sh - Run all tests
|
||||
# =====================================================================
|
||||
# This script runs all tests in the correct order:
|
||||
# 1. Run pre-test setup
|
||||
# 2. Run integration tests
|
||||
# 3. Run accessibility tests
|
||||
# =====================================================================
|
||||
|
||||
set -e
|
||||
|
||||
echo "=== Running all tests ==="
|
||||
|
||||
# Get the directory of this script
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
|
||||
# Get the base URL from the command line or use the default
|
||||
if [ -z "$1" ]; then
|
||||
BASE_URL="http://localhost:8080"
|
||||
else
|
||||
BASE_URL="$1"
|
||||
fi
|
||||
|
||||
# Run pre-test setup
|
||||
echo "=== Running pre-test setup ==="
|
||||
"$SCRIPT_DIR/pre-test-setup.sh"
|
||||
|
||||
# Run integration tests
|
||||
echo "=== Running integration tests ==="
|
||||
for test_script in "$SCRIPT_DIR/integration"/*.sh; do
|
||||
if [ -f "$test_script" ]; then
|
||||
echo "Running $test_script..."
|
||||
if "$test_script" "$BASE_URL"; then
|
||||
echo "✅ $(basename "$test_script") passed"
|
||||
else
|
||||
echo "❌ $(basename "$test_script") failed"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Run accessibility tests
|
||||
echo "=== Running accessibility tests ==="
|
||||
if [ -f "$SCRIPT_DIR/accessibility/run-accessibility-tests.sh" ]; then
|
||||
if "$SCRIPT_DIR/accessibility/run-accessibility-tests.sh" "$BASE_URL"; then
|
||||
echo "✅ Accessibility tests passed"
|
||||
else
|
||||
echo "❌ Accessibility tests failed"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
fi
|
||||
else
|
||||
echo "⚠️ Accessibility tests not found, skipping"
|
||||
fi
|
||||
|
||||
# Print summary
|
||||
echo "=== Test Summary ==="
|
||||
if [ "$FAILED_TESTS" -gt 0 ]; then
|
||||
echo "❌ $FAILED_TESTS test suites failed"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ All tests passed"
|
||||
exit 0
|
||||
fi
|
|
@ -0,0 +1,192 @@
|
|||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# run-tests.sh - Main test runner for the resume site
|
||||
# =====================================================================
|
||||
# This script starts a local Caddy server and runs all tests against it
|
||||
# =====================================================================
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
# Define constants
|
||||
TEST_PORT=8080
|
||||
CADDY_DIR="docker/resume"
|
||||
CADDY_FILE="Caddyfile.local"
|
||||
TESTS_DIR="$(dirname "$0")"
|
||||
LOG_FILE="$TESTS_DIR/test-run.log"
|
||||
|
||||
# Function to clean up processes on exit
|
||||
cleanup() {
|
||||
echo "Cleaning up..."
|
||||
# Find and kill any Caddy processes we started
|
||||
if [ -f "$TESTS_DIR/.caddy.pid" ]; then
|
||||
CADDY_PID=$(cat "$TESTS_DIR/.caddy.pid")
|
||||
if ps -p $CADDY_PID > /dev/null; then
|
||||
echo "Stopping Caddy server (PID: $CADDY_PID)"
|
||||
kill $CADDY_PID
|
||||
fi
|
||||
rm "$TESTS_DIR/.caddy.pid"
|
||||
fi
|
||||
|
||||
echo "Cleanup complete"
|
||||
}
|
||||
|
||||
# Register the cleanup function to run on exit
|
||||
trap cleanup EXIT
|
||||
|
||||
# Start the Caddy server
|
||||
start_caddy() {
|
||||
echo "Starting Caddy server on port $TEST_PORT..."
|
||||
|
||||
# Navigate to the Caddy directory
|
||||
cd "$CADDY_DIR"
|
||||
|
||||
# Check if Caddyfile.local exists, if not, create it from Caddyfile
|
||||
if [ ! -f "$CADDY_FILE" ]; then
|
||||
echo "Creating $CADDY_FILE from Caddyfile..."
|
||||
cp Caddyfile "$CADDY_FILE"
|
||||
# Modify the Caddyfile.local to use the test port
|
||||
sed -i '' "s/:80/:$TEST_PORT/g" "$CADDY_FILE"
|
||||
fi
|
||||
|
||||
# Start Caddy in the background using the local config
|
||||
echo "Running: caddy run --config $CADDY_FILE"
|
||||
mkdir -p $(dirname "$LOG_FILE") && caddy run --config "$CADDY_FILE" > "$LOG_FILE" 2>&1 &
|
||||
CADDY_PID=$!
|
||||
mkdir -p "$TESTS_DIR" && echo $CADDY_PID > "$TESTS_DIR/.caddy.pid"
|
||||
|
||||
# Return to the original directory
|
||||
cd - > /dev/null
|
||||
|
||||
# Wait for Caddy to start
|
||||
echo "Waiting for Caddy to start..."
|
||||
sleep 2
|
||||
|
||||
# Check if Caddy is running
|
||||
if ! ps -p $CADDY_PID > /dev/null; then
|
||||
echo "Failed to start Caddy server. Check $LOG_FILE for details."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Caddy server started with PID: $CADDY_PID"
|
||||
|
||||
# Wait a bit more to ensure Caddy is fully initialized
|
||||
sleep 3
|
||||
}
|
||||
|
||||
# Run Node.js server for tests that require it
|
||||
start_node_server() {
|
||||
if [ -f "$TESTS_DIR/serve.js" ]; then
|
||||
echo "Starting Node.js server for tests..."
|
||||
node "$TESTS_DIR/serve.js" > "$TESTS_DIR/node-server.log" 2>&1 &
|
||||
NODE_SERVER_PID=$!
|
||||
echo $NODE_SERVER_PID > "$TESTS_DIR/.node-server.pid"
|
||||
|
||||
# Wait for Node.js server to start
|
||||
sleep 2
|
||||
|
||||
# Check if Node.js server is running
|
||||
if ! ps -p $NODE_SERVER_PID > /dev/null; then
|
||||
echo "Failed to start Node.js server. Check $TESTS_DIR/node-server.log for details."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Node.js server started with PID: $NODE_SERVER_PID"
|
||||
fi
|
||||
}
|
||||
|
||||
# Clean up Node.js server
|
||||
cleanup_node_server() {
|
||||
if [ -f "$TESTS_DIR/.node-server.pid" ]; then
|
||||
NODE_SERVER_PID=$(cat "$TESTS_DIR/.node-server.pid")
|
||||
if ps -p $NODE_SERVER_PID > /dev/null; then
|
||||
echo "Stopping Node.js server (PID: $NODE_SERVER_PID)"
|
||||
kill $NODE_SERVER_PID
|
||||
fi
|
||||
rm "$TESTS_DIR/.node-server.pid"
|
||||
fi
|
||||
}
|
||||
|
||||
# Run shell script tests
|
||||
run_shell_tests() {
|
||||
echo "Running shell script tests..."
|
||||
|
||||
# Run unit tests
|
||||
echo "Running unit tests..."
|
||||
if [ -d "$TESTS_DIR/unit" ] && [ "$(ls -A "$TESTS_DIR/unit")" ]; then
|
||||
for test in "$TESTS_DIR/unit"/*.sh; do
|
||||
if [ -f "$test" ] && [ -x "$test" ]; then
|
||||
echo "Running unit test: $(basename "$test")"
|
||||
"$test" || echo "FAILED: $(basename "$test")"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "No unit tests found."
|
||||
fi
|
||||
|
||||
# Run integration tests
|
||||
echo "Running integration tests..."
|
||||
if [ -d "$TESTS_DIR/integration" ] && [ "$(ls -A "$TESTS_DIR/integration")" ]; then
|
||||
for test in "$TESTS_DIR/integration"/*.sh; do
|
||||
if [ -f "$test" ] && [ -x "$test" ]; then
|
||||
echo "Running integration test: $(basename "$test")"
|
||||
"$test" "http://localhost:$TEST_PORT" || echo "FAILED: $(basename "$test")"
|
||||
fi
|
||||
done
|
||||
|
||||
# Check for subdirectories
|
||||
for dir in "$TESTS_DIR/integration"/*/; do
|
||||
if [ -d "$dir" ]; then
|
||||
for test in "$dir"*.sh; do
|
||||
if [ -f "$test" ] && [ -x "$test" ]; then
|
||||
echo "Running integration test: $(basename "$test")"
|
||||
"$test" "http://localhost:$TEST_PORT" || echo "FAILED: $(basename "$test")"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "No integration tests found."
|
||||
fi
|
||||
|
||||
# Run e2e tests
|
||||
echo "Running e2e tests..."
|
||||
if [ -d "$TESTS_DIR/e2e" ] && [ "$(ls -A "$TESTS_DIR/e2e")" ]; then
|
||||
for test in "$TESTS_DIR/e2e"/*.sh; do
|
||||
if [ -f "$test" ] && [ -x "$test" ]; then
|
||||
echo "Running e2e test: $(basename "$test")"
|
||||
"$test" "http://localhost:$TEST_PORT" || echo "FAILED: $(basename "$test")"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "No e2e tests found."
|
||||
fi
|
||||
}
|
||||
|
||||
# Run JavaScript tests
|
||||
run_js_tests() {
|
||||
echo "Running JavaScript tests..."
|
||||
|
||||
# Check if Playwright is installed
|
||||
if command -v npx &> /dev/null && npx playwright --version &> /dev/null; then
|
||||
# Run Playwright tests
|
||||
echo "Running Playwright tests..."
|
||||
npx playwright test || echo "FAILED: Playwright tests"
|
||||
else
|
||||
echo "Playwright not found, skipping Playwright tests."
|
||||
fi
|
||||
|
||||
# Run Lighthouse tests if available
|
||||
if [ -f "$TESTS_DIR/lighthouse.js" ]; then
|
||||
echo "Running Lighthouse tests..."
|
||||
node "$TESTS_DIR/lighthouse.js" "http://localhost:$TEST_PORT" || echo "FAILED: Lighthouse tests"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution
|
||||
echo "=== Starting Test Suite ==="
|
||||
start_caddy
|
||||
start_node_server
|
||||
run_shell_tests
|
||||
run_js_tests
|
||||
cleanup_node_server
|
||||
echo "=== Test Suite Completed ==="
|
|
@ -0,0 +1,48 @@
|
|||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# includes-test.sh - Test the includes.js functionality
|
||||
# =====================================================================
|
||||
# This script checks if the includes.js file is valid JavaScript
|
||||
# =====================================================================
|
||||
|
||||
echo "=== Testing includes.js ==="
|
||||
|
||||
# Path to the includes.js file
|
||||
INCLUDES_JS="docker/resume/includes.js"
|
||||
|
||||
# Check if the file exists
|
||||
if [ ! -f "$INCLUDES_JS" ]; then
|
||||
echo "❌ File not found: $INCLUDES_JS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ File exists: $INCLUDES_JS"
|
||||
|
||||
# Check if the file is valid JavaScript using node
|
||||
if command -v node &> /dev/null; then
|
||||
echo "Checking if the file is valid JavaScript..."
|
||||
if node --check "$INCLUDES_JS" &> /dev/null; then
|
||||
echo "✅ File is valid JavaScript"
|
||||
else
|
||||
echo "❌ File contains JavaScript syntax errors"
|
||||
node --check "$INCLUDES_JS"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "⚠️ Node.js not found, skipping JavaScript syntax check"
|
||||
fi
|
||||
|
||||
# Check for required functions
|
||||
echo "Checking for required functions..."
|
||||
REQUIRED_FUNCTIONS=("includeHTML")
|
||||
for func in "${REQUIRED_FUNCTIONS[@]}"; do
|
||||
if grep -q "function $func" "$INCLUDES_JS"; then
|
||||
echo "✅ Required function found: $func"
|
||||
else
|
||||
echo "❌ Required function not found: $func"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "=== includes.js Test Completed Successfully ==="
|
||||
exit 0
|
|
@ -0,0 +1,45 @@
|
|||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# theme-test.sh - Test the theme.js functionality
|
||||
# =====================================================================
|
||||
# This script checks if the theme.js file is valid JavaScript
|
||||
# =====================================================================
|
||||
|
||||
echo "=== Testing theme.js ==="
|
||||
|
||||
# Path to the theme.js file
|
||||
THEME_JS="docker/resume/theme.js"
|
||||
|
||||
# Check if the file exists
|
||||
if [ ! -f "$THEME_JS" ]; then
|
||||
echo "❌ File not found: $THEME_JS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ File exists: $THEME_JS"
|
||||
|
||||
# Check if the file is valid JavaScript using node
|
||||
if command -v node &> /dev/null; then
|
||||
echo "Checking if the file is valid JavaScript..."
|
||||
if node --check "$THEME_JS" &> /dev/null; then
|
||||
echo "✅ File is valid JavaScript"
|
||||
else
|
||||
echo "❌ File contains JavaScript syntax errors"
|
||||
node --check "$THEME_JS"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "⚠️ Node.js not found, skipping JavaScript syntax check"
|
||||
fi
|
||||
|
||||
# Check for theme-related functionality
|
||||
echo "Checking for theme-related functionality..."
|
||||
if grep -q "dark" "$THEME_JS" || grep -q "light" "$THEME_JS" || grep -q "theme" "$THEME_JS"; then
|
||||
echo "✅ Theme-related functionality found"
|
||||
else
|
||||
echo "❌ No theme-related functionality found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== theme.js Test Completed Successfully ==="
|
||||
exit 0
|
Loading…
Reference in New Issue