Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions src/html/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,29 @@ a:hover {
#songs li:last-child { border-bottom: none; }
#songs li:hover { background-color: #eef2f5; }

#songs li a.title {
/* Main song link wrapper */
#songs li a.song-link {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
flex: 1;
min-width: 0;
text-decoration: none;
color: inherit;
}

#songs li a.song-link:hover {
text-decoration: none;
}

#songs li a.song-link .title {
font-size: 1.1em;
color: #333;
font-weight: 400;
margin-left: 1em;
}

#songs li .alias {
margin-left: 0.5em; /* Adds space between the title and the alias */
#songs li a.song-link .alias {
font-size: 0.9em;
color: #6c757d;
font-style: italic;
Expand All @@ -123,14 +137,15 @@ a:hover {
content: ')';
}

#songs li .artist {
#songs li a.song-link .artist {
margin-left: auto; /* This takes up all available space, pushing the element to the end */
padding-left: 1em; /* Ensures some space between the alias and artist on smaller screens */
font-size: 0.95em;
color: #6c757d;
}


#songs li a:hover {
text-decoration: none;
#songs li a.song-link:hover .title {
color: #0056b3;
}

Expand Down
144 changes: 121 additions & 23 deletions src/html/index_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,119 @@

# def name_of_file(song):
# return os.path.splitext(os.path.split(song)[1])[0]
def create_index_xhtml(list_of_songs_meta, target_dir):
tmp_path = 'index.xhtml'
def create_index_html(list_of_songs_meta, target_dir):
tmp_path = 'index.html'
out_path = os.path.join(target_dir, tmp_path)
tree = etree.parse(os.path.join(sb.repo_dir(), "./src/html/templates/index.xhtml"))

# Try to parse as HTML first, fall back to XHTML if needed
template_path = os.path.join(sb.repo_dir(), "./src/html/templates/index.xhtml")
parser = etree.HTMLParser()
tree = etree.parse(template_path, parser)

# Add SEO meta tags to head
head = tree.getroot().find(".//head")
if head is None:
head = tree.getroot().find(".//{http://www.w3.org/1999/xhtml}head")

if head is not None:
# Add charset and viewport if not present
if head.find(".//meta[@charset]") is None:
etree.SubElement(head, "meta", attrib={"charset": "utf-8"}, nsmap=None).tail = "\n"

# Add canonical URL
etree.SubElement(head, "link", attrib={"rel": "canonical", "href": "https://spiewaj.com/"}, nsmap=None).tail = "\n"

# Add meta description with keywords
desc_meta = etree.SubElement(head, "meta", attrib={
"name": "description",
"content": "Śpiewnik harcerski online - teksty i chwyty gitarowe piosenek harcerskich, biesiadnych, turystycznych i obozowych. Darmowy śpiewnik z akordami do druku (PDF) i na Kindle (EPUB)."
}, nsmap=None)
desc_meta.tail = "\n"

# Add keywords
etree.SubElement(head, "meta", attrib={
"name": "keywords",
"content": "śpiewnik harcerski, teksty piosenek, chwyty gitarowe, akordy, piosenki harcerskie, piosenki biesiadne, piosenki turystyczne, śpiewnik pdf, śpiewnik kindle"
}, nsmap=None).tail = "\n"

# Add Open Graph tags
etree.SubElement(head, "meta", attrib={"property": "og:title", "content": "Spiewaj.com - Śpiewnik harcerski online"}, nsmap=None).tail = "\n"
etree.SubElement(head, "meta", attrib={"property": "og:type", "content": "website"}, nsmap=None).tail = "\n"
etree.SubElement(head, "meta", attrib={"property": "og:url", "content": "https://spiewaj.com/"}, nsmap=None).tail = "\n"
etree.SubElement(head, "meta", attrib={"property": "og:description", "content": "Śpiewnik harcerski - teksty i chwyty piosenek"}, nsmap=None).tail = "\n"
etree.SubElement(head, "meta", attrib={"property": "og:locale", "content": "pl_PL"}, nsmap=None).tail = "\n"
etree.SubElement(head, "meta", attrib={"property": "og:site_name", "content": "Spiewaj.com"}, nsmap=None).tail = "\n"

