Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions src/lib/utils/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,21 @@ export function buildOpenGraphImage(title: string, description: string): string
)}&subtitle=${encodeURIComponent(description)}`;
}

/**
* Escapes `<` as the JSON unicode escape `\u003c` so the result can be embedded
* inside HTML `<script>` tags (including SvelteKit route data) without `</script>`
* or similar sequences in string values ending the script early. `JSON.parse`
* still yields the original `<`.
*/
export function escapeJsonLtForHtmlScript(json: string): string {
return json.replace(/</g, '\\u003c');
}

/**
* Returns an inlined JSON-LD script tag without breaking IDE formatting.
*
* Escapes `<` as `\u003c` so content like `</script>` inside string values
* (e.g. code samples in forum posts) cannot prematurely close the script element
* in HTML; JSON.parse still yields the original `<`.
*/
export function getInlinedScriptTag(jsonSchema: string): string {
const safeForInlineScript = jsonSchema.replace(/</g, '\\u003c');
const safeForInlineScript = escapeJsonLtForHtmlScript(jsonSchema);
return `<script type="application/ld+json">${safeForInlineScript}</` + 'script>';
}

Expand All @@ -43,7 +49,7 @@ export function organizationJsonSchema() {
legalName: 'Appwrite Code Ltd.',
description:
'A secure open-source backend server provides the core APIs required to build web and mobile applications. Appwrite provides authentication, database, storage, functions, messaging, and advanced realtime capabilities.',
logo: 'https://appwrite.io/assets/logotype/white.avif'
logo: 'https://appwrite.io/assets/logotype/white.png'
});
}

Expand Down Expand Up @@ -246,5 +252,5 @@ export function createDiscussionForumPageSchema(options: {
mainEntity
};

return JSON.stringify(graph);
return escapeJsonLtForHtmlScript(JSON.stringify(graph));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Pre-escaping creates redundant second pass in getInlinedScriptTag

createDiscussionForumPageSchema now returns a pre-escaped string (all < replaced with \u003c), but its output is then passed to getInlinedScriptTag, which calls escapeJsonLtForHtmlScript a second time. The second pass is a no-op because no < characters remain, so the HTML output is correct. However, every other schema function (organizationJsonSchema, softwareAppSchema, createPostSchema, etc.) returns raw JSON and relies on getInlinedScriptTag to do the escaping — createDiscussionForumPageSchema is now the only outlier.

If the motivation is to protect structuredDataJsonLd when SvelteKit serializes it into the page's devalue inline script (a separate <script> from the JSON-LD tag), that is a valid concern — but it may be worth a comment at the call site in +page.server.ts explaining why this function pre-escapes while others do not, so future callers of getInlinedScriptTag don't expect all inputs to behave the same way.

}
33 changes: 31 additions & 2 deletions src/routes/(marketing)/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
<script lang="ts">
import { browser } from '$app/environment';
import { browser, building } from '$app/environment';
import { page } from '$app/state';
import {
DEFAULT_HERO_CTA,
DEFAULT_HERO_SUBTITLE,
DEFAULT_HERO_TITLE
} from '$lib/statsig/constants';
import { resolveHeroQueryOverrides } from '$lib/statsig/hero-query-overrides';
import type { HeroLayoutVariant } from '$lib/statsig/hero-layout';
import type { PageData } from './$types';
import Bento from './(components)/bento/bento.svelte';
import CaseStudies from './(components)/case-studies.svelte';
import Features from './(components)/features.svelte';
Expand All @@ -14,10 +23,30 @@
import { FooterNav, MainFooter } from '$lib/components';
import LogoList from './(components)/logo-list.svelte';
import Ai from './(components)/ai.svelte';

/** Same baseline + query resolution as `hero.svelte`; tab title prefixes the brand. */
type MarketingHeroPageData = PageData & {
statsigBootstrap?: string | null;
statsigStableUserId?: string | null;
statsigUserAgent?: string | null;
};

const data = $derived(page.data as MarketingHeroPageData);

const heroPageTitle = $derived(
resolveHeroQueryOverrides(building ? new URLSearchParams() : page.url.searchParams, {
heroLayout: (data.heroLayout ?? 0) as HeroLayoutVariant,
heroSubtitle: data.heroSubtitle ?? DEFAULT_HERO_SUBTITLE,
heroTitle: data.heroTitle ?? DEFAULT_HERO_TITLE,
heroCta: data.heroCta ?? DEFAULT_HERO_CTA
}).heroTitle
);

const homepageDocumentTitle = $derived(`Appwrite - ${heroPageTitle}`);
</script>

<Head
title="Appwrite - Build like a team of hundreds"
title={homepageDocumentTitle}
description="Build like a team of hundreds with Appwrite's all-in-one, open-source infrastructure. Launch in minutes, use any framework, and scale affordably with Auth, Database, Storage, Functions, Realtime, Messaging, and Sites for static sites, SSR, and CSR frontends."
/>

Expand Down
2 changes: 1 addition & 1 deletion src/routes/threads/[id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@
display: grid;
grid-template-columns: 1fr auto;
gap: 4rem;
align-items: center;
align-items: start;

margin-block-start: 2.5rem;
}
Expand Down
Loading