Major rewrite for 0.1.6 - Dynamic database entry generation based on json files, dynamic run page generation and more
All checks were successful
/ build-site (push) Successful in 3m22s
/ release (push) Successful in 34s
/ checking (push) Successful in 23s

This commit is contained in:
Firq 2024-01-02 23:19:14 +01:00
parent e75a575417
commit 7fe9e8c25f
Signed by: Firq
GPG key ID: 3ACC61C8CEC83C20
28 changed files with 2954 additions and 45 deletions

View file

@ -2,9 +2,7 @@ on:
push: push:
tags: tags:
- '[0-9]+\.[0-9]+\.[0-9]+' - '[0-9]+\.[0-9]+\.[0-9]+'
- '[0-9]+\.[0-9]+\.[0-9]+a[0-9]+' - '[0-9]+\.[0-9]+\.[0-9]+pre[0-9]+'
- '[0-9]+\.[0-9]+\.[0-9]+b[0-9]+'
- '[0-9]+\.[0-9]+\.[0-9]+rc[0-9]+'
jobs: jobs:
checking: checking:
@ -25,6 +23,10 @@ jobs:
steps: steps:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
with:
config-inline: |
[registry."docker.io"]
mirrors = ["https://docker-cache.neshweb.net"]
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:

5
.prettierignore Normal file
View file

@ -0,0 +1,5 @@
**/.git
**/node_modules
**/.vscode
**/public
**/dist

6
.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true
}

View file

@ -7,7 +7,7 @@
"**/.DS_Store": true, "**/.DS_Store": true,
"**/Thumbs.db": true, "**/Thumbs.db": true,
"**/__pycache__": true, "**/__pycache__": true,
"**/node_modules": false "**/node_modules": true
}, },
"hide-files.files": [] "hide-files.files": []
} }

2265
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{ {
"name": "fgo-ta-com-website", "name": "fgo-ta-com-website",
"type": "module", "type": "module",
"version": "0.1.5", "version": "0.1.6",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"start": "astro dev", "start": "astro dev",
@ -10,7 +10,17 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astro-community/astro-embed-youtube": "^0.4.3",
"@astrojs/check": "^0.3.4",
"@astrojs/sitemap": "^3.0.3", "@astrojs/sitemap": "^3.0.3",
"astro": "^4.0.7" "astro": "^4.0.7",
} "autoprefixer": "^10.4.16",
"iconoir": "^7.3.0",
"postcss-preset-env": "^9.3.0",
"typescript": "^5.3.3"
},
"browserslist": [
"last 2 versions",
">0.5% and not dead"
]
} }

View file

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 7 KiB

View file

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -1,4 +1,7 @@
--- ---
import packagejson from '../../package.json'
const version = packagejson.version
const release = `https://forgejo.neshweb.net/Firq/fgo-ta-com-website/releases/tag/${version}`
--- ---
<div> <div>
@ -9,6 +12,8 @@
game in general. game in general.
<br /> <br />
<a href="https://firq.dev" target="_blank" rel="noopener noreferrer">Feel free to check out my own site.</a> <a href="https://firq.dev" target="_blank" rel="noopener noreferrer">Feel free to check out my own site.</a>
<br />
<span class="version">(Website version: <a href={release} target="_blank" rel="noopener noreferrer">{version}</a>)</span>
</span> </span>
<slot /> <slot />
</div> </div>
@ -30,5 +35,10 @@
a { a {
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
color: var(--c-darkpurple);
}
.version {
font-size: 0.7em;
} }
</style> </style>

View file