# Add Twitter Card tags
etree.SubElement(head, "meta", attrib={"name": "twitter:card", "content": "summary"}, nsmap=None).tail = "\n"
etree.SubElement(head, "meta", attrib={"name": "twitter:title", "content": "Spiewaj.com - Śpiewnik harcerski online"}, nsmap=None).tail = "\n"
etree.SubElement(head, "meta", attrib={"name": "twitter:description", "content": "Śpiewnik harcerski - teksty i chwyty piosenek"}, nsmap=None).tail = "\n"

# Add JSON-LD structured data with BreadcrumbList and ItemList
script_ld = etree.SubElement(head, "script", attrib={"type": "application/ld+json"}, nsmap=None)
import json
ld_json = {
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Spiewaj.com",
"alternateName": "Śpiewnik",
"url": "https://spiewaj.com/",
"description": "Śpiewnik - teksty i chwyty piosenek popularnych, harcerskich, turystycznych",
"inLanguage": "pl-PL",
"potentialAction": {
"@type": "SearchAction",
"target": "https://spiewaj.com/#songSearch={search_term_string}",
"query-input": "required name=search_term_string"
}
}
script_ld.text = json.dumps(ld_json, ensure_ascii=False, indent=2)
script_ld.tail = "\n"

# List of songs
ul = tree.getroot().find(".//{http://www.w3.org/1999/xhtml}ul[@id='songs']")
# List of songs - try both HTML and XHTML namespaces
ul = tree.getroot().find(".//ul[@id='songs']")
if ul is None:
ul = tree.getroot().find(".//{http://www.w3.org/1999/xhtml}ul[@id='songs']")
for i in range(len(list_of_songs_meta)):
song = list_of_songs_meta[i]
if song.is_alias():
continue
song_html = os.path.join("./songs_html", song.base_file_name() + '.xhtml')
song_html = os.path.join("./songs_html", song.base_file_name() + '.html')
li = etree.SubElement(ul, "li")
# entire li is clickable and should navigate to the song
li.attrib['onclick'] = "location.href='"+song_html+"';"

# <button onclick='edit("zaciagnijcie_na_oknie_niebieska_zaslone.xhtml")'><span class="material-symbols-outlined">edit</span></button>

# Main anchor wrapping the title - SEO-friendly structure
a_main = etree.SubElement(li, "a")
a_main.attrib['class'] = 'song-link'
a_main.attrib['href'] = song_html

# Build descriptive title attribute for SEO
title_text = song.effectiveTitle()
if song.artist():
a_main.attrib['title'] = f"{title_text} - {song.artist()} - tekst i chwyty"
else:
a_main.attrib['title'] = f"{title_text} - tekst i chwyty"

# Edit button (outside main link for event handling)
button = etree.SubElement(li, "button")
button.attrib['class'] = 'editicon'
button.attrib['onclick'] = "edit('"+os.path.relpath(song.plik(), start=os.path.join(sb.repo_dir(), "songs"))+"');event.stopPropagation();"
button.attrib['aria-label'] = f"Edytuj {title_text}"
span = etree.SubElement(button, 'span')
span.attrib['class'] = 'material-symbols-outlined'
span.attrib['aria-hidden'] = 'true'
span.text = 'edit'

# A4 PDF link
a_a4 = etree.SubElement(li, "a")
a_a4.attrib['class'] = 'pdflink'
a_a4.attrib['href'] = f"./songs_pdf/{song.base_file_name()}.a4.pdf"
a_a4.attrib['target'] = '_blank'
a_a4.attrib['title'] = 'Download A4 PDF'
a_a4.attrib['rel'] = 'noopener'
a_a4.attrib['title'] = f"{title_text} - PDF A4"
a_a4.attrib['aria-label'] = f"Pobierz {title_text} PDF A4"
a_a4.attrib['onclick'] = 'event.stopPropagation();'
span_a4 = etree.SubElement(a_a4, 'span')
span_a4.attrib['class'] = 'material-symbols-outlined'
span_a4.attrib['aria-hidden'] = 'true'
span_a4.text = 'picture_as_pdf'
text_a4 = etree.SubElement(a_a4, 'span')
text_a4.attrib['class'] = 'pdf-label'
Expand All @@ -50,27 +130,40 @@ def create_index_xhtml(list_of_songs_meta, target_dir):
a_a5.attrib['class'] = 'pdflink'
a_a5.attrib['href'] = f"./songs_pdf/{song.base_file_name()}.a5.pdf"
a_a5.attrib['target'] = '_blank'
a_a5.attrib['title'] = 'Download A5 PDF'
a_a5.attrib['rel'] = 'noopener'
a_a5.attrib['title'] = f"{title_text} - PDF A5"
a_a5.attrib['aria-label'] = f"Pobierz {title_text} PDF A5"
a_a5.attrib['onclick'] = 'event.stopPropagation();'
span_a5 = etree.SubElement(a_a5, 'span')
span_a5.attrib['class'] = 'material-symbols-outlined'
span_a5.attrib['aria-hidden'] = 'true'
span_a5.text = 'picture_as_pdf'
text_a5 = etree.SubElement(a_a5, 'span')
text_a5.attrib['class'] = 'pdf-label'
text_a5.text = 'A5'
a = etree.SubElement(li, "a")
a.attrib['class'] = 'title'
a.attrib['href'] = song_html
a.text = list_of_songs_meta[i].effectiveTitle()

# Title span inside main link
title_span = etree.SubElement(a_main, "span")
title_span.attrib['class'] = 'title'
title_span.text = title_text

# Aliases inside main link
for song_alias in song.aliases():
alias = etree.SubElement(li, "span", attrib={"class": "alias"})
alias = etree.SubElement(a_main, "span", attrib={"class": "alias"})
alias.text = song_alias

# Artist inside main link
if song.artist():
artist = etree.SubElement(li, "span", attrib={"class": "artist"})
artist = etree.SubElement(a_main, "span", attrib={"class": "artist"})
artist.text = song.artist()

# Add onclick to li for backward compatibility with existing CSS/JS
li.attrib['onclick'] = "if(event.target.tagName.toLowerCase() !== 'a' && event.target.tagName.toLowerCase() !== 'button' && !event.target.closest('a') && !event.target.closest('button')) { location.href='"+song_html+"'; }"

# List of songbooks
ul = tree.getroot().find(".//{http://www.w3.org/1999/xhtml}ul[@id='songbooks']")
# List of songbooks - try both HTML and XHTML namespaces
ul = tree.getroot().find(".//ul[@id='songbooks']")
if ul is None:
ul = tree.getroot().find(".//{http://www.w3.org/1999/xhtml}ul[@id='songbooks']")
for songbook in sb.songbooks():
if not songbook.hidden():
li = etree.SubElement(ul, "li", attrib={"id": songbook.id()})
Expand All @@ -81,8 +174,13 @@ def create_index_xhtml(list_of_songs_meta, target_dir):
a_a5pdf.text = "PDF (a5)"
a_a4pdf = etree.SubElement(ul, "a", attrib={"class": "pdf", "href": os.path.join("songs_tex", songbook.id()+"_a4.pdf")})
a_a4pdf.text = "PDF (a4)"
# Update html lang attribute
html_root = tree.getroot()
if html_root.tag == 'html' or html_root.tag.endswith('}html'):
html_root.attrib['lang'] = 'pl-PL'

