feat: DEV-99 blog migration (#3870)
* feat: blog migration w/ fixes Co-authored-by: Prospector <prospectordev@gmail.com> * feat: add changelog button to news page * fix: lint issues * refactor: replace nuxt content with `@modrinth/blog` * feat: shared public folder * feat: try lazy loading html content * feat: rss + hide newsletter btn + blog.config.ts * feat: add new chapter modrinth servers post * fix: lint issues * fix: only generate RSS feed if changes detected * fix: utils dep * fix: lockfile dep * feat: GET /email/subscribe + subscription button * fix: lint issues * feat: articles.json for app * Made grid more responsive * fix: changes * Make margin slightly smaller in lists * Fix footer link * feat: latest news * Fix responsiveness * Remove old utm link * Update changelog * Lint --------- Co-authored-by: Prospector <prospectordev@gmail.com>
51
apps/frontend/src/components/ui/NewsletterButton.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { MailIcon, CheckIcon } from "@modrinth/assets";
|
||||
import { ref, watchEffect } from "vue";
|
||||
import { useBaseFetch } from "~/composables/fetch.js";
|
||||
|
||||
const auth = await useAuth();
|
||||
const showSubscriptionConfirmation = ref(false);
|
||||
const subscribed = ref(false);
|
||||
|
||||
async function checkSubscribed() {
|
||||
if (auth.value?.user) {
|
||||
try {
|
||||
const { data } = await useBaseFetch("auth/email/subscribe", {
|
||||
method: "GET",
|
||||
});
|
||||
subscribed.value = data?.subscribed || false;
|
||||
} catch {
|
||||
subscribed.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
checkSubscribed();
|
||||
});
|
||||
|
||||
async function subscribe() {
|
||||
try {
|
||||
await useBaseFetch("auth/email/subscribe", {
|
||||
method: "POST",
|
||||
});
|
||||
showSubscriptionConfirmation.value = true;
|
||||
} catch {
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
showSubscriptionConfirmation.value = false;
|
||||
subscribed.value = true;
|
||||
}, 2500);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ButtonStyled v-if="auth?.user && !subscribed" color="brand" type="outlined">
|
||||
<button v-tooltip="`Subscribe to the Modrinth newsletter`" @click="subscribe">
|
||||
<template v-if="!showSubscriptionConfirmation"> <MailIcon /> Subscribe </template>
|
||||
<template v-else> <CheckIcon /> Subscribed! </template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
86
apps/frontend/src/components/ui/ShareArticleButtons.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
v-tooltip="`Share on Bluesky`"
|
||||
:href="`https://bsky.app/intent/compose?text=${encodedUrl}`"
|
||||
target="_blank"
|
||||
>
|
||||
<BlueskyIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
v-tooltip="`Share on Mastodon`"
|
||||
:href="`https://tootpick.org/#text=${encodedUrl}`"
|
||||
target="_blank"
|
||||
>
|
||||
<MastodonIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
v-tooltip="`Share on X`"
|
||||
:href="`https://www.x.com/intent/post?url=${encodedUrl}`"
|
||||
target="_blank"
|
||||
>
|
||||
<TwitterIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
v-tooltip="`Share via email`"
|
||||
:href="`mailto:${encodedTitle ? `?subject=${encodedTitle}&` : `?`}body=${encodedUrl}`"
|
||||
target="_blank"
|
||||
>
|
||||
<MailIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<button
|
||||
v-tooltip="copied ? `Copied to clipboard` : `Copy link`"
|
||||
:disabled="copied"
|
||||
class="relative grid place-items-center overflow-hidden"
|
||||
@click="copyToClipboard(url)"
|
||||
>
|
||||
<CheckIcon
|
||||
class="absolute transition-all ease-in-out"
|
||||
:class="copied ? 'translate-y-0' : 'translate-y-7'"
|
||||
/>
|
||||
<LinkIcon
|
||||
class="absolute transition-all ease-in-out"
|
||||
:class="copied ? '-translate-y-7' : 'translate-y-0'"
|
||||
/>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
BlueskyIcon,
|
||||
CheckIcon,
|
||||
LinkIcon,
|
||||
MailIcon,
|
||||
MastodonIcon,
|
||||
TwitterIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
|
||||
const props = defineProps<{
|
||||
title?: string;
|
||||
url: string;
|
||||
}>();
|
||||
|
||||
const copied = ref(false);
|
||||
const encodedUrl = computed(() => encodeURIComponent(props.url));
|
||||
const encodedTitle = computed(() => (props.title ? encodeURIComponent(props.title) : undefined));
|
||||
|
||||
async function copyToClipboard(text: string) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
copied.value = true;
|
||||
setTimeout(() => {
|
||||
copied.value = false;
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
50
apps/frontend/src/components/ui/news/LatestNewsRow.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="mx-2 p-4 !py-8 shadow-md sm:mx-8 sm:p-32">
|
||||
<div class="my-8 flex items-center justify-between">
|
||||
<h2 class="m-0 mx-auto text-3xl font-extrabold sm:text-4xl">Latest news from Modrinth</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="latestArticles" class="grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-4">
|
||||
<div
|
||||
v-for="(article, index) in latestArticles"
|
||||
:key="article.slug"
|
||||
:class="{ 'max-xl:hidden': index === 2 }"
|
||||
>
|
||||
<NewsArticleCard :article="article" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-2 my-8 flex w-full items-center justify-center">
|
||||
<ButtonStyled color="brand" size="large">
|
||||
<nuxt-link to="/news">
|
||||
<NewspaperIcon />
|
||||
View all news
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NewspaperIcon } from "@modrinth/assets";
|
||||
import { articles as rawArticles } from "@modrinth/blog";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { ref, computed } from "vue";
|
||||
import NewsArticleCard from "./NewsArticleCard.vue";
|
||||
|
||||
const articles = ref(
|
||||
rawArticles
|
||||
.map((article) => ({
|
||||
...article,
|
||||
path: `/news/article/${article.slug}`,
|
||||
thumbnail: article.thumbnail
|
||||
? `/news/article/${article.slug}/thumbnail.webp`
|
||||
: `/news/default.jpg`,
|
||||
title: article.title,
|
||||
summary: article.summary,
|
||||
date: article.date,
|
||||
}))
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()),
|
||||
);
|
||||
|
||||
const latestArticles = computed(() => articles.value.slice(0, 3));
|
||||
</script>
|
||||
40
apps/frontend/src/components/ui/news/NewsArticleCard.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from "dayjs";
|
||||
|
||||
interface Article {
|
||||
path: string;
|
||||
thumbnail: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
article: Article;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nuxt-link
|
||||
:to="`${article.path}/`"
|
||||
class="active:scale-[0.99]! group flex flex-col transition-all ease-in-out hover:brightness-125"
|
||||
>
|
||||
<article class="flex h-full grow flex-col gap-4">
|
||||
<img
|
||||
:src="article.thumbnail"
|
||||
class="aspect-video w-full rounded-xl border-[1px] border-solid border-button-border object-cover"
|
||||
/>
|
||||
<div class="flex grow flex-col gap-2">
|
||||
<h3 class="m-0 text-base leading-tight group-hover:underline">
|
||||
{{ article.title }}
|
||||
</h3>
|
||||
<p v-if="article.summary" class="m-0 text-sm leading-tight">
|
||||
{{ article.summary }}
|
||||
</p>
|
||||
<div class="mt-auto text-sm text-secondary">
|
||||
{{ dayjs(article.date).format("MMMM D, YYYY") }}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</nuxt-link>
|
||||
</template>
|
||||
@@ -1211,9 +1211,9 @@ const footerLinks = [
|
||||
label: formatMessage(defineMessage({ id: "layout.footer.about", defaultMessage: "About" })),
|
||||
links: [
|
||||
{
|
||||
href: "https://blog.modrinth.com",
|
||||
href: "/news",
|
||||
label: formatMessage(
|
||||
defineMessage({ id: "layout.footer.about.blog", defaultMessage: "Blog" }),
|
||||
defineMessage({ id: "layout.footer.about.news", defaultMessage: "News" }),
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -383,8 +383,8 @@
|
||||
"layout.footer.about": {
|
||||
"message": "About"
|
||||
},
|
||||
"layout.footer.about.blog": {
|
||||
"message": "Blog"
|
||||
"layout.footer.about.news": {
|
||||
"message": "News"
|
||||
},
|
||||
"layout.footer.about.careers": {
|
||||
"message": "Careers"
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
{{ formatProjectType(project.project_type).toLowerCase() }}. You may choose one from our
|
||||
list or provide a custom license. You may also provide a custom URL to your chosen license;
|
||||
otherwise, the license text will be displayed. See our
|
||||
<a
|
||||
href="https://blog.modrinth.com/licensing-guide/"
|
||||
<nuxt-link
|
||||
to="/news/article/licensing-guide/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="text-link"
|
||||
>
|
||||
licensing guide
|
||||
</a>
|
||||
</nuxt-link>
|
||||
for more information.
|
||||
</p>
|
||||
|
||||
|
||||
@@ -122,8 +122,8 @@
|
||||
<h3>Creator Monetization Program data</h3>
|
||||
<p>
|
||||
When you sign up for our
|
||||
<a href="https://blog.modrinth.com/p/creator-monetization-beta">
|
||||
Creator Monetization Program</a
|
||||
<nuxt-link to="/news/article/creator-monetization-beta">
|
||||
Creator Monetization Program</nuxt-link
|
||||
>
|
||||
(the "CMP"), we collect:
|
||||
</p>
|
||||
|
||||
263
apps/frontend/src/pages/news/article/[slug].vue
Normal file
@@ -0,0 +1,263 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { RssIcon, GitGraphIcon } from "@modrinth/assets";
|
||||
import dayjs from "dayjs";
|
||||
import { articles as rawArticles } from "@modrinth/blog";
|
||||
import { computed } from "vue";
|
||||
import ShareArticleButtons from "~/components/ui/ShareArticleButtons.vue";
|
||||
import NewsletterButton from "~/components/ui/NewsletterButton.vue";
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
const route = useRoute();
|
||||
|
||||
const rawArticle = rawArticles.find((article) => article.slug === route.params.slug);
|
||||
|
||||
if (!rawArticle) {
|
||||
throw createError({
|
||||
fatal: true,
|
||||
statusCode: 404,
|
||||
message: "The requested article could not be found.",
|
||||
});
|
||||
}
|
||||
|
||||
const html = await rawArticle.html();
|
||||
|
||||
const article = computed(() => ({
|
||||
...rawArticle,
|
||||
path: `/news/${rawArticle.slug}`,
|
||||
thumbnail: rawArticle.thumbnail
|
||||
? `/news/article/${rawArticle.slug}/thumbnail.webp`
|
||||
: `/news/default.jpg`,
|
||||
title: rawArticle.title,
|
||||
summary: rawArticle.summary,
|
||||
date: rawArticle.date,
|
||||
html,
|
||||
}));
|
||||
|
||||
const articleTitle = computed(() => article.value.title);
|
||||
const articleUrl = computed(() => `https://modrinth.com/news/article/${route.params.slug}`);
|
||||
|
||||
const thumbnailPath = computed(() =>
|
||||
article.value.thumbnail
|
||||
? `${config.public.siteUrl}${article.value.thumbnail}`
|
||||
: `${config.public.siteUrl}/news/default.jpg`,
|
||||
);
|
||||
|
||||
const dayjsDate = computed(() => dayjs(article.value.date));
|
||||
|
||||
useSeoMeta({
|
||||
title: () => `${articleTitle.value} - Modrinth News`,
|
||||
ogTitle: () => articleTitle.value,
|
||||
description: () => article.value.summary,
|
||||
ogDescription: () => article.value.summary,
|
||||
ogType: "article",
|
||||
ogImage: () => thumbnailPath.value,
|
||||
articlePublishedTime: () => dayjsDate.value.toISOString(),
|
||||
twitterCard: "summary_large_image",
|
||||
twitterImage: () => thumbnailPath.value,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page experimental-styles-within py-6">
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-between gap-4 border-0 border-b-[1px] border-solid border-divider px-6 pb-6"
|
||||
>
|
||||
<nuxt-link :to="`/news`">
|
||||
<h1 class="m-0 text-3xl font-extrabold hover:underline">News</h1>
|
||||
</nuxt-link>
|
||||
<div class="flex gap-2">
|
||||
<NewsletterButton />
|
||||
<ButtonStyled circular>
|
||||
<a v-tooltip="`RSS feed`" aria-label="RSS feed" href="/news/feed/rss.xml" target="_blank">
|
||||
<RssIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular icon-only>
|
||||
<a v-tooltip="`Changelog`" href="/news/changelog" aria-label="Changelog">
|
||||
<GitGraphIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<article class="mt-6 flex flex-col gap-4 px-6">
|
||||
<h2 class="m-0 text-2xl font-extrabold leading-tight sm:text-4xl">{{ article.title }}</h2>
|
||||
<p class="m-0 text-base leading-tight sm:text-lg">{{ article.summary }}</p>
|
||||
<div class="mt-auto text-sm text-secondary sm:text-base">
|
||||
Posted on {{ dayjsDate.format("MMMM D, YYYY") }}
|
||||
</div>
|
||||
<ShareArticleButtons :title="article.title" :url="articleUrl" />
|
||||
<img
|
||||
:src="article.thumbnail"
|
||||
class="aspect-video w-full rounded-xl border-[1px] border-solid border-button-border object-cover sm:rounded-2xl"
|
||||
:alt="article.title"
|
||||
/>
|
||||
<div class="markdown-body" v-html="article.html" />
|
||||
<h3
|
||||
class="mb-0 mt-4 border-0 border-t-[1px] border-solid border-divider pt-4 text-base font-extrabold sm:text-lg"
|
||||
>
|
||||
Share this article
|
||||
</h3>
|
||||
<ShareArticleButtons :title="article.title" :url="articleUrl" />
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
> *:not(.full-width-bg),
|
||||
> .full-width-bg > * {
|
||||
max-width: 56rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.brand-gradient-bg {
|
||||
background: var(--brand-gradient-bg);
|
||||
border-color: var(--brand-gradient-border);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.page {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
article {
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.markdown-body) {
|
||||
h1,
|
||||
h2 {
|
||||
border-bottom: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul > li:not(:last-child) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
p {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
strong {
|
||||
color: var(--color-contrast);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
@media (min-width: 640px) {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
@media (min-width: 640px) {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.125rem;
|
||||
@media (min-width: 640px) {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1.25rem;
|
||||
font-size: 0.875rem;
|
||||
@media (min-width: 640px) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-brand);
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
a {
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
a {
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
border: 1px solid var(--color-button-border);
|
||||
border-radius: var(--radius-md);
|
||||
@media (min-width: 640px) {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
}
|
||||
|
||||
> img,
|
||||
> :has(img:first-child:last-child) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
158
apps/frontend/src/pages/news/index.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { ChevronRightIcon, RssIcon, GitGraphIcon } from "@modrinth/assets";
|
||||
import dayjs from "dayjs";
|
||||
import { articles as rawArticles } from "@modrinth/blog";
|
||||
import { computed, ref } from "vue";
|
||||
import NewsletterButton from "~/components/ui/NewsletterButton.vue";
|
||||
import NewsArticleCard from "~/components/ui/news/NewsArticleCard.vue";
|
||||
|
||||
const articles = ref(
|
||||
rawArticles
|
||||
.map((article) => ({
|
||||
...article,
|
||||
path: `/news/article/${article.slug}`,
|
||||
thumbnail: article.thumbnail
|
||||
? `/news/article/${article.slug}/thumbnail.webp`
|
||||
: `/news/default.jpg`,
|
||||
title: article.title,
|
||||
summary: article.summary,
|
||||
date: article.date,
|
||||
}))
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()),
|
||||
);
|
||||
|
||||
const featuredArticle = computed(() => articles.value?.[0]);
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
useSeoMeta({
|
||||
title: "Modrinth News",
|
||||
ogTitle: "Modrinth News",
|
||||
description: "Keep up-to-date on the latest news from Modrinth.",
|
||||
ogDescription: "Keep up-to-date on the latest news from Modrinth.",
|
||||
ogType: "website",
|
||||
ogImage: () => `${config.public.siteUrl}/news/thumbnail.jpg`,
|
||||
twitterCard: "summary_large_image",
|
||||
twitterImage: () => `${config.public.siteUrl}/news/thumbnail.jpg`,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page experimental-styles-within py-6">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 px-6">
|
||||
<div>
|
||||
<h1 class="m-0 text-3xl font-extrabold">News</h1>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<NewsletterButton />
|
||||
<ButtonStyled circular>
|
||||
<a v-tooltip="`RSS feed`" aria-label="RSS feed" href="/news/feed/rss.xml" target="_blank">
|
||||
<RssIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular icon-only>
|
||||
<a v-tooltip="`Changelog`" href="/news/changelog" aria-label="Changelog">
|
||||
<GitGraphIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="articles && articles.length">
|
||||
<div
|
||||
v-if="featuredArticle"
|
||||
class="full-width-bg brand-gradient-bg mt-6 border-0 border-y-[1px] border-solid py-4"
|
||||
>
|
||||
<nuxt-link
|
||||
:to="`${featuredArticle.path}/`"
|
||||
class="active:scale-[0.99]! group flex transition-all ease-in-out hover:brightness-125"
|
||||
>
|
||||
<article class="featured-article px-6">
|
||||
<div class="featured-image-container">
|
||||
<img
|
||||
:src="featuredArticle.thumbnail"
|
||||
class="aspect-video w-full rounded-2xl border-[1px] border-solid border-button-border object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="featured-content">
|
||||
<p class="m-0 font-bold">Featured article</p>
|
||||
<h3 class="m-0 text-3xl leading-tight group-hover:underline">
|
||||
{{ featuredArticle?.title }}
|
||||
</h3>
|
||||
<p class="m-0 text-lg leading-tight">{{ featuredArticle?.summary }}</p>
|
||||
<div class="mt-auto text-secondary">
|
||||
{{ dayjs(featuredArticle?.date).format("MMMM D, YYYY") }}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 px-6">
|
||||
<div class="group flex w-fit items-center gap-1">
|
||||
<h2 class="m-0 text-xl font-extrabold">More articles</h2>
|
||||
<ChevronRightIcon
|
||||
v-if="false"
|
||||
class="ml-0 h-6 w-6 transition-all group-hover:ml-1 group-hover:text-brand"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-[repeat(auto-fill,minmax(250px,1fr))] gap-4">
|
||||
<NewsArticleCard v-for="article in articles" :key="article.path" :article="article" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="pt-4">Error: Articles could not be loaded.</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
> *:not(.full-width-bg),
|
||||
> .full-width-bg > * {
|
||||
max-width: 56rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.brand-gradient-bg {
|
||||
background: var(--brand-gradient-bg);
|
||||
border-color: var(--brand-gradient-border);
|
||||
}
|
||||
|
||||
.featured-article {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.featured-image-container {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.featured-content {
|
||||
flex: 1;
|
||||
min-width: 16rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.featured-article {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.featured-image-container {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.featured-content {
|
||||
order: 2;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 80 KiB |
BIN
apps/frontend/src/public/news/article/carbon-ads/thumbnail.webp
Normal file
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 75 KiB |
BIN
apps/frontend/src/public/news/article/creator-update/oauth.jpg
Normal file
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 417 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 31 KiB |
BIN
apps/frontend/src/public/news/article/knossos-v2.1.0/styling.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 65 KiB |
BIN
apps/frontend/src/public/news/article/modrinth-app-beta/app.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
apps/frontend/src/public/news/article/modrinth-app-beta/auth.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 67 KiB |
BIN
apps/frontend/src/public/news/article/redesign/adorn.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
apps/frontend/src/public/news/article/redesign/consistency.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
apps/frontend/src/public/news/article/redesign/iris.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
apps/frontend/src/public/news/article/redesign/jellysquid.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
apps/frontend/src/public/news/article/redesign/notifications.jpg
Normal file
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 77 KiB |
BIN
apps/frontend/src/public/news/article/redesign/thumbnail.webp
Normal file
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 26 KiB |
BIN
apps/frontend/src/public/news/changelog.jpg
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
apps/frontend/src/public/news/default.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
165
apps/frontend/src/public/news/feed/articles.json
Normal file
@@ -0,0 +1,165 @@
|
||||
{
|
||||
"articles": [
|
||||
{
|
||||
"title": "A New Chapter for Modrinth Servers",
|
||||
"summary": "Modrinth Servers is now fully operated in-house by the Modrinth Team.",
|
||||
"thumbnail": "https://modrinth.com/news/article/a-new-chapter-for-modrinth-servers/thumbnail.webp",
|
||||
"date": "2025-03-13T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/a-new-chapter-for-modrinth-servers"
|
||||
},
|
||||
{
|
||||
"title": "Host your own server with Modrinth Servers — now in beta",
|
||||
"summary": "Fast, simple, reliable servers directly integrated into Modrinth.",
|
||||
"thumbnail": "https://modrinth.com/news/article/modrinth-servers-beta/thumbnail.webp",
|
||||
"date": "2024-11-03T06:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/modrinth-servers-beta"
|
||||
},
|
||||
{
|
||||
"title": "Quintupling Creator Revenue and Becoming Sustainable",
|
||||
"summary": "Announcing an update to our monetization program, creator split, and more!",
|
||||
"thumbnail": "https://modrinth.com/news/article/becoming-sustainable/thumbnail.webp",
|
||||
"date": "2024-09-13T20:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/becoming-sustainable"
|
||||
},
|
||||
{
|
||||
"title": "Introducing Modrinth+, a refreshed site look, and a new advertising system!",
|
||||
"summary": "Learn about this major update to Modrinth.",
|
||||
"thumbnail": "https://modrinth.com/news/article/design-refresh/thumbnail.webp",
|
||||
"date": "2024-08-21T20:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/design-refresh"
|
||||
},
|
||||
{
|
||||
"title": "Malware Discovery Disclosure: \"Windows Borderless\" mod",
|
||||
"summary": "Threat Analysis and Plan of Action",
|
||||
"thumbnail": "https://modrinth.com/news/article/windows-borderless-malware-disclosure/thumbnail.webp",
|
||||
"date": "2024-05-07T20:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/windows-borderless-malware-disclosure"
|
||||
},
|
||||
{
|
||||
"title": "A Sustainable Path Forward for Modrinth",
|
||||
"summary": "Our capital return and what’s next.",
|
||||
"thumbnail": "https://modrinth.com/news/default.jpg",
|
||||
"date": "2024-04-04T20:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/capital-return"
|
||||
},
|
||||
{
|
||||
"title": "Creator Update: Analytics, Organizations, Collections, and more",
|
||||
"summary": "December may be over, but we’re not done giving gifts.",
|
||||
"thumbnail": "https://modrinth.com/news/article/creator-update/thumbnail.webp",
|
||||
"date": "2024-01-06T20:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/creator-update"
|
||||
},
|
||||
{
|
||||
"title": "Correcting Inflated Download Counts due to Rate Limiting Issue",
|
||||
"summary": "A rate limiting issue caused inflated download counts in certain countries.",
|
||||
"thumbnail": "https://modrinth.com/news/default.jpg",
|
||||
"date": "2023-11-10T20:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/download-adjustment"
|
||||
},
|
||||
{
|
||||
"title": "Introducing Modrinth App Beta",
|
||||
"summary": "Changing the modded Minecraft landscape with the new Modrinth App, alongside several other major features.",
|
||||
"thumbnail": "https://modrinth.com/news/default.jpg",
|
||||
"date": "2023-08-05T20:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/modrinth-app-beta"
|
||||
},
|
||||
{
|
||||
"title": "(April Fools 2023) Powering up your experience: Modrinth Technologies™️ beta launch!",
|
||||
"summary": "Welcome to the new era of Modrinth. We can't wait to hear your feedback.",
|
||||
"thumbnail": "https://modrinth.com/news/article/new-site-beta/thumbnail.webp",
|
||||
"date": "2023-04-01T08:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/new-site-beta"
|
||||
},
|
||||
{
|
||||
"title": "Accelerating Modrinth's Development",
|
||||
"summary": "Our fundraiser and the future of Modrinth!",
|
||||
"thumbnail": "https://modrinth.com/news/default.jpg",
|
||||
"date": "2023-02-01T20:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/accelerating-development"
|
||||
},
|
||||
{
|
||||
"title": "Two years of Modrinth: a retrospective",
|
||||
"summary": "The history of Modrinth as we know it from December 2020 to December 2022.",
|
||||
"thumbnail": "https://modrinth.com/news/default.jpg",
|
||||
"date": "2023-01-07T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/two-years-of-modrinth-history"
|
||||
},
|
||||
{
|
||||
"title": "Modrinth's Anniversary Update",
|
||||
"summary": "Marking two years of Modrinth and discussing our New Year's Resolutions for 2023.",
|
||||
"thumbnail": "https://modrinth.com/news/article/two-years-of-modrinth/thumbnail.webp",
|
||||
"date": "2023-01-07T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/two-years-of-modrinth"
|
||||
},
|
||||
{
|
||||
"title": "Creators can now make money on Modrinth!",
|
||||
"summary": "Yes, you read the title correctly: Modrinth's creator monetization program, also known as payouts, is now in an open beta phase. Read on for more information!",
|
||||
"thumbnail": "https://modrinth.com/news/article/creator-monetization/thumbnail.webp",
|
||||
"date": "2022-11-12T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/creator-monetization"
|
||||
},
|
||||
{
|
||||
"title": "Modrinth's Carbon Ads experiment",
|
||||
"summary": "As a step towards implementing author payouts, we're experimenting with a couple different ad providers to see which one works the best for us.",
|
||||
"thumbnail": "https://modrinth.com/news/article/carbon-ads/thumbnail.webp",
|
||||
"date": "2022-09-08T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/carbon-ads"
|
||||
},
|
||||
{
|
||||
"title": "Plugins and Resource Packs now have a home on Modrinth",
|
||||
"summary": "A small update with a big impact: plugins and resource packs are now available on Modrinth!",
|
||||
"thumbnail": "https://modrinth.com/news/article/plugins-resource-packs/thumbnail.webp",
|
||||
"date": "2022-08-27T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/plugins-resource-packs"
|
||||
},
|
||||
{
|
||||
"title": "Changes to Modrinth Modpacks",
|
||||
"summary": "CurseForge CDN links requested to be removed by the end of the month",
|
||||
"thumbnail": "https://modrinth.com/news/article/modpack-changes/thumbnail.webp",
|
||||
"date": "2022-05-28T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/modpack-changes"
|
||||
},
|
||||
{
|
||||
"title": "Modrinth Modpacks: Now in alpha testing",
|
||||
"summary": "After over a year of development, we're happy to announce that modpack support is now in alpha testing.",
|
||||
"thumbnail": "https://modrinth.com/news/article/modpacks-alpha/thumbnail.webp",
|
||||
"date": "2022-05-15T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/modpacks-alpha"
|
||||
},
|
||||
{
|
||||
"title": "This week in Modrinth development: Filters and Fixes",
|
||||
"summary": "After a great first week since Modrinth launched out of beta, we have continued to improve the user interface based on feedback.",
|
||||
"thumbnail": "https://modrinth.com/news/article/knossos-v2.1.0/thumbnail.webp",
|
||||
"date": "2022-03-09T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/knossos-v2.1.0"
|
||||
},
|
||||
{
|
||||
"title": "Now showing on Modrinth: A new look!",
|
||||
"summary": "After months of relatively quiet development, Modrinth has released many new features and improvements, including a redesign. Read on to learn more!",
|
||||
"thumbnail": "https://modrinth.com/news/article/redesign/thumbnail.webp",
|
||||
"date": "2022-02-27T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/redesign"
|
||||
},
|
||||
{
|
||||
"title": "Beginner's Guide to Licensing your Mods",
|
||||
"summary": "Software licenses; the nitty-gritty legal aspect of software development. They're more important than you think.",
|
||||
"thumbnail": "https://modrinth.com/news/article/licensing-guide/thumbnail.webp",
|
||||
"date": "2021-05-16T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/licensing-guide"
|
||||
},
|
||||
{
|
||||
"title": "Welcome to Modrinth Beta",
|
||||
"summary": "After six months of work, Modrinth enters Beta, helping modders host their mods with ease!",
|
||||
"thumbnail": "https://modrinth.com/news/article/modrinth-beta/thumbnail.webp",
|
||||
"date": "2020-12-01T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/modrinth-beta"
|
||||
},
|
||||
{
|
||||
"title": "What is Modrinth?",
|
||||
"summary": "Hello, we are Modrinth – an open source mods hosting platform. Sounds dry, doesn't it? So let me tell you our story – and I promise, it won't be boring!",
|
||||
"thumbnail": "https://modrinth.com/news/default.jpg",
|
||||
"date": "2020-11-27T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/whats-modrinth"
|
||||
}
|
||||
]
|
||||
}
|
||||
223
apps/frontend/src/public/news/feed/rss.xml
Normal file
BIN
apps/frontend/src/public/news/thumbnail.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |