Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Script tags are not being pushed to document when using partials #2805

Open
vitoUwu opened this issue Jan 4, 2025 · 1 comment
Open

Script tags are not being pushed to document when using partials #2805

vitoUwu opened this issue Jan 4, 2025 · 1 comment

Comments

@vitoUwu
Copy link

vitoUwu commented Jan 4, 2025

I work at Deco.cx and here we created a feature called "Async Rendering" that renders sections of the website asynchronously, a "Skeleton" of the HTML is sent to the client as quickly as possible, while other sections that depend on data from external APIs are rendered asynchronously and returned to the client after resolution. One of the frameworks we use is Fresh and for this feature we use "partials" to return the component to the client.

Basically this is the fresh implementation
image

We recently found a problem with this feature, our SEO components were not adding structured data to the HTML when async rendering is active in this component, and after some investigations I found where this problem could be happening. This is our SEO component:

import { Head } from "$fresh/runtime.ts";
import type { ImageWidget } from "../../admin/widgets.ts";
import { stripHTML } from "../utils/html.ts";
import { JSX } from "preact";

export const renderTemplateString = (template: string, value: string) =>
  template.replace("%s", value);

export type SEOSection = JSX.Element;
export type OGType = "website" | "article";

export interface Props {
  title?: string;
  /**
   * @title Title template
   * @description add a %s whenever you want it to be replaced with the product name, category name or search term
   * @default %s
   */
  titleTemplate?: string;
  description?: string;
  /**
   * @title Description template
   * @description add a %s whenever you want it to be replaced with the product name, category name or search term
   * @default %s
   */
  descriptionTemplate?: string;
  /** @default website */
  type?: OGType;
  /** @description Recommended: 1200 x 630 px (up to 5MB) */
  image?: ImageWidget;
  /** @description Recommended: 16 x 16 px */
  favicon?: ImageWidget;
  /** @description Suggested color that browsers should use to customize the display of the page or of the surrounding user interface */
  themeColor?: string;
  /** @title Canonical URL */
  canonical?: string;
  /**
   * @title Disable indexing
   * @description In testing, you can use this to prevent search engines from indexing your site
   */
  noIndexing?: boolean;

  jsonLDs?: unknown[];
}

function Component({
  title: t = "",
  titleTemplate = "%s",
  description: desc,
  descriptionTemplate = "%s",
  type,
  image,
  favicon,
  themeColor,
  canonical,
  noIndexing,
  jsonLDs = [],
}: Props) {
  const twitterCard = type === "website" ? "summary" : "summary_large_image";
  const description = stripHTML(desc || "");
  const title = stripHTML(t);

  return (
    <Head>
      <title>{renderTemplateString(titleTemplate, title)}</title>
      <meta
        name="description"
        content={renderTemplateString(descriptionTemplate, description)}
      />
      <meta name="theme-color" content={themeColor} />
      <link rel="icon" href={favicon} />

      {/* Twitter tags */}
      <meta property="twitter:title" content={title} />
      <meta property="twitter:description" content={description} />
      <meta property="twitter:image" content={image} />
      <meta property="twitter:card" content={twitterCard} />

      {/* OpenGraph tags */}
      <meta property="og:title" content={title} />
      <meta property="og:description" content={description} />
      <meta property="og:type" content={type} />
      <meta property="og:image" content={image} />

      {/* Link tags */}
      {canonical && <link rel="canonical" href={canonical} />}

      {/* No index, no follow */}
      {noIndexing && <meta name="robots" content="noindex, nofollow" />}
      {!noIndexing && <meta name="robots" content="index, follow" />}

      {jsonLDs.map((json) => (
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{
            __html: JSON.stringify({
              "@context": "https://schema.org",
              // @ts-expect-error Trust me, I'm an engineer
              ...json,
            }),
          }}
        />
      ))}
    </Head>
  );
}

export default Component;

The structured data is passed as "jsonLDs" props and added to the <Head> component inside a <script> tag, looking for the partials implementation I've find out that the implementation ignores scripts inside the "head" element:

} else if (child.nodeName === "SCRIPT") {
const script = child as HTMLScriptElement;
if (script.src === `${INTERNAL_PREFIX}/fresh-runtime.js`) return;
// TODO: What to do with script tags?
} else if (child.nodeName === "STYLE") {

We need that these scripts are pushed to the actual document's head so the crawlers can read these scripts and improve our websites SEO.

I'm willing to contribute to this problem, I just need to be sure how we can fix this without causing problems

@vitoUwu
Copy link
Author

vitoUwu commented Jan 4, 2025

Another important point that I forgot to mention, when I remove the script from <Head> and start sending it in the body, the script is added to the document but it comes without any content, I haven't discovered the exact cause yet but I'm seeing what could be happening

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant