Fix duplicate footer issues and remove npm dependency
ci/woodpecker/push/woodpecker Pipeline failed Details

This commit is contained in:
colin 2025-07-06 18:36:59 -04:00
parent f0d296f108
commit 2c43fe7784
51 changed files with 2895 additions and 1736 deletions

View File

@ -8,4 +8,5 @@
/README.md /README.md
/stack.production.yml /stack.production.yml
/stack.staging.yml /stack.staging.yml
/tests/ # /tests/
Dockerfile.production

25
docker/package-lock.json generated Normal file
View File

@ -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"
}
}
}
}

View File

@ -29,7 +29,7 @@ colinknapp.com {
Cache-Control "public, max-age=31536000, immutable" Cache-Control "public, max-age=31536000, immutable"
# CSP with hashes for scripts and styles # 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 # Handle 404s
@ -79,7 +79,7 @@ colinknapp.com {
Cache-Control "public, max-age=31536000, immutable" Cache-Control "public, max-age=31536000, immutable"
# CSP with hashes for scripts and styles # 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 # Handle 404s

View File

@ -39,6 +39,6 @@
Cache-Control "public, max-age=31536000, immutable" Cache-Control "public, max-age=31536000, immutable"
# CSP with hashes for scripts and styles # 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';"
} }
} }

View File

@ -1,43 +1,21 @@
FROM caddy:2-alpine FROM caddy:2.7-alpine
# Install required tools for hash calculation and CSP updates # Install dependencies
RUN apk add --no-cache bash coreutils findutils grep sed xxd perl gawk RUN apk add --no-cache nodejs bash
# 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/
# Set working directory # Set working directory
WORKDIR /srv WORKDIR /srv
# Run the update-csp-hashes.sh script to update CSP hashes # Copy website files
RUN chmod +x /srv/caddy.sh /srv/update-csp-hashes.sh && \ COPY . /srv
cd /srv && \
./update-csp-hashes.sh
# 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 EXPOSE 8080
# Run Caddy # Start Caddy with the local Caddyfile
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile"] CMD ["caddy", "run", "--config", "/srv/Caddyfile.local"]

View File

@ -1 +1 @@
FROM git.nixc.us/colin/resume:staging FROM git.nixc.us/nixc/resume:staging

View File

@ -52,7 +52,7 @@
} }
</style> </style>
</head> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -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")"

View File

@ -15,13 +15,19 @@
<li class="dropdown"> <li class="dropdown">
<a href="/stories/" id="nav-stories">Stories</a> <a href="/stories/" id="nav-stories">Stories</a>
<div class="dropdown-content"> <div class="dropdown-content">
<a href="/stories/viperwire.html" id="nav-viperwire">ViperWire</a> <a href="/stories/airport-dns.html" id="nav-airportdns">Airport Dns</a>
<a href="/stories/fawe-plotsquared.html" id="nav-fawe">FastAsyncWorldEdit</a> <a href="/stories/app-development.html" id="nav-appdevelopment">App Development</a>
<a href="/stories/healthcare-platform.html" id="nav-healthcare">Healthcare Platform</a> <a href="/stories/athion-turnaround.html" id="nav-athionturnaround">Athion Turnaround</a>
<a href="/stories/wordpress-security.html" id="nav-wordpress">WordPress Security</a> <a href="/stories/fawe-plotsquared.html" id="nav-faweplotsquared">Fawe Plotsquared</a>
<a href="/stories/airport-dns.html" id="nav-airport">Airport DNS</a> <a href="/stories/healthcare-platform.html" id="nav-healthcareplatform">Healthcare Platform</a>
<a href="/stories/nitric-leadership.html" id="nav-nitric">NitricConcepts</a> <a href="/stories/motherboard-repair.html" id="nav-motherboardrepair">Motherboard Repair</a>
<a href="/stories/open-source-success.html" id="nav-opensource">Open Source Success</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> </div>
</li> </li>
<li class="dropdown"> <li class="dropdown">

View File

@ -10,7 +10,7 @@
<script src="theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script> <script src="theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
<script src="includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script> <script src="includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
</head> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -10,7 +10,7 @@
<script src="theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script> <script src="theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
<script src="includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script> <script src="includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
</head> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <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 &rarr;</a></p> <p><a href="stories/open-source-success.html" class="read-more">Read more about my open source success &rarr;</a></p>
</div> </div>
</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> </div>
<!-- Footer Include --> <!-- Footer Include -->

