From 7b40f7045b67f793e37fc0afd321a45a688b0edf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 29 Sep 2026 05:50:15 +0000 Subject: [PATCH 1/2] Fill legal footer pages for The Open Stock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace empty Copyrights, Privacy & Security, Terms of Use, and Sitemap placeholders with product copy adapted from ditectrev.com, including the not-legal/investment-advice disclaimer from issue #78. Co-authored-by: ✅ Daniel Danielecki --- app/(main)/copyrights/page.tsx | 12 +- app/(main)/privacy-and-security/page.tsx | 12 +- app/(main)/sitemap/page.tsx | 8 +- app/(main)/terms-of-use/page.tsx | 12 +- components/HtmlSitemapPage.tsx | 62 ++ components/LegalDocumentPage.tsx | 139 ++++ components/LegalPlaceholderPage.tsx | 9 - components/__tests__/HtmlSitemapPage.test.tsx | 20 + .../__tests__/LegalDocumentPage.test.tsx | 48 ++ lib/__tests__/html-sitemap.test.ts | 27 + lib/__tests__/legal-content.test.ts | 60 ++ lib/footer-content.ts | 12 + lib/html-sitemap.ts | 48 ++ lib/legal-content.ts | 666 ++++++++++++++++++ lib/site-seo.ts | 13 + 15 files changed, 1121 insertions(+), 27 deletions(-) create mode 100644 components/HtmlSitemapPage.tsx create mode 100644 components/LegalDocumentPage.tsx delete mode 100644 components/LegalPlaceholderPage.tsx create mode 100644 components/__tests__/HtmlSitemapPage.test.tsx create mode 100644 components/__tests__/LegalDocumentPage.test.tsx create mode 100644 lib/__tests__/html-sitemap.test.ts create mode 100644 lib/__tests__/legal-content.test.ts create mode 100644 lib/html-sitemap.ts create mode 100644 lib/legal-content.ts diff --git a/app/(main)/copyrights/page.tsx b/app/(main)/copyrights/page.tsx index d633ed3..72a7797 100644 --- a/app/(main)/copyrights/page.tsx +++ b/app/(main)/copyrights/page.tsx @@ -1,13 +1,15 @@ import type { Metadata } from "next"; -import { LegalPlaceholderPage } from "@/components/LegalPlaceholderPage"; +import { LegalDocumentPage } from "@/components/LegalDocumentPage"; +import { COPYRIGHTS_DOCUMENT } from "@/lib/legal-content"; import { buildPageMetadata } from "@/lib/site-seo"; export const metadata: Metadata = buildPageMetadata({ - title: "Copyrights", - description: "Copyrights for The Open Stock.", - path: "/copyrights", + title: COPYRIGHTS_DOCUMENT.title, + description: COPYRIGHTS_DOCUMENT.description, + path: COPYRIGHTS_DOCUMENT.path, + keywords: COPYRIGHTS_DOCUMENT.keywords, }); export default function CopyrightsPage() { - return ; + return ; } diff --git a/app/(main)/privacy-and-security/page.tsx b/app/(main)/privacy-and-security/page.tsx index cbe8915..7bb15e5 100644 --- a/app/(main)/privacy-and-security/page.tsx +++ b/app/(main)/privacy-and-security/page.tsx @@ -1,13 +1,15 @@ import type { Metadata } from "next"; -import { LegalPlaceholderPage } from "@/components/LegalPlaceholderPage"; +import { LegalDocumentPage } from "@/components/LegalDocumentPage"; +import { PRIVACY_DOCUMENT } from "@/lib/legal-content"; import { buildPageMetadata } from "@/lib/site-seo"; export const metadata: Metadata = buildPageMetadata({ - title: "Privacy & Security", - description: "Privacy and security for The Open Stock.", - path: "/privacy-and-security", + title: PRIVACY_DOCUMENT.title, + description: PRIVACY_DOCUMENT.description, + path: PRIVACY_DOCUMENT.path, + keywords: PRIVACY_DOCUMENT.keywords, }); export default function PrivacyAndSecurityPage() { - return ; + return ; } diff --git a/app/(main)/sitemap/page.tsx b/app/(main)/sitemap/page.tsx index 60de885..d87f172 100644 --- a/app/(main)/sitemap/page.tsx +++ b/app/(main)/sitemap/page.tsx @@ -1,13 +1,15 @@ import type { Metadata } from "next"; -import { LegalPlaceholderPage } from "@/components/LegalPlaceholderPage"; +import { HtmlSitemapPage } from "@/components/HtmlSitemapPage"; import { buildPageMetadata } from "@/lib/site-seo"; export const metadata: Metadata = buildPageMetadata({ title: "Sitemap", - description: "Sitemap for The Open Stock.", + description: + "Directory of public The Open Stock pages: charts, news, ranking, tools, glossary, and legal resources.", path: "/sitemap", + keywords: ["sitemap", "site directory"], }); export default function SitemapPage() { - return ; + return ; } diff --git a/app/(main)/terms-of-use/page.tsx b/app/(main)/terms-of-use/page.tsx index f9cd4c5..b7982fa 100644 --- a/app/(main)/terms-of-use/page.tsx +++ b/app/(main)/terms-of-use/page.tsx @@ -1,13 +1,15 @@ import type { Metadata } from "next"; -import { LegalPlaceholderPage } from "@/components/LegalPlaceholderPage"; +import { LegalDocumentPage } from "@/components/LegalDocumentPage"; +import { TERMS_DOCUMENT } from "@/lib/legal-content"; import { buildPageMetadata } from "@/lib/site-seo"; export const metadata: Metadata = buildPageMetadata({ - title: "Terms of Use", - description: "Terms of use for The Open Stock.", - path: "/terms-of-use", + title: TERMS_DOCUMENT.title, + description: TERMS_DOCUMENT.description, + path: TERMS_DOCUMENT.path, + keywords: TERMS_DOCUMENT.keywords, }); export default function TermsOfUsePage() { - return ; + return ; } diff --git a/components/HtmlSitemapPage.tsx b/components/HtmlSitemapPage.tsx new file mode 100644 index 0000000..2fcc5c8 --- /dev/null +++ b/components/HtmlSitemapPage.tsx @@ -0,0 +1,62 @@ +import Link from "next/link"; +import { HTML_SITEMAP_GROUPS } from "@/lib/html-sitemap"; +import { + DNA_BODY_SECONDARY, + DNA_DISPLAY, + DNA_EYEBROW, + DNA_HEADING, + DNA_HERO_LEAD, + DNA_INSTRUMENT_PANEL, + DNA_MARKETING_STACK, + DNA_SECTION_RULE, +} from "@/lib/design-dna"; +import { SITE_NAME } from "@/lib/site-seo"; + +const sitemapLinkClass = + "underline decoration-stone-400 underline-offset-2 hover:text-stone-900 dark:hover:text-stone-100"; + +export function HtmlSitemapPage() { + return ( +
+
+

