{"record":{"id":"16b3fdbeb18019e9","repo":"siyuan-note/siyuan","slug":"video-poster-is-required","errorCode":null,"errorMessage":"video poster is required","messagePattern":"video poster is required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/boot_appearance.go","lineNumber":469,"sourceCode":"\t}\n\n\tlayerIDs := map[string]bool{}\n\tfor _, layer := range manifest.Layers {\n\t\tif layer == nil || !isValidBootAppearanceID(layer.ID) || layerIDs[layer.ID] {\n\t\t\terr = errors.New(\"invalid or duplicate layer ID\")\n\t\t\treturn nil, err\n\t\t}\n\t\tlayerIDs[layer.ID] = true\n\t\tif layer.Type != \"image\" && layer.Type != \"video\" {\n\t\t\terr = fmt.Errorf(\"unsupported layer type [%s]\", layer.Type)\n\t\t\treturn nil, err\n\t\t}\n\t\tif _, _, err = validateBootAppearanceResource(pluginDir, appearanceDir, layer.Src, layer.Type); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid layer source: %w\", err)\n\t\t}\n\t\tif layer.Type == \"video\" {\n\t\t\tif layer.Poster == \"\" {\n\t\t\t\terr = errors.New(\"video poster is required\")\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif _, _, err = validateBootAppearanceResource(pluginDir, appearanceDir, layer.Poster, \"image\"); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid video poster: %w\", err)\n\t\t\t}\n\t\t} else if layer.Poster != \"\" {\n\t\t\terr = errors.New(\"image layer cannot declare a poster\")\n\t\t\treturn nil, err\n\t\t}\n\t\tfit := layer.Fit\n\t\tif fit == \"\" {\n\t\t\tfit = \"cover\"\n\t\t}\n\t\tif !isValidBootAppearanceFit(fit) {\n\t\t\terr = fmt.Errorf(\"invalid layer fit [%s]\", fit)\n\t\t\treturn nil, err\n\t\t}\n\t\tposition := layer.Position","sourceCodeStart":451,"sourceCodeEnd":487,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/boot_appearance.go#L451-L487","documentation":"Video layers must declare a poster image: if layer.Type is \"video\" and layer.Poster is empty, loadBootAppearance aborts. The poster is shown before the video loads/plays on the boot screen, so it is mandatory for video layers.","triggerScenarios":"GetBootAppearances/getBootAppearanceByID loading a manifest.json where a layer with \"type\": \"video\" omits the poster field or sets it to \"\".","commonSituations":"Author adds a video layer copied from an image-layer template that had no poster field; poster considered optional; a generator that only emits src for video layers.","solutions":["Add a poster field pointing to a PNG/JPEG/WebP image in the appearance directory for every video layer","Generate a poster by exporting a frame of the video (e.g. first frame) as poster.png","Re-validate the manifest so the video layer reads like {\"type\":\"video\",\"src\":\"x.mp4\",\"poster\":\"poster.png\"}"],"exampleFix":"// before\n{\"id\": \"anim\", \"type\": \"video\", \"src\": \"anim.mp4\"}\n// after\n{\"id\": \"anim\", \"type\": \"video\", \"src\": \"anim.mp4\", \"poster\": \"anim-poster.png\"}","handlingStrategy":"validation","validationCode":"manifest.layers?.forEach(l => {\n  if (l.type === \"video\" && (!l.poster || typeof l.poster !== \"string\")) {\n    throw new Error(`video layer ${l.id} requires a poster`);\n  }\n});","typeGuard":"const hasPoster = (l: { type: string; poster?: string }): boolean =>\n  l.type !== \"video\" || (typeof l.poster === \"string\" && l.poster.length > 0);","tryCatchPattern":null,"preventionTips":["Always export a poster frame when adding a video layer","Template your video-layer JSON with poster included","Lint manifests so video layers without poster fail the build","Keep the poster next to the video in the same directory"],"tags":["validation","video","plugin","manifest"],"backgroundTag":"missing-required-config-field","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}