@nuxt/scripts 1.0.0-beta.21 → 1.0.0-beta.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/client/200.html +1 -1
  2. package/dist/client/404.html +1 -1
  3. package/dist/client/_nuxt/{BlmrFwhD.js → B7aPLMNo.js} +1 -1
  4. package/dist/client/_nuxt/{DvbTvDd0.js → BNNMZFwZ.js} +21 -21
  5. package/dist/client/_nuxt/{BwCYQWJt.js → Bh9fd9qr.js} +1 -1
  6. package/dist/client/_nuxt/{ZrewjUYk.js → UTi7FhVv.js} +1 -1
  7. package/dist/client/_nuxt/builds/latest.json +1 -1
  8. package/dist/client/_nuxt/builds/meta/7b372941-1db0-4ea4-80d2-a41f53088a98.json +1 -0
  9. package/dist/client/_nuxt/error-404.DMdWw4vT.css +1 -0
  10. package/dist/client/_nuxt/error-500.CROTF27X.css +1 -0
  11. package/dist/client/index.html +1 -1
  12. package/dist/module.d.mts +2 -2
  13. package/dist/module.d.ts +2 -2
  14. package/dist/module.json +1 -1
  15. package/dist/module.mjs +57 -50
  16. package/dist/registry.mjs +30 -4
  17. package/dist/runtime/components/GoogleMaps/ScriptGoogleMaps.d.vue.ts +3 -3
  18. package/dist/runtime/components/GoogleMaps/ScriptGoogleMaps.vue +14 -1
  19. package/dist/runtime/components/GoogleMaps/ScriptGoogleMaps.vue.d.ts +3 -3
  20. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsAdvancedMarkerElement.d.vue.ts +2 -2
  21. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsAdvancedMarkerElement.vue.d.ts +2 -2
  22. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsMarker.d.vue.ts +2 -2
  23. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsMarker.vue.d.ts +2 -2
  24. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsPolygon.d.vue.ts +2 -2
  25. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsPolygon.vue.d.ts +2 -2
  26. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsPolyline.d.vue.ts +2 -2
  27. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsPolyline.vue.d.ts +2 -2
  28. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsRectangle.d.vue.ts +2 -2
  29. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsRectangle.vue.d.ts +2 -2
  30. package/dist/runtime/components/ScriptBlueskyEmbed.d.vue.ts +87 -0
  31. package/dist/runtime/components/ScriptBlueskyEmbed.vue +85 -0
  32. package/dist/runtime/components/ScriptBlueskyEmbed.vue.d.ts +87 -0
  33. package/dist/runtime/components/ScriptInstagramEmbed.d.vue.ts +2 -2
  34. package/dist/runtime/components/ScriptInstagramEmbed.vue +4 -1
  35. package/dist/runtime/components/ScriptInstagramEmbed.vue.d.ts +2 -2
  36. package/dist/runtime/components/ScriptXEmbed.d.vue.ts +2 -2
  37. package/dist/runtime/components/ScriptXEmbed.vue +5 -2
  38. package/dist/runtime/components/ScriptXEmbed.vue.d.ts +2 -2
  39. package/dist/runtime/registry/bluesky-embed.d.ts +116 -0
  40. package/dist/runtime/registry/bluesky-embed.js +72 -0
  41. package/dist/runtime/registry/gravatar.js +2 -2
  42. package/dist/runtime/registry/schemas.d.ts +20 -3
  43. package/dist/runtime/registry/schemas.js +20 -3
  44. package/dist/runtime/registry/x-embed.js +1 -1
  45. package/dist/runtime/server/bluesky-embed-image.d.ts +2 -0
  46. package/dist/runtime/server/bluesky-embed-image.js +7 -0
  47. package/dist/runtime/server/bluesky-embed.d.ts +16 -0
  48. package/dist/runtime/server/bluesky-embed.js +59 -0
  49. package/dist/runtime/server/google-maps-geocode-proxy.d.ts +2 -0
  50. package/dist/runtime/server/google-maps-geocode-proxy.js +34 -0
  51. package/dist/runtime/server/google-static-maps-proxy.js +1 -12
  52. package/dist/runtime/server/gravatar-proxy.js +1 -16
  53. package/dist/runtime/server/instagram-embed-asset.js +8 -42
  54. package/dist/runtime/server/instagram-embed-image.js +6 -54
  55. package/dist/runtime/server/instagram-embed.d.ts +16 -0
  56. package/dist/runtime/server/instagram-embed.js +171 -42
  57. package/dist/runtime/server/utils/image-proxy.d.ts +12 -0
  58. package/dist/runtime/server/utils/image-proxy.js +70 -0
  59. package/dist/runtime/server/x-embed-image.js +5 -49
  60. package/dist/runtime/types.d.ts +11 -0
  61. package/dist/runtime/utils.d.ts +1 -0
  62. package/dist/runtime/utils.js +11 -2
  63. package/dist/stats.mjs +1 -1
  64. package/dist/types-source.mjs +18 -6
  65. package/package.json +8 -7
  66. package/dist/client/_nuxt/builds/meta/6660a023-888d-415f-b66d-ce774e6f8f11.json +0 -1
  67. package/dist/client/_nuxt/error-404.CHeaW3dp.css +0 -1
  68. package/dist/client/_nuxt/error-500.DvOvWme_.css +0 -1