Resources · Directory

+

Sitemap

+

+ Public pages on {SITE_NAME}. Use this directory to jump to tools, + product surfaces, and legal resources. +

+

+ Machines can also read{" "} + + sitemap.xml + + . +

+
+ +
+ {HTML_SITEMAP_GROUPS.map((group) => ( +
+

+ {group.label} +

+
    + {group.links.map((link) => ( +
  • + + {link.label} + +
  • + ))} +
+
+ ))} +
+
+ ); +} diff --git a/components/LegalDocumentPage.tsx b/components/LegalDocumentPage.tsx new file mode 100644 index 0000000..7b0d42b --- /dev/null +++ b/components/LegalDocumentPage.tsx @@ -0,0 +1,139 @@ +import Link from "next/link"; +import type { LegalDocument, LegalInline } from "@/lib/legal-content"; +import { isLegalLink } from "@/lib/legal-content"; +import { + DNA_BODY, + DNA_BODY_SECONDARY, + DNA_CALLOUT, + DNA_CAPTION, + DNA_DISPLAY, + DNA_EYEBROW, + DNA_HEADING, + DNA_HERO_LEAD, + DNA_MARKETING_STACK, + DNA_SECTION_RULE, +} from "@/lib/design-dna"; +import { SITE_NAME } from "@/lib/site-seo"; + +const legalLinkClass = + "underline decoration-stone-400 underline-offset-2 hover:text-stone-900 dark:hover:text-stone-100"; + +function LegalRichText({ parts }: { parts: readonly LegalInline[] }) { + return ( + <> + {parts.map((part, index) => { + if (!isLegalLink(part)) { + return {part}; + } + + const className = legalLinkClass; + if (part.href.startsWith("/")) { + return ( + + {part.label} + + ); + } + + const isMailOrTel = + part.href.startsWith("mailto:") || part.href.startsWith("tel:"); + return ( + + {part.label} + + ); + })} + + ); +} + +export function LegalDocumentPage({ document }: { document: LegalDocument }) { + const showAdviceCallout = document.slug === "terms-of-use"; + + return ( +
+
+

Resources · Legal

+

{document.title}

+

{document.subtitle}

+

+ {SITE_NAME} product copy — not a substitute for professional advice. +

+
+ + {showAdviceCallout ? ( +

+ {SITE_NAME} is not legal, tax, accounting, or investment advice. + Charts, AI, rankings, and news are informational only. You can lose + money. Get qualified advice before you act. +

+ ) : null} + + {document.sections.map((section) => ( +
+

+ {section.title} +

+
+ {section.blocks.map((block, blockIndex) => { + if (block.type === "ul") { + return ( +
    + {block.items.map((item, itemIndex) => ( +
  • + +
  • + ))} +
+ ); + } + + return ( +

+ +

+ ); + })} +
+
+ ))} + +

+ Related:{" "} + + Copyrights + + {" · "} + + Privacy & Security + + {" · "} + + Terms of Use + + {" · "} + + Sitemap + +

+
+ ); +} diff --git a/components/LegalPlaceholderPage.tsx b/components/LegalPlaceholderPage.tsx deleted file mode 100644 index d8659aa..0000000 --- a/components/LegalPlaceholderPage.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { DNA_DISPLAY, DNA_PAGE_STACK } from "@/lib/design-dna"; - -export function LegalPlaceholderPage({ title }: { title: string }) { - return ( -
-

{title}

-
- ); -} diff --git a/components/__tests__/HtmlSitemapPage.test.tsx b/components/__tests__/HtmlSitemapPage.test.tsx new file mode 100644 index 0000000..e7c36b2 --- /dev/null +++ b/components/__tests__/HtmlSitemapPage.test.tsx @@ -0,0 +1,20 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { HtmlSitemapPage } from "@/components/HtmlSitemapPage"; +import { FOOTER_RESOURCES, FOOTER_TOOLS } from "@/lib/footer-content"; + +describe("HtmlSitemapPage", () => { + it("renders grouped links for tools and resources", () => { + render(); + expect(screen.getByTestId("sitemap-page")).toBeInTheDocument(); + expect( + screen.getByRole("heading", { level: 1, name: "Sitemap" }) + ).toBeInTheDocument(); + for (const item of [...FOOTER_TOOLS, ...FOOTER_RESOURCES]) { + expect(screen.getByRole("link", { name: item.label })).toHaveAttribute( + "href", + item.href + ); + } + }); +}); diff --git a/components/__tests__/LegalDocumentPage.test.tsx b/components/__tests__/LegalDocumentPage.test.tsx new file mode 100644 index 0000000..e78e648 --- /dev/null +++ b/components/__tests__/LegalDocumentPage.test.tsx @@ -0,0 +1,48 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { LegalDocumentPage } from "@/components/LegalDocumentPage"; +import { + COPYRIGHTS_DOCUMENT, + PRIVACY_DOCUMENT, + TERMS_DOCUMENT, +} from "@/lib/legal-content"; + +describe("LegalDocumentPage", () => { + it("renders copyrights sections", () => { + render(); + expect(screen.getByTestId("copyrights-page")).toBeInTheDocument(); + expect( + screen.getByRole("heading", { level: 1, name: "Copyrights" }) + ).toBeInTheDocument(); + expect( + screen.getByRole("heading", { level: 2, name: "Content" }) + ).toBeInTheDocument(); + expect( + screen.getByRole("heading", { level: 2, name: "Licensing" }) + ).toBeInTheDocument(); + }); + + it("renders privacy contact details", () => { + render(); + expect( + screen.getByRole("heading", { level: 1, name: "Privacy & Security" }) + ).toBeInTheDocument(); + const mailLinks = screen.getAllByRole("link", { + name: "contact@ditectrev.com", + }); + expect(mailLinks.length).toBeGreaterThan(0); + for (const link of mailLinks) { + expect(link).toHaveAttribute("href", "mailto:contact@ditectrev.com"); + } + }); + + it("highlights the not-advice disclaimer on terms", () => { + render(); + expect(screen.getByTestId("not-advice-callout")).toHaveTextContent( + /not legal, tax, accounting, or investment advice/i + ); + expect( + screen.getByRole("heading", { level: 2, name: "Disclaimers" }) + ).toBeInTheDocument(); + }); +}); diff --git a/lib/__tests__/html-sitemap.test.ts b/lib/__tests__/html-sitemap.test.ts new file mode 100644 index 0000000..bd3370b --- /dev/null +++ b/lib/__tests__/html-sitemap.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { FOOTER_RESOURCES, FOOTER_TOOLS } from "@/lib/footer-content"; +import { htmlSitemapPaths, HTML_SITEMAP_GROUPS } from "@/lib/html-sitemap"; + +describe("html-sitemap", () => { + it("lists footer tools and resources", () => { + const paths = htmlSitemapPaths(); + for (const item of [...FOOTER_TOOLS, ...FOOTER_RESOURCES]) { + expect(paths).toContain(item.href); + } + }); + + it("does not list agency service pages", () => { + const paths = htmlSitemapPaths(); + expect(paths).not.toContain("/services"); + expect(paths).not.toContain("/methodology"); + expect(paths).not.toContain("/about-us"); + }); + + it("groups product, tools, and resources", () => { + expect(HTML_SITEMAP_GROUPS.map((group) => group.id)).toEqual([ + "product", + "tools", + "resources", + ]); + }); +}); diff --git a/lib/__tests__/legal-content.test.ts b/lib/__tests__/legal-content.test.ts new file mode 100644 index 0000000..6d41da7 --- /dev/null +++ b/lib/__tests__/legal-content.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from "vitest"; +import { COMPANY_LEGAL, FOOTER_CONTACT } from "@/lib/footer-content"; +import { + COPYRIGHTS_DOCUMENT, + LEGAL_DOCUMENTS, + PRIVACY_DOCUMENT, + TERMS_DOCUMENT, + legalDocumentPlainText, +} from "@/lib/legal-content"; + +describe("legal-content", () => { + it("covers the three footer legal documents", () => { + expect(LEGAL_DOCUMENTS.map((doc) => doc.path)).toEqual([ + "/copyrights", + "/privacy-and-security", + "/terms-of-use", + ]); + }); + + it("uses footer company details", () => { + const privacy = legalDocumentPlainText(PRIVACY_DOCUMENT); + const terms = legalDocumentPlainText(TERMS_DOCUMENT); + expect(privacy).toContain(FOOTER_CONTACT.address); + expect(privacy).toContain(FOOTER_CONTACT.email); + expect(privacy).toContain(FOOTER_CONTACT.phone); + expect(terms).toContain(COMPANY_LEGAL.legalName); + expect(terms).toContain(COMPANY_LEGAL.vies); + expect(terms).toContain(COMPANY_LEGAL.regon); + }); + + it("states that The Open Stock is not legal or investment advice", () => { + const terms = legalDocumentPlainText(TERMS_DOCUMENT); + expect(terms.toLowerCase()).toContain("not legal"); + expect(terms.toLowerCase()).toContain("investment advice"); + expect(terms.toLowerCase()).toContain("past performance"); + }); + + it("does not copy agency-only claims", () => { + const combined = LEGAL_DOCUMENTS.map(legalDocumentPlainText).join("\n"); + expect(combined.toLowerCase()).not.toContain("digital strategy"); + expect(combined.toLowerCase()).not.toContain("ui and ux mock"); + expect(combined.toLowerCase()).not.toContain("participating in courses"); + expect(combined.toLowerCase()).not.toContain("privacy shield"); + expect(combined.toLowerCase()).not.toContain("credit cards (last 3"); + }); + + it("describes this product’s processors, not extra billing products", () => { + const privacy = legalDocumentPlainText(PRIVACY_DOCUMENT); + expect(privacy).toContain("Appwrite"); + expect(privacy).toContain("Stripe"); + expect(privacy.toLowerCase()).not.toContain("refund"); + expect(privacy.toLowerCase()).not.toContain("chargeback"); + }); + + it("keeps copyrights distinct from third-party market data", () => { + const copyrights = legalDocumentPlainText(COPYRIGHTS_DOCUMENT); + expect(copyrights.toLowerCase()).toContain("ticker"); + expect(copyrights.toLowerCase()).toContain("all rights reserved"); + }); +}); diff --git a/lib/footer-content.ts b/lib/footer-content.ts index 06cec06..c8e8950 100644 --- a/lib/footer-content.ts +++ b/lib/footer-content.ts @@ -10,6 +10,18 @@ export const FOOTER_CONTACT = { taxId: "Tax ID: PL9121899240", } as const; +/** Operator identity used on legal pages (same entity as the footer). */ +export const COMPANY_LEGAL = { + legalName: "IBStructure Daniel Danielecki", + tradingAs: "Ditectrev", + dpoName: "Daniel Danielecki", + vies: "PL9121899240", + nip: "9121899240", + regon: "366592971", + country: "Poland (European Union)", + githubRepo: "https://github.com/Ditectrev/Open-Source-Stock-Application", +} as const; + export const FOOTER_TOOLS = [ { label: "Sectors", href: "/sectors" }, { label: "Calendars", href: "/calendars" }, diff --git a/lib/html-sitemap.ts b/lib/html-sitemap.ts new file mode 100644 index 0000000..4cf547d --- /dev/null +++ b/lib/html-sitemap.ts @@ -0,0 +1,48 @@ +/** + * Human-readable HTML sitemap groups for /sitemap. + * Routes match the live app (not ditectrev.com agency/service pages). + */ + +import { FOOTER_RESOURCES, FOOTER_TOOLS } from "@/lib/footer-content"; +import { MAIN_NAV } from "@/lib/nav-routes"; + +export type SitemapLink = { + label: string; + href: string; +}; + +export type SitemapGroup = { + id: string; + label: string; + links: readonly SitemapLink[]; +}; + +const PRODUCT_LINKS: readonly SitemapLink[] = [ + ...MAIN_NAV.map((item) => ({ label: item.label, href: item.href })), + { label: "Stock of the Day", href: "/stock-of-the-day" }, + { label: "Compare", href: "/compare" }, +]; + +export const HTML_SITEMAP_GROUPS: readonly SitemapGroup[] = [ + { + id: "product", + label: "Product", + links: PRODUCT_LINKS, + }, + { + id: "tools", + label: "Tools", + links: FOOTER_TOOLS, + }, + { + id: "resources", + label: "Resources", + links: FOOTER_RESOURCES, + }, +]; + +export function htmlSitemapPaths(): string[] { + return HTML_SITEMAP_GROUPS.flatMap((group) => + group.links.map((link) => link.href) + ); +} diff --git a/lib/legal-content.ts b/lib/legal-content.ts new file mode 100644 index 0000000..883f795 --- /dev/null +++ b/lib/legal-content.ts @@ -0,0 +1,666 @@ +/** + * Product copy for Copyrights, Privacy & Security, and Terms of Use. + * Adapted from ditectrev.com legal pages for The Open Stock (not a design agency). + */ + +import { COMPANY_LEGAL, FOOTER_CONTACT } from "@/lib/footer-content"; +import { SITE_NAME } from "@/lib/site-seo"; + +export type LegalLink = { + href: string; + label: string; +}; + +export type LegalInline = string | LegalLink; + +export type LegalBlock = + | { type: "p"; parts: readonly LegalInline[] } + | { type: "ul"; items: readonly (readonly LegalInline[])[] }; + +export type LegalSection = { + id: string; + title: string; + blocks: readonly LegalBlock[]; +}; + +export type LegalDocument = { + slug: "copyrights" | "privacy-and-security" | "terms-of-use"; + title: string; + subtitle: string; + path: string; + description: string; + keywords: readonly string[]; + sections: readonly LegalSection[]; +}; + +const MAILTO: LegalLink = { + href: `mailto:${FOOTER_CONTACT.email}`, + label: FOOTER_CONTACT.email, +}; + +const TEL: LegalLink = { + href: `tel:${FOOTER_CONTACT.phone.replace(/\s+/g, "")}`, + label: FOOTER_CONTACT.phone, +}; + +const TERMS: LegalLink = { + href: "/terms-of-use", + label: "Terms of Use", +}; + +const COPYRIGHTS: LegalLink = { + href: "/copyrights", + label: "Copyrights", +}; + +const PRIVACY: LegalLink = { + href: "/privacy-and-security", + label: "Privacy & Security", +}; + +const HOME: LegalLink = { + href: "/", + label: "theopenstock.com", +}; + +export const COPYRIGHTS_DOCUMENT: LegalDocument = { + slug: "copyrights", + title: "Copyrights", + subtitle: "Before you copy anything from us, kindly read this carefully.", + path: "/copyrights", + description: `Copyrights for ${SITE_NAME}: site content, software, and third-party market data.`, + keywords: ["copyrights", "intellectual property", "market data attribution"], + sections: [ + { + id: "content", + title: "Content", + blocks: [ + { + type: "p", + parts: [ + "Content on ", + HOME, + " — including copy, charts UI, graphics, layout, original software, and other material we publish — belongs to ", + `${COMPANY_LEGAL.legalName} (${COMPANY_LEGAL.tradingAs})`, + ", its affiliates, contributors, or licensors, except where a third party owns it. Third-party material includes market data, news, research, company names, logos, and ticker symbols.", + ], + }, + { + type: "p", + parts: [ + "Unless the ", + TERMS, + ", a license we publish, or the law says otherwise, you may not adapt, copy, scrape or extract data, distribute, frame, rebrand, reverse engineer, republish, or otherwise use that Content in a way that causes us loss. The same applies to manual use and to automated tools such as robots, spiders, or scrapers.", + ], + }, + { + type: "p", + parts: [ + "Nothing here grants you a license to our copyrights, trademarks, or product, except the rights we expressly give you. You need written permission from the owner for any other use.", + ], + }, + ], + }, + { + id: "licensing", + title: "Licensing", + blocks: [ + { + type: "p", + parts: [ + `${SITE_NAME} is provided with all rights reserved unless we publish a license for a given asset. In some scenarios we might publish source under a permissive license (for example BSD, ISC, or MIT) or another license; that has to be agreed with us. Viewing the `, + { + href: COMPANY_LEGAL.githubRepo, + label: "GitHub repository", + }, + " does not by itself grant a license to rebrand or commercially reuse the product.", + ], + }, + ], + }, + { + id: "content-infringes", + title: "Content infringes", + blocks: [ + { + type: "p", + parts: [ + "If you believe Content on this site infringes your rights or a third party's, contact us with enough detail to identify the material and the rights holder. We will look into it and may remove the material. Knowingly making a misrepresentation may expose you to liability.", + ], + }, + ], + }, + { + id: "material-from-the-website", + title: "Material from the website", + blocks: [ + { + type: "p", + parts: [ + "Material from the website may be re-used without written permission where a statutory exception to copyright applies in your jurisdiction.", + ], + }, + ], + }, + ], +}; + +export const PRIVACY_DOCUMENT: LegalDocument = { + slug: "privacy-and-security", + title: "Privacy & Security", + subtitle: + "We respect your privacy, but running this site requires some of your data.", + path: "/privacy-and-security", + description: `How ${SITE_NAME} collects, uses, and protects personal data for accounts, trials, and the hosted app.`, + keywords: ["privacy", "security", "cookies", "GDPR", "personal data"], + sections: [ + { + id: "information-scope", + title: "Information scope", + blocks: [ + { + type: "p", + parts: [ + "This ", + PRIVACY, + " page explains what happens when you use ", + HOME, + " (the Website and Asset defined in the ", + TERMS, + "). Personal information is any data that can identify you, alone or combined with other data. If something is unclear, contact us.", + ], + }, + ], + }, + { + id: "data-collection", + title: "Data collection", + blocks: [ + { + type: "p", + parts: [ + "Personal information includes data such as your name and email address. Data collected by this website is controlled by ", + COMPANY_LEGAL.legalName, + " at ", + `${FOOTER_CONTACT.address}. `, + FOOTER_CONTACT.taxId, + ". You can contact us:", + ], + }, + { + type: "ul", + items: [ + ["By email: ", MAILTO], + [`By mail: ${FOOTER_CONTACT.address}`], + ["By phone: ", TEL], + ], + }, + { + type: "p", + parts: [ + "You can use much of ", + SITE_NAME, + " without an account. Creating an account, starting a trial, or subscribing is voluntary, but those features need the data below.", + ], + }, + { + type: "p", + parts: [ + "Depending on how you use the app, we may process: account details (email, name, and OAuth identifiers if you sign in with Google or Apple via Appwrite); an HTTP-only session cookie to keep you signed in; subscription status and Stripe checkout or customer references for optional paid tiers (card numbers are handled by Stripe, not stored by us); optional AI provider API keys you choose to save; trial abuse-prevention data such as IP address and a device fingerprint from browser characteristics; server logs (IP, user agent, timestamps, URLs); theme preference in local storage; analytics cookies when Google Tag Manager is enabled on a deployment; and advertising identifiers if ads are shown on the free tier.", + ], + }, + { + type: "p", + parts: [ + "We do not ask for biometric, health, genetic, criminal, political, or similar sensitive data, and you should not send it to us.", + ], + }, + { + type: "p", + parts: [ + "Your data is treated confidentially, in line with applicable data-protection law and this page. We share data with law enforcement only when local law requires it. We do not sell your personal data. We use processors that help run the product — including Appwrite (accounts, stored keys, trial and subscription records), Stripe (payments), Vercel (hosting), Google or Apple (sign-in), and Google Tag Manager when configured. Market-data providers such as Yahoo Finance, Finnhub, and CNN receive the requests needed to load quotes, news, and public market metrics; they are not given your account profile for that purpose.", + ], + }, + { + type: "p", + parts: [ + "No website can fully prevent unlawful access. We use HTTPS and restrict access to systems that store personal data, but we cannot take responsibility for loss caused by attackers. Information security also depends on technology we do not control.", + ], + }, + { + type: "p", + parts: [ + "We keep account and subscription data while your account or paid plan is active, and for a limited time afterward if the law or dispute handling requires it. Trial identifiers are kept long enough to enforce one trial per person. Server logs are kept for a short operational period. You can ask us to delete account data; backups may take longer to expire.", + ], + }, + { + type: "p", + parts: [ + "We collect data to operate ", + SITE_NAME, + ", authenticate you, process optional subscriptions, prevent trial abuse, keep the service secure, meet legal duties, measure usage when analytics are enabled, and show ads on the free tier when ads are on. This product is a stock research app, not a hiring or newsletter platform.", + ], + }, + { + type: "p", + parts: [ + "Processors may handle data in the European Union and in other countries (for example the United States). Where that happens, we rely on those providers' safeguards. By using the site you acknowledge that such transfers can occur.", + ], + }, + ], + }, + { + id: "cookies", + title: "Cookies", + blocks: [ + { + type: "p", + parts: [ + "This website uses cookies and similar storage (including local and session storage). Cookies are small text files stored on your device if your browser allows them.", + ], + }, + { + type: "p", + parts: [ + "We use them so the app can function (signed-in session, theme, trial safeguards) and, when enabled, to understand usage or deliver ads. Categories we may use:", + ], + }, + { + type: "ul", + items: [ + [ + "Strictly necessary — sign-in session and security features that the app cannot run without.", + ], + [ + "Performance — how people use the site, when analytics such as Google Tag Manager are configured.", + ], + ["Functionality — interface choices such as light or dark theme."], + [ + "Targeting or advertising — ads on the free tier, when advertising is enabled.", + ], + [ + "Third party — cookies set by processors such as Appwrite, Stripe, Google, or Apple when you use their flows.", + ], + ], + }, + { + type: "p", + parts: [ + "You can block or delete cookies in your browser. That may limit sign-in, trials, or other features.", + ], + }, + ], + }, + { + id: "third-party-software-and-services", + title: "Third-party software and services", + blocks: [ + { + type: "p", + parts: [ + "The app depends on third-party software and services. Those parties have their own privacy policies, which we may require you to accept. Our policy covers information we collect; it does not control how those parties use data they collect directly. Visiting linked sites is at your own risk. We welcome feedback if a third-party integration looks unsafe.", + ], + }, + { + type: "p", + parts: [ + "You can sign in with a single sign-on provider (Google or Apple). Read their privacy policies before you use them.", + ], + }, + { + type: "p", + parts: [ + "Footer links include our social profiles. If you interact with us there, those platforms' rules apply, and we may see the public information you share.", + ], + }, + ], + }, + { + id: "legal-rights", + title: "Legal rights", + blocks: [ + { + type: "p", + parts: [ + "Under applicable data-protection law you may have the right to access, be informed, correct, erase, restrict, object, port your data, complain to a supervisory authority, and withdraw consent. Requests that are not clearly unfounded, excessive, or repetitive are free of charge. We may charge a fee or refuse in those cases. We may ask for extra details to confirm it is you. Deleted data cannot always be removed from backups immediately.", + ], + }, + ], + }, + { + id: "children", + title: "Children", + blocks: [ + { + type: "p", + parts: [ + `${SITE_NAME} is not intended for children under 13 (or a higher age required where you live). We do not knowingly collect personal data from those children. A parent or guardian should contact us if a child is using the app; we will delete associated account data. By giving us your data you confirm you are old enough under local law. Children below that age should ask a parent or guardian to contact us.`, + ], + }, + ], + }, + { + id: "anonymization", + title: "Anonymization", + blocks: [ + { + type: "p", + parts: [ + "Where we can, we keep analytics and logs from identifying you. You can browse much of the site without an account. An account needs enough detail to sign you in. Fake details may break support, billing, or security features.", + ], + }, + ], + }, + { + id: "data-protection-officer", + title: "Data protection officer", + blocks: [ + { + type: "p", + parts: [ + "The data protection officer is ", + COMPANY_LEGAL.dpoName, + ". That person monitors data-protection compliance and is the contact for people whose data we process. Questions: ", + MAILTO, + " or ", + TEL, + ".", + ], + }, + ], + }, + { + id: "data-breaches", + title: "Data breaches", + blocks: [ + { + type: "p", + parts: [ + "If we detect a personal-data breach, we will inform affected users as soon as we reasonably can, and we will cooperate with legal authorities where required.", + ], + }, + ], + }, + { + id: "opposition-to-marketing-campaigns", + title: "Opposition to marketing campaigns", + blocks: [ + { + type: "p", + parts: [ + "Do not harvest contact details from this site for marketing campaigns. We may take action if that happens.", + ], + }, + ], + }, + ], +}; + +export const TERMS_DOCUMENT: LegalDocument = { + slug: "terms-of-use", + title: "Terms of Use", + subtitle: "Essential terms that apply when you use The Open Stock.", + path: "/terms-of-use", + description: `Terms of use for ${SITE_NAME}: not legal or investment advice, acceptable use, and limits of liability.`, + keywords: [ + "terms of use", + "not investment advice", + "not legal advice", + "disclaimer", + ], + sections: [ + { + id: "introduction", + title: "Introduction", + blocks: [ + { + type: "p", + parts: [ + "These ", + TERMS, + " describe the rules for individuals (you, your) who use our Assets: the website ", + HOME, + " and the ", + SITE_NAME, + " application, unless a page states its own terms.", + ], + }, + { + type: "p", + parts: [ + "They sit together with our ", + COPYRIGHTS, + " and ", + PRIVACY, + ". By accessing the site you agree to these legal rules. This information is free of charge.", + ], + }, + ], + }, + { + id: "about-ditectrev", + title: `About ${COMPANY_LEGAL.tradingAs}`, + blocks: [ + { + type: "p", + parts: [ + SITE_NAME, + " (", + HOME, + ") is operated by ", + COMPANY_LEGAL.legalName, + ` ("company", "${COMPANY_LEGAL.tradingAs}", "${COMPANY_LEGAL.legalName}", "our", "us", "we") — a company registered in ${COMPANY_LEGAL.country} under European registration number (VIES) ${COMPANY_LEGAL.vies}. Polish tax identification number (NIP) is ${COMPANY_LEGAL.nip}; business entity identification number (REGON) is ${COMPANY_LEGAL.regon}.`, + ], + }, + { + type: "p", + parts: [ + "Registered office and mail: ", + `${FOOTER_CONTACT.address}. `, + FOOTER_CONTACT.taxId, + ". Phone: ", + TEL, + ". Email: ", + MAILTO, + ".", + ], + }, + ], + }, + { + id: "disclaimers", + title: "Disclaimers", + blocks: [ + { + type: "p", + parts: [ + `${SITE_NAME} is not legal, tax, accounting, or investment advice. Nothing here is an offer to buy or sell any security or cryptoasset. We are not a broker, investment adviser, bank, or licensed financial institution.`, + ], + }, + { + type: "p", + parts: [ + "Charts, quotes, screeners, heatmaps, calendars, news, rankings, Fear & Greed, analyst targets, glossary definitions, and optional AI (including Stock of the Day and symbol predictions) are informational tools for your own research. They can be late, incomplete, or wrong. Third-party market data is provided as-is.", + ], + }, + { + type: "p", + parts: [ + "Past performance does not guarantee future results. You can lose money. Before you act, get advice from a qualified professional who knows your situation. Our content is not intended to advise you.", + ], + }, + { + type: "p", + parts: [ + "We may update the site and these legal rules (", + COPYRIGHTS, + ", ", + PRIVACY, + ", and ", + TERMS, + "). Check them from time to time. If versions conflict, the newest ", + TERMS, + " apply.", + ], + }, + { + type: "p", + parts: [ + "The site may link to or load third-party services. We do not control them and are not responsible for their content. Problems with those services have to be taken up with them. A link is not an endorsement.", + ], + }, + { + type: "p", + parts: [ + "You are responsible for making sure anyone who uses the site through your connection knows these rules.", + ], + }, + { + type: "p", + parts: [ + "If we do not exercise a right here, that is not a waiver. These ", + TERMS, + " are an agreement between you and the company; they do not confer rights on other people.", + ], + }, + { + type: "p", + parts: [ + "You may link to public pages that exist. Do not present a link as if it were our page when it is not. We may withdraw linking permission without notice.", + ], + }, + ], + }, + { + id: "intellectual-property", + title: "Intellectual property", + blocks: [ + { + type: "p", + parts: [ + "Use of this website is also governed by our ", + COPYRIGHTS, + " and ", + PRIVACY, + ". By accessing the site you agree to them.", + ], + }, + { + type: "p", + parts: [ + "If you submit material (for example account details, saved API keys, or feedback), you grant us a non-exclusive license to use it as needed to operate ", + SITE_NAME, + ". We treat that material as needed to provide the service, not as public content, unless you post it publicly yourself.", + ], + }, + ], + }, + { + id: "cyber-security", + title: "Cyber security", + blocks: [ + { + type: "p", + parts: [ + "We cannot guarantee that the site will be free of bugs, viruses, or other harmful content. You are responsible for the security of the device and software you use to access it, including your own virus protection.", + ], + }, + { + type: "p", + parts: [ + "Do not introduce malware or attack the site. Unauthorized access, scanning for weaknesses, theft of API keys, denial-of-service, and similar activity is forbidden and may be a criminal offence under applicable computer-misuse and cybercrime laws. We may report it to authorities, cooperate with them, and end your access immediately.", + ], + }, + { + type: "p", + parts: [ + "You are responsible for content you submit and for keeping your own backups. We may remove submitted material without giving a reason.", + ], + }, + ], + }, + { + id: "privacy", + title: "Privacy", + blocks: [ + { + type: "p", + parts: [ + "Use of the website is also governed by our ", + PRIVACY, + " page, which explains how we may use personal data.", + ], + }, + ], + }, + { + id: "availability", + title: "Availability", + blocks: [ + { + type: "p", + parts: [ + "The website and Assets are provided “as is” and on an “as available” basis. We do not guarantee they will be free of defects. We may suspend or withdraw all or part of the service. We try to keep market data and copy up to date, but we do not warrant that it is always accurate.", + ], + }, + { + type: "p", + parts: [ + "You may use the site from a computer or mobile device that supports HTML, CSS, and JavaScript. We aim for accessibility but cannot guarantee full Web Content Accessibility Guidelines (WCAG) conformance.", + ], + }, + ], + }, + { + id: "limitations-of-liability", + title: "Limitations of liability", + blocks: [ + { + type: "p", + parts: [ + "To the maximum extent permitted by law we do not accept liability for direct or indirect loss — including trading losses, missed opportunities, business interruption, lost profits, lost data, or reputational harm — whether or not it was foreseeable, and whether it comes from market data, AI output, or downtime.", + ], + }, + { + type: "p", + parts: [ + "We do not exclude or limit liability for death or personal injury caused by our negligence, or any other liability that cannot lawfully be excluded.", + ], + }, + ], + }, + ], +}; + +export const LEGAL_DOCUMENTS = [ + COPYRIGHTS_DOCUMENT, + PRIVACY_DOCUMENT, + TERMS_DOCUMENT, +] as const; + +export function getLegalDocument( + slug: LegalDocument["slug"] +): LegalDocument | undefined { + return LEGAL_DOCUMENTS.find((doc) => doc.slug === slug); +} + +export function isLegalLink(part: LegalInline): part is LegalLink { + return typeof part === "object" && "href" in part; +} + +export function flattenLegalText(parts: readonly LegalInline[]): string { + return parts + .map((part) => (typeof part === "string" ? part : part.label)) + .join(""); +} + +export function legalDocumentPlainText(doc: LegalDocument): string { + return doc.sections + .flatMap((section) => + section.blocks.flatMap((block) => { + if (block.type === "p") return [flattenLegalText(block.parts)]; + return block.items.map((item) => flattenLegalText(item)); + }) + ) + .join("\n"); +} diff --git a/lib/site-seo.ts b/lib/site-seo.ts index c7c0a37..24eff6d 100644 --- a/lib/site-seo.ts +++ b/lib/site-seo.ts @@ -228,4 +228,17 @@ export const PUBLIC_ROUTES = [ changeFrequency: "daily" as const, }, { path: "/pricing", priority: 0.7, changeFrequency: "weekly" as const }, + { path: "/glossary", priority: 0.55, changeFrequency: "monthly" as const }, + { path: "/copyrights", priority: 0.3, changeFrequency: "yearly" as const }, + { + path: "/privacy-and-security", + priority: 0.3, + changeFrequency: "yearly" as const, + }, + { path: "/sitemap", priority: 0.3, changeFrequency: "monthly" as const }, + { + path: "/terms-of-use", + priority: 0.3, + changeFrequency: "yearly" as const, + }, ]; From 889945c12d5abe5cc4258919069a8dd06cbe660c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 29 Sep 2026 05:51:11 +0000 Subject: [PATCH 2/2] Keep glossary off the XML sitemap list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ✅ Daniel Danielecki --- lib/site-seo.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/site-seo.ts b/lib/site-seo.ts index 24eff6d..5efaebc 100644 --- a/lib/site-seo.ts +++ b/lib/site-seo.ts @@ -228,7 +228,6 @@ export const PUBLIC_ROUTES = [ changeFrequency: "daily" as const, }, { path: "/pricing", priority: 0.7, changeFrequency: "weekly" as const }, - { path: "/glossary", priority: 0.55, changeFrequency: "monthly" as const }, { path: "/copyrights", priority: 0.3, changeFrequency: "yearly" as const }, { path: "/privacy-and-security",