Skip to content
Open
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
30 changes: 26 additions & 4 deletions docs/src/components/starlight/Head.astro
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Issue:

We already have an StructuredData component where the schema objects are typed using TypeScript. You can find it here. We should add new schema types there and use that component for consistency.

Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
---
/**
* Custom Head component for RocketSim docs
* Includes proper favicon and meta tags matching the main site
*/
import Default from "@astrojs/starlight/components/Head.astro";
import PlausibleAnalytics from "@/components/PlausibleAnalytics.astro";
import StructuredData from "@/components/StructuredData.astro";
import config from "@/config/config.json";
import faqEntries from "@/data/faq-support.json";

const { entry } = Astro.props;
const title = entry?.data?.title ?? "RocketSim Docs";
const description = entry?.data?.description ?? "";
const slug = entry?.slug ?? "";
const pageUrl = `${config.site.base_url}/${slug}`;

const isContentPage = title !== "RocketSim Docs" && title !== "404";
const isFaqPage = slug.endsWith("support/faq");

const ogImage = `${config.site.base_url}${config.metadata.meta_image}`;
---
Expand All @@ -32,4 +39,19 @@ const ogImage = `${config.site.base_url}${config.metadata.meta_image}`;
<meta property="twitter:image" content={ogImage} />
<meta name="twitter:site" content="@rocketsim_app" />

{
isContentPage && !isFaqPage && (
<StructuredData
type="techArticle"
techArticle={{ title, description, url: pageUrl }}
/>
)
}

{
isFaqPage && (
<StructuredData type="faqPage" faqEntries={faqEntries} url={pageUrl} />
)
}
Comment on lines +42 to +55
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

set:html={JSON.stringify(schema)} injects raw JSON into a <script> tag. If any field (e.g. title/description) ever contains </script> or certain < sequences, it can break out of the script context and become an XSS vector. Consider escaping < (commonly replacing it with \u003c in the JSON string) before injecting, or using a safer serialization helper.

Copilot uses AI. Check for mistakes.

<PlausibleAnalytics />
58 changes: 58 additions & 0 deletions docs/src/data/faq-support.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
[
{
"question": "Do you offer out-of-the App Store distribution?",
"answer": "Yes, we do. Get in touch via support@rocketsim.app."
},
{
"question": "Are there non-recurring subscriptions as well?",
"answer": "Yes, you can consider buying Team Licenses at rocketsim.app/team-insights."
},
{
"question": "I can't reimburse App Store subscriptions, is there an alternative?",
"answer": "Yes, we offer Team Licenses at rocketsim.app/team-insights."
},
{
"question": "Do you provide the option for commercial or team licenses?",
"answer": "Yes, check out Team Licenses at rocketsim.app/team-insights."
},
{
"question": "Can I buy a lifetime license?",
"answer": "No, but you can join SwiftLee Weekly's referral program to get a lifetime RocketSim license. Just join the newsletter and follow the instructions in the email."
},
{
"question": "Why is my video not accepted by App Store Connect?",
"answer": "The videos are optimized for App Previews following Apple's specifications. App Store Connect does not accept each device, so your selected Simulator could not support App Previews. Make sure to use a Simulator matching a device from the App Preview specifications."
},
{
"question": "Why wouldn't I just use xcrun simctl?",
"answer": "RocketSim uses xcrun simctl as well, but it enhances the output and provides interfaces for quicker access. Recordings, for example, are enhanced with touches and device bezels that you won't get through the command line tools."
},
{
"question": "Why does RocketSim need screen recording permissions?",
"answer": "RocketSim is sandboxed and can't read NSWindow titles without screen recording permissions. RocketSim needs to read the title of the Simulator windows to determine the currently active Simulator."
},
{
"question": "Where can I follow active development?",
"answer": "RocketSim is developed by Antoine van der Lee. You can follow him or the official RocketSim Twitter Account for updates about development."
},
{
"question": "Where can I report bugs or feature requests?",
"answer": "Issues and feature requests are managed on GitHub at github.com/AvdLee/RocketSimApp/issues."
},
{
"question": "I only get JPEG images, how can I get PNG images again?",
"answer": "You've likely enabled App Store Connect (ASC) Optimization. ASC requires JPEG images without alpha layer. Disable the option and you should get PNGs again."
},
{
"question": "Why are my iPad captures upside-down?",
"answer": "RocketSim cannot detect landscape-left or landscape-right and defaults to one landscape rotation. The fix is simple: rotate your Simulator twice and restart the recording."
},
{
"question": "Can I create transparent captures?",
"answer": "Yes, make sure to disable App Preview Optimized and set your background color to transparent."
},
{
"question": "Network Speed Control isn't working for me, what can I do?",
"answer": "This is usually caused by missing system permissions. Quit Xcode, all Simulators, and RocketSim, then reopen them. Open System Settings → Privacy & Security and approve any pending RocketSim permissions. On macOS Sequoia and later, enable the Network Extension in System Settings → General → Login Items & Extensions."
}
]
141 changes: 139 additions & 2 deletions docs/src/layouts/components/StructuredData.astro
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ type OfferProps = {
};
};