@@ -1,4 +1,21 @@
1
1
  import { any, array, boolean, custom, literal, minLength, number, object, optional, pipe, record, string, union } from "#nuxt-scripts-validator";
2
+ export const BlueskyEmbedOptions = object({
3
+ /**
4
+ * The Bluesky post URL to embed.
5
+ * @example 'https://bsky.app/profile/bsky.app/post/3mgnwwvj3u22a'
6
+ */
7
+ postUrl: string(),
8
+ /**
9
+ * Custom API endpoint for fetching post data.
10
+ * @default '/_scripts/embed/bluesky'
11
+ */
12
+ apiEndpoint: optional(string()),
13
+ /**
14
+ * Custom image proxy endpoint.
15
+ * @default '/_scripts/embed/bluesky-image'
16
+ */
17
+ imageProxyEndpoint: optional(string())
18
+ });
2
19
  export const ClarityOptions = object({
3
20
  /**
4
21
  * The Clarity token.
@@ -428,7 +445,7 @@ export const InstagramEmbedOptions = object({
428
445
  captions: optional(boolean()),
429
446
  /**
430
447
  * Custom API endpoint for fetching embed HTML.
431
- * @default '/api/_scripts/instagram-embed'
448
+ * @default '/_scripts/embed/instagram'
432
449
  */
433
450
  apiEndpoint: optional(string())
434
451
  });
@@ -826,12 +843,12 @@ export const XEmbedOptions = object({
826
843
  tweetId: string(),
827
844
  /**
828
845
  * Optional: Custom API endpoint for fetching tweet data.
829
- * @default '/api/_scripts/x-embed'
846
+ * @default '/_scripts/embed/x'
830
847
  */
831
848
  apiEndpoint: optional(string()),
832
849
  /**
833
850
  * Optional: Custom image proxy endpoint.
834
- * @default '/api/_scripts/x-embed-image'
851
+ * @default '/_scripts/embed/x-image'
835
852
  */
836
853
  imageProxyEndpoint: optional(string())
837
854
  });
@@ -1,6 +1,6 @@
1
1
  import { XEmbedOptions } from "./schemas.js";
2
2
  export { XEmbedOptions };
