Everything you need to build a Svelte project, powered by sv.
If you're seeing this, you've probably already done this step. Congrats!
# create a new project
npx sv create my-app
To recreate this project with the same configuration:
# recreate this project
npx sv create --template minimal --types ts --install npm svelteBlog
Once you've created a project and installed dependencies with npm install (or pnpm install or yarn), start a development server:
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
To create a production version of your app:
npm run build
You can preview the production build with npm run preview.
To deploy your app, you may need to install an adapter for your target environment.
+<slot></slot>
+<script lang="ts">
+ import type { Article } from "$lib/types";
+ import {articlesCache} from "$lib/data/articlesCache";
+
+ let articles : Article[] = $state([]);
+ let isLoading = $state(true);
+
+ $effect(() => {
+ articles = articlesCache;
+ isLoading = false;
+ })
+</script>
+
+<main class="blog-index">
+ {#if isLoading}
+ <div class="loading">Laster artikler...</div>
+ {/if}
+
+ {#if articles && articles.length > 0}
+ <div class="article-grid">
+ {#each articles as article}
+ <div class="article-card">
+ <h2><a href="/articles/{article.id}">{article.title}</a></h2>
+ <p>{article.preamble}</p>
+ </div>
+ {/each}
+ </div>
+ {:else}
+ <div class="no-articles">Ingen artikler funnet.</div>
+ {/if}
+
+</main>
+
+<style>
+ .loading {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ }
+
+ .article-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, 300px);
+ max-width: 1000px;
+ margin: 0 auto;
+ gap: 2rem;
+ }
+</style>
+import type {Article} from "$lib/types";
+
+export const wait = async (amount: number) => new Promise(res => setTimeout(res, amount ?? 100));
+
+export class BloggState {
+ articles= $state<Article[]>([]);
+ isLoading = $state<boolean>(false);
+ error = $state<string | null>(null);
+ isLoaded = $state<boolean>(false);
+
+ async loadArticles(reload: boolean = false) {
+ if (this.isLoaded || reload) {
+ return this.articles;
+ }
+
+ this.isLoading = true;
+ this.error = null;
+
+ let rawResponse = await fetch('/api/articles');
+
+ if (rawResponse.ok && rawResponse) {
+ let content = await rawResponse.json();
+ this.articles = content.articles;
+ this.isLoaded = true;
+ } else {
+ this.error = "Feil ved lasting av artikler: " + rawResponse.statusText;
+ }
+
+ await wait(1000);
+ this.isLoading = false;
+ }
+
+ async loadArticle(id: string) {
+ if (!this.isLoaded) {
+ await this.loadArticles();
+ }
+
+ return this.articles.find(article => article.id === id);
+ }
+}
+
+export const bloggState = new BloggState();
<script lang="ts">
import type { Article } from "$lib/types";
- import {articlesCache} from "$lib/data/articlesCache";
+ import { bloggState} from "$lib/state/BloggState.svelte";
let articles : Article[] = $state([]);
- let isLoading = $state(true);
$effect(() => {
- articles = articlesCache;
- isLoading = false;
- })
+ bloggState.loadArticles();
+ });
+
+ $effect(() => {
+ articles = bloggState.articles;
+ });
</script>
<main class="blog-index">
- {#if isLoading}
+ {#if bloggState.isLoading}
<div class="loading">Laster artikler...</div>
{/if}
+<section class="header-area">
+ <div class="site-name">Svelte Blog</div>
+</section>
+
+<style>
+ .header-area {
+ background: var(--bg-primary);
+ border-bottom: 1px solid var(--border-default);
+ padding: 1.5rem 2rem;
+ display: flex;
+ align-items: center;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ position: relative;
+ z-index: 100;
+ }
+
+ .site-name {
+ font-size: clamp(1.5rem, 4vw, 2.25rem);
+ font-weight: 700;
+ color: var(--text-primary);
+ margin: 0;
+ letter-spacing: -0.5px;
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.05);
+ transition: color 0.2s ease;
+ }
+
+ .site-name:hover {
+ color: var(--text-accent);
+ }
+
+ /* Responsiv: mindre skjermer */
+ @media (max-width: 768px) {
+ .header-area {
+ padding: 1rem;
+ }
+
+ .site-name {
+ font-size: clamp(1.3rem, 6vw, 2rem);
+ }
+ }
+</style>
+<script>
+ import Header from "$lib/components/Header.svelte";
+</script>
+
+<Header></Header>
+
<slot></slot>
+<script lang="ts">
+ import {marked} from "marked";
+
+ let {
+ toHtml
+ } = $props();
+
+ marked.use({
+ mangle: false,
+ headerIds: false
+ });
+</script>
+
+{#if toHtml}
+ {@html marked(toHtml) }
+{/if}
export class BloggState {
await this.loadArticles();
}
- return this.articles.find(article => article.id === id);
+ let article = this.articles.find(article => article.id === id);
+ if (article) {
+ return article;
+ } else {
+ return null;
+ }
}
}
<Header></Header>
-<slot></slot>
\ No newline at end of file
+<slot></slot>
+
+<style>
+ :global(a) {
+ text-decoration: none;
+ color: inherit; /* Optional: to keep the link color consistent with surrounding text */
+ }
+
+ :global(a:hover) {
+ color: #007bff; /* Optional: change color on hover */
+ }
+
+ :global(a:visited) {
+ color: inherit; /* Optional: change color for visited links */
+ }
+</style>
+<script lang="ts">
+ import type { Article } from "$lib/types";
+ import { bloggState} from "$lib/state/BloggState.svelte.js";
+ import { page } from '$app/state';
+ import {onMount} from "svelte";
+ import Markdown from "$lib/components/Markdown.svelte";
+
+ let isLoading = $state<boolean>(false);
+ let article = $state<Article | null>(null);
+
+ onMount(async () => {
+ if (page.params.articleId) {
+ article = await bloggState.loadArticle(page.params.articleId);
+ }
+ });
+
+
+</script>
+
+{#if bloggState.isLoading}
+ <div class="loading">Laster artikler...</div>
+{/if}
+
+<div class="buttons">
+ <a href="/" class="back-button">Tilbake til forsiden...</a>
+</div>
+
+
+{#if article}
+ <article class="blog-post">
+ <h1>{article.title}</h1>
+ <div class="blog-time">{article.publishedDate}</div>
+ <div class="blog-content">
+ <Markdown toHtml={article.contents}></Markdown>
+ </div>
+ </article>
+{:else}
+ <p>Ingen artikkel funnet...</p>
+{/if}
+
+<style>
+ .blog-post {
+ max-width: 1000px;
+ margin: 0 auto;
+ }
+
+ .buttons {
+ max-width: 1000px;
+ margin: 25px auto;
+ }
+
+ .buttons .back-button {
+ margin-top: 55px;
+ padding: 10px 17px;
+ border: 1px solid #ddd;
+ background: aliceblue;
+ border-radius: 7px;
+ }
+</style>
+a {
+ text-decoration: none;
+ color: inherit; /* Optional: to keep the link color consistent with surrounding text */
+}
+
+a:hover {
+ color: #007bff; /* Optional: change color on hover */
+}
+
+a:visited {
+ color: inherit; /* Optional: change color for visited links */
+}
<script>
+ import '../app.css';
import Header from "$lib/components/Header.svelte";
</script>
@@ -7,16 +8,5 @@
<slot></slot>
<style>
- :global(a) {
- text-decoration: none;
- color: inherit; /* Optional: to keep the link color consistent with surrounding text */
- }
- :global(a:hover) {
- color: #007bff; /* Optional: change color on hover */
- }
-
- :global(a:visited) {
- color: inherit; /* Optional: change color for visited links */
- }
</style>