lightnet 2.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +428 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1 -0
  4. package/__e2e__/detailPage.spec.ts +0 -0
  5. package/__e2e__/fixtures/basics/astro.config.mjs +38 -0
  6. package/__e2e__/fixtures/basics/node_modules/.bin/astro +17 -0
  7. package/__e2e__/fixtures/basics/node_modules/.bin/astro-check +17 -0
  8. package/__e2e__/fixtures/basics/node_modules/.bin/tailwind +17 -0
  9. package/__e2e__/fixtures/basics/node_modules/.bin/tailwindcss +17 -0
  10. package/__e2e__/fixtures/basics/node_modules/.bin/tsc +17 -0
  11. package/__e2e__/fixtures/basics/node_modules/.bin/tsserver +17 -0
  12. package/__e2e__/fixtures/basics/package.json +19 -0
  13. package/__e2e__/fixtures/basics/public/favicon.svg +1 -0
  14. package/__e2e__/fixtures/basics/public/files/example.pdf +0 -0
  15. package/__e2e__/fixtures/basics/src/assets/logo.png +0 -0
  16. package/__e2e__/fixtures/basics/src/content/categories/christian-living.json +3 -0
  17. package/__e2e__/fixtures/basics/src/content/categories/teens.json +3 -0
  18. package/__e2e__/fixtures/basics/src/content/categories/theology.json +3 -0
  19. package/__e2e__/fixtures/basics/src/content/media/faithful-freestyle--en.json +13 -0
  20. package/__e2e__/fixtures/basics/src/content/media/how-to-kickflip--de.json +12 -0
  21. package/__e2e__/fixtures/basics/src/content/media/images/cover.jpg +0 -0
  22. package/__e2e__/fixtures/basics/src/content/media/images/how-to-kickflip--en.webp +0 -0
  23. package/__e2e__/fixtures/basics/src/content/media-collections/how-to-articles.json +3 -0
  24. package/__e2e__/fixtures/basics/src/content/media-types/book.json +9 -0
  25. package/__e2e__/fixtures/basics/src/content/media-types/video.json +7 -0
  26. package/__e2e__/fixtures/basics/src/content.config.ts +3 -0
  27. package/__e2e__/fixtures/basics/src/pages/[locale]/index.astro +14 -0
  28. package/__e2e__/fixtures/basics/src/translations/de.json +10 -0
  29. package/__e2e__/fixtures/basics/src/translations/en.json +10 -0
  30. package/__e2e__/fixtures/basics/tailwind.config.mjs +8 -0
  31. package/__e2e__/homepage.spec.ts +108 -0
  32. package/__e2e__/search.spec.ts +16 -0
  33. package/__e2e__/test-utils.ts +80 -0
  34. package/__tests__/pages/details-page/create-content-metadata.spec.ts +104 -0
  35. package/__tests__/utils/markdown.spec.ts +33 -0
  36. package/exports/components.ts +9 -0
  37. package/exports/content.ts +8 -0
  38. package/exports/details-page.ts +1 -0
  39. package/exports/i18n.ts +2 -0
  40. package/exports/index.ts +6 -0
  41. package/exports/utils.ts +2 -0
  42. package/package.json +54 -0
  43. package/playwright.config.ts +30 -0
  44. package/src/astro-integration/config.ts +185 -0
  45. package/src/astro-integration/integration.ts +74 -0
  46. package/src/astro-integration/project-context.ts +5 -0
  47. package/src/astro-integration/virtual.d.ts +14 -0
  48. package/src/astro-integration/vite-plugin-lightnet-config.ts +55 -0
  49. package/src/components/CategoriesOverview.astro +37 -0
  50. package/src/components/Gallery.astro +121 -0
  51. package/src/components/Hero.astro +82 -0
  52. package/src/components/HighlightSection.astro +71 -0
  53. package/src/components/Icon.tsx +27 -0
  54. package/src/components/MediaItemList.astro +84 -0
  55. package/src/components/Section.astro +49 -0
  56. package/src/content/astro-image.ts +14 -0
  57. package/src/content/content-schema-internal.ts +52 -0
  58. package/src/content/content-schema.ts +263 -0
  59. package/src/content/external-api.ts +7 -0
  60. package/src/content/get-categories.ts +15 -0
  61. package/src/content/get-languages.ts +14 -0
  62. package/src/content/get-media-items.ts +27 -0
  63. package/src/content/get-media-types.ts +23 -0
  64. package/src/content/query-media-items.ts +89 -0
  65. package/src/content/resolve-category-label.ts +20 -0
  66. package/src/i18n/get-locale-paths.ts +8 -0
  67. package/src/i18n/languages.ts +10 -0
  68. package/src/i18n/locals.d.ts +38 -0
  69. package/src/i18n/locals.ts +28 -0
  70. package/src/i18n/resolve-default-locale.ts +30 -0
  71. package/src/i18n/resolve-language.ts +25 -0
  72. package/src/i18n/resolve-locales.ts +5 -0
  73. package/src/i18n/translate.ts +64 -0
  74. package/src/i18n/translations/de.json +25 -0
  75. package/src/i18n/translations/en.json +25 -0
  76. package/src/layouts/MarkdownPage.astro +11 -0
  77. package/src/layouts/Page.astro +54 -0
  78. package/src/layouts/components/Favicon.astro +32 -0
  79. package/src/layouts/components/LanguagePicker.astro +38 -0
  80. package/src/layouts/components/Menu.astro +28 -0
  81. package/src/layouts/components/MenuItem.astro +21 -0
  82. package/src/layouts/components/PageNavigation.astro +65 -0
  83. package/src/layouts/components/PageTitle.astro +44 -0
  84. package/src/layouts/components/PreloadReact.tsx +3 -0
  85. package/src/pages/404.astro +14 -0
  86. package/src/pages/RedirectToDefaultLocale.astro +3 -0
  87. package/src/pages/api/search-response.ts +14 -0
  88. package/src/pages/api/search.ts +47 -0
  89. package/src/pages/details-page/DefaultDetails.astro +44 -0
  90. package/src/pages/details-page/DetailsPage.astro +53 -0
  91. package/src/pages/details-page/VideoDetails.astro +43 -0
  92. package/src/pages/details-page/components/Authors.astro +19 -0
  93. package/src/pages/details-page/components/Content.astro +73 -0
  94. package/src/pages/details-page/components/Cover.astro +35 -0
  95. package/src/pages/details-page/components/Description.astro +26 -0
  96. package/src/pages/details-page/components/MediaCollection.astro +39 -0
  97. package/src/pages/details-page/components/MediaCollections.astro +21 -0
  98. package/src/pages/details-page/components/OpenButton.astro +34 -0
  99. package/src/pages/details-page/components/SectionTitle.astro +8 -0
  100. package/src/pages/details-page/components/ShareButton.astro +58 -0
  101. package/src/pages/details-page/components/Title.astro +18 -0
  102. package/src/pages/details-page/components/VideoPlayer.astro +78 -0
  103. package/src/pages/details-page/components/details/Categories.astro +31 -0
  104. package/src/pages/details-page/components/details/Details.astro +17 -0
  105. package/src/pages/details-page/components/details/Label.astro +3 -0
  106. package/src/pages/details-page/components/details/Languages.astro +46 -0
  107. package/src/pages/details-page/utils/create-content-metadata.ts +78 -0
  108. package/src/pages/search-page/Search.tsx +71 -0
  109. package/src/pages/search-page/SearchPage.astro +51 -0
  110. package/src/pages/search-page/components/ResultList.tsx +135 -0
  111. package/src/pages/search-page/components/SearchFilter.tsx +189 -0
  112. package/src/pages/search-page/hooks/use-debounce.ts +17 -0
  113. package/src/pages/search-page/hooks/use-search.ts +95 -0
  114. package/src/pages/search-page/types.ts +9 -0
  115. package/src/pages/search-page/utils/search-translations.ts +22 -0
  116. package/src/pages/search-page/utils/use-provided-translations.ts +5 -0
  117. package/src/utils/markdown.ts +41 -0
  118. package/src/utils/paths.ts +45 -0
  119. package/src/utils/urls.ts +29 -0
  120. package/src/utils/verify-schema.ts +38 -0
  121. package/tailwind.config.ts +56 -0
  122. package/vitest.config.js +19 -0