3
- export function proxyXImageUrl(url, proxyEndpoint = "/api/_scripts/x-embed-image") {
3
+ export function proxyXImageUrl(url, proxyEndpoint = "/_scripts/embed/x-image") {
4
4
  const separator = proxyEndpoint.includes("?") ? "&" : "?";
5
5
  return `${proxyEndpoint}${separator}url=${encodeURIComponent(url)}`;
6
6
  }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
2
+ export default _default;
@@ -0,0 +1,7 @@
1
+ import { createImageProxyHandler } from "./utils/image-proxy.js";
2
+ export default createImageProxyHandler({
3
+ allowedDomains: [
4
+ "cdn.bsky.app",
5
+ "av-cdn.bsky.app"
6
+ ]
7
+ });
@@ -0,0 +1,16 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ uri: string;
3
+ cid: string;
4
+ author: Record<string, any>;
5
+ record: Record<string, any>;
6
+ embed?: Record<string, any>;
7
+ likeCount: number;
8
+ repostCount: number;
9
+ replyCount: number;
10
+ quoteCount: number;
11
+ indexedAt: string;
12
+ labels: Array<{
13
+ val: string;
14
+ }>;
15
+ }>>;
16
+ export default _default;
@@ -0,0 +1,59 @@
1
+ import { createError, defineEventHandler, getQuery, setHeader } from "h3";
2
+ import { $fetch } from "ofetch";
3
+ const BSKY_POST_URL_RE = /^https:\/\/bsky\.app\/profile\/([^/]+)\/post\/([^/?]+)$/;
4
+ export default defineEventHandler(async (event) => {
5
+ const query = getQuery(event);
6
+ const postUrl = query.url;
7
+ if (!postUrl) {
8
+ throw createError({
9
+ statusCode: 400,
10
+ statusMessage: "Post URL is required"
11
+ });
12
+ }
13
+ const match = postUrl.match(BSKY_POST_URL_RE);
14
+ if (!match) {
15
+ throw createError({
16
+ statusCode: 400,
17
+ statusMessage: "Invalid Bluesky post URL"
18
+ });
19
+ }
20
+ const [, actor, rkey] = match;
21
+ let did = actor;
22
+ if (!actor.startsWith("did:")) {
23
+ const profile = await $fetch(
24
+ `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(actor)}`
25
+ ).catch((error) => {
26
+ throw createError({
27
+ statusCode: error.statusCode || 500,
28
+ statusMessage: "Failed to resolve Bluesky handle"
29
+ });
30
+ });
31
+ did = profile.did;
32
+ }
33
+ const uri = `at://${did}/app.bsky.feed.post/${rkey}`;
34
+ const response = await $fetch(
35
+ `https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${encodeURIComponent(uri)}&depth=0&parentHeight=0`
36
+ ).catch((error) => {
37
+ throw createError({
38
+ statusCode: error.statusCode || 500,
39
+ statusMessage: error.statusMessage || "Failed to fetch Bluesky post"
40
+ });
41
+ });
42
+ if (response.thread.$type !== "app.bsky.feed.defs#threadViewPost") {
43
+ throw createError({
44
+ statusCode: 404,
45
+ statusMessage: "Post not found"
46
+ });
47
+ }
48
+ const post = response.thread.post;
49
+ const hasOptOut = post.labels?.some((l) => l.val === "!no-unauthenticated") || post.author?.labels?.some((l) => l.val === "!no-unauthenticated");
50
+ if (hasOptOut) {
51
+ throw createError({
52
+ statusCode: 403,
53
+ statusMessage: "Author has opted out of external embedding"
54
+ });
55
+ }
56
+ setHeader(event, "Content-Type", "application/json");
57
+ setHeader(event, "Cache-Control", "public, max-age=600, s-maxage=600");
58
+ return post;
59
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
2
+ export default _default;
@@ -0,0 +1,34 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ import { createError, defineEventHandler, getQuery, setHeader } from "h3";
3
+ import { $fetch } from "ofetch";
4
+ import { withQuery } from "ufo";
5
+ export default defineEventHandler(async (event) => {
6
+ const runtimeConfig = useRuntimeConfig();
7
+ const privateConfig = runtimeConfig["nuxt-scripts"]?.googleMapsGeocodeProxy;
8
+ const apiKey = privateConfig?.apiKey;
9
+ if (!apiKey) {
10
+ throw createError({
11
+ statusCode: 500,
12
+ statusMessage: "Google Maps API key not configured for geocode proxy"
13
+ });
14
+ }
15
+ const query = getQuery(event);
16
+ const { key: _clientKey, ...safeQuery } = query;
17
+ const geocodeUrl = withQuery("https://maps.googleapis.com/maps/api/geocode/json", {
18
+ ...safeQuery,
19
+ key: apiKey
20
+ });
21
+ const data = await $fetch(geocodeUrl, {
22
+ headers: {
23
+ "User-Agent": "Nuxt Scripts Google Geocode Proxy"
24
+ }
25
+ }).catch((error) => {
26
+ throw createError({
27
+ statusCode: error.statusCode || 500,
28
+ statusMessage: error.statusMessage || "Failed to geocode"
29
+ });
30
+ });
31
+ setHeader(event, "Content-Type", "application/json");
32
+ setHeader(event, "Cache-Control", "public, max-age=86400, s-maxage=86400");
33
+ return data;
34
+ });
@@ -1,5 +1,5 @@
1
1
  import { useRuntimeConfig } from "#imports";
2
- import { createError, defineEventHandler, getHeader, getQuery, setHeader } from "h3";
2
+ import { createError, defineEventHandler, getQuery, setHeader } from "h3";
3
3
  import { $fetch } from "ofetch";
4
4
  import { withQuery } from "ufo";
5
5
  export default defineEventHandler(async (event) => {
@@ -19,17 +19,6 @@ export default defineEventHandler(async (event) => {
19
19
  statusMessage: "Google Maps API key not configured for proxy"
20
20
  });
21
21
  }
22
- const referer = getHeader(event, "referer");
23
- const host = getHeader(event, "host");
24
- if (referer && host) {
25
- const refererUrl = new URL(referer).host;
26
- if (refererUrl !== host) {
27
- throw createError({
28
- statusCode: 403,
29
- statusMessage: "Invalid referer"
30
- });
31
- }
32
- }
33
22
  const query = getQuery(event);
34
23
  const { key: _clientKey, ...safeQuery } = query;
35
24
  const googleMapsUrl = withQuery("https://maps.googleapis.com/maps/api/staticmap", {
@@ -1,25 +1,10 @@
1
1
  import { useRuntimeConfig } from "#imports";
2
- import { createError, defineEventHandler, getHeader, getQuery, setHeader } from "h3";
2
+ import { createError, defineEventHandler, getQuery, setHeader } from "h3";
3
3
  import { $fetch } from "ofetch";
4
4
  import { withQuery } from "ufo";
5
5
  export default defineEventHandler(async (event) => {
6
6
  const runtimeConfig = useRuntimeConfig();
7
7
  const proxyConfig = runtimeConfig.public["nuxt-scripts"]?.gravatarProxy;
8
- const referer = getHeader(event, "referer");
9
- const host = getHeader(event, "host");
10
- if (referer && host) {
11
- let refererHost;
12
- try {
13
- refererHost = new URL(referer).host;
14
- } catch {
15
- }
16
- if (refererHost && refererHost !== host) {
17
- throw createError({
18
- statusCode: 403,
19
- statusMessage: "Invalid referer"
20
- });
21
- }
22
- }
23
8
  const query = getQuery(event);
24
9
  let hash = query.hash;
25
10
  const email = query.email;
@@ -1,43 +1,9 @@
1
- import { createError, defineEventHandler, getQuery, setHeader } from "h3";
2
- import { $fetch } from "ofetch";
3
- const AMP_RE = /&amp;/g;
4
- export default defineEventHandler(async (event) => {
5
- const query = getQuery(event);
6
- const url = query.url?.replace(AMP_RE, "&");
7
- if (!url) {
8
- throw createError({
9
- statusCode: 400,
10
- statusMessage: "Asset URL is required"
11
- });
12
- }
13
- let parsedUrl;
14
- try {
15
- parsedUrl = new URL(url);
16
- } catch {
17
- throw createError({
18
- statusCode: 400,
19
- statusMessage: "Invalid asset URL"
20
- });
21
- }
22
- if (parsedUrl.hostname !== "static.cdninstagram.com") {
23
- throw createError({
24
- statusCode: 403,
25
- statusMessage: "Domain not allowed"
26
- });
27
- }
28
- const response = await $fetch.raw(url, {
29
- headers: {
30
- "Accept": "*/*",
31
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
32
- }
33
- }).catch((error) => {
34
- throw createError({
35
- statusCode: error.statusCode || 500,
36
- statusMessage: error.statusMessage || "Failed to fetch asset"
37
- });
38
- });
39
- const contentType = response.headers.get("content-type") || "application/octet-stream";
40
- setHeader(event, "Content-Type", contentType);
41
- setHeader(event, "Cache-Control", "public, max-age=86400, s-maxage=86400");
42
- return response._data;
1
+ import { createImageProxyHandler } from "./utils/image-proxy.js";
2
+ export default createImageProxyHandler({
3
+ allowedDomains: ["static.cdninstagram.com"],
4
+ accept: "*/*",
5
+ userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
6
+ cacheMaxAge: 86400,
7
+ contentType: "application/octet-stream",
8
+ decodeAmpersands: true
43
9
  });
@@ -1,55 +1,7 @@
1
- import { createError, defineEventHandler, getQuery, setHeader } from "h3";
2
- import { $fetch } from "ofetch";
3
- const AMP_RE = /&amp;/g;
4
- export default defineEventHandler(async (event) => {
5
- const query = getQuery(event);
6
- const url = query.url?.replace(AMP_RE, "&");
7
- if (!url) {
8
- throw createError({
9
- statusCode: 400,
10
- statusMessage: "Image URL is required"
11
- });
12
- }
13
- let parsedUrl;
14
- try {
15
- parsedUrl = new URL(url);
16
- } catch {
17
- throw createError({
18
- statusCode: 400,
19
- statusMessage: "Invalid image URL"
20
- });
21
- }
22
- if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
23
- throw createError({
24
- statusCode: 400,
25
- statusMessage: "Invalid URL scheme"
26
- });
27
- }
28
- if (!parsedUrl.hostname.endsWith(".cdninstagram.com") && parsedUrl.hostname !== "scontent.cdninstagram.com") {
29
- throw createError({
30
- statusCode: 403,
31
- statusMessage: "Domain not allowed"
32
- });
33
- }
34
- const response = await $fetch.raw(url, {
35
- redirect: "manual",
36
- headers: {
37
- "Accept": "image/webp,image/jpeg,image/png,image/*,*/*;q=0.8",
38
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
39
- }
40
- }).catch((error) => {
41
- throw createError({
42
- statusCode: error.statusCode || 500,
43
- statusMessage: error.statusMessage || "Failed to fetch image"
44
- });
45
- });
46
- if (response.status >= 300 && response.status < 400) {
47
- throw createError({
48
- statusCode: 403,
49
- statusMessage: "Redirects not allowed"
50
- });
51
- }
52
- setHeader(event, "Content-Type", response.headers.get("content-type") || "image/jpeg");
53
- setHeader(event, "Cache-Control", "public, max-age=3600, s-maxage=3600");
54
- return response._data;
1
+ import { createImageProxyHandler } from "./utils/image-proxy.js";
2
+ export default createImageProxyHandler({
3
+ allowedDomains: (hostname) => hostname.endsWith(".cdninstagram.com") || hostname === "scontent.cdninstagram.com",
4
+ userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
5
+ followRedirects: false,
6
+ decodeAmpersands: true
55
7
  });
@@ -1,2 +1,18 @@
1
+ export declare const RSRC_RE: RegExp;
2
+ export declare const AMP_RE: RegExp;
3
+ export declare const SCONTENT_RE: RegExp;
4
+ export declare const STATIC_CDN_RE: RegExp;
5
+ export declare const LOOKASIDE_RE: RegExp;
6
+ export declare const INSTAGRAM_IMAGE_HOSTS: string[];
7
+ export declare const INSTAGRAM_ASSET_HOST = "static.cdninstagram.com";
8
+ export declare function proxyImageUrl(url: string): string;
9
+ export declare function proxyAssetUrl(url: string): string;
10
+ export declare function rewriteUrl(url: string): string;
11
+ export declare function rewriteUrlsInText(text: string): string;
12
+ /**
13
+ * Scope CSS rules under a parent selector and strip global/page-level rules.
14
+ * Removes :root, html, body selectors and @charset/@import at-rules.
15
+ */
16
+ export declare function scopeCss(css: string, scopeSelector: string): string;
1
17
  declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string>>;
2
18
  export default _default;
@@ -1,16 +1,128 @@
1
1
  import { createError, defineEventHandler, getQuery, setHeader } from "h3";
2
2
  import { $fetch } from "ofetch";
3
- const LINK_RE = /<link[^>]+rel=["']stylesheet["'][^>]+href=["']([^"']+)["'][^>]*>/gi;
4
- const LINK_RE_2 = /<link[^>]+href=["']([^"']+)["'][^>]+rel=["']stylesheet["'][^>]*>/gi;
5
- const RSRC_RE = /url\(\/rsrc\.php([^)]+)\)/g;
6
- const SCRIPT_RE = /<script[\s\S]*?<\/script>/gi;
7
- const STYLESHEET_RE = /<link[^>]+rel=["']stylesheet["'][^>]*>/gi;
8
- const CSS_RE = /<link[^>]+href=["'][^"']+\.css[^"']*["'][^>]*>/gi;
9
- const NOSCRIPT_RE = /<noscript>[\s\S]*?<\/noscript>/gi;
10
- const SCONTENT_RE = /https:\/\/scontent[^"'\s),]+\.cdninstagram\.com[^"'\s),]+/g;
11
- const STATIC_CDN_RE = /https:\/\/static\.cdninstagram\.com[^"'\s),]+/g;
12
- const LOOKASIDE_RE = /https:\/\/lookaside\.instagram\.com[^"'\s),]+/g;
13
- const AMP_RE = /&amp;/g;
3
+ import { ELEMENT_NODE, parse, renderSync, TEXT_NODE, walkSync } from "ultrahtml";
4
+ export const RSRC_RE = /url\(\/rsrc\.php([^)]+)\)/g;
5
+ export const AMP_RE = /&amp;/g;
6
+ export const SCONTENT_RE = /https:\/\/scontent[^"'\s),]+\.cdninstagram\.com[^"'\s),]+/g;
7
+ export const STATIC_CDN_RE = /https:\/\/static\.cdninstagram\.com[^"'\s),]+/g;
8
+ export const LOOKASIDE_RE = /https:\/\/lookaside\.instagram\.com[^"'\s),]+/g;
9
+ export const INSTAGRAM_IMAGE_HOSTS = ["scontent.cdninstagram.com", "lookaside.instagram.com"];
10
+ export const INSTAGRAM_ASSET_HOST = "static.cdninstagram.com";
11
+ const CHARSET_RE = /@charset\s[^;]+;/gi;
12
+ const IMPORT_RE = /@import\s[^;]+;/gi;
13
+ const WHITESPACE_RE = /\s/;
14
+ const AT_RULE_NAME_RE = /@([\w-]+)/;
15
+ const MULTI_SPACE_RE = /\s+/g;
16
+ const SRCSET_SPLIT_RE = /\s+/;
17
+ export function proxyImageUrl(url) {
18
+ return `/_scripts/embed/instagram-image?url=${encodeURIComponent(url.replace(AMP_RE, "&"))}`;
19
+ }
20
+ export function proxyAssetUrl(url) {
21
+ return `/_scripts/embed/instagram-asset?url=${encodeURIComponent(url.replace(AMP_RE, "&"))}`;
22
+ }
23
+ export function rewriteUrl(url) {
24
+ try {
25
+ const parsed = new URL(url);
26
+ if (parsed.hostname === INSTAGRAM_ASSET_HOST)
27
+ return proxyAssetUrl(url);
28
+ if (INSTAGRAM_IMAGE_HOSTS.some((h) => parsed.hostname === h || parsed.hostname.endsWith(`.cdninstagram.com`)))
29
+ return proxyImageUrl(url);
30
+ } catch {
31
+ }
32
+ return url;
33
+ }
34
+ export function rewriteUrlsInText(text) {
35
+ return text.replace(SCONTENT_RE, (m) => proxyImageUrl(m)).replace(STATIC_CDN_RE, (m) => proxyAssetUrl(m)).replace(LOOKASIDE_RE, (m) => proxyImageUrl(m));
36
+ }
37
+ function removeNode(node) {
38
+ node.type = TEXT_NODE;
39
+ node.value = "";
40
+ node.name = void 0;
41
+ node.attributes = {};
42
+ node.children = [];
43
+ }
44
+ export function scopeCss(css, scopeSelector) {
45
+ let result = css.replace(CHARSET_RE, "");
46
+ result = result.replace(IMPORT_RE, "");
47
+ return processRules(result, scopeSelector);
48
+ }
49
+ function processRules(css, scopeSelector) {
50
+ const output = [];
51
+ let i = 0;
52
+ while (i < css.length) {
53
+ while (i < css.length && WHITESPACE_RE.test(css[i])) i++;
54
+ if (i >= css.length)
55
+ break;
56
+ if (css[i] === "@") {
57
+ const atRule = extractAtRule(css, i);
58
+ if (atRule) {
59
+ const atName = atRule.content.match(AT_RULE_NAME_RE)?.[1]?.toLowerCase();
60
+ if (atName === "media" || atName === "supports" || atName === "layer") {
61
+ const braceStart = atRule.content.indexOf("{");
62
+ const innerCss = atRule.content.slice(braceStart + 1, -1);
63
+ const scopedInner = processRules(innerCss, scopeSelector);
64
+ output.push(`${atRule.content.slice(0, braceStart + 1) + scopedInner}}`);
65
+ } else if (atName === "keyframes" || atName === "-webkit-keyframes" || atName === "font-face") {
66
+ output.push(atRule.content);
67
+ }
68
+ i = atRule.end;
69
+ continue;
70
+ }
71
+ }
72
+ const bracePos = css.indexOf("{", i);
73
+ if (bracePos === -1)
74
+ break;
75
+ const selector = css.slice(i, bracePos).trim();
76
+ const block = extractBlock(css, bracePos);
77
+ if (!block)
78
+ break;
79
+ i = block.end;
80
+ if (!selector)
81
+ continue;
82
+ const selectors = selector.split(",").map((s) => s.trim());
83
+ const filteredSelectors = selectors.filter((s) => {
84
+ const normalized = s.replace(MULTI_SPACE_RE, " ").trim().toLowerCase();
85
+ return normalized !== ":root" && normalized !== "html" && normalized !== "body" && !normalized.startsWith(":root ") && !normalized.startsWith("html ") && !normalized.startsWith("body ") && normalized !== "html, body";
86
+ });
87
+ if (filteredSelectors.length === 0)
88
+ continue;
89
+ const scopedSelectors = filteredSelectors.map((s) => {
90
+ return `${scopeSelector} ${s}`;
91
+ });
92
+ output.push(`${scopedSelectors.join(", ")} ${block.content}`);
93
+ }
94
+ return output.join("\n");
95
+ }
96
+ function extractAtRule(css, start) {
97
+ const bracePos = css.indexOf("{", start);
98
+ const semiPos = css.indexOf(";", start);
99
+ if (semiPos !== -1 && (bracePos === -1 || semiPos < bracePos)) {
100
+ return { content: css.slice(start, semiPos + 1), end: semiPos + 1 };
101
+ }
102
+ if (bracePos === -1)
103
+ return null;
104
+ const block = extractBlock(css, bracePos);
105
+ if (!block)
106
+ return null;
107
+ return {
108
+ content: css.slice(start, bracePos) + block.content,
109
+ end: block.end
110
+ };
111
+ }
112
+ function extractBlock(css, openBrace) {
113
+ let depth = 0;
114
+ for (let j = openBrace; j < css.length; j++) {
115
+ if (css[j] === "{") {
116
+ depth++;
117
+ } else if (css[j] === "}") {
118
+ depth--;
119
+ if (depth === 0) {
120
+ return { content: css.slice(openBrace, j + 1), end: j + 1 };
121
+ }
122
+ }
123
+ }
124
+ return null;
125
+ }
14
126
  export default defineEventHandler(async (event) => {
15
127
  const query = getQuery(event);
16
128
  const postUrl = query.url;
@@ -42,7 +154,6 @@ export default defineEventHandler(async (event) => {
42
154
  const html = await $fetch(embedUrl, {
43
155
  headers: {
44
156
  "Accept": "text/html",
45
- // Use simple UA - full Chrome UA triggers JS-heavy version without static content
46
157
  "User-Agent": "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
47
158
  }
48
159
  }).catch((error) => {
@@ -51,16 +162,45 @@ export default defineEventHandler(async (event) => {
51
162
  statusMessage: error.statusMessage || "Failed to fetch Instagram embed"
52
163
  });
53
164
  });
165
+ const ast = parse(html);
54
166
  const cssUrls = [];
55
- let match;
56
- while ((match = LINK_RE.exec(html)) !== null) {
57
- if (match[1])
58
- cssUrls.push(match[1]);
59
- }
60
- while ((match = LINK_RE_2.exec(html)) !== null) {
61
- if (match[1])
62
- cssUrls.push(match[1]);
63
- }
167
+ walkSync(ast, (node) => {
168
+ if (node.type !== ELEMENT_NODE)
169
+ return;
170
+ if (node.name === "link" && node.attributes.rel === "stylesheet" && node.attributes.href) {
171
+ cssUrls.push(node.attributes.href);
172
+ removeNode(node);
173
+ return;
174
+ }
175
+ if (node.name === "script" || node.name === "noscript" || node.name === "style") {
176
+ removeNode(node);
177
+ return;
178
+ }
179
+ for (const attr of ["src", "poster"]) {
180
+ if (node.attributes[attr])
181
+ node.attributes[attr] = rewriteUrl(node.attributes[attr]);
182
+ }
183
+ if (node.attributes.srcset) {
184
+ node.attributes.srcset = node.attributes.srcset.split(",").map((entry) => {
185
+ const parts = entry.trim().split(SRCSET_SPLIT_RE);
186
+ const url = parts[0];
187
+ const descriptor = parts.slice(1).join(" ");
188
+ return url ? `${rewriteUrl(url)}${descriptor ? ` ${descriptor}` : ""}` : entry;
189
+ }).join(", ");
190
+ }
191
+ if (node.attributes.style)
192
+ node.attributes.style = rewriteUrlsInText(node.attributes.style);
193
+ });
194
+ walkSync(ast, (node) => {
195
+ if (node.type === TEXT_NODE && node.value)
196
+ node.value = rewriteUrlsInText(node.value);
197
+ });
198
+ let bodyNode = null;
199
+ walkSync(ast, (node) => {
200
+ if (node.type === ELEMENT_NODE && node.name === "body")
201
+ bodyNode = node;
202
+ });
203
+ const bodyHtml = bodyNode ? bodyNode.children.map((child) => renderSync(child)).join("") : renderSync(ast);
64
204
  const cssContents = await Promise.all(
65
205
  cssUrls.map(
66
206
  (url) => $fetch(url, {
@@ -71,30 +211,19 @@ export default defineEventHandler(async (event) => {
71
211
  let combinedCss = cssContents.join("\n");
72
212
  combinedCss = combinedCss.replace(
73
213
  RSRC_RE,
74
- (_m, path) => `url(/api/_scripts/instagram-embed-asset?url=${encodeURIComponent(`https://static.cdninstagram.com/rsrc.php${path}`)})`
214
+ (_m, path) => `url(/_scripts/embed/instagram-asset?url=${encodeURIComponent(`https://static.cdninstagram.com/rsrc.php${path}`)})`
75
215
  );
216
+ combinedCss = rewriteUrlsInText(combinedCss);
217
+ combinedCss = scopeCss(combinedCss, ".instagram-embed-root");
76
218
  const baseStyles = `
77
- html { background: white; max-width: 540px; width: calc(100% - 2px); border-radius: 3px; border: 1px solid rgb(219, 219, 219); display: block; margin: 0px 0px 12px; min-width: 326px; padding: 0px; }
78
- #splash-screen { display: none !important; }
79
- .Embed { opacity: 1 !important; visibility: visible !important; }
80
- .EmbeddedMedia, .EmbeddedMediaImage { display: block !important; visibility: visible !important; }
219
+ .instagram-embed-root { background: white; max-width: 540px; width: calc(100% - 2px); border-radius: 3px; border: 1px solid rgb(219, 219, 219); display: block; margin: 0px 0px 12px; min-width: 326px; padding: 0px; }
220
+ .instagram-embed-root #splash-screen { display: none !important; }
221
+ .instagram-embed-root .Embed { opacity: 1 !important; visibility: visible !important; }
222
+ .instagram-embed-root .EmbeddedMedia, .instagram-embed-root .EmbeddedMediaImage { display: block !important; visibility: visible !important; }
81
223
  `;
82
- let rewrittenHtml = html.replace(SCRIPT_RE, "").replace(STYLESHEET_RE, "").replace(CSS_RE, "").replace(NOSCRIPT_RE, "").replace(
83
- SCONTENT_RE,
84
- (m) => `/api/_scripts/instagram-embed-image?url=${encodeURIComponent(m.replace(AMP_RE, "&"))}`
85
- ).replace(
86
- STATIC_CDN_RE,
87
- (m) => `/api/_scripts/instagram-embed-asset?url=${encodeURIComponent(m.replace(AMP_RE, "&"))}`
88
- ).replace(
89
- LOOKASIDE_RE,
90
- (m) => `/api/_scripts/instagram-embed-image?url=${encodeURIComponent(m.replace(AMP_RE, "&"))}`
91
- );
92
- rewrittenHtml = rewrittenHtml.replace(
93
- "</head>",
94
- `<style>${baseStyles}
95
- ${combinedCss}</style></head>`
96
- );
224
+ const result = `<div class="instagram-embed-root"><style>${baseStyles}
225
+ ${combinedCss}</style>${bodyHtml}</div>`;
97
226
  setHeader(event, "Content-Type", "text/html");
98
227
  setHeader(event, "Cache-Control", "public, max-age=600, s-maxage=600");
99
- return rewrittenHtml;
228
+ return result;
100
229
  });
@@ -0,0 +1,12 @@
1
+ export interface ImageProxyConfig {
2
+ allowedDomains: string[] | ((hostname: string) => boolean);
3
+ accept?: string;
4
+ userAgent?: string;
5
+ cacheMaxAge?: number;
6
+ contentType?: string;
7
+ /** Follow redirects (default: true). Set to false to reject redirects (SSRF protection). */
8
+ followRedirects?: boolean;
9
+ /** Decode &amp; in URL query parameter */
10
+ decodeAmpersands?: boolean;
11
+ }
12
+ export declare function createImageProxyHandler(config: ImageProxyConfig): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;