feat(seo): add Seo component and robots.txtUnverified
c177ffe parent: 9010e40 added
web/src/lib/components/Seo.svelte +56 -0 | new file mode 100644 | ||
| @@ -0,0 +1,56 @@ | ||
| 1 | +<script lang="ts"> | |
| 2 | + import { page } from "$app/state"; | |
| 3 | + | |
| 4 | + interface Props { | |
| 5 | + title?: string; | |
| 6 | + description?: string; | |
| 7 | + type?: string; | |
| 8 | + noindex?: boolean; | |
| 9 | + } | |
| 10 | + | |
| 11 | + let { | |
| 12 | + title, | |
| 13 | + description, | |
| 14 | + type = "website", | |
| 15 | + noindex = false, | |
| 16 | + }: Props = $props(); | |
| 17 | + | |
| 18 | + const siteName = "Glean"; | |
| 19 | + const pageTitle = $derived( | |
| 20 | + title ? `${title} — ${siteName}` : `${siteName} — The social RSS reader`, | |
| 21 | + ); | |
| 22 | + const canonical = $derived(page.url.origin + page.url.pathname); | |
| 23 | + const absoluteImage = $derived(new URL("/banner.png", page.url).href); | |
| 24 | + const plainDescription = $derived( | |
| 25 | + (description ?? "") | |
| 26 | + .replace(/<[^>]*>/g, " ") | |
| 27 | + .replace(/\s+/g, " ") | |
| 28 | + .trim() | |
| 29 | + .slice(0, 200), | |
| 30 | + ); | |
| 31 | +</script> | |
| 32 | + | |
| 33 | +<svelte:head> | |
| 34 | + <title>{pageTitle}</title> | |
| 35 | + {#if plainDescription} | |
| 36 | + <meta name="description" content={plainDescription} /> | |
| 37 | + {/if} | |
| 38 | + <link rel="canonical" href={canonical} /> | |
| 39 | + {#if noindex} | |
| 40 | + <meta name="robots" content="noindex" /> | |
| 41 | + {/if} | |
| 42 | + <meta property="og:site_name" content={siteName} /> | |
| 43 | + <meta property="og:title" content={pageTitle} /> | |
| 44 | + {#if plainDescription} | |
| 45 | + <meta property="og:description" content={plainDescription} /> | |
| 46 | + {/if} | |
| 47 | + <meta property="og:type" content={type} /> | |
| 48 | + <meta property="og:url" content={canonical} /> | |
| 49 | + <meta property="og:image" content={absoluteImage} /> | |
| 50 | + <meta name="twitter:card" content="summary_large_image" /> | |
| 51 | + <meta name="twitter:title" content={pageTitle} /> | |
| 52 | + {#if plainDescription} | |
| 53 | + <meta name="twitter:description" content={plainDescription} /> | |
| 54 | + {/if} | |
| 55 | + <meta name="twitter:image" content={absoluteImage} /> | |
| 56 | +</svelte:head> | |
| new file mode 100644 | |||
| @@ -0,0 +1,56 @@ | |||
| 1 | +<script lang="ts"> | ||
| 2 | + import { page } from "$app/state"; | ||
| 3 | + | ||
| 4 | + interface Props { | ||
| 5 | + title?: string; | ||
| 6 | + description?: string; | ||
| 7 | + type?: string; | ||
| 8 | + noindex?: boolean; | ||
| 9 | + } | ||
| 10 | + | ||
| 11 | + let { | ||
| 12 | + title, | ||
| 13 | + description, | ||
| 14 | + type = "website", | ||
| 15 | + noindex = false, | ||
| 16 | + }: Props = $props(); | ||
| 17 | + | ||
| 18 | + const siteName = "Glean"; | ||
| 19 | + const pageTitle = $derived( | ||
| 20 | + title ? `${title} — ${siteName}` : `${siteName} — The social RSS reader`, | ||
| 21 | + ); | ||
| 22 | + const canonical = $derived(page.url.origin + page.url.pathname); | ||
| 23 | + const absoluteImage = $derived(new URL("/banner.png", page.url).href); | ||
| 24 | + const plainDescription = $derived( | ||
| 25 | + (description ?? "") | ||
| 26 | + .replace(/<[^>]*>/g, " ") | ||
| 27 | + .replace(/\s+/g, " ") | ||
| 28 | + .trim() | ||
| 29 | + .slice(0, 200), | ||
| 30 | + ); | ||
| 31 | +</script> | ||
| 32 | + | ||
| 33 | +<svelte:head> | ||
| 34 | + <title>{pageTitle}</title> | ||
| 35 | + {#if plainDescription} | ||
| 36 | + <meta name="description" content={plainDescription} /> | ||
| 37 | + {/if} | ||
| 38 | + <link rel="canonical" href={canonical} /> | ||
| 39 | + {#if noindex} | ||
| 40 | + <meta name="robots" content="noindex" /> | ||
| 41 | + {/if} | ||
| 42 | + <meta property="og:site_name" content={siteName} /> | ||
| 43 | + <meta property="og:title" content={pageTitle} /> | ||
| 44 | + {#if plainDescription} | ||
| 45 | + <meta property="og:description" content={plainDescription} /> | ||
| 46 | + {/if} | ||
| 47 | + <meta property="og:type" content={type} /> | ||
| 48 | + <meta property="og:url" content={canonical} /> | ||
| 49 | + <meta property="og:image" content={absoluteImage} /> | ||
| 50 | + <meta name="twitter:card" content="summary_large_image" /> | ||
| 51 | + <meta name="twitter:title" content={pageTitle} /> | ||
| 52 | + {#if plainDescription} | ||
| 53 | + <meta name="twitter:description" content={plainDescription} /> | ||
| 54 | + {/if} | ||
| 55 | + <meta name="twitter:image" content={absoluteImage} /> | ||
| 56 | +</svelte:head> | ||
modified
web/src/routes/+error.svelte +6 -0 | @@ -1,8 +1,14 @@ | ||
| 1 | 1 | <script lang="ts"> |
| 2 | 2 | import { page } from "$app/state"; |
| 3 | 3 | import Logo from "$lib/components/Logo.svelte"; |
| 4 | + import Seo from "$lib/components/Seo.svelte"; | |
| 4 | 5 | </script> |
| 5 | 6 | |
| 7 | +<Seo | |
| 8 | + title={page.status === 404 ? "Not found" : "Error " + page.status} | |
| 9 | + noindex | |
| 10 | +/> | |
| 11 | + | |
| 6 | 12 | <div class="flex min-h-screen flex-col items-center justify-center px-4 py-12"> |
| 7 | 13 | <div class="w-full max-w-sm text-center"> |
| 8 | 14 | <div class="mb-8 flex justify-center"><Logo size="lg" /></div> |
| @@ -1,8 +1,14 @@ | |||
| 1 | <script lang="ts"> | 1 | <script lang="ts"> |
| 2 | import { page } from "$app/state"; | 2 | import { page } from "$app/state"; |
| 3 | import Logo from "$lib/components/Logo.svelte"; | 3 | import Logo from "$lib/components/Logo.svelte"; |
| 4 | + import Seo from "$lib/components/Seo.svelte"; | ||
| 4 | </script> | 5 | </script> |
| 5 | 6 | ||
| 7 | +<Seo | ||
| 8 | + title={page.status === 404 ? "Not found" : "Error " + page.status} | ||
| 9 | + noindex | ||
| 10 | +/> | ||
| 11 | + | ||
| 6 | <div class="flex min-h-screen flex-col items-center justify-center px-4 py-12"> | 12 | <div class="flex min-h-screen flex-col items-center justify-center px-4 py-12"> |
| 7 | <div class="w-full max-w-sm text-center"> | 13 | <div class="w-full max-w-sm text-center"> |
| 8 | <div class="mb-8 flex justify-center"><Logo size="lg" /></div> | 14 | <div class="mb-8 flex justify-center"><Logo size="lg" /></div> |
modified
web/src/routes/+layout.svelte +0 -8 | @@ -121,14 +121,6 @@ | ||
| 121 | 121 | </script> |
| 122 | 122 | |
| 123 | 123 | <svelte:head> |
| 124 | - <title>Glean</title> | |
| 125 | - <meta property="og:title" content="Glean" /> | |
| 126 | - <meta | |
| 127 | - property="og:description" | |
| 128 | - content="The social RSS reader built on AT Protocol." | |
| 129 | - /> | |
| 130 | - <meta property="og:image" content="/banner.png" /> | |
| 131 | - <meta property="og:type" content="website" /> | |
| 132 | 124 | <meta name="theme-color" content="#00754A" /> |
| 133 | 125 | <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> |
| 134 | 126 | <link rel="manifest" href="/manifest.json" /> |
| @@ -121,14 +121,6 @@ | |||
| 121 | </script> | 121 | </script> |
| 122 | 122 | ||
| 123 | <svelte:head> | 123 | <svelte:head> |
| 124 | - <title>Glean</title> | ||
| 125 | - <meta property="og:title" content="Glean" /> | ||
| 126 | - <meta | ||
| 127 | - property="og:description" | ||
| 128 | - content="The social RSS reader built on AT Protocol." | ||
| 129 | - /> | ||
| 130 | - <meta property="og:image" content="/banner.png" /> | ||
| 131 | - <meta property="og:type" content="website" /> | ||
| 132 | <meta name="theme-color" content="#00754A" /> | 124 | <meta name="theme-color" content="#00754A" /> |
| 133 | <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> | 125 | <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> |
| 134 | <link rel="manifest" href="/manifest.json" /> | 126 | <link rel="manifest" href="/manifest.json" /> |
modified
web/src/routes/+page.svelte +5 -0 | @@ -2,8 +2,13 @@ | ||
| 2 | 2 | // Landing page is static; auth gating happens in +page.server.ts. |
| 3 | 3 | import Logo from "$lib/components/Logo.svelte"; |
| 4 | 4 | import Icon from "$lib/components/Icon.svelte"; |
| 5 | + import Seo from "$lib/components/Seo.svelte"; | |
| 5 | 6 | </script> |
| 6 | 7 | |
| 8 | +<Seo | |
| 9 | + description="The social RSS reader built on AT Protocol. Read your feeds, see what your circle reads, and discover new sources through personalized recommendations." | |
| 10 | +/> | |
| 11 | + | |
| 7 | 12 | <!-- HERO --> |
| 8 | 13 | <section class="border-b-2 border-[var(--border)]"> |
| 9 | 14 | <div |
| @@ -2,8 +2,13 @@ | |||
| 2 | // Landing page is static; auth gating happens in +page.server.ts. | 2 | // Landing page is static; auth gating happens in +page.server.ts. |
| 3 | import Logo from "$lib/components/Logo.svelte"; | 3 | import Logo from "$lib/components/Logo.svelte"; |
| 4 | import Icon from "$lib/components/Icon.svelte"; | 4 | import Icon from "$lib/components/Icon.svelte"; |
| 5 | + import Seo from "$lib/components/Seo.svelte"; | ||
| 5 | </script> | 6 | </script> |
| 6 | 7 | ||
| 8 | +<Seo | ||
| 9 | + description="The social RSS reader built on AT Protocol. Read your feeds, see what your circle reads, and discover new sources through personalized recommendations." | ||
| 10 | +/> | ||
| 11 | + | ||
| 7 | <!-- HERO --> | 12 | <!-- HERO --> |
| 8 | <section class="border-b-2 border-[var(--border)]"> | 13 | <section class="border-b-2 border-[var(--border)]"> |
| 9 | <div | 14 | <div |
modified
web/src/routes/articles/+page.svelte +3 -0 | @@ -7,6 +7,7 @@ | ||
| 7 | 7 | import EmptyState from "$lib/components/EmptyState.svelte"; |
| 8 | 8 | import Icon from "$lib/components/Icon.svelte"; |
| 9 | 9 | import NewArticlesBanner from "$lib/components/NewArticlesBanner.svelte"; |
| 10 | + import Seo from "$lib/components/Seo.svelte"; | |
| 10 | 11 | import { endpoints } from "$lib/api"; |
| 11 | 12 | |
| 12 | 13 | let { data }: { data: PageData } = $props(); |
| @@ -88,6 +89,8 @@ | ||
| 88 | 89 | } |
| 89 | 90 | </script> |
| 90 | 91 | |
| 92 | +<Seo title={data.feed?.title || "Articles"} /> | |
| 93 | + | |
| 91 | 94 | {#if data.feed} |
| 92 | 95 | <!-- Feed header --> |
| 93 | 96 | <div class="mb-6"> |
| @@ -7,6 +7,7 @@ | |||
| 7 | import EmptyState from "$lib/components/EmptyState.svelte"; | 7 | import EmptyState from "$lib/components/EmptyState.svelte"; |
| 8 | import Icon from "$lib/components/Icon.svelte"; | 8 | import Icon from "$lib/components/Icon.svelte"; |
| 9 | import NewArticlesBanner from "$lib/components/NewArticlesBanner.svelte"; | 9 | import NewArticlesBanner from "$lib/components/NewArticlesBanner.svelte"; |
| 10 | + import Seo from "$lib/components/Seo.svelte"; | ||
| 10 | import { endpoints } from "$lib/api"; | 11 | import { endpoints } from "$lib/api"; |
| 11 | 12 | ||
| 12 | let { data }: { data: PageData } = $props(); | 13 | let { data }: { data: PageData } = $props(); |
| @@ -88,6 +89,8 @@ | |||
| 88 | } | 89 | } |
| 89 | </script> | 90 | </script> |
| 90 | 91 | ||
| 92 | +<Seo title={data.feed?.title || "Articles"} /> | ||
| 93 | + | ||
| 91 | {#if data.feed} | 94 | {#if data.feed} |
| 92 | <!-- Feed header --> | 95 | <!-- Feed header --> |
| 93 | <div class="mb-6"> | 96 | <div class="mb-6"> |
modified
web/src/routes/articles/[id]/+page.svelte +7 -0 | @@ -4,6 +4,7 @@ | ||
| 4 | 4 | import Favicon from "$lib/components/Favicon.svelte"; |
| 5 | 5 | import AnnotationCard from "$lib/components/AnnotationCard.svelte"; |
| 6 | 6 | import Icon from "$lib/components/Icon.svelte"; |
| 7 | + import Seo from "$lib/components/Seo.svelte"; | |
| 7 | 8 | import BlueskyLogo from "$lib/components/BlueskyLogo.svelte"; |
| 8 | 9 | import { endpoints } from "$lib/api"; |
| 9 | 10 | import { youtubeID, isEmbedURL } from "$lib/format"; |
| @@ -451,6 +452,12 @@ | ||
| 451 | 452 | } |
| 452 | 453 | </script> |
| 453 | 454 | |
| 455 | +<Seo | |
| 456 | + title={data.article.title} | |
| 457 | + description={data.article.summary} | |
| 458 | + type="article" | |
| 459 | +/> | |
| 460 | + | |
| 454 | 461 | <svelte:window |
| 455 | 462 | onmouseup={onMouseUp} |
| 456 | 463 | onkeydown={onKeydown} |
| @@ -4,6 +4,7 @@ | |||
| 4 | import Favicon from "$lib/components/Favicon.svelte"; | 4 | import Favicon from "$lib/components/Favicon.svelte"; |
| 5 | import AnnotationCard from "$lib/components/AnnotationCard.svelte"; | 5 | import AnnotationCard from "$lib/components/AnnotationCard.svelte"; |
| 6 | import Icon from "$lib/components/Icon.svelte"; | 6 | import Icon from "$lib/components/Icon.svelte"; |
| 7 | + import Seo from "$lib/components/Seo.svelte"; | ||
| 7 | import BlueskyLogo from "$lib/components/BlueskyLogo.svelte"; | 8 | import BlueskyLogo from "$lib/components/BlueskyLogo.svelte"; |
| 8 | import { endpoints } from "$lib/api"; | 9 | import { endpoints } from "$lib/api"; |
| 9 | import { youtubeID, isEmbedURL } from "$lib/format"; | 10 | import { youtubeID, isEmbedURL } from "$lib/format"; |
| @@ -451,6 +452,12 @@ | |||
| 451 | } | 452 | } |
| 452 | </script> | 453 | </script> |
| 453 | 454 | ||
| 455 | +<Seo | ||
| 456 | + title={data.article.title} | ||
| 457 | + description={data.article.summary} | ||
| 458 | + type="article" | ||
| 459 | +/> | ||
| 460 | + | ||
| 454 | <svelte:window | 461 | <svelte:window |
| 455 | onmouseup={onMouseUp} | 462 | onmouseup={onMouseUp} |
| 456 | onkeydown={onKeydown} | 463 | onkeydown={onKeydown} |
modified
web/src/routes/auth/login/+page.svelte +6 -0 | @@ -1,6 +1,7 @@ | ||
| 1 | 1 | <script lang="ts"> |
| 2 | 2 | import { endpoints } from "$lib/api"; |
| 3 | 3 | import Logo from "$lib/components/Logo.svelte"; |
| 4 | + import Seo from "$lib/components/Seo.svelte"; | |
| 4 | 5 | import type { Actor } from "$lib/types"; |
| 5 | 6 | |
| 6 | 7 | let handle = $state(""); |
| @@ -70,6 +71,11 @@ | ||
| 70 | 71 | } |
| 71 | 72 | </script> |
| 72 | 73 | |
| 74 | +<Seo | |
| 75 | + title="Sign in" | |
| 76 | + description="Sign in to Glean with your Bluesky handle or any AT Protocol account." | |
| 77 | +/> | |
| 78 | + | |
| 73 | 79 | <div class="flex min-h-screen items-center justify-center px-4 py-12"> |
| 74 | 80 | <div class="w-full max-w-sm"> |
| 75 | 81 | <div class="mb-8 flex justify-center"> |
| @@ -1,6 +1,7 @@ | |||
| 1 | <script lang="ts"> | 1 | <script lang="ts"> |
| 2 | import { endpoints } from "$lib/api"; | 2 | import { endpoints } from "$lib/api"; |
| 3 | import Logo from "$lib/components/Logo.svelte"; | 3 | import Logo from "$lib/components/Logo.svelte"; |
| 4 | + import Seo from "$lib/components/Seo.svelte"; | ||
| 4 | import type { Actor } from "$lib/types"; | 5 | import type { Actor } from "$lib/types"; |
| 5 | 6 | ||
| 6 | let handle = $state(""); | 7 | let handle = $state(""); |
| @@ -70,6 +71,11 @@ | |||
| 70 | } | 71 | } |
| 71 | </script> | 72 | </script> |
| 72 | 73 | ||
| 74 | +<Seo | ||
| 75 | + title="Sign in" | ||
| 76 | + description="Sign in to Glean with your Bluesky handle or any AT Protocol account." | ||
| 77 | +/> | ||
| 78 | + | ||
| 73 | <div class="flex min-h-screen items-center justify-center px-4 py-12"> | 79 | <div class="flex min-h-screen items-center justify-center px-4 py-12"> |
| 74 | <div class="w-full max-w-sm"> | 80 | <div class="w-full max-w-sm"> |
| 75 | <div class="mb-8 flex justify-center"> | 81 | <div class="mb-8 flex justify-center"> |
modified
web/src/routes/dashboard/+page.svelte +3 -0 | @@ -7,6 +7,7 @@ | ||
| 7 | 7 | import EmptyState from "$lib/components/EmptyState.svelte"; |
| 8 | 8 | import Icon from "$lib/components/Icon.svelte"; |
| 9 | 9 | import NewArticlesBanner from "$lib/components/NewArticlesBanner.svelte"; |
| 10 | + import Seo from "$lib/components/Seo.svelte"; | |
| 10 | 11 | import { invalidateAll } from "$app/navigation"; |
| 11 | 12 | import { endpoints } from "$lib/api"; |
| 12 | 13 | import type { |
| @@ -88,6 +89,8 @@ | ||
| 88 | 89 | ]); |
| 89 | 90 | </script> |
| 90 | 91 | |
| 92 | +<Seo title="Dashboard" /> | |
| 93 | + | |
| 91 | 94 | <!-- Header --> |
| 92 | 95 | <div |
| 93 | 96 | class="mb-6 flex flex-wrap items-end justify-between gap-3 border-b-2 border-[var(--border)] pb-4" |
| @@ -7,6 +7,7 @@ | |||
| 7 | import EmptyState from "$lib/components/EmptyState.svelte"; | 7 | import EmptyState from "$lib/components/EmptyState.svelte"; |
| 8 | import Icon from "$lib/components/Icon.svelte"; | 8 | import Icon from "$lib/components/Icon.svelte"; |
| 9 | import NewArticlesBanner from "$lib/components/NewArticlesBanner.svelte"; | 9 | import NewArticlesBanner from "$lib/components/NewArticlesBanner.svelte"; |
| 10 | + import Seo from "$lib/components/Seo.svelte"; | ||
| 10 | import { invalidateAll } from "$app/navigation"; | 11 | import { invalidateAll } from "$app/navigation"; |
| 11 | import { endpoints } from "$lib/api"; | 12 | import { endpoints } from "$lib/api"; |
| 12 | import type { | 13 | import type { |
| @@ -88,6 +89,8 @@ | |||
| 88 | ]); | 89 | ]); |
| 89 | </script> | 90 | </script> |
| 90 | 91 | ||
| 92 | +<Seo title="Dashboard" /> | ||
| 93 | + | ||
| 91 | <!-- Header --> | 94 | <!-- Header --> |
| 92 | <div | 95 | <div |
| 93 | class="mb-6 flex flex-wrap items-end justify-between gap-3 border-b-2 border-[var(--border)] pb-4" | 96 | class="mb-6 flex flex-wrap items-end justify-between gap-3 border-b-2 border-[var(--border)] pb-4" |
modified
web/src/routes/feeds/+page.svelte +3 -0 | @@ -6,6 +6,7 @@ | ||
| 6 | 6 | import EmptyState from "$lib/components/EmptyState.svelte"; |
| 7 | 7 | import FeedRecommendationCard from "$lib/components/FeedRecommendationCard.svelte"; |
| 8 | 8 | import Icon from "$lib/components/Icon.svelte"; |
| 9 | + import Seo from "$lib/components/Seo.svelte"; | |
| 9 | 10 | import { endpoints } from "$lib/api"; |
| 10 | 11 | import { invalidateAll } from "$app/navigation"; |
| 11 | 12 | import type { FeedRecommendation } from "$lib/types"; |
| @@ -90,6 +91,8 @@ | ||
| 90 | 91 | } |
| 91 | 92 | </script> |
| 92 | 93 | |
| 94 | +<Seo title="Discover feeds" /> | |
| 95 | + | |
| 93 | 96 | <div class="flex items-center justify-between gap-3 mb-2 flex-wrap"> |
| 94 | 97 | <h1 class="text-2xl font-extrabold uppercase tracking-tight"> |
| 95 | 98 | Feeds <span class="text-base font-normal text-[var(--muted)]" |
| @@ -6,6 +6,7 @@ | |||
| 6 | import EmptyState from "$lib/components/EmptyState.svelte"; | 6 | import EmptyState from "$lib/components/EmptyState.svelte"; |
| 7 | import FeedRecommendationCard from "$lib/components/FeedRecommendationCard.svelte"; | 7 | import FeedRecommendationCard from "$lib/components/FeedRecommendationCard.svelte"; |
| 8 | import Icon from "$lib/components/Icon.svelte"; | 8 | import Icon from "$lib/components/Icon.svelte"; |
| 9 | + import Seo from "$lib/components/Seo.svelte"; | ||
| 9 | import { endpoints } from "$lib/api"; | 10 | import { endpoints } from "$lib/api"; |
| 10 | import { invalidateAll } from "$app/navigation"; | 11 | import { invalidateAll } from "$app/navigation"; |
| 11 | import type { FeedRecommendation } from "$lib/types"; | 12 | import type { FeedRecommendation } from "$lib/types"; |
| @@ -90,6 +91,8 @@ | |||
| 90 | } | 91 | } |
| 91 | </script> | 92 | </script> |
| 92 | 93 | ||
| 94 | +<Seo title="Discover feeds" /> | ||
| 95 | + | ||
| 93 | <div class="flex items-center justify-between gap-3 mb-2 flex-wrap"> | 96 | <div class="flex items-center justify-between gap-3 mb-2 flex-wrap"> |
| 94 | <h1 class="text-2xl font-extrabold uppercase tracking-tight"> | 97 | <h1 class="text-2xl font-extrabold uppercase tracking-tight"> |
| 95 | Feeds <span class="text-base font-normal text-[var(--muted)]" | 98 | Feeds <span class="text-base font-normal text-[var(--muted)]" |
modified
web/src/routes/library/+page.svelte +3 -0 | @@ -3,6 +3,7 @@ | ||
| 3 | 3 | import ArticleCard from "$lib/components/ArticleCard.svelte"; |
| 4 | 4 | import AnnotationCard from "$lib/components/AnnotationCard.svelte"; |
| 5 | 5 | import EmptyState from "$lib/components/EmptyState.svelte"; |
| 6 | + import Seo from "$lib/components/Seo.svelte"; | |
| 6 | 7 | |
| 7 | 8 | let { data }: { data: PageData } = $props(); |
| 8 | 9 | |
| @@ -22,6 +23,8 @@ | ||
| 22 | 23 | } |
| 23 | 24 | </script> |
| 24 | 25 | |
| 26 | +<Seo title="Library" /> | |
| 27 | + | |
| 25 | 28 | <h1 class="text-2xl font-extrabold uppercase tracking-tight mb-2">Library</h1> |
| 26 | 29 | <p class="mb-6 text-xs uppercase tracking-widest text-[var(--muted)]"> |
| 27 | 30 | Your liked articles and annotations. |
| @@ -3,6 +3,7 @@ | |||
| 3 | import ArticleCard from "$lib/components/ArticleCard.svelte"; | 3 | import ArticleCard from "$lib/components/ArticleCard.svelte"; |
| 4 | import AnnotationCard from "$lib/components/AnnotationCard.svelte"; | 4 | import AnnotationCard from "$lib/components/AnnotationCard.svelte"; |
| 5 | import EmptyState from "$lib/components/EmptyState.svelte"; | 5 | import EmptyState from "$lib/components/EmptyState.svelte"; |
| 6 | + import Seo from "$lib/components/Seo.svelte"; | ||
| 6 | 7 | ||
| 7 | let { data }: { data: PageData } = $props(); | 8 | let { data }: { data: PageData } = $props(); |
| 8 | 9 | ||
| @@ -22,6 +23,8 @@ | |||
| 22 | } | 23 | } |
| 23 | </script> | 24 | </script> |
| 24 | 25 | ||
| 26 | +<Seo title="Library" /> | ||
| 27 | + | ||
| 25 | <h1 class="text-2xl font-extrabold uppercase tracking-tight mb-2">Library</h1> | 28 | <h1 class="text-2xl font-extrabold uppercase tracking-tight mb-2">Library</h1> |
| 26 | <p class="mb-6 text-xs uppercase tracking-widest text-[var(--muted)]"> | 29 | <p class="mb-6 text-xs uppercase tracking-widest text-[var(--muted)]"> |
| 27 | Your liked articles and annotations. | 30 | Your liked articles and annotations. |
modified
web/src/routes/profile/[did]/+page.svelte +3 -0 | @@ -6,6 +6,7 @@ | ||
| 6 | 6 | import Icon from "$lib/components/Icon.svelte"; |
| 7 | 7 | import BlueskyLogo from "$lib/components/BlueskyLogo.svelte"; |
| 8 | 8 | import { endpoints } from "$lib/api"; |
| 9 | + import Seo from "$lib/components/Seo.svelte"; | |
| 9 | 10 | import { invalidateAll } from "$app/navigation"; |
| 10 | 11 | |
| 11 | 12 | let { data }: { data: PageData } = $props(); |
| @@ -39,6 +40,8 @@ | ||
| 39 | 40 | } |
| 40 | 41 | </script> |
| 41 | 42 | |
| 43 | +<Seo title={data.profile_user.display_name || data.profile_user.handle} /> | |
| 44 | + | |
| 42 | 45 | <div class="mx-auto max-w-2xl"> |
| 43 | 46 | <section class="panel mb-6 p-4 sm:p-6"> |
| 44 | 47 | <div |
| @@ -6,6 +6,7 @@ | |||
| 6 | import Icon from "$lib/components/Icon.svelte"; | 6 | import Icon from "$lib/components/Icon.svelte"; |
| 7 | import BlueskyLogo from "$lib/components/BlueskyLogo.svelte"; | 7 | import BlueskyLogo from "$lib/components/BlueskyLogo.svelte"; |
| 8 | import { endpoints } from "$lib/api"; | 8 | import { endpoints } from "$lib/api"; |
| 9 | + import Seo from "$lib/components/Seo.svelte"; | ||
| 9 | import { invalidateAll } from "$app/navigation"; | 10 | import { invalidateAll } from "$app/navigation"; |
| 10 | 11 | ||
| 11 | let { data }: { data: PageData } = $props(); | 12 | let { data }: { data: PageData } = $props(); |
| @@ -39,6 +40,8 @@ | |||
| 39 | } | 40 | } |
| 40 | </script> | 41 | </script> |
| 41 | 42 | ||
| 43 | +<Seo title={data.profile_user.display_name || data.profile_user.handle} /> | ||
| 44 | + | ||
| 42 | <div class="mx-auto max-w-2xl"> | 45 | <div class="mx-auto max-w-2xl"> |
| 43 | <section class="panel mb-6 p-4 sm:p-6"> | 46 | <section class="panel mb-6 p-4 sm:p-6"> |
| 44 | <div | 47 | <div |
added
web/src/routes/robots.txt/+server.ts +19 -0 | new file mode 100644 | ||
| @@ -0,0 +1,19 @@ | ||
| 1 | +import type { RequestHandler } from "./$types"; | |
| 2 | + | |
| 3 | +export const GET: RequestHandler = ({ url }) => { | |
| 4 | + const body = [ | |
| 5 | + "User-agent: *", | |
| 6 | + "Disallow: /dashboard", | |
| 7 | + "Disallow: /articles", | |
| 8 | + "Disallow: /feeds", | |
| 9 | + "Disallow: /library", | |
| 10 | + "Disallow: /profile", | |
| 11 | + "Disallow: /stats", | |
| 12 | + "", | |
| 13 | + `Sitemap: ${url.origin}/sitemap.xml`, | |
| 14 | + "", | |
| 15 | + ].join("\n"); | |
| 16 | + return new Response(body, { | |
| 17 | + headers: { "content-type": "text/plain; charset=utf-8" }, | |
| 18 | + }); | |
| 19 | +}; | |
| new file mode 100644 | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | +import type { RequestHandler } from "./$types"; | ||
| 2 | + | ||
| 3 | +export const GET: RequestHandler = ({ url }) => { | ||
| 4 | + const body = [ | ||
| 5 | + "User-agent: *", | ||
| 6 | + "Disallow: /dashboard", | ||
| 7 | + "Disallow: /articles", | ||
| 8 | + "Disallow: /feeds", | ||
| 9 | + "Disallow: /library", | ||
| 10 | + "Disallow: /profile", | ||
| 11 | + "Disallow: /stats", | ||
| 12 | + "", | ||
| 13 | + `Sitemap: ${url.origin}/sitemap.xml`, | ||
| 14 | + "", | ||
| 15 | + ].join("\n"); | ||
| 16 | + return new Response(body, { | ||
| 17 | + headers: { "content-type": "text/plain; charset=utf-8" }, | ||
| 18 | + }); | ||
| 19 | +}; | ||
modified
web/src/routes/stats/+page.svelte +3 -0 | @@ -1,12 +1,15 @@ | ||
| 1 | 1 | <script lang="ts"> |
| 2 | 2 | import type { PageData } from "./$types"; |
| 3 | 3 | import EmptyState from "$lib/components/EmptyState.svelte"; |
| 4 | + import Seo from "$lib/components/Seo.svelte"; | |
| 4 | 5 | |
| 5 | 6 | let { data }: { data: PageData } = $props(); |
| 6 | 7 | |
| 7 | 8 | const categories = $derived(Object.keys(data.metrics)); |
| 8 | 9 | </script> |
| 9 | 10 | |
| 11 | +<Seo title="Stats" /> | |
| 12 | + | |
| 10 | 13 | <h1 class="text-2xl font-extrabold uppercase tracking-tight mb-2">Stats</h1> |
| 11 | 14 | <p class="mb-6 text-xs uppercase tracking-widest text-[var(--muted)]"> |
| 12 | 15 | Application metrics and performance data. |
| @@ -1,12 +1,15 @@ | |||
| 1 | <script lang="ts"> | 1 | <script lang="ts"> |
| 2 | import type { PageData } from "./$types"; | 2 | import type { PageData } from "./$types"; |
| 3 | import EmptyState from "$lib/components/EmptyState.svelte"; | 3 | import EmptyState from "$lib/components/EmptyState.svelte"; |
| 4 | + import Seo from "$lib/components/Seo.svelte"; | ||
| 4 | 5 | ||
| 5 | let { data }: { data: PageData } = $props(); | 6 | let { data }: { data: PageData } = $props(); |
| 6 | 7 | ||
| 7 | const categories = $derived(Object.keys(data.metrics)); | 8 | const categories = $derived(Object.keys(data.metrics)); |
| 8 | </script> | 9 | </script> |
| 9 | 10 | ||
| 11 | +<Seo title="Stats" /> | ||
| 12 | + | ||
| 10 | <h1 class="text-2xl font-extrabold uppercase tracking-tight mb-2">Stats</h1> | 13 | <h1 class="text-2xl font-extrabold uppercase tracking-tight mb-2">Stats</h1> |
| 11 | <p class="mb-6 text-xs uppercase tracking-widest text-[var(--muted)]"> | 14 | <p class="mb-6 text-xs uppercase tracking-widest text-[var(--muted)]"> |
| 12 | Application metrics and performance data. | 15 | Application metrics and performance data. |
modified
web/src/routes/terms/+page.svelte +9 -0 | @@ -1,3 +1,12 @@ | ||
| 1 | +<script lang="ts"> | |
| 2 | + import Seo from "$lib/components/Seo.svelte"; | |
| 3 | +</script> | |
| 4 | + | |
| 5 | +<Seo | |
| 6 | + title="Terms of Service" | |
| 7 | + description="Terms of service for Glean, the social RSS reader built on AT Protocol." | |
| 8 | +/> | |
| 9 | + | |
| 1 | 10 | <div class="mx-auto max-w-2xl px-4 py-12"> |
| 2 | 11 | <h1 class="text-2xl font-extrabold uppercase tracking-tight"> |
| 3 | 12 | Terms of Service |
| @@ -1,3 +1,12 @@ | |||
| 1 | +<script lang="ts"> | ||
| 2 | + import Seo from "$lib/components/Seo.svelte"; | ||
| 3 | +</script> | ||
| 4 | + | ||
| 5 | +<Seo | ||
| 6 | + title="Terms of Service" | ||
| 7 | + description="Terms of service for Glean, the social RSS reader built on AT Protocol." | ||
| 8 | +/> | ||
| 9 | + | ||
| 1 | <div class="mx-auto max-w-2xl px-4 py-12"> | 10 | <div class="mx-auto max-w-2xl px-4 py-12"> |
| 2 | <h1 class="text-2xl font-extrabold uppercase tracking-tight"> | 11 | <h1 class="text-2xl font-extrabold uppercase tracking-tight"> |
| 3 | Terms of Service | 12 | Terms of Service |
modified
web/src/routes/trending/+page.svelte +6 -0 | @@ -3,12 +3,18 @@ | ||
| 3 | 3 | import TrendingCard from "$lib/components/TrendingCard.svelte"; |
| 4 | 4 | import Pagination from "$lib/components/Pagination.svelte"; |
| 5 | 5 | import EmptyState from "$lib/components/EmptyState.svelte"; |
| 6 | + import Seo from "$lib/components/Seo.svelte"; | |
| 6 | 7 | |
| 7 | 8 | let { data }: { data: PageData } = $props(); |
| 8 | 9 | |
| 9 | 10 | const scope = $derived(data.scope); |
| 10 | 11 | </script> |
| 11 | 12 | |
| 13 | +<Seo | |
| 14 | + title="Trending" | |
| 15 | + description="What the Glean community is reading right now." | |
| 16 | +/> | |
| 17 | + | |
| 12 | 18 | {#if !data.user} |
| 13 | 19 | <div |
| 14 | 20 | class="panel mb-6 flex items-center justify-between gap-4 p-4 flex-wrap" |
| @@ -3,12 +3,18 @@ | |||
| 3 | import TrendingCard from "$lib/components/TrendingCard.svelte"; | 3 | import TrendingCard from "$lib/components/TrendingCard.svelte"; |
| 4 | import Pagination from "$lib/components/Pagination.svelte"; | 4 | import Pagination from "$lib/components/Pagination.svelte"; |
| 5 | import EmptyState from "$lib/components/EmptyState.svelte"; | 5 | import EmptyState from "$lib/components/EmptyState.svelte"; |
| 6 | + import Seo from "$lib/components/Seo.svelte"; | ||
| 6 | 7 | ||
| 7 | let { data }: { data: PageData } = $props(); | 8 | let { data }: { data: PageData } = $props(); |
| 8 | 9 | ||
| 9 | const scope = $derived(data.scope); | 10 | const scope = $derived(data.scope); |
| 10 | </script> | 11 | </script> |
| 11 | 12 | ||
| 13 | +<Seo | ||
| 14 | + title="Trending" | ||
| 15 | + description="What the Glean community is reading right now." | ||
| 16 | +/> | ||
| 17 | + | ||
| 12 | {#if !data.user} | 18 | {#if !data.user} |
| 13 | <div | 19 | <div |
| 14 | class="panel mb-6 flex items-center justify-between gap-4 p-4 flex-wrap" | 20 | class="panel mb-6 flex items-center justify-between gap-4 p-4 flex-wrap" |