5using System.Collections.Generic;
9using System.Text.RegularExpressions;
12using System.Text.Json;
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 OriginalHtmlLength {
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
83 OriginalHtmlLength = htmlSource?.Length ?? 0,
84 Url = browser.GetSetCurrentUrl()
87 if (
string.IsNullOrWhiteSpace(htmlSource))
89 result.Message =
"Empty HTML";
95 var doc =
new HtmlDocument();
96 doc.LoadHtml(htmlSource);
99 result.Title = ExtractTitle(doc) ??
"Untitled";
102 var nextDataNode = doc.DocumentNode.SelectSingleNode(
"//script[@id='__NEXT_DATA__']");
103 if (nextDataNode !=
null)
105 result.IsPagesRouter =
true;
106 result.RawNextDataJson = nextDataNode.InnerText.Trim();
107 result.ParsedStructuredData = result.RawNextDataJson;
108 result.Success =
true;
112 var flightScripts = doc.DocumentNode.SelectNodes(
"//script")
113 ?.Where(s => s.InnerText?.Contains(
"self.__next_f.push") ==
true)
114 ?.Select(s => s.InnerText.Trim())
115 ?.ToList() ??
new List<string>();
117 if (flightScripts.Any())
119 result.IsAppRouter =
true;
120 result.FlightChunks = flightScripts;
124 result.ParsedStructuredData = DecodeFlightChunks(flightScripts) ??
string.Join(
"\n", flightScripts);
126 result.Success =
true;
127 result.Message = $
"Found {flightScripts.Count} React Flight chunks (App Router)";
131 result.Message =
"No __NEXT_DATA__ or self.__next_f.push found. Not a detectable Next.js site or SSR disabled.";
136 result.Success =
false;
137 result.Message = ex.Message;
143 private static string ExtractTitle(HtmlDocument doc)
145 return doc.DocumentNode.SelectSingleNode(
"//title")?.InnerText.Trim()
146 ?? doc.DocumentNode.SelectSingleNode(
"//h1")?.InnerText.Trim();
157 internal static string DecodeFlightChunks(List<string> flightScripts)
159 if (flightScripts ==
null || flightScripts.Count == 0)
165 var streamBuilder =
new StringBuilder();
166 foreach (var script
in flightScripts)
167 foreach (var arr
in ExtractPushArrays(script))
171 using (var doc = JsonDocument.Parse(arr))
173 var root = doc.RootElement;
174 if (root.ValueKind == JsonValueKind.Array && root.GetArrayLength() >= 2 &&
175 root[1].ValueKind == JsonValueKind.String)
176 streamBuilder.Append(root[1].GetString());
182 string stream = streamBuilder.ToString();
183 if (
string.IsNullOrWhiteSpace(stream))
187 var data =
new List<string>();
188 var modules =
new List<string>();
189 var other =
new List<string>();
190 var seen =
new HashSet<string>(StringComparer.Ordinal);
192 string UniqueKey(
string id)
194 string k = id;
int n = 1;
195 while (seen.Contains(k)) k =
id +
"_" + (++n);
201 var curContent =
new StringBuilder();
205 if (curId ==
null)
return;
207 string content = curContent.ToString();
208 string key = UniqueKey(curId);
210 if (IsValidJson(content))
211 data.Add($
"\"{key}\":{content}");
212 else if (content.Length > 1 &&
char.IsLetter(content[0]) && IsValidJson(content.Substring(1)))
214 string val = content.Substring(1);
215 if (
'I' == content[0]) modules.Add($
"\"{key}\":{val}");
216 else other.Add($
"\"{key}\":{val}");
219 other.Add($
"\"{key}\":{JsonSerializer.Serialize(content)}");
225 foreach (var line
in stream.Split(
'\n'))
227 int colon = line.IndexOf(
':');
228 string prefix = colon > 0 ? line.Substring(0, colon) :
null;
229 bool isRowStart = prefix !=
null && Regex.IsMatch(prefix,
"^[0-9a-fA-F]+$");
235 curContent.Append(line.Substring(colon + 1));
237 else if (curId !=
null)
240 curContent.Append(
'\n').Append(line);
245 if (0 == data.Count && 0 == modules.Count && 0 == other.Count)
249 var combined =
new StringBuilder();
250 combined.Append(
"{\"data\":{").Append(
string.Join(
",", data)).Append(
"}");
251 combined.Append(
",\"modules\":{").Append(
string.Join(
",", modules)).Append(
"}");
253 combined.Append(
",\"other\":{").Append(
string.Join(
",", other)).Append(
"}");
254 combined.Append(
"}");
256 return PrettyPrintOrRaw(combined.ToString());
268 private static IEnumerable<string> ExtractPushArrays(
string script)
270 if (
string.IsNullOrEmpty(script))
274 while ((idx = script.IndexOf(
"__next_f.push(", idx, StringComparison.Ordinal)) >= 0)
276 int open = script.IndexOf(
'[', idx);
281 bool inStr =
false, esc =
false, closed =
false;
284 for (
int i = open; i < script.Length; i++)
289 if (esc) esc =
false;
290 else if (
'\\' == c) esc =
true;
291 else if (
'"' == c) inStr =
false;
295 if (
'"' == c) inStr =
true;
296 else if (
'[' == c) depth++;
297 else if (
']' == c) { depth--;
if (0 == depth) { end = i; closed =
true;
break; } }
304 yield
return script.Substring(open, end - open + 1);
309 private static bool IsValidJson(
string s)
311 if (
string.IsNullOrWhiteSpace(s))
315 if (
'{' != c &&
'[' != c &&
'"' != c &&
'-' != c &&
false ==
char.IsDigit(c)
316 &&
"tfn".IndexOf(c) < 0)
318 try {
using (JsonDocument.Parse(s))
return true; }
319 catch {
return false; }
322 public static void SaveToFile(NextJsHydrationResult result,
string filePath)
324 var sb =
new System.Text.StringBuilder();
325 sb.AppendLine($
"# Next.js Hydration Data - {result.Title}");
326 sb.AppendLine($
"URL: {result.Url}");
327 sb.AppendLine($
"Type: {DescribeType(result)}");
328 sb.AppendLine($
"Saved: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
329 sb.AppendLine($
"Original HTML length: {result.OriginalHtmlLength / 1024} KB");
330 sb.AppendLine(
"---");
332 if (result.IsPagesRouter && !
string.IsNullOrEmpty(result.RawNextDataJson))
334 sb.AppendLine(
"## __NEXT_DATA__ (JSON)");
335 sb.AppendLine(PrettyPrintOrRaw(result.RawNextDataJson));
337 else if (result.IsAppRouter)
339 sb.AppendLine($
"## React Flight (decoded from {result.FlightChunks.Count} chunks)");
340 sb.AppendLine(result.ParsedStructuredData);
345 sb.AppendLine(
string.IsNullOrWhiteSpace(result.Message)
346 ?
"No Next.js hydration data (__NEXT_DATA__ or self.__next_f.push) found on this page."
350 File.WriteAllText(filePath, sb.ToString());
354 private static string DescribeType(NextJsHydrationResult result)
356 if (result.IsPagesRouter)
return "Pages Router (__NEXT_DATA__)";
357 if (result.IsAppRouter)
return "App Router (React Flight)";
358 return "None detected (no Next.js SSR hydration data)";
365 private static string PrettyPrintOrRaw(
string json)
369 using (var doc = JsonDocument.Parse(json))
370 return JsonSerializer.Serialize(doc.RootElement,
new JsonSerializerOptions
372 WriteIndented = true,
373 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...