@@ -0,0 +1,38 @@
1
+ declare namespace App {
2
+ interface Locals {
3
+ /**
4
+ * Provides internationalization helpers.
5
+ */
6
+ i18n: {
7
+ /**
8
+ * Translate a key to the language of the current locale.
9
+ *
10
+ * @param TranslationKey to be translated.
11
+ */
12
+ t: import("./translate").TranslateFn
13
+
14
+ /**
15
+ * The current locale or the default locale if the current locale is not available.
16
+ *
17
+ * In comparison to Astro.currentLocale this will always return a locale.
18
+ * Use Astro.currentLocale if you want to know the locale that is included in the current path.
19
+ */
20
+ currentLocale: string
21
+
22
+ /**
23
+ * The current text direction. Left-to-right or right-to-left.
24
+ */
25
+ direction: "ltr" | "rtl"
26
+
27
+ /**
28
+ * The default locale as defined in the project configuration.
29
+ */
30
+ defaultLocale: string
31
+
32
+ /**
33
+ * The available locales as defined in the project configuration.
34
+ */
35
+ locales: string[]
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,28 @@
1
+ import type { MiddlewareHandler } from "astro"
2
+ import config from "virtual:lightnet/config"
3
+
4
+ import { resolveDefaultLocale } from "./resolve-default-locale"
5
+ import { resolveLanguage } from "./resolve-language"
6
+ import { resolveLocales } from "./resolve-locales"
7
+ import { useTranslate } from "./translate"
8
+
9
+ export const onRequest: MiddlewareHandler = (
10
+ { locals, currentLocale: astroCurrentLocale },
11
+ next,
12
+ ) => {
13
+ if (!locals.i18n) {
14
+ const t = useTranslate(astroCurrentLocale)
15
+ const defaultLocale = resolveDefaultLocale(config)
16
+ const locales = resolveLocales(config)
17
+ const currentLocale = astroCurrentLocale ?? defaultLocale
18
+ const { direction } = resolveLanguage(currentLocale)
19
+ locals.i18n = {
20
+ t,
21
+ currentLocale,
22
+ defaultLocale,
23
+ direction,
24
+ locales,
25
+ }
26
+ }
27
+ return next()
28
+ }
@@ -0,0 +1,30 @@
1
+ import { AstroError } from "astro/errors"
2
+
3
+ export const resolveDefaultLocale = ({
4
+ languages,
5
+ }: {
6
+ languages: {
7
+ code: string
8
+ isDefaultLocale?: boolean
9
+ translations?: unknown
10
+ }[]
11
+ }) => {
12
+ const defaultLanguage = languages.find((l) => l.isDefaultLocale)
13
+ if (defaultLanguage) {
14
+ return defaultLanguage.code
15
+ }
16
+ const uiLanguages = languages.filter((l) => !!l.translations)
17
+ if (uiLanguages.length === 0) {
18
+ throw new AstroError(
19
+ "No user interface language found",
20
+ "Make sure you include a translations object for at least one of your languages.",
21
+ )
22
+ }
23
+ if (uiLanguages.length > 1) {
24
+ throw new AstroError(
25
+ "Could not identify the default user interface language",
26
+ "Make sure you have set one language to be the default language by setting the isDefaultLocale property.",
27
+ )
28
+ }
29
+ return uiLanguages[0].code
30
+ }
@@ -0,0 +1,25 @@
1
+ import { AstroError } from "astro/errors"
2
+
3
+ import { ALL_LANGUAGES } from "./languages"
4
+ import type { TranslateFn } from "./translate"
5
+
6
+ const languages = Object.fromEntries(
7
+ ALL_LANGUAGES.map((lang) => [lang.code, lang]),
8
+ )
9
+
10
+ export const resolveLanguage = (bcp47: string) => {
11
+ const language = languages[bcp47]
12
+
13
+ if (!language) {
14
+ throw new AstroError(`There is no language definition for: ${bcp47}`)
15
+ }
16
+ return language
17
+ }
18
+
19
+ export const resolveTranslatedLanguage = (bcp47: string, t: TranslateFn) => {
20
+ const language = resolveLanguage(bcp47)
21
+ return {
22
+ ...language,
23
+ name: t(language.label, { allowFixedStrings: true }),
24
+ }
25
+ }
@@ -0,0 +1,5 @@
1
+ import type { Language } from "../astro-integration/config"
2
+
3
+ export const resolveLocales = ({ languages }: { languages: Language[] }) => {
4
+ return languages.filter((l) => !!l.translations).map((l) => l.code)
5
+ }
@@ -0,0 +1,64 @@
1
+ import { AstroError } from "astro/errors"
2
+ import config from "virtual:lightnet/config"
3
+
4
+ import { resolveDefaultLocale } from "./resolve-default-locale"
5
+ import de from "./translations/de.json"
6
+ import en from "./translations/en.json"
7
+
8
+ type TranslationsByLocales = Record<string, Record<string, string>>
9
+
10
+ // We add (string & NonNullable<unknown>) to preserve typescript autocompletion for known keys
11
+ export type TranslationKey = keyof typeof en | (string & NonNullable<unknown>)
12
+ export type TranslationOptions = { allowFixedStrings?: boolean }
13
+
14
+ export type TranslateFn = (
15
+ key: TranslationKey,
16
+ options?: TranslationOptions,
17
+ ) => string
18
+
19
+ const configTranslations = config.languages
20
+ .filter((l) => !!l.translations)
21
+ .reduce((prev, curr) => ({ ...prev, [curr.code]: curr.translations }), {})
22
+ const translationsByLocales = merge({ de, en }, configTranslations)
23
+ const defaultLocale = resolveDefaultLocale(config)
24
+
25
+ export function useTranslate(locale: string | undefined): TranslateFn {
26
+ const resolvedLocale = locale ?? defaultLocale
27
+ const translations = translationsByLocales[resolvedLocale]
28
+ const defaultTranslations = translationsByLocales[defaultLocale]
29
+ if (!translations) {
30
+ throw new AstroError(
31
+ `No translations found for language ${resolvedLocale}`,
32
+ "Add them to your lightnet config inside astro.config.mjs.",
33
+ )
34
+ }
35
+ return (key: TranslationKey, options?: TranslationOptions) => {
36
+ const value = translations[key] ?? defaultTranslations[key]
37
+ const isTranslationKey = key.startsWith("custom.") || key.startsWith("ln.")
38
+ if (!value && options?.allowFixedStrings && !isTranslationKey) {
39
+ return key
40
+ }
41
+ if (!value) {
42
+ throw new AstroError(
43
+ `Missing translation: '${key}' is undefined for language '${resolvedLocale}'.`,
44
+ `Add a translation for '${key}' to src/translations/${resolvedLocale}.json`,
45
+ )
46
+ }
47
+ return value
48
+ }
49
+ }
50
+
51
+ function merge(
52
+ source: TranslationsByLocales,
53
+ toMerge: TranslationsByLocales,
54
+ ): TranslationsByLocales {
55
+ const result = { ...source }
56
+ for (const key of Object.keys(toMerge)) {
57
+ if (!source[key]) {
58
+ result[key] = toMerge[key]
59
+ } else {
60
+ result[key] = { ...source[key], ...toMerge[key] }
61
+ }
62
+ }
63
+ return result
64
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "ln.404.go-to-the-home-page": "Zurück zur Startseite",
3
+ "ln.404.page-not-found": "Seite nicht gefunden",
4
+ "ln.common.categories": "Kategorien",
5
+ "ln.common.category": "Kategorie",
6
+ "ln.common.language": "Sprache",
7
+ "ln.common.languages": "Sprachen",
8
+ "ln.common.type": "Typ",
9
+ "ln.common.a11y.external-link": "Externer link",
10
+ "ln.details.open": "Öffnen",
11
+ "ln.details.part-of": "Teil der Sammlung",
12
+ "ln.details.download": "Download",
13
+ "ln.details.share": "Teilen",
14
+ "ln.header.a11y.open-main-menu": "Öffne Hauptmenü",
15
+ "ln.header.a11y.select-language": "Sprache auswählen",
16
+ "ln.home.title": "Startseite",
17
+ "ln.search.all-categories": "Alle Kategorien",
18
+ "ln.search.all-languages": "Alle Sprachen",
19
+ "ln.search.all-types": "Alle Typen",
20
+ "ln.search.more-results": "Mehr Ergebnisse",
21
+ "ln.search.no-results": "Keine Ergebnisse",
22
+ "ln.search.placeholder": "Suche in Titel, Autor, Beschreibung...",
23
+ "ln.search.title": "Suche",
24
+ "ln.share.url-copied-to-clipboard": "URL in die Zwischenablage kopiert"
25
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "ln.404.go-to-the-home-page": "Go to the Home page",
3
+ "ln.404.page-not-found": "Page not found",
4
+ "ln.common.categories": "Categories",
5
+ "ln.common.category": "Category",
6
+ "ln.common.language": "Language",
7
+ "ln.common.languages": "Languages",
8
+ "ln.common.a11y.external-link": "External link",
9
+ "ln.common.type": "Type",
10
+ "ln.details.open": "Open",
11
+ "ln.details.share": "Share",
12
+ "ln.details.part-of-collection": "Part of Collection",
13
+ "ln.details.download": "Download",
14
+ "ln.header.a11y.open-main-menu": "Open Main Menu",
15
+ "ln.header.a11y.select-language": "Select language",
16
+ "ln.home.title": "Home",
17
+ "ln.search.all-categories": "All Categories",
18
+ "ln.search.all-languages": "All Languages",
19
+ "ln.search.all-types": "All Types",
20
+ "ln.search.more-results": "More results",
21
+ "ln.search.no-results": "No Results",
22
+ "ln.search.placeholder": "Search in Title, Author, Description...",
23
+ "ln.search.title": "Search",
24
+ "ln.share.url-copied-to-clipboard": "URL copied to clipboard"
25
+ }
@@ -0,0 +1,11 @@
1
+ ---
2
+ import Page from "./Page.astro"
3
+ ---
4
+
5
+ <Page>
6
+ <article
7
+ class="prose prose-gray mx-auto mt-8 max-w-screen-md px-4 sm:mt-16 md:px-8"
8
+ >
9
+ <slot />
10
+ </article>
11
+ </Page>
@@ -0,0 +1,54 @@
1
+ ---
2
+ import { ClientRouter } from "astro:transitions"
3
+ import config from "virtual:lightnet/config"
4
+
5
+ import { resolveLanguage } from "../i18n/resolve-language"
6
+ import Favicon from "./components/Favicon.astro"
7
+ import PageNavigation from "./components/PageNavigation.astro"
8
+ import PageTitle from "./components/PageTitle.astro"
9
+ import PreloadReact from "./components/PreloadReact"
10
+
11
+ interface Props {
12
+ title?: string
13
+ description?: string
14
+ }
15
+
16
+ const { title, description } = Astro.props
17
+ const configTitle = Astro.locals.i18n.t(config.title, {
18
+ allowFixedStrings: true,
19
+ })
20
+
21
+ const { currentLocale } = Astro.locals.i18n
22
+ const language = resolveLanguage(currentLocale)
23
+ ---
24
+
25
+ <!doctype html>
26
+ <html lang={currentLocale} dir={language.direction}>
27
+ <head>
28
+ <meta charset="UTF-8" />
29
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
30
+ <title>{title ? `${title} | ${configTitle}` : configTitle}</title>
31
+ {description && <meta name="description" content={description} />}
32
+ {config.manifest && <link rel="manifest" href={config.manifest} />}
33
+ <link rel="prefetch" href="/api/search.json" />
34
+ <Favicon />
35
+ <ClientRouter />
36
+ </head>
37
+ <body class="overflow-y-scroll bg-gray-50 text-gray-900">
38
+ <header
39
+ class="fixed top-0 z-50 h-14 w-full bg-gray-50 shadow-lg sm:h-20"
40
+ transition:animate="none"
41
+ >
42
+ <div
43
+ class="mx-auto flex h-full max-w-screen-xl justify-between px-4 md:px-8"
44
+ >
45
+ <PageTitle />
46
+ <PageNavigation />
47
+ </div>
48
+ </header>
49
+ <main class="mx-auto min-h-screen pb-8 pt-14 sm:py-20">
50
+ <slot />
51
+ </main>
52
+ <PreloadReact client:idle />
53
+ </body>
54
+ </html>
@@ -0,0 +1,32 @@
1
+ ---
2
+ import { extname } from "node:path"
3
+
4
+ import { AstroError } from "astro/errors"
5
+ import config from "virtual:lightnet/config"
6
+
7
+ const faviconTypes = {
8
+ ".ico": "image/x-icon",
9
+ ".gif": "image/gif",
10
+ ".jpeg": "image/jpeg",
11
+ ".jpg": "image/jpeg",
12
+ ".png": "image/png",
13
+ ".svg": "image/svg+xml",
14
+ } as Record<string, string>
15
+
16
+ const favicons = config.favicon?.map((favicon) => {
17
+ const type = faviconTypes[extname(favicon.href)]
18
+ if (!type) {
19
+ throw new AstroError(
20
+ `Unsupported favicon ${favicon.href}`,
21
+ "favicon must be a .ico, .gif, .jpg, .png, or .svg file",
22
+ )
23
+ }
24
+ return { ...favicon, type }
25
+ })
26
+ ---
27
+
28
+ {
29
+ favicons?.map(({ rel, type, href, sizes }) => (
30
+ <link rel={rel} type={type} href={href} sizes={sizes} />
31
+ ))
32
+ }
@@ -0,0 +1,38 @@
1
+ ---
2
+ import { resolveTranslatedLanguage } from "../../i18n/resolve-language"
3
+ import { localizePath } from "../../utils/paths"
4
+ import Menu from "./Menu.astro"
5
+ import MenuItem from "./MenuItem.astro"
6
+
7
+ const { t, locales } = Astro.locals.i18n
8
+
9
+ const translations = locales
10
+ .map((locale) => ({
11
+ locale,
12
+ label: resolveTranslatedLanguage(locale, t).name,
13
+ active: locale === Astro.currentLocale,
14
+ href: currentPathWithLocale(locale),
15
+ }))
16
+ .sort((a, b) => a.label.localeCompare(b.label))
17
+
18
+ function currentPathWithLocale(locale: string) {
19
+ const currentPath = Astro.url.pathname
20
+ const currentPathWithoutLocale =
21
+ Astro.currentLocale && currentPath.startsWith(`/${Astro.currentLocale}`)
22
+ ? currentPath.slice(Astro.currentLocale.length + 1)
23
+ : currentPath
24
+ return localizePath(locale, currentPathWithoutLocale)
25
+ }
26
+ ---
27
+
28
+ {
29
+ translations.length > 1 && (
30
+ <Menu icon="mdi--translate" label="ln.header.a11y.select-language">
31
+ {translations.map(({ label, locale, active, href }) => (
32
+ <MenuItem href={href} hreflang={locale} active={active}>
33
+ {label}
34
+ </MenuItem>
35
+ ))}
36
+ </Menu>
37
+ )
38
+ }
@@ -0,0 +1,28 @@
1
+ ---
2
+ import Icon from "../../components/Icon"
3
+
4
+ interface Props {
5
+ icon: string
6
+ label: string
7
+ }
8
+
9
+ const { icon, label } = Astro.props
10
+ ---
11
+
12
+ <div class="dy-dropdown dy-dropdown-end">
13
+ <div
14
+ role="button"
15
+ tabindex="0"
16
+ aria-label={Astro.locals.i18n.t(label)}
17
+ class="hover:text-primary flex rounded-md p-3 text-gray-600"
18
+ >
19
+ <Icon className={icon} ariaLabel="" />
20
+ </div>
21
+
22
+ <ul
23
+ tabindex="0"
24
+ class="dy-dropdown-content top-px me-3 mt-[3.25rem] w-48 overflow-hidden rounded-b-md bg-gray-50 py-3 shadow-lg sm:mt-16"
25
+ >
26
+ <slot />
27
+ </ul>
28
+ </div>
@@ -0,0 +1,21 @@
1
+ ---
2
+ interface Props {
3
+ href: string
4
+ active: boolean
5
+ hreflang?: string
6
+ target?: "_blank" | "_self"
7
+ }
8
+ const { href, hreflang, active, target } = Astro.props
9
+ ---
10
+
11
+ <li>
12
+ <a
13
+ href={href}
14
+ hreflang={hreflang}
15
+ target={target}
16
+ class="flex items-center gap-3 px-6 py-3 decoration-gray-800"
17
+ class:list={[active ? "font-bold" : "hover:underline"]}
18
+ >
19
+ <slot />
20
+ </a>
21
+ </li>
@@ -0,0 +1,65 @@
1
+ ---
2
+ import config from "virtual:lightnet/config"
3
+
4
+ import Icon from "../../components/Icon"
5
+ import { localizePath, searchPagePath } from "../../utils/paths"
6
+ import { isExternalUrl } from "../../utils/urls"
7
+ import LanguagePicker from "./LanguagePicker.astro"
8
+ import Menu from "./Menu.astro"
9
+ import MenuItem from "./MenuItem.astro"
10
+
11
+ const currentPath = Astro.url.pathname
12
+ const items = (config.mainMenu ?? []).map(({ href, label, requiresLocale }) => {
13
+ const isExternal = isExternalUrl(href)
14
+ const path =
15
+ isExternal || !requiresLocale
16
+ ? href
17
+ : localizePath(Astro.currentLocale, href)
18
+ const isActive =
19
+ !isExternal &&
20
+ (currentPath === localizePath(Astro.currentLocale, href) ||
21
+ currentPath === localizePath(Astro.currentLocale, `${href}/`) ||
22
+ currentPath === href)
23
+ return {
24
+ path,
25
+ isExternal,
26
+ label,
27
+ isActive,
28
+ }
29
+ })
30
+
31
+ const t = Astro.locals.i18n.t
32
+ ---
33
+
34
+ <nav class="-me-3 flex items-center">
35
+ <a
36
+ class="hover:text-primary flex p-3 text-gray-600"
37
+ aria-label={t("ln.search.title")}
38
+ data-astro-prefetch="viewport"
39
+ href={searchPagePath(Astro.currentLocale)}
40
+ ><Icon className="mdi--magnify" ariaLabel="" /></a
41
+ >
42
+ <LanguagePicker />
43
+
44
+ {
45
+ !!items.length && (
46
+ <Menu icon="mdi--menu" label="ln.header.a11y.open-main-menu">
47
+ {items.map(({ label, path, isActive, isExternal }) => (
48
+ <MenuItem
49
+ href={path}
50
+ active={isActive}
51
+ target={isExternal ? "_blank" : "_self"}
52
+ >
53
+ {t(label, { allowFixedStrings: true })}
54
+ {isExternal && (
55
+ <Icon
56
+ className="mdi--external-link shrink-0 text-base"
57
+ ariaLabel=""
58
+ />
59
+ )}
60
+ </MenuItem>
61
+ ))}
62
+ </Menu>
63
+ )
64
+ }
65
+ </nav>
@@ -0,0 +1,44 @@
1
+ ---
2
+ import type { ImageMetadata } from "astro"
3
+ import { Image } from "astro:assets"
4
+ import config from "virtual:lightnet/config"
5
+ import logo from "virtual:lightnet/logo"
6
+
7
+ import { localizePath } from "../../utils/paths"
8
+
9
+ function getWidth(logo: ImageMetadata) {
10
+ const size = config.logo?.size ?? 28
11
+ // Calculate width to use for the logo.
12
+ // This based on the size that should measure the shorter side
13
+ // of the image
14
+ return Math.floor(size * Math.max(1, logo.width / logo.height))
15
+ }
16
+
17
+ const logoAlt = config.logo?.alt ?? ""
18
+ const t = Astro.locals.i18n.t
19
+ ---
20
+
21
+ <a
22
+ class="flex items-center gap-3 font-bold text-gray-600 md:text-lg"
23
+ href={localizePath(Astro.currentLocale, "/")}
24
+ >
25
+ {
26
+ logo &&
27
+ (logo.format === "svg" ? (
28
+ <img
29
+ src={logo.src}
30
+ alt={t(logoAlt, { allowFixedStrings: true })}
31
+ style={`width:${getWidth(logo)}px;`}
32
+ />
33
+ ) : (
34
+ <Image
35
+ src={logo}
36
+ alt={t(logoAlt, { allowFixedStrings: true })}
37
+ style={`width:${getWidth(logo)}px;`}
38
+ width={getWidth(logo)}
39
+ densities={[1.5, 2, 3]}
40
+ loading="eager"
41
+ />
42
+ ))
43
+ }{t(config.title, { allowFixedStrings: true })}</a
44
+ >
@@ -0,0 +1,3 @@
1
+ export default function PreloadReact() {
2
+ return <></>
3
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import Page from "../layouts/Page.astro"
3
+ ---
4
+
5
+ <Page>
6
+ <div class="flex flex-col items-center">
7
+ <p class="pb-8 pt-20 text-center text-3xl opacity-80">
8
+ {Astro.locals.i18n.t("ln.404.page-not-found")}
9
+ </p>
10
+ <a href={`/${Astro.locals.i18n.defaultLocale}`} class="dy-btn"
11
+ >{Astro.locals.i18n.t("ln.404.go-to-the-home-page")}</a
12
+ >
13
+ </div>
14
+ </Page>
@@ -0,0 +1,3 @@
1
+ ---
2
+ return Astro.rewrite(`/${Astro.locals.i18n.defaultLocale}`)
3
+ ---
@@ -0,0 +1,14 @@
1
+ export type SearchItem = {
2
+ title: string
3
+ id: string
4
+ type: string
5
+ authors?: string[]
6
+ categories?: string[]
7
+ description?: string
8
+ language: string
9
+ image: { src: string; width: number; height: number }
10
+ }
11
+
12
+ export type SearchResponse = {
13
+ items: SearchItem[]
14
+ }
@@ -0,0 +1,47 @@
1
+ import type { APIRoute } from "astro"
2
+ import { getImage } from "astro:assets"
3
+
4
+ import type { MediaItemEntry } from "../../content/content-schema-internal"
5
+ import { getMediaItems } from "../../content/get-media-items"
6
+ import { markdownToText } from "../../utils/markdown"
7
+ import type { SearchItem } from "./search-response"
8
+
9
+ export const GET: APIRoute = async () => {
10
+ const items = await searchResults()
11
+ return new Response(JSON.stringify({ items }))
12
+ }
13
+
14
+ export async function searchResults() {
15
+ const items: SearchItem[] = []
16
+ for await (const mediaItem of await getMediaItems()) {
17
+ items.push(await createSearchItem(mediaItem))
18
+ }
19
+ return items.sort((a, b) => {
20
+ return 10 * a.type.localeCompare(b.type) + a.title.localeCompare(b.title)
21
+ })
22
+ }
23
+
24
+ async function createSearchItem(mediaItem: MediaItemEntry) {
25
+ const {
26
+ id,
27
+ data: { image, title, authors, categories, language, description, type },
28
+ } = mediaItem
29
+ const {
30
+ src,
31
+ attributes: { width, height },
32
+ } = await getImage({
33
+ src: image,
34
+ format: "webp",
35
+ width: 144,
36
+ })
37
+ return {
38
+ title,
39
+ id,
40
+ type: type.id,
41
+ authors,
42
+ categories: categories?.map(({ id }) => id),
43
+ description: markdownToText(description)?.slice(0, 350),
44
+ language,
45
+ image: { src, width, height },
46
+ }
47
+ }