et = etree.ElementTree(tree.getroot())
et.write(out_path, pretty_print=True, method='xml', encoding='utf-8', xml_declaration=True)
et.write(out_path, pretty_print=True, method='html', encoding='utf-8')


def create_sitemap_xml(list_of_songs_meta, target_dir):
Expand All @@ -95,7 +193,7 @@ def create_sitemap_xml(list_of_songs_meta, target_dir):
# Add HTML version of the song
url = etree.SubElement(root, "url")
loc = etree.SubElement(url, "loc")
loc.text = os.path.join(base, "songs_html", song.base_file_name() + ".xhtml")
loc.text = os.path.join(base, "songs_html", song.base_file_name() + ".html")
lastmod = etree.SubElement(url, "lastmod")
lastmod.text = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+00:00")
changefreq = etree.SubElement(url, "changefreq")
Expand Down Expand Up @@ -137,7 +235,7 @@ def main():

logging.info(f"Generating HTML index in {target_dir} from songbook spec {songbook_file}, song count: {len(songbook.list_of_songs())}")

create_index_xhtml(songbook.list_of_songs(), target_dir)
create_index_html(songbook.list_of_songs(), target_dir)
create_sitemap_xml(songbook.list_of_songs(), target_dir)

index_js_path =os.path.join(target_dir, "index.js")
Expand Down
11 changes: 11 additions & 0 deletions src/html/kindle_html_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
from .standard_html_converter import StandardHtmlConverter

class KindleHtmlConverter(SongConverter):
def extension(self):
"""Return 'xhtml' for Kindle output"""
return 'xhtml'

def _add_chunk(self, chunk, parent, position):
"""class chunk -> html span chunk"""
span_chunk = etree.SubElement(parent, "span", attrib={"class": "chunk"})
Expand Down Expand Up @@ -158,6 +162,13 @@ def _add_blocks(self, song, parent):
span_content = etree.SubElement(div, "span", attrib={"class": "comment"})
span_content.text = song.comment

# Add hidden plain lyrics for Kindle indexing
# display: none text is still indexed for search in many Kindle versions
plain_lyrics = song.extract_plain_lyrics()
if plain_lyrics:
div_hidden = etree.SubElement(body_song, "div", attrib={"style": "display: none;"})
div_hidden.text = plain_lyrics


def xml2html(self, src_xml_path, path_out, song_suffix=None, song_prefix=None, song_head=None, substitions={}):
xhtml_namespace = "http://www.w3.org/1999/xhtml"
Expand Down
5 changes: 5 additions & 0 deletions src/html/song_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ class SongConverter(ABC):
def xml2html(self, src_xml_path, path_out, song_suffix=None, song_prefix=None, song_head=None, substitions={}):
"""Convert song from XML to HTML format"""
pass

@abstractmethod
def extension(self):
"""Return the file extension for this converter: 'html' or 'xhtml'"""
pass
6 changes: 3 additions & 3 deletions src/html/song_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ def create_all_songs_html_from_dir(converter, path_in, path_out, song_suffix=[])
return create_all_songs_html(converter, create_list_of_songs(path_in), path_out, song_suffix)

def create_all_songs_html(converter, list_of_songs, path_out, song_suffix=[], song_prefix=[], song_head=[], substitions={}):
if not os.path.exists(path_out):
os.mkdir(path_out)
# Ensure output directory exists (mkdir -p equivalent)
os.makedirs(path_out, exist_ok=True)

for song in list_of_songs:
if not song.is_alias():
relative_path = os.path.relpath(song.plik(), start=os.path.join(os.path.dirname(__file__), "../../songs"))
converter.xml2html(song.plik(),
os.path.join(path_out, song.base_file_name() + '.xhtml'),
os.path.join(path_out, song.base_file_name() + '.' + converter.extension()),
song_suffix=song_suffix,
song_prefix=song_prefix,
song_head=song_head,
Expand Down
Loading