@ -1,9 +1,13 @@
--- ---
import { Image } from 'astro:assets';
import logo from '../assets/logo.svg'
import hamburger from 'iconoir/icons/menu.svg'
const hamburger_src_url = `url("${hamburger.src}")`;
--- ---
<header> <header>
<a href="/" rel="noopener noreferrer" aria-label="Home"> <a href="/" rel="noopener noreferrer" aria-label="Home">
<img src="/assets/logo.svg" alt="" /> <Image src={logo} alt="Website Logo"/>
</a> </a>
<ul class="desktop"> <ul class="desktop">
<slot /> <slot />
@ -13,11 +17,11 @@
<slot /> <slot />
</ul> </ul>
<div class="placeholder"></div> <div class="placeholder"></div>
<i class="iconoir-menu"></i> <div class="hamburger-menu"></div>
</button> </button>
</header> </header>
<style> <style define:vars={{ hamburger_src_url }}>
header { header {
z-index: 1000; z-index: 1000;
position: sticky; position: sticky;
@ -79,16 +83,6 @@
height: 64px; height: 64px;
} }
.mobile > i {
position: static;
color: white;
font-weight: bold;
font-size: 2em;
align-self: flex-start;
padding-right: 1em;
padding-top: 1.15rem;
}
.mobile > ul { .mobile > ul {
display: none; display: none;
padding: 0px; padding: 0px;
@ -111,6 +105,17 @@
justify-self: top; justify-self: top;
} }
.hamburger-menu {
mask: var(--hamburger_src_url) no-repeat center;
background-color: white;
width: 2em;
height: 2em;
position: static;
align-self: flex-start;
padding-right: 1em;
padding-top: 2.5em;
}
@media (min-width: 1140px) { @media (min-width: 1140px) {
.mobile { .mobile {
display: none; display: none;

View file

@ -3,7 +3,7 @@ export interface Props {
currentPage?: string currentPage?: string
link: string link: string
text: string text: string
icon: string icon: ImageMetadata
} }
const { icon, text, link, currentPage } = Astro.props const { icon, text, link, currentPage } = Astro.props
@ -17,6 +17,7 @@ if (currentPage === slug) {
currPage = 'current' currPage = 'current'
} }
const icon_src_url = `url("${icon.src}")`;
const fulllink = `/${slug}` const fulllink = `/${slug}`
--- ---
@ -28,12 +29,12 @@ const fulllink = `/${slug}`
class={currPage} class={currPage}
tabindex="0" tabindex="0"
> >
<i class={icon}></i> <div class="icon"></div>
{text} {text}
</a> </a>
</li> </li>
<style> <style define:vars={{ icon_src_url }}>
li { li {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -41,7 +42,9 @@ const fulllink = `/${slug}`
display: flex; display: flex;
width: 200px; width: 200px;
} }
li > a { li > a {
display: inline-flex;
color: white; color: white;
text-decoration: none; text-decoration: none;
justify-content: center; justify-content: center;
@ -49,11 +52,29 @@ const fulllink = `/${slug}`
font-size: 1.4em; font-size: 1.4em;
height: 100%; height: 100%;
font-weight: bold; font-weight: bold;
gap: 0.2em;
} }
li > a:hover { li > a:hover {
color: var(--c-purplepink); color: var(--c-purplepink);
} }
li > a:hover > .icon {
background-color: var(--c-purplepink);
}
.current { .current {
color: var(--c-darkpurple); color: var(--c-darkpurple) !important;
}
.current > .icon {
background-color: var(--c-darkpurple) !important;
}
.icon {
mask: var(--icon_src_url) no-repeat center;
background-color: white;
width: 1.4em;
height: 1.4em;
} }
</style> </style>

View file

@ -0,0 +1,103 @@
---
export interface Props {
url: string | undefined
title: string
questReleaseDate: string
shortdescription: string
}
const options_date: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'long',
day: '2-digit',
}
const { shortdescription, questReleaseDate, url, title } = Astro.props
const render_date = new Date(questReleaseDate).toLocaleDateString('en-GB', options_date)
---
<a href={url} rel="noopener noreferrer">
<div class="circle"></div>
<article>
<h2>{title}</h2>
<h3>{render_date}</h3>
<p>{shortdescription}</p>
</article>
</a>
<style>
.circle {
display: none;
}
@media (min-width: 900px) {
.circle {
margin: 1rem 1rem 1rem 0rem;
position: relative;
display: flex;
visibility: visible;
height: 1.5rem;
width: 1.5rem;
border-radius: 40%;
background-color: var(--c-darkpurple);
transition: transform var(--speed) var(--ease);
}
a:hover > .circle {
height: 1.75rem;
width: 1.75rem;
translate: -0.125rem;
margin-right: 0.825rem;
}
article {
margin-left: 0.5rem;
}
}
a {
align-items: center;
justify-content: center;
display: flex;
text-decoration: none;
height: auto;
margin: 0.5rem;
width: 100%;
}
p {
color: white;
text-align: left;
font-size: 1.1em;
margin: 0.5em;
}
article > h2 {
margin: 0.3rem 0.5rem;
color: var(--c-darkpurple);
font-size: 1.5rem;
line-height: normal;
text-decoration: none;
}
article > h3 {
margin: 0.2em 0.5rem;
color: white;
font-size: 1rem;
line-height: normal;
text-decoration: none;
}
article {
display: flex;
flex: 1;
flex-wrap: wrap;
flex-direction: column;
align-items: flex-start;
align-content: flex-start;
justify-content: center;
background-color: var(--c-darkergray);
padding: 10px;
text-align: center;
transition: transform var(--speed) var(--ease);
min-height: 100%;
border-radius: 1.25rem;
}
a:hover > article {
transform: scaleY(102.5%) scaleX(101%);
}
</style>

184
src/components/taCard.astro Normal file
View file

@ -0,0 +1,184 @@
---
import type { ImageMetadata } from 'astro'
import { Image } from 'astro:assets'
export interface Props {
title: string,
link: string,
date: string,
servant: string,
turns: string,
runner: string
}
const { turns, runner, date, servant, link, title } =
Astro.props
const options_date: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}
const servantImagePath = `/src/assets/ta_servants/${servant}.png`
const formatted_date = new Date(date).toLocaleDateString('de-DE', options_date)
const servant_images = import.meta.glob<{ default: ImageMetadata }>(
'/src/assets/ta_servants/*.png'
)
---
<a href={link} target="_blank" rel="noopener noreferrer" aria-label={title}>
<article>
<Image src={servant_images[servantImagePath]()} alt="" class="icon"/>
<div class="title">
<h2>{title}</h2>
</div>
<p>
<span>
By {runner}<br /> •
</span>
{formatted_date}
</p>
<div class="expand-on-hover">
<h2>{turns}</h2>
</div>
</article>
</a>
<style>
div {
display: none;
}
span {
display: none;
}
span {
display: flex;
}
a {
text-decoration: none;
}
article {
background-color: var(--c-darkergray);
border-color: var(--c-darkgray);
padding: 10px;
text-align: center;
transition: transform var(--speed) var(--ease);
height: auto;
width: auto;
max-width: 8rem;
border-radius: 1.25rem;
padding-bottom: 1.5rem;
--size-value: 7rem;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
article:hover {
transform: scale(var(--hover-scale));
}
article > .icon {
border-radius: 1.25rem;
width: var(--size-value);
height: var(--size-value);
margin: 0.5rem;
}
article:hover .title {
display: flex;
position: absolute;
align-items: center;
justify-content: center;
text-align: center;
background-color: var(--c-darkgray);
height: calc(var(--size-value) + 0.1rem);
width: calc(var(--size-value) + 0.1rem);
opacity: 90%;
border-radius: 1.25rem;
top: 1.1em;
}
article:hover .title h2 {
margin: 0;
display: inline-flex;
font-weight: bold;
color: white;
font-size: 18px;
line-height: 150%;
padding: 0.5rem;
}
article .title h2 {
display: none;
}
article .title {
display: none;
}
.icon {
display: flex;
justify-content: center;
align-items: center;
}
p {
display: flex;
text-align: center;
justify-content: center;
align-items: center;
line-height: 100%;
text-decoration: none;
color: white;
font-size: 1rem;
font-weight: bold;
padding-top: 0.5rem;
margin: 0.5rem 0px;
flex-wrap: wrap;
flex-direction: column;
}
.expand-on-hover {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: space-evenly;
background-color: var(--c-duskgray);
z-index: 99;
transform: scaleY(0);
transform-origin: top;
position: absolute;
top: 90%;
left: 0px;
right: 0px;
color: white;
border-radius: 0px 0px 1.25rem 1.25rem;
}
.expand-on-hover img {
width: 3rem;
height: 3rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
border-radius: 0.5rem;
}
.expand-on-hover h2 {
margin: 0.5rem;
}
article:hover .expand-on-hover {
transform: scaleY(1);
transition: transform 200ms ease-in-out;
background-color: var(--c-duskgray);
}
</style>

View file

@ -0,0 +1,26 @@
{
"info": {
"title": "Cernunnos",
"questReleaseDate": "2023-07-10",
"shortdescription": "One of FGOs most notorious boss fights due to up to 100% special defense, strong DoT damage and powerful field effects",
"description": "One of FGOs most notorious boss fights due to up to 100% special defense, strong DoT damage and powerful field effects - and still, the TA community prevailed and created some of the most amazing runs of all time"
},
"data": [
{
"title": "Cernunnos 4T (No Castoria)",
"link": "https://www.youtube.com/watch?v=WrHudtdfivA",
"date": "2023-07-19",
"servant": "shishou",
"turns": "4T",
"runner": "Firq"
},
{
"title": "Cernunnos 4T (FLO)",
"link": "https://www.youtube.com/watch?O1f-go7uJQM",
"date": "2023-07-19",
"servant": "shishou",
"turns": "4T",
"runner": "Requiem"
}
]
}

View file

@ -1,7 +1,11 @@
--- ---
import Navbar from '../components/navbar.astro' import Navbar from '../components/navbar.astro'
import NavbarEntry from '../components/navbarEntry.astro' import NavbarEntry from '../components/navbarEntry.astro'
import navdata from '../../static/assets/data/_navdata.json' import navdata from '../../static/data/_navdata.json'
import embed from '../assets/embed.png'
import home from 'iconoir/icons/home.svg'
import database from 'iconoir/icons/database.svg'
import type { IconsLookup } from '../types/icons'
export interface Props { export interface Props {
title: string title: string
@ -9,6 +13,11 @@ export interface Props {
descriptionOverride?: string descriptionOverride?: string
} }
const icons: IconsLookup = {
home: home,
database: database
}
const { descriptionOverride, currentpage, title } = Astro.props const { descriptionOverride, currentpage, title } = Astro.props
let description let description
@ -22,6 +31,11 @@ let currPage = 'https://fgo-ta.com/'
if (currentpage !== 'home') { if (currentpage !== 'home') {
currPage += currentpage currPage += currentpage
} }
const mapped_navdata = navdata.map((item) => ({
...item,
...{ icon: icons[item.icon] },
}))
--- ---
<!DOCTYPE html> <!DOCTYPE html>
@ -36,23 +50,19 @@ if (currentpage !== 'home') {
<meta property="og:title" content={title} /> <meta property="og:title" content={title} />
<meta property="og:url" content={currPage} /> <meta property="og:url" content={currPage} />
<meta property="og:description" content={description} /> <meta property="og:description" content={description} />
<meta property="og:image" content="/assets/embed.png" /> <meta property="og:image" content={embed.src} />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta name="theme-color" content="#b86cff" /> <meta name="theme-color" content="#b86cff" />
<!-- Links --> <!-- Links -->
<link rel="icon" type="image/ico" href="/assets/favicon.ico" /> <link rel="icon" type="image/ico" href="/favicon.ico" />
<link rel="sitemap" href="/sitemap-index.xml" /> <link rel="sitemap" href="/sitemap-index.xml" />
<link href="https://mastodon.neshweb.net/@Firq" rel="me" /> <link href="https://mastodon.neshweb.net/@Firq" rel="me" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/iconoir-icons/iconoir@main/css/iconoir.css"
/>
</head> </head>
<body> <body>
<Navbar> <Navbar>
{ {
navdata.map((item) => ( mapped_navdata.map((item) => (
<NavbarEntry currentPage={currentpage} {...item} /> <NavbarEntry currentPage={currentpage} {...item} />
)) ))
} }

View file

@ -1,13 +1,15 @@
--- ---
export interface Props { export interface Props {
title: string title: string
description: string
} }
const { title } = Astro.props const { title, description } = Astro.props
--- ---
<div class="base"> <div class="base">
<h1>{title}</h1> <h1>{title}</h1>
<h2>{description}</h2>
<div> <div>
<slot /> <slot />
</div> </div>
@ -45,6 +47,16 @@ const { title } = Astro.props
background-color: var(--c-darkgray); background-color: var(--c-darkgray);
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
div h2 {
color: white;
font-size: 16px;
font-weight: 600;
max-width: 75;
margin: 1rem;
line-height: 20px;
text-align: center;
}
@media (min-width: 512px) { @media (min-width: 512px) {
div { div {
row-gap: 1.5em; row-gap: 1.5em;
@ -64,10 +76,18 @@ const { title } = Astro.props
} }
} }
@media (min-width: 1500px) { @media (min-width: 1140px) {
.base { .base {
margin-left: 10%; margin-left: 10%;
margin-right: 10%; margin-right: 10%;
} }
div h1 {
margin-left: unset;
margin-right: unset;
}
div h2 {
text-align: left;
}
} }
</style> </style>

View file

@ -0,0 +1,100 @@
---
export interface Props {
title: string
}
const { title } = Astro.props
---
<section>
<h1>{title}</h1>
<div class="wrapper">
<div class="line"></div>
<slot />
<div class="drop"></div>
</div>
</section>
<style>
.drop {
display: flex;
position: absolute;
visibility: visible;
left: 0;
right: 0;
bottom: -5rem;
margin-left: auto;
margin-right: auto;
height: 1.5rem;
width: 1.5rem;
border-radius: 0% 50% 50% 50%;
transform: rotate(45deg);
background-color: var(--c-darkpurple);
}
.line {
display: flex;
position: absolute;
visibility: visible;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
background-color: var(--c-darkpurple);
height: calc(100% + 5rem);
translate: 0% 2rem;
width: 0.25rem;
z-index: -1;
}
h1 {
font-size: 40px;
line-height: 48px;
letter-spacing: -1px;
color: white;
font-size: 2.25rem;
margin-top: 1rem;
margin-bottom: 0;
margin-left: auto;
margin-right: auto;
padding: 0.25rem 0.75rem;
max-width: max-content;
background-color: var(--c-darkgray);
padding: 0.25rem 1.5rem;
border-radius: 0.5rem;
padding-bottom: 0.5rem;
}
.wrapper {
margin: 2rem 3rem 0.5rem 3rem;
display: flex;
flex-flow: column wrap;
row-gap: 1em;
column-gap: 1em;
align-self: center;
align-items: stretch;
justify-content: space-around;
padding: 1em;
color: white;
font-size: 1em;
position: relative;
}
@media (min-width: 900px) {
.drop {
margin-left: 1.5rem;
}
.line {
margin-left: 2.1rem;
}
h1 {
margin-left: 3rem;
}
}
@media (min-width: 1500px) {
.wrapper {
margin-left: 20rem;
margin-right: 20rem;
}
h1 {
margin-left: 20rem;
margin-right: 20rem;
}
}
</style>

View file

@ -0,0 +1,52 @@
---
import Layout from '../layouts/Layout.astro'
import BaseSection from '../layouts/baseSection.astro'
import TACard from '../components/taCard.astro'
import type { filedata } from '../types/ta'
export interface Props {
datafile: string
}
const { datafile } = Astro.props
const fulldata = import.meta.glob<{ default: any }>(
`../content/data/*.json`
)
const filecontent: filedata = (
await fulldata[`../content/data/${datafile}.json`]()
)['default']
const title = filecontent.info.title
---
<Layout
title={title}
currentpage="database-entry"
descriptionOverride={filecontent.info.shortdescription}
>
<a href="/database">&lt&lt Back to database</a>
<BaseSection title={title} description={filecontent.info.description}>
{filecontent.data.map((item) => <TACard {...item} />)}
</BaseSection>
<div class="placeholder"></div>
</Layout>
<style>
.placeholder {
visibility: hidden;
width: 100%;
height: 2.5rem;
}
a {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
text-align: center;
color: white;
background-color: var(--c-gray);
padding: 0.5rem 0px;
text-decoration: none;
}
</style>

37
src/pages/database.astro Normal file
View file

@ -0,0 +1,37 @@
---
import Layout from '../layouts/Layout.astro'
import QuestListing from '../components/questListing.astro'
import DatabaseSection from '../layouts/databaseSection.astro'
import { findSlug } from '../utils/slugTools'
import type { filedata } from '../types/ta'
const description =
'My own small blog. Topics include FGO, TA, Programming, web technologies and more!'
const questInfo = []
const fulldata = import.meta.glob<{ default: filedata }>(`../content/data/*.json`)
for (const [key, value] of Object.entries(fulldata)) {
const url = `${Astro.url}/${findSlug(key)}`
questInfo.push({
...(await value())['default'].info,
url: url,
})
}
questInfo.sort(
(a, b) => Date.parse(b.questReleaseDate) - Date.parse(a.questReleaseDate)
)
---
<Layout
title="TA Database"
currentpage="database"
descriptionOverride={description}
>
<DatabaseSection title="FGO NA TA Database">
{questInfo.map((quest) => <QuestListing {...quest} />)}
</DatabaseSection>
</Layout>
<style></style>

View file

@ -0,0 +1,23 @@
---
import TaShowcaseLayout from '../../layouts/taShowcaseLayout.astro'
import {findSlug} from '../../utils/slugTools'
export function getStaticPaths() {
const fulldata = import.meta.glob<{ default: any }>(
`../../content/data/*.json`
)
const keylist = Object.keys(fulldata).map(
(item) => findSlug(item)
)
const paths: { params: { slug: string } }[] = []
for (const key of keylist) {
paths.push({ params: { slug: key! } })
}
return paths
}
const { slug } = Astro.params
---
<TaShowcaseLayout datafile={slug} />

3
src/types/icons.ts Normal file
View file

@ -0,0 +1,3 @@
export interface IconsLookup {
[key: string]: ImageMetadata
}

20
src/types/ta.ts Normal file
View file

@ -0,0 +1,20 @@
interface tadata {
title: string
link: string
servant: string
turns: string
runner: string
date: string
}
interface info {
title: string
questReleaseDate: string
description: string
shortdescription: string
}
export interface filedata {
info: info
data: tadata[]
}

3
src/utils/slugTools.ts Normal file
View file

@ -0,0 +1,3 @@
export function findSlug(filepath: string) {
return filepath.match(/(?:.*[\\/])(.+)(?:\.json)/)?.[1]
}

View file

@ -1,7 +0,0 @@
[
{
"link": "/",
"text": "Home",
"icon": "iconoir-home-alt"
}
]

12
static/data/_navdata.json Normal file
View file

@ -0,0 +1,12 @@
[
{
"link": "/",
"text": "Home",
"icon": "home"
},
{
"link": "/database",
"text": "TA Database",
"icon": "database"
}
]

View file

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

7
static/robots.txt Normal file
View file

@ -0,0 +1,7 @@
user-agent:*
Disallow: /assets/data/
User-agent: GPTBot
Disallow: /
Sitemap: https://firq.dev/sitemap-index.xml