You've already forked AstralRinth
forked from didirus/AstralRinth
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>
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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
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
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>
|
||||
Reference in New Issue
Block a user