View File

@ -52,7 +52,7 @@
} }
</style> </style>
</head> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -13,7 +13,7 @@
<script src="../utils.js" integrity="sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=" crossorigin="anonymous"></script> <script src="../utils.js" integrity="sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=" crossorigin="anonymous"></script>
<!-- Add tool-specific scripts here --> <!-- Add tool-specific scripts here -->
</head> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -13,7 +13,7 @@
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script> <script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
<script src="tool-example.js" defer></script> <script src="tool-example.js" defer></script>
</head> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

99
docker/resume/sitemap.xml Normal file
View File

@ -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>

View File

@ -9,9 +9,9 @@
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous"> <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="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" 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> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -9,9 +9,9 @@
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous"> <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="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" 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> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -9,9 +9,9 @@
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous"> <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="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" 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> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -9,9 +9,9 @@
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous"> <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="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" 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> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -9,9 +9,9 @@
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous"> <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="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" 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> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -9,9 +9,9 @@
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous"> <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="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" 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> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>
@ -23,101 +23,100 @@
<hr> <hr>
<div class="stories-grid"> <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"> <div class="story-card">
<h2>Airport DNS Infrastructure</h2> <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-excerpt">Category: Infrastructure & Resilience | Date: 2019-Present...</p>
<p class="story-meta">Category: Infrastructure | 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> <a href="airport-dns.html" class="story-link">Read Full Story</a>
</div> </div>
<div class="story-card"> <div class="story-card">
<h2>NitricConcepts Leadership</h2> <h2>App Development for Influencers</h2>
<p class="story-excerpt">Managing a distributed team of 45 contractors and implementing DevSecOps practices across multiple timezones.</p> <p class="story-excerpt">Category: Mobile Development, Analytics | Date: 2013-2018...</p>
<p class="story-meta">Category: Leadership | 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> <a href="app-development.html" class="story-link">Read Full Story</a>
</div> </div>
<div class="story-card"> <div class="story-card">
<h2>Athion.net Turnaround</h2> <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-excerpt">Category: Business Turnaround, System Optimization | Date: 2013-2017...</p>
<p class="story-meta">Category: Business Turnaround | 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> <a href="athion-turnaround.html" class="story-link">Read Full Story</a>
</div> </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"> <div class="story-card">
<h2>MotherboardRepair.ca</h2> <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-excerpt">Category: Entrepreneurship, Sustainable Technology | Date: 2019-Present...</p>
<p class="story-meta">Category: Entrepreneurship, Sustainability | 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> <a href="motherboard-repair.html" class="story-link">Read Full Story</a>
</div> </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"> <div class="story-card">
<h2>ShowerLoop Project</h2> <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-excerpt">Category: Web Development, Sustainability | Date: 2016...</p>
<p class="story-meta">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> <a href="showerloop.html" class="story-link">Read Full Story</a>
</div> </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> </div>
<hr> <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> </div>
<!-- Footer Include --> <!-- Footer Include -->

View File

@ -9,9 +9,9 @@
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous"> <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="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" 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> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -9,9 +9,9 @@
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous"> <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="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" 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> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -9,9 +9,9 @@
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50="> <link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script> <script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></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> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -9,9 +9,9 @@
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous"> <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="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" 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> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -156,11 +156,13 @@
border-radius: 4px; border-radius: 4px;
text-decoration: none; text-decoration: none;
transition: all 0.3s ease; transition: all 0.3s ease;
color: #004494;
} }
.story-nav-link:hover { .story-nav-link:hover {
background-color: var(--theme-hover); background-color: var(--theme-hover);
text-decoration: none; text-decoration: none;
color: #003366;
} }
.story-nav-link.prev::before { .story-nav-link.prev::before {
@ -240,6 +242,10 @@
.placeholder-notice a { .placeholder-notice a {
font-weight: bold; font-weight: bold;
color: var(--accent-color); color: #004494;
text-decoration: underline; text-decoration: underline;
} }
.placeholder-notice a:hover {
color: #003366;
}

View File

@ -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;
}
}

View File

@ -7,11 +7,11 @@
<title>Story Example - Colin Knapp</title> <title>Story Example - Colin Knapp</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico"> <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="../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="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script> <script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
</head> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -9,9 +9,9 @@
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous"> <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="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" 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> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -9,8 +9,8 @@
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous"> <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="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" 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=">
<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';">
</head> </head>
<body> <body>
<!-- Header Include --> <!-- Header Include -->

View File

@ -9,9 +9,9 @@
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous"> <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="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" 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> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -9,9 +9,9 @@
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous"> <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="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" 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> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -9,9 +9,9 @@
<link rel="stylesheet" href="../styles.css" integrity="sha256-efXJB9ojE48KDEisFG5s+pGha1fH1bZA/IKW/ZKrL50=" crossorigin="anonymous"> <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="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" 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> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -10,7 +10,7 @@
<script src="theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script> <script src="theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
<script src="includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script> <script src="includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
</head> </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> <body>
<!-- Header Include --> <!-- Header Include -->
<div id="header-include"></div> <div id="header-include"></div>

View File

@ -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}

80
docker/resume/update-all.sh Executable file
View File

@ -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"

View File

@ -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 };

View File

@ -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();

1730
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,23 +6,35 @@ const { defineConfig, devices } = require('@playwright/test');
*/ */
module.exports = defineConfig({ module.exports = defineConfig({
testDir: './tests', testDir: './tests',
timeout: 30 * 1000, /* Maximum time one test can run for. */
timeout: 60 * 1000,
expect: { expect: {
/**
* Maximum time expect() should wait for the condition to be met.
*/
timeout: 5000 timeout: 5000
}, },
/* Run tests in files in parallel */
fullyParallel: true, fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html', reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0, actionTimeout: 0,
trace: 'on-first-retry', /* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:8080', 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: [ projects: [
{ {
name: 'chromium', name: 'chromium',
@ -36,11 +48,21 @@ module.exports = defineConfig({
name: 'webkit', name: 'webkit',
use: { ...devices['Desktop Safari'] }, 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: { webServer: {
command: 'node tests/serve.js', command: 'node tests/serve.js',
port: 8080, url: 'http://localhost:8080',
reuseExistingServer: true, reuseExistingServer: !process.env.CI,
}, },
}); });

View File

@ -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 { execSync } = require('child_process');
const LOCAL_URL = 'http://localhost:8080'; const path = require('path');
const viewports = [ describe('Accessibility Tests', () => {
{ width: 375, height: 667, name: 'mobile' }, test('Run accessibility tests', () => {
{ width: 1024, height: 768, name: 'desktop' }
];
async function getPageUrl(page) {
try { try {
// Try production first // Run the accessibility tests
await page.goto(PRODUCTION_URL, { timeout: 60000 }); const scriptPath = path.join(__dirname, 'accessibility', 'run-accessibility-tests.sh');
return PRODUCTION_URL; execSync(`bash ${scriptPath}`, { stdio: 'inherit' });
} catch (error) { } catch (error) {
console.log('Production site not available, falling back to local'); // If the tests fail, the script will exit with a non-zero code
await page.goto(LOCAL_URL, { timeout: 60000 }); // Jest will catch this and mark the test as failed
return LOCAL_URL; 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');
} }
}); });
}); });
});

View File

@ -1,80 +1,64 @@
# Accessibility Testing # Accessibility Testing Framework
This directory contains tests for WCAG 2.1 AAA compliance. This directory contains tools and scripts for testing the website's accessibility compliance with WCAG 2.1 AAA standards.
## Overview ## Overview
The accessibility testing framework uses multiple tools to provide comprehensive coverage: The accessibility testing framework uses multiple approaches:
1. **axe-core**: JavaScript library for automated accessibility testing 1. **Automated Testing with Pa11y**: Command-line accessibility testing using Pa11y
2. **Pa11y**: Command-line tool for accessibility testing 2. **Automated Testing with axe-core**: JavaScript-based accessibility testing using axe-core
3. **Manual testing checklist**: For criteria that cannot be automatically tested 3. **Manual Testing Checklist**: A comprehensive checklist for aspects that can't be automated
## Requirements
- Node.js and npm
- Pa11y: `npm install -g pa11y`
- axe-core: `npm install axe-core`
- Playwright: `npm install playwright`
## Running Tests ## Running Tests
To run all accessibility tests: To run all accessibility tests:
```bash ```bash
./run-accessibility-tests.sh [base-url] npm run test:accessibility
``` ```
Default base URL is `http://localhost:8080` if not specified. Or run directly:
## Individual Test Scripts ```bash
./tests/accessibility/run-accessibility-tests.sh [BASE_URL]
```
- `axe-test.js`: Runs axe-core against all pages The default BASE_URL is `http://localhost:8080` if not specified.
- `pa11y-test.sh`: Runs Pa11y against all pages
- `manual-checklist.md`: Checklist for manual testing
## WCAG 2.1 AAA Compliance ## Test Components
These tests check for WCAG 2.1 AAA compliance, which includes: ### Pa11y Tests
- **Perceivable**: Pa11y tests are run using the `pa11y-test.sh` script, which:
- 7:1 contrast ratio for text
- Text spacing customization
- No loss of content when text is resized
- Audio description for video
- **Operable**: - Tests multiple pages against WCAG 2.1 AAA standards
- No timing constraints - Generates JSON reports for each page
- No interruptions - Creates a summary report
- Multiple ways to find content
- Proper heading structure
- **Understandable**: ### axe-core Tests
- Unusual words are defined
- Abbreviations are expanded
- Reading level appropriate for audience
- Context-sensitive help available
- **Robust**: axe-core tests are run using the `playwright-axe.js` script, which:
- Proper ARIA usage
- Compatibility with assistive technologies
## Test Reports - Uses Playwright to load pages
- Injects axe-core into each page
- Runs accessibility tests against WCAG 2.1 AAA standards
- Generates detailed reports
Reports are saved in the `../reports` directory: ### Manual Testing
- `axe-summary.json`: Summary of axe-core test results 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.
- `pa11y-summary.json`: Summary of Pa11y test results
- `accessibility-summary.json`: Combined summary of all tests
## Manual Testing ## Reports
Some WCAG 2.1 AAA criteria require manual testing. Use the `manual-checklist.md` file to document these tests. Test reports are saved in the `tests/reports/` directory:
## Additional Tools - 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`)
For more comprehensive testing, consider using: ## Requirements
- **Accessibility Insights for Web**: Browser extension for detailed accessibility testing - Node.js
- **NVDA or VoiceOver**: Screen readers for testing screen reader compatibility - NPM packages: pa11y, axe-core, playwright
- **Keyboard-only navigation**: Test all functionality without using a mouse - For Pa11y: `npm install -g pa11y` or use npx

View File

@ -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();

View File

@ -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}`);

View File

@ -1,99 +1,119 @@
# WCAG 2.1 AAA Manual Testing Checklist # Manual Accessibility Testing Checklist
This checklist covers WCAG 2.1 AAA criteria that require manual testing and cannot be fully automated. This checklist covers aspects of WCAG 2.1 AAA compliance that cannot be fully automated and require manual verification.
## Instructions ## Instructions
For each item, mark it as: 1. For each page in the site, go through this checklist
2. Mark each item as:
- ✅ Pass - ✅ Pass
- ❌ Fail - ❌ Fail
- N/A Not Applicable - N/A Not applicable
3. For any failures, note the specific issue and location
## Text Spacing (1.4.12 AAA) ## Pages to Test
- [ ] Text can be displayed with: - Home page (/)
- Line height of at least 1.5 times the font size - Stories index (/stories/)
- Spacing after paragraphs of at least 2 times the font size - Individual story pages (e.g., /stories/open-source-success.html)
- Letter spacing of at least 0.12 times the font size - Tools pages (e.g., /one-pager-tools/csv-tool.html)
- Word spacing of at least 0.16 times the font size
## No Timing (2.2.3 AAA) ## Checklist
- [ ] Timing is not an essential part of the activity presented by the content ### Perceivable
- [ ] No time limits or constraints on user interaction
## Interruptions (2.2.4 AAA) #### 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 - [ ] Interruptions can be postponed or suppressed by the user
- [ ] No unexpected popups or modal dialogs
## Re-authenticating (2.2.5 AAA)
#### 2.2.5 Re-authenticating (AAA)
- [ ] When an authenticated session expires, the user can continue the activity without loss of data after re-authenticating - [ ] When an authenticated session expires, the user can continue the activity without loss of data after re-authenticating
## Three Flashes (2.3.2 AAA) #### 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 - [ ] Web pages do not contain anything that flashes more than three times in any one second period
## Location (2.4.8 AAA) #### 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 - [ ] Information about the user's location within a set of web pages is available
## Link Purpose (2.4.9 AAA) #### 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 - [ ] A mechanism is available to allow the purpose of each link to be identified from link text alone
## Section Headings (2.4.10 AAA) #### 2.4.10 Section Headings (AAA)
- [ ] Section headings are used to organize the content - [ ] Section headings are used to organize the content
- [ ] Headings follow a logical hierarchy (h1, h2, h3, etc.)
## Unusual Words (3.1.3 AAA) #### 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 - [ ] A mechanism is available for identifying specific definitions of words or phrases used in an unusual or restricted way
## Abbreviations (3.1.4 AAA) #### 3.1.4 Abbreviations (AAA)
- [ ] A mechanism for identifying the expanded form or meaning of abbreviations is available - [ ] A mechanism for identifying the expanded form or meaning of abbreviations is available
## Reading Level (3.1.5 AAA) #### 3.1.5 Reading Level (AAA)
- [ ] When text requires reading ability more advanced than the lower secondary education level, supplemental content is available
- [ ] Content does not require reading ability more advanced than the lower secondary education level
- [ ] Supplemental content is available for more complex text
## Pronunciation (3.1.6 AAA)
#### 3.1.6 Pronunciation (AAA)
- [ ] A mechanism is available for identifying specific pronunciation of words where meaning is ambiguous without knowing the pronunciation - [ ] A mechanism is available for identifying specific pronunciation of words where meaning is ambiguous without knowing the pronunciation
## Error Prevention (3.3.6 AAA) #### 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
- [ ] For submissions that cause legal commitments or financial transactions:
- Submissions are reversible
- Data entered is checked for errors
- User can review and confirm before final submission
## Help (3.3.5 AAA)
#### 3.3.5 Help (AAA)
- [ ] Context-sensitive help is available - [ ] Context-sensitive help is available
## Screen Reader Testing #### 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
- [ ] Test with at least one screen reader (e.g., NVDA, VoiceOver) ## Notes and Issues
- [ ] All content can be accessed and understood through screen reader
- [ ] Interactive elements are properly announced
- [ ] Form controls have proper labels and instructions
## User Testing <!-- Document any issues found during manual testing here -->
- [ ] Testing with users with disabilities has been conducted
- [ ] Feedback has been incorporated into the website
## Notes and Observations
(Add any notes or observations here)
## Tester Information ## Tester Information
- Tester Name: - Name:
- Date: - Date:
- Browser/Assistive Technology Used: - Browser/assistive technology used:

View File

@ -1,9 +1,8 @@
#!/bin/bash #!/bin/bash
# ===================================================================== # =====================================================================
# pa11y-test.sh - Test for WCAG 2.1 AAA compliance using Pa11y # pa11y-test.sh - Run Pa11y accessibility tests
# ===================================================================== # =====================================================================
# This script runs Pa11y against all pages of the website to check for # This script runs Pa11y accessibility tests for WCAG 2.1 AAA compliance
# WCAG 2.1 AAA compliance.
# ===================================================================== # =====================================================================
set -e set -e
@ -15,98 +14,87 @@ else
BASE_URL="$1" BASE_URL="$1"
fi fi
# Create reports directory if it doesn't exist TESTS_DIR="$(dirname "$0")"
REPORTS_DIR="$(dirname "$0")/../reports" REPORTS_DIR="$TESTS_DIR/../reports"
mkdir -p "$REPORTS_DIR" mkdir -p "$REPORTS_DIR"
# List of pages to test
PAGES=(
"/"
"/stories/"
"/stories/open-source-success.html"
"/stories/viperwire.html"
"/one-pager-tools/csv-tool.html"
)
echo "=== Testing WCAG 2.1 AAA Compliance with Pa11y ===" echo "=== Testing WCAG 2.1 AAA Compliance with Pa11y ==="
echo "Using base URL: $BASE_URL" echo "Using base URL: $BASE_URL"
# Function to run Pa11y on a single page # Get URLs from sitemap
run_pa11y() { SITEMAP_URL="$BASE_URL/sitemap.xml"
local page="$1" echo "Fetching pages from sitemap: $SITEMAP_URL"
local url="${BASE_URL}${page}" URLS=($(curl -s "$SITEMAP_URL" | grep -o '<loc>.*</loc>' | sed 's/<loc>//;s/<\/loc>//'))
local filename=$(echo "$page" | sed 's/\//-/g' | sed 's/^-//' | sed 's/-$//')
if [ -z "$filename" ]; then if [ ${#URLS[@]} -eq 0 ]; then
filename="index" echo "No URLs found in sitemap, or sitemap could not be fetched."
exit 1
fi fi
echo "Testing $url..." echo "Found ${#URLS[@]} pages to test."
# Run Pa11y with WCAG 2.1 AAA standard # Track failures
if command -v pa11y &> /dev/null; then FAILURES=0
pa11y --standard WCAG2AAA --reporter json "$url" > "$REPORTS_DIR/pa11y-$filename.json" || true
# Count issues # Run tests for each URL
issues=$(jq 'length' "$REPORTS_DIR/pa11y-$filename.json") for url in "${URLS[@]}"; do
echo "Found $issues issues on $url" echo "Testing ${url}..."
# Show summary of issues # Create a report file name from the URL
if [ "$issues" -gt 0 ]; then 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:" echo "Issues summary:"
jq -r '.[] | "- " + .type + ": " + .message' "$REPORTS_DIR/pa11y-$filename.json" | head -n 5 jq -r '.issues[] | "- \(.type): \(.message)\n Selector: \(.selector)"' "${report_file}" 2>/dev/null | head -n 10
if [ "$issues" -gt 5 ]; then # If there are more than 5 issues, indicate that
if [ "${issues}" -gt 5 ]; then
echo "... and $((issues - 5)) more issues." echo "... and $((issues - 5)) more issues."
fi fi
fi fi
return $issues
else else
echo "Pa11y not installed. Install with: npm install -g pa11y" echo "Error running Pa11y on ${url}"
return 1 # 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
} fi
FAILURES=$((FAILURES + 1))
# Run Pa11y on all pages
total_issues=0
failed_pages=0
for page in "${PAGES[@]}"; do
run_pa11y "$page"
issues=$?
if [ "$issues" -gt 0 ]; then
failed_pages=$((failed_pages + 1))
total_issues=$((total_issues + issues))
fi fi
echo "---" echo "---"
done done
# Create summary report # Create summary report
cat > "$REPORTS_DIR/pa11y-summary.json" << EOL jq -n --arg timestamp "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
{ --arg baseUrl "${BASE_URL}" \
"timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")", --arg failures "${FAILURES}" \
"baseUrl": "$BASE_URL", --arg total "${#URLS[@]}" \
'{
"timestamp": $timestamp,
"baseUrl": $baseUrl,
"summary": { "summary": {
"totalPages": ${#PAGES[@]}, "total": ($total | tonumber),
"failedPages": $failed_pages, "failures": ($failures | tonumber),
"totalIssues": $total_issues "passed": (($total | tonumber) - ($failures | tonumber))
} }
} }' > "${REPORTS_DIR}/pa11y-summary.json"
EOL
# Print summary # Exit with appropriate status
echo "=== Pa11y Test Summary ===" if [ "${FAILURES}" -gt 0 ]; then
echo "Total pages tested: ${#PAGES[@]}"
echo "Pages with issues: $failed_pages"
echo "Total issues found: $total_issues"
if [ "$total_issues" -gt 0 ]; then
echo "=== Pa11y Tests Failed ==="
exit 1 exit 1
else else
echo "=== All Pa11y Tests Passed ==="
exit 0 exit 0
fi fi

View File

@ -10,15 +10,8 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const axeCore = require('axe-core'); const axeCore = require('axe-core');
// URLs to test // Base URL to test
const BASE_URL = process.argv[2] || 'http://localhost:8080'; 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 // Create reports directory if it doesn't exist
const reportsDir = path.join(__dirname, '../reports'); const reportsDir = path.join(__dirname, '../reports');
@ -26,16 +19,48 @@ if (!fs.existsSync(reportsDir)) {
fs.mkdirSync(reportsDir, { recursive: true }); 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) { async function runAxe(page, pageUrl) {
// Inject axe-core into the page try {
await page.evaluate(axeSource => { // Use a file URL to load axe-core from a local file to avoid CSP issues
const script = document.createElement('script'); const axeScriptPath = path.join(__dirname, 'axe-core.js');
script.text = axeSource; fs.writeFileSync(axeScriptPath, axeCore.source);
document.head.appendChild(script);
}, axeCore.source); // Add the script as a file
await page.addScriptTag({ path: axeScriptPath, type: 'text/javascript' });
// Run axe with WCAG 2.1 AAA rules // Run axe with WCAG 2.1 AAA rules
const results = await page.evaluate(() => { const results = await page.evaluate(() => {
if (typeof axe === 'undefined') {
return { error: 'axe-core not loaded' };
}
return new Promise(resolve => { return new Promise(resolve => {
axe.run(document, { axe.run(document, {
runOnly: { runOnly: {
@ -52,11 +77,21 @@ async function runAxe(page, pageUrl) {
}); });
}); });
// Clean up temporary file
fs.unlinkSync(axeScriptPath);
return results; return results;
} catch (error) {
console.error('Error running axe:', error);
return { error: error.toString() };
}
} }
async function testPage(browser, pageUrl) { async function testPage(browser, pageUrl) {
const page = await browser.newPage(); const context = await browser.newContext({
bypassCSP: true
});
const page = await context.newPage();
console.log(`Testing ${pageUrl}...`); console.log(`Testing ${pageUrl}...`);
try { try {
@ -89,9 +124,12 @@ async function testPage(browser, pageUrl) {
console.log('\nViolations:'); console.log('\nViolations:');
violations.forEach((violation, i) => { violations.forEach((violation, i) => {
console.log(`${i + 1}. ${violation.id} - ${violation.help} (Impact: ${violation.impact})`); console.log(`${i + 1}. ${violation.id} - ${violation.help} (Impact: ${violation.impact})`);
console.log(` ${violation.description}`); console.log(` Description: ${violation.description}`);
console.log(` WCAG: ${violation.tags.filter(t => t.startsWith('wcag')).join(', ')}`); console.log(` Help: ${violation.helpUrl}`);
console.log(` Elements: ${violation.nodes.length}`); violation.nodes.forEach(node => {
console.log(` - Element: ${node.html}`);
console.log(` Selector: ${node.target.join(', ')}`);
});
}); });
} }
@ -107,6 +145,7 @@ async function testPage(browser, pageUrl) {
return { url: pageUrl, success: false, error: error.toString() }; return { url: pageUrl, success: false, error: error.toString() };
} finally { } finally {
await page.close(); await page.close();
await context.close();
} }
} }
@ -114,10 +153,15 @@ async function runTests() {
const browser = await chromium.launch(); const browser = await chromium.launch();
const results = []; 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 { try {
// Test each page // Test each page
for (const pagePath of PAGES) { for (const pagePath of pagesToTest) {
const pageUrl = `${BASE_URL}${pagePath}`; const pageUrl = `${BASE_URL}${pagePath.startsWith('/') ? '' : '/'}${pagePath}`;
const result = await testPage(browser, pageUrl); const result = await testPage(browser, pageUrl);
results.push(result); results.push(result);
} }

View File

@ -65,7 +65,7 @@ echo "$TESTS_DIR/manual-checklist.md"
# Generate combined report # Generate combined report
echo "Generating combined accessibility report..." echo "Generating combined accessibility report..."
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
cat > "$REPORTS_DIR/accessibility-summary.json" << EOL cat > "$REPORTS_DIR/accessibility-summary.json" << EOJSON
{ {
"timestamp": "$TIMESTAMP", "timestamp": "$TIMESTAMP",
"baseUrl": "$BASE_URL", "baseUrl": "$BASE_URL",
@ -86,7 +86,7 @@ cat > "$REPORTS_DIR/accessibility-summary.json" << EOL
} }
} }
} }
EOL EOJSON
# Print summary # Print summary
echo "=== Accessibility Test Summary ===" echo "=== Accessibility Test Summary ==="

View File

@ -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

View File

@ -1,35 +1,50 @@
#!/bin/bash #!/bin/bash
# ===================================================================== # =====================================================================
# pre-test-setup.sh - Setup for tests # pre-test-setup.sh - Setup environment before running tests
# ===================================================================== # =====================================================================
# This script sets up the environment for testing # This script sets up the environment before running tests:
# 1. Generates sitemap.xml
# 2. Updates CSP hashes
# ===================================================================== # =====================================================================
set -e # Exit on any error set -e
TESTS_DIR="$(dirname "$0")" echo "=== Setting up test environment ==="
RESUME_DIR="$(pwd)/docker/resume"
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 correct directory # Check if we're in the right directory
if [ ! -d "$RESUME_DIR" ]; then if [ ! -d "$RESUME_DIR" ]; then
echo "Error: Could not find the resume directory at $RESUME_DIR" echo "❌ Could not find resume directory at $RESUME_DIR"
echo "Make sure you're running this script from the project root"
exit 1 exit 1
fi fi
# Run the CSP hash update script # Change to the resume directory
echo "Running CSP hash update script..."
cd "$RESUME_DIR" 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 if [ -f "./update-csp-hashes.sh" ]; then
echo "=== Updating CSP hashes ==="
./update-csp-hashes.sh ./update-csp-hashes.sh
else else
echo "Error: Could not find update-csp-hashes.sh script" echo "⚠️ update-csp-hashes.sh not found, skipping CSP hash updates"
exit 1 fi
fi fi
# Return to the original directory echo "=== Test environment setup complete ==="
cd - > /dev/null exit 0
echo "=== Test Environment Setup Complete ==="

View File

@ -1,112 +1,64 @@
#!/bin/bash #!/bin/bash
# ===================================================================== # =====================================================================
# run-all-tests.sh - Run all tests against an existing server # run-all-tests.sh - Run all tests
# ===================================================================== # =====================================================================
# This script runs all tests against an existing server # This script runs all tests in the correct order:
# 1. Run pre-test setup
# 2. Run integration tests
# 3. Run accessibility tests
# ===================================================================== # =====================================================================
set -e # Exit on any error set -e
# Define constants 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" BASE_URL="http://localhost:8080"
TESTS_DIR="$(dirname "$0")" else
BASE_URL="$1"
fi
# Run pre-test setup # Run pre-test setup
echo "Running pre-test setup..." echo "=== Running pre-test setup ==="
if [ -f "$TESTS_DIR/pre-test-setup.sh" ]; then "$SCRIPT_DIR/pre-test-setup.sh"
"$TESTS_DIR/pre-test-setup.sh"
else
echo "Warning: pre-test-setup.sh not found, skipping setup"
fi
# Check if the server is running
echo "Checking if server is running at $BASE_URL..."
if ! curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/" | grep -q "200"; then
echo "Server is not running at $BASE_URL. Please start it using:"
echo "cd docker/resume && ./caddy.sh"
exit 1
fi
echo "Server is running at $BASE_URL"
# 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 # Run integration tests
echo "Running integration tests..." echo "=== Running integration tests ==="
if [ -d "$TESTS_DIR/integration" ] && [ "$(ls -A "$TESTS_DIR/integration")" ]; then for test_script in "$SCRIPT_DIR/integration"/*.sh; do
for test in "$TESTS_DIR/integration"/*.sh; do if [ -f "$test_script" ]; then
if [ -f "$test" ] && [ -x "$test" ]; then echo "Running $test_script..."
echo "Running integration test: $(basename "$test")" if "$test_script" "$BASE_URL"; then
"$test" "$BASE_URL" || echo "FAILED: $(basename "$test")" echo "$(basename "$test_script") passed"
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" "$BASE_URL" || echo "FAILED: $(basename "$test")"
fi
done
fi
done
else else
echo "No integration tests found." echo "$(basename "$test_script") failed"
FAILED_TESTS=$((FAILED_TESTS + 1))
fi 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" "$BASE_URL" || echo "FAILED: $(basename "$test")"
fi fi
done 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 else
echo "No e2e tests found." echo "❌ Accessibility tests failed"
FAILED_TESTS=$((FAILED_TESTS + 1))
fi 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 else
echo "Playwright not found, skipping Playwright tests." echo "⚠️ Accessibility tests not found, skipping"
fi fi
# Run Lighthouse tests if available # Print summary
if [ -f "$TESTS_DIR/lighthouse.js" ]; then echo "=== Test Summary ==="
echo "Running Lighthouse tests..." if [ "$FAILED_TESTS" -gt 0 ]; then
node "$TESTS_DIR/lighthouse.js" "$BASE_URL" || echo "FAILED: Lighthouse tests" echo "$FAILED_TESTS test suites failed"
exit 1
else
echo "✅ All tests passed"
exit 0
fi fi
}
# Main execution
echo "=== Starting Test Suite ==="
run_shell_tests
run_js_tests
echo "=== Test Suite Completed ==="