type TechArticleProps = {
type: "techArticle";
techArticle: {
title: string;
description: string;
url: string;
};
};

type FAQPageProps = {
type: "faqPage";
faqEntries: Array<{
question: string;
answer: string;
}>;
url: string;
};

type StaticProps = {
type: "static";
};
Expand All @@ -48,7 +66,7 @@ type WebsiteProps = {
};

// prettier-ignore
export type Props = ArticleProps | ProductProps | OfferProps | StaticProps | WebsiteProps;
export type Props = ArticleProps | ProductProps | OfferProps | TechArticleProps | FAQPageProps | StaticProps | WebsiteProps;

// TypeScript interfaces for JSON-LD schemas
interface SchemaBase {
Expand Down Expand Up @@ -157,6 +175,31 @@ interface OfferSchema extends SchemaBase {
description?: string;
}

interface TechArticleSchema extends SchemaBase {
"@type": "TechArticle";
headline: string;
description: string;
url: string;
author: OrganizationSchema;
publisher: OrganizationSchema;
mainEntityOfPage: {
"@type": "WebPage";
"@id": string;
};
}

interface FAQPageSchema extends SchemaBase {
"@type": "FAQPage";
mainEntity: Array<{
"@type": "Question";
name: string;
acceptedAnswer: {
"@type": "Answer";
text: string;
};
}>;
}

const { type } = Astro.props;

// Author data (hardcoded - single author blog)
Expand Down Expand Up @@ -341,6 +384,100 @@ if (type === "product") {
schemas.push(breadcrumbSchema);
}

if (type === "techArticle") {
const { techArticle } = Astro.props;

const techArticleSchema: TechArticleSchema = {
"@context": "https://schema.org",
"@type": "TechArticle",
headline: techArticle.title,
description: techArticle.description,
url: techArticle.url,
author: organizationData,
publisher: {
...organizationData,
logo: `${config.site.base_url}/images/rocketsim-app-icon.png`,
},
mainEntityOfPage: {
"@type": "WebPage",
"@id": techArticle.url,
},
};
schemas.push(techArticleSchema);

const breadcrumbSchema: BreadcrumbListSchema = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"@id": `${techArticle.url}#breadcrumb`,
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: config.site.base_url,
},
{
"@type": "ListItem",
position: 2,
name: "Docs",
item: `${config.site.base_url}/docs`,
},
{
"@type": "ListItem",
position: 3,
name: techArticle.title,
item: techArticle.url,
},
],
};
schemas.push(breadcrumbSchema);
}

if (type === "faqPage") {
const { faqEntries, url } = Astro.props;

const faqPageSchema: FAQPageSchema = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqEntries.map((faq) => ({
"@type": "Question" as const,
name: faq.question,
acceptedAnswer: {
"@type": "Answer" as const,
text: faq.answer,
},
})),
};
schemas.push(faqPageSchema);

const breadcrumbSchema: BreadcrumbListSchema = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"@id": `${url}#breadcrumb`,
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: config.site.base_url,
},
{
"@type": "ListItem",
position: 2,
name: "Docs",
item: `${config.site.base_url}/docs`,
},
{
"@type": "ListItem",
position: 3,
name: "FAQ",
item: url,
},
],
};
schemas.push(breadcrumbSchema);
}

if (type === "offer") {
const { offers } = Astro.props;
// 1. SoftwareApplication with individual Offer schemas for each tier
Expand Down Expand Up @@ -439,7 +576,7 @@ if (type === "offer") {
<script
type="application/ld+json"
is:inline
set:html={JSON.stringify(schema)}
set:html={JSON.stringify(schema).replaceAll("<", "\\u003c")}
/>
))
}