5using System.Collections.Generic;
9using System.Text.RegularExpressions;
11using System.Text.Json;
12using Newtonsoft.Json.Linq;
18 public bool Success {
get;
set; }
19 public string RawNextDataJson {
get;
set; } =
string.Empty;
20 public List<string> FlightChunks {
get;
set; } =
new List<string>();
21 public string ParsedStructuredData {
get;
set; } =
string.Empty;
22 public string Url {
get;
set; } =
string.Empty;
23 public string Title {
get;
set; } =
"Untitled";
24 public int HydrationPayloadLength {
get;
set; }
25 public string Message {
get;
set; } =
string.Empty;
26 public bool IsPagesRouter {
get;
set; }
27 public bool IsAppRouter {
get;
set; }
29 private JsonDocument _parsedJson;
31 public JsonDocument GetJson()
33 if (_parsedJson !=
null)
36 if (
string.IsNullOrWhiteSpace(RawNextDataJson))
38 Message =
"RawNextDataJson is empty";
44 _parsedJson = JsonDocument.Parse(RawNextDataJson);
45 Message =
"JSON parsed successfully";
48 catch (JsonException ex)
50 Message = $
"JSON parse failed: [{ex.Message}]";
53 int errorPos = (int)(ex.LineNumber > 0 ? ex.LineNumber : 0);
54 string snippet = RawNextDataJson.Length > 300
55 ? RawNextDataJson.Substring(0, 300) +
"..."
65 Message = $
"Unexpected error parsing JSON: {ex.Message}";
70 public void DisposeJson()
72 _parsedJson?.Dispose();
77 internal static class NextJsHydrationExtractor
81 internal const string HydrationScript =
83 " var nextData = document.querySelector('script#__NEXT_DATA__');" +
84 " var heading = document.querySelector('h1');" +
86 " var scripts = document.querySelectorAll('script');" +
87 " for (var idx = 0; idx < scripts.length; idx++) {" +
88 " var text = scripts[idx].textContent;" +
89 " if (text && text.indexOf('self.__next_f.push') >= 0) chunks.push(text.trim());" +
91 " return JSON.stringify({" +
92 " title: document.title || (heading ? heading.textContent.trim() : null)," +
93 " nextData: nextData ? nextData.textContent.trim() : null," +
94 " flightChunks: chunks" +
109 HydrationPayloadLength = scriptsJson?.Length ?? 0,
110 Url = browser.GetSetCurrentUrl()
113 if (
string.IsNullOrWhiteSpace(scriptsJson))
115 result.Message =
"The page reported nothing to extract";
121 JObject reported = JObject.Parse(scriptsJson);
123 result.Title = (string)reported[
"title"] ??
"Untitled";
125 string nextData = (string)reported[
"nextData"];
127 if (
false ==
string.IsNullOrWhiteSpace(nextData))
129 result.IsPagesRouter =
true;
130 result.RawNextDataJson = nextData;
131 result.ParsedStructuredData = nextData;
132 result.Success =
true;
135 var chunks = reported[
"flightChunks"] as JArray;
137 if (
null != chunks && 0 < chunks.Count)
139 result.IsAppRouter =
true;
140 result.FlightChunks = chunks.Select(chunk => (
string)chunk).ToList();
141 result.ParsedStructuredData = DecodeFlightChunks(result.FlightChunks)
142 ??
string.Join(Environment.NewLine, result.FlightChunks);
143 result.Success =
true;
144 result.Message = $
"Found {result.FlightChunks.Count} React Flight chunks (App Router)";
146 else if (
false == result.Success)
147 result.Message =
"No __NEXT_DATA__ or self.__next_f.push found. Not a detectable Next.js site or SSR disabled.";
151 result.Success =
false;
152 result.Message = ex.Message;
165 internal static string DecodeFlightChunks(List<string> flightScripts)
167 if (flightScripts ==
null || flightScripts.Count == 0)
173 var streamBuilder =
new StringBuilder();
174 foreach (var script
in flightScripts)
175 foreach (var arr
in ExtractPushArrays(script))
179 using (var doc = JsonDocument.Parse(arr))
181 var root = doc.RootElement;
182 if (root.ValueKind == JsonValueKind.Array && root.GetArrayLength() >= 2 &&
183 root[1].ValueKind == JsonValueKind.String)
184 streamBuilder.Append(root[1].GetString());
190 string stream = streamBuilder.ToString();
191 if (
string.IsNullOrWhiteSpace(stream))
195 var data =
new List<string>();
196 var modules =
new List<string>();
197 var other =
new List<string>();
198 var seen =
new HashSet<string>(StringComparer.Ordinal);
200 string UniqueKey(
string id)
202 string k = id;
int n = 1;
203 while (seen.Contains(k)) k =
id +
"_" + (++n);
209 var curContent =
new StringBuilder();
213 if (curId ==
null)
return;
215 string content = curContent.ToString();
216 string key = UniqueKey(curId);
218 if (IsValidJson(content))
219 data.Add($
"\"{key}\":{content}");
220 else if (content.Length > 1 &&
char.IsLetter(content[0]) && IsValidJson(content.Substring(1)))
222 string val = content.Substring(1);
223 if (
'I' == content[0]) modules.Add($
"\"{key}\":{val}");
224 else other.Add($
"\"{key}\":{val}");
227 other.Add($
"\"{key}\":{JsonSerializer.Serialize(content)}");
233 foreach (var line
in stream.Split(
'\n'))
235 int colon = line.IndexOf(
':');
236 string prefix = colon > 0 ? line.Substring(0, colon) :
null;
237 bool isRowStart = prefix !=
null && Regex.IsMatch(prefix,
"^[0-9a-fA-F]+$");
243 curContent.Append(line.Substring(colon + 1));
245 else if (curId !=
null)
248 curContent.Append(
'\n').Append(line);
253 if (0 == data.Count && 0 == modules.Count && 0 == other.Count)
257 var combined =
new StringBuilder();
258 combined.Append(
"{\"data\":{").Append(
string.Join(
",", data)).Append(
"}");
259 combined.Append(
",\"modules\":{").Append(
string.Join(
",", modules)).Append(
"}");
261 combined.Append(
",\"other\":{").Append(
string.Join(
",", other)).Append(
"}");
262 combined.Append(
"}");
264 return PrettyPrintOrRaw(combined.ToString());
276 private static IEnumerable<string> ExtractPushArrays(
string script)
278 if (
string.IsNullOrEmpty(script))
282 while ((idx = script.IndexOf(
"__next_f.push(", idx, StringComparison.Ordinal)) >= 0)
284 int open = script.IndexOf(
'[', idx);
289 bool inStr =
false, esc =
false, closed =
false;
292 for (
int i = open; i < script.Length; i++)
297 if (esc) esc =
false;
298 else if (
'\\' == c) esc =
true;
299 else if (
'"' == c) inStr =
false;
303 if (
'"' == c) inStr =
true;
304 else if (
'[' == c) depth++;
305 else if (
']' == c) { depth--;
if (0 == depth) { end = i; closed =
true;
break; } }
312 yield
return script.Substring(open, end - open + 1);
317 private static bool IsValidJson(
string s)
319 if (
string.IsNullOrWhiteSpace(s))
323 if (
'{' != c &&
'[' != c &&
'"' != c &&
'-' != c &&
false ==
char.IsDigit(c)
324 &&
"tfn".IndexOf(c) < 0)
326 try {
using (JsonDocument.Parse(s))
return true; }
327 catch {
return false; }
330 public static void SaveToFile(NextJsHydrationResult result,
string filePath)
332 var sb =
new System.Text.StringBuilder();
333 sb.AppendLine($
"# Next.js Hydration Data - {result.Title}");
334 sb.AppendLine($
"URL: {result.Url}");
335 sb.AppendLine($
"Type: {DescribeType(result)}");
336 sb.AppendLine($
"Saved: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
337 sb.AppendLine($
"Hydration payload length: {result.HydrationPayloadLength / 1024} KB");
338 sb.AppendLine(
"---");
340 if (result.IsPagesRouter && !
string.IsNullOrEmpty(result.RawNextDataJson))
342 sb.AppendLine(
"## __NEXT_DATA__ (JSON)");
343 sb.AppendLine(PrettyPrintOrRaw(result.RawNextDataJson));
345 else if (result.IsAppRouter)
347 sb.AppendLine($
"## React Flight (decoded from {result.FlightChunks.Count} chunks)");
348 sb.AppendLine(result.ParsedStructuredData);
353 sb.AppendLine(
string.IsNullOrWhiteSpace(result.Message)
354 ?
"No Next.js hydration data (__NEXT_DATA__ or self.__next_f.push) found on this page."
358 File.WriteAllText(filePath, sb.ToString());
362 private static string DescribeType(NextJsHydrationResult result)
364 if (result.IsPagesRouter)
return "Pages Router (__NEXT_DATA__)";
365 if (result.IsAppRouter)
return "App Router (React Flight)";
366 return "None detected (no Next.js SSR hydration data)";
373 private static string PrettyPrintOrRaw(
string json)
377 using (var doc = JsonDocument.Parse(json))
378 return JsonSerializer.Serialize(doc.RootElement,
new JsonSerializerOptions
380 WriteIndented = true,
381 Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
Browser object that contains the fluent methods to create your Browser workflow. Instatiated using G...
Everything starts here, all the GPAL controls and global settings using fluent syntax are here....
static void PublishSimpleEvent(GPALEventType gPALEventType, string msg, dynamic gPALObject=null, Enums.GPALObjectType gPALObjectType=GPALObjectType.None, Exception ex=null)
Publish a message to either the information channel or exception channel (if exception passed in) Pub...