I made a pretty unusual setup for generating a start page. All you need is this python script, sites.yml
, and index.css
. By default it expects to write the data to /var/www/html/index.html
with index.css
at /var/www/html/index.css
. I find this script makes it pretty easy for me to add new sites whenever I want. If you want to look at other examples of start pages feel free to look at r/startpages
#!/usr/bin/env python3
import yaml
from sys import exit
try:
with open('sites.yml', 'r') as input_file:
data = yaml.safe_load(input_file)
except FileNotFoundError:
print('sites.yml was not found')
exit(1)
except yaml.scanner.ScannerError:
print('Failed to parse yaml file')
exit(2)
html = '''
<html>
<head>
<link rel="stylesheet" type="text/css" href="index.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="theme-color" content="#000000">
<title>Start Page</title>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code&display=swap" rel="stylesheet">
</head>
<body>
<div id="root">
<div class="App" style="display: flex;">
'''
for category, links in data.items():
print(f'Adding category: {category}')
html += '<div class="Feed_root">'
html += f'<h2 class="Feed_title">#{category}</h2>'
if links is not None:
for header, values in links.items():
print(f'Adding link: {header}')
try:
url = values['url']
except:
url = ''
try:
meta = values['meta']
except:
meta = ''
try:
description = values['description']
except:
description = ''
html += '<div class="Feed_item">'
html += f'<a href="{url}" target="_blank" rel="noopener noreferrer" class="Feed_link">'
html += f'<span class="Feed_meta">{meta}</span>'
html += f'<span class="Feed_heading">{header}</span>'
html += '</a>'
html += f'<span class="Feed_description">{description}</span>'
html += '</div>'
html += '</div>'
html += '''
</div>
</div>
</body>
</html>
'''
with open('/var/www/html/index.html', 'w') as f:
f.write(html)
Here's an example of what sites.yml
could look like. Each category
generates a new column with each site with it's data. You can have as many categories and site entries as you want. The python script will also ignore any lines starting with #
.
self-hosted:
thelounge:
url: 'https://thelounge.chat/'
meta: 'communication'
description: 'A self-hosted IRC client'
gitea:
url: 'https://gitea.com/'
meta: 'code'
description: 'A self-hosted git server with web UI'
bitwarden:
url: 'https://github.com/dani-garcia/vaultwarden'
meta: 'password'
description: 'A self-hosted password manager'
searx:
url: 'https://searx.me/'
meta: 'search'
description: 'A self-hosted search engine'
blog:
url: 'https://cobalt-org.github.io/'
meta: 'blog'
description: 'My self-hosted blog'
miniflux:
url: 'https://miniflux.app/'
meta: 'rss'
description: 'My self-hosted RSS reader, also has my YT subscriptions'
personal:
hackaday:
url: 'https://hackaday.com'
meta: 'electronics'
description: 'Hackaday serves up Fresh Hacks Every Day from around the Internet'
fedora magazine:
url: 'https://fedoramagazine.org/'
meta: 'linux'
description: 'A magazine that published various tips about using Fedora'
shopping:
ebay:
url: 'https://ebay.com'
meta: 'shopping'
description: 'eBay'
amazon:
url: 'https://smile.amazon.com'
meta: 'shopping'
description: 'Amazon'
media:
youtube:
url: 'https://youtube.com'
meta: 'video'
description: 'YouTube'
hulu:
url: 'https://hulu.com'
meta: 'video'
description: 'Hulu'
netflix:
url: 'https://netflix.com'
meta: 'video'
description: 'Netflix'
work:
intranet:
url: 'http://intranet'
meta: 'admin'
description: 'Misc internal links'
work email:
url: 'https://outlook.office.com'
meta: 'communication'
description: 'Email but for work'
category:
name:
url: 'url here'
meta: 'any tags you want'
description: 'a brief description'
Here's my index.css
, feel free to modify it how you want.
.jfk-bubble {
filter: invert(100%) hue-rotate(180deg) contrast(90%) !important;
}
[data-darkreader-inline-bgcolor] {
background-color: var(--darkreader-inline-bgcolor) !important;
}
[data-darkreader-inline-bgimage] {
background-image: var(--darkreader-inline-bgimage) !important;
}
[data-darkreader-inline-border] {
border-color: var(--darkreader-inline-border) !important;
}
[data-darkreader-inline-border-bottom] {
border-bottom-color: var(--darkreader-inline-border-bottom) !important;
}
[data-darkreader-inline-border-left] {
border-left-color: var(--darkreader-inline-border-left) !important;
}
[data-darkreader-inline-border-right] {
border-right-color: var(--darkreader-inline-border-right) !important;
}
[data-darkreader-inline-border-top] {
border-top-color: var(--darkreader-inline-border-top) !important;
}
[data-darkreader-inline-boxshadow] {
box-shadow: var(--darkreader-inline-boxshadow) !important;
}
[data-darkreader-inline-color] {
color: var(--darkreader-inline-color) !important;
}
[data-darkreader-inline-fill] {
fill: var(--darkreader-inline-fill) !important;
}
[data-darkreader-inline-stroke] {
stroke: var(--darkreader-inline-stroke) !important;
}
[data-darkreader-inline-outline] {
outline-color: var(--darkreader-inline-outline) !important;
}
html {
background-color: #181a1b !important;
}
html, body, input, textarea, select, button {
background-color: #181a1b;
}
html, body, input, textarea, select, button {
border-color: #575757;
color: #e8e6e3;
}
a {
color: #3391ff;
}
table {
border-color: #4c4c4c;
}
::placeholder {
color: #bab5ab;
}
::selection {
background-color: #005ccc;
color: #ffffff;
}
::-moz-selection {
background-color: #005ccc;
color: #ffffff;
}
input:-webkit-autofill,
textarea:-webkit-autofill,
select:-webkit-autofill {
background-color: #545b00 !important;
color: #e8e6e3 !important;
}
::-webkit-scrollbar {
background-color: #1c1e1f;
color: #c5c1b9;
}
::-webkit-scrollbar-thumb {
background-color: #2a2c2e;
}
::-webkit-scrollbar-thumb:hover {
background-color: #323537;
}
::-webkit-scrollbar-thumb:active {
background-color: #3d4043;
}
::-webkit-scrollbar-corner {
background-color: #181a1b;
}
* {
scrollbar-color: #2a2c2e #1c1e1f;
}
:focus {
outline: .1rem solid #24d1e7
}
html {
font-size: 62.5%;
font-family: "Fira Code", sans-serif;
background: #282a36
}
body {
font-size: 1.6em;
overflow-x: hidden;
margin: 0 auto;
max-width: 120rem
}
body * {
font-family: "Fira Code", sans-serif;
font-size: 1.4rem;
box-sizing: border-box
}
a {
color: #24d1e7;
text-decoration: none
}
img {
max-width: 100%
}
::-webkit-input-placeholder {
color: #f1f1f1
}
:-ms-input-placeholder {
color: #f1f1f1
}
::-ms-input-placeholder {
color: #f1f1f1
}
::placeholder {
color: #f1f1f1
}
.Feed_root {
flex: 0 1 33%;
font-size: 1rem;
overflow: hidden;
text-overflow: ellipsis;
padding: 2rem;
margin-right: 5rem
}
.Feed_root:last-child {
margin-right: 0
}
.Feed_title {
font-size: 2rem;
color: #bd99ff
}
.Feed_heading {
overflow: hidden
}
.Feed_link:hover .Feed_heading {
text-decoration: underline
}
.Feed_item {
color: #fff;
width: 100%;
padding: .5rem 0 1rem;
margin-bottom: 1rem
}
.Feed_description {
margin-top: .5rem;
font-size: 1.2rem;
color: #fff;
display: block
}
.Feed_meta {
font-size: 1rem;
color: #aaa;
font-style: italic;
display: block;
margin-top: .4rem;
text-decoration: none!important
}
.Feed_image {
width: 100%;
margin-top: 1rem
}
.vimvixen-hint {
background-color: #7b5300 !important;
border-color: #d8b013 !important;
color: #f3e8c8 !important;
}
::placeholder {
opacity: 0.5 !important;
}