1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198import { useEffect, useState } from "react";
import { useGlobalStore } from "../utils";
import style from "./AuditRemoteCsvInput.module.scss";
import { Card } from "./Card";
import { StyledLabeledInput } from "./StyledLabeledInput";
import { useDebouncedCallback } from "use-debounce";
import * as API from "aws-amplify/api";
import { MdCheckCircle, MdError } from "react-icons/md";
import { TbAlertTriangle } from "react-icons/tb";
import { AuditPagesInputTable } from "./AuditPagesInputTable";
const URL_SOFT_LIMIT = 10_000;
interface Page {
url: string;
type: "html" | "pdf";
id?: string;
}
interface ChildProps {
csvUrl: string
setCsvUrl: (value: React.SetStateAction<string>) => void
validCsv: boolean
setValidCsv: (value: React.SetStateAction<boolean>) => void
pages: Page[]
setParentPages: (newValue: Page[]) => void; // Callback function prop
}
// test file: https://equalify.app/wp-content/uploads/2026/02/url-import-template.csv
export const AuditRemoteCsvInput: React.FC<ChildProps> = ({ csvUrl, setCsvUrl, validCsv, setValidCsv, setParentPages, pages
}) => {
const { setAnnounceMessage } = useGlobalStore();
const [error, setError] = useState<string | null>("");
/**
* Normalize a URL by removing trailing slashes from the path
* This ensures URLs like "https://example.com/" and "https://example.com" are treated as the same
*/
const normalizeUrl = (url: string): string => {
// Remove trailing slash unless it's just the root path
return url.replace(/\/+$/, '') || url;
};
const validateAndFormatUrl = (input: string): string | null => {
// Trim whitespace
let url = input.trim();
if (!url) return null;
// Add https:// if no protocol is specified
if (!url.match(/^https?:\/\//i)) {
url = "https://" + url;
}
// Validate URL format
try {
const urlObj = new URL(url);
// Check if it's http or https
if (!["http:", "https:"].includes(urlObj.protocol)) {
setError("Only HTTP and HTTPS URLs are supported");
return null;
}
setError(null);
// Normalize the URL to remove trailing slashes
return normalizeUrl(urlObj.href);
} catch {
setError(
"Invalid URL format. Please enter a valid URL (e.g., example.com or https://example.com)"
);
return null;
}
};
/**
* Parse a line that may contain a URL and optional type separated by comma
* Format: url,type (e.g., "https://example.com,html" or "https://example.com/doc.pdf,pdf")
* If no type specified, defaults to "html"
*/
const parseUrlWithType = (line: string): { url: string; type: "html" | "pdf" } => {
const trimmedLine = line.trim();
// Check if the line ends with ,html or ,pdf (case-insensitive)
const htmlMatch = trimmedLine.match(/^(.+),\s*html\s*$/i);
const pdfMatch = trimmedLine.match(/^(.+),\s*pdf\s*$/i);
if (pdfMatch) {
return { url: pdfMatch[1].trim(), type: "pdf" };
}
if (htmlMatch) {
return { url: htmlMatch[1].trim(), type: "html" };
}
// No type specified, default to html
return { url: trimmedLine, type: "html" };
};
const updatePages = (lines: Page[]) => {
const newPages: Page[] = [];
const errors = [];
lines.forEach((line, index) => {
// Parse line for URL and optional type (format: url,type)
const { url: rawUrl, type: pageType } = line;
// Validate and format URL
const validUrl = validateAndFormatUrl(rawUrl);
if (!validUrl) {
errors.push(`Line ${index + 1}: Invalid URL format`);
return;
}
newPages.push({ url: validUrl, type: pageType });
});
//console.log(newPages);
setParentPages(newPages)
}
useEffect(() => {
(async () => {
if (csvUrl.length <= 5) {
setError("");
return;
}
console.log("Fetching and validating CSV...", csvUrl);
const fetchedCsv = await fetchAndValidateCsv();
setError(fetchedCsv.error);
if (fetchedCsv.success) {
setValidCsv(true);
const updatedPages = updatePages(fetchedCsv.data);
}
})()
}, [csvUrl])
const fetchAndValidateCsv = async () => {
const response = await API.get({
apiName: "auth",
path: "/fetchRemoteCsv",
options: {
queryParams: { url: csvUrl.trim() },
},
}).response;
const resp = (await response.body.json()) as any;
console.log(resp);
return resp;
}
return (
<div className={style.AuditRemoteCsvInput}>
<Card variant="inset-light">
<h3>Connect your Wordpress Site to Equalify</h3>
<p className="font-small">
Automatically keep your accessibility audits in sync with your latest WordPress content. Once connected, Equalify will pull all your published pages, posts, and optionally PDF files from a secure CSV feed generated by your site.
</p>
<p className="font-small">
To connect your WordPress site:
<ol>
<li>Install and activate the <a target="_blank" href="https://github.com/EqualifyEverything/equalify-wp-integration">Equalify WordPress Integration plugin</a> on your site.</li>
<li>In your WordPress admin, go to Settings {">"} Equalify Integration.</li>
<li>Copy the CSV Feed URL shown at the top of that page.</li>
<li>Paste it in the field below and save.</li>
</ol>
</p>
<p className="font-small">Equalify will use this feed to discover and audit your URLs. You can control which URLs are included — or exclude specific pages — from the plugin's settings page at any time.</p>
<StyledLabeledInput>
<label htmlFor="remote-csv-url">CSV Feed URL</label>
<input id="remote-csv-url" type="url" value={csvUrl} onChange={(e) => { setCsvUrl(e.target.value) }} />
</StyledLabeledInput>
{error &&
<Card variant="short-error"><MdError className="icon-small" /><div className="font-small"><b>There was a problem with your CSV.</b>{error}</div></Card>
}
{validCsv &&
<Card variant="short-success"><MdCheckCircle className="icon-small" /><div className="font-small"><b>CSV Found!</b></div></Card>
}
{validCsv && pages.length >= URL_SOFT_LIMIT && (
<Card variant="short-error">
<TbAlertTriangle className="icon-small" />
<div className="font-small">
<b>Large audit:</b> This CSV contains {pages.length.toLocaleString()} URLs. Large audits take significantly longer to scan.
</div>
</Card>
)}
{pages.length > 0 &&
<AuditPagesInputTable
pages={pages}
removePages={() => { }}
isShared={true}
updatePageType={() => { }}
/>
}
</Card>
</div